Compare commits
287
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c16b9d14d | ||
|
|
3ba11b5508 | ||
|
|
65f74987f8 | ||
|
|
aaed8fe971 | ||
|
|
cba305ab9b | ||
|
|
913256cac4 | ||
|
|
75d14782f4 | ||
|
|
c30de7f40d | ||
|
|
ca96e55c07 | ||
|
|
64698fb97e | ||
|
|
662a461cfa | ||
|
|
2a15138014 | ||
|
|
97727908fa | ||
|
|
16b204f83b | ||
|
|
16ebb8eb3a | ||
|
|
f8c902a138 | ||
|
|
01c01dedbb | ||
|
|
007a39ebfa | ||
|
|
bec3132417 | ||
|
|
8c2bdc65eb | ||
|
|
e7a46ac3a2 | ||
|
|
0f9d7a115f | ||
|
|
05802896a8 | ||
|
|
a44e8722c6 | ||
|
|
690e671a25 | ||
|
|
46b98b99e2 | ||
|
|
9e339cd451 | ||
|
|
95085cbdeb | ||
|
|
c1b0f32609 | ||
|
|
f9b328278a | ||
|
|
8a5b9ba2c3 | ||
|
|
763e0ff5d8 | ||
|
|
93e8ec1df3 | ||
|
|
f087e55c3c | ||
|
|
53b3cc9817 | ||
|
|
b2bbdb8ad4 | ||
|
|
417a24bb16 | ||
|
|
72c1c01e47 | ||
|
|
aea6924015 | ||
|
|
6fb39d6384 | ||
|
|
4be7354b36 | ||
|
|
ab23e4ba2c | ||
|
|
e688a703ca | ||
|
|
49961db9a9 | ||
|
|
56f8d3700a | ||
|
|
c68e1d017e | ||
|
|
a09e7fc709 | ||
|
|
024f25f003 | ||
|
|
9d101666ab | ||
|
|
f5ae14de9e | ||
|
|
4917084bbf | ||
|
|
5c7a8df55f | ||
|
|
8656f6fb10 | ||
|
|
4f4d823c2a | ||
|
|
1d77247bcd | ||
|
|
fe6293eb28 | ||
|
|
0ad7d6a2c4 | ||
|
|
662fb390bc | ||
|
|
c037be3456 | ||
|
|
4b54b798a3 | ||
|
|
63cfc16ba5 | ||
|
|
b28edb5a83 | ||
|
|
719052d777 | ||
|
|
22d71c30f9 | ||
|
|
562a562d15 | ||
|
|
f94cb832a0 | ||
|
|
8fbecc41a5 | ||
|
|
87be84f620 | ||
|
|
7661823c8b | ||
|
|
2aba7bb16e | ||
|
|
735304cee0 | ||
|
|
1ca2900a63 | ||
|
|
53c966fc11 | ||
|
|
9da2e0054c | ||
|
|
03efe1dfb5 | ||
|
|
4ee840fa82 | ||
|
|
64a8ebbdbb | ||
|
|
e8da4ac531 | ||
|
|
bf77775cd6 | ||
|
|
711adc2e17 | ||
|
|
9a62ef6276 | ||
|
|
552ae18feb | ||
|
|
bd999eb0f3 | ||
|
|
bd0c22c129 | ||
|
|
4920b4c1a2 | ||
|
|
83cb4ec7fa | ||
|
|
ed4279d321 | ||
|
|
c43b5521ae | ||
|
|
d389390b0c | ||
|
|
60ae8c9e72 | ||
|
|
b543ff6334 | ||
|
|
f744957447 | ||
|
|
e3de7ec112 | ||
|
|
61ed66d4f0 | ||
|
|
393e174a6d | ||
|
|
c4f2af0bfe | ||
|
|
294da35347 | ||
|
|
44103a8dc4 | ||
|
|
ea36df1c29 | ||
|
|
814dac672c | ||
|
|
88838dda0b | ||
|
|
5ce1c571ed | ||
|
|
f2881f3a3a | ||
|
|
78eb2d8fcd | ||
|
|
89ca295b69 | ||
|
|
9a3378d07b | ||
|
|
c0eb6b94cc | ||
|
|
12f81754a0 | ||
|
|
190adb6bc1 | ||
|
|
d7233992fe | ||
|
|
c29d191a5e | ||
|
|
b980321557 | ||
|
|
3a37192782 | ||
|
|
f3ca55d183 | ||
|
|
6955164f71 | ||
|
|
d20bfb1bf1 | ||
|
|
fa8db521d5 | ||
|
|
59bccfe1d6 | ||
|
|
506eb78a80 | ||
|
|
ce0ba4ff40 | ||
|
|
42795e517b | ||
|
|
590adb5b4e | ||
|
|
c204d90d14 | ||
|
|
e9bfaa865a | ||
|
|
e0bc5869d6 | ||
|
|
84ff24c5b5 | ||
|
|
b8da4d7d42 | ||
|
|
e1b48ab7c8 | ||
|
|
70f250f7bb | ||
|
|
4c3be448f0 | ||
|
|
8ede32f4c4 | ||
|
|
d465719c6f | ||
|
|
bc3049c255 | ||
|
|
8624b36e89 | ||
|
|
e4b1c78102 | ||
|
|
be47ac0edb | ||
|
|
6fcc97caf1 | ||
|
|
6bdef28532 | ||
|
|
314e6ed9ad | ||
|
|
99ffa1d6d4 | ||
|
|
d23c9ca4b1 | ||
|
|
cc67d707d9 | ||
|
|
583559ec60 | ||
|
|
e7195f7db0 | ||
|
|
04bb3657bb | ||
|
|
f1563aa1fc | ||
|
|
0795299b48 | ||
|
|
3380addd78 | ||
|
|
2862fdc9fd | ||
|
|
c9e413a3d3 | ||
|
|
6413bc6b18 | ||
|
|
fbb47c2593 | ||
|
|
a446739432 | ||
|
|
ec7655a71d | ||
|
|
c8d2c4e17e | ||
|
|
b513fddbd5 | ||
|
|
c0c7bdfa5b | ||
|
|
55b8e91298 | ||
|
|
ea125605d1 | ||
|
|
8353800dac | ||
|
|
cd08b5dad8 | ||
|
|
3a73c8bf1c | ||
|
|
4389a6ddb7 | ||
|
|
8a2ed7df58 | ||
|
|
00d6ff90c6 | ||
|
|
9bfd32b5c2 | ||
|
|
f57dcc9788 | ||
|
|
159fad40ef | ||
|
|
4aa4089ddd | ||
|
|
f6cb75ff1c | ||
|
|
61e4643503 | ||
|
|
6492ad5500 | ||
|
|
8ed0a86cf5 | ||
|
|
52170fc12f | ||
|
|
4c8ecb05e0 | ||
|
|
9e95ad3fad | ||
|
|
523925459c | ||
|
|
a11f760162 | ||
|
|
e35b73d067 | ||
|
|
2620e40872 | ||
|
|
79bc10cdce | ||
|
|
5a43b33a1d | ||
|
|
c9f1d05126 | ||
|
|
c5f4f6ddd0 | ||
|
|
f332d64aa6 | ||
|
|
bb4d7b9d56 | ||
|
|
f6cefd6230 | ||
|
|
c6b45499c4 | ||
|
|
30bbf51e77 | ||
|
|
bb6dbfa617 | ||
|
|
1778c07ef9 | ||
|
|
075e3711a9 | ||
|
|
32b16055fb | ||
|
|
1daa925019 | ||
|
|
f58f9c3fe3 | ||
|
|
af98524806 | ||
|
|
ecf430dd33 | ||
|
|
892e329207 | ||
|
|
543fbb70cc | ||
|
|
3a191e8d06 | ||
|
|
402b9a5539 | ||
|
|
ef80915519 | ||
|
|
2e83898777 | ||
|
|
3d988b4644 | ||
|
|
bcdefaa828 | ||
|
|
0d48a0b579 | ||
|
|
c59c17f9f0 | ||
|
|
d5c967e9ad | ||
|
|
55793d8463 | ||
|
|
fc3ef2c59e | ||
|
|
accc3e0ef1 | ||
|
|
ba657e8311 | ||
|
|
53fef3bdbf | ||
|
|
21afb058cf | ||
|
|
5232703dd9 | ||
|
|
406ce4056a | ||
|
|
21e0ea086e | ||
|
|
f50a9aaf3a | ||
|
|
60e73212e8 | ||
|
|
6660c85150 | ||
|
|
2cfd57fdc3 | ||
|
|
5e5411a34f | ||
|
|
01852c1528 | ||
|
|
4488bd23c7 | ||
|
|
c2696c6eeb | ||
|
|
4ef86a3e6a | ||
|
|
badea83e9e | ||
|
|
fc2f119584 | ||
|
|
23a92ec38c | ||
|
|
fe2f3faee4 | ||
|
|
3e5a14fbc8 | ||
|
|
527b4293c4 | ||
|
|
4e2fe7aaa1 | ||
|
|
0a109111cb | ||
|
|
3b6e8ceedb | ||
|
|
99229a8453 | ||
|
|
383e5f8382 | ||
|
|
a11570b563 | ||
|
|
af1feb3920 | ||
|
|
a6df8d261a | ||
|
|
f96942beee | ||
|
|
7994698349 | ||
|
|
ab495c6572 | ||
|
|
804688b3c1 | ||
|
|
47c986d517 | ||
|
|
8435096e5a | ||
|
|
e30a7d184b | ||
|
|
b4e3d922b1 | ||
|
|
a028b6ae8b | ||
|
|
9565931042 | ||
|
|
1042391702 | ||
|
|
7c2ff7c037 | ||
|
|
ad0803f405 | ||
|
|
03675aaa60 | ||
|
|
c0c19c0425 | ||
|
|
0bbb27e3d3 | ||
|
|
214d283366 | ||
|
|
7d95ddf6a5 | ||
|
|
a19c5c269a | ||
|
|
d641866cc4 | ||
|
|
6102e0af6b | ||
|
|
225bae08d3 | ||
|
|
d8b5abb12f | ||
|
|
ba6d7f4177 | ||
|
|
d9a3da96bb | ||
|
|
87d1f33c05 | ||
|
|
56e9218646 | ||
|
|
2d5534b3a6 | ||
|
|
d68c6c151c | ||
|
|
dfb910f270 | ||
|
|
ddf3a5cfa0 | ||
|
|
5976b35c90 | ||
|
|
6b204fce15 | ||
|
|
f731d0f9f0 | ||
|
|
e231d0c4be | ||
|
|
e4328a2ed4 | ||
|
|
069e489d03 | ||
|
|
990dffc11c | ||
|
|
b4140d6457 | ||
|
|
91af5fd27c | ||
|
|
0bb16f9795 | ||
|
|
be7efb2ad0 | ||
|
|
fbd9575df7 | ||
|
|
6d967497ba | ||
|
|
7816b4e450 | ||
|
|
240c2f847c | ||
|
|
e39a102c14 |
Generated
-8
@@ -1,8 +0,0 @@
|
|||||||
# 디폴트 무시된 파일
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
||||||
# 에디터 기반 HTTP 클라이언트 요청
|
|
||||||
/httpRequests/
|
|
||||||
# Datasource local storage ignored files
|
|
||||||
/dataSources/
|
|
||||||
/dataSources.local.xml
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
import { LinkPage } from "@/pages/link";
|
||||||
|
export default LinkPage;
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { ModifyLinkPage as VModifyLinkPage } from "@/pages/link";
|
||||||
|
|
||||||
|
export default async function ModifyLinkPage({
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
params: Promise<{ id: string }>;
|
||||||
|
}) {
|
||||||
|
const { id } = await params;
|
||||||
|
return <VModifyLinkPage linkSn={id} />;
|
||||||
|
};
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
import { RegistLinkPage } from "@/pages/link";
|
||||||
|
export default RegistLinkPage;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
import { LinkLogPage } from "@/pages/linkLog";
|
||||||
|
export default LinkLogPage;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
import { LinkParamsPage } from "@/pages/linkParams";
|
||||||
|
export default LinkParamsPage;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
import { LinkStatsPage } from "@/pages/linkStats";
|
||||||
|
export default LinkStatsPage;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
import { LinkTablePage } from "@/pages/linkTable";
|
||||||
|
export default LinkTablePage;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
import { UserStatisticsPage } from "@/pages/user-statistics";
|
||||||
|
export default UserStatisticsPage;
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import {
|
||||||
|
type LinkService,
|
||||||
|
type LinkSearchParam,
|
||||||
|
type LinkList,
|
||||||
|
type LinkFormData,
|
||||||
|
type LinkInfo,
|
||||||
|
type LinkLogList,
|
||||||
|
type LinkStatsList,
|
||||||
|
type LinkVrblList,
|
||||||
|
type LinkVrblFormData,
|
||||||
|
type LinkVrbl,
|
||||||
|
type LinkTable,
|
||||||
|
type LinkTableColumnsList,
|
||||||
|
type LinkTableColumns,
|
||||||
|
type LinkTableColData,
|
||||||
|
type LinkTableFormData,
|
||||||
|
} from "../model/link-interface";
|
||||||
|
|
||||||
|
export class LinkApiService implements LinkService {
|
||||||
|
public async getLinkList(params: LinkSearchParam): Promise<LinkList> {
|
||||||
|
const response = await axios.get<LinkList>(
|
||||||
|
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkList`,
|
||||||
|
{ params }
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getLinkInfo(linkSn: string): Promise<LinkInfo> {
|
||||||
|
const response = await axios.get<LinkInfo>(
|
||||||
|
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkInfo/${linkSn}`,
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async linkQuickStart(linkSn: string): Promise<string> {
|
||||||
|
const response = await axios.patch<string>(
|
||||||
|
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/linkQuickStart/${linkSn}`,
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateLinkInfo(data: LinkFormData): Promise<void> {
|
||||||
|
await axios.put<void>(
|
||||||
|
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/updateLinkInfo`,
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async insertLinkInfo(data: LinkFormData): Promise<void> {
|
||||||
|
await axios.post<void>(
|
||||||
|
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/insertLinkInfo`,
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async deleteLink(linkSn: string): Promise<void> {
|
||||||
|
await axios.delete<void>(
|
||||||
|
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/deleteLink/${linkSn}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getLinkLogList(params: LinkSearchParam): Promise<LinkLogList> {
|
||||||
|
const response = await axios.get<LinkLogList>(
|
||||||
|
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/selectLinkLoglist`,
|
||||||
|
{ params }
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getLinkStatsList(params: LinkSearchParam): Promise<LinkStatsList> {
|
||||||
|
const response = await axios.get<LinkStatsList>(
|
||||||
|
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/selectLinkStats`,
|
||||||
|
{ params }
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getLinkVrblList(linkSn: string): Promise<LinkVrblList> {
|
||||||
|
const response = await axios.get<LinkVrblList>(
|
||||||
|
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkVrblList/${linkSn}`,
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async insertLinkVrbl(data: LinkVrblFormData): Promise<number> {
|
||||||
|
const response = await axios.post<number>(
|
||||||
|
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/insertLinkVrbl`,
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async deleteLinkVrbl(data: string[]): Promise<void> {
|
||||||
|
await axios.delete<void>(
|
||||||
|
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/deleteLinkVrbl`,
|
||||||
|
{
|
||||||
|
data
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getLinkVrblInfo(linkVrblSn: string): Promise<LinkVrbl> {
|
||||||
|
const response = await axios.get<LinkVrbl>(
|
||||||
|
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkVrblInfo/${linkVrblSn}`,
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateLinkVrblInfo(data: LinkVrblFormData): Promise<void> {
|
||||||
|
await axios.put<void>(
|
||||||
|
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/updateLinkVrblInfo`,
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getLinkTableInfo(linkSn: string): Promise<LinkTable> {
|
||||||
|
const response = await axios.get<LinkTable>(
|
||||||
|
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableInfo/${linkSn}`,
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getLinkTableColList(linkSn: string): Promise<LinkTableColumnsList> {
|
||||||
|
const response = await axios.get<LinkTableColumnsList>(
|
||||||
|
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableColList/${linkSn}`,
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getLinkTableColInfo(params: { colSn: string; tblSn:string; }): Promise<LinkTableColumns> {
|
||||||
|
const response = await axios.get<LinkTableColumns>(
|
||||||
|
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableColInfo`,
|
||||||
|
{ params }
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async insertLinkTableCol(data: LinkTableColData): Promise<void> {
|
||||||
|
await axios.post<void>(
|
||||||
|
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/insertLinkTableCol`,
|
||||||
|
data
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async deleteLinkTableCol(data: string[]): Promise<void> {
|
||||||
|
await axios.delete<void>(
|
||||||
|
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/deleteLinkTableCol`,
|
||||||
|
{
|
||||||
|
data
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateLinkTableCol(data: LinkTableColData): Promise<void> {
|
||||||
|
axios.put<void>(
|
||||||
|
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/updateLinkTableCol`,
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async insertLinkTableInfo(data: LinkTableFormData): Promise<void> {
|
||||||
|
await axios.post<void>(
|
||||||
|
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/insertLinkTableInfo`,
|
||||||
|
data
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateLinkTableInfo(data: LinkTableFormData): Promise<void> {
|
||||||
|
await axios.put<void>(
|
||||||
|
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/updateLinkTableInfo`,
|
||||||
|
data
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async countLinkTblNm(tblNm: string): Promise<{count: number, tbl_sn: string}> {
|
||||||
|
const response = await axios.get<{count: number, tbl_sn: string}>(
|
||||||
|
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/countLinkTblNm/${tblNm}`,
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateLinkResultCode(linkSn: string): Promise<void> {
|
||||||
|
await axios.patch<void>(
|
||||||
|
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/updateLinkResultCode/${linkSn}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LinkApi = new LinkApiService();
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
export { LinkApi } from "./api/link-service";
|
||||||
|
export {
|
||||||
|
type LinkSearchParam,
|
||||||
|
type Link,
|
||||||
|
type LinkFormData,
|
||||||
|
type LinkLog,
|
||||||
|
type LinkStats,
|
||||||
|
type LinkVrbl,
|
||||||
|
type LinkVrblFormData,
|
||||||
|
type LinkTable,
|
||||||
|
type LinkTableColumns,
|
||||||
|
type LinkTableColData,
|
||||||
|
type LinkTableFormData,
|
||||||
|
linkFormSchema,
|
||||||
|
linkVrblFormSchema,
|
||||||
|
LinkTableColumnsSchema,
|
||||||
|
LinkTableSchema,
|
||||||
|
} from "./model/link-interface"
|
||||||
@@ -0,0 +1,235 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
import type { PageableParams, PageableList } from "@/shared/model";
|
||||||
|
|
||||||
|
export interface Link {
|
||||||
|
link_sn: string;
|
||||||
|
link_nm: string;
|
||||||
|
link_clsf_cd: string;
|
||||||
|
link_url: string;
|
||||||
|
link_srvr_ip: string;
|
||||||
|
link_db_cd: string;
|
||||||
|
link_key: string;
|
||||||
|
link_id: string;
|
||||||
|
link_pswd: string;
|
||||||
|
link_cycle_stng: string;
|
||||||
|
srvc_usg: string;
|
||||||
|
last_link_rslt_cd: string;
|
||||||
|
last_link_excn_dt: string;
|
||||||
|
next_link_excn_dt: string;
|
||||||
|
last_link_err_cd: string;
|
||||||
|
mdfcn_dt: string;
|
||||||
|
mdfr: string;
|
||||||
|
link_batch_time: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LinkLog {
|
||||||
|
rownum: number;
|
||||||
|
link_sn: string;
|
||||||
|
link_nm: string;
|
||||||
|
link_rslt_cd: string;
|
||||||
|
err_cn: string;
|
||||||
|
link_err_cd: string;
|
||||||
|
log_crt_dt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LinkInfo extends LinkFormData {
|
||||||
|
link_sn: string;
|
||||||
|
link_nm: string;
|
||||||
|
link_clsf_cd: string;
|
||||||
|
link_url: string;
|
||||||
|
link_srvr_ip: string;
|
||||||
|
link_db_cd: string;
|
||||||
|
link_key: string;
|
||||||
|
link_id: string;
|
||||||
|
link_pswd: string;
|
||||||
|
srvc_usg: string;
|
||||||
|
last_link_rslt_cd: string;
|
||||||
|
last_link_excn_dt: string;
|
||||||
|
next_link_excn_dt: string;
|
||||||
|
last_link_err_cd: string;
|
||||||
|
mdfcn_dt: string;
|
||||||
|
mdfr: string;
|
||||||
|
link_batch_time: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LinkStats {
|
||||||
|
link_sn: string;
|
||||||
|
link_nm: string;
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LinkVrbl {
|
||||||
|
link_vrbl_sn: string;
|
||||||
|
link_vrbl_nm: string;
|
||||||
|
link_vrbl_vl: string;
|
||||||
|
link_vrbl_cd: string;
|
||||||
|
link_vrbl_expln: string;
|
||||||
|
mdfcn_dt: string;
|
||||||
|
mdfr: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LinkTable {
|
||||||
|
link_nm: string;
|
||||||
|
link_sn: string;
|
||||||
|
tbl_sn: string;
|
||||||
|
sou_tbl_nm: string;
|
||||||
|
sou_tbl_korn_nm: string;
|
||||||
|
sou_tbl_expln: string;
|
||||||
|
trsf_tbl_nm: string;
|
||||||
|
trsf_tbl_korn_nm: string;
|
||||||
|
trsf_tbl_expln: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LinkTableColumns {
|
||||||
|
tbl_sn: string;
|
||||||
|
col_sn: string;
|
||||||
|
sou_col_nm: string;
|
||||||
|
sou_col_korn_nm: string;
|
||||||
|
sou_col_data_frm: string;
|
||||||
|
sou_col_sz: number;
|
||||||
|
trsf_col_nm: string;
|
||||||
|
trsf_col_korn_nm: string;
|
||||||
|
trsf_col_data_frm: string;
|
||||||
|
trsf_col_sz: number
|
||||||
|
pk_yn: string;
|
||||||
|
col_seq: number;
|
||||||
|
mdfcn_dt: string;
|
||||||
|
mdfr: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const linkFormSchema = z.object({
|
||||||
|
link_clsf_cd: z.string().max(8).nullable().refine((val) => {
|
||||||
|
// 빈 값이나 "none" 값이면 false 반환
|
||||||
|
return val && val !== "none";
|
||||||
|
}, {
|
||||||
|
message: "연계 분류를 선택해주세요"
|
||||||
|
}),
|
||||||
|
link_sn: z.string().max(50).nullable().optional(),
|
||||||
|
link_nm: z.string().max(40, { message: '연계명은 40자 이하로 입력해 주세요.' }).nonempty({ message: '연계명은 필수입니다.' }),
|
||||||
|
link_url: z.string().max(1000, { message: '연계 URL은 1000자 이하로 입력해 주세요.' }).nonempty({ message: '연계 URL은 필수입니다.' }),
|
||||||
|
link_srvr_ip: z.string().max(20).nullable().optional(),
|
||||||
|
link_db_cd: z.string().max(10).nullable().optional(),
|
||||||
|
link_key: z.string().max(100, { message: '연계 KEY는 100자 이하로 입력해 주세요.' }).nonempty({ message: '연계 KEY는 필수입니다.' }),
|
||||||
|
link_id: z.string().max(20).nullable().optional(),
|
||||||
|
link_pswd: z.string().max(128).nullable().optional(),
|
||||||
|
srvc_usg: z.string().max(40).nullable().optional(),
|
||||||
|
last_link_rslt_cd: z.string().max(4).nullable().optional(),
|
||||||
|
last_link_excn_dt: z.string().max(40).nullable().optional(),
|
||||||
|
next_link_excn_dt: z.string().max(40).nullable().optional(),
|
||||||
|
last_link_err_cd: z.string().max(10).nullable().optional(),
|
||||||
|
mdfcn_dt: z.string().nullable().optional(),
|
||||||
|
mdfr: z.string().nullable().optional(),
|
||||||
|
link_batch_time: z.coerce.number().nullable().optional().refine((val) => {
|
||||||
|
if (val == null) return true;
|
||||||
|
return val >= 5;
|
||||||
|
}, {
|
||||||
|
message: "연계 주기는 5분 이상이어야 합니다."
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const linkVrblFormSchema = z.object({
|
||||||
|
link_sn: z.string().max(50).nullable().optional(),
|
||||||
|
link_vrbl_sn: z.string().max(50).nullable().optional(),
|
||||||
|
link_vrbl_nm: z.string().max(40, { message: '변수명은 40자 이하로 입력해 주세요.' }).nonempty({ message: '변수명은 필수입니다.' }),
|
||||||
|
link_vrbl_expln: z.string().max(4000, { message: '설명은 400자 이하로 입력해 주세요.' }).nullable().optional(),
|
||||||
|
link_vrbl_cd: z.string().max(10, { message: '변수코드는 10자 이하로 입력해 주세요.' }).nullable().optional(),
|
||||||
|
link_vrbl_vl: z.string().max(1000, { message: '변수값은 1000자 이하로 입력해 주세요.' }).nonempty({ message: '변수값은 필수입니다.' }),
|
||||||
|
mdfcn_dt: z.string().nullable().optional(),
|
||||||
|
mdfr: z.string().nullable().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const LinkTableColumnsSchema = z.object({
|
||||||
|
link_sn: z.string().max(50).nullable().optional(),
|
||||||
|
tbl_sn: z.string().max(50).nullable().optional(),
|
||||||
|
col_sn: z.string().max(50).nullable().optional(),
|
||||||
|
sou_col_nm: z.string().max(40, { message: '40자 이하로 입력해 주세요.' }).nullable().optional(),
|
||||||
|
sou_col_korn_nm: z.string().max(40, { message: '40자 이하로 입력해 주세요.' }).nullable().optional(),
|
||||||
|
sou_col_data_frm: z.string().max(40, { message: '40자 이하로 입력해 주세요.' }).nullable().optional(),
|
||||||
|
sou_col_sz: z.coerce.number().nullable().optional().refine((val) => {
|
||||||
|
if (val == null) return true;
|
||||||
|
return val.toString().replace('.', '').length <= 14;
|
||||||
|
}, {
|
||||||
|
message: '최대 14자리까지 입력 가능합니다.',
|
||||||
|
}),
|
||||||
|
pk_yn: z.string().max(1).nonempty(),
|
||||||
|
trsf_col_nm: z.string().max(40, { message: '40자 이하로 입력해 주세요.' }).nonempty({ message: '변환컬럼명은 필수입니다.' }),
|
||||||
|
trsf_col_korn_nm: z.string().max(40, { message: '40자 이하로 입력해 주세요.' }).nonempty({ message: '변환컬럼 한글명은 필수입니다.' }),
|
||||||
|
trsf_col_data_frm: z.string().max(40, { message: '40자 이하로 입력해 주세요.' }).nonempty({ message: '변환컬럼 데이터 형식은 필수입니다.' }),
|
||||||
|
trsf_col_sz: z.coerce.number().nullable().optional().refine((val) => {
|
||||||
|
if (val == null) return true;
|
||||||
|
return val.toString().replace('.', '').length <= 14;
|
||||||
|
}, {
|
||||||
|
message: '최대 14자리까지 입력 가능합니다.',
|
||||||
|
}),
|
||||||
|
col_seq: z.coerce.number().nullable().optional().refine((val) => {
|
||||||
|
if (val == null) return true;
|
||||||
|
return val > 0;
|
||||||
|
}, {
|
||||||
|
message: "컬럼 매칭 순서는 0 이상이어야 합니다."
|
||||||
|
}),
|
||||||
|
mdfcn_dt: z.string().nullable().optional(),
|
||||||
|
mdfr: z.string().nullable().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const LinkTableSchema = z.object({
|
||||||
|
link_sn: z.string().max(50).nullable().optional(),
|
||||||
|
tbl_sn: z.string().max(50).nullable().optional(),
|
||||||
|
link_nm: z.string().max(50).nullable().optional(),
|
||||||
|
sou_tbl_nm: z.string().max(40, { message: '40자 이하로 입력해 주세요.' }).nullable().optional(),
|
||||||
|
sou_tbl_korn_nm: z.string().max(40, { message: '40자 이하로 입력해 주세요.' }).nullable().optional(),
|
||||||
|
sou_tbl_expln: z.string().max(2000, { message: '설명은 2000자 이하로 입력해 주세요.' }).nullable().optional(),
|
||||||
|
trsf_tbl_nm: z.string().max(40, { message: '40자 이하로 입력해 주세요.' }).nonempty({ message: '변환테이블명은 필수입니다.' }),
|
||||||
|
trsf_tbl_korn_nm: z.string().max(40, { message: '40자 이하로 입력해 주세요.' }).nonempty({ message: '변환테이블 한글명은 필수입니다.' }),
|
||||||
|
trsf_tbl_expln: z.string().max(2000, { message: '설명은 2000자 이하로 입력해 주세요.' }).nullable().optional(),
|
||||||
|
mdfcn_dt: z.string().nullable().optional(),
|
||||||
|
mdfr: z.string().nullable().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
interface LinkStatsListObject {
|
||||||
|
linkSuccStats: LinkStats[] | null;
|
||||||
|
linkFailStats: LinkStats[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LinkSearchParam extends PageableParams {
|
||||||
|
linkSn?: string;
|
||||||
|
linkNm?: string;
|
||||||
|
startDate?: string;
|
||||||
|
endDate?: string;
|
||||||
|
isSucc?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LinkFormData = z.infer<typeof linkFormSchema>;
|
||||||
|
export type LinkVrblFormData = z.infer<typeof linkVrblFormSchema>;
|
||||||
|
export type LinkTableColData = z.infer<typeof LinkTableColumnsSchema>;
|
||||||
|
export type LinkTableFormData = z.infer<typeof LinkTableSchema>;
|
||||||
|
export type LinkList = PageableList<Link>;
|
||||||
|
export type LinkLogList = PageableList<LinkLog>;
|
||||||
|
export type LinkStatsList = LinkStatsListObject;
|
||||||
|
export type LinkVrblList = PageableList<LinkVrbl>;
|
||||||
|
export type LinkTableColumnsList = PageableList<LinkTableColumns>;
|
||||||
|
|
||||||
|
export interface LinkService {
|
||||||
|
getLinkList(params: LinkSearchParam): Promise<LinkList>;
|
||||||
|
getLinkInfo(params: string): Promise<LinkInfo>;
|
||||||
|
linkQuickStart(params: string): Promise<string>;
|
||||||
|
updateLinkInfo(data: LinkFormData): Promise<void>;
|
||||||
|
deleteLink(params: string): Promise<void>;
|
||||||
|
getLinkLogList(params: LinkSearchParam): Promise<LinkLogList>;
|
||||||
|
getLinkStatsList(params: LinkSearchParam): Promise<LinkStatsList>;
|
||||||
|
getLinkVrblList(params: string): Promise<LinkVrblList>;
|
||||||
|
insertLinkVrbl(data: LinkVrblFormData): Promise<number>;
|
||||||
|
deleteLinkVrbl(data: string[]): Promise<void>;
|
||||||
|
getLinkVrblInfo(params: string): Promise<LinkVrbl>;
|
||||||
|
updateLinkVrblInfo(data: LinkVrblFormData): Promise<void>;
|
||||||
|
getLinkTableInfo(params: string): Promise<LinkTable>;
|
||||||
|
getLinkTableColList(params: string): Promise<LinkTableColumnsList>;
|
||||||
|
getLinkTableColInfo(params: { colSn: string; tblSn: string; }): Promise<LinkTableColumns>;
|
||||||
|
insertLinkTableCol(data: LinkTableColData): Promise<void>;
|
||||||
|
deleteLinkTableCol(data: string[]): Promise<void>;
|
||||||
|
updateLinkTableCol(data: LinkTableColData): Promise<void>;
|
||||||
|
insertLinkTableInfo(data: LinkTableFormData): Promise<void>;
|
||||||
|
updateLinkTableInfo(data: LinkTableFormData): Promise<void>;
|
||||||
|
insertLinkInfo(data: LinkFormData): Promise<void>;
|
||||||
|
countLinkTblNm(tblNm: string): Promise<{ count: number, tbl_sn: string }>;
|
||||||
|
updateLinkResultCode(linkSn: string): Promise<void>;
|
||||||
|
}
|
||||||
@@ -47,7 +47,7 @@ function AddAdminDialog() {
|
|||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [userSearchQuery, setUserSearchQuery] = useState<UserSearchParams>({
|
const [userSearchQuery, setUserSearchQuery] = useState<UserSearchParams>({
|
||||||
keywordType: "",
|
keywordType: "userName",
|
||||||
keyword: "",
|
keyword: "",
|
||||||
});
|
});
|
||||||
const [selectedGroupId, setSelectedGroupId] = useState("");
|
const [selectedGroupId, setSelectedGroupId] = useState("");
|
||||||
@@ -193,6 +193,7 @@ function AddAdminDialog() {
|
|||||||
|
|
||||||
<form onSubmit={handleSearch} className="flex items-center gap-2">
|
<form onSubmit={handleSearch} className="flex items-center gap-2">
|
||||||
<Select
|
<Select
|
||||||
|
value={userSearchQuery.keywordType}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
setUserSearchQuery({ ...userSearchQuery, keywordType: value })
|
setUserSearchQuery({ ...userSearchQuery, keywordType: value })
|
||||||
}
|
}
|
||||||
@@ -201,8 +202,8 @@ function AddAdminDialog() {
|
|||||||
<SelectValue placeholder="선택" />
|
<SelectValue placeholder="선택" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="userId">아이디</SelectItem>
|
|
||||||
<SelectItem value="userName">이름</SelectItem>
|
<SelectItem value="userName">이름</SelectItem>
|
||||||
|
<SelectItem value="userId">아이디</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
|||||||
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
DateRangePicker,
|
|
||||||
Input,
|
Input,
|
||||||
Label,
|
Label,
|
||||||
Select,
|
Select,
|
||||||
@@ -15,13 +14,14 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@common/components";
|
} from "@common/components";
|
||||||
|
import { columns } from "@/pages/apilog/model/table-columns";
|
||||||
import { ApiLogApi, type ApiLogParams } from "@/entities/apilog";
|
import { ApiLogApi, type ApiLogParams } from "@/entities/apilog";
|
||||||
import { useDataTable } from "@/shared/lib";
|
import { useDataTable } from "@/shared/lib";
|
||||||
import { ENDPOINT } from "@/shared/config";
|
import { ENDPOINT } from "@/shared/config";
|
||||||
import { columns } from "@/pages/apilog/model/table-columns";
|
import { InputDateRangePicker } from "@/shared/ui/input-date-range-picker";
|
||||||
|
|
||||||
type DateRange = {
|
type DateRange = {
|
||||||
from: Date | undefined;
|
from?: Date | undefined;
|
||||||
to?: Date | undefined;
|
to?: Date | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -35,6 +35,7 @@ function ApiLogPage() {
|
|||||||
pageIndex: 0,
|
pageIndex: 0,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
});
|
});
|
||||||
|
const [presetDateRange, setPresetDateRange] = useState("");
|
||||||
const [dateRange, setDateRange] = useState<DateRange>(defaultDateRange);
|
const [dateRange, setDateRange] = useState<DateRange>(defaultDateRange);
|
||||||
const [userId, setUserId] = useState("");
|
const [userId, setUserId] = useState("");
|
||||||
const [apiHttpPmhd, setApiHttpPmhd] = useState("");
|
const [apiHttpPmhd, setApiHttpPmhd] = useState("");
|
||||||
@@ -84,8 +85,10 @@ function ApiLogPage() {
|
|||||||
...searchParams,
|
...searchParams,
|
||||||
userId,
|
userId,
|
||||||
apiHttpPmhd,
|
apiHttpPmhd,
|
||||||
dmndDtStart: dayjs(dateRange.from).format("YYYY-MM-DD"),
|
dmndDtStart: dateRange.from
|
||||||
dmndDtEnd: dayjs(dateRange.to).format("YYYY-MM-DD"),
|
? dayjs(dateRange.from).format("YYYY-MM-DD")
|
||||||
|
: "",
|
||||||
|
dmndDtEnd: dateRange.to ? dayjs(dateRange.to).format("YYYY-MM-DD") : "",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -126,31 +129,34 @@ function ApiLogPage() {
|
|||||||
default:
|
default:
|
||||||
presetDateRange = undefined;
|
presetDateRange = undefined;
|
||||||
}
|
}
|
||||||
|
setPresetDateRange(value);
|
||||||
setDateRange(presetDateRange || defaultDateRange);
|
setDateRange(presetDateRange || defaultDateRange);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDateRangeChange = (dateRange: DateRange) => {
|
||||||
|
setDateRange(dateRange);
|
||||||
|
setPresetDateRange("");
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="card card-border">
|
<div className="card card-border">
|
||||||
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
|
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
|
||||||
<Label className="mr-2">조회기간</Label>
|
<Label className="mr-2">조회기간</Label>
|
||||||
<DateRangePicker
|
<Select value={presetDateRange} onValueChange={handlePresetDateRange}>
|
||||||
value={dateRange}
|
<SelectTrigger className="w-[12rem]">
|
||||||
onValueChange={(value) => setDateRange(value || defaultDateRange)}
|
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
|
||||||
>
|
</SelectTrigger>
|
||||||
<div className="card card-border flex items-center gap-2">
|
<SelectContent>
|
||||||
<Select onValueChange={handlePresetDateRange}>
|
<SelectItem value="1W">일주일</SelectItem>
|
||||||
<SelectTrigger>
|
<SelectItem value="1M">1개월</SelectItem>
|
||||||
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
|
<SelectItem value="3M">3개월</SelectItem>
|
||||||
</SelectTrigger>
|
</SelectContent>
|
||||||
<SelectContent>
|
</Select>
|
||||||
<SelectItem value="1W">일주일</SelectItem>
|
<InputDateRangePicker
|
||||||
<SelectItem value="1M">1개월</SelectItem>
|
dateRange={dateRange}
|
||||||
<SelectItem value="3M">3개월</SelectItem>
|
setDateRange={handleDateRangeChange}
|
||||||
</SelectContent>
|
/>
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</DateRangePicker>
|
|
||||||
<Label className="ml-4 mr-2 text-label">사용자ID</Label>
|
<Label className="ml-4 mr-2 text-label">사용자ID</Label>
|
||||||
<Input
|
<Input
|
||||||
value={userId}
|
value={userId}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { client } from "@/shared/api";
|
||||||
|
import { ENDPOINT } from "@/shared/config";
|
||||||
|
import type { Dashboard, DashboardService } from "../model/dashboard-interface";
|
||||||
|
|
||||||
|
export class DashboardApiService implements DashboardService {
|
||||||
|
public async getDashboard(): Promise<Dashboard> {
|
||||||
|
const response = await client.get<Dashboard>(
|
||||||
|
ENDPOINT.CMS_SERVICE.DASHBOARD,
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DashboardApi = new DashboardApiService();
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import type { ChartData, ChartDataset } from "chart.js";
|
||||||
|
import { getCssVariableValue } from "@common/components";
|
||||||
|
import type {
|
||||||
|
MenuStatisticsContent,
|
||||||
|
SiteStatisticsContent,
|
||||||
|
} from "../model/dashboard-interface";
|
||||||
|
|
||||||
|
export function processSiteAccessData(
|
||||||
|
siteAccessList: SiteStatisticsContent[],
|
||||||
|
): ChartData<"line">["datasets"] {
|
||||||
|
const siteGroups: Record<string, ChartDataset> = siteAccessList.reduce(
|
||||||
|
(acc, item) => {
|
||||||
|
if (!acc[item.siteNm]) {
|
||||||
|
acc[item.siteNm] = {
|
||||||
|
label: item.siteNm,
|
||||||
|
data: Array(24).fill(0), // 24시간 데이터 초기화
|
||||||
|
borderColor: getCssVariableValue(
|
||||||
|
`--chart-${Math.floor(Math.random() * 5) + 1}`,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 시간대별 접속 카운트 누적
|
||||||
|
const hourIndex = parseInt(item.accessHour, 10);
|
||||||
|
if (!isNaN(hourIndex) && hourIndex >= 0 && hourIndex < 24) {
|
||||||
|
acc[item.siteNm]!.data[hourIndex] = item.accessCnt;
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, ChartDataset>,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Object.values(siteGroups) as ChartData<"line">["datasets"];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function processMenuAccessData(
|
||||||
|
menuAccessList: MenuStatisticsContent[],
|
||||||
|
): ChartData<"bar">["datasets"] {
|
||||||
|
const data = menuAccessList.map((item) => item.accessCnt);
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: "메뉴별 접속 횟수",
|
||||||
|
data,
|
||||||
|
backgroundColor: "rgb(32, 148, 250)",
|
||||||
|
},
|
||||||
|
] as ChartData<"bar">["datasets"];
|
||||||
|
}
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
import { type ChartData, type ChartOptions } from "chart.js";
|
||||||
|
|
||||||
|
const commonData = {
|
||||||
|
labels: ["1", "2", "3", "4", "5", "6", "7"],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
fill: true,
|
||||||
|
pointRadius: 0,
|
||||||
|
tension: 0.5,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const commonOptions = {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const chartData: Record<
|
||||||
|
string,
|
||||||
|
{ data: ChartData<"line" | "bar">; options: ChartOptions<"line" | "bar"> }
|
||||||
|
> = {
|
||||||
|
memberChartData: {
|
||||||
|
data: {
|
||||||
|
...commonData,
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
...commonData.datasets[0],
|
||||||
|
data: [8, 10, 7, 12, 9, 13, 12],
|
||||||
|
borderColor: "rgba(255, 255, 255, 0.6)",
|
||||||
|
backgroundColor: "rgba(255, 255, 255, 0.1)",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
...commonOptions,
|
||||||
|
scales: {
|
||||||
|
...commonOptions.scales,
|
||||||
|
y: {
|
||||||
|
...commonOptions.scales.y,
|
||||||
|
min: 5,
|
||||||
|
max: 15,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
postChartData: {
|
||||||
|
data: {
|
||||||
|
...commonData,
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
...commonData.datasets[0],
|
||||||
|
data: [5200, 5350, 5450, 5520, 5600, 5650, 5678],
|
||||||
|
borderColor: "rgba(86, 138, 255, 1)",
|
||||||
|
backgroundColor: "rgba(86, 138, 255, 0.1)",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
...commonOptions,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
visitorChartData: {
|
||||||
|
data: {
|
||||||
|
...commonData,
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
...commonData.datasets[0],
|
||||||
|
data: [1800, 2100, 1950, 2300, 2150, 2250, 2345],
|
||||||
|
borderColor: "rgba(149, 131, 252)",
|
||||||
|
backgroundColor: "rgba(149, 131, 252, 0.1)",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
...commonOptions,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
apiChartData: {
|
||||||
|
data: {
|
||||||
|
...commonData,
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
...commonData.datasets[0],
|
||||||
|
data: [41000, 42500, 43200, 44100, 44800, 45200, 45678],
|
||||||
|
borderColor: "rgba(220, 103, 206)",
|
||||||
|
backgroundColor: "rgba(220, 103, 206, 0.1)",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
...commonOptions,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
siteAccessChartData: {
|
||||||
|
data: {
|
||||||
|
labels: Array.from({ length: 24 }, (_, i) =>
|
||||||
|
i.toString().padStart(2, "0"),
|
||||||
|
),
|
||||||
|
datasets: [],
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
...commonOptions,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
position: "top",
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
mode: "index",
|
||||||
|
intersect: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
title: {
|
||||||
|
text: "시간대",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
title: {
|
||||||
|
text: "접속자 수",
|
||||||
|
},
|
||||||
|
beginAtZero: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
menuAccessChartData: {
|
||||||
|
data: {
|
||||||
|
labels: [],
|
||||||
|
datasets: [],
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
...commonOptions,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
position: "top",
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
mode: "index",
|
||||||
|
intersect: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
title: {
|
||||||
|
text: "메뉴명",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
title: {
|
||||||
|
text: "접속 횟수",
|
||||||
|
},
|
||||||
|
beginAtZero: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
export interface SiteStatisticsContent {
|
||||||
|
siteId: string;
|
||||||
|
siteNm: string;
|
||||||
|
accessHour: string;
|
||||||
|
accessCnt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MenuStatisticsContent {
|
||||||
|
menuCd: number;
|
||||||
|
menuNm: string;
|
||||||
|
accessCnt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Dashboard {
|
||||||
|
memberTotCnt: number;
|
||||||
|
memberTodayCnt: number;
|
||||||
|
pstTotCnt: number;
|
||||||
|
pstTodayCnt: number;
|
||||||
|
accessTodayCnt: number;
|
||||||
|
accessBefDayPer: number;
|
||||||
|
apiTodayCnt: number;
|
||||||
|
waitMemberCnt: number;
|
||||||
|
waitCntnCnt: number;
|
||||||
|
notiList: [];
|
||||||
|
schdlList: [];
|
||||||
|
siteAccessList: SiteStatisticsContent[];
|
||||||
|
menuAccessList: MenuStatisticsContent[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DashboardService {
|
||||||
|
getDashboard(): Promise<Dashboard>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import dayjs from "dayjs";
|
||||||
|
import type { ColumnDef } from "@tanstack/react-table";
|
||||||
|
import { Badge } from "@common/components";
|
||||||
|
import type { RecentPost } from "@/entities/post";
|
||||||
|
|
||||||
|
const noticePostTableColumns: ColumnDef<RecentPost>[] = [
|
||||||
|
{
|
||||||
|
header: "번호",
|
||||||
|
cell: ({ row, table }) => {
|
||||||
|
if (row.original.upndFixYn === "Y") {
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
color="orange"
|
||||||
|
className="flex size-8 items-center justify-center"
|
||||||
|
>
|
||||||
|
<i className="diveicon di-volume-up-fill" />
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const totalRows = table.getRowCount();
|
||||||
|
const currentPage = table.getState().pagination.pageIndex;
|
||||||
|
const pageSize = table.getState().pagination.pageSize;
|
||||||
|
return totalRows - (currentPage * pageSize + row.index);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "pstTtl",
|
||||||
|
header: "제목",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "wrtrNm",
|
||||||
|
header: "작성자",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "regDt",
|
||||||
|
header: "작성일",
|
||||||
|
cell: ({ row }) => dayjs(row.original.regDt).format("YYYY-MM-DD"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "inqNocs",
|
||||||
|
header: "조회수",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const recentPostTableColumns: ColumnDef<RecentPost>[] = [
|
||||||
|
{
|
||||||
|
header: "번호",
|
||||||
|
cell: ({ row, table }) => {
|
||||||
|
const totalRows = table.getRowCount();
|
||||||
|
const currentPage = table.getState().pagination.pageIndex;
|
||||||
|
const pageSize = table.getState().pagination.pageSize;
|
||||||
|
return totalRows - (currentPage * pageSize + row.index);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "pstTtl",
|
||||||
|
header: "제목",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "wrtrNm",
|
||||||
|
header: "작성자",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "regDt",
|
||||||
|
header: "작성일",
|
||||||
|
cell: ({ row }) => dayjs(row.original.regDt).format("YYYY-MM-DD"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "inqNocs",
|
||||||
|
header: "조회수",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export { noticePostTableColumns, recentPostTableColumns };
|
||||||
@@ -1,5 +1,433 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { getCoreRowModel } from "@tanstack/react-table";
|
||||||
|
import {
|
||||||
|
Chart as ChartJS,
|
||||||
|
CategoryScale,
|
||||||
|
LinearScale,
|
||||||
|
PointElement,
|
||||||
|
LineElement,
|
||||||
|
BarElement,
|
||||||
|
Filler,
|
||||||
|
Legend,
|
||||||
|
Tooltip,
|
||||||
|
type ChartData,
|
||||||
|
} from "chart.js";
|
||||||
|
import { Bar, Line } from "react-chartjs-2";
|
||||||
|
import { PostApi, type RecentPost } from "@/entities/post";
|
||||||
|
import { ENDPOINT } from "@/shared/config";
|
||||||
|
import { useDataTable } from "@/shared/lib";
|
||||||
|
import { DashboardApi } from "../api/dashboard-service";
|
||||||
|
import { chartData } from "../model/chart-data";
|
||||||
|
import {
|
||||||
|
noticePostTableColumns,
|
||||||
|
recentPostTableColumns,
|
||||||
|
} from "../model/table-columns";
|
||||||
|
import { processMenuAccessData, processSiteAccessData } from "../lib/utils";
|
||||||
|
|
||||||
|
ChartJS.register(
|
||||||
|
CategoryScale,
|
||||||
|
LinearScale,
|
||||||
|
PointElement,
|
||||||
|
LineElement,
|
||||||
|
BarElement,
|
||||||
|
Filler,
|
||||||
|
Legend,
|
||||||
|
Tooltip,
|
||||||
|
);
|
||||||
|
|
||||||
function DashboardPage() {
|
function DashboardPage() {
|
||||||
return <></>;
|
const router = useRouter();
|
||||||
|
const { data } = useQuery({
|
||||||
|
queryKey: [ENDPOINT.CMS_SERVICE.DASHBOARD],
|
||||||
|
queryFn: () => DashboardApi.getDashboard(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const noticePostQuery = useQuery({
|
||||||
|
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS],
|
||||||
|
queryFn: () =>
|
||||||
|
PostApi.getRecentPostList({
|
||||||
|
page: 0,
|
||||||
|
size: 5,
|
||||||
|
keywordType: "bbsTypeCd",
|
||||||
|
keyword: "BBSNOTICE",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const recentPostQuery = useQuery({
|
||||||
|
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS],
|
||||||
|
queryFn: () =>
|
||||||
|
PostApi.getRecentPostList({
|
||||||
|
page: 0,
|
||||||
|
size: 5,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleNoticePostRowClick = (row: RecentPost) => {
|
||||||
|
router.push(`/bbs/bbsnotice/${row.bbsId}/${row.pstNo}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRecentPostRowClick = (row: RecentPost) => {
|
||||||
|
router.push(
|
||||||
|
`/bbs/${row.bbsTypeCd.toLowerCase()}/${row.bbsId}/${row.pstNo}`,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { tableComponent: noticePostTableComponent } = useDataTable({
|
||||||
|
query: noticePostQuery,
|
||||||
|
tableOptions: {
|
||||||
|
columns: noticePostTableColumns,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
},
|
||||||
|
dataTableOptions: {
|
||||||
|
variant: "line",
|
||||||
|
onRowClick: handleNoticePostRowClick,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { tableComponent: recentPostTableComponent } = useDataTable({
|
||||||
|
query: recentPostQuery,
|
||||||
|
tableOptions: {
|
||||||
|
columns: recentPostTableColumns,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
},
|
||||||
|
dataTableOptions: {
|
||||||
|
variant: "line",
|
||||||
|
onRowClick: handleRecentPostRowClick,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
{/* 첫번째 라인 */}
|
||||||
|
<div className="grid grid-cols-4 gap-1">
|
||||||
|
{/* 총 회원수 카드 */}
|
||||||
|
<div className="card card-border h-80 !bg-primary !p-0">
|
||||||
|
<div className="relative h-full">
|
||||||
|
<div className="absolute left-0 top-0 p-6 tracking-tighter text-white">
|
||||||
|
<h3 className="text-2xl font-medium">총 회원수</h3>
|
||||||
|
<div className="mt-2 text-6xl font-bold">
|
||||||
|
{data?.memberTotCnt
|
||||||
|
? data.memberTotCnt.toLocaleString()
|
||||||
|
: "-"}
|
||||||
|
</div>
|
||||||
|
<p className="mt-1 text-2xl opacity-80">
|
||||||
|
금일 신규 가입자
|
||||||
|
{data?.memberTodayCnt
|
||||||
|
? data.memberTodayCnt.toLocaleString()
|
||||||
|
: "-"}
|
||||||
|
명
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/* 차트 영역 */}
|
||||||
|
<div className="h-full">
|
||||||
|
<Line
|
||||||
|
data={chartData.memberChartData!.data as ChartData<"line">}
|
||||||
|
options={chartData.memberChartData!.options}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* 전체 게시물 카드 */}
|
||||||
|
<div className="card card-border h-80 !p-0">
|
||||||
|
<div className="relative h-full">
|
||||||
|
<div className="absolute left-0 top-0 p-6 tracking-tighter">
|
||||||
|
<h3 className="text-2xl font-medium">전체 게시물</h3>
|
||||||
|
<div className="mt-2 text-6xl font-bold">
|
||||||
|
{data?.pstTotCnt ? data.pstTotCnt.toLocaleString() : "-"}
|
||||||
|
</div>
|
||||||
|
<p className="mt-1 text-2xl opacity-80">
|
||||||
|
금일 +
|
||||||
|
{data?.pstTodayCnt ? data.pstTodayCnt.toLocaleString() : "-"}{" "}
|
||||||
|
등록
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/* 차트 영역 */}
|
||||||
|
<div className="h-full">
|
||||||
|
<Line
|
||||||
|
data={chartData.postChartData!.data as ChartData<"line">}
|
||||||
|
options={chartData.postChartData!.options}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* 오늘 접속자 수 카드 */}
|
||||||
|
<div className="card card-border h-80 !p-0">
|
||||||
|
<div className="relative h-full">
|
||||||
|
<div className="absolute left-0 top-0 p-6 tracking-tighter">
|
||||||
|
<h3 className="text-2xl font-medium">오늘 접속자 수</h3>
|
||||||
|
<div className="mt-2 text-6xl font-bold">
|
||||||
|
{data?.accessTodayCnt
|
||||||
|
? data.accessTodayCnt.toLocaleString()
|
||||||
|
: "-"}
|
||||||
|
</div>
|
||||||
|
<p className="mt-1 text-2xl opacity-80">
|
||||||
|
전일 대비 +
|
||||||
|
{data?.accessBefDayPer
|
||||||
|
? data.accessBefDayPer.toLocaleString()
|
||||||
|
: "-"}
|
||||||
|
%
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/* 차트 영역 */}
|
||||||
|
<div className="h-full">
|
||||||
|
<Line
|
||||||
|
data={chartData.visitorChartData!.data as ChartData<"line">}
|
||||||
|
options={chartData.visitorChartData!.options}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* API 호출 건수 카드 */}
|
||||||
|
<div className="card card-border h-80 !p-0">
|
||||||
|
<div className="relative h-full">
|
||||||
|
<div className="absolute left-0 top-0 p-6 tracking-tighter">
|
||||||
|
<h3 className="text-2xl font-medium">API 호출 건수</h3>
|
||||||
|
<div className="mt-2 text-6xl font-bold">
|
||||||
|
{data?.apiTodayCnt ? data.apiTodayCnt.toLocaleString() : "-"}
|
||||||
|
</div>
|
||||||
|
<p className="mt-1 text-2xl opacity-80">금일 누적</p>
|
||||||
|
</div>
|
||||||
|
{/* 차트 영역 */}
|
||||||
|
<div className="h-full">
|
||||||
|
<Line
|
||||||
|
data={chartData.apiChartData!.data as ChartData<"line">}
|
||||||
|
options={chartData.apiChartData!.options}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 두번째 라인 */}
|
||||||
|
<div className="grid grid-cols-2 gap-1">
|
||||||
|
{/* 승인 대기 알림 */}
|
||||||
|
<div className="card card-border">
|
||||||
|
<div className="card-header">
|
||||||
|
<h3 className="text-2xl font-medium">승인 대기 알림</h3>
|
||||||
|
</div>
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="flex gap-4">
|
||||||
|
{/* 사용자 승인 */}
|
||||||
|
<div className="flex flex-1 flex-col items-start rounded-md bg-accent p-6">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="mr-4 flex h-10 w-10 items-center justify-center rounded-lg bg-red-100">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
className="text-red-500"
|
||||||
|
>
|
||||||
|
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
|
||||||
|
<circle cx="12" cy="7" r="4"></circle>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<span className="text-2xl font-medium">사용자 승인</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-5">
|
||||||
|
<div className="text-5xl font-bold">
|
||||||
|
{data?.waitMemberCnt
|
||||||
|
? data.waitMemberCnt.toLocaleString()
|
||||||
|
: "0"}
|
||||||
|
건
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 text-2xl text-gray-500">
|
||||||
|
신규 가입 승인 대기
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* 매뉴 승인 */}
|
||||||
|
<div className="flex flex-1 flex-col items-start rounded-md bg-accent p-6">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="mr-4 flex h-10 w-10 items-center justify-center rounded-lg bg-blue-100">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
className="text-blue-500"
|
||||||
|
>
|
||||||
|
<rect x="3" y="3" width="7" height="7"></rect>
|
||||||
|
<rect x="14" y="3" width="7" height="7"></rect>
|
||||||
|
<rect x="14" y="14" width="7" height="7"></rect>
|
||||||
|
<rect x="3" y="14" width="7" height="7"></rect>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<span className="text-2xl font-medium">메뉴 승인</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-5">
|
||||||
|
<div className="text-5xl font-bold">
|
||||||
|
{(3).toLocaleString()}건
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 text-2xl text-gray-500">
|
||||||
|
매뉴 수정 승인 대기
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* 컨텐츠 승인 */}
|
||||||
|
<div className="flex flex-1 flex-col items-start rounded-md bg-accent p-6">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="mr-4 flex h-10 w-10 items-center justify-center rounded-lg bg-yellow-100">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
className="text-yellow-500"
|
||||||
|
>
|
||||||
|
<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"></path>
|
||||||
|
<polyline points="14 2 14 8 20 8"></polyline>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<span className="text-2xl font-medium">컨텐츠 승인</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-5">
|
||||||
|
<div className="text-5xl font-bold">
|
||||||
|
{data?.waitCntnCnt
|
||||||
|
? data.waitCntnCnt.toLocaleString()
|
||||||
|
: "0"}
|
||||||
|
건
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 text-2xl text-gray-500">
|
||||||
|
컨텐츠 수정 승인 대기
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-1">
|
||||||
|
{/* 실시간 알림 */}
|
||||||
|
<div className="card card-border">
|
||||||
|
<div className="card-header">
|
||||||
|
<h3 className="text-2xl font-medium">실시간 알림</h3>
|
||||||
|
</div>
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between border-b border-border pb-4">
|
||||||
|
<span className="font-medium">신규 사용자 가입</span>
|
||||||
|
<span>{(1).toLocaleString()}분 전</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between border-b border-border pb-4">
|
||||||
|
<span className="font-medium">메뉴 수정 요청</span>
|
||||||
|
<span>{(5).toLocaleString()}분 전</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between border-b border-border pb-4">
|
||||||
|
<span className="font-medium">게시물 신고</span>
|
||||||
|
<span>{(10).toLocaleString()}분 전</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="font-medium">컨텐츠 승인 요청</span>
|
||||||
|
<span>{(15).toLocaleString()}분 전</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* 일정 알림 */}
|
||||||
|
<div className="card card-border">
|
||||||
|
<div className="card-header">
|
||||||
|
<h3 className="text-2xl font-medium">일정 알림</h3>
|
||||||
|
</div>
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between border-b border-border pb-4">
|
||||||
|
<span className="font-medium">
|
||||||
|
{(3).toLocaleString()}월 정기 시스템 점검
|
||||||
|
</span>
|
||||||
|
<span>내일</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between border-b border-border pb-4">
|
||||||
|
<span className="font-medium">서버 업데이트 예정</span>
|
||||||
|
<span>{(3).toLocaleString()}일 후</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between border-b border-border pb-4">
|
||||||
|
<span className="font-medium">백업 스케줄</span>
|
||||||
|
<span>{(5).toLocaleString()}일 후</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="font-medium">정기 보안 점검</span>
|
||||||
|
<span>{(7).toLocaleString()}일 후</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 세번째 라인 */}
|
||||||
|
<div className="grid grid-cols-2 gap-1">
|
||||||
|
{/* 접속자 통계 */}
|
||||||
|
<div className="card card-border">
|
||||||
|
<div className="card-header">
|
||||||
|
<h3 className="text-2xl font-medium">접속자 통계</h3>
|
||||||
|
</div>
|
||||||
|
<div className="h-80 p-4">
|
||||||
|
<Line
|
||||||
|
data={{
|
||||||
|
...chartData.siteAccessChartData!.data,
|
||||||
|
datasets: processSiteAccessData(data?.siteAccessList ?? []),
|
||||||
|
}}
|
||||||
|
options={chartData.siteAccessChartData!.options}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* 메뉴별 접속 통계 */}
|
||||||
|
<div className="card card-border">
|
||||||
|
<div className="card-header">
|
||||||
|
<h3 className="text-2xl font-medium">메뉴별 접속 통계</h3>
|
||||||
|
</div>
|
||||||
|
<div className="h-80 p-4">
|
||||||
|
<Bar
|
||||||
|
data={{
|
||||||
|
labels:
|
||||||
|
data?.menuAccessList?.map((item) => item.menuNm) || [],
|
||||||
|
datasets: processMenuAccessData(data?.menuAccessList || []),
|
||||||
|
}}
|
||||||
|
options={chartData.menuAccessChartData!.options}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 네번째 라인 */}
|
||||||
|
<div className="grid grid-cols-2 gap-1">
|
||||||
|
{/* 공지사항 */}
|
||||||
|
<div className="card card-border !p-0">
|
||||||
|
<div className="card-header !m-0 !border-none">
|
||||||
|
<h3 className="text-2xl font-medium">공지사항</h3>
|
||||||
|
</div>
|
||||||
|
{noticePostTableComponent}
|
||||||
|
</div>
|
||||||
|
{/* 신규 게시글 */}
|
||||||
|
<div className="card card-border !p-0">
|
||||||
|
<div className="card-header !m-0 !border-none">
|
||||||
|
<h3 className="text-2xl font-medium">신규 게시글</h3>
|
||||||
|
</div>
|
||||||
|
{recentPostTableComponent}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { DashboardPage };
|
export { DashboardPage };
|
||||||
|
|||||||
@@ -161,11 +161,11 @@ function GroupForm({ form }: GroupFormProps) {
|
|||||||
>
|
>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<RadioGroupItem value="Y" id="mngrY" />
|
<RadioGroupItem value="Y" id="mngrY" />
|
||||||
<Label htmlFor="mngrY">Y</Label>
|
<Label htmlFor="mngrY">예</Label>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<RadioGroupItem value="N" id="mngrN" />
|
<RadioGroupItem value="N" id="mngrN" />
|
||||||
<Label htmlFor="mngrN">N</Label>
|
<Label htmlFor="mngrN">아니오</Label>
|
||||||
</div>
|
</div>
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { memo, useState } from "react";
|
import { memo, useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||||
@@ -43,7 +43,7 @@ const GroupsDialog = memo(
|
|||||||
const { data: group } = useQuery({
|
const { data: group } = useQuery({
|
||||||
queryKey: [ENDPOINT.CMS_SERVICE.GROUPS, groupId],
|
queryKey: [ENDPOINT.CMS_SERVICE.GROUPS, groupId],
|
||||||
queryFn: () => GroupApi.findGroupById(groupId!),
|
queryFn: () => GroupApi.findGroupById(groupId!),
|
||||||
enabled: !!groupId && open, // open 시점 groupId가 있을 때만 상세 조회 API 호출
|
enabled: !!groupId && open, // open 시점 groupId가 있을 때만 상세 조회 API 호출
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutateAsync: createGroup } = useMutationWrapper({
|
const { mutateAsync: createGroup } = useMutationWrapper({
|
||||||
@@ -77,7 +77,7 @@ const GroupsDialog = memo(
|
|||||||
delYn: group?.delYn ?? "",
|
delYn: group?.delYn ?? "",
|
||||||
authrtId: group?.authrtId ?? "",
|
authrtId: group?.authrtId ?? "",
|
||||||
siteId: group?.siteId ?? "",
|
siteId: group?.siteId ?? "",
|
||||||
mngrYn: group?.mngrYn ?? "",
|
mngrYn: group?.mngrYn ?? "Y",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -91,6 +91,10 @@ const GroupsDialog = memo(
|
|||||||
if (await confirm(confirmMessage)) callback(data);
|
if (await confirm(confirmMessage)) callback(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) form.reset();
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen} {...props}>
|
<Dialog open={open} onOpenChange={setOpen} {...props}>
|
||||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ function CreatePage() {
|
|||||||
resolver: zodResolver(imageFormSchema),
|
resolver: zodResolver(imageFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
atflYn: "Y",
|
atflYn: "Y",
|
||||||
|
useYn: "Y",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ function ImageForm({ form, imgSn, ...props }: ImageFormProps) {
|
|||||||
{...field}
|
{...field}
|
||||||
type="number"
|
type="number"
|
||||||
min={0}
|
min={0}
|
||||||
|
max={9999}
|
||||||
value={field.value ?? ""}
|
value={field.value ?? ""}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
field.onChange(parseInt(e.target.value))
|
field.onChange(parseInt(e.target.value))
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export { ModifyLinkPage } from "./ui/modify-page";
|
||||||
|
export { RegistLinkPage } from "./ui/regist-page";
|
||||||
|
export { LinkPage } from "./ui/link-page";
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import dayjs from "dayjs";
|
||||||
|
import { createColumnHelper } from "@tanstack/react-table";
|
||||||
|
import { type Link } from "@/entities/link";
|
||||||
|
import QuickStartButton from "../ui/QuickStartButton";
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<Link>();
|
||||||
|
|
||||||
|
const convertMinutesToPeriod = (minutes: number): string => {
|
||||||
|
if (!minutes) return '';
|
||||||
|
|
||||||
|
const year = Math.floor(minutes / (525600)); // 365일 * 24시간 * 60분
|
||||||
|
minutes = minutes % 525600;
|
||||||
|
|
||||||
|
const month = Math.floor(minutes / (43200)); // 30일 * 24시간 * 60분
|
||||||
|
minutes = minutes % 43200;
|
||||||
|
|
||||||
|
const day = Math.floor(minutes / 1440); // 24시간 * 60분
|
||||||
|
minutes = minutes % 1440;
|
||||||
|
|
||||||
|
const hour = Math.floor(minutes / 60);
|
||||||
|
const minute = minutes % 60;
|
||||||
|
|
||||||
|
const parts = [];
|
||||||
|
if (year > 0) parts.push(`${year}년`);
|
||||||
|
if (month > 0) parts.push(`${month}개월`);
|
||||||
|
if (day > 0) parts.push(`${day}일`);
|
||||||
|
if (hour > 0) parts.push(`${hour}시간`);
|
||||||
|
if (minute > 0) parts.push(`${minute}분`);
|
||||||
|
|
||||||
|
return parts.join(' ');
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
columnHelper.accessor((_, index) => index + 1, {
|
||||||
|
id: 'no',
|
||||||
|
header: 'NO',
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('link_sn', {
|
||||||
|
id: 'link_sn',
|
||||||
|
header: '연계일련번호',
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('link_nm', {
|
||||||
|
id: 'link_nm',
|
||||||
|
header: '연계 명',
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('link_url', {
|
||||||
|
id: 'link_url',
|
||||||
|
header: '연계 URL',
|
||||||
|
cell: (info) => (info.getValue() ? info.getValue() : '-'),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('link_srvr_ip', {
|
||||||
|
id: 'link_srvr_ip',
|
||||||
|
header: '연계서버 IP',
|
||||||
|
cell: (info) => (info.getValue() ? info.getValue() : '-'),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('link_batch_time', {
|
||||||
|
id: 'link_batch_time',
|
||||||
|
header: '연계 주기',
|
||||||
|
cell: (info) => convertMinutesToPeriod(Number(info.getValue())),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('srvc_usg', {
|
||||||
|
id: 'srvc_usg',
|
||||||
|
header: '서비스용도',
|
||||||
|
cell: (info) => (info.getValue() ? info.getValue() : '-'),
|
||||||
|
}),
|
||||||
|
columnHelper.display({
|
||||||
|
id: 'last_link_rslt_cd',
|
||||||
|
header: '연계상태',
|
||||||
|
cell: (info) => {
|
||||||
|
const resultCd = info.row.original.last_link_rslt_cd;
|
||||||
|
return <span className={`${(resultCd === 'SUCC') ? 'text-[rgb(53,162,235)]' : (resultCd === 'FAIL' ? 'text-[rgb(255,99,132)]' : 'text-[rgba(31,208,120,1)]')}`}>{resultCd === 'SUCC' ? '성공' : (resultCd === 'FAIL' ? '실패' : '대기')}</span>
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('last_link_excn_dt', {
|
||||||
|
id: 'last_link_excn_dt',
|
||||||
|
header: '최종연계일',
|
||||||
|
cell: (info) => (info.getValue() ? dayjs(info.getValue()).format('YYYY-MM-DD HH:mm') : '-'),
|
||||||
|
}),
|
||||||
|
columnHelper.display({
|
||||||
|
id: 'quick_start_btn',
|
||||||
|
header: '즉시 실행',
|
||||||
|
cell: (info) => {
|
||||||
|
return <QuickStartButton linkSn={info.row.original.link_sn} />
|
||||||
|
}
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
export { columns };
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
import { LinkApi } from "@/entities/link";
|
||||||
|
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||||
|
import { Button } from "@common/components";
|
||||||
|
|
||||||
|
const QuickStartButton: React.FC<{ linkSn: string }> = ({ linkSn }) => {
|
||||||
|
const { setMessage: alert } = useAlertStore((state) => state);
|
||||||
|
|
||||||
|
const { mutateAsync: updateQuickStart } = useMutation({
|
||||||
|
mutationFn: (linkSn: string) => LinkApi.linkQuickStart(linkSn),
|
||||||
|
onSuccess: (result) => {
|
||||||
|
if (result === 'succ') {
|
||||||
|
alert('즉시 실행 요청이 완료되었습니다.');
|
||||||
|
} else {
|
||||||
|
alert('기존 요청이 실행 대기중입니다.');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
alert('즉시 실행 요청을 실패했습니다.');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
color="sky"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
updateQuickStart(linkSn);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
즉시 실행
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default QuickStartButton;
|
||||||
@@ -0,0 +1,590 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Input,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
Select,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectValue,
|
||||||
|
} from "@common/components";
|
||||||
|
import { FormItem, FormControl, FormField, Form } from "@/shared/ui/form";
|
||||||
|
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
||||||
|
import type { UseFormReturn } from "react-hook-form";
|
||||||
|
import { type LinkFormData } from "@/entities/link";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
|
||||||
|
type LinkFormProps = {
|
||||||
|
form: UseFormReturn<LinkFormData>;
|
||||||
|
linkSn?: string;
|
||||||
|
updateLinkInfo?: (data: LinkFormData) => void;
|
||||||
|
handleInsertLink?: (data: LinkFormData) => void;
|
||||||
|
deleteLink?: () => void;
|
||||||
|
updateLinkResultCode?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LinkForm = ({ form, linkSn, updateLinkInfo, handleInsertLink, deleteLink, updateLinkResultCode }: LinkFormProps) => {
|
||||||
|
const isModifyMode = !!linkSn;
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const { confirm } = useConfirm();
|
||||||
|
const [onConfirm, setOnConfirm] = useState(false);
|
||||||
|
|
||||||
|
const handleConfirm = async (callback: () => Promise<void>) => {
|
||||||
|
setOnConfirm(true);
|
||||||
|
await callback();
|
||||||
|
setOnConfirm(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (data: LinkFormData) => {
|
||||||
|
const confirmMsg = !!linkSn ? '수정 하시겠습니까?' : '저장 하시겠습니까?';
|
||||||
|
|
||||||
|
await handleConfirm(async () => {
|
||||||
|
|
||||||
|
if (await confirm(confirmMsg)) {
|
||||||
|
if (linkSn) {
|
||||||
|
updateLinkInfo?.(data);
|
||||||
|
} else {
|
||||||
|
handleInsertLink?.(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
await handleConfirm(async () => {
|
||||||
|
if (await confirm('삭제하시겠습니까?')) {
|
||||||
|
deleteLink?.();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
const convertMinutesToPeriod = (minutes: number) => {
|
||||||
|
if (!minutes) return {
|
||||||
|
year: 0,
|
||||||
|
month: 0,
|
||||||
|
day: 0,
|
||||||
|
hour: 0,
|
||||||
|
minute: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
const year = Math.floor(minutes / (525600)); // 365일 * 24시간 * 60분
|
||||||
|
minutes = minutes % 525600;
|
||||||
|
|
||||||
|
const month = Math.floor(minutes / (43200)); // 30일 * 24시간 * 60분
|
||||||
|
minutes = minutes % 43200;
|
||||||
|
|
||||||
|
const day = Math.floor(minutes / 1440); // 24시간 * 60분
|
||||||
|
minutes = minutes % 1440;
|
||||||
|
|
||||||
|
const hour = Math.floor(minutes / 60);
|
||||||
|
const minute = minutes % 60;
|
||||||
|
|
||||||
|
const timeParts = {
|
||||||
|
year: year,
|
||||||
|
month: month,
|
||||||
|
day: day,
|
||||||
|
hour: hour,
|
||||||
|
minute: minute,
|
||||||
|
};
|
||||||
|
|
||||||
|
return timeParts;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(handleSubmit)}>
|
||||||
|
<div className="card">
|
||||||
|
<Table variant="secondary">
|
||||||
|
<colgroup>
|
||||||
|
<col width="20%" />
|
||||||
|
<col width="80%" />
|
||||||
|
</colgroup>
|
||||||
|
<TableBody>
|
||||||
|
{isModifyMode &&
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>연계 일련번호</TableHead>
|
||||||
|
<TableCell>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="link_sn"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
value={field.value ?? ""}
|
||||||
|
className="w-[12rem] !text-[1.3rem]"
|
||||||
|
disabled={true}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
}
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>* 연계 명</TableHead>
|
||||||
|
<TableCell>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="link_nm"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
value={field.value ?? ""}
|
||||||
|
className="!text-[1.3rem]"
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
{form.formState.errors.link_nm && (
|
||||||
|
<p className="text-red-500 text-base pt-2">
|
||||||
|
{form.formState.errors.link_nm.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>* 연계 분류</TableHead>
|
||||||
|
<TableCell>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="link_clsf_cd"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Select value={field.value ?? "none"} onValueChange={(value) => field.onChange(value)}>
|
||||||
|
<SelectTrigger id="link_clsf_cd" className="w-[20rem]">
|
||||||
|
<SelectValue placeholder="전체"></SelectValue>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="none">선택</SelectItem>
|
||||||
|
<SelectItem value="link0001">TIBERO 데이터베이스</SelectItem>
|
||||||
|
<SelectItem value="link0002">ORACLE 데이터베이스</SelectItem>
|
||||||
|
<SelectItem value="link0003">공공데이터 포털 API</SelectItem>
|
||||||
|
<SelectItem value="link0004">한강홍수통제소 API</SelectItem>
|
||||||
|
<SelectItem value="link0005">기상청 API</SelectItem>
|
||||||
|
<SelectItem value="link0006">국가지하수정보센터 API</SelectItem>
|
||||||
|
<SelectItem value="link0007">행정정보공동이용센터 연계</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
{form.formState.errors.link_clsf_cd && (
|
||||||
|
<p className="text-red-500 text-base pt-2">
|
||||||
|
{form.formState.errors.link_clsf_cd.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>* 연계 URL</TableHead>
|
||||||
|
<TableCell>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="link_url"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
value={field.value ?? ""}
|
||||||
|
className="!text-[1.3rem]"
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
{form.formState.errors.link_url && (
|
||||||
|
<p className="text-red-500 text-base pt-2">
|
||||||
|
{form.formState.errors.link_url.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>연계서버 IP</TableHead>
|
||||||
|
<TableCell>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="link_srvr_ip"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
value={field.value ?? ""}
|
||||||
|
className="!text-[1.3rem]"
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>* 연계 KEY</TableHead>
|
||||||
|
<TableCell>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="link_key"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
value={field.value ?? ""}
|
||||||
|
className="!text-[1.3rem]"
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
{form.formState.errors.link_key && (
|
||||||
|
<p className="text-red-500 text-base pt-2">
|
||||||
|
{form.formState.errors.link_key.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>연계 ID</TableHead>
|
||||||
|
<TableCell>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="link_id"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
value={field.value ?? ""}
|
||||||
|
className="!text-[1.3rem]"
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>연계 비밀번호</TableHead>
|
||||||
|
<TableCell>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="link_pswd"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
type="password"
|
||||||
|
value={field.value ?? ""}
|
||||||
|
className="!text-[1.3rem]"
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>서비스 용도</TableHead>
|
||||||
|
<TableCell>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="srvc_usg"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
value={field.value ?? ""}
|
||||||
|
className="!text-[1.3rem]"
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>연계 주기</TableHead>
|
||||||
|
<TableCell>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="link_batch_time"
|
||||||
|
render={({ field }) => {
|
||||||
|
const timeParts = convertMinutesToPeriod(Number(field.value));
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<div className="flex items-center gap-6">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Input
|
||||||
|
value={timeParts.year}
|
||||||
|
className="w-20 !text-[1.3rem]"
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
onChange={(e) => {
|
||||||
|
const total = (Number(e.target.value) * 525600) +
|
||||||
|
(timeParts.month * 43200) +
|
||||||
|
(timeParts.day * 1440) +
|
||||||
|
(timeParts.hour * 60) +
|
||||||
|
timeParts.minute;
|
||||||
|
field.onChange(total);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="ml-2">년</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Input
|
||||||
|
value={timeParts.month}
|
||||||
|
className="w-20 !text-[1.3rem]"
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
max={11}
|
||||||
|
onChange={(e) => {
|
||||||
|
const total = (timeParts.year * 525600) +
|
||||||
|
(Number(e.target.value) * 43200) +
|
||||||
|
(timeParts.day * 1440) +
|
||||||
|
(timeParts.hour * 60) +
|
||||||
|
timeParts.minute;
|
||||||
|
field.onChange(total);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="ml-2">월</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Input
|
||||||
|
value={timeParts.day}
|
||||||
|
className="w-20 !text-[1.3rem]"
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
max={30}
|
||||||
|
onChange={(e) => {
|
||||||
|
const total = (timeParts.year * 525600) +
|
||||||
|
(timeParts.month * 43200) +
|
||||||
|
(Number(e.target.value) * 1440) +
|
||||||
|
(timeParts.hour * 60) +
|
||||||
|
timeParts.minute;
|
||||||
|
field.onChange(total);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="ml-2">일</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Input
|
||||||
|
value={timeParts.hour}
|
||||||
|
className="w-20 !text-[1.3rem]"
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
max={23}
|
||||||
|
onChange={(e) => {
|
||||||
|
const total = (timeParts.year * 525600) +
|
||||||
|
(timeParts.month * 43200) +
|
||||||
|
(timeParts.day * 1440) +
|
||||||
|
(Number(e.target.value) * 60) +
|
||||||
|
timeParts.minute;
|
||||||
|
field.onChange(total);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="ml-2">시간</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Input
|
||||||
|
value={timeParts.minute}
|
||||||
|
className="w-20 !text-[1.3rem]"
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
max={59}
|
||||||
|
onChange={(e) => {
|
||||||
|
const total = (timeParts.year * 525600) +
|
||||||
|
(timeParts.month * 43200) +
|
||||||
|
(timeParts.day * 1440) +
|
||||||
|
(timeParts.hour * 60) +
|
||||||
|
Number(e.target.value);
|
||||||
|
field.onChange(total);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="ml-2">분</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
{form.formState.errors.link_batch_time && (
|
||||||
|
<p className="text-red-500 text-base pt-2">
|
||||||
|
{form.formState.errors.link_batch_time.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
{isModifyMode &&
|
||||||
|
<>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>연계 상태</TableHead>
|
||||||
|
<TableCell>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="last_link_rslt_cd"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<div className="flex items-center gap-6">
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
value={field.value ? (field.value === 'SUCC' ? '성공' : (field.value === 'FAIL' ? '실패' : '대기')) : ''}
|
||||||
|
className="!text-[1.3rem] w-[15rem]"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
<Button color="green" type="button" onClick={() => updateLinkResultCode?.()}>대기로 전환</Button>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>최종 연계 실행일</TableHead>
|
||||||
|
<TableCell>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="last_link_excn_dt"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
value={field.value ? dayjs(field.value).format('YYYY-MM-DD HH:mm:ss') : ""}
|
||||||
|
className="!text-[1.3rem]"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>다음 연계 실행일</TableHead>
|
||||||
|
<TableCell>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="next_link_excn_dt"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
value={field.value ? dayjs(field.value).format('YYYY-MM-DD HH:mm:ss') : ""}
|
||||||
|
className="!text-[1.3rem]"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>수정자</TableHead>
|
||||||
|
<TableCell>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="mdfr"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
value={field.value ?? ""}
|
||||||
|
className="!text-[1.3rem]"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>수정일시</TableHead>
|
||||||
|
<TableCell>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="mdfcn_dt"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
value={field.value ? dayjs(field.value).format('YYYY-MM-DD') : ""}
|
||||||
|
className="!text-[1.3rem]"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-center gap-4 pb-12 pt-10">
|
||||||
|
<Button
|
||||||
|
size="lg"
|
||||||
|
color="white"
|
||||||
|
type="button"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
>
|
||||||
|
목록
|
||||||
|
</Button>
|
||||||
|
<Button size="lg" type="submit">
|
||||||
|
저장
|
||||||
|
</Button>
|
||||||
|
{isModifyMode && (
|
||||||
|
<Button size="lg" color="red" type="button" onClick={handleDelete}>
|
||||||
|
삭제
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
<ConfirmDialog isOpen={onConfirm} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LinkForm;
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import "@common/assets/styles/grid.css";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { Button, Input, Label, LoadingWithAnimation } from "@common/components";
|
||||||
|
import { getCoreRowModel } from "@tanstack/react-table";
|
||||||
|
import { LinkApi, type Link, type LinkSearchParam } from "@/entities/link";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { columns } from "../model/table-columns";
|
||||||
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { useDataTable } from "@/shared/lib";
|
||||||
|
|
||||||
|
function LinkPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const [linkSn, setLinkSn] = useState<string>('');
|
||||||
|
const [linkNm, setLinkNm] = useState<string>('');
|
||||||
|
|
||||||
|
const [linkSearchParam, setLinkSearchParam] = useState<LinkSearchParam>({
|
||||||
|
linkSn: '',
|
||||||
|
linkNm: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const query = useQuery({
|
||||||
|
queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkList`, linkSearchParam],
|
||||||
|
queryFn: () => LinkApi.getLinkList(linkSearchParam),
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleResetSearchParam = () => {
|
||||||
|
setLinkSn('');
|
||||||
|
setLinkNm('');
|
||||||
|
setLinkSearchParam({
|
||||||
|
...linkSearchParam,
|
||||||
|
linkSn: '',
|
||||||
|
linkNm: '',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setLinkSearchParam({
|
||||||
|
...linkSearchParam,
|
||||||
|
linkSn,
|
||||||
|
linkNm
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRowClick = (link: Link) => {
|
||||||
|
queryClient.removeQueries({
|
||||||
|
queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkList`],
|
||||||
|
});
|
||||||
|
queryClient.removeQueries({
|
||||||
|
queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkInfo`],
|
||||||
|
});
|
||||||
|
router.push(`link/register/${link.link_sn}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClickRegist = () => {
|
||||||
|
router.push('link/register');
|
||||||
|
};
|
||||||
|
|
||||||
|
const { tableComponent } = useDataTable({
|
||||||
|
query,
|
||||||
|
tableOptions: {
|
||||||
|
columns,
|
||||||
|
manualPagination: true,
|
||||||
|
enableMultiRowSelection: true,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
},
|
||||||
|
dataTableOptions: {
|
||||||
|
onRowClick: handleRowClick,
|
||||||
|
viewPagination: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="card card-border !mt-6">
|
||||||
|
<div className="card card-border">
|
||||||
|
<form className="flex items-center gap-2 p-2" onSubmit={handleSubmit}>
|
||||||
|
<Label htmlFor="liSnSearchInput" className="mr-2 text-label">
|
||||||
|
일련번호
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem] mr-4"
|
||||||
|
id="liSnSearchInput"
|
||||||
|
value={linkSn}
|
||||||
|
onChange={(e) => setLinkSn(e.target.value)}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
<Label htmlFor="liNmSearchInput" className="mr-2 text-label">
|
||||||
|
연계 명
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem] mr-2"
|
||||||
|
id="liNmSearchInput"
|
||||||
|
value={linkNm}
|
||||||
|
onChange={(e) => setLinkNm(e.target.value)}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
<Button color="green" className="h-[3.2rem]" type="button" onClick={handleResetSearchParam}>
|
||||||
|
초기화
|
||||||
|
</Button>
|
||||||
|
<Button className="h-[3.2rem]" type="submit">
|
||||||
|
조회
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div className="card card-border flex flex-col h-[calc(100vh-28rem)]">
|
||||||
|
<div className="card-header">
|
||||||
|
<h3 className="text-2xl font-medium text-label">연계 목록</h3>
|
||||||
|
<div>
|
||||||
|
<Button onClick={handleClickRegist}>등록</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="card !m-[1.2rem] overflow-auto h-full !p-0">
|
||||||
|
{!query.isLoading ?
|
||||||
|
<div>{tableComponent}</div>
|
||||||
|
:
|
||||||
|
<div className="[data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center h-full">
|
||||||
|
<div className="relative size-[5rem]">
|
||||||
|
<LoadingWithAnimation color="primary" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { LinkPage };
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { LinkApi, linkFormSchema, type LinkFormData } from "@/entities/link";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import LinkForm from "./link-form";
|
||||||
|
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
|
||||||
|
import { AUTH_USER_ID } from "@/shared/config";
|
||||||
|
import { getCookie } from "@common/components";
|
||||||
|
|
||||||
|
function ModifyLinkPage({ linkSn }: { linkSn: string }) {
|
||||||
|
const router = useRouter();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const { setMessage: alert } = useAlertStore((state) => state);
|
||||||
|
const userUniqId = getCookie(AUTH_USER_ID) ?? "";
|
||||||
|
|
||||||
|
// 컴포넌트 마운트 시 캐시 초기화
|
||||||
|
useEffect(() => {
|
||||||
|
queryClient.removeQueries({
|
||||||
|
queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkInfo`],
|
||||||
|
});
|
||||||
|
}, [queryClient]);
|
||||||
|
|
||||||
|
const { data: linkInfo, refetch } = useQuery({
|
||||||
|
queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkInfo`],
|
||||||
|
queryFn: () => LinkApi.getLinkInfo(linkSn),
|
||||||
|
enabled: !!linkSn,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutateAsync: updateLinkInfo } = useMutation({
|
||||||
|
mutationFn: (data: LinkFormData) => LinkApi.updateLinkInfo({
|
||||||
|
...data,
|
||||||
|
link_clsf_cd: data.link_clsf_cd !== 'none' ? data.link_clsf_cd : null,
|
||||||
|
mdfr: userUniqId
|
||||||
|
}),
|
||||||
|
onSuccess: () => {
|
||||||
|
alert('수정되었습니다.');
|
||||||
|
router.push('/link');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutateAsync: deleteLink } = useMutation({
|
||||||
|
mutationFn: () => LinkApi.deleteLink(linkSn),
|
||||||
|
onSuccess: () => {
|
||||||
|
alert('삭제되었습니다.', router.back);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutateAsync: updateLinkResultCode } = useMutation({
|
||||||
|
mutationFn: () => LinkApi.updateLinkResultCode(linkSn),
|
||||||
|
onSuccess: () => {
|
||||||
|
alert('대기로 전환되었습니다.');
|
||||||
|
refetch();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchUserInfo = async () => {
|
||||||
|
const userData = await getUserInfo();
|
||||||
|
return userData.userName;
|
||||||
|
};
|
||||||
|
|
||||||
|
const form = useForm<LinkFormData>({
|
||||||
|
resolver: zodResolver(linkFormSchema),
|
||||||
|
defaultValues: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const applyFormData = async () => {
|
||||||
|
if (linkInfo) {
|
||||||
|
let newData = { ...linkInfo };
|
||||||
|
|
||||||
|
if (linkInfo.mdfr) {
|
||||||
|
const userName = await fetchUserInfo();
|
||||||
|
newData = {
|
||||||
|
...linkInfo,
|
||||||
|
mdfr: userName,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
form.reset(newData);
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
applyFormData();
|
||||||
|
}, [linkInfo]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="card card-border !mb-0 !mt-6">
|
||||||
|
<div className="card-header">
|
||||||
|
<h3 className="text-2xl font-medium text-label">연계정보 수정</h3>
|
||||||
|
</div>
|
||||||
|
<LinkForm
|
||||||
|
form={form}
|
||||||
|
linkSn={linkSn}
|
||||||
|
updateLinkInfo={updateLinkInfo}
|
||||||
|
deleteLink={deleteLink}
|
||||||
|
updateLinkResultCode={updateLinkResultCode}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { ModifyLinkPage };
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { LinkApi, type LinkFormData, linkFormSchema } from "@/entities/link";
|
||||||
|
import LinkForm from "./link-form";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { AUTH_USER_ID } from "@/shared/config";
|
||||||
|
import { getCookie } from "@common/components";
|
||||||
|
|
||||||
|
function RegistLinkPage() {
|
||||||
|
const { setMessage: alert } = useAlertStore((state) => state);
|
||||||
|
const router = useRouter();
|
||||||
|
const userUniqId = getCookie(AUTH_USER_ID) ?? "";
|
||||||
|
|
||||||
|
const form = useForm<LinkFormData>({
|
||||||
|
resolver: zodResolver(linkFormSchema),
|
||||||
|
defaultValues: {
|
||||||
|
link_nm: '',
|
||||||
|
link_url: '',
|
||||||
|
link_srvr_ip: '',
|
||||||
|
link_db_cd: '',
|
||||||
|
link_key: '',
|
||||||
|
link_id: '',
|
||||||
|
link_pswd: '',
|
||||||
|
srvc_usg: '',
|
||||||
|
link_batch_time: 5,
|
||||||
|
link_clsf_cd: 'none',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutate: insertLinkInfo } = useMutation({
|
||||||
|
mutationFn: (data: LinkFormData) => LinkApi.insertLinkInfo(data),
|
||||||
|
onSuccess: () => {
|
||||||
|
alert('저장되었습니다.');
|
||||||
|
router.push('/link');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleInsertLink = (data: LinkFormData) => {
|
||||||
|
const insertData = {
|
||||||
|
...data,
|
||||||
|
link_clsf_cd: data.link_clsf_cd !== 'none' ? data.link_clsf_cd : null,
|
||||||
|
next_link_excn_dt: dayjs().add(Number(data.link_batch_time), 'minute').format('YYYY-MM-DD HH:mm:ss'),
|
||||||
|
mdfr: userUniqId,
|
||||||
|
};
|
||||||
|
|
||||||
|
insertLinkInfo(insertData);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="card card-border !mb-0 !mt-6">
|
||||||
|
<div className="card-header">
|
||||||
|
<h3 className="text-2xl font-medium text-label">연계 등록</h3>
|
||||||
|
</div>
|
||||||
|
<LinkForm form={form} handleInsertLink={handleInsertLink} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { RegistLinkPage };
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { LinkLogPage } from './ui/link-log-page';
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import dayjs from "dayjs";
|
||||||
|
import { createColumnHelper } from "@tanstack/react-table";
|
||||||
|
import { type LinkLog } from "@/entities/link";
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<LinkLog>();
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
columnHelper.accessor('rownum', {
|
||||||
|
id: 'rownum',
|
||||||
|
header: 'NO',
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('link_sn', {
|
||||||
|
id: 'link_sn',
|
||||||
|
header: '연계일련번호',
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('link_nm', {
|
||||||
|
id: 'link_nm',
|
||||||
|
header: '연계 명',
|
||||||
|
}),
|
||||||
|
columnHelper.display({
|
||||||
|
id: 'link_rslt_cd',
|
||||||
|
header: '연계실행결과',
|
||||||
|
cell: (info) => {
|
||||||
|
const isSucc = info.row.original.link_rslt_cd.trim() === 'SUCC';
|
||||||
|
return <span className={`${isSucc ? 'text-[rgb(53,162,235)]' : 'text-[rgb(255,99,132)]'}`}>{isSucc ? '성공' : '실패'}</span>
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('link_err_cd', {
|
||||||
|
id: 'link_err_cd',
|
||||||
|
header: '연계오류코드',
|
||||||
|
cell: (info) => (info.getValue() ? info.getValue() : '-'),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('err_cn', {
|
||||||
|
id: 'err_cn',
|
||||||
|
header: '연계오류메세지',
|
||||||
|
cell: (info) => (
|
||||||
|
<div
|
||||||
|
className="max-w-[500px] truncate"
|
||||||
|
title={info.getValue() ?? ''}
|
||||||
|
>
|
||||||
|
{info.getValue() || '-'}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('log_crt_dt', {
|
||||||
|
id: 'log_crt_dt',
|
||||||
|
header: '연계실행일자',
|
||||||
|
cell: (info) => {
|
||||||
|
const date = dayjs(info.getValue());
|
||||||
|
return date.format('YYYY-MM-DD HH:mm');
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
export { columns };
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import "@common/assets/styles/grid.css";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Button, Input, Label, DatePicker, Select, SelectTrigger, SelectContent, SelectItem, SelectValue, LoadingWithAnimation } from "@common/components";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { getCoreRowModel, type PaginationState, type RowSelectionState, } from "@tanstack/react-table";
|
||||||
|
import { LinkApi, type LinkSearchParam } from "@/entities/link";
|
||||||
|
import { columns } from "../model/table-columns";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||||
|
import { useDataTable } from "@/shared/lib";
|
||||||
|
|
||||||
|
function LinkLogPage() {
|
||||||
|
const { setMessage: alert } = useAlertStore((state) => state);
|
||||||
|
|
||||||
|
const [linkSn, setLinkSn] = useState<string>('');
|
||||||
|
const [linkNm, setLinkNm] = useState<string>('');
|
||||||
|
const [isSucc, setIsSucc] = useState<string>('ALL');
|
||||||
|
const [startDate, setStartDate] = useState<string>(dayjs().subtract(7, "day").format('YYYY-MM-DD'));
|
||||||
|
const [endDate, setEndDate] = useState<string>(dayjs().format('YYYY-MM-DD'));
|
||||||
|
|
||||||
|
const [linkSearchParam, setLinkSearchParam] = useState<LinkSearchParam>({
|
||||||
|
linkSn: '',
|
||||||
|
linkNm: '',
|
||||||
|
isSucc: '',
|
||||||
|
startDate: '',
|
||||||
|
endDate: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const [pagination, setPagination] = useState<PaginationState>({
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||||
|
|
||||||
|
const query = useQuery({
|
||||||
|
queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkList`, linkSearchParam, pagination],
|
||||||
|
queryFn: () =>
|
||||||
|
LinkApi.getLinkLogList({
|
||||||
|
...linkSearchParam,
|
||||||
|
page: pagination.pageIndex,
|
||||||
|
size: pagination.pageSize,
|
||||||
|
isSucc,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { tableComponent } = useDataTable({
|
||||||
|
query,
|
||||||
|
tableOptions: {
|
||||||
|
columns,
|
||||||
|
state: {
|
||||||
|
pagination,
|
||||||
|
rowSelection,
|
||||||
|
},
|
||||||
|
manualPagination: true,
|
||||||
|
enableMultiRowSelection: true,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
onPaginationChange: setPagination,
|
||||||
|
onRowSelectionChange: setRowSelection,
|
||||||
|
},
|
||||||
|
dataTableOptions: {
|
||||||
|
viewPagination: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleResetSearchParam = () => {
|
||||||
|
setIsSucc('ALL');
|
||||||
|
setLinkNm('');
|
||||||
|
setLinkSn('');
|
||||||
|
setStartDate(dayjs().subtract(7, "day").format('YYYY-MM-DD'));
|
||||||
|
setEndDate(dayjs().format('YYYY-MM-DD'));
|
||||||
|
setLinkSearchParam({
|
||||||
|
...linkSearchParam,
|
||||||
|
linkSn: '',
|
||||||
|
linkNm: '',
|
||||||
|
isSucc: 'ALL',
|
||||||
|
startDate: dayjs().subtract(7, "day").format('YYYY-MM-DD'),
|
||||||
|
endDate: dayjs().format('YYYY-MM-DD'),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e?: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
e?.preventDefault();
|
||||||
|
|
||||||
|
if (startDate && endDate) {
|
||||||
|
|
||||||
|
if (startDate > endDate) {
|
||||||
|
alert('종료일이 시작일보다 이전일 수 없습니다.');
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
setPagination({
|
||||||
|
...pagination,
|
||||||
|
pageIndex: 0,
|
||||||
|
});
|
||||||
|
setLinkSearchParam({
|
||||||
|
...linkSearchParam,
|
||||||
|
linkSn,
|
||||||
|
linkNm,
|
||||||
|
isSucc,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alert('조회 날짜를 선택해주세요.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="card card-border !mt-6">
|
||||||
|
<div className="card card-border">
|
||||||
|
<form className="flex items-center gap-2 p-2" onSubmit={handleSubmit}>
|
||||||
|
<Label htmlFor="liSnSearchInput" className="mr-2 text-label">
|
||||||
|
일련번호
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem] mr-4"
|
||||||
|
id="liSnSearchInput"
|
||||||
|
value={linkSn}
|
||||||
|
onChange={(e) => setLinkSn(e.target.value)}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
<Label htmlFor="liNmSearchInput" className="mr-2 text-label">
|
||||||
|
연계 명
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem] mr-4"
|
||||||
|
id="liNmSearchInput"
|
||||||
|
value={linkNm}
|
||||||
|
onChange={(e) => setLinkNm(e.target.value)}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
<Label htmlFor="liResultCode" className="mr-2 text-label">
|
||||||
|
실행결과
|
||||||
|
</Label>
|
||||||
|
<Select onValueChange={(e) => setIsSucc(e)} value={isSucc}>
|
||||||
|
<SelectTrigger id="liResultCode" className="w-[10rem] mr-4">
|
||||||
|
<SelectValue placeholder="전체"></SelectValue>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="ALL">전체</SelectItem>
|
||||||
|
<SelectItem value="SUCC">성공</SelectItem>
|
||||||
|
<SelectItem value="FAIL">실패</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Label htmlFor="sDateSearchInput" className="mx-2 text-label">
|
||||||
|
시작일
|
||||||
|
</Label>
|
||||||
|
<DatePicker id="sDateSearchInput" mode={"single"} selected={dayjs(startDate).toDate()} onSelect={(e) => setStartDate(dayjs(e).format('YYYY-MM-DD'))} />
|
||||||
|
<Label htmlFor="eDateSearchInput" className="mx-2 text-label">
|
||||||
|
종료일
|
||||||
|
</Label>
|
||||||
|
<DatePicker id="eDateSearchInput" mode={"single"} className="mr-2" selected={dayjs(endDate).toDate()} onSelect={(e) => setEndDate(dayjs(e).format('YYYY-MM-DD'))} />
|
||||||
|
<Button color="green" className="h-[3.2rem]" type="button" onClick={handleResetSearchParam}>
|
||||||
|
초기화
|
||||||
|
</Button>
|
||||||
|
<Button className="h-[3.2rem]" type="submit">
|
||||||
|
조회
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div className="card card-border flex flex-col h-[calc(100vh-28rem)]">
|
||||||
|
<div className="card-header">
|
||||||
|
<h3 className="text-2xl font-medium text-label">로그 목록 (검색 조건에 따른 건수 : {query.data?.totalElements.toLocaleString('ko-KR') ?? 0} )</h3>
|
||||||
|
</div>
|
||||||
|
<div className="card !m-[1.2rem] overflow-auto h-full !p-0">
|
||||||
|
{query.isLoading ?
|
||||||
|
<div className="[data-theme='black']:bg-transparent rounded-lg p-10 h-full flex justify-center items-center">
|
||||||
|
<div className="relative size-[5rem]">
|
||||||
|
<LoadingWithAnimation color="primary" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
:
|
||||||
|
<div>{tableComponent}</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
export { LinkLogPage };
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { LinkParamsPage } from './ui/link-params-page';
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { createColumnHelper } from "@tanstack/react-table";
|
||||||
|
import { type Link } from "@/entities/link";
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<Link>();
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
columnHelper.accessor((_, index) => index + 1, {
|
||||||
|
id: 'no',
|
||||||
|
header: 'NO',
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('link_sn', {
|
||||||
|
id: 'link_sn',
|
||||||
|
header: '연계 일련번호',
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('link_nm', {
|
||||||
|
id: 'link_nm',
|
||||||
|
header: '연계 명',
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('srvc_usg', {
|
||||||
|
id: 'srvc_usg',
|
||||||
|
header: '서비스용도',
|
||||||
|
cell: (info) => (info.getValue() ? info.getValue() : '-'),
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
export { columns };
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import { createColumnHelper } from "@tanstack/react-table";
|
||||||
|
import { type LinkVrbl } from "@/entities/link";
|
||||||
|
import { Checkbox } from "@common/components";
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<LinkVrbl>();
|
||||||
|
|
||||||
|
function vrblColumns({
|
||||||
|
selectedVrblIds,
|
||||||
|
setSelectedVrblIds,
|
||||||
|
}: {
|
||||||
|
selectedVrblIds: string[];
|
||||||
|
setSelectedVrblIds: React.Dispatch<React.SetStateAction<string[]>>;
|
||||||
|
}) {
|
||||||
|
return [
|
||||||
|
columnHelper.display({
|
||||||
|
id: "select",
|
||||||
|
header: ({ table }) => {
|
||||||
|
const allRows = table.getRowModel().rows;
|
||||||
|
const allSelected =
|
||||||
|
allRows.length > 0 &&
|
||||||
|
allRows.every((row) =>
|
||||||
|
selectedVrblIds.includes(row.original.link_vrbl_sn)
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center items-center">
|
||||||
|
<Checkbox
|
||||||
|
className="bg-white"
|
||||||
|
checked={
|
||||||
|
allSelected
|
||||||
|
? true
|
||||||
|
: false
|
||||||
|
}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
if (checked) {
|
||||||
|
setSelectedVrblIds(allRows.map((row) => row.original.link_vrbl_sn));
|
||||||
|
} else {
|
||||||
|
setSelectedVrblIds([]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const id = row.original.link_vrbl_sn;
|
||||||
|
const checked = selectedVrblIds.includes(id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center items-center">
|
||||||
|
<Checkbox
|
||||||
|
checked={checked}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
setSelectedVrblIds((prev) =>
|
||||||
|
checked ? [...prev, id] : prev.filter((x) => x !== id)
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
columnHelper.accessor("link_vrbl_sn", {
|
||||||
|
id: "link_vrbl_sn",
|
||||||
|
header: "연계 변수 일련번호",
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("link_vrbl_nm", {
|
||||||
|
id: "link_vrbl_nm",
|
||||||
|
header: "변수 명",
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("link_vrbl_cd", {
|
||||||
|
id: "link_vrbl_cd",
|
||||||
|
header: "변수코드",
|
||||||
|
cell: (info) => (info.getValue() ? info.getValue() : '-'),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("link_vrbl_vl", {
|
||||||
|
id: "link_vrbl_vl",
|
||||||
|
header: "변수 값",
|
||||||
|
cell: (info) => (info.getValue() === 'now' ? '요청일자' : info.getValue() === 'nowMonth' ? '요청월' : info.getValue()),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("link_vrbl_expln", {
|
||||||
|
id: "link_vrbl_expln",
|
||||||
|
header: "설명",
|
||||||
|
cell: (info) => (
|
||||||
|
<div
|
||||||
|
className="max-w-72 truncate"
|
||||||
|
title={info.getValue() ?? ''}
|
||||||
|
>
|
||||||
|
{info.getValue() || '-'}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export { vrblColumns };
|
||||||
@@ -0,0 +1,351 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogTrigger,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogFooter,
|
||||||
|
DialogClose,
|
||||||
|
Button,
|
||||||
|
Input,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
Checkbox,
|
||||||
|
RadioGroup,
|
||||||
|
RadioGroupItem,
|
||||||
|
Label
|
||||||
|
} from "@common/components";
|
||||||
|
import { FormItem, FormControl, FormField, Form } from "@/shared/ui/form";
|
||||||
|
import { type LinkVrblFormData } from "@/entities/link";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import type { UseFormReturn } from "react-hook-form";
|
||||||
|
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
||||||
|
|
||||||
|
type LinkParamsDialogProps = {
|
||||||
|
selectedLink: {
|
||||||
|
linkSn: string;
|
||||||
|
linkNm: string;
|
||||||
|
};
|
||||||
|
form: UseFormReturn<LinkVrblFormData>;
|
||||||
|
insertLinkVrbl: (data: LinkVrblFormData) => void;
|
||||||
|
dialogOpen: boolean;
|
||||||
|
onOpenChange: (val: boolean) => void;
|
||||||
|
updateLinkVrbl: (data: LinkVrblFormData) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function LinkParamsDialog({ selectedLink, form, insertLinkVrbl, dialogOpen, onOpenChange, updateLinkVrbl }: LinkParamsDialogProps) {
|
||||||
|
|
||||||
|
const isModifyMode = !!form.getValues('link_vrbl_sn');
|
||||||
|
|
||||||
|
const { confirm } = useConfirm();
|
||||||
|
const [onConfirm, setOnConfirm] = useState<boolean>(false);
|
||||||
|
const [isToday, setIsToday] = useState<boolean>(false);
|
||||||
|
const [checkNowMonth, setCheckNowMonth] = useState<string>('now');
|
||||||
|
|
||||||
|
const handleConfirm = async (callback: () => Promise<void>) => {
|
||||||
|
setOnConfirm(true);
|
||||||
|
await callback();
|
||||||
|
setOnConfirm(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
function replaceEmptyWithNull<T extends Record<string, unknown>>(data: T): T {
|
||||||
|
const result = {} as T;
|
||||||
|
for (const [key, value] of Object.entries(data)) {
|
||||||
|
(result as Record<string, unknown>)[key] = value === '' ? null : value;
|
||||||
|
}
|
||||||
|
return result as T;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (data: LinkVrblFormData) => {
|
||||||
|
const confirmMsg = isModifyMode ? '수정 하시겠습니까?' : '저장 하시겠습니까?';
|
||||||
|
|
||||||
|
await handleConfirm(async () => {
|
||||||
|
const cleanData = replaceEmptyWithNull(data);
|
||||||
|
|
||||||
|
if (await confirm(confirmMsg)) {
|
||||||
|
if (isModifyMode) {
|
||||||
|
updateLinkVrbl(isToday ? { ...cleanData, link_vrbl_vl: checkNowMonth } : cleanData);
|
||||||
|
} else {
|
||||||
|
insertLinkVrbl(isToday ? { ...cleanData, link_vrbl_vl: checkNowMonth } : cleanData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCheckTodayValue = (check: boolean | string) => {
|
||||||
|
if (check) {
|
||||||
|
setIsToday(true);
|
||||||
|
form.setValue('link_vrbl_vl', (checkNowMonth === 'now' ? '요청일자' : '요청월'));
|
||||||
|
} else {
|
||||||
|
setIsToday(false);
|
||||||
|
form.setValue('link_vrbl_vl', '');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!dialogOpen) {
|
||||||
|
setIsToday(false);
|
||||||
|
setCheckNowMonth('now');
|
||||||
|
form.reset({
|
||||||
|
link_vrbl_nm: '',
|
||||||
|
link_vrbl_expln: '',
|
||||||
|
link_vrbl_cd: '',
|
||||||
|
link_vrbl_vl: '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isModifyMode && (form.getValues('link_vrbl_vl') === 'now' || form.getValues('link_vrbl_vl') === 'nowMonth')) {
|
||||||
|
setIsToday(true);
|
||||||
|
setCheckNowMonth(form.getValues('link_vrbl_vl'));
|
||||||
|
}
|
||||||
|
|
||||||
|
}, [dialogOpen]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={dialogOpen}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
onOpenChange(open);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button color="green" className="h-[3.2rem]" onClick={() => onOpenChange(true)}>행 추가</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-[55rem]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
{selectedLink.linkNm} ({selectedLink.linkSn})
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(handleSubmit)}>
|
||||||
|
<div className="card">
|
||||||
|
<Table variant="secondary">
|
||||||
|
<colgroup>
|
||||||
|
<col width="30%" />
|
||||||
|
<col width="70%" />
|
||||||
|
</colgroup>
|
||||||
|
<TableBody>
|
||||||
|
{isModifyMode &&
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>변수 일련번호</TableHead>
|
||||||
|
<TableCell>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="link_vrbl_sn"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
value={field.value ?? ""}
|
||||||
|
className="!text-[1.3rem]"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
}
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>* 변수 명</TableHead>
|
||||||
|
<TableCell>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="link_vrbl_nm"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
value={field.value ?? ""}
|
||||||
|
className="!text-[1.3rem]"
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
{form.formState.errors.link_vrbl_nm && (
|
||||||
|
<p className="text-red-500 text-base pt-2">
|
||||||
|
{form.formState.errors.link_vrbl_nm.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>변수 설명</TableHead>
|
||||||
|
<TableCell>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="link_vrbl_expln"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
value={field.value ?? ""}
|
||||||
|
className="!text-[1.3rem]"
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
{form.formState.errors.link_vrbl_expln && (
|
||||||
|
<p className="text-red-500 text-base pt-2">
|
||||||
|
{form.formState.errors.link_vrbl_expln.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>변수 코드</TableHead>
|
||||||
|
<TableCell>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="link_vrbl_cd"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
value={field.value ?? ""}
|
||||||
|
className="!text-[1.3rem]"
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
{form.formState.errors.link_vrbl_cd && (
|
||||||
|
<p className="text-red-500 text-base pt-2">
|
||||||
|
{form.formState.errors.link_vrbl_cd.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>* 변수 값</TableHead>
|
||||||
|
<TableCell>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="link_vrbl_vl"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<div className="flex items-center gap-x-2 mb-4">
|
||||||
|
<Checkbox id="isToday" className="!border-gray-600" checked={isToday} onCheckedChange={(check) => handleCheckTodayValue(check)} />
|
||||||
|
<label htmlFor="isToday" className="text-gray-600">변수 값이 오늘 날짜 일시 체크해 주세요.</label>
|
||||||
|
</div>
|
||||||
|
{isToday &&
|
||||||
|
<div className="flex items-center gap-x-2 mb-4">
|
||||||
|
<RadioGroup
|
||||||
|
name="checkNowMonth"
|
||||||
|
onValueChange={(value) => {
|
||||||
|
setCheckNowMonth(value);
|
||||||
|
form.setValue('link_vrbl_vl', (value === 'now' ? '요청일자' : '요청월'));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<RadioGroupItem value="now" id="now" checked={checkNowMonth === 'now'} />
|
||||||
|
<Label htmlFor="now">요청일자(YYYY-MM-DD)</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<RadioGroupItem value="nowMonth" id="nowMonth" checked={checkNowMonth === 'nowMonth'} />
|
||||||
|
<Label htmlFor="nowMonth">요청월(YYYY-MM)</Label>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
value={field.value ? (field.value === 'now' ? '요청일자' : field.value === 'nowMonth' ? '요청월' : field.value) : ''}
|
||||||
|
className={`!text-[1.3rem] ${isToday ? 'bg-gray-100' : ''}`}
|
||||||
|
autoComplete="off"
|
||||||
|
disabled={isToday}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
{form.formState.errors.link_vrbl_vl && (
|
||||||
|
<p className="text-red-500 text-base pt-2">
|
||||||
|
{form.formState.errors.link_vrbl_vl.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
{isModifyMode &&
|
||||||
|
<>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>수정자</TableHead>
|
||||||
|
<TableCell>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="mdfr"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
value={field.value ?? ""}
|
||||||
|
className="!text-[1.3rem]"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>수정일시</TableHead>
|
||||||
|
<TableCell>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="mdfcn_dt"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
value={field.value ? dayjs(field.value).format('YYYY-MM-DD') : ""}
|
||||||
|
className="!text-[1.3rem]"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button type="submit">저장</Button>
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button color="white">취소</Button>
|
||||||
|
</DialogClose>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
<ConfirmDialog isOpen={onConfirm} />
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
export { LinkParamsDialog };
|
||||||
@@ -0,0 +1,405 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useMemo } from "react";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
DataTable,
|
||||||
|
Input,
|
||||||
|
Label,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
LoadingWithAnimation,
|
||||||
|
} from "@common/components";
|
||||||
|
import { getCoreRowModel, useReactTable, type PaginationState, type RowSelectionState, } from "@tanstack/react-table";
|
||||||
|
import { LinkApi, type Link, type LinkSearchParam, type LinkVrblFormData, linkVrblFormSchema } from "@/entities/link";
|
||||||
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { columns } from "../model/table-columns";
|
||||||
|
import { vrblColumns } from "../model/vrbl-table-columns";
|
||||||
|
import { type LinkVrbl } from "@/entities/link";
|
||||||
|
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||||
|
import { LinkParamsDialog } from "./link-params-dialog";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
|
||||||
|
import { AUTH_USER_ID } from "@/shared/config";
|
||||||
|
import { getCookie } from "@common/components";
|
||||||
|
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||||
|
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
||||||
|
import { useDataTable } from "@/shared/lib";
|
||||||
|
|
||||||
|
function LinkParamsPage() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const [linkSn, setLinkSn] = useState<string>('');
|
||||||
|
const [linkNm, setLinkNm] = useState<string>('');
|
||||||
|
|
||||||
|
const [linkSearchParam, setLinkSearchParam] = useState<LinkSearchParam>({
|
||||||
|
linkSn: '',
|
||||||
|
linkNm: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const [selectedLink, setSelectedLink] = useState<{
|
||||||
|
linkSn: string;
|
||||||
|
linkNm: string;
|
||||||
|
}>({
|
||||||
|
linkSn: '',
|
||||||
|
linkNm: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||||
|
|
||||||
|
const [pagination, setPagination] = useState<PaginationState>({
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { confirm } = useConfirm();
|
||||||
|
|
||||||
|
const [selectedVrblIds, setSelectedVrblIds] = useState<string[]>([]);
|
||||||
|
const [isDBdata, setIsDBData] = useState<boolean>(false);
|
||||||
|
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
|
||||||
|
const [onConfirm, setOnConfirm] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const userUniqId = getCookie(AUTH_USER_ID) ?? "";
|
||||||
|
const { setMessage: alert } = useAlertStore((state) => state);
|
||||||
|
|
||||||
|
const dbIdList = ['link0001', 'link0002'];
|
||||||
|
|
||||||
|
// Link 목록 쿼리
|
||||||
|
const query = useQuery({
|
||||||
|
queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkList`, linkSearchParam],
|
||||||
|
queryFn: () => LinkApi.getLinkList({
|
||||||
|
...linkSearchParam,
|
||||||
|
page: pagination.pageIndex,
|
||||||
|
size: pagination.pageSize,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
// LinkVrbl 목록 쿼리 - 별도 관리
|
||||||
|
const linkVrblQuery = useQuery({
|
||||||
|
queryKey: ['linkVrblList', selectedLink.linkSn],
|
||||||
|
queryFn: () => LinkApi.getLinkVrblList(selectedLink.linkSn),
|
||||||
|
enabled: !!selectedLink.linkSn && !dbIdList.includes(selectedLink.linkSn),
|
||||||
|
staleTime: 0, // 항상 최신 데이터를 가져오도록 설정
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleResetSearchParam = () => {
|
||||||
|
setSelectedLink({
|
||||||
|
linkSn: '',
|
||||||
|
linkNm: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
setLinkSn('');
|
||||||
|
setLinkNm('');
|
||||||
|
setLinkSearchParam({
|
||||||
|
...linkSearchParam,
|
||||||
|
linkSn: '',
|
||||||
|
linkNm: ''
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirm = async (callback: () => Promise<void>) => {
|
||||||
|
setOnConfirm(true);
|
||||||
|
await callback();
|
||||||
|
setOnConfirm(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { mutateAsync: insertLinkVrbl } = useMutation({
|
||||||
|
mutationFn: async (data: LinkVrblFormData) =>
|
||||||
|
await LinkApi.insertLinkVrbl({
|
||||||
|
...data,
|
||||||
|
link_sn: selectedLink.linkSn,
|
||||||
|
mdfr: userUniqId,
|
||||||
|
}),
|
||||||
|
onSuccess: async () => {
|
||||||
|
// 쿼리 무효화를 통한 최신 데이터 가져오기
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: ['linkVrblList', selectedLink.linkSn]
|
||||||
|
});
|
||||||
|
setDialogOpen(false);
|
||||||
|
alert('저장되었습니다.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutateAsync: updateLinkVrbl } = useMutation({
|
||||||
|
mutationFn: async (data: LinkVrblFormData) => await LinkApi.updateLinkVrblInfo({
|
||||||
|
...data,
|
||||||
|
link_sn: selectedLink.linkSn,
|
||||||
|
mdfr: userUniqId,
|
||||||
|
}),
|
||||||
|
onSuccess: async () => {
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: ['linkVrblList', selectedLink.linkSn]
|
||||||
|
});
|
||||||
|
setDialogOpen(false);
|
||||||
|
alert('저장되었습니다.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutateAsync: deleteLinkVrbl } = useMutation({
|
||||||
|
mutationFn: async (data: string[]) => await LinkApi.deleteLinkVrbl(data),
|
||||||
|
onSuccess: async () => {
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: ['linkVrblList', selectedLink.linkSn]
|
||||||
|
});
|
||||||
|
setSelectedVrblIds([]);
|
||||||
|
alert('삭제되었습니다.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// linkVrblQuery의 데이터를 사용
|
||||||
|
const tableData = useMemo(() => {
|
||||||
|
return linkVrblQuery.data?.content ?? [];
|
||||||
|
}, [linkVrblQuery.data]);
|
||||||
|
|
||||||
|
const vrblCols = useMemo(
|
||||||
|
() =>
|
||||||
|
vrblColumns({
|
||||||
|
selectedVrblIds,
|
||||||
|
setSelectedVrblIds,
|
||||||
|
}),
|
||||||
|
[selectedVrblIds]
|
||||||
|
);
|
||||||
|
|
||||||
|
const vrblTable = useReactTable({
|
||||||
|
data: tableData,
|
||||||
|
columns: vrblCols,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = useForm<LinkVrblFormData>({
|
||||||
|
resolver: zodResolver(linkVrblFormSchema),
|
||||||
|
defaultValues: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleRowClick = async (link: Link) => {
|
||||||
|
setSelectedLink({
|
||||||
|
linkSn: link.link_sn,
|
||||||
|
linkNm: link.link_nm,
|
||||||
|
});
|
||||||
|
|
||||||
|
setSelectedVrblIds([]);
|
||||||
|
|
||||||
|
if (dbIdList.includes(link.link_clsf_cd)) {
|
||||||
|
setIsDBData(true);
|
||||||
|
} else {
|
||||||
|
setIsDBData(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
setSelectedLink({
|
||||||
|
linkSn: '',
|
||||||
|
linkNm: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
setLinkSearchParam({
|
||||||
|
...linkSearchParam,
|
||||||
|
linkSn,
|
||||||
|
linkNm
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchUserInfo = async () => {
|
||||||
|
const userData = await getUserInfo();
|
||||||
|
return userData.userName;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteParam = async () => {
|
||||||
|
if (selectedVrblIds.length === 0) {
|
||||||
|
alert('삭제할 파라미터를 선택해 주세요.');
|
||||||
|
} else {
|
||||||
|
await handleConfirm(async () => {
|
||||||
|
if (await confirm('선택한 파라미터 정보를 삭제하시겠습니까?')) {
|
||||||
|
deleteLinkVrbl(selectedVrblIds);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleVrblRowClick = async (data: LinkVrbl) => {
|
||||||
|
const linkVrblInfo = await LinkApi.getLinkVrblInfo(data.link_vrbl_sn);
|
||||||
|
|
||||||
|
if (linkVrblInfo) {
|
||||||
|
let newData = { ...linkVrblInfo };
|
||||||
|
|
||||||
|
if (linkVrblInfo.mdfr) {
|
||||||
|
const userName = await fetchUserInfo();
|
||||||
|
newData = {
|
||||||
|
...linkVrblInfo,
|
||||||
|
mdfr: userName,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
form.reset(newData);
|
||||||
|
}
|
||||||
|
setDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { tableComponent } = useDataTable({
|
||||||
|
query,
|
||||||
|
tableOptions: {
|
||||||
|
columns,
|
||||||
|
state: {
|
||||||
|
pagination,
|
||||||
|
rowSelection,
|
||||||
|
},
|
||||||
|
manualPagination: true,
|
||||||
|
enableMultiRowSelection: true,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
onPaginationChange: setPagination,
|
||||||
|
onRowSelectionChange: setRowSelection,
|
||||||
|
},
|
||||||
|
dataTableOptions: {
|
||||||
|
onRowClick: handleRowClick,
|
||||||
|
viewPagination: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// LinkVrbl 데이터 표시 로직
|
||||||
|
const renderLinkVrblContent = () => {
|
||||||
|
if (isDBdata) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center mt-14 text-gray-500 text-3xl flex-1">
|
||||||
|
변수를 지원하지 않는 연계 데이터입니다.
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selectedLink.linkSn) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center mt-14 text-gray-500 text-3xl flex-1">
|
||||||
|
연계 데이터를 선택해주세요.
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (linkVrblQuery.isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="[data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center h-full">
|
||||||
|
<div className="relative size-[5rem]">
|
||||||
|
<LoadingWithAnimation color="primary" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="mb-2 mt-14 flex justify-end items-center flex-none">
|
||||||
|
{tableData.length > 0 && (
|
||||||
|
<Button color="red" className="h-[3.2rem] mr-2" type="button" onClick={handleDeleteParam}>
|
||||||
|
행 삭제
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<LinkParamsDialog
|
||||||
|
selectedLink={selectedLink}
|
||||||
|
form={form}
|
||||||
|
insertLinkVrbl={insertLinkVrbl}
|
||||||
|
dialogOpen={dialogOpen}
|
||||||
|
onOpenChange={setDialogOpen}
|
||||||
|
updateLinkVrbl={updateLinkVrbl}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{tableData.length > 0 ? (
|
||||||
|
<div className="overflow-auto flex-1 min-h-[300px]">
|
||||||
|
<DataTable table={vrblTable} onRowClick={handleVrblRowClick} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-center h-20 text-gray-500 text-3xl">
|
||||||
|
등록된 연계 변수가 없습니다.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="card card-border !mt-6">
|
||||||
|
<div className="card card-border">
|
||||||
|
<form className="flex items-center gap-2 p-2" onSubmit={handleSubmit}>
|
||||||
|
<Label htmlFor="liSnSearchInput" className="mr-2 text-label">
|
||||||
|
일련번호
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem] mr-4"
|
||||||
|
id="liSnSearchInput"
|
||||||
|
value={linkSn}
|
||||||
|
onChange={(e) => setLinkSn(e.target.value)}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
<Label htmlFor="liNmSearchInput" className="mr-2 text-label">
|
||||||
|
연계 명
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem] mr-2"
|
||||||
|
id="liNmSearchInput"
|
||||||
|
value={linkNm}
|
||||||
|
onChange={(e) => setLinkNm(e.target.value)}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
<Button color="green" className="h-[3.2rem]" type="button" onClick={handleResetSearchParam}>
|
||||||
|
초기화
|
||||||
|
</Button>
|
||||||
|
<Button className="h-[3.2rem]" type="submit">
|
||||||
|
조회
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div className="card !mb-0 flex h-[calc(100vh-28rem)] gap-5 !p-0">
|
||||||
|
<div className="card card-border w-1/2 !mx-0 flex flex-col">
|
||||||
|
<div className="card-header">
|
||||||
|
<h3 className="text-2xl font-medium text-label">연계 목록</h3>
|
||||||
|
</div>
|
||||||
|
<div className="card !m-[1.2rem] overflow-auto !p-0 h-full">
|
||||||
|
{!query.isLoading ? (
|
||||||
|
<div>{tableComponent}</div>
|
||||||
|
) : (
|
||||||
|
<div className="[data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center h-full">
|
||||||
|
<div className="relative size-[5rem]">
|
||||||
|
<LoadingWithAnimation color="primary" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="card card-border w-1/2 !mx-0 flex flex-col">
|
||||||
|
<div className="card-header">
|
||||||
|
<h3 className="text-2xl font-medium text-label">연계 변수 정보</h3>
|
||||||
|
</div>
|
||||||
|
<div className="card !m-[1.2rem] !p-0 overflow-auto h-full flex flex-col">
|
||||||
|
<Table variant="secondary" className="flex-none">
|
||||||
|
<colgroup>
|
||||||
|
<col width="20%" />
|
||||||
|
<col width="80%" />
|
||||||
|
</colgroup>
|
||||||
|
<TableBody>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>연계 일련번호</TableHead>
|
||||||
|
<TableCell>
|
||||||
|
{selectedLink.linkSn ? selectedLink.linkSn : '-'}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>연계 명</TableHead>
|
||||||
|
<TableCell>
|
||||||
|
{selectedLink.linkNm ? selectedLink.linkNm : '-'}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
{renderLinkVrblContent()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ConfirmDialog isOpen={onConfirm} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { LinkParamsPage };
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { LinkStatsPage } from './ui/link-stats-page';
|
||||||
@@ -0,0 +1,378 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect, useRef } from "react";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import { LinkApi, type LinkStats, type LinkSearchParam } from "@/entities/link";
|
||||||
|
import { Button, Label, DatePicker, LoadingWithAnimation } from "@common/components";
|
||||||
|
import {
|
||||||
|
Chart as ChartJS,
|
||||||
|
CategoryScale,
|
||||||
|
LinearScale,
|
||||||
|
PointElement,
|
||||||
|
LineElement,
|
||||||
|
Tooltip,
|
||||||
|
Legend,
|
||||||
|
type ChartOptions,
|
||||||
|
type Chart,
|
||||||
|
} from "chart.js";
|
||||||
|
import { Line } from "react-chartjs-2";
|
||||||
|
import { getCoreRowModel, type PaginationState, type RowSelectionState, } from "@tanstack/react-table";
|
||||||
|
import { columns } from "@/pages/linkLog/model/table-columns";
|
||||||
|
import { useDataTable } from "@/shared/lib";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
interface LinkStatsListObject {
|
||||||
|
linkSuccStats: LinkStats[] | null;
|
||||||
|
linkFailStats: LinkStats[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function LinkStatsPage() {
|
||||||
|
|
||||||
|
const succChartRef = useRef<Chart<"line"> | null>(null);
|
||||||
|
const failChartRef = useRef<Chart<"line"> | null>(null);
|
||||||
|
|
||||||
|
const [startDate, setStartDate] = useState<string>(dayjs().subtract(7, "day").format('YYYY-MM-DD'));
|
||||||
|
const [endDate, setEndDate] = useState<string>(dayjs().format('YYYY-MM-DD'));
|
||||||
|
|
||||||
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||||
|
|
||||||
|
const [pagination, setPagination] = useState<PaginationState>({
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [linkStatsList, setLinkStatsList] = useState<LinkStatsListObject | null>(null);
|
||||||
|
const [logQueryParam, setLogQueryParam] = useState<{
|
||||||
|
isSucc: 'SUCC' | 'FAIL';
|
||||||
|
linkSn?: string;
|
||||||
|
linkNm?: string;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
const query = useQuery({
|
||||||
|
queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkList`, logQueryParam, pagination],
|
||||||
|
queryFn: () =>
|
||||||
|
LinkApi.getLinkLogList({
|
||||||
|
...logQueryParam,
|
||||||
|
page: pagination.pageIndex,
|
||||||
|
size: pagination.pageSize,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
}),
|
||||||
|
enabled: !!logQueryParam,
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const { tableComponent } = useDataTable({
|
||||||
|
query,
|
||||||
|
tableOptions: {
|
||||||
|
columns,
|
||||||
|
state: {
|
||||||
|
pagination,
|
||||||
|
rowSelection,
|
||||||
|
},
|
||||||
|
manualPagination: true,
|
||||||
|
enableMultiRowSelection: true,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
onPaginationChange: setPagination,
|
||||||
|
onRowSelectionChange: setRowSelection,
|
||||||
|
},
|
||||||
|
dataTableOptions: {
|
||||||
|
viewPagination: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const [linkSearchParam, setLinkSearchParam] = useState<LinkSearchParam>({
|
||||||
|
linkSn: '',
|
||||||
|
linkNm: '',
|
||||||
|
startDate: '',
|
||||||
|
endDate: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
ChartJS.register(
|
||||||
|
LineElement,
|
||||||
|
CategoryScale,
|
||||||
|
LinearScale,
|
||||||
|
PointElement,
|
||||||
|
Tooltip,
|
||||||
|
Legend
|
||||||
|
);
|
||||||
|
|
||||||
|
const succData = {
|
||||||
|
labels: linkStatsList?.linkSuccStats?.map((e) => e.link_nm),
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: '연계 성공',
|
||||||
|
data: linkStatsList?.linkSuccStats?.map((e) => e.count),
|
||||||
|
fill: false,
|
||||||
|
borderColor: 'rgb(53, 162, 235)',
|
||||||
|
backgroundColor: 'rgb(53, 162, 235)',
|
||||||
|
tension: 0.1,
|
||||||
|
pointRadius: 6,
|
||||||
|
pointHoverRadius: 8,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const succOptions: ChartOptions<'line'> = {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
position: 'top',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
min: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const failData = {
|
||||||
|
labels: linkStatsList?.linkFailStats?.map((e) => e.link_nm),
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: '연계 실패',
|
||||||
|
data: linkStatsList?.linkFailStats?.map((e) => e.count),
|
||||||
|
fill: false,
|
||||||
|
borderColor: 'rgb(255, 99, 132)',
|
||||||
|
backgroundColor: 'rgb(255, 99, 132)',
|
||||||
|
tension: 0.1,
|
||||||
|
pointRadius: 6,
|
||||||
|
pointHoverRadius: 8,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const failOptions: ChartOptions<'line'> = {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
position: 'top',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
min: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResetSearchParam = async () => {
|
||||||
|
setLinkSearchParam({
|
||||||
|
linkSn: '',
|
||||||
|
linkNm: '',
|
||||||
|
startDate: dayjs().subtract(7, "day").format('YYYY-MM-DD'),
|
||||||
|
endDate: dayjs().format('YYYY-MM-DD'),
|
||||||
|
});
|
||||||
|
|
||||||
|
setLogQueryParam({
|
||||||
|
isSucc: 'SUCC',
|
||||||
|
linkNm: '',
|
||||||
|
linkSn: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
setStartDate(dayjs().subtract(7, "day").format('YYYY-MM-DD'));
|
||||||
|
setEndDate(dayjs().format('YYYY-MM-DD'));
|
||||||
|
|
||||||
|
const data = await LinkApi.getLinkStatsList({
|
||||||
|
...linkSearchParam,
|
||||||
|
linkSn: '',
|
||||||
|
linkNm: '',
|
||||||
|
startDate: dayjs().subtract(7, "day").format('YYYY-MM-DD'),
|
||||||
|
endDate: dayjs().format('YYYY-MM-DD'),
|
||||||
|
});
|
||||||
|
setLinkStatsList(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e?: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
e?.preventDefault();
|
||||||
|
|
||||||
|
if (startDate && endDate) {
|
||||||
|
|
||||||
|
if (startDate > endDate) {
|
||||||
|
alert('종료일이 시작일보다 이전일 수 없습니다.');
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
const data = await LinkApi.getLinkStatsList({
|
||||||
|
...linkSearchParam,
|
||||||
|
startDate,
|
||||||
|
endDate
|
||||||
|
});
|
||||||
|
setLinkStatsList(data);
|
||||||
|
setLogQueryParam({
|
||||||
|
isSucc: 'SUCC',
|
||||||
|
linkNm: '',
|
||||||
|
linkSn: '',
|
||||||
|
});
|
||||||
|
setLinkSearchParam({
|
||||||
|
...linkSearchParam,
|
||||||
|
startDate,
|
||||||
|
endDate
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alert('조회 날짜를 선택해주세요.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSuccChartClick = async (event: React.MouseEvent<HTMLCanvasElement>) => {
|
||||||
|
if (!succChartRef.current) return;
|
||||||
|
|
||||||
|
const chart = succChartRef.current;
|
||||||
|
|
||||||
|
const points = chart.getElementsAtEventForMode(
|
||||||
|
event.nativeEvent,
|
||||||
|
"nearest",
|
||||||
|
{ intersect: true },
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
if (points[0] && linkStatsList?.linkSuccStats) {
|
||||||
|
const index = points[0].index;
|
||||||
|
setPagination({
|
||||||
|
...pagination,
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: 100,
|
||||||
|
});
|
||||||
|
setLogQueryParam({
|
||||||
|
isSucc: 'SUCC',
|
||||||
|
linkNm: linkStatsList.linkSuccStats[index]?.link_nm,
|
||||||
|
linkSn: linkStatsList.linkSuccStats[index]?.link_sn,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFailChartClick = async (event: React.MouseEvent<HTMLCanvasElement>) => {
|
||||||
|
if (!failChartRef.current) return;
|
||||||
|
|
||||||
|
const chart = failChartRef.current;
|
||||||
|
|
||||||
|
const points = chart.getElementsAtEventForMode(
|
||||||
|
event.nativeEvent,
|
||||||
|
"nearest",
|
||||||
|
{ intersect: true },
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
if (points[0] && linkStatsList?.linkFailStats) {
|
||||||
|
const index = points[0].index;
|
||||||
|
setPagination({
|
||||||
|
...pagination,
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: 100,
|
||||||
|
});
|
||||||
|
setLogQueryParam({
|
||||||
|
isSucc: 'FAIL',
|
||||||
|
linkNm: linkStatsList.linkFailStats[index]?.link_nm,
|
||||||
|
linkSn: linkStatsList.linkFailStats[index]?.link_sn,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmitToday = async () => {
|
||||||
|
const data = await LinkApi.getLinkStatsList({
|
||||||
|
...linkSearchParam,
|
||||||
|
startDate: dayjs().format('YYYY-MM-DD'),
|
||||||
|
endDate: dayjs().format('YYYY-MM-DD'),
|
||||||
|
});
|
||||||
|
setEndDate(dayjs().format('YYYY-MM-DD'));
|
||||||
|
setStartDate(dayjs().format('YYYY-MM-DD'));
|
||||||
|
setLinkStatsList(data);
|
||||||
|
setLinkSearchParam({
|
||||||
|
...linkSearchParam,
|
||||||
|
startDate: dayjs().format('YYYY-MM-DD'),
|
||||||
|
endDate: dayjs().format('YYYY-MM-DD'),
|
||||||
|
});
|
||||||
|
setLogQueryParam({
|
||||||
|
isSucc: 'SUCC',
|
||||||
|
linkNm: '',
|
||||||
|
linkSn: '',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchData = async () => {
|
||||||
|
const data = await LinkApi.getLinkStatsList({
|
||||||
|
...linkSearchParam,
|
||||||
|
startDate,
|
||||||
|
endDate
|
||||||
|
});
|
||||||
|
setLinkStatsList(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
setLinkSearchParam({
|
||||||
|
...linkSearchParam,
|
||||||
|
startDate,
|
||||||
|
endDate
|
||||||
|
});
|
||||||
|
|
||||||
|
fetchData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="card !mb-0 !min-h-[calc(100vh-16.8rem)]">
|
||||||
|
<div className="card card-border">
|
||||||
|
<form className="flex items-center gap-2 p-2" onSubmit={handleSubmit}>
|
||||||
|
<Label htmlFor="sDateSearchInput" className="mx-2 text-label">
|
||||||
|
시작일
|
||||||
|
</Label>
|
||||||
|
<DatePicker id="sDateSearchInput" mode={"single"} selected={dayjs(startDate).toDate()} onSelect={(e) => setStartDate(dayjs(e).format('YYYY-MM-DD'))} />
|
||||||
|
<Label htmlFor="eDateSearchInput" className="mx-2 text-label">
|
||||||
|
종료일
|
||||||
|
</Label>
|
||||||
|
<DatePicker id="eDateSearchInput" mode={"single"} className="mr-2" selected={dayjs(endDate).toDate()} onSelect={(e) => setEndDate(dayjs(e).format('YYYY-MM-DD'))} />
|
||||||
|
<Button color="green" className="h-[3.2rem]" type="button" onClick={handleResetSearchParam}>
|
||||||
|
초기화
|
||||||
|
</Button>
|
||||||
|
<Button className="h-[3.2rem]" type="submit">
|
||||||
|
조회
|
||||||
|
</Button>
|
||||||
|
<Button className="h-[3.2rem]" type="button" onClick={handleSubmitToday}>
|
||||||
|
오늘 날짜로 조회
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div className="card card-border">
|
||||||
|
<div className="card !m-[1.2rem] !p-0 flex flex-col gap-y-8">
|
||||||
|
<div className="card-header">
|
||||||
|
<h3 className="text-2xl font-medium text-label">조회 일자 : {`${linkSearchParam.startDate} ~ ${linkSearchParam.endDate}`}</h3>
|
||||||
|
</div>
|
||||||
|
<div className="h-[32rem] w-full">
|
||||||
|
<Line ref={succChartRef} data={succData} options={succOptions} onClick={handleSuccChartClick} />
|
||||||
|
</div>
|
||||||
|
<div className="h-[32rem]">
|
||||||
|
<Line ref={failChartRef} data={failData} options={failOptions} onClick={handleFailChartClick} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="card !m-[1.2rem] !p-0">
|
||||||
|
<div className="card-header">
|
||||||
|
<h3 className="text-2xl font-medium text-label">
|
||||||
|
{logQueryParam?.linkNm ? `${logQueryParam.linkNm} 연계 ${logQueryParam.isSucc === 'FAIL' ? '실패' : '성공'} 로그 목록 (${linkSearchParam.startDate} ~ ${linkSearchParam.endDate}) - ${query.data?.totalElements.toLocaleString('ko-KR') ?? 0}건` : '로그 목록'}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="card !m-[1.2rem] h-[40rem] overflow-auto !p-0">
|
||||||
|
{logQueryParam?.linkNm ?
|
||||||
|
query.isLoading ?
|
||||||
|
<div className="[data-theme='black']:bg-transparent rounded-lg p-10 h-full flex justify-center items-center">
|
||||||
|
<div className="relative size-[5rem]">
|
||||||
|
<LoadingWithAnimation color="primary" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
:
|
||||||
|
<div>{tableComponent}</div>
|
||||||
|
:
|
||||||
|
<div className="flex items-center justify-center h-full text-gray-500 text-3xl">
|
||||||
|
차트 데이터를 선택해주세요.
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
export { LinkStatsPage };
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { LinkTablePage} from "./ui/link-table-page";
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import { createColumnHelper } from "@tanstack/react-table";
|
||||||
|
import { type LinkTableColumns } from "@/entities/link";
|
||||||
|
import { Checkbox } from "@common/components";
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<LinkTableColumns>();
|
||||||
|
|
||||||
|
function tableColumns({ selectedColIds, setSelectedColIds }: {
|
||||||
|
selectedColIds: string[];
|
||||||
|
setSelectedColIds: React.Dispatch<React.SetStateAction<string[]>>;
|
||||||
|
}) {
|
||||||
|
return [
|
||||||
|
columnHelper.display({
|
||||||
|
id: "select",
|
||||||
|
header: ({ table }) => {
|
||||||
|
const allRows = table.getRowModel().rows;
|
||||||
|
const allSelected =
|
||||||
|
allRows.length > 0 &&
|
||||||
|
allRows.every((row) =>
|
||||||
|
selectedColIds.includes(row.original.col_sn)
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center items-center">
|
||||||
|
<Checkbox
|
||||||
|
className="bg-white"
|
||||||
|
checked={
|
||||||
|
allSelected
|
||||||
|
? true
|
||||||
|
: false
|
||||||
|
}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
if (checked) {
|
||||||
|
setSelectedColIds(allRows.map((row) => row.original.col_sn));
|
||||||
|
} else {
|
||||||
|
setSelectedColIds([]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const id = row.original.col_sn;
|
||||||
|
const checked = selectedColIds.includes(id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center items-center">
|
||||||
|
<Checkbox
|
||||||
|
checked={checked}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
setSelectedColIds((prev) =>
|
||||||
|
checked ? [...prev, id] : prev.filter((x) => x !== id)
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('col_sn', {
|
||||||
|
id: 'col_sn',
|
||||||
|
header: '컬럼 일련번호'
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('sou_col_nm', {
|
||||||
|
id: 'sou_col_nm',
|
||||||
|
header: '원천컬럼명',
|
||||||
|
cell: (info) => info.getValue() ? info.getValue() : '-',
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('sou_col_korn_nm', {
|
||||||
|
id: 'sou_col_korn_nm',
|
||||||
|
header: '원천컬럼 한글명',
|
||||||
|
cell: (info) => info.getValue() ? info.getValue() : '-',
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('sou_col_sz', {
|
||||||
|
id: 'sou_col_sz',
|
||||||
|
header: '원천컬럼 크기',
|
||||||
|
cell: (info) => info.getValue() ? info.getValue() : '-',
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('trsf_col_nm', {
|
||||||
|
id: 'trsf_col_nm',
|
||||||
|
header: '변환컬럼명',
|
||||||
|
cell: (info) => info.getValue() ? info.getValue() : '-',
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('trsf_col_korn_nm', {
|
||||||
|
id: 'trsf_col_korn_nm',
|
||||||
|
header: '변환컬럼 한글명',
|
||||||
|
cell: (info) => info.getValue() ? info.getValue() : '-',
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('trsf_col_sz', {
|
||||||
|
id: 'trsf_col_sz',
|
||||||
|
header: '변환컬럼 크기',
|
||||||
|
cell: (info) => info.getValue() ? info.getValue() : '-',
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('pk_yn', {
|
||||||
|
id: 'pk_yn',
|
||||||
|
header: 'PK 여부',
|
||||||
|
cell: (info) => info.getValue() === 'Y' ? '예' : '아니오',
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
export { tableColumns };
|
||||||
@@ -0,0 +1,503 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogTrigger,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogFooter,
|
||||||
|
DialogClose,
|
||||||
|
Button,
|
||||||
|
Input,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
Select,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectValue,
|
||||||
|
Checkbox
|
||||||
|
} from "@common/components";
|
||||||
|
import { FormItem, FormControl, FormField, Form } from "@/shared/ui/form";
|
||||||
|
import { type LinkTableColData, type LinkTable } from "@/entities/link";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import type { UseFormReturn } from "react-hook-form";
|
||||||
|
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
||||||
|
|
||||||
|
interface LinkTableDialogProps {
|
||||||
|
dialogOpen: boolean;
|
||||||
|
onOpenChange: (val: boolean) => void;
|
||||||
|
form: UseFormReturn<LinkTableColData>;
|
||||||
|
tableInfo: LinkTable;
|
||||||
|
insertLinkTableCol: (data: LinkTableColData) => void;
|
||||||
|
updateLinkTableCol: (data: LinkTableColData) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function LinkTableDialog({ dialogOpen, onOpenChange, form, tableInfo, insertLinkTableCol, updateLinkTableCol }: LinkTableDialogProps) {
|
||||||
|
|
||||||
|
const isModifyMode = !!form.getValues('col_sn');
|
||||||
|
|
||||||
|
const { confirm } = useConfirm();
|
||||||
|
const [onConfirm, setOnConfirm] = useState<boolean>(false);
|
||||||
|
const [useColSeq, setUseColSeq] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const handleConfirm = async (callback: () => Promise<void>) => {
|
||||||
|
setOnConfirm(true);
|
||||||
|
await callback();
|
||||||
|
setOnConfirm(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (data: LinkTableColData) => {
|
||||||
|
const confirmMsg = isModifyMode ? '수정 하시겠습니까?' : '저장 하시겠습니까?';
|
||||||
|
|
||||||
|
await handleConfirm(async () => {
|
||||||
|
if (await confirm(confirmMsg)) {
|
||||||
|
if (!isModifyMode) {
|
||||||
|
insertLinkTableCol({
|
||||||
|
...data,
|
||||||
|
link_sn: tableInfo.link_sn,
|
||||||
|
tbl_sn: tableInfo.tbl_sn,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
updateLinkTableCol({
|
||||||
|
...data,
|
||||||
|
link_sn: tableInfo.link_sn,
|
||||||
|
tbl_sn: tableInfo.tbl_sn,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCheckColSeqValue = (check: boolean | string) => {
|
||||||
|
if (check) {
|
||||||
|
setUseColSeq(false);
|
||||||
|
form.setValue('col_seq', null);
|
||||||
|
} else {
|
||||||
|
setUseColSeq(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!dialogOpen) {
|
||||||
|
setUseColSeq(false);
|
||||||
|
form.reset({
|
||||||
|
tbl_sn: '',
|
||||||
|
col_sn: '',
|
||||||
|
sou_col_nm: '',
|
||||||
|
sou_col_korn_nm: '',
|
||||||
|
sou_col_data_frm: '',
|
||||||
|
sou_col_sz: null,
|
||||||
|
trsf_col_nm: '',
|
||||||
|
trsf_col_korn_nm: '',
|
||||||
|
trsf_col_data_frm: '',
|
||||||
|
trsf_col_sz: null,
|
||||||
|
col_seq: null,
|
||||||
|
pk_yn: 'N',
|
||||||
|
mdfcn_dt: '',
|
||||||
|
mdfr: '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isModifyMode && form.getValues('col_seq')) {
|
||||||
|
setUseColSeq(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
}, [dialogOpen]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={dialogOpen}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
onOpenChange(open);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button color="green" className="h-[3.2rem]" onClick={() => onOpenChange(true)}>행 추가</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-[55rem]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
{tableInfo.trsf_tbl_korn_nm} ({tableInfo.tbl_sn})
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(handleSubmit)}>
|
||||||
|
<div className="card">
|
||||||
|
<Table variant="secondary">
|
||||||
|
<colgroup>
|
||||||
|
<col width="30%" />
|
||||||
|
<col width="70%" />
|
||||||
|
</colgroup>
|
||||||
|
<TableBody>
|
||||||
|
{isModifyMode &&
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>컬럼 일련번호</TableHead>
|
||||||
|
<TableCell>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="col_sn"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
value={field.value ?? ""}
|
||||||
|
className="!text-[1.3rem]"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
}
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>* 원천컬럼명</TableHead>
|
||||||
|
<TableCell>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="sou_col_nm"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
value={field.value ?? ""}
|
||||||
|
className="!text-[1.3rem]"
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
{form.formState.errors.sou_col_nm && (
|
||||||
|
<p className="text-red-500 text-base pt-2">
|
||||||
|
{form.formState.errors.sou_col_nm.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>* 원천컬럼 한글명</TableHead>
|
||||||
|
<TableCell>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="sou_col_korn_nm"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
value={field.value ?? ""}
|
||||||
|
className="!text-[1.3rem]"
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
{form.formState.errors.sou_col_korn_nm && (
|
||||||
|
<p className="text-red-500 text-base pt-2">
|
||||||
|
{form.formState.errors.sou_col_korn_nm.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>* 원천컬럼 데이터 형식</TableHead>
|
||||||
|
<TableCell>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="sou_col_data_frm"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
value={field.value ?? ""}
|
||||||
|
className="!text-[1.3rem]"
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
{form.formState.errors.sou_col_data_frm && (
|
||||||
|
<p className="text-red-500 text-base pt-2">
|
||||||
|
{form.formState.errors.sou_col_data_frm.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>원천컬럼 크기</TableHead>
|
||||||
|
<TableCell>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="sou_col_sz"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
value={field.value ?? ""}
|
||||||
|
className="!text-[1.3rem]"
|
||||||
|
autoComplete="off"
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
max={99999999999999}
|
||||||
|
onChange={(e) => {
|
||||||
|
const onlyDigits = e.target.value.replace(/\D/g, "").slice(0, 14);
|
||||||
|
field.onChange(onlyDigits); // react-hook-form에 반영
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<p className="text-gray-500 text-base pt-2">
|
||||||
|
※ 14자리까지 입력 가능합니다.
|
||||||
|
</p>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>PK 여부</TableHead>
|
||||||
|
<TableCell>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="pk_yn"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Select value={field.value ?? "N"} onValueChange={field.onChange}>
|
||||||
|
<SelectTrigger id="liResultCode" className="w-[10rem] mr-4">
|
||||||
|
<SelectValue placeholder="아니오"></SelectValue>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="Y">예</SelectItem>
|
||||||
|
<SelectItem value="N">아니오</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>* 변환컬럼명</TableHead>
|
||||||
|
<TableCell>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="trsf_col_nm"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
value={field.value ?? ""}
|
||||||
|
className="!text-[1.3rem]"
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
{form.formState.errors.trsf_col_nm && (
|
||||||
|
<p className="text-red-500 text-base pt-2">
|
||||||
|
{form.formState.errors.trsf_col_nm.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>* 변환컬럼 한글명</TableHead>
|
||||||
|
<TableCell>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="trsf_col_korn_nm"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
value={field.value ?? ""}
|
||||||
|
className="!text-[1.3rem]"
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
{form.formState.errors.trsf_col_korn_nm && (
|
||||||
|
<p className="text-red-500 text-base pt-2">
|
||||||
|
{form.formState.errors.trsf_col_korn_nm.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>* 변환컬럼 데이터 형식</TableHead>
|
||||||
|
<TableCell>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="trsf_col_data_frm"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
value={field.value ?? ""}
|
||||||
|
className="!text-[1.3rem]"
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
{form.formState.errors.trsf_col_data_frm && (
|
||||||
|
<p className="text-red-500 text-base pt-2">
|
||||||
|
{form.formState.errors.trsf_col_data_frm.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>변환컬럼 크기</TableHead>
|
||||||
|
<TableCell>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="trsf_col_sz"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
value={field.value ?? ""}
|
||||||
|
className="!text-[1.3rem]"
|
||||||
|
autoComplete="off"
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
max={99999999999999}
|
||||||
|
onChange={(e) => {
|
||||||
|
const onlyDigits = e.target.value.replace(/\D/g, "").slice(0, 14);
|
||||||
|
field.onChange(onlyDigits); // react-hook-form에 반영
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<p className="text-gray-500 text-base pt-2">
|
||||||
|
※ 14자리까지 입력 가능합니다.
|
||||||
|
</p>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>컬럼 매칭 순서</TableHead>
|
||||||
|
<TableCell>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="col_seq"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<div className="flex items-center gap-x-2 mb-4">
|
||||||
|
<Checkbox id="useColSeq" className="!border-gray-600" checked={!useColSeq} onCheckedChange={(check) => handleCheckColSeqValue(check)} />
|
||||||
|
<label htmlFor="useColSeq" className="text-gray-600">사용안함</label>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
value={field.value ?? ""}
|
||||||
|
className="!text-[1.3rem]"
|
||||||
|
autoComplete="off"
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
disabled={!useColSeq}
|
||||||
|
max={999}
|
||||||
|
onChange={(e) => {
|
||||||
|
const onlyDigits = e.target.value.replace(/\D/g, "").slice(0, 3);
|
||||||
|
field.onChange(onlyDigits); // react-hook-form에 반영
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<p className="text-gray-500 text-base pt-2">
|
||||||
|
※ 3자리까지 입력 가능합니다.
|
||||||
|
</p>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
{isModifyMode &&
|
||||||
|
<>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>수정자</TableHead>
|
||||||
|
<TableCell>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="mdfr"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
value={field.value ?? ""}
|
||||||
|
className="!text-[1.3rem]"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>수정일시</TableHead>
|
||||||
|
<TableCell>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="mdfcn_dt"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
value={field.value ? dayjs(field.value).format('YYYY-MM-DD') : ""}
|
||||||
|
className="!text-[1.3rem]"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button type="submit">저장</Button>
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button color="white">취소</Button>
|
||||||
|
</DialogClose>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
<ConfirmDialog isOpen={onConfirm} />
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { LinkTableDialog };
|
||||||
@@ -0,0 +1,710 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useMemo } from "react";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
DataTable,
|
||||||
|
Input,
|
||||||
|
Label,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
LoadingWithAnimation,
|
||||||
|
} from "@common/components";
|
||||||
|
import { getCoreRowModel, type PaginationState, type RowSelectionState, useReactTable } from "@tanstack/react-table";
|
||||||
|
import { LinkApi, type Link, type LinkSearchParam, type LinkTable, type LinkTableColumns, type LinkTableColData, type LinkTableFormData, LinkTableColumnsSchema, LinkTableSchema } from "@/entities/link";
|
||||||
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { columns } from "../../linkParams/model/table-columns";
|
||||||
|
import { tableColumns } from "../model/table-columns";
|
||||||
|
import { LinkTableDialog } from "./link-table-dialog";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
|
||||||
|
import { AUTH_USER_ID } from "@/shared/config";
|
||||||
|
import { getCookie } from "@common/components";
|
||||||
|
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||||
|
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||||
|
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
||||||
|
import { FormItem, FormControl, FormField, Form } from "@/shared/ui/form";
|
||||||
|
import { useDataTable } from "@/shared/lib";
|
||||||
|
|
||||||
|
function LinkTablePage() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const userUniqId = getCookie(AUTH_USER_ID) ?? "";
|
||||||
|
|
||||||
|
const [linkSn, setLinkSn] = useState<string>('');
|
||||||
|
const [linkNm, setLinkNm] = useState<string>('');
|
||||||
|
|
||||||
|
const [linkSearchParam, setLinkSearchParam] = useState<LinkSearchParam>({
|
||||||
|
linkSn: '',
|
||||||
|
linkNm: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const [selectedTableInfo, setSelectedTableInfo] = useState<LinkTable | null>(null);
|
||||||
|
const [tableColList, setTableColList] = useState<LinkTableColumns[]>([]);
|
||||||
|
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
|
||||||
|
const [selectedColIds, setSelectedColIds] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||||
|
|
||||||
|
const [pagination, setPagination] = useState<PaginationState>({
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { setMessage: alert } = useAlertStore((state) => state);
|
||||||
|
|
||||||
|
const { confirm } = useConfirm();
|
||||||
|
const [onConfirm, setOnConfirm] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const handleConfirm = async (callback: () => Promise<void>) => {
|
||||||
|
setOnConfirm(true);
|
||||||
|
await callback();
|
||||||
|
setOnConfirm(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const query = useQuery({
|
||||||
|
queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkList`, linkSearchParam],
|
||||||
|
queryFn: () => LinkApi.getLinkList({
|
||||||
|
...linkSearchParam,
|
||||||
|
page: pagination.pageIndex,
|
||||||
|
size: pagination.pageSize,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutateAsync: insertLinkTableCol } = useMutation({
|
||||||
|
mutationFn: async (data: LinkTableColData) => await LinkApi.insertLinkTableCol({
|
||||||
|
...data,
|
||||||
|
mdfr: userUniqId,
|
||||||
|
}),
|
||||||
|
onSuccess: async () => {
|
||||||
|
if (selectedTableInfo) {
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableInfo`]
|
||||||
|
});
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableColList`]
|
||||||
|
});
|
||||||
|
|
||||||
|
const tableData = await LinkApi.getLinkTableInfo(selectedTableInfo.link_sn);
|
||||||
|
const tableColList = await LinkApi.getLinkTableColList(selectedTableInfo.link_sn);
|
||||||
|
|
||||||
|
setSelectedTableInfo(tableData);
|
||||||
|
setTableColList(tableColList.content);
|
||||||
|
setSelectedColIds([]);
|
||||||
|
setDialogOpen(false);
|
||||||
|
alert('저장되었습니다.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutateAsync: updateLinkTableCol } = useMutation({
|
||||||
|
mutationFn: async (data: LinkTableColData) => await LinkApi.updateLinkTableCol({
|
||||||
|
...data,
|
||||||
|
mdfr: userUniqId,
|
||||||
|
}),
|
||||||
|
onSuccess: async () => {
|
||||||
|
if (selectedTableInfo) {
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableInfo`]
|
||||||
|
});
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableColList`]
|
||||||
|
});
|
||||||
|
|
||||||
|
const tableData = await LinkApi.getLinkTableInfo(selectedTableInfo.link_sn);
|
||||||
|
const tableColList = await LinkApi.getLinkTableColList(selectedTableInfo.link_sn);
|
||||||
|
|
||||||
|
setSelectedTableInfo(tableData);
|
||||||
|
setTableColList(tableColList.content);
|
||||||
|
setSelectedColIds([]);
|
||||||
|
setDialogOpen(false);
|
||||||
|
alert('수정되었습니다.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutateAsync: deleteLinkTableCol } = useMutation({
|
||||||
|
mutationFn: async (data: string[]) => await LinkApi.deleteLinkTableCol(data),
|
||||||
|
onSuccess: async () => {
|
||||||
|
if (selectedTableInfo) {
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableInfo`]
|
||||||
|
});
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableColList`]
|
||||||
|
});
|
||||||
|
|
||||||
|
const tableData = await LinkApi.getLinkTableInfo(selectedTableInfo.link_sn);
|
||||||
|
const tableColList = await LinkApi.getLinkTableColList(selectedTableInfo.link_sn);
|
||||||
|
|
||||||
|
setSelectedTableInfo(tableData);
|
||||||
|
setTableColList(tableColList.content);
|
||||||
|
setSelectedColIds([]);
|
||||||
|
setDialogOpen(false);
|
||||||
|
alert('삭제되었습니다.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutateAsync: insertLinkTableInfo } = useMutation({
|
||||||
|
mutationFn: async (data: LinkTableFormData) => await LinkApi.insertLinkTableInfo(data),
|
||||||
|
onSuccess: async () => {
|
||||||
|
if (selectedTableInfo) {
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableInfo`]
|
||||||
|
});
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableColList`]
|
||||||
|
});
|
||||||
|
|
||||||
|
const tableData = await LinkApi.getLinkTableInfo(selectedTableInfo.link_sn);
|
||||||
|
const tableColList = await LinkApi.getLinkTableColList(selectedTableInfo.link_sn);
|
||||||
|
|
||||||
|
tableForm.reset({
|
||||||
|
link_sn: tableData.link_sn,
|
||||||
|
link_nm: tableData.link_nm,
|
||||||
|
tbl_sn: tableData.tbl_sn ?? "",
|
||||||
|
sou_tbl_nm: tableData.sou_tbl_nm ?? "",
|
||||||
|
sou_tbl_korn_nm: tableData.sou_tbl_korn_nm ?? "",
|
||||||
|
sou_tbl_expln: tableData.sou_tbl_expln ?? "",
|
||||||
|
trsf_tbl_expln: tableData.trsf_tbl_expln ?? "",
|
||||||
|
trsf_tbl_nm: tableData.trsf_tbl_nm ?? "",
|
||||||
|
trsf_tbl_korn_nm: tableData.trsf_tbl_korn_nm ?? "",
|
||||||
|
});
|
||||||
|
setSelectedTableInfo(tableData);
|
||||||
|
setTableColList(tableColList.content);
|
||||||
|
setSelectedColIds([]);
|
||||||
|
alert('저장되었습니다.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutateAsync: updateLinkTableInfo } = useMutation({
|
||||||
|
mutationFn: async (data: LinkTableFormData) => await LinkApi.updateLinkTableInfo(data),
|
||||||
|
onSuccess: async () => {
|
||||||
|
if (selectedTableInfo) {
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableInfo`]
|
||||||
|
});
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableColList`]
|
||||||
|
});
|
||||||
|
|
||||||
|
const tableData = await LinkApi.getLinkTableInfo(selectedTableInfo.link_sn);
|
||||||
|
const tableColList = await LinkApi.getLinkTableColList(selectedTableInfo.link_sn);
|
||||||
|
|
||||||
|
setSelectedTableInfo(tableData);
|
||||||
|
setTableColList(tableColList.content);
|
||||||
|
setSelectedColIds([]);
|
||||||
|
alert('수정되었습니다.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const tableCol = useMemo(
|
||||||
|
() =>
|
||||||
|
tableColumns({ selectedColIds, setSelectedColIds }),
|
||||||
|
[selectedColIds]
|
||||||
|
);
|
||||||
|
|
||||||
|
const colTable = useReactTable({
|
||||||
|
data: tableColList,
|
||||||
|
columns: tableCol,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = useForm<LinkTableColData>({
|
||||||
|
resolver: zodResolver(LinkTableColumnsSchema),
|
||||||
|
defaultValues: {
|
||||||
|
pk_yn: "N"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const tableForm = useForm<LinkTableFormData>({
|
||||||
|
resolver: zodResolver(LinkTableSchema),
|
||||||
|
defaultValues: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleResetSearchParam = async () => {
|
||||||
|
setSelectedTableInfo(null);
|
||||||
|
setTableColList([]);
|
||||||
|
setLinkSn('');
|
||||||
|
setLinkNm('');
|
||||||
|
setLinkSearchParam({
|
||||||
|
...linkSearchParam,
|
||||||
|
linkSn: '',
|
||||||
|
linkNm: '',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setSelectedTableInfo(null);
|
||||||
|
setTableColList([]);
|
||||||
|
|
||||||
|
setLinkSearchParam({
|
||||||
|
...linkSearchParam,
|
||||||
|
linkSn,
|
||||||
|
linkNm
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRowClick = async (data: Link) => {
|
||||||
|
const tableData = await LinkApi.getLinkTableInfo(data.link_sn);
|
||||||
|
const tableColList = await LinkApi.getLinkTableColList(data.link_sn);
|
||||||
|
|
||||||
|
setSelectedTableInfo(tableData);
|
||||||
|
setTableColList(tableColList.content);
|
||||||
|
setSelectedColIds([]);
|
||||||
|
|
||||||
|
tableForm.reset({
|
||||||
|
link_sn: tableData.link_sn,
|
||||||
|
link_nm: tableData.link_nm,
|
||||||
|
tbl_sn: tableData.tbl_sn ?? "",
|
||||||
|
sou_tbl_nm: tableData.sou_tbl_nm ?? "",
|
||||||
|
sou_tbl_korn_nm: tableData.sou_tbl_korn_nm ?? "",
|
||||||
|
sou_tbl_expln: tableData.sou_tbl_expln ?? "",
|
||||||
|
trsf_tbl_expln: tableData.trsf_tbl_expln ?? "",
|
||||||
|
trsf_tbl_nm: tableData.trsf_tbl_nm ?? "",
|
||||||
|
trsf_tbl_korn_nm: tableData.trsf_tbl_korn_nm ?? "",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleColumnRowClick = async (data: LinkTableColumns) => {
|
||||||
|
const colInfo = await LinkApi.getLinkTableColInfo({ colSn: data.col_sn, tblSn: data.tbl_sn });
|
||||||
|
|
||||||
|
if (colInfo) {
|
||||||
|
let newData = { ...colInfo };
|
||||||
|
|
||||||
|
if (colInfo.mdfr) {
|
||||||
|
const userName = await fetchUserInfo();
|
||||||
|
newData = {
|
||||||
|
...colInfo,
|
||||||
|
mdfr: userName,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
form.reset(newData);
|
||||||
|
|
||||||
|
}
|
||||||
|
setDialogOpen(true)
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchUserInfo = async () => {
|
||||||
|
const userData = await getUserInfo();
|
||||||
|
return userData.userName;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteParam = async () => {
|
||||||
|
if (selectedColIds.length === 0) {
|
||||||
|
alert('삭제할 컬럼을 선택해 주세요.');
|
||||||
|
} else {
|
||||||
|
await handleConfirm(async () => {
|
||||||
|
if (await confirm('선택한 컬럼 정보를 삭제하시겠습니까?')) {
|
||||||
|
deleteLinkTableCol(selectedColIds);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTableSubmit = async (data: LinkTableFormData) => {
|
||||||
|
const regTblInfo = await LinkApi.countLinkTblNm(data.trsf_tbl_nm);
|
||||||
|
const confirmMsg = (regTblInfo.count > 0 && data.trsf_tbl_nm !== selectedTableInfo?.trsf_tbl_nm) ? `이미 사용중인 테이블 명입니다. 해당 테이블 명으로 ${!data.tbl_sn ? '저장' : '수정'}하시겠습니까?` : `${!data.tbl_sn ? '저장' : '수정'}하시겠습니까?`;
|
||||||
|
if (!data.tbl_sn) {
|
||||||
|
await handleConfirm(async () => {
|
||||||
|
if (await confirm(confirmMsg)) {
|
||||||
|
insertLinkTableInfo({
|
||||||
|
...data,
|
||||||
|
mdfr: userUniqId,
|
||||||
|
tbl_sn: (regTblInfo.count > 0) ? regTblInfo.tbl_sn : "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await handleConfirm(async () => {
|
||||||
|
if (await confirm(confirmMsg)) {
|
||||||
|
updateLinkTableInfo({
|
||||||
|
...data,
|
||||||
|
tbl_sn: (regTblInfo.count > 0) ? regTblInfo.tbl_sn : "",
|
||||||
|
mdfr: userUniqId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const { tableComponent } = useDataTable({
|
||||||
|
query,
|
||||||
|
tableOptions: {
|
||||||
|
columns,
|
||||||
|
state: {
|
||||||
|
pagination,
|
||||||
|
rowSelection,
|
||||||
|
},
|
||||||
|
manualPagination: true,
|
||||||
|
enableMultiRowSelection: true,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
onPaginationChange: setPagination,
|
||||||
|
onRowSelectionChange: setRowSelection,
|
||||||
|
},
|
||||||
|
dataTableOptions: {
|
||||||
|
onRowClick: handleRowClick,
|
||||||
|
viewPagination: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="card card-border !mt-6">
|
||||||
|
<div className="card card-border">
|
||||||
|
<form className="flex items-center gap-2 p-2" onSubmit={handleSubmit}>
|
||||||
|
<Label htmlFor="liSnSearchInput" className="mr-2 text-label">
|
||||||
|
일련번호
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem] mr-4"
|
||||||
|
id="liSnSearchInput"
|
||||||
|
value={linkSn}
|
||||||
|
onChange={(e) => setLinkSn(e.target.value)}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
<Label htmlFor="liNmSearchInput" className="mr-2 text-label">
|
||||||
|
연계 명
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem] mr-2"
|
||||||
|
id="liNmSearchInput"
|
||||||
|
value={linkNm}
|
||||||
|
onChange={(e) => setLinkNm(e.target.value)}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
<Button color="green" className="h-[3.2rem]" type="button" onClick={handleResetSearchParam}>
|
||||||
|
초기화
|
||||||
|
</Button>
|
||||||
|
<Button className="h-[3.2rem]" type="submit">
|
||||||
|
조회
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div className="card !mb-0 flex h-[calc(100vh-28rem)] gap-5 !p-0">
|
||||||
|
<div className="card card-border w-1/2 !mx-0 flex flex-col">
|
||||||
|
<div className="card-header">
|
||||||
|
<h3 className="text-2xl font-medium text-label">연계 목록</h3>
|
||||||
|
</div>
|
||||||
|
<div className="card !m-[1.2rem] overflow-auto !p-0 h-full">
|
||||||
|
{!query.isLoading ?
|
||||||
|
<div>{tableComponent}</div>
|
||||||
|
:
|
||||||
|
<div className="[data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center h-full">
|
||||||
|
<div className="relative size-[5rem]">
|
||||||
|
<LoadingWithAnimation color="primary" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="card card-border w-1/2 !mx-0 flex flex-col">
|
||||||
|
<div className="card-header">
|
||||||
|
<h3 className="text-2xl font-medium text-label">연계 테이블 정보</h3>
|
||||||
|
</div>
|
||||||
|
<div className="card !m-[1.2rem] !p-0 h-full overflow-auto flex flex-col">
|
||||||
|
<Form {...tableForm}>
|
||||||
|
<form onSubmit={tableForm.handleSubmit(handleTableSubmit)}>
|
||||||
|
{selectedTableInfo &&
|
||||||
|
<div className="flex justify-end mb-2">
|
||||||
|
<Button className="h-[3.2rem]" type="submit" color={selectedTableInfo.tbl_sn ? "green" : "primary"}>
|
||||||
|
{selectedTableInfo.tbl_sn ? "수정" : "저장"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<Table variant="secondary" className="flex-none">
|
||||||
|
<colgroup>
|
||||||
|
<col width="20%" />
|
||||||
|
<col width="80%" />
|
||||||
|
</colgroup>
|
||||||
|
<TableBody>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>연계 일련번호</TableHead>
|
||||||
|
<TableCell className="!p-2">
|
||||||
|
{selectedTableInfo ?
|
||||||
|
<FormField
|
||||||
|
control={tableForm.control}
|
||||||
|
name="link_sn"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
value={field.value ?? ""}
|
||||||
|
className="!text-[1.3rem] w-[400px]"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
:
|
||||||
|
<span className="px-2 py-[1.2rem]">-</span>
|
||||||
|
}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>연계 명</TableHead>
|
||||||
|
<TableCell className="!p-2">
|
||||||
|
{selectedTableInfo ?
|
||||||
|
<FormField
|
||||||
|
control={tableForm.control}
|
||||||
|
name="link_nm"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
value={field.value ?? ""}
|
||||||
|
className="!text-[1.3rem] w-[400px]"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
:
|
||||||
|
<span className="px-2 py-[1.2rem]">-</span>
|
||||||
|
}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>원천 테이블 명</TableHead>
|
||||||
|
<TableCell className="!p-2">
|
||||||
|
{selectedTableInfo ?
|
||||||
|
<FormField
|
||||||
|
control={tableForm.control}
|
||||||
|
name="sou_tbl_nm"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
value={field.value ?? ""}
|
||||||
|
className="!text-[1.3rem] w-[400px]"
|
||||||
|
placeholder={!field.value ? "-" : ""}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
{tableForm.formState.errors.sou_tbl_nm && (
|
||||||
|
<p className="text-red-500 text-base pt-2">
|
||||||
|
{tableForm.formState.errors.sou_tbl_nm.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
:
|
||||||
|
<span className="px-2 py-[1.2rem]">-</span>
|
||||||
|
}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>원천 테이블 한글명</TableHead>
|
||||||
|
<TableCell className="!p-2">
|
||||||
|
{selectedTableInfo ?
|
||||||
|
<FormField
|
||||||
|
control={tableForm.control}
|
||||||
|
name="sou_tbl_korn_nm"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
value={field.value ?? ""}
|
||||||
|
className="!text-[1.3rem] w-[400px]"
|
||||||
|
placeholder={!field.value ? "-" : ""}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
{tableForm.formState.errors.sou_tbl_korn_nm && (
|
||||||
|
<p className="text-red-500 text-base pt-2">
|
||||||
|
{tableForm.formState.errors.sou_tbl_korn_nm.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
:
|
||||||
|
<span className="px-2 py-[1.2rem]">-</span>
|
||||||
|
}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>원천 테이블 설명</TableHead>
|
||||||
|
<TableCell className="!p-2">
|
||||||
|
{selectedTableInfo ?
|
||||||
|
<FormField
|
||||||
|
control={tableForm.control}
|
||||||
|
name="sou_tbl_expln"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
value={field.value ?? ""}
|
||||||
|
className="!text-[1.3rem] w-[400px]"
|
||||||
|
placeholder={!field.value ? "-" : ""}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
{tableForm.formState.errors.sou_tbl_expln && (
|
||||||
|
<p className="text-red-500 text-base pt-2">
|
||||||
|
{tableForm.formState.errors.sou_tbl_expln.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
:
|
||||||
|
<span className="px-2 py-[1.2rem]">-</span>
|
||||||
|
}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>* 변환 테이블 명</TableHead>
|
||||||
|
<TableCell className="!p-2">
|
||||||
|
{selectedTableInfo ?
|
||||||
|
<FormField
|
||||||
|
control={tableForm.control}
|
||||||
|
name="trsf_tbl_nm"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
value={field.value ?? ""}
|
||||||
|
className="!text-[1.3rem] w-[400px]"
|
||||||
|
placeholder={!field.value ? "-" : ""}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
{tableForm.formState.errors.trsf_tbl_nm && (
|
||||||
|
<p className="text-red-500 text-base pt-2">
|
||||||
|
{tableForm.formState.errors.trsf_tbl_nm.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
:
|
||||||
|
<span className="px-2 py-[1.2rem]">-</span>
|
||||||
|
}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>* 변환 테이블 한글명</TableHead>
|
||||||
|
<TableCell className="!p-2">
|
||||||
|
{selectedTableInfo ?
|
||||||
|
<FormField
|
||||||
|
control={tableForm.control}
|
||||||
|
name="trsf_tbl_korn_nm"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
value={field.value ?? ""}
|
||||||
|
className="!text-[1.3rem] w-[400px]"
|
||||||
|
placeholder={!field.value ? "-" : ""}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
{tableForm.formState.errors.trsf_tbl_korn_nm && (
|
||||||
|
<p className="text-red-500 text-base pt-2">
|
||||||
|
{tableForm.formState.errors.trsf_tbl_korn_nm.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
:
|
||||||
|
<span className="px-2 py-[1.2rem]">-</span>
|
||||||
|
}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>변환 테이블 설명</TableHead>
|
||||||
|
<TableCell className="!p-2">
|
||||||
|
{selectedTableInfo ?
|
||||||
|
<FormField
|
||||||
|
control={tableForm.control}
|
||||||
|
name="trsf_tbl_expln"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
value={field.value ?? ""}
|
||||||
|
className="!text-[1.3rem] w-[400px]"
|
||||||
|
placeholder={!field.value ? "-" : ""}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
{tableForm.formState.errors.trsf_tbl_expln && (
|
||||||
|
<p className="text-red-500 text-base pt-2">
|
||||||
|
{tableForm.formState.errors.trsf_tbl_expln.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
:
|
||||||
|
<span className="px-2 py-[1.2rem]">-</span>
|
||||||
|
}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
{selectedTableInfo ?
|
||||||
|
<>
|
||||||
|
<div className="mb-2 mt-14 flex justify-end items-center flex-none">
|
||||||
|
{tableColList.length > 0 &&
|
||||||
|
<Button color="red" className="h-[3.2rem] mr-2" type="button" onClick={handleDeleteParam}>
|
||||||
|
행 삭제
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
{selectedTableInfo.tbl_sn &&
|
||||||
|
<LinkTableDialog dialogOpen={dialogOpen} onOpenChange={setDialogOpen} form={form} tableInfo={selectedTableInfo} insertLinkTableCol={insertLinkTableCol} updateLinkTableCol={updateLinkTableCol} />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
{selectedTableInfo.tbl_sn ?
|
||||||
|
(tableColList.length > 0 ?
|
||||||
|
<div className="flex-1 overflow-auto min-h-[300px]">
|
||||||
|
<DataTable table={colTable} onRowClick={handleColumnRowClick} />
|
||||||
|
</div>
|
||||||
|
:
|
||||||
|
<div className="flex items-center justify-center h-20 text-gray-500 text-3xl">
|
||||||
|
등록된 테이블 컬럼이 없습니다.
|
||||||
|
</div>)
|
||||||
|
:
|
||||||
|
<div className="flex items-center justify-center h-20 text-gray-500 text-3xl">
|
||||||
|
테이블 정보를 등록해주세요.
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
:
|
||||||
|
<div className="flex items-center justify-center text-gray-500 text-3xl mt-14 flex-1">
|
||||||
|
연계 데이터를 선택해주세요.
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ConfirmDialog isOpen={onConfirm} />
|
||||||
|
</div >
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
export { LinkTablePage };
|
||||||
@@ -6,7 +6,6 @@ import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
|||||||
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
DateRangePicker,
|
|
||||||
Input,
|
Input,
|
||||||
Label,
|
Label,
|
||||||
Select,
|
Select,
|
||||||
@@ -16,13 +15,14 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
LoadingWithImage,
|
LoadingWithImage,
|
||||||
} from "@common/components";
|
} from "@common/components";
|
||||||
|
import { columns } from "@/pages/loginlog/model/table-columns";
|
||||||
import { LoginlogApi, type LoginlogParams } from "@/entities/loginlog";
|
import { LoginlogApi, type LoginlogParams } from "@/entities/loginlog";
|
||||||
import { useDataTable } from "@/shared/lib";
|
import { useDataTable } from "@/shared/lib";
|
||||||
import { ENDPOINT } from "@/shared/config";
|
import { ENDPOINT } from "@/shared/config";
|
||||||
import { columns } from "@/pages/loginlog/model/table-columns";
|
import { InputDateRangePicker } from "@/shared/ui/input-date-range-picker";
|
||||||
|
|
||||||
type DateRange = {
|
type DateRange = {
|
||||||
from: Date | undefined;
|
from?: Date | undefined;
|
||||||
to?: Date | undefined;
|
to?: Date | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -32,13 +32,12 @@ function LoginlogPage() {
|
|||||||
to: dayjs().toDate(),
|
to: dayjs().toDate(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const [dateRange, setDateRange] = useState<DateRange>(defaultDateRange);
|
|
||||||
const [isManualLoading, setIsManualLoading] = useState(false);
|
|
||||||
|
|
||||||
const [pagination, setPagination] = useState<PaginationState>({
|
const [pagination, setPagination] = useState<PaginationState>({
|
||||||
pageIndex: 0,
|
pageIndex: 0,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
});
|
});
|
||||||
|
const [presetDateRange, setPresetDateRange] = useState("");
|
||||||
|
const [dateRange, setDateRange] = useState<DateRange>(defaultDateRange);
|
||||||
const [userId, setUserId] = useState("");
|
const [userId, setUserId] = useState("");
|
||||||
const [errOcrnYn, setErrOcrnYn] = useState("N");
|
const [errOcrnYn, setErrOcrnYn] = useState("N");
|
||||||
const [searchParams, setSearchParams] = useState<LoginlogParams>({
|
const [searchParams, setSearchParams] = useState<LoginlogParams>({
|
||||||
@@ -48,6 +47,7 @@ function LoginlogPage() {
|
|||||||
searchDateTo: dayjs().format("YYYY-MM-DD"),
|
searchDateTo: dayjs().format("YYYY-MM-DD"),
|
||||||
sort: "lgnDt,desc",
|
sort: "lgnDt,desc",
|
||||||
});
|
});
|
||||||
|
const [isManualLoading, setIsManualLoading] = useState(false);
|
||||||
|
|
||||||
const query = useQuery({
|
const query = useQuery({
|
||||||
queryKey: [ENDPOINT.USER_SERVICE.LOGINLOG, searchParams, pagination],
|
queryKey: [ENDPOINT.USER_SERVICE.LOGINLOG, searchParams, pagination],
|
||||||
@@ -88,8 +88,12 @@ function LoginlogPage() {
|
|||||||
...searchParams,
|
...searchParams,
|
||||||
userId,
|
userId,
|
||||||
errOcrnYn,
|
errOcrnYn,
|
||||||
searchDateFrom: dayjs(dateRange.from).format("YYYY-MM-DD"),
|
searchDateFrom: dateRange.from
|
||||||
searchDateTo: dayjs(dateRange.to).format("YYYY-MM-DD"),
|
? dayjs(dateRange.from).format("YYYY-MM-DD")
|
||||||
|
: "",
|
||||||
|
searchDateTo: dateRange.to
|
||||||
|
? dayjs(dateRange.to).format("YYYY-MM-DD")
|
||||||
|
: "",
|
||||||
});
|
});
|
||||||
query.refetch().finally(() => {
|
query.refetch().finally(() => {
|
||||||
setIsManualLoading(false);
|
setIsManualLoading(false);
|
||||||
@@ -133,31 +137,34 @@ function LoginlogPage() {
|
|||||||
default:
|
default:
|
||||||
presetDateRange = undefined;
|
presetDateRange = undefined;
|
||||||
}
|
}
|
||||||
|
setPresetDateRange(value);
|
||||||
setDateRange(presetDateRange || defaultDateRange);
|
setDateRange(presetDateRange || defaultDateRange);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDateRangeChange = (dateRange: DateRange) => {
|
||||||
|
setDateRange(dateRange);
|
||||||
|
setPresetDateRange("");
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="card card-border">
|
<div className="card card-border">
|
||||||
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
|
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
|
||||||
<Label className="mr-2">조회기간</Label>
|
<Label className="mr-2">조회기간</Label>
|
||||||
<DateRangePicker
|
<Select value={presetDateRange} onValueChange={handlePresetDateRange}>
|
||||||
value={dateRange}
|
<SelectTrigger className="w-[12rem]">
|
||||||
onValueChange={(value) => setDateRange(value || defaultDateRange)}
|
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
|
||||||
>
|
</SelectTrigger>
|
||||||
<div className="card card-border flex items-center gap-2">
|
<SelectContent>
|
||||||
<Select onValueChange={handlePresetDateRange}>
|
<SelectItem value="1W">일주일</SelectItem>
|
||||||
<SelectTrigger>
|
<SelectItem value="1M">1개월</SelectItem>
|
||||||
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
|
<SelectItem value="3M">3개월</SelectItem>
|
||||||
</SelectTrigger>
|
</SelectContent>
|
||||||
<SelectContent>
|
</Select>
|
||||||
<SelectItem value="1W">일주일</SelectItem>
|
<InputDateRangePicker
|
||||||
<SelectItem value="1M">1개월</SelectItem>
|
dateRange={dateRange}
|
||||||
<SelectItem value="3M">3개월</SelectItem>
|
setDateRange={handleDateRangeChange}
|
||||||
</SelectContent>
|
/>
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</DateRangePicker>
|
|
||||||
<Label className="ml-4 mr-2 text-label">사용자ID</Label>
|
<Label className="ml-4 mr-2 text-label">사용자ID</Label>
|
||||||
<Input
|
<Input
|
||||||
className="h-[3.2rem] w-[20rem] rounded-sm !text-[1.3rem]"
|
className="h-[3.2rem] w-[20rem] rounded-sm !text-[1.3rem]"
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ function MenuPage() {
|
|||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: [ENDPOINT.CMS_SERVICE.MENU_ROLES],
|
queryKey: [ENDPOINT.CMS_SERVICE.MENU_ROLES],
|
||||||
});
|
});
|
||||||
|
form.reset();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
|||||||
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
DateRangePicker,
|
|
||||||
Input,
|
Input,
|
||||||
Label,
|
Label,
|
||||||
Select,
|
Select,
|
||||||
@@ -15,16 +14,17 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@common/components";
|
} from "@common/components";
|
||||||
|
import { columns } from "@/pages/privacylog/model/table-columns";
|
||||||
import {
|
import {
|
||||||
PrivacyLogPrivacy,
|
PrivacyLogPrivacy,
|
||||||
type PrivacyLogParams,
|
type PrivacyLogParams,
|
||||||
} from "@/entities/privacylog";
|
} from "@/entities/privacylog";
|
||||||
import { useDataTable } from "@/shared/lib";
|
import { useDataTable } from "@/shared/lib";
|
||||||
import { ENDPOINT } from "@/shared/config";
|
import { ENDPOINT } from "@/shared/config";
|
||||||
import { columns } from "@/pages/privacylog/model/table-columns";
|
import { InputDateRangePicker } from "@/shared/ui/input-date-range-picker";
|
||||||
|
|
||||||
type DateRange = {
|
type DateRange = {
|
||||||
from: Date | undefined;
|
from?: Date | undefined;
|
||||||
to?: Date | undefined;
|
to?: Date | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -38,6 +38,7 @@ function PrivacyLogPage() {
|
|||||||
pageIndex: 0,
|
pageIndex: 0,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
});
|
});
|
||||||
|
const [presetDateRange, setPresetDateRange] = useState("");
|
||||||
const [dateRange, setDateRange] = useState<DateRange>(defaultDateRange);
|
const [dateRange, setDateRange] = useState<DateRange>(defaultDateRange);
|
||||||
const [inqInfoKeyword, setInqInfoKeyword] = useState("");
|
const [inqInfoKeyword, setInqInfoKeyword] = useState("");
|
||||||
const [searchParams, setSearchParams] = useState<PrivacyLogParams>({
|
const [searchParams, setSearchParams] = useState<PrivacyLogParams>({
|
||||||
@@ -85,8 +86,10 @@ function PrivacyLogPage() {
|
|||||||
setSearchParams({
|
setSearchParams({
|
||||||
...searchParams,
|
...searchParams,
|
||||||
inqInfoKeyword,
|
inqInfoKeyword,
|
||||||
inqDtStart: dayjs(dateRange.from).format("YYYY-MM-DD"),
|
inqDtStart: dateRange.from
|
||||||
inqDtEnd: dayjs(dateRange.to).format("YYYY-MM-DD"),
|
? dayjs(dateRange.from).format("YYYY-MM-DD")
|
||||||
|
: "",
|
||||||
|
inqDtEnd: dateRange.to ? dayjs(dateRange.to).format("YYYY-MM-DD") : "",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -125,38 +128,40 @@ function PrivacyLogPage() {
|
|||||||
default:
|
default:
|
||||||
presetDateRange = undefined;
|
presetDateRange = undefined;
|
||||||
}
|
}
|
||||||
|
setPresetDateRange(value);
|
||||||
setDateRange(presetDateRange || defaultDateRange);
|
setDateRange(presetDateRange || defaultDateRange);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDateRangeChange = (dateRange: DateRange) => {
|
||||||
|
setDateRange(dateRange);
|
||||||
|
setPresetDateRange("");
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="card card-border">
|
<div className="card card-border">
|
||||||
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
|
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
|
||||||
<Label className="mr-2">조회기간</Label>
|
<Label className="mr-2">조회기간</Label>
|
||||||
<DateRangePicker
|
<Select value={presetDateRange} onValueChange={handlePresetDateRange}>
|
||||||
value={dateRange}
|
<SelectTrigger className="w-[12rem]">
|
||||||
onValueChange={(value) => setDateRange(value || defaultDateRange)}
|
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
|
||||||
>
|
</SelectTrigger>
|
||||||
<div className="card card-border flex items-center gap-2">
|
<SelectContent>
|
||||||
<Select onValueChange={handlePresetDateRange}>
|
<SelectItem value="1W">일주일</SelectItem>
|
||||||
<SelectTrigger>
|
<SelectItem value="1M">1개월</SelectItem>
|
||||||
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
|
<SelectItem value="3M">3개월</SelectItem>
|
||||||
</SelectTrigger>
|
</SelectContent>
|
||||||
<SelectContent>
|
</Select>
|
||||||
<SelectItem value="1W">일주일</SelectItem>
|
<InputDateRangePicker
|
||||||
<SelectItem value="1M">1개월</SelectItem>
|
dateRange={dateRange}
|
||||||
<SelectItem value="3M">3개월</SelectItem>
|
setDateRange={handleDateRangeChange}
|
||||||
</SelectContent>
|
/>
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</DateRangePicker>
|
|
||||||
<Label className="ml-4 mr-2 text-label">조회정보</Label>
|
<Label className="ml-4 mr-2 text-label">조회정보</Label>
|
||||||
<Input
|
<Input
|
||||||
className="h-[3.2rem] w-[20rem] rounded-sm !text-[1.3rem]"
|
className="h-[3.2rem] w-[20rem] rounded-sm !text-[1.3rem]"
|
||||||
value={inqInfoKeyword}
|
value={inqInfoKeyword}
|
||||||
onChange={(e) => setInqInfoKeyword(e.target.value)}
|
onChange={(e) => setInqInfoKeyword(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button className="h-[3.2rem]" type="submit">
|
<Button className="h-[3.2rem]" type="submit">
|
||||||
조회
|
조회
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
} from "@tanstack/react-table";
|
} from "@tanstack/react-table";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
DateRangePicker,
|
|
||||||
Input,
|
Input,
|
||||||
Label,
|
Label,
|
||||||
Select,
|
Select,
|
||||||
@@ -31,13 +30,14 @@ import {
|
|||||||
} from "@/entities/post";
|
} from "@/entities/post";
|
||||||
import { BoardApi } from "@/entities/board";
|
import { BoardApi } from "@/entities/board";
|
||||||
import { AuthorityButton } from "@/entities/menu-authority";
|
import { AuthorityButton } from "@/entities/menu-authority";
|
||||||
import { useAlertStore, useSelectPrompt, useDataTable } from "@/shared/lib";
|
|
||||||
import { ENDPOINT } from "@/shared/config";
|
import { ENDPOINT } from "@/shared/config";
|
||||||
|
import { InputDateRangePicker } from "@/shared/ui/input-date-range-picker";
|
||||||
|
import { useAlertStore, useSelectPrompt, useDataTable } from "@/shared/lib";
|
||||||
import { columns } from "../model/table-columns";
|
import { columns } from "../model/table-columns";
|
||||||
import { MovePostDialog } from "./move-post-dialog";
|
import { MovePostDialog } from "./move-post-dialog";
|
||||||
|
|
||||||
type DateRange = {
|
type DateRange = {
|
||||||
from: Date | undefined;
|
from?: Date | undefined;
|
||||||
to?: Date | undefined;
|
to?: Date | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -51,22 +51,21 @@ function RecentPage() {
|
|||||||
const { confirm } = useConfirm();
|
const { confirm } = useConfirm();
|
||||||
const alert = useAlertStore((state) => state.setMessage);
|
const alert = useAlertStore((state) => state.setMessage);
|
||||||
|
|
||||||
const [dateRange, setDateRange] = useState<DateRange | undefined>({
|
|
||||||
from: undefined,
|
|
||||||
to: undefined,
|
|
||||||
});
|
|
||||||
const [tableSort, setTableSort] = useState("regDt,desc");
|
const [tableSort, setTableSort] = useState("regDt,desc");
|
||||||
const [pagination, setPagination] = useState<PaginationState>({
|
const [pagination, setPagination] = useState<PaginationState>({
|
||||||
pageIndex: 0,
|
pageIndex: 0,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
});
|
});
|
||||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||||
|
const [presetDateRange, setPresetDateRange] = useState("");
|
||||||
|
const [dateRange, setDateRange] = useState<DateRange>({
|
||||||
|
from: undefined,
|
||||||
|
to: undefined,
|
||||||
|
});
|
||||||
const [keywordType, setKeywordType] = useState("");
|
const [keywordType, setKeywordType] = useState("");
|
||||||
const [keyword, setKeyword] = useState("");
|
const [keyword, setKeyword] = useState("");
|
||||||
const [bbsId, setBbsId] = useState<string | null>(null);
|
const [bbsId, setBbsId] = useState<string | null>(null);
|
||||||
const [delYn, setDelYn] = useState("N");
|
const [delYn, setDelYn] = useState("N");
|
||||||
|
|
||||||
const [searchParams, setSearchParams] = useState<RecentPostParams>({
|
const [searchParams, setSearchParams] = useState<RecentPostParams>({
|
||||||
keywordType: "",
|
keywordType: "",
|
||||||
keyword: "",
|
keyword: "",
|
||||||
@@ -170,7 +169,7 @@ function RecentPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handlePresetDateRange = (value: string) => {
|
const handlePresetDateRange = (value: string) => {
|
||||||
let presetDateRange: DateRange | undefined;
|
let presetDateRange: DateRange;
|
||||||
switch (value) {
|
switch (value) {
|
||||||
case "1W":
|
case "1W":
|
||||||
presetDateRange = {
|
presetDateRange = {
|
||||||
@@ -191,11 +190,20 @@ function RecentPage() {
|
|||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
presetDateRange = undefined;
|
presetDateRange = {
|
||||||
|
from: undefined,
|
||||||
|
to: undefined,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
setPresetDateRange(value);
|
||||||
setDateRange(presetDateRange);
|
setDateRange(presetDateRange);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDateRangeChange = (dateRange: DateRange) => {
|
||||||
|
setDateRange(dateRange);
|
||||||
|
setPresetDateRange("");
|
||||||
|
};
|
||||||
|
|
||||||
const onSoftDeletePost = async () => {
|
const onSoftDeletePost = async () => {
|
||||||
const { resolved, value: delRsn } =
|
const { resolved, value: delRsn } =
|
||||||
await prompt("삭제 사유를 입력해주세요.");
|
await prompt("삭제 사유를 입력해주세요.");
|
||||||
@@ -250,21 +258,20 @@ function RecentPage() {
|
|||||||
<div className="card card-border">
|
<div className="card card-border">
|
||||||
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
|
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
|
||||||
<Label className="mr-2">날짜</Label>
|
<Label className="mr-2">날짜</Label>
|
||||||
<DateRangePicker value={dateRange} onValueChange={setDateRange}>
|
<Select value={presetDateRange} onValueChange={handlePresetDateRange}>
|
||||||
<div className="card card-border flex items-center gap-2">
|
<SelectTrigger className="w-[12rem]">
|
||||||
<Select onValueChange={handlePresetDateRange}>
|
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
|
||||||
<SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
|
<SelectContent>
|
||||||
</SelectTrigger>
|
<SelectItem value="1W">일주일</SelectItem>
|
||||||
<SelectContent>
|
<SelectItem value="1M">1개월</SelectItem>
|
||||||
<SelectItem value="1W">일주일</SelectItem>
|
<SelectItem value="3M">3개월</SelectItem>
|
||||||
<SelectItem value="1M">1개월</SelectItem>
|
</SelectContent>
|
||||||
<SelectItem value="3M">3개월</SelectItem>
|
</Select>
|
||||||
</SelectContent>
|
<InputDateRangePicker
|
||||||
</Select>
|
dateRange={dateRange}
|
||||||
</div>
|
setDateRange={handleDateRangeChange}
|
||||||
</DateRangePicker>
|
/>
|
||||||
|
|
||||||
<Label className="ml-4 mr-2 text-label">게시판</Label>
|
<Label className="ml-4 mr-2 text-label">게시판</Label>
|
||||||
<Select
|
<Select
|
||||||
value={bbsId?.toString() || ""}
|
value={bbsId?.toString() || ""}
|
||||||
@@ -281,7 +288,6 @@ function RecentPage() {
|
|||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
<Label className="ml-4 mr-2 text-label">상태</Label>
|
<Label className="ml-4 mr-2 text-label">상태</Label>
|
||||||
<Select value={delYn} onValueChange={(value) => setDelYn(value)}>
|
<Select value={delYn} onValueChange={(value) => setDelYn(value)}>
|
||||||
<SelectTrigger className="w-[10rem]">
|
<SelectTrigger className="w-[10rem]">
|
||||||
@@ -292,7 +298,6 @@ function RecentPage() {
|
|||||||
<SelectItem value="Y">삭제</SelectItem>
|
<SelectItem value="Y">삭제</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
<Label className="ml-4 mr-2 text-label">검색</Label>
|
<Label className="ml-4 mr-2 text-label">검색</Label>
|
||||||
<Select
|
<Select
|
||||||
value={keywordType}
|
value={keywordType}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export { UserStatisticsApi } from "./user-statistics-service";
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
import {
|
||||||
|
type AccessLog,
|
||||||
|
type AccessLogParams,
|
||||||
|
type AccessLogService,
|
||||||
|
type PageableList,
|
||||||
|
} from "../model/user-statistics-interface";
|
||||||
|
|
||||||
|
// 목 데이터
|
||||||
|
const mockAccessLogs: AccessLog[] = [
|
||||||
|
{
|
||||||
|
no: 1,
|
||||||
|
accessTime: "2024-01-15 14:30:25",
|
||||||
|
accessIp: "192.168.1.100",
|
||||||
|
accessRegion: "서울특별시",
|
||||||
|
urlPath: "/index",
|
||||||
|
browser: "Chrome",
|
||||||
|
site: "SITE1",
|
||||||
|
accessType: "메인",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
no: 2,
|
||||||
|
accessTime: "2024-01-15 14:28:15",
|
||||||
|
accessIp: "203.241.50.123",
|
||||||
|
accessRegion: "부산광역시",
|
||||||
|
urlPath: "/board/list",
|
||||||
|
browser: "Edge",
|
||||||
|
site: "SITE2",
|
||||||
|
accessType: "게시판",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
no: 3,
|
||||||
|
accessTime: "2024-01-15 14:25:42",
|
||||||
|
accessIp: "175.223.45.67",
|
||||||
|
accessRegion: "대구광역시",
|
||||||
|
urlPath: "/statistics",
|
||||||
|
browser: "Safari",
|
||||||
|
site: "SITE1",
|
||||||
|
accessType: "통계",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
no: 4,
|
||||||
|
accessTime: "2024-01-15 14:22:18",
|
||||||
|
accessIp: "211.252.87.45",
|
||||||
|
accessRegion: "인천광역시",
|
||||||
|
urlPath: "/contents/view",
|
||||||
|
browser: "Firefox",
|
||||||
|
site: "SITE3",
|
||||||
|
accessType: "컨텐츠",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
no: 5,
|
||||||
|
accessTime: "2024-01-15 14:20:33",
|
||||||
|
accessIp: "121.254.12.89",
|
||||||
|
accessRegion: "광주광역시",
|
||||||
|
urlPath: "/search",
|
||||||
|
browser: "Chrome",
|
||||||
|
site: "SITE1",
|
||||||
|
accessType: "검색",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 목 API 서비스
|
||||||
|
class UserStatisticsApiImpl implements AccessLogService {
|
||||||
|
async getAccessLogList(
|
||||||
|
params: AccessLogParams,
|
||||||
|
): Promise<PageableList<AccessLog>> {
|
||||||
|
// 실제 API 호출 대신 목 데이터 반환
|
||||||
|
const { page = 0, size = 10 } = params;
|
||||||
|
|
||||||
|
// 필터링 로직 (실제로는 서버에서 처리)
|
||||||
|
let filteredLogs = [...mockAccessLogs];
|
||||||
|
|
||||||
|
if (params.urlPath) {
|
||||||
|
filteredLogs = filteredLogs.filter((log) =>
|
||||||
|
log.urlPath.toLowerCase().includes(params.urlPath!.toLowerCase()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.accessIp) {
|
||||||
|
filteredLogs = filteredLogs.filter((log) =>
|
||||||
|
log.accessIp.includes(params.accessIp!),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.accessRegion) {
|
||||||
|
filteredLogs = filteredLogs.filter((log) =>
|
||||||
|
log.accessRegion.includes(params.accessRegion!),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.browser) {
|
||||||
|
filteredLogs = filteredLogs.filter(
|
||||||
|
(log) => log.browser.toLowerCase() === params.browser!.toLowerCase(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.siteId) {
|
||||||
|
filteredLogs = filteredLogs.filter((log) => log.site === params.siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 페이지네이션
|
||||||
|
const start = page * size;
|
||||||
|
const end = start + size;
|
||||||
|
const paginatedLogs = filteredLogs.slice(start, end);
|
||||||
|
|
||||||
|
// 목 응답 생성
|
||||||
|
return {
|
||||||
|
content: paginatedLogs,
|
||||||
|
pageable: {
|
||||||
|
pageNumber: page,
|
||||||
|
pageSize: size,
|
||||||
|
sort: {
|
||||||
|
empty: true,
|
||||||
|
sorted: false,
|
||||||
|
unsorted: true,
|
||||||
|
},
|
||||||
|
offset: page * size,
|
||||||
|
paged: true,
|
||||||
|
unpaged: false,
|
||||||
|
},
|
||||||
|
last: (page + 1) * size >= filteredLogs.length,
|
||||||
|
totalPages: Math.ceil(filteredLogs.length / size),
|
||||||
|
totalElements: filteredLogs.length,
|
||||||
|
size,
|
||||||
|
number: page,
|
||||||
|
sort: {
|
||||||
|
empty: true,
|
||||||
|
sorted: false,
|
||||||
|
unsorted: true,
|
||||||
|
},
|
||||||
|
first: page === 0,
|
||||||
|
numberOfElements: paginatedLogs.length,
|
||||||
|
empty: paginatedLogs.length === 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UserStatisticsApi = new UserStatisticsApiImpl();
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { UserStatisticsPage } from "./ui/user-statistics-page";
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
import { type ChartData, type ChartOptions } from "chart.js";
|
||||||
|
|
||||||
|
// 일별 접속 현황 차트 데이터
|
||||||
|
export const monthlyStatisticsData = {
|
||||||
|
labels: [
|
||||||
|
"1일",
|
||||||
|
"2일",
|
||||||
|
"3일",
|
||||||
|
"4일",
|
||||||
|
"5일",
|
||||||
|
"6일",
|
||||||
|
"7일",
|
||||||
|
"8일",
|
||||||
|
"9일",
|
||||||
|
"10일",
|
||||||
|
"11일",
|
||||||
|
"12일",
|
||||||
|
"13일",
|
||||||
|
"14일",
|
||||||
|
"15일",
|
||||||
|
],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
type: "bar" as const,
|
||||||
|
label: "페이지뷰",
|
||||||
|
data: [
|
||||||
|
850, 1000, 750, 750, 700, 750, 780, 800, 850, 900, 1200, 1700, 2300,
|
||||||
|
2100, 2600,
|
||||||
|
],
|
||||||
|
backgroundColor: "rgba(54, 162, 235, 0.8)",
|
||||||
|
order: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "line" as const,
|
||||||
|
label: "신규 접속자",
|
||||||
|
data: [
|
||||||
|
700, 600, 500, 550, 500, 550, 500, 550, 600, 850, 1200, 1500, 1700,
|
||||||
|
2300, 2200,
|
||||||
|
],
|
||||||
|
borderColor: "rgba(255, 99, 132, 0.8)",
|
||||||
|
borderWidth: 2,
|
||||||
|
tension: 0.4,
|
||||||
|
pointRadius: 3,
|
||||||
|
order: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "line" as const,
|
||||||
|
label: "총 접속자",
|
||||||
|
data: [
|
||||||
|
200, 300, 400, 500, 600, 500, 550, 500, 400, 500, 350, 800, 900, 1200,
|
||||||
|
1300,
|
||||||
|
],
|
||||||
|
borderColor: "rgba(75, 192, 192, 0.8)",
|
||||||
|
borderWidth: 2,
|
||||||
|
tension: 0.4,
|
||||||
|
pointRadius: 3,
|
||||||
|
order: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const monthlyStatisticsOptions: ChartOptions<"bar"> = {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
position: "top" as const,
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
mode: "index" as const,
|
||||||
|
intersect: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// 사이트별 접속자 차트 데이터
|
||||||
|
export const siteVisitorsData: ChartData<"bar"> = {
|
||||||
|
labels: ["SITE1", "SITE2", "SITE3", "SITE4", "SITE5"],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: "사이트별 접속자",
|
||||||
|
data: [2500, 2000, 1500, 1200, 800],
|
||||||
|
backgroundColor: [
|
||||||
|
"rgba(54, 162, 235, 0.8)",
|
||||||
|
"rgba(153, 102, 255, 0.8)",
|
||||||
|
"rgba(255, 99, 132, 0.8)",
|
||||||
|
"rgba(255, 159, 64, 0.8)",
|
||||||
|
"rgba(255, 205, 86, 0.8)",
|
||||||
|
],
|
||||||
|
borderWidth: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const siteVisitorsOptions: ChartOptions<"bar"> = {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
indexAxis: "y" as const,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// 브라우저별 접속자 차트 데이터
|
||||||
|
export const browserStatisticsData: ChartData<"doughnut"> = {
|
||||||
|
labels: ["Chrome", "Edge", "Safari", "Firefox", "Internet Explorer"],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
data: [45, 25, 15, 10, 5],
|
||||||
|
backgroundColor: [
|
||||||
|
"rgba(54, 162, 235, 0.8)",
|
||||||
|
"rgba(153, 102, 255, 0.8)",
|
||||||
|
"rgba(255, 99, 132, 0.8)",
|
||||||
|
"rgba(255, 159, 64, 0.8)",
|
||||||
|
"rgba(255, 205, 86, 0.8)",
|
||||||
|
],
|
||||||
|
borderColor: [
|
||||||
|
"rgba(54, 162, 235, 1)",
|
||||||
|
"rgba(153, 102, 255, 1)",
|
||||||
|
"rgba(255, 99, 132, 1)",
|
||||||
|
"rgba(255, 159, 64, 1)",
|
||||||
|
"rgba(255, 205, 86, 1)",
|
||||||
|
],
|
||||||
|
borderWidth: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const browserStatisticsOptions: ChartOptions<"doughnut"> = {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
position: "bottom" as const,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const chartData = {
|
||||||
|
monthlyStatisticsData,
|
||||||
|
monthlyStatisticsOptions,
|
||||||
|
siteVisitorsData,
|
||||||
|
siteVisitorsOptions,
|
||||||
|
browserStatisticsData,
|
||||||
|
browserStatisticsOptions,
|
||||||
|
};
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { createColumnHelper } from "@tanstack/react-table";
|
||||||
|
import { type AccessLog } from "./user-statistics-interface";
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<AccessLog>();
|
||||||
|
|
||||||
|
export const columns = [
|
||||||
|
columnHelper.accessor("no", {
|
||||||
|
header: "No",
|
||||||
|
cell: (info) => info.getValue(),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("accessTime", {
|
||||||
|
header: "접속일시",
|
||||||
|
cell: (info) => info.getValue(),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("accessIp", {
|
||||||
|
header: "접속IP",
|
||||||
|
cell: (info) => info.getValue(),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("accessRegion", {
|
||||||
|
header: "접속지역",
|
||||||
|
cell: (info) => info.getValue(),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("urlPath", {
|
||||||
|
header: "URL경로",
|
||||||
|
cell: (info) => info.getValue(),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("browser", {
|
||||||
|
header: "브라우저",
|
||||||
|
cell: (info) => info.getValue(),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("site", {
|
||||||
|
header: "사이트",
|
||||||
|
cell: (info) => info.getValue(),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("accessType", {
|
||||||
|
header: "메뉴명",
|
||||||
|
cell: (info) => info.getValue(),
|
||||||
|
}),
|
||||||
|
];
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
export interface AccessLog {
|
||||||
|
no: number;
|
||||||
|
accessTime: string;
|
||||||
|
accessIp: string;
|
||||||
|
accessRegion: string;
|
||||||
|
urlPath: string;
|
||||||
|
browser: string;
|
||||||
|
site: string;
|
||||||
|
accessType: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AccessLogParams {
|
||||||
|
page?: number;
|
||||||
|
size?: number;
|
||||||
|
urlPath?: string;
|
||||||
|
accessIp?: string;
|
||||||
|
accessRegion?: string;
|
||||||
|
browser?: string;
|
||||||
|
accessDateStart?: string;
|
||||||
|
accessDateEnd?: string;
|
||||||
|
siteId?: string;
|
||||||
|
sort?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AccessLogService {
|
||||||
|
getAccessLogList(params: AccessLogParams): Promise<PageableList<AccessLog>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PageableList<T> {
|
||||||
|
content: T[];
|
||||||
|
pageable: {
|
||||||
|
pageNumber: number;
|
||||||
|
pageSize: number;
|
||||||
|
sort: {
|
||||||
|
empty: boolean;
|
||||||
|
sorted: boolean;
|
||||||
|
unsorted: boolean;
|
||||||
|
};
|
||||||
|
offset: number;
|
||||||
|
paged: boolean;
|
||||||
|
unpaged: boolean;
|
||||||
|
};
|
||||||
|
last: boolean;
|
||||||
|
totalPages: number;
|
||||||
|
totalElements: number;
|
||||||
|
size: number;
|
||||||
|
number: number;
|
||||||
|
sort: {
|
||||||
|
empty: boolean;
|
||||||
|
sorted: boolean;
|
||||||
|
unsorted: boolean;
|
||||||
|
};
|
||||||
|
first: boolean;
|
||||||
|
numberOfElements: number;
|
||||||
|
empty: boolean;
|
||||||
|
}
|
||||||
@@ -0,0 +1,324 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||||
|
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||||
|
import {
|
||||||
|
Chart as ChartJS,
|
||||||
|
CategoryScale,
|
||||||
|
LinearScale,
|
||||||
|
PointElement,
|
||||||
|
LineElement,
|
||||||
|
BarElement,
|
||||||
|
ArcElement,
|
||||||
|
Filler,
|
||||||
|
Legend,
|
||||||
|
Tooltip,
|
||||||
|
type ChartData,
|
||||||
|
} from "chart.js";
|
||||||
|
import { Bar, Doughnut } from "react-chartjs-2";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Input,
|
||||||
|
Label,
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@common/components";
|
||||||
|
import { SiteSelect } from "@/entities/site";
|
||||||
|
import { useDataTable } from "@/shared/lib";
|
||||||
|
import { InputDateRangePicker } from "@/shared/ui/input-date-range-picker";
|
||||||
|
import { UserStatisticsApi } from "../api";
|
||||||
|
import { chartData } from "../model/chart-data";
|
||||||
|
import { columns } from "../model/table-columns";
|
||||||
|
import { type AccessLogParams } from "../model/user-statistics-interface";
|
||||||
|
|
||||||
|
ChartJS.register(
|
||||||
|
CategoryScale,
|
||||||
|
LinearScale,
|
||||||
|
PointElement,
|
||||||
|
LineElement,
|
||||||
|
BarElement,
|
||||||
|
ArcElement,
|
||||||
|
Filler,
|
||||||
|
Legend,
|
||||||
|
Tooltip,
|
||||||
|
);
|
||||||
|
|
||||||
|
type DateRange = {
|
||||||
|
from?: Date | undefined;
|
||||||
|
to?: Date | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
function UserStatisticsPage() {
|
||||||
|
const defaultDateRange: DateRange = {
|
||||||
|
from: dayjs().subtract(7, "day").toDate(),
|
||||||
|
to: dayjs().toDate(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const [pagination, setPagination] = useState<PaginationState>({
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: 10,
|
||||||
|
});
|
||||||
|
const [keywordType, setKeywordType] = useState("");
|
||||||
|
const [keyword, setKeyword] = useState("");
|
||||||
|
const [presetDateRange, setPresetDateRange] = useState("1W");
|
||||||
|
const [dateRange, setDateRange] = useState<DateRange>(defaultDateRange);
|
||||||
|
const [searchParams, setSearchParams] = useState<AccessLogParams>({
|
||||||
|
urlPath: "",
|
||||||
|
accessIp: "",
|
||||||
|
accessRegion: "",
|
||||||
|
browser: "",
|
||||||
|
accessDateStart: dayjs().subtract(7, "day").format("YYYY-MM-DD"),
|
||||||
|
accessDateEnd: dayjs().format("YYYY-MM-DD"),
|
||||||
|
sort: "accessTime,desc",
|
||||||
|
});
|
||||||
|
|
||||||
|
const query = useQuery({
|
||||||
|
queryKey: ["user-statistics", searchParams, pagination],
|
||||||
|
queryFn: () =>
|
||||||
|
UserStatisticsApi.getAccessLogList({
|
||||||
|
...searchParams,
|
||||||
|
page: pagination.pageIndex,
|
||||||
|
size: pagination.pageSize,
|
||||||
|
}),
|
||||||
|
placeholderData: keepPreviousData,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { tableComponent, tableInstance: table } = useDataTable({
|
||||||
|
query,
|
||||||
|
tableOptions: {
|
||||||
|
columns,
|
||||||
|
state: {
|
||||||
|
pagination,
|
||||||
|
},
|
||||||
|
manualPagination: true,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
onPaginationChange: setPagination,
|
||||||
|
},
|
||||||
|
dataTableOptions: {
|
||||||
|
viewPagination: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handlePresetDateRange = (value: string) => {
|
||||||
|
let presetDateRange: DateRange;
|
||||||
|
switch (value) {
|
||||||
|
case "1W":
|
||||||
|
presetDateRange = {
|
||||||
|
from: dayjs().subtract(7, "day").toDate(),
|
||||||
|
to: dayjs().toDate(),
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case "1M":
|
||||||
|
presetDateRange = {
|
||||||
|
from: dayjs().subtract(30, "day").toDate(),
|
||||||
|
to: dayjs().toDate(),
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case "3M":
|
||||||
|
presetDateRange = {
|
||||||
|
from: dayjs().subtract(90, "day").toDate(),
|
||||||
|
to: dayjs().toDate(),
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
presetDateRange = defaultDateRange;
|
||||||
|
}
|
||||||
|
setPresetDateRange(value);
|
||||||
|
setDateRange(presetDateRange);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDateRangeChange = (dateRange: DateRange) => {
|
||||||
|
setDateRange(dateRange);
|
||||||
|
setPresetDateRange("");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setPagination({
|
||||||
|
...pagination,
|
||||||
|
pageIndex: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 검색 조건에 따라 검색 파라미터 설정
|
||||||
|
const params: AccessLogParams = {
|
||||||
|
sort: "accessTime,desc",
|
||||||
|
accessDateStart: dateRange.from
|
||||||
|
? dayjs(dateRange.from).format("YYYY-MM-DD")
|
||||||
|
: "",
|
||||||
|
accessDateEnd: dateRange.to
|
||||||
|
? dayjs(dateRange.to).format("YYYY-MM-DD")
|
||||||
|
: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
// 검색어 타입에 따라 파라미터 설정
|
||||||
|
switch (keywordType) {
|
||||||
|
case "urlPath":
|
||||||
|
params.urlPath = keyword;
|
||||||
|
break;
|
||||||
|
case "ip":
|
||||||
|
params.accessIp = keyword;
|
||||||
|
break;
|
||||||
|
case "region":
|
||||||
|
params.accessRegion = keyword;
|
||||||
|
break;
|
||||||
|
case "browser":
|
||||||
|
params.browser = keyword;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSearchParams(params);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
setKeywordType("");
|
||||||
|
setKeyword("");
|
||||||
|
setDateRange(defaultDateRange);
|
||||||
|
setPresetDateRange("1W");
|
||||||
|
setSearchParams({
|
||||||
|
urlPath: "",
|
||||||
|
accessIp: "",
|
||||||
|
accessRegion: "",
|
||||||
|
browser: "",
|
||||||
|
accessDateStart: dayjs().subtract(7, "day").format("YYYY-MM-DD"),
|
||||||
|
accessDateEnd: dayjs().format("YYYY-MM-DD"),
|
||||||
|
sort: "accessTime,desc",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="card card-border !mt-6">
|
||||||
|
<div className="card card-border">
|
||||||
|
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
|
||||||
|
<Label className="mr-2">날짜</Label>
|
||||||
|
<Select value={presetDateRange} onValueChange={handlePresetDateRange}>
|
||||||
|
<SelectTrigger className="w-[12rem]">
|
||||||
|
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="1W">일주일</SelectItem>
|
||||||
|
<SelectItem value="1M">1개월</SelectItem>
|
||||||
|
<SelectItem value="3M">3개월</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<InputDateRangePicker
|
||||||
|
dateRange={dateRange}
|
||||||
|
setDateRange={handleDateRangeChange}
|
||||||
|
/>
|
||||||
|
<Label className="ml-4 mr-2 text-label">사이트</Label>
|
||||||
|
<SiteSelect enabledAllSite className="w-[16rem]" />
|
||||||
|
<Label className="ml-4 mr-2 text-label">검색</Label>
|
||||||
|
<Select
|
||||||
|
value={keywordType}
|
||||||
|
onValueChange={(value) => setKeywordType(value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-[3.2rem] w-[10rem]">
|
||||||
|
<SelectValue placeholder="선택" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="urlPath">URL경로</SelectItem>
|
||||||
|
<SelectItem value="ip">접속IP</SelectItem>
|
||||||
|
<SelectItem value="region">접속지역</SelectItem>
|
||||||
|
<SelectItem value="browser">브라우저</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Input
|
||||||
|
value={keyword}
|
||||||
|
onChange={(e) => setKeyword(e.target.value)}
|
||||||
|
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
|
||||||
|
/>
|
||||||
|
<Button className="h-[3.2rem]" type="submit">
|
||||||
|
조회
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
color="white"
|
||||||
|
className="h-[3.2rem]"
|
||||||
|
onClick={handleReset}
|
||||||
|
>
|
||||||
|
초기화
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 차트 영역 */}
|
||||||
|
<div className="grid grid-cols-2">
|
||||||
|
{/* 일별 접속 현황 */}
|
||||||
|
<div className="card card-border">
|
||||||
|
<div className="card-header">
|
||||||
|
<h3 className="text-xl font-medium">일별 접속 현황</h3>
|
||||||
|
</div>
|
||||||
|
<div className="h-96 p-4">
|
||||||
|
<Bar
|
||||||
|
data={chartData.monthlyStatisticsData as ChartData<"bar">}
|
||||||
|
options={chartData.monthlyStatisticsOptions}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2">
|
||||||
|
{/* 사이트별 접속자 */}
|
||||||
|
<div className="card card-border">
|
||||||
|
<div className="card-header">
|
||||||
|
<h3 className="text-xl font-medium">사이트별 접속자</h3>
|
||||||
|
</div>
|
||||||
|
<div className="h-96 p-4">
|
||||||
|
<Bar
|
||||||
|
data={chartData.siteVisitorsData}
|
||||||
|
options={chartData.siteVisitorsOptions}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 브라우저별 접속자 */}
|
||||||
|
<div className="card card-border">
|
||||||
|
<div className="card-header">
|
||||||
|
<h3 className="text-xl font-medium">브라우저별 접속자</h3>
|
||||||
|
</div>
|
||||||
|
<div className="h-96 p-4">
|
||||||
|
<Doughnut
|
||||||
|
data={chartData.browserStatisticsData}
|
||||||
|
options={chartData.browserStatisticsOptions}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 접속 로그 상세 */}
|
||||||
|
<div className="card card-border">
|
||||||
|
<div className="card-header">
|
||||||
|
<div className="flex w-full items-center justify-between">
|
||||||
|
<h3 className="text-2xl font-medium text-label">접속 로그 상세</h3>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Select
|
||||||
|
value={table.getState().pagination.pageSize.toString()}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
table.setPageSize(parseInt(value));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-[3.2rem] w-[12rem]">
|
||||||
|
<SelectValue placeholder="페이지 크기" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{[10, 20, 30, 40, 50].map((pageSize) => (
|
||||||
|
<SelectItem key={pageSize} value={pageSize.toString()}>
|
||||||
|
{pageSize}개씩 보기
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="card !m-[1.2rem] !p-0">{tableComponent}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { UserStatisticsPage };
|
||||||
@@ -8,7 +8,6 @@ import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
DateRangePicker,
|
|
||||||
Input,
|
Input,
|
||||||
Label,
|
Label,
|
||||||
Select,
|
Select,
|
||||||
@@ -25,10 +24,11 @@ import {
|
|||||||
} from "@/entities/user";
|
} from "@/entities/user";
|
||||||
import { ENDPOINT } from "@/shared/config";
|
import { ENDPOINT } from "@/shared/config";
|
||||||
import { useDataTable } from "@/shared/lib";
|
import { useDataTable } from "@/shared/lib";
|
||||||
|
import { InputDateRangePicker } from "@/shared/ui/input-date-range-picker";
|
||||||
import { columns } from "../model/table-columns";
|
import { columns } from "../model/table-columns";
|
||||||
|
|
||||||
type DateRange = {
|
type DateRange = {
|
||||||
from: Date | undefined;
|
from?: Date | undefined;
|
||||||
to?: Date | undefined;
|
to?: Date | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -39,7 +39,8 @@ function UsersPage() {
|
|||||||
pageIndex: 0,
|
pageIndex: 0,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
});
|
});
|
||||||
const [dateRange, setDateRange] = useState<DateRange | undefined>({
|
const [presetDateRange, setPresetDateRange] = useState("");
|
||||||
|
const [dateRange, setDateRange] = useState<DateRange>({
|
||||||
from: undefined,
|
from: undefined,
|
||||||
to: undefined,
|
to: undefined,
|
||||||
});
|
});
|
||||||
@@ -61,7 +62,7 @@ function UsersPage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { data: userSttsStats } = useQuery({
|
const { data: userSttsStats } = useQuery({
|
||||||
queryKey: [ENDPOINT.USER_SERVICE.USERS],
|
queryKey: [ENDPOINT.USER_SERVICE.USERS, "userStatusStatistics"],
|
||||||
queryFn: () => UserApi.selectUserSttsStats(),
|
queryFn: () => UserApi.selectUserSttsStats(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -144,7 +145,7 @@ function UsersPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handlePresetDateRange = (value: string) => {
|
const handlePresetDateRange = (value: string) => {
|
||||||
let presetDateRange: DateRange | undefined;
|
let presetDateRange: DateRange;
|
||||||
switch (value) {
|
switch (value) {
|
||||||
case "1M":
|
case "1M":
|
||||||
presetDateRange = {
|
presetDateRange = {
|
||||||
@@ -165,14 +166,24 @@ function UsersPage() {
|
|||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
presetDateRange = undefined;
|
presetDateRange = {
|
||||||
|
from: undefined,
|
||||||
|
to: undefined,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
setPresetDateRange(value);
|
||||||
setDateRange(presetDateRange);
|
setDateRange(presetDateRange);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getUserSttsStatsCnt = (code: string): number => {
|
const handleDateRangeChange = (dateRange: DateRange) => {
|
||||||
if (!Array.isArray(userSttsStats)) return 0;
|
setDateRange(dateRange);
|
||||||
return userSttsStats.find((item) => item.sttsCd === code)?.cnt ?? 0;
|
setPresetDateRange("");
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUserSttsStatsCnt = (code: string): string => {
|
||||||
|
if (!Array.isArray(userSttsStats)) return "0";
|
||||||
|
const count = userSttsStats.find((item) => item.sttsCd === code)?.cnt ?? 0;
|
||||||
|
return count.toLocaleString();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUserStateCodeChange = (code: string, checked: boolean) => {
|
const handleUserStateCodeChange = (code: string, checked: boolean) => {
|
||||||
@@ -277,23 +288,23 @@ function UsersPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Label className="mr-2 text-label">기간</Label>
|
<Label className="mr-2 text-label">기간</Label>
|
||||||
<DateRangePicker value={dateRange} onValueChange={setDateRange}>
|
<Select
|
||||||
<div className="card card-border flex items-center gap-2">
|
value={presetDateRange}
|
||||||
<Select onValueChange={handlePresetDateRange}>
|
onValueChange={handlePresetDateRange}
|
||||||
<SelectTrigger>
|
>
|
||||||
<SelectValue
|
<SelectTrigger className="w-[12rem]">
|
||||||
placeholder="기간 선택"
|
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
|
||||||
className="w-[24rem]"
|
</SelectTrigger>
|
||||||
/>
|
<SelectContent>
|
||||||
</SelectTrigger>
|
<SelectItem value="1M">1개월</SelectItem>
|
||||||
<SelectContent>
|
<SelectItem value="3M">3개월</SelectItem>
|
||||||
<SelectItem value="1M">1개월</SelectItem>
|
<SelectItem value="6M">6개월</SelectItem>
|
||||||
<SelectItem value="3M">3개월</SelectItem>
|
</SelectContent>
|
||||||
<SelectItem value="6M">6개월</SelectItem>
|
</Select>
|
||||||
</SelectContent>
|
<InputDateRangePicker
|
||||||
</Select>
|
dateRange={dateRange}
|
||||||
</div>
|
setDateRange={handleDateRangeChange}
|
||||||
</DateRangePicker>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export const ENDPOINT = {
|
|||||||
PRIVACYLOG: "/user-service//api/v1/prvc-inq-logs",
|
PRIVACYLOG: "/user-service//api/v1/prvc-inq-logs",
|
||||||
},
|
},
|
||||||
CMS_SERVICE: {
|
CMS_SERVICE: {
|
||||||
|
DASHBOARD: "/cms-service/api/v1/dashboard",
|
||||||
SITES: "/cms-service/api/v1/sites",
|
SITES: "/cms-service/api/v1/sites",
|
||||||
IPS: "/cms-service/api/v1/site-ips",
|
IPS: "/cms-service/api/v1/site-ips",
|
||||||
DOMAINS: "/cms-service/api/v1/site-dmns",
|
DOMAINS: "/cms-service/api/v1/site-dmns",
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { InputDatePicker } from "@common/components";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useAlertStore } from "../lib";
|
||||||
|
|
||||||
|
type DateRange = {
|
||||||
|
from?: Date;
|
||||||
|
to?: Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
type InputDateRangePickerProps = {
|
||||||
|
dateRange: DateRange;
|
||||||
|
setDateRange: (dateRange: DateRange) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
function InputDateRangePicker({
|
||||||
|
dateRange,
|
||||||
|
setDateRange,
|
||||||
|
}: InputDateRangePickerProps) {
|
||||||
|
const alert = useAlertStore((state) => state.setMessage);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (dateRange.from && dateRange.to && dateRange.from > dateRange.to) {
|
||||||
|
alert("시작일보다 종료일이 커야 합니다.");
|
||||||
|
setDateRange({ ...dateRange, to: dateRange.from });
|
||||||
|
}
|
||||||
|
}, [dateRange]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<InputDatePicker
|
||||||
|
value={dateRange.from}
|
||||||
|
onChange={(date) => {
|
||||||
|
setDateRange({ ...dateRange, from: date });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
~
|
||||||
|
<InputDatePicker
|
||||||
|
value={dateRange.to}
|
||||||
|
onChange={(date) => {
|
||||||
|
setDateRange({ ...dateRange, to: date });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { InputDateRangePicker };
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
NEXT_PUBLIC_SITE_ID = DM
|
NEXT_PUBLIC_SITE_ID = DM
|
||||||
NEXT_PUBLIC_PORT = 3000
|
NEXT_PUBLIC_PORT = 3000
|
||||||
NEXT_PUBLIC_PROXY_HOST = http://localhost:3000
|
NEXT_PUBLIC_PROXY_HOST = http://localhost:3000
|
||||||
NEXT_PUBLIC_SERVER_API_URL = http://con.mind-one.co.kr:11080/api/v1
|
NEXT_PUBLIC_DM_SERVER_API_URL = http://con.mind-one.co.kr:11080/api/v1
|
||||||
|
NEXT_PUBLIC_SERVER_API_URL = http://dev.onasys.co.kr:8000
|
||||||
NEXT_PUBLIC_FO_SERVER_API_URL = http://con.mind-one.co.kr:11082/api/v1
|
NEXT_PUBLIC_FO_SERVER_API_URL = http://con.mind-one.co.kr:11082/api/v1
|
||||||
NEXT_PUBLIC_CS_SERVER_API_URL = http://dev.onasys.co.kr:8000
|
NEXT_PUBLIC_CS_SERVER_API_URL = http://dev.onasys.co.kr:8000
|
||||||
NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload
|
NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload
|
||||||
@@ -9,9 +10,7 @@ NEXT_PUBLIC_CLAIM_NAME = Authorization
|
|||||||
NEXT_PUBLIC_AUTH_USER_ID = token-id
|
NEXT_PUBLIC_AUTH_USER_ID = token-id
|
||||||
NEXT_PUBLIC_ACCESS_TOKEN = access-token
|
NEXT_PUBLIC_ACCESS_TOKEN = access-token
|
||||||
NEXT_PUBLIC_REFRESH_TOKEN = refresh-token
|
NEXT_PUBLIC_REFRESH_TOKEN = refresh-token
|
||||||
|
NEXT_PUBLIC_REUSE_API_URL=http://con.mind-one.co.kr:11080/api/v1
|
||||||
|
|
||||||
|
|
||||||
NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api # 개발 서버 API
|
NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api # 개발 서버 API
|
||||||
# NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.128:8883/sewerage-service/api # 최용성
|
# NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.128:8883/sewerage-service/api # 최용성
|
||||||
#NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.66:8883/sewerage-service/api # 박이정
|
#NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.66:8883/sewerage-service/api # 박이정
|
||||||
|
|||||||
+7
-2
@@ -4,11 +4,16 @@ NEXT_PUBLIC_AUTH_USER_ID = token-id
|
|||||||
NEXT_PUBLIC_ACCESS_TOKEN = access-token
|
NEXT_PUBLIC_ACCESS_TOKEN = access-token
|
||||||
NEXT_PUBLIC_REFRESH_TOKEN = refresh-token
|
NEXT_PUBLIC_REFRESH_TOKEN = refresh-token
|
||||||
NEXT_PUBLIC_PORT = 11035
|
NEXT_PUBLIC_PORT = 11035
|
||||||
NEXT_PUBLIC_PROXY_HOST = http://con.mind-one.co.kr:11035
|
#NEXT_PUBLIC_PROXY_HOST = http://con.mind-one.co.kr:11035
|
||||||
NEXT_PUBLIC_SERVER_API_URL = http://con.mind-one.co.kr:11080/api/v1
|
NEXT_PUBLIC_PROXY_HOST = http://localhost:3000/
|
||||||
|
NEXT_PUBLIC_DM_SERVER_API_URL = http://con.mind-one.co.kr:11080/api/v1
|
||||||
|
NEXT_PUBLIC_SERVER_API_URL = http://dev.onasys.co.kr:8000
|
||||||
NEXT_PUBLIC_FO_SERVER_API_URL = http://con.mind-one.co.kr:11082/api/v1
|
NEXT_PUBLIC_FO_SERVER_API_URL = http://con.mind-one.co.kr:11082/api/v1
|
||||||
NEXT_PUBLIC_CS_SERVER_API_URL = http://dev.onasys.co.kr:8000
|
NEXT_PUBLIC_CS_SERVER_API_URL = http://dev.onasys.co.kr:8000
|
||||||
|
NEXT_PUBLIC_GEOSERVER_URL = http://rsone.iptime.org:10010/geoserver
|
||||||
|
NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver
|
||||||
NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload
|
NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload
|
||||||
|
NEXT_PUBLIC_REUSE_API_URL=http://con.mind-one.co.kr:11080/api/v1
|
||||||
NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api # 개발 서버 API
|
NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api # 개발 서버 API
|
||||||
#NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.128:8883/sewerage-service/api # 최용성
|
#NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.128:8883/sewerage-service/api # 최용성
|
||||||
#NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.66:8883/sewerage-service/api # 박이정
|
#NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.66:8883/sewerage-service/api # 박이정
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ NEXT_PUBLIC_CLAIM_NAME = Authorization
|
|||||||
NEXT_PUBLIC_AUTH_USER_ID = token-id
|
NEXT_PUBLIC_AUTH_USER_ID = token-id
|
||||||
NEXT_PUBLIC_ACCESS_TOKEN = access-token
|
NEXT_PUBLIC_ACCESS_TOKEN = access-token
|
||||||
NEXT_PUBLIC_REFRESH_TOKEN = refresh-token
|
NEXT_PUBLIC_REFRESH_TOKEN = refresh-token
|
||||||
|
NEXT_PUBLIC_REUSE_API_URL=http://con.mind-one.co.kr:11080/api/v1
|
||||||
NEXT_PUBLIC_SEWERAGE_API_URL = http://apigateway.iipbs-dev.svc.cluster.local:8000/sewerage-service/api
|
NEXT_PUBLIC_SEWERAGE_API_URL = http://apigateway.iipbs-dev.svc.cluster.local:8000/sewerage-service/api
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
"use client";
|
||||||
|
import Base from "../base";
|
||||||
|
import "../globals.css";
|
||||||
|
import { StoreProvider } from "../store";
|
||||||
|
|
||||||
|
export default function BaseLayout({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) {
|
||||||
|
return (
|
||||||
|
<StoreProvider>
|
||||||
|
<Base>{children}</Base>
|
||||||
|
</StoreProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
+10
-1
@@ -9,6 +9,7 @@ import { Toaster } from 'react-hot-toast';
|
|||||||
import SdmLogin from '../sdmLogin/page';
|
import SdmLogin from '../sdmLogin/page';
|
||||||
import type { DemandCompType } from '@/app/store/sdmMng';
|
import type { DemandCompType } from '@/app/store/sdmMng';
|
||||||
import TransValueFormat from '@/app/common/transValueFormat';
|
import TransValueFormat from '@/app/common/transValueFormat';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
const TABLE_STYLES = {
|
const TABLE_STYLES = {
|
||||||
header: "text-center border border-[var(--border)] bg-[rgb(234,243,251)] text-[rgb(100,119,132)] whitespace-pre-line",
|
header: "text-center border border-[var(--border)] bg-[rgb(234,243,251)] text-[rgb(100,119,132)] whitespace-pre-line",
|
||||||
@@ -22,6 +23,9 @@ type SdmApplyProps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function SdmApplyList({}: SdmApplyProps) {
|
export default function SdmApplyList({}: SdmApplyProps) {
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const {getSdmInfoStatusList, Sdm, Common} = useDmStore((state) => state);
|
const {getSdmInfoStatusList, Sdm, Common} = useDmStore((state) => state);
|
||||||
const {successLogin, sdmInfoStatusList, upHeadSdmInfoStatus} = Sdm;
|
const {successLogin, sdmInfoStatusList, upHeadSdmInfoStatus} = Sdm;
|
||||||
const {selectSidoData, selectSigunData} = Common;
|
const {selectSidoData, selectSigunData} = Common;
|
||||||
@@ -185,7 +189,12 @@ export default function SdmApplyList({}: SdmApplyProps) {
|
|||||||
body={<SdmLogin/>}
|
body={<SdmLogin/>}
|
||||||
bodyClass="sm:max-w-[90vw] w-[530px] h-[430px]"
|
bodyClass="sm:max-w-[90vw] w-[530px] h-[430px]"
|
||||||
open={!isLogin}
|
open={!isLogin}
|
||||||
onOpenChange={(open) => !open && setIsLogin(false)}
|
onOpenChange={(open) => {
|
||||||
|
if(!open){
|
||||||
|
setIsLogin(false)
|
||||||
|
router.push('/');
|
||||||
|
}
|
||||||
|
}}
|
||||||
noHeaderYn="Y"
|
noHeaderYn="Y"
|
||||||
noFooterYn="Y"
|
noFooterYn="Y"
|
||||||
/>
|
/>
|
||||||
+77
-34
@@ -6,7 +6,7 @@ import { SdmInfoRegForm } from "@/components/ui/sdmInfoInsertForm/component";
|
|||||||
import { UiModal } from "@common/business_components/ui/dialog_ui";
|
import { UiModal } from "@common/business_components/ui/dialog_ui";
|
||||||
import { Button } from "@common/components/ui";
|
import { Button } from "@common/components/ui";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useSearchParams } from "next/navigation";
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
import { Suspense, useEffect, useState } from "react";
|
import { Suspense, useEffect, useState } from "react";
|
||||||
import { FormProvider, useForm, type SubmitHandler } from "react-hook-form";
|
import { FormProvider, useForm, type SubmitHandler } from "react-hook-form";
|
||||||
import { Toaster } from "react-hot-toast";
|
import { Toaster } from "react-hot-toast";
|
||||||
@@ -24,61 +24,64 @@ const sdmInfoFormSchema = z.object({
|
|||||||
money: z.string()
|
money: z.string()
|
||||||
.regex(/^[0-9]+$/, "희망단가는 숫자만 입력 가능합니다"),
|
.regex(/^[0-9]+$/, "희망단가는 숫자만 입력 가능합니다"),
|
||||||
/**요구수질 */
|
/**요구수질 */
|
||||||
reqWaterQlty : z.string().optional(),
|
reqWaterQlty: z.string().optional(),
|
||||||
grssClfg: z.string()
|
grssClfg: z.string()
|
||||||
.min(1, "총대장균군수를 입력해주세요")
|
.min(1, "총대장균군수를 입력해주세요")
|
||||||
.regex(/^[0-9]+$/, "총대장균군수는 숫자만 입력 가능합니다"),
|
.regex(/^[0-9]+$/, "총대장균군수는 숫자만 입력 가능합니다"),
|
||||||
tn : z.string()
|
tn: z.string()
|
||||||
.min(1, "총질소를 입력해주세요")
|
.min(1, "총질소를 입력해주세요")
|
||||||
.regex(/^[0-9]+$/, "총질소는 숫자만 입력 가능합니다"),
|
.regex(/^[0-9]+$/, "총질소는 숫자만 입력 가능합니다"),
|
||||||
combResdch : z.string()
|
combResdch: z.string()
|
||||||
.min(1, "결합잔류염소를 입력해주세요")
|
.min(1, "결합잔류염소를 입력해주세요")
|
||||||
.regex(/^[0-9]+$/, "결합잔류염소는 숫자만 입력 가능합니다"),
|
.regex(/^[0-9]+$/, "결합잔류염소는 숫자만 입력 가능합니다"),
|
||||||
tph : z.string()
|
tph: z.string()
|
||||||
.min(1, "총인산물을 입력해주세요")
|
.min(1, "총인산물을 입력해주세요")
|
||||||
.regex(/^[0-9]+$/, "총인산물은 숫자만 입력 가능합니다"),
|
.regex(/^[0-9]+$/, "총인산물은 숫자만 입력 가능합니다"),
|
||||||
trbt : z.string()
|
trbt: z.string()
|
||||||
.min(1, "탁도를 입력해주세요")
|
.min(1, "탁도를 입력해주세요")
|
||||||
.regex(/^[0-9]+$/, "탁도는 숫자만 입력 가능합니다"),
|
.regex(/^[0-9]+$/, "탁도는 숫자만 입력 가능합니다"),
|
||||||
ph : z.string()
|
ph: z.string()
|
||||||
.min(1, "수소이온농도를 입력해주세요")
|
.min(1, "수소이온농도를 입력해주세요")
|
||||||
.regex(/^[0-9]+$/, "수소이온농도는 숫자만 입력 가능합니다"),
|
.regex(/^[0-9]+$/, "수소이온농도는 숫자만 입력 가능합니다"),
|
||||||
bod : z.string()
|
bod: z.string()
|
||||||
.min(1, "생물학적 산소요구량을 입력해주세요")
|
.min(1, "생물학적 산소요구량을 입력해주세요")
|
||||||
.regex(/^[0-9]+$/, "생물학적 산소요구량은 숫자만 입력 가능합니다"),
|
.regex(/^[0-9]+$/, "생물학적 산소요구량은 숫자만 입력 가능합니다"),
|
||||||
/**컬럼확인필요 */
|
/**컬럼확인필요 */
|
||||||
clIonQnt : z.string()
|
clIonQnt: z.string()
|
||||||
.min(1, "염화물을 입력해주세요")
|
.min(1, "염화물을 입력해주세요")
|
||||||
.regex(/^[0-9]+$/, "염화물은 숫자만 입력 가능합니다"),
|
.regex(/^[0-9]+$/, "염화물은 숫자만 입력 가능합니다"),
|
||||||
smll : z.string()
|
smll: z.string()
|
||||||
.min(1, "냄새를 입력해주세요")
|
.min(1, "냄새를 입력해주세요")
|
||||||
.regex(/^[0-9]+$/, "냄새는 숫자만 입력 가능합니다"),
|
.regex(/^[0-9]+$/, "냄새는 숫자만 입력 가능합니다"),
|
||||||
cdtnv : z.string()
|
cdtnv: z.string()
|
||||||
.min(1, "전기전도도를 입력해주세요")
|
.min(1, "전기전도도를 입력해주세요")
|
||||||
.regex(/^[0-9]+$/, "전기전도도는 숫자만 입력 가능합니다"),
|
.regex(/^[0-9]+$/, "전기전도도는 숫자만 입력 가능합니다"),
|
||||||
crmty : z.string()
|
crmty: z.string()
|
||||||
.min(1, "색도를 입력해주세요")
|
.min(1, "색도를 입력해주세요")
|
||||||
.regex(/^[0-9]+$/, "색도는 숫자만 입력 가능합니다"),
|
.regex(/^[0-9]+$/, "색도는 숫자만 입력 가능합니다"),
|
||||||
rmrk : z.string().optional(),
|
rmrk: z.string().optional(),
|
||||||
/* telno: z.string()
|
/* telno: z.string()
|
||||||
.min(8, "전화번호는 8자 이상 입력해주세요")
|
.min(8, "전화번호는 8자 이상 입력해주세요")
|
||||||
.max(12, "전화번호는 12자 이내로 입력해주세요")
|
.max(12, "전화번호는 12자 이내로 입력해주세요")
|
||||||
.regex(/^[0-9-]+$/, "전화번호는 숫자와 하이픈(-)만 입력 가능합니다"), */
|
.regex(/^[0-9-]+$/, "전화번호는 숫자와 하이픈(-)만 입력 가능합니다"), */
|
||||||
candidateTitle: z.string().optional(),
|
candidateTitle: z.array(z.object({
|
||||||
|
ruseUtztnFcltNm: z.string(),
|
||||||
|
ruseUtztnFcltCd: z.string(),
|
||||||
|
})).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type FormData = z.infer<typeof sdmInfoFormSchema>;
|
export type FormData = z.infer<typeof sdmInfoFormSchema>;
|
||||||
|
|
||||||
function SdmInfoInsertContent() {
|
function SdmInfoInsertContent() {
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const {insertSdmInfoData, Sdm} = useDmStore((state) => state);
|
const {insertSdmInfoData, Sdm ,setSubsInfo} = useDmStore((state) => state);
|
||||||
const {successLogin} = Sdm;
|
const { successLogin } = Sdm;
|
||||||
const [ismoneyDisabled, setIsmoneyDisabled] = useState(false);
|
const [ismoneyDisabled, setIsmoneyDisabled] = useState(false);
|
||||||
const formSchema = sdmInfoFormSchema;
|
const formSchema = sdmInfoFormSchema;
|
||||||
|
|
||||||
const [isLogin, setIsLogin] = useState(false);
|
const [isLogin, setIsLogin] = useState(false);
|
||||||
const [facilityName, setFacilityName] = useState<string>('');
|
const [facilityName, setFacilityName] = useState<string>('');
|
||||||
const [defaultValues, setDefaultValues] = useState<FormData>({
|
const [defaultValues, setDefaultValues] = useState<FormData>({
|
||||||
sdmNm: '',
|
sdmNm: '',
|
||||||
demandYmd: new Date(),
|
demandYmd: new Date(),
|
||||||
demandQnty: '',
|
demandQnty: '',
|
||||||
@@ -100,9 +103,15 @@ function SdmInfoInsertContent() {
|
|||||||
crmty: '',
|
crmty: '',
|
||||||
rmrk: '',
|
rmrk: '',
|
||||||
/** 후보지 제목 */
|
/** 후보지 제목 */
|
||||||
candidateTitle: '',
|
candidateTitle: [
|
||||||
|
{ ruseUtztnFcltNm: '', ruseUtztnFcltCd: '' },
|
||||||
|
{ ruseUtztnFcltNm: '', ruseUtztnFcltCd: '' },
|
||||||
|
{ ruseUtztnFcltNm: '', ruseUtztnFcltCd: '' },
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const user = sessionStorage.getItem('user');
|
const user = sessionStorage.getItem('user');
|
||||||
setDefaultValues(prev => ({
|
setDefaultValues(prev => ({
|
||||||
@@ -115,10 +124,12 @@ function SdmInfoInsertContent() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const local = localStorage.getItem('user');
|
const local = localStorage.getItem('user');
|
||||||
if(local) {
|
if (local) {
|
||||||
setIsLogin(true);
|
setIsLogin(true);
|
||||||
}
|
}
|
||||||
}, []);
|
setSubsInfo([]); // subsInfo 초기화 후보지 처음 화면 들어올때 초기화(스토어내 객체만)
|
||||||
|
|
||||||
|
}, [setSubsInfo]);
|
||||||
|
|
||||||
const form = useForm<FormData>({
|
const form = useForm<FormData>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
@@ -126,6 +137,28 @@ function SdmInfoInsertContent() {
|
|||||||
mode: "onChange",
|
mode: "onChange",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const subsInfo = useDmStore((state) => state.map.subsInfo);
|
||||||
|
|
||||||
|
useEffect(()=>{
|
||||||
|
//map.subsInfo 스토어 객체 값이 변경되면 해당 값이 폼에 자동으로 들어가도록 수정 대신 값이 있어야함
|
||||||
|
if(subsInfo.length>0){
|
||||||
|
form.setValue('candidateTitle', subsInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
},[subsInfo, form])
|
||||||
|
|
||||||
|
//후보지 팝업 값 셋팅
|
||||||
|
useEffect(() => {
|
||||||
|
const handleMessage = (event: MessageEvent) => {
|
||||||
|
if (event.data?.type === 'SET_SUBS_INFO') {
|
||||||
|
setSubsInfo(event.data.payload); // zustand로 값 설정
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('message', handleMessage);
|
||||||
|
return () => window.removeEventListener('message', handleMessage);
|
||||||
|
}, [setSubsInfo]);
|
||||||
|
|
||||||
// 로그인 성공 시 모달 닫기 및 폼 업데이트
|
// 로그인 성공 시 모달 닫기 및 폼 업데이트
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (successLogin) {
|
if (successLogin) {
|
||||||
@@ -138,6 +171,7 @@ function SdmInfoInsertContent() {
|
|||||||
setFacilityName(userData.dmdCoNm);
|
setFacilityName(userData.dmdCoNm);
|
||||||
form.setValue('sdmNm', userData.dmdCoNm);
|
form.setValue('sdmNm', userData.dmdCoNm);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error parsing user data:', error);
|
console.error('Error parsing user data:', error);
|
||||||
}
|
}
|
||||||
@@ -152,11 +186,11 @@ function SdmInfoInsertContent() {
|
|||||||
form.setValue('money', '');
|
form.setValue('money', '');
|
||||||
}
|
}
|
||||||
}, [ismoneyDisabled, form]);
|
}, [ismoneyDisabled, form]);
|
||||||
|
const router = useRouter();
|
||||||
const onSubmit: SubmitHandler<FormData> = async (data) => {
|
const onSubmit: SubmitHandler<FormData> = async (data) => {
|
||||||
try {
|
try {
|
||||||
// Transform form data to match API schema
|
// Transform form data to match API schema
|
||||||
const apiData : DemandCompType = {
|
const apiData: DemandCompType = {
|
||||||
dmdInfoId: JSON.parse(sessionStorage.getItem('user') || '{}')?.dmdInfoId?.toString() || searchParams.get('dmdInfoId') + (searchParams.get('bznmTotNo') || ''),
|
dmdInfoId: JSON.parse(sessionStorage.getItem('user') || '{}')?.dmdInfoId?.toString() || searchParams.get('dmdInfoId') + (searchParams.get('bznmTotNo') || ''),
|
||||||
dmdInfoFk: JSON.parse(sessionStorage.getItem('user') || '{}')?.dmdInfoId?.toString() || searchParams.get('dmdInfoId') + (searchParams.get('bznmTotNo') || ''),
|
dmdInfoFk: JSON.parse(sessionStorage.getItem('user') || '{}')?.dmdInfoId?.toString() || searchParams.get('dmdInfoId') + (searchParams.get('bznmTotNo') || ''),
|
||||||
dmdCoNm: JSON.parse(sessionStorage.getItem('user') || '{}')?.dmdCoNm?.toString() || data.sdmNm,
|
dmdCoNm: JSON.parse(sessionStorage.getItem('user') || '{}')?.dmdCoNm?.toString() || data.sdmNm,
|
||||||
@@ -186,17 +220,22 @@ function SdmInfoInsertContent() {
|
|||||||
index: 0,
|
index: 0,
|
||||||
reqDt: "",
|
reqDt: "",
|
||||||
dmdInfoSn: 0,
|
dmdInfoSn: 0,
|
||||||
status : '',
|
status: '',
|
||||||
|
splyCndtList: data.candidateTitle || [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
await insertSdmInfoData(apiData);
|
await insertSdmInfoData(apiData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error submitting form:', error);
|
console.error('Error submitting form:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50">
|
<div className="min-h-screen bg-[#F2F2F2]">
|
||||||
<div className="container mx-auto px-4 py-8">
|
<div className="container mx-auto px-4 py-8">
|
||||||
<div className="text-center mb-8">
|
<div className="text-center mb-8">
|
||||||
<h1 className="text-4xl font-bold text-gray-900 underline">
|
<h1 className="text-4xl font-bold text-gray-900 underline">
|
||||||
@@ -207,14 +246,14 @@ function SdmInfoInsertContent() {
|
|||||||
<div className="max-w-3xl mx-auto bg-white rounded-lg shadow-md p-6">
|
<div className="max-w-3xl mx-auto bg-white rounded-lg shadow-md p-6">
|
||||||
<FormProvider {...form}>
|
<FormProvider {...form}>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||||
<SdmInfoRegForm
|
<SdmInfoRegForm
|
||||||
form={form}
|
form={form}
|
||||||
onCheckboxChange={setIsmoneyDisabled}
|
onCheckboxChange={setIsmoneyDisabled}
|
||||||
facilityName={facilityName}
|
facilityName={facilityName}
|
||||||
/>
|
/>
|
||||||
<div className="flex justify-center pt-4">
|
<div className="flex justify-center pt-4">
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="w-40 bg-blue-600 hover:bg-blue-700 text-white font-semibold py-2 px-4 rounded-md transition-colors duration-200"
|
className="w-40 bg-blue-600 hover:bg-blue-700 text-white font-semibold py-2 px-4 rounded-md transition-colors duration-200"
|
||||||
>
|
>
|
||||||
수요정보 등록
|
수요정보 등록
|
||||||
@@ -225,16 +264,20 @@ function SdmInfoInsertContent() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!isLogin?
|
{!isLogin ?
|
||||||
<UiModal
|
<UiModal
|
||||||
body={<SdmLogin/>}
|
body={<SdmLogin />}
|
||||||
bodyClass="sm:max-w-[90vw] w-[530px] h-[430px]"
|
bodyClass="sm:max-w-[90vw] w-[530px] h-[430px]"
|
||||||
open={!isLogin}
|
open={!isLogin}
|
||||||
onOpenChange={(open) => !open && setIsLogin(false)}
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
router.push("/");
|
||||||
|
}
|
||||||
|
}}
|
||||||
noHeaderYn="Y"
|
noHeaderYn="Y"
|
||||||
noFooterYn="Y"
|
noFooterYn="Y"
|
||||||
/>
|
/>
|
||||||
:null}
|
: null}
|
||||||
<Toaster position="top-center" />
|
<Toaster position="top-center" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
+15
-12
@@ -8,7 +8,7 @@ import { useCallback, useEffect, useRef } from "react";
|
|||||||
import { Toaster } from "react-hot-toast";
|
import { Toaster } from "react-hot-toast";
|
||||||
|
|
||||||
export default function MyForm() {
|
export default function MyForm() {
|
||||||
const {Common, initializeCommonData, getDemandCompList, insertSdmData, Sdm} = useDmStore((state) => state);
|
const { Common, initializeCommonData, getDemandCompList, insertSdmData, Sdm } = useDmStore((state) => state);
|
||||||
const { selectSidoData, selectSigunData } = Common;
|
const { selectSidoData, selectSigunData } = Common;
|
||||||
const { demandCompList, successNextPage } = Sdm;
|
const { demandCompList, successNextPage } = Sdm;
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -33,10 +33,10 @@ export default function MyForm() {
|
|||||||
router.push(`/pages/sdmMng/sdmInfoInsert?${queryParams.toString()}`);
|
router.push(`/pages/sdmMng/sdmInfoInsert?${queryParams.toString()}`);
|
||||||
}
|
}
|
||||||
}, [successNextPage, router]);
|
}, [successNextPage, router]);
|
||||||
|
|
||||||
const callback = useCallback((searchText: string, pageCnt: number) => {
|
const callback = useCallback((searchText: string, pageCnt: number) => {
|
||||||
searchText = searchText.trim();
|
searchText = searchText.trim();
|
||||||
if(searchText.length > 0) {
|
if (searchText.length > 0) {
|
||||||
getDemandCompList(searchText, pageCnt);
|
getDemandCompList(searchText, pageCnt);
|
||||||
}
|
}
|
||||||
}, [getDemandCompList]);
|
}, [getDemandCompList]);
|
||||||
@@ -47,6 +47,7 @@ export default function MyForm() {
|
|||||||
dmdInfoSn: '',
|
dmdInfoSn: '',
|
||||||
dmdInfoFk: '',
|
dmdInfoFk: '',
|
||||||
status: '',
|
status: '',
|
||||||
|
splyCndtList: []
|
||||||
};
|
};
|
||||||
formDataRef.current = convertedData;
|
formDataRef.current = convertedData;
|
||||||
insertSdmData(convertedData).catch(error => {
|
insertSdmData(convertedData).catch(error => {
|
||||||
@@ -55,15 +56,17 @@ export default function MyForm() {
|
|||||||
}, [insertSdmData]);
|
}, [insertSdmData]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50">
|
<div className="min-h-screen bg-[#F2F2F2]">
|
||||||
<SdmInsertForm
|
<div className="pt-12">
|
||||||
selectSidoData={selectSidoData}
|
<SdmInsertForm
|
||||||
selectSigunData={selectSigunData}
|
selectSidoData={selectSidoData}
|
||||||
dataList={demandCompList}
|
selectSigunData={selectSigunData}
|
||||||
rtnValue={callback}
|
dataList={demandCompList}
|
||||||
rtnInputData={handleInsertSdmData}
|
rtnValue={callback}
|
||||||
/>
|
rtnInputData={handleInsertSdmData}
|
||||||
<Toaster position="top-center" />
|
/>
|
||||||
|
<Toaster position="top-center" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,339 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import Image from 'next/image';
|
||||||
|
|
||||||
|
const NAVS = [
|
||||||
|
{ id: 'card1', label: '물재이용이란' },
|
||||||
|
{ id: 'card2', label: '물재이용 필요성' },
|
||||||
|
{ id: 'card3', label: '빗물 재이용' },
|
||||||
|
{ id: 'card4', label: '중수 재이용' },
|
||||||
|
{ id: 'card5', label: '하수 재이용' },
|
||||||
|
{ id: 'card6', label: '물재이용 목표' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function WrAboutPage() {
|
||||||
|
const [activeIdx, setActiveIdx] = useState(0);
|
||||||
|
const navRefs = useRef<(HTMLLIElement | null)[]>([]);
|
||||||
|
const cardRefs = useRef<(HTMLElement | null)[]>([]);
|
||||||
|
|
||||||
|
// 클릭 시 이동
|
||||||
|
const handleNavClick = (idx: number) => {
|
||||||
|
setActiveIdx(idx);
|
||||||
|
const targetCard = cardRefs.current[idx];
|
||||||
|
if (targetCard) {
|
||||||
|
targetCard.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 키보드 이벤트 처리
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent, idx: number) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
handleNavClick(idx);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const observerOptions = {
|
||||||
|
root: null,
|
||||||
|
rootMargin: '-200px 0px 0px 0px', // 상단에서 200px 지점을 기준으로
|
||||||
|
threshold: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver((entries) => {
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
const idx = cardRefs.current.findIndex(ref => ref === entry.target);
|
||||||
|
if (idx !== -1) {
|
||||||
|
setActiveIdx(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, observerOptions);
|
||||||
|
|
||||||
|
// 각 카드 요소를 관찰
|
||||||
|
const cardsSnapshot = [...cardRefs.current];
|
||||||
|
|
||||||
|
cardsSnapshot.forEach((card) => {
|
||||||
|
if (card) {
|
||||||
|
observer.observe(card);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cardsSnapshot.forEach((card) => {
|
||||||
|
if (card) {
|
||||||
|
observer.unobserve(card);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='bg-[#F2F2F2]'>
|
||||||
|
<main className="relative w-[950px] mx-auto pt-[100px] font-sans">
|
||||||
|
{/* 사이드바 */}
|
||||||
|
<nav className="fixed left-[290px] top-[195px] w-[210px] z-50" aria-label="물재이용 정보 섹션 내비게이션">
|
||||||
|
<ul role="list" className="relative space-y-6">
|
||||||
|
{NAVS.map((nav, idx) => (
|
||||||
|
<li
|
||||||
|
key={nav.id}
|
||||||
|
ref={(el) => { navRefs.current[idx] = el; }}
|
||||||
|
role="listitem"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-current={idx === activeIdx ? 'true' : 'false'}
|
||||||
|
aria-label={`현재 위치: ${nav.label}`}
|
||||||
|
onClick={() => handleNavClick(idx)}
|
||||||
|
onKeyDown={(e) => handleKeyDown(e, idx)}
|
||||||
|
className={`font-NotoSansKR nav-item relative pl-6 text-2xl font-bold text-[#383838] cursor-pointer transition-all duration-300
|
||||||
|
after:content-[''] after:absolute after:top-6 after:left-[0.5rem] after:w-[2px] after:h-[24px] after:bg-[#C5C5C5] last:after:hidden
|
||||||
|
${idx === activeIdx ? 'text-4xl font-bold text-[#383838]' : ''}`}
|
||||||
|
>
|
||||||
|
<span className={`absolute left-[0.5rem] top-1 w-4 h-4 rounded-full -translate-x-1/2 transition-all duration-300
|
||||||
|
${idx === activeIdx ? 'bg-blue-500 border-blue-500' : 'bg-white border-gray-400 border'}`}></span>
|
||||||
|
{nav.label}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<article ref={(el) => { cardRefs.current[0] = el; }} className="card-wrap pl-[100px] mb-24 !pt-0" id="card1" aria-labelledby="card1-title">
|
||||||
|
<h2 id="card1-title" className="font-NotoSansKR relative text-center text-4xl font-medium text-gray-900 mb-6">
|
||||||
|
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
||||||
|
물재이용이란
|
||||||
|
</h2>
|
||||||
|
<section aria-labelledby="card1-desc" className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px] ">
|
||||||
|
<h1 id="card1-desc" className="font-NotoSansKR text-[#269DDA] text-5xl font-bold mb-8">물 재이용이 무엇인가요?</h1>
|
||||||
|
<p className="font-NotoSansKR text-[#231815] text-2xl mb-10 leading-relaxed">
|
||||||
|
물 재이용은 빗물, 오수, 하수처리수, 폐수처리수 및 발전소 온배수를<br />
|
||||||
|
물 재이용시설을 이용하여 처리하고, 그 처리된 물을 생활, 공업, 농업, 조경,<br />
|
||||||
|
하천 유지 등의 용도로 이용하는 것을 말합니다.
|
||||||
|
</p>
|
||||||
|
<Image
|
||||||
|
src="/img/wrAbout/img_01_1.png"
|
||||||
|
alt="이 이미지는 물 재이용 시스템의 개요를 나타냅니다. 가정, 공공기관, 하수처리장 등에서 발생한 생활하수, 빗물, 하수처리수를 물 재이용 시설에서 처리한 후, 청소나 화장실, 농업, 살수차, 하천 유지, 공장, 공원 및 골프장 등 다양한 용도로 활용하는 과정을 도식화한 것입니다."
|
||||||
|
width={800}
|
||||||
|
height={400}
|
||||||
|
className="w-[90%] mx-auto mb-8 rounded"
|
||||||
|
/>
|
||||||
|
<p className="font-NotoSansKR text-center font-normal text-2xl">
|
||||||
|
환경부는 2011년에 '물의 재이용 촉진 및 지원에 관한 법률'을 제정하여<br />
|
||||||
|
한국환경공단과 함께 물 재이용 확대 및 촉진을 위해 노력하고 있습니다.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
{/* 카드2 */}
|
||||||
|
<article ref={(el) => { cardRefs.current[1] = el; }} className="card-wrap pl-[100px] mb-24" id="card2" aria-labelledby="card2-title">
|
||||||
|
<h2 id="card2-title" className="font-NotoSansKR relative text-center text-4xl font-medium text-gray-900 mb-6">
|
||||||
|
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
||||||
|
물재이용 필요성
|
||||||
|
</h2>
|
||||||
|
<section aria-labelledby="card2-desc" className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px] ">
|
||||||
|
<h1 id="card2-desc" className="font-NotoSansKR text-[#ED5534] text-5xl font-semibold mb-8">물 재이용, 왜 필요한가요?</h1>
|
||||||
|
<p className="font-NotoSansKR text-2xl font-normal mb-10 leading-relaxed">
|
||||||
|
빗물, 중수, 하수처리수는 지속적으로 이용 가능한 순환자원입니다.<br />
|
||||||
|
물 재이용을 통한 효율적인 수자원 활용은 물 스트레스국가인 우리나라에서<br />가뭄대응능력 향상 및 하천수질보전을 위해 반드시 필요한 일입니다.
|
||||||
|
</p>
|
||||||
|
<Image
|
||||||
|
src="/img/wrAbout/img_02_1.png"
|
||||||
|
alt="이 이미지는 기후변화로 인한 온도 상승 추이를 설명한다. 왼쪽에는 지구 그림과 온도계가 함께 표시되어 있으며, 지구 평균 기온은 지난 100년간 약 0.9℃ 상승했음을 나타낸다. 오른쪽에는 대한민국 지도와 함께 온도계가 있으며, 지난 100년간 약 1.8℃ 상승했음을 보여준다. 이는 지구 평균보다 2배 높은 수치이며, 자료 출처는 「기후변화 평가보고서 2020, 환경부·기상청」이다."
|
||||||
|
width={800}
|
||||||
|
height={400}
|
||||||
|
className="w-[90%] mx-auto mb-8 w-[80%]"
|
||||||
|
/>
|
||||||
|
<p className="font-NotoSansKR text-2xl mb-10 leading-relaxed">
|
||||||
|
- 우리나라의 기후변화 진행속도는 세계 평균을 상회하고 있습니다.<br />
|
||||||
|
- 기후변화로 인한 국지적인 가뭄은 더욱 자주, 더욱 심하게 일어나고 있으며, 우리의 물 안보를 위협하고 있습니다.
|
||||||
|
</p>
|
||||||
|
<Image
|
||||||
|
src="/img/wrAbout/img_02_2.png"
|
||||||
|
alt="세계 평균 1인당 강수량은 15,044m³이며, 대한민국은 2,546m³로 약 1/6 수준입니다. 계절별 강수량은 봄 10%, 여름 70%, 가을 15%, 겨울 5%입니다. 이는 여름철에 강수량이 집중되는 문제를 나타냅니다."
|
||||||
|
width={800}
|
||||||
|
height={400}
|
||||||
|
className="w-[90%] mx-auto mb-8"
|
||||||
|
/>
|
||||||
|
<p className="font-NotoSansKR text-2xl mb-10 leading-relaxed">
|
||||||
|
- 우리나라의 1인당 강수량은 2,546㎥ 으로 세계 평균 15,044㎥의 약 1/6 수준입니다.<br />
|
||||||
|
- 우리나라 강수량의 70%는 여름철에 집중되어 있어 이용 가능한 수자원확보가 어렵습니다.
|
||||||
|
</p>
|
||||||
|
<Image
|
||||||
|
src="/img/wrAbout/img_02_3.png"
|
||||||
|
alt="물 재이용은 환경을 보호하며 지속 가능한 발전이 가능하게 하고, 안정적인 수자원 확보, 깨끗하고 경제적인 공업용수 공급, 하천 수질 개선을 통한 생태계 회복에 기여합니다."
|
||||||
|
width={800}
|
||||||
|
height={400}
|
||||||
|
className="w-[93%] mx-auto"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
{/* 카드3 */}
|
||||||
|
<article ref={(el) => { cardRefs.current[2] = el; }} className="card-wrap pl-[100px] mb-24" id="card3" aria-labelledby="card3-title">
|
||||||
|
<h2 id="card3-title" className="font-NotoSansKR relative text-center text-4xl font-medium text-gray-900 mb-6">
|
||||||
|
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
||||||
|
빗물 재이용
|
||||||
|
</h2>
|
||||||
|
<section aria-labelledby="card3-desc" className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px] ">
|
||||||
|
<h1 id="card3-desc" className="font-NotoSansKR leading-snug text-[#269DDA] text-5xl font-bold mb-6">친환경 수자원 빗물,<br />편리하게 사용할 수 있어요!</h1>
|
||||||
|
<p className="font-NotoSansKR text-2xl mb-10 leading-relaxed">가뭄을 극복하고 홍수를 예방하기 위해<br />비가 내릴 때 빗물을 모아두었다가 사용합니다.</p>
|
||||||
|
<Image
|
||||||
|
src="/img/wrAbout/img_03_1.png"
|
||||||
|
alt="주택 지붕에 내린 빗물을 저장조에 모아 조경용수, 화장실용수, 세척·살수용수로 재활용하는 과정을 보여줍니다."
|
||||||
|
width={800}
|
||||||
|
height={400}
|
||||||
|
className="w-[90%] mx-auto mb-6"
|
||||||
|
/>
|
||||||
|
<Image
|
||||||
|
src="/img/wrAbout/img_03_2.png"
|
||||||
|
alt="빗물이용"
|
||||||
|
width={200}
|
||||||
|
height={100}
|
||||||
|
className="mb-5 w-[20%]"
|
||||||
|
/>
|
||||||
|
<p className="font-NotoSansKR text-2xl leading-relaxed">
|
||||||
|
- 의무 설치대상: 지붕면적 1,000㎡ 이상의 운동장, 체육관, 공공업무시설 등<br />
|
||||||
|
- 설 치 기 준: 집수시설, 처리시설, 빗물 저류조(지붕 집수면적 x 0.05m 이상)<br />
|
||||||
|
- 활 용 용 도: 골프장 등의 조경용수, 화장실용수, 세척·살수용수 등
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
{/* 카드4 */}
|
||||||
|
<article ref={(el) => { cardRefs.current[3] = el; }} className="card-wrap pl-[100px] mb-24" id="card4" aria-labelledby="card4-title">
|
||||||
|
<h2 id="card4-title" className="font-NotoSansKR relative text-center text-4xl font-medium text-gray-900 mb-6">
|
||||||
|
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
||||||
|
중수 재이용
|
||||||
|
</h2>
|
||||||
|
<section aria-labelledby="card4-desc" className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px] ">
|
||||||
|
<h1 id="card4-desc" className="font-NotoSansKR leading-snug text-[#9771B0] text-5xl font-bold mb-6">요모조모 쓸모 많은 중수,<br />알뜰하게 재이용할 수 있어요!</h1>
|
||||||
|
<p className="font-NotoSansKR text-2xl mb-10 leading-relaxed">한번 사용한 물을 중수도 시설을 통해 재생하여<br />개별적 또는 지역적으로 재이용할 수 있습니다.</p>
|
||||||
|
<Image
|
||||||
|
src="/img/wrAbout/img_04_1.png"
|
||||||
|
alt="욕실, 세면대, 주방, 세탁기 등에서 발생한 생활용수를 중수도 시설에서 처리하여 화장실 변기용수와 조경용수로 재이용하는 시스템 흐름도입니다."
|
||||||
|
width={800}
|
||||||
|
height={400}
|
||||||
|
className="w-[90%] mx-auto mb-6"
|
||||||
|
/>
|
||||||
|
<Image
|
||||||
|
src="/img/wrAbout/img_04_2.png"
|
||||||
|
alt="중수재이용"
|
||||||
|
width={200}
|
||||||
|
height={100}
|
||||||
|
className="mb-5 w-[20%]"
|
||||||
|
/>
|
||||||
|
<p className="font-NotoSansKR text-2xl leading-relaxed">
|
||||||
|
- 의무 설치대상: 일정규모 이상의 대규모점포, 숙박/목욕장, 공장, 각종 개발사업 등<br />
|
||||||
|
- 의무 재이용량: 물 사용량의 10% 이상<br />
|
||||||
|
- 활 용 용 도: 청소·화장실용수, 세척·살수용수, 조경용수, 공업용수 등
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
{/* 카드5 */}
|
||||||
|
<article ref={(el) => { cardRefs.current[4] = el; }} className="card-wrap pl-[100px] mb-24" id="card5" aria-labelledby="card5-title">
|
||||||
|
<h2 id="card5-title" className="font-NotoSansKR relative text-center text-4xl font-medium text-gray-900 mb-6">
|
||||||
|
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
||||||
|
하수 재이용
|
||||||
|
</h2>
|
||||||
|
<section aria-labelledby="card5-desc" className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px] ">
|
||||||
|
<h1 id="card5-desc" className="font-NotoSansKR leading-snug text-[#0099A4] text-5xl font-bold mb-6">아직 할 일이 많은 하수처리수,<br />풍족하게 재이용할 수 있어요!</h1>
|
||||||
|
<p className="font-NotoSansKR text-2xl mb-10 leading-relaxed">사용하고 버려져 공공하수처리장으로 모여든 물은<br />용도에 맞는 처리 후 재이용 할 수 있습니다.</p>
|
||||||
|
<Image
|
||||||
|
src="/img/wrAbout/img_05_1.png"
|
||||||
|
alt="주택과 건물에서 발생한 하수가 공공하수처리장을 거쳐 처리된 후, 물 재이용 시설에서 정화되어 공업용수, 하천유지용수, 농업용수, 세척·살수용수로 재활용됩니다."
|
||||||
|
width={800}
|
||||||
|
height={400}
|
||||||
|
className="w-[90%] mx-auto mb-6"
|
||||||
|
/>
|
||||||
|
<Image
|
||||||
|
src="/img/wrAbout/img_05_2.png"
|
||||||
|
alt="하수처리 재이용"
|
||||||
|
width={200}
|
||||||
|
height={100}
|
||||||
|
className="mb-5 w-[20%]"
|
||||||
|
/>
|
||||||
|
<p className="font-NotoSansKR text-2xl leading-relaxed">
|
||||||
|
- 의무 이용대상: 공공하수처리장(시설용량 5,000㎥/일 이상)<br />
|
||||||
|
- 의무 재이용량: 일 하수처리량의 10% 이상<br />
|
||||||
|
- 활 용 용 도: 공업용수, 하천유지용수, 농업용수 등
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
{/* 카드6 */}
|
||||||
|
<article ref={(el) => { cardRefs.current[5] = el; }} className="card-wrap pl-[100px] mb-24" id="card6" aria-labelledby="card6-title">
|
||||||
|
<h2 id="card6-title" className="font-NotoSansKR relative text-center text-4xl font-medium text-gray-900 mb-6">
|
||||||
|
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
||||||
|
물재이용 목표
|
||||||
|
</h2>
|
||||||
|
<section aria-labelledby="card6-desc" className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px] ">
|
||||||
|
<h1 id="card6-desc" className="font-NotoSansKR leading-snug text-[#269DDA] text-5xl font-bold mb-6">2030년 물 재이용 목표를 위해<br />모두의 관심과 노력이 필요합니다.</h1>
|
||||||
|
<p className="font-NotoSansKR text-2xl mb-6 leading-relaxed">
|
||||||
|
환경부는 2030년까지의 제2차 물 재이용 기본계획을 수립하여<br />한국환경공단과 함께 물 재이용 확대 및 촉진을 위해 노력하고 있습니다.
|
||||||
|
</p>
|
||||||
|
<Image
|
||||||
|
src="/img/wrAbout/img_06_1.png"
|
||||||
|
alt="지속가능한 물 재이용 정착으로 건전한 물순환 확산"
|
||||||
|
width={800}
|
||||||
|
height={400}
|
||||||
|
className="w-[80%] mx-auto mb-6"
|
||||||
|
/>
|
||||||
|
<div className="flex gap-3 flex-wrap justify-between bg-gray-100 p-6 rounded-lg mb-8">
|
||||||
|
<div className="flex-1 text-center">
|
||||||
|
<Image
|
||||||
|
src="/img/wrAbout/icon_01.png"
|
||||||
|
alt=""
|
||||||
|
width={70}
|
||||||
|
height={70}
|
||||||
|
className="mx-auto mb-4 w-[auto] h-[70px]"
|
||||||
|
/>
|
||||||
|
<h3 className="font-NotoSansKR text-center font-medium text-nowrap mb-2 text-2xl">도시 물 순환이용<br />체계로의 전환</h3>
|
||||||
|
<ul className="font-NotoSansKR text-xl text-gray-600 leading-relaxed space-y-1">
|
||||||
|
<li className="text-nowrap">- 지속가능한 물 순환이용체계 구축</li>
|
||||||
|
<li>- 물 재이용시설 관리체계 강화</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 text-center">
|
||||||
|
<Image
|
||||||
|
src="/img/wrAbout/icon_02.png"
|
||||||
|
alt=""
|
||||||
|
width={70}
|
||||||
|
height={70}
|
||||||
|
className="mx-auto mb-4 w-[auto] h-[70px]"
|
||||||
|
/>
|
||||||
|
<h3 className="font-NotoSansKR text-center font-medium text-nowrap mb-2 text-2xl">유역기반 하수처리수 재이용<br />수요확대 및 공급체계 구축</h3>
|
||||||
|
<ul className="font-NotoSansKR text-xl text-gray-600 leading-relaxed space-y-1">
|
||||||
|
<li>- 유역기반 통합물관리 연계 강화</li>
|
||||||
|
<li>- 하수처리수 재이용 공업용수 공급 활성화</li>
|
||||||
|
<li>- 하수처리수 재이용을 활용한 물순환 촉진</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 text-center">
|
||||||
|
<Image
|
||||||
|
src="/img/wrAbout/icon_03.png"
|
||||||
|
alt=""
|
||||||
|
width={70}
|
||||||
|
height={70}
|
||||||
|
className="mx-auto mb-4 w-[auto] h-[70px]"
|
||||||
|
/>
|
||||||
|
<h3 className="font-NotoSansKR text-center font-medium text-nowrap mb-2 text-2xl">물 재이용 인식제고<br />및 산업발전 기반 마련</h3>
|
||||||
|
<ul className="font-NotoSansKR text-xl text-gray-600 leading-relaxed space-y-1">
|
||||||
|
<li>- 물 재이용 교육 및 대국민 홍보 강화</li>
|
||||||
|
<li>- 핵심기술 육성 및 산업발전 지원 강화</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Image
|
||||||
|
src="/img/wrAbout/img_06_2.png"
|
||||||
|
alt="물 재이용량은 2014년 13.3억톤에서 2030년 25.9억톤까지 증가할 것으로 예상됩니다. 그에 따른 기대 효과로는 연간 상수대체 17.9억톤, 물산업 시장성장 약 2조3,541억 원, 건천화된 73개 하천 해소, 약 20,550명의 고용 효과가 포함됩니다."
|
||||||
|
width={800}
|
||||||
|
height={400}
|
||||||
|
className="w-[90%] mx-auto"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
+64
-64
@@ -2,49 +2,49 @@
|
|||||||
|
|
||||||
export default function WrExamplePage() {
|
export default function WrExamplePage() {
|
||||||
return (
|
return (
|
||||||
<div className="bg-[#F1F6F9]">
|
<div className="bg-[#F2F2F2]">
|
||||||
<div className="relative w-[950px] mx-auto pt-[50px] font-sans">
|
<div className="relative w-[950px] mx-auto pt-[50px] font-sans">
|
||||||
<h2 className="relative text-center text-4xl font-bold text-gray-900 mb-6">
|
<h2 className="font-NotoSansKR relative text-center text-4xl font-bold text-gray-900 mb-6">
|
||||||
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 rounded-full"></span>
|
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 rounded-full"></span>
|
||||||
물 재이용사례
|
물 재이용사례
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-[#F1F6F9] text-gray-800">
|
<div className="bg-[#F2F2F2] text-gray-800">
|
||||||
<div className="relative w-[950px] mx-auto pt-[50px] font-sans">
|
<div className="relative w-[950px] mx-auto pt-[50px] font-sans">
|
||||||
{/* 카드 1 */}
|
{/* 카드 1 */}
|
||||||
<div className="card-wrap mb-24" id="card1">
|
<div className="card-wrap mb-24" id="card1">
|
||||||
<h2 className="relative text-center text-4xl font-bold text-gray-900 mb-6">
|
<h2 className="font-NotoSansKR relative text-center text-4xl font-bold text-gray-900 mb-6">
|
||||||
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
||||||
사업사례 1
|
사업사례 1
|
||||||
</h2>
|
</h2>
|
||||||
<div className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px]">
|
<div className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px]">
|
||||||
{/* 사업명 */}
|
{/* 사업명 */}
|
||||||
<section>
|
<section>
|
||||||
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업명</h2>
|
<h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업명</h2>
|
||||||
<p className="text-2xl font-semibold text-gray-800 leading-relaxed">
|
<p className="font-NotoSansKR text-2xl font-semibold text-gray-800 leading-relaxed">
|
||||||
경기권역 반도체 사업장 물 재이용 사업<br />
|
경기권역 반도체 사업장 물 재이용 사업<br />
|
||||||
: 삼성전자 반도체 사업장 하수재이용수 공급(재이용시설 설치, 공급망 구축 등)
|
: 삼성전자 반도체 사업장 하수재이용수 공급(재이용시설 설치, 공급망 구축 등)
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
{/* 사업 배경 및 목적 */}
|
{/* 사업 배경 및 목적 */}
|
||||||
<section className="mt-8">
|
<section className="mt-8">
|
||||||
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 배경 및 목적</h2>
|
<h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 배경 및 목적</h2>
|
||||||
<p className="text-2xl font-semibold text-gray-800 leading-relaxed">
|
<p className="font-NotoSansKR text-2xl font-semibold text-gray-800 leading-relaxed">
|
||||||
삼성전자는 환경경영전략 실현과 반도체 공장의 안정적 용수 공급을 위해 하수 재이용수를 공업용수로 도입하기로 결정
|
삼성전자는 환경경영전략 실현과 반도체 공장의 안정적 용수 공급을 위해 하수 재이용수를 공업용수로 도입하기로 결정
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
{/* 사업 주요 내용 */}
|
{/* 사업 주요 내용 */}
|
||||||
<section className="mt-8">
|
<section className="mt-8">
|
||||||
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 주요 내용</h2>
|
<h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 주요 내용</h2>
|
||||||
<p className="text-2xl font-semibold text-gray-800 leading-relaxed">
|
<p className="font-NotoSansKR text-2xl font-semibold text-gray-800 leading-relaxed">
|
||||||
1단계 사업[화성·오산(12만㎥/일) → 삼성 화성·기흥(29년 공급)]<br />
|
1단계 사업[화성·오산(12만㎥/일) → 삼성 화성·기흥(29년 공급)]<br />
|
||||||
2단계 사업[수원(21만㎥/일) → 삼성 평택(30년 공급)]
|
2단계 사업[수원(21만㎥/일) → 삼성 평택(30년 공급)]
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
{/* 특이사항 */}
|
{/* 특이사항 */}
|
||||||
<section className="mt-8">
|
<section className="mt-8">
|
||||||
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">특이사항</h2>
|
<h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">특이사항</h2>
|
||||||
<p className="text-2xl font-semibold text-gray-800 leading-relaxed">
|
<p className="font-NotoSansKR text-2xl font-semibold text-gray-800 leading-relaxed">
|
||||||
본 사업은 광역 하수 재이용 공급망 구축의 첫 사업으로 초기 민관협의체를 구성·운영 후 관계기관(환경부, 경기도, 지자체, 공단, 수공, 삼성전자) 간 MOU 체결을 통해 사업을 추진하였으며, 반도체 사업장에 하수 재이용수를 공업용수로 공급하는 국내 첫 사업으로 위협부담 해소를 위해 사업을 단계별로 시행
|
본 사업은 광역 하수 재이용 공급망 구축의 첫 사업으로 초기 민관협의체를 구성·운영 후 관계기관(환경부, 경기도, 지자체, 공단, 수공, 삼성전자) 간 MOU 체결을 통해 사업을 추진하였으며, 반도체 사업장에 하수 재이용수를 공업용수로 공급하는 국내 첫 사업으로 위협부담 해소를 위해 사업을 단계별로 시행
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
@@ -63,15 +63,15 @@ export default function WrExamplePage() {
|
|||||||
|
|
||||||
{/* 카드2 */}
|
{/* 카드2 */}
|
||||||
<div className="card-wrap mb-24" id="card2">
|
<div className="card-wrap mb-24" id="card2">
|
||||||
<h2 className="relative text-center text-4xl font-bold text-gray-900 mb-6">
|
<h2 className="font-NotoSansKR relative text-center text-4xl font-bold text-gray-900 mb-6">
|
||||||
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
||||||
사업사례 2
|
사업사례 2
|
||||||
</h2>
|
</h2>
|
||||||
<div className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px]">
|
<div className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px]">
|
||||||
{/* 사업명 */}
|
{/* 사업명 */}
|
||||||
<section>
|
<section>
|
||||||
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업명</h2>
|
<h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업명</h2>
|
||||||
<p className="text-2xl font-semibold text-gray-800 leading-relaxed">
|
<p className="font-NotoSansKR text-2xl font-semibold text-gray-800 leading-relaxed">
|
||||||
경기도 파주시 하수재이용<br />
|
경기도 파주시 하수재이용<br />
|
||||||
: 운정·금촌하수처리시설의 하수처리수를 막공법(U/F, R/O)으로 재처리하여 파주 LCD 산업단지내 3개 업체*에 공업용수 공급<br /><br />
|
: 운정·금촌하수처리시설의 하수처리수를 막공법(U/F, R/O)으로 재처리하여 파주 LCD 산업단지내 3개 업체*에 공업용수 공급<br /><br />
|
||||||
*LG디스플레이(P10공장), 희성전자, 엔비텍 등
|
*LG디스플레이(P10공장), 희성전자, 엔비텍 등
|
||||||
@@ -79,15 +79,15 @@ export default function WrExamplePage() {
|
|||||||
</section>
|
</section>
|
||||||
{/* 사업 방식 */}
|
{/* 사업 방식 */}
|
||||||
<section className="mt-8">
|
<section className="mt-8">
|
||||||
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 방식</h2>
|
<h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 방식</h2>
|
||||||
<p className="text-2xl font-semibold text-gray-800 leading-relaxed">
|
<p className="font-NotoSansKR text-2xl font-semibold text-gray-800 leading-relaxed">
|
||||||
민간투자사업(시행사 : 파주그린허브(주), 운영사 : 테크로스)
|
민간투자사업(시행사 : 파주그린허브(주), 운영사 : 테크로스)
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
{/* 사업 주요 내용 */}
|
{/* 사업 주요 내용 */}
|
||||||
<section className="mt-8">
|
<section className="mt-8">
|
||||||
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 주요 내용</h2>
|
<h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 주요 내용</h2>
|
||||||
<p className="text-2xl font-semibold text-gray-800 leading-relaxed">
|
<p className="font-NotoSansKR text-2xl font-semibold text-gray-800 leading-relaxed">
|
||||||
처리공정 : 전처리분리막(U/F) + 역삼투서비(R/O)<br />
|
처리공정 : 전처리분리막(U/F) + 역삼투서비(R/O)<br />
|
||||||
공급량 : 4만m³/일<br />
|
공급량 : 4만m³/일<br />
|
||||||
관로 : 13.6km<br />
|
관로 : 13.6km<br />
|
||||||
@@ -96,8 +96,8 @@ export default function WrExamplePage() {
|
|||||||
</section>
|
</section>
|
||||||
{/* 특이사항 */}
|
{/* 특이사항 */}
|
||||||
<section className="mt-8">
|
<section className="mt-8">
|
||||||
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">특이사항</h2>
|
<h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">특이사항</h2>
|
||||||
<p className="text-2xl font-semibold text-gray-800 leading-relaxed">
|
<p className="font-NotoSansKR text-2xl font-semibold text-gray-800 leading-relaxed">
|
||||||
하수처리수 재이용시설 자체 농축폐수 처리공정 개발 및 적용 성공사례
|
하수처리수 재이용시설 자체 농축폐수 처리공정 개발 및 적용 성공사례
|
||||||
</p>
|
</p>
|
||||||
<div className="grid grid-cols-1 gap-0 items-start">
|
<div className="grid grid-cols-1 gap-0 items-start">
|
||||||
@@ -124,29 +124,29 @@ export default function WrExamplePage() {
|
|||||||
</colgroup>
|
</colgroup>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={3} className="bg-[#ffc000] h-[30px] text-xl text-white font-semibold text-center pt-[6px] pb-[6px] px-0">하수처리수 공급</td>
|
<td colSpan={3} className="font-NotoSansKR bg-[#ffc000] h-[30px] text-xl text-white font-semibold text-center pt-[6px] pb-[6px] px-0">하수처리수 공급</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr className="text-xl h-[30px]">
|
<tr className="text-xl h-[30px]">
|
||||||
<td className="text-xl h-[30px] border-black border p-1 font-bold">위치</td>
|
<td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">위치</td>
|
||||||
<td className="text-xl h-[30px] border-black border p-1 font-bold">금촌하수</td>
|
<td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">금촌하수</td>
|
||||||
<td className="text-xl h-[30px] border-black border p-1 font-bold">운정하수</td>
|
<td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">운정하수</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr className="text-sm">
|
<tr className="text-sm">
|
||||||
<td className="text-xl h-[30px] border-black border p-1 font-bold">시설용량</td>
|
<td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">시설용량</td>
|
||||||
<td className="text-xl h-[30px] border-black border p-1 font-bold">44,000㎥/일</td>
|
<td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">44,000㎥/일</td>
|
||||||
<td className="text-xl h-[30px] border-black border p-1 font-bold">60,000㎥/일</td>
|
<td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">60,000㎥/일</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr className="text-sm">
|
<tr className="text-sm">
|
||||||
<td className="text-xl h-[30px] border-black border p-1 font-bold">유입량</td>
|
<td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">유입량</td>
|
||||||
<td className="text-xl h-[30px] border-black border p-1 font-bold">27,000㎥/일<br />자연유하</td>
|
<td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">27,000㎥/일<br />자연유하</td>
|
||||||
<td className="text-xl h-[30px] border-black border p-1 font-bold">30,000㎥/일<br />이송펌프장</td>
|
<td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">30,000㎥/일<br />이송펌프장</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{/* 첫 화살표 (자연유하 / 관로이송) */}
|
{/* 첫 화살표 (자연유하 / 관로이송) */}
|
||||||
<div className="flex h-[30px] justify-between items-center my-3 px-4">
|
<div className="flex h-[30px] justify-between items-center my-3 px-4">
|
||||||
<div className="flex text-xl items-center gap-1 text-orange-600 font-bold pl-6">
|
<div className="flex text-xl items-center gap-1 text-orange-600 font-bold pl-6">
|
||||||
<span>자연유하: 161m</span>
|
<span className="font-NotoSansKR">자연유하: 161m</span>
|
||||||
<Image
|
<Image
|
||||||
src="/img/wrExample/img_arrow_down_1.png"
|
src="/img/wrExample/img_arrow_down_1.png"
|
||||||
alt="오른쪽 화살표"
|
alt="오른쪽 화살표"
|
||||||
@@ -163,7 +163,7 @@ export default function WrExamplePage() {
|
|||||||
height={24}
|
height={24}
|
||||||
className="h-6 w-6"
|
className="h-6 w-6"
|
||||||
/>
|
/>
|
||||||
<span>관로이송: 3.2km</span>
|
<span className="font-NotoSansKR">관로이송: 3.2km</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -177,26 +177,26 @@ export default function WrExamplePage() {
|
|||||||
</colgroup>
|
</colgroup>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={2} className="bg-[#00b050] text-xl h-[30px] text-white font-semibold text-center pt-[6px] pb-[6px] px-0">물재이용 처리시설</td>
|
<td colSpan={2} className="font-NotoSansKR bg-[#00b050] text-xl h-[30px] text-white font-semibold text-center pt-[6px] pb-[6px] px-0">물재이용 처리시설</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr className="text-sm">
|
<tr className="text-sm">
|
||||||
<td className="text-xl h-[30px] border-black border p-1 font-bold">유입유량</td>
|
<td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">유입유량</td>
|
||||||
<td className="text-xl h-[30px] border-black border p-1 font-bold">57,000㎥/일</td>
|
<td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">57,000㎥/일</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr className="text-sm">
|
<tr className="text-sm">
|
||||||
<td className="text-xl h-[30px] border-black border p-1 font-bold">재이용시설</td>
|
<td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">재이용시설</td>
|
||||||
<td className="text-xl h-[30px] border-black border p-1 font-bold">40,000㎥/일</td>
|
<td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">40,000㎥/일</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr className="text-sm">
|
<tr className="text-sm">
|
||||||
<td className="text-xl h-[30px] border-black border p-1 font-bold">농축폐수처리</td>
|
<td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">농축폐수처리</td>
|
||||||
<td className="text-xl h-[30px] border-black border p-1 font-bold">17,000㎥/일</td>
|
<td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">17,000㎥/일</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{/* 두 번째 화살표 (공급관로) */}
|
{/* 두 번째 화살표 (공급관로) */}
|
||||||
<div className="flex flex-col items-center my-3">
|
<div className="flex flex-col items-center my-3">
|
||||||
<div className="flex h-[30px] items-center gap-1 text-xl text-blue-600 font-semibold">
|
<div className="flex h-[30px] items-center gap-1 text-xl text-blue-600 font-semibold">
|
||||||
<span>공급관로 L=10.4km</span>
|
<span className="font-NotoSansKR">공급관로 L=10.4km</span>
|
||||||
<Image
|
<Image
|
||||||
src="/img/wrExample/img_arrow_down_2.png"
|
src="/img/wrExample/img_arrow_down_2.png"
|
||||||
alt="아래 화살표"
|
alt="아래 화살표"
|
||||||
@@ -217,15 +217,15 @@ export default function WrExamplePage() {
|
|||||||
</colgroup>
|
</colgroup>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={2} className="bg-[#3399ff] h-[30px] text-xl text-white font-semibold text-center pt-[6px] pb-[6px] px-0">수요처 공급</td>
|
<td colSpan={2} className="font-NotoSansKR bg-[#3399ff] h-[30px] text-xl text-white font-semibold text-center pt-[6px] pb-[6px] px-0">수요처 공급</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr className="text-sm">
|
<tr className="text-sm">
|
||||||
<td className="text-xl h-[30px] border-black border p-1 font-bold">수요처</td>
|
<td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">수요처</td>
|
||||||
<td className="text-xl h-[30px] border-black border p-1 font-bold">LG디스플레이 외 2개사</td>
|
<td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">LG디스플레이 외 2개사</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr className="text-sm">
|
<tr className="text-sm">
|
||||||
<td className="text-xl h-[30px] border-black border p-1 font-bold">수요량</td>
|
<td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">수요량</td>
|
||||||
<td className="text-xl h-[30px] border-black border p-1 font-bold">40,000㎥/일</td>
|
<td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">40,000㎥/일</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -238,30 +238,30 @@ export default function WrExamplePage() {
|
|||||||
|
|
||||||
{/* 카드3 */}
|
{/* 카드3 */}
|
||||||
<div className="card-wrap mb-24" id="card3">
|
<div className="card-wrap mb-24" id="card3">
|
||||||
<h2 className="relative text-center text-4xl font-bold text-gray-900 mb-6">
|
<h2 className="font-NotoSansKR relative text-center text-4xl font-bold text-gray-900 mb-6">
|
||||||
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
||||||
사업사례 3
|
사업사례 3
|
||||||
</h2>
|
</h2>
|
||||||
<div className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px]">
|
<div className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px]">
|
||||||
{/* 사업명 */}
|
{/* 사업명 */}
|
||||||
<section>
|
<section>
|
||||||
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업명</h2>
|
<h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업명</h2>
|
||||||
<p className="text-2xl font-semibold text-gray-800 leading-relaxed">
|
<p className="font-NotoSansKR text-2xl font-semibold text-gray-800 leading-relaxed">
|
||||||
충남 보령 하수재이용<br />
|
충남 보령 하수재이용<br />
|
||||||
: 보령·신보령화력발전소에 하루 1만톤의 탈황용수 공급을 위해 보령하수처리시설의 하수처리수 재이용 사업 추진
|
: 보령·신보령화력발전소에 하루 1만톤의 탈황용수 공급을 위해 보령하수처리시설의 하수처리수 재이용 사업 추진
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
{/* 사업 방식 */}
|
{/* 사업 방식 */}
|
||||||
<section className="mt-8">
|
<section className="mt-8">
|
||||||
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 방식</h2>
|
<h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 방식</h2>
|
||||||
<p className="text-2xl font-semibold text-gray-800 leading-relaxed">
|
<p className="font-NotoSansKR text-2xl font-semibold text-gray-800 leading-relaxed">
|
||||||
2017년 한국환경공단과 사업대행 위 · 수탁 협약 체결
|
2017년 한국환경공단과 사업대행 위 · 수탁 협약 체결
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
{/* 사업 주요 내용 */}
|
{/* 사업 주요 내용 */}
|
||||||
<section className="mt-8">
|
<section className="mt-8">
|
||||||
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 주요 내용</h2>
|
<h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 주요 내용</h2>
|
||||||
<p className="text-2xl font-semibold text-gray-800 leading-relaxed">
|
<p className="font-NotoSansKR text-2xl font-semibold text-gray-800 leading-relaxed">
|
||||||
처리공정 : 전처리분리막(U/F) + 역삼투서비(R/O)<br />
|
처리공정 : 전처리분리막(U/F) + 역삼투서비(R/O)<br />
|
||||||
공급량 : 1만m3/일<br />
|
공급량 : 1만m3/일<br />
|
||||||
관로 : 8.7km<br />
|
관로 : 8.7km<br />
|
||||||
@@ -270,8 +270,8 @@ export default function WrExamplePage() {
|
|||||||
</section>
|
</section>
|
||||||
{/* 특이사항 */}
|
{/* 특이사항 */}
|
||||||
<section className="mt-8">
|
<section className="mt-8">
|
||||||
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">특이사항</h2>
|
<h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">특이사항</h2>
|
||||||
<p className="text-2xl font-semibold text-gray-800 leading-relaxed">
|
<p className="font-NotoSansKR text-2xl font-semibold text-gray-800 leading-relaxed">
|
||||||
국내 유일 하수재이용 공업용수 공급 재정사업으로 2016년 충남 서부권 가뭄 시 지역사회 기여 차원에서 기관간 MOU (환경부, 충청남도, 보령시, 한국중부발전㈜)를 바탕
|
국내 유일 하수재이용 공업용수 공급 재정사업으로 2016년 충남 서부권 가뭄 시 지역사회 기여 차원에서 기관간 MOU (환경부, 충청남도, 보령시, 한국중부발전㈜)를 바탕
|
||||||
으로 추진 되어 경제성보다 공익성을 바탕으로 국가전력 기반시설의 안정적 운영 도모
|
으로 추진 되어 경제성보다 공익성을 바탕으로 국가전력 기반시설의 안정적 운영 도모
|
||||||
</p>
|
</p>
|
||||||
@@ -290,22 +290,22 @@ export default function WrExamplePage() {
|
|||||||
|
|
||||||
{/* 카드4 */}
|
{/* 카드4 */}
|
||||||
<div className="card-wrap mb-24" id="card4">
|
<div className="card-wrap mb-24" id="card4">
|
||||||
<h2 className="relative text-center text-4xl font-bold text-gray-900 mb-6">
|
<h2 className="font-NotoSansKR relative text-center text-4xl font-bold text-gray-900 mb-6">
|
||||||
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
||||||
사업사례 4
|
사업사례 4
|
||||||
</h2>
|
</h2>
|
||||||
<div className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px]">
|
<div className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px]">
|
||||||
{/* 사업명 */}
|
{/* 사업명 */}
|
||||||
<section>
|
<section>
|
||||||
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업명</h2>
|
<h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업명</h2>
|
||||||
<p className="text-2xl font-semibold text-gray-800 leading-relaxed">
|
<p className="font-NotoSansKR text-2xl font-semibold text-gray-800 leading-relaxed">
|
||||||
경기도 수원시 빗물이용과 중수도
|
경기도 수원시 빗물이용과 중수도
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
{/* 사업 내용 */}
|
{/* 사업 내용 */}
|
||||||
<section className="mt-8">
|
<section className="mt-8">
|
||||||
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 내용</h2>
|
<h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 내용</h2>
|
||||||
<p className="text-2xl font-semibold text-gray-800 leading-relaxed">
|
<p className="font-NotoSansKR text-2xl font-semibold text-gray-800 leading-relaxed">
|
||||||
(빗물) 월드컵경기장 등 4개소에 노면 빗물분사시스템 및 빗물 공급기를 설치하여 혹서기 도시 열섬효과 및 도로 비산먼지 저감<br />
|
(빗물) 월드컵경기장 등 4개소에 노면 빗물분사시스템 및 빗물 공급기를 설치하여 혹서기 도시 열섬효과 및 도로 비산먼지 저감<br />
|
||||||
※ 시설 가동중 도로 노면 온도의 7.2 ~ 26.3℃ 감소 효과 확인(21.7월, 수원시청).<br />
|
※ 시설 가동중 도로 노면 온도의 7.2 ~ 26.3℃ 감소 효과 확인(21.7월, 수원시청).<br />
|
||||||
다만, 가뭄기나 강우량이 적을 경우 빗물을 원활히 공급하기 다소 곤란<br /><br />
|
다만, 가뭄기나 강우량이 적을 경우 빗물을 원활히 공급하기 다소 곤란<br /><br />
|
||||||
@@ -320,7 +320,7 @@ export default function WrExamplePage() {
|
|||||||
height={250}
|
height={250}
|
||||||
className="w-[100%] mx-auto"
|
className="w-[100%] mx-auto"
|
||||||
/>
|
/>
|
||||||
<span className="text-xl block text-center font-semibold my-2"><월드컵경기장 인근도로 노면 살수></span>
|
<span className="font-NotoSansKR text-xl block text-center font-semibold my-2"><월드컵경기장 인근도로 노면 살수></span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Image
|
<Image
|
||||||
@@ -330,7 +330,7 @@ export default function WrExamplePage() {
|
|||||||
height={250}
|
height={250}
|
||||||
className="w-[100%] mx-auto"
|
className="w-[100%] mx-auto"
|
||||||
/>
|
/>
|
||||||
<span className="text-xl block text-center font-semibold my-2"><주유기 모양의 빗물 공급기></span>
|
<span className="font-NotoSansKR text-xl block text-center font-semibold my-2"><주유기 모양의 빗물 공급기></span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Image
|
<Image
|
||||||
@@ -340,7 +340,7 @@ export default function WrExamplePage() {
|
|||||||
height={250}
|
height={250}
|
||||||
className="w-[100%] mx-auto"
|
className="w-[100%] mx-auto"
|
||||||
/>
|
/>
|
||||||
<span className="text-xl block text-center font-semibold my-2"><빗물주유기에서 살수용수 공급></span>
|
<span className="font-NotoSansKR text-xl block text-center font-semibold my-2"><빗물주유기에서 살수용수 공급></span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Image
|
<Image
|
||||||
@@ -350,7 +350,7 @@ export default function WrExamplePage() {
|
|||||||
height={250}
|
height={250}
|
||||||
className="w-[100%] mx-auto"
|
className="w-[100%] mx-auto"
|
||||||
/>
|
/>
|
||||||
<span className="text-xl block text-center font-semibold my-2"><빗물 공급기 및 빗물분사장치 위치></span>
|
<span className="font-NotoSansKR text-xl block text-center font-semibold my-2"><빗물 공급기 및 빗물분사장치 위치></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
|
export default function WrOverviewPage() {
|
||||||
|
return (
|
||||||
|
<div className="bg-[#F2F2F2]">
|
||||||
|
<div className="relative w-[950px] mx-auto pt-[50px] font-sans">
|
||||||
|
{/* 카드 1 - 물재이용 사업 추진 절차 */}
|
||||||
|
<div className="card-wrap mb-24" id="card1">
|
||||||
|
<h2 className="font-NotoSansKR relative text-center text-4xl font-bold text-gray-900 mb-6">
|
||||||
|
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
||||||
|
물재이용 사업 추진 절차
|
||||||
|
</h2>
|
||||||
|
<div className="bg-white border border-gray-300 rounded-xl p-[60px] py-[50px]">
|
||||||
|
<table className="table-fixed w-full text-center text-sm">
|
||||||
|
<caption className="sr-only">물 재이용 추진 절차 상세 표</caption>
|
||||||
|
<colgroup>
|
||||||
|
<col style={{ width: "28%" }} />
|
||||||
|
<col style={{ width: "2%" }} />
|
||||||
|
<col style={{ width: "35%" }} />
|
||||||
|
<col style={{ width: "5%" }} />
|
||||||
|
<col style={{ width: "30%" }} />
|
||||||
|
</colgroup>
|
||||||
|
<tbody>
|
||||||
|
{/* 1단계 */}
|
||||||
|
<tr><td></td><td></td><td className="font-NotoSansKR text-[17px] bg-[#EAEFF3] font-bold py-5">타당성 조사</td><td></td><td></td></tr>
|
||||||
|
<tr><td></td><td></td><td className="font-NotoSansKR text-2xl font-bold text-center">↓</td><td></td><td></td></tr>
|
||||||
|
{/* 2단계 */}
|
||||||
|
<tr><td className="font-NotoSansKR text-2xl bg-[#F4D62D] text-black font-bold py-5">매칭시스템 신청</td><td></td><td className="font-NotoSansKR text-[17px] bg-[#EAEFF3] font-bold py-5">사업계획(안) 수립</td><td></td><td></td></tr>
|
||||||
|
<tr><td></td><td></td><td className="font-NotoSansKR text-2xl font-bold text-center">↓</td><td></td><td></td></tr>
|
||||||
|
{/* 3단계 */}
|
||||||
|
<tr><td className="font-NotoSansKR text-2xl bg-[#F4D62D] text-black font-bold py-5">하수도정보시스템 신청</td><td></td><td className="font-NotoSansKR text-[17px] bg-[#EAEFF3] font-bold py-5">국고지원 신청</td><td className="font-NotoSansKR text-2xl font-bold">→</td><td className="font-NotoSansKR text-2xl text-[#fff] bg-[#00B076] font-bold py-5">기술검토(한국환경공단)</td></tr>
|
||||||
|
<tr><td></td><td></td><td className="font-NotoSansKR text-2xl font-bold text-center">↓</td><td></td><td></td></tr>
|
||||||
|
{/* 4단계 */}
|
||||||
|
<tr><td></td><td></td><td className="font-NotoSansKR text-[17px] bg-[#EAEFF3] font-bold py-5">국고지원 확정 통보</td><td></td><td></td></tr>
|
||||||
|
<tr><td></td><td></td><td className="font-NotoSansKR text-2xl font-bold text-center">↓</td><td></td><td></td></tr>
|
||||||
|
{/* 5단계 */}
|
||||||
|
<tr><td></td><td></td><td className="font-NotoSansKR text-[17px] bg-[#EAEFF3] font-bold py-5">사업계획 수립(실시설계)</td><td></td><td></td></tr>
|
||||||
|
<tr><td></td><td></td><td className="font-NotoSansKR text-2xl font-bold text-center">↓</td><td></td><td></td></tr>
|
||||||
|
{/* 6단계 */}
|
||||||
|
<tr><td className="font-NotoSansKR text-2xl bg-[#F4D62D] text-black font-bold py-5">(행정관) 기술검토 신청</td><td></td><td className="font-NotoSansKR text-[17px] bg-[#EAEFF3] font-bold py-5">재원협의(지방환경관서장)</td><td className="font-NotoSansKR text-2xl font-bold">→</td><td className="font-NotoSansKR text-2xl text-[#fff] bg-[#00B076] font-bold py-5">기술검토(한국환경공단)</td></tr>
|
||||||
|
<tr><td></td><td></td><td className="font-NotoSansKR text-2xl font-bold text-center">↓</td><td></td><td></td></tr>
|
||||||
|
{/* 7단계 */}
|
||||||
|
<tr><td className="font-NotoSansKR text-2xl bg-[#F4D62D] text-black font-bold py-5">(행정관) 기술검토 신청</td><td></td><td className="font-NotoSansKR text-[17px] bg-[#EAEFF3] font-bold py-5">설치승인(시도지사)</td><td className="font-NotoSansKR text-2xl font-bold">→</td><td className="font-NotoSansKR text-2xl text-[#fff] bg-[#00B076] font-bold py-5">기술검토(한국환경공단)</td></tr>
|
||||||
|
<tr><td></td><td></td><td className="font-NotoSansKR text-2xl font-bold text-center">↓</td><td></td><td></td></tr>
|
||||||
|
{/* 8단계 */}
|
||||||
|
<tr><td></td><td></td><td className="font-NotoSansKR text-[17px] bg-[#EAEFF3] font-bold py-5">공사시행</td><td></td><td></td></tr>
|
||||||
|
<tr><td></td><td></td><td className="font-NotoSansKR text-2xl font-bold text-center">↓</td><td></td><td></td></tr>
|
||||||
|
{/* 9단계 */}
|
||||||
|
<tr><td className="font-NotoSansKR text-2xl bg-[#F4D62D] text-black font-bold py-5">(행정관) 기술검토 신청</td><td></td><td className="font-NotoSansKR text-[17px] bg-[#EAEFF3] font-bold py-5">준공(국고정산)</td><td className="font-NotoSansKR text-2xl font-bold">→</td><td className="font-NotoSansKR text-2xl text-[#fff] bg-[#00B076] font-bold py-5">기술검토(한국환경공단)</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 카드 2 - 물재이용 매칭 시스템 */}
|
||||||
|
<div className="mt-36 card-wrap mb-24" id="card2">
|
||||||
|
<h2 className="font-NotoSansKR relative text-center text-4xl font-bold text-gray-900 mb-6">
|
||||||
|
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
||||||
|
물재이용 매칭 시스템이란?
|
||||||
|
</h2>
|
||||||
|
<div className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px]">
|
||||||
|
{/* 개요 */}
|
||||||
|
<section>
|
||||||
|
<h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">개요</h2>
|
||||||
|
<p className="font-NotoSansKR text-2xl font-bold text-gray-800 leading-relaxed">
|
||||||
|
기후변화 및 지자체의 개발속도 차이에 따라 물 자원의 수요-공급 불균형이 발생하여 빗물, 중수도, 하수처리 재처리수 등의 공급 여유가 있는 지자체의 재이용수를 부족한 지자체 수요처에 공급하여 불균형 해소를 통한 수자원 절약 및 물 자원 순환
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* 구성 */}
|
||||||
|
<section className="mt-8">
|
||||||
|
<h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">구성</h2>
|
||||||
|
<ul className=" font-NotoSansKR list-[square] pl-6 text-2xl font-bold text-gray-800 leading-relaxed space-y-2">
|
||||||
|
<li>공급자: 하수처리장, 빗물이용시설, 중수도 등</li>
|
||||||
|
<li>수요자: 산업체(냉각수), 공공시설(조경, 도로청소), 농민(농업용수) 등</li>
|
||||||
|
<li>시스템: 수질, 수량, 거리 기반 매칭, 중개</li>
|
||||||
|
<li>운영주체: 한국환경공단</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* 절차 */}
|
||||||
|
<section className="mt-8">
|
||||||
|
<h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 주요 내용</h2>
|
||||||
|
<p className="font-NotoSansKR text-2xl font-bold text-gray-800 leading-relaxed">
|
||||||
|
① 공급자 등록 → ② 수요자 조건 입력 → ③ 매칭 검토 → ④ 시스템 매칭 → ⑤ 계약·공급 이행
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* 기대효과 */}
|
||||||
|
<section className="mt-8">
|
||||||
|
<h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">기대효과</h2>
|
||||||
|
<ul className="font-NotoSansKR list-[square] pl-6 text-2xl font-bold text-gray-800 leading-relaxed space-y-2">
|
||||||
|
<li>물 자원 수요-공급 조정을 통한 효율적 분배</li>
|
||||||
|
<li>물 재이용시설 데이터 수집으로 체계적 관리</li>
|
||||||
|
<li>지자체, 사업시행자 등 재이용 정보 획득 용이</li>
|
||||||
|
</ul>
|
||||||
|
<div className="text-center my-6">
|
||||||
|
<Image
|
||||||
|
src="/img/wrOverview/img_ov_01_1.png"
|
||||||
|
alt="하수 재이용 시스템 흐름도. 저장탱크에서 재처리된 물이 손 씻기, 수도, 세정 등에 공급되며 재이용 활성화를 통해 최적의 물공급 방안이 마련되는 과정을 시각화함."
|
||||||
|
width={650}
|
||||||
|
height={500}
|
||||||
|
className="h-[500px] mx-auto mb-4 rounded"
|
||||||
|
/>
|
||||||
|
<p className="font-NotoSansKR text-2xl font-bold text-gray-800 mt-10">(하수 재이용 시스템 개요도)</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
+36
-36
@@ -12,10 +12,10 @@ interface RegionData {
|
|||||||
|
|
||||||
interface TableData {
|
interface TableData {
|
||||||
region: string;
|
region: string;
|
||||||
sewage: number;
|
sewage: string;
|
||||||
rainwater: number;
|
rainwater: string;
|
||||||
reclaimed: number;
|
reclaimed: string;
|
||||||
thermal: number;
|
thermal: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -161,7 +161,7 @@ const WrStatusPage: React.FC = () => {
|
|||||||
chartTitle : '',
|
chartTitle : '',
|
||||||
chartClassName : "h-[300px] w-full",
|
chartClassName : "h-[300px] w-full",
|
||||||
graphSize : 40,
|
graphSize : 40,
|
||||||
yFieldText : '개소수(건)',
|
yFieldText : '시설(개소)',
|
||||||
showLegend : true
|
showLegend : true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,24 +193,24 @@ const WrStatusPage: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const tableData: TableData[] = [
|
const tableData: TableData[] = [
|
||||||
{ region: '서울특별시', sewage: 4, rainwater: 816, reclaimed: 371, thermal: 0 },
|
{ region: '전국', sewage: '717(1,137,538)', rainwater: '3544(13,945)', reclaimed: '1018(222,476)', thermal: '123(492)' },
|
||||||
{ region: '부산광역시', sewage: 13, rainwater: 68, reclaimed: 20, thermal: 1 },
|
{ region: '서울특별시', sewage: '4(114,908)', rainwater: '816(430)', reclaimed: '371(341)', thermal: '0' },
|
||||||
{ region: '대구광역시', sewage: 9, rainwater: 294, reclaimed: 14, thermal: 2 },
|
{ region: '부산광역시', sewage: '13(146,197)', rainwater: '68(335)', reclaimed: '20(512)', thermal: '1(18)' },
|
||||||
{ region: '인천광역시', sewage: 17, rainwater: 165, reclaimed: 35, thermal: 15 },
|
{ region: '대구광역시', sewage: '9(102,380)', rainwater: '294(133)', reclaimed: '14(44)', thermal: '2(12)' },
|
||||||
{ region: '광주광역시', sewage: 3, rainwater: 74, reclaimed: 15, thermal: 15 },
|
{ region: '인천광역시', sewage: '17(57,691)', rainwater: '165(642)', reclaimed: '35(7,859)', thermal: '15(32)' },
|
||||||
{ region: '대전광역시', sewage: 2, rainwater: 52, reclaimed: 11, thermal: 20 },
|
{ region: '광주광역시', sewage: '3(28,738)', rainwater: '74(23)', reclaimed: '15(450)', thermal: '15(6)' },
|
||||||
{ region: '울산광역시', sewage: 8, rainwater: 111, reclaimed: 19, thermal: 30 },
|
{ region: '대전광역시', sewage: '2(11,966)', rainwater: '52(2)', reclaimed: '11(492)', thermal: '20(100)' },
|
||||||
{ region: '세종특별자치시', sewage: 9, rainwater: 65, reclaimed: 0, thermal: 0 },
|
{ region: '울산광역시', sewage: '8(36,076)', rainwater: '111(39)', reclaimed: '19(28,904)', thermal: '30(130)' },
|
||||||
{ region: '경기도', sewage: 176, rainwater: 1009, reclaimed: 252, thermal: 40 },
|
{ region: '세종특별자치시', sewage: '9(2,555)', rainwater: '65(16)', reclaimed: '0(0)', thermal: '0' },
|
||||||
{ region: '강원특별자치도', sewage: 69, rainwater: 69, reclaimed: 22, thermal: 0 },
|
{ region: '경기도', sewage: '176(313,298)', rainwater: '1009(4,735)', reclaimed: '252(77,096)', thermal: '40(143)' },
|
||||||
{ region: '충청북도', sewage: 38, rainwater: 17, reclaimed: 91, thermal: 0 },
|
{ region: '강원특별자치도', sewage: '69(19,677)', rainwater: '69(4)', reclaimed: '22(447)', thermal: '0' },
|
||||||
{ region: '충청남도', sewage: 67, rainwater: 54, reclaimed: 188, thermal: 0 },
|
{ region: '충청북도', sewage: '38(15,607)', rainwater: '17(91)', reclaimed: '91(2,211)', thermal: '0' },
|
||||||
{ region: '전라북도', sewage: 55, rainwater: 33, reclaimed: 220, thermal: 0 },
|
{ region: '충청남도', sewage: '67(41,368)', rainwater: '54(77)', reclaimed: '188(50,434)', thermal: '0' },
|
||||||
{ region: '전라남도', sewage: 83, rainwater: 16, reclaimed: 75, thermal: 0 },
|
{ region: '전라북도', sewage: '55(42,671)', rainwater: '33(199)', reclaimed: '220(9,453)', thermal: '0' },
|
||||||
{ region: '경상북도', sewage: 81, rainwater: 37, reclaimed: 70, thermal: 0 },
|
{ region: '전라남도', sewage: '83(23,560)', rainwater: '16(885)', reclaimed: '75(15,412)', thermal: '0' },
|
||||||
{ region: '경상남도', sewage: 75, rainwater: 27, reclaimed: 140, thermal: 0 },
|
{ region: '경상북도', sewage: '81(129,081)', rainwater: '37(341)', reclaimed: '70(27,818)', thermal: '0' },
|
||||||
{ region: '제주특별자치도', sewage: 8, rainwater: 47, reclaimed: 64, thermal: 0 },
|
{ region: '경상남도', sewage: '75(50,101)', rainwater: '27(244)', reclaimed: '140(343)', thermal: '0' },
|
||||||
|
{ region: '제주특별자치도', sewage: '8(1,666)', rainwater: '47(5,746)', reclaimed: '64(660)', thermal: '0' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const regionData: RegionData[] = [
|
const regionData: RegionData[] = [
|
||||||
@@ -395,7 +395,7 @@ const WrStatusPage: React.FC = () => {
|
|||||||
}, [selectedRegion]);
|
}, [selectedRegion]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen bg-[#F4F7FB] text-gray-800">
|
<div className="flex h-screen bg-[#F2F2F2] text-gray-800">
|
||||||
{/* 우측 콘텐츠 영역 */}
|
{/* 우측 콘텐츠 영역 */}
|
||||||
<main className="flex-1 p-6 overflow-auto">
|
<main className="flex-1 p-6 overflow-auto">
|
||||||
{/* 상단 제목 */}
|
{/* 상단 제목 */}
|
||||||
@@ -410,8 +410,8 @@ const WrStatusPage: React.FC = () => {
|
|||||||
<div className="bg-white rounded-xl shadow border border-[#DBE2E6] overflow-hidden max-h-[900px]">
|
<div className="bg-white rounded-xl shadow border border-[#DBE2E6] overflow-hidden max-h-[900px]">
|
||||||
{/* 타이틀 영역 */}
|
{/* 타이틀 영역 */}
|
||||||
<div className="border-b border-[#DBE2E6] px-4 py-3">
|
<div className="border-b border-[#DBE2E6] px-4 py-3">
|
||||||
<h3 className="text-[12px] font-semibold">재이용시설개소수(2023)</h3>
|
<h3 className="text-[12px] font-semibold">재이용시설현황(2023)</h3>
|
||||||
<p className="text-[12px] text-[#999] font-medium">행정구역별 재이용시설 개소수 조회</p>
|
<p className="text-[12px] text-[#999] font-medium">행정구역별 재이용시설현황(개소)</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 본문 콘텐츠 */}
|
{/* 본문 콘텐츠 */}
|
||||||
@@ -455,19 +455,19 @@ const WrStatusPage: React.FC = () => {
|
|||||||
<div className="absolute bottom-2 right-2 bg-white rounded-lg shadow-md p-4 text-sm space-y-2">
|
<div className="absolute bottom-2 right-2 bg-white rounded-lg shadow-md p-4 text-sm space-y-2">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<span className="inline-block w-4 h-4 rounded-sm" style={{backgroundColor: '#FF6B6B'}}></span>
|
<span className="inline-block w-4 h-4 rounded-sm" style={{backgroundColor: '#FF6B6B'}}></span>
|
||||||
<span className="text-[12px] text-gray-900">하수재이용시설 (총 717건)</span>
|
<span className="text-[12px] text-gray-900">하수재이용시설 (총 717개소)</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<span className="inline-block w-4 h-4 rounded-sm" style={{backgroundColor: '#4ECDC4'}}></span>
|
<span className="inline-block w-4 h-4 rounded-sm" style={{backgroundColor: '#4ECDC4'}}></span>
|
||||||
<span className="text-[12px] text-gray-900">빗물이용시설 (총 3,730건)</span>
|
<span className="text-[12px] text-gray-900">빗물이용시설 (총 3,730개소)</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<span className="inline-block w-4 h-4 rounded-sm" style={{backgroundColor: '#45B7D1'}}></span>
|
<span className="inline-block w-4 h-4 rounded-sm" style={{backgroundColor: '#45B7D1'}}></span>
|
||||||
<span className="text-[12px] text-gray-900">중수도시설 (총 1,074건)</span>
|
<span className="text-[12px] text-gray-900">중수도시설 (총 1,074개소)</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<span className="inline-block w-4 h-4 rounded-sm" style={{backgroundColor: '#FED16A'}}></span>
|
<span className="inline-block w-4 h-4 rounded-sm" style={{backgroundColor: '#FED16A'}}></span>
|
||||||
<span className="text-[12px] text-gray-900">온배수시설 (총 201건)</span>
|
<span className="text-[12px] text-gray-900">온배수시설 (총 201개소)</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -482,7 +482,7 @@ const WrStatusPage: React.FC = () => {
|
|||||||
<div className="flex justify-between items-center border-b border-[#DBE2E6] px-4 py-3">
|
<div className="flex justify-between items-center border-b border-[#DBE2E6] px-4 py-3">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-[12px] font-semibold">재이용시설현황(2023)</h3>
|
<h3 className="text-[12px] font-semibold">재이용시설현황(2023)</h3>
|
||||||
<p className="text-[12px] text-[#999] font-medium">행정구역별 재이용시설 현황 비교</p>
|
<p className="text-[12px] text-[#999] font-medium">행정구역별 재이용시설현황(개소)</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
@@ -493,10 +493,10 @@ const WrStatusPage: React.FC = () => {
|
|||||||
<tr>
|
<tr>
|
||||||
<th className="text-[12px] font-semibold px-4 py-2">번호</th>
|
<th className="text-[12px] font-semibold px-4 py-2">번호</th>
|
||||||
<th className="text-[12px] font-semibold px-4 py-2">시도</th>
|
<th className="text-[12px] font-semibold px-4 py-2">시도</th>
|
||||||
<th className="text-[12px] font-semibold px-4 py-2">하수</th>
|
<th className="text-[12px] font-semibold px-4 py-2">하수(이용량,천𝓂³/년)</th>
|
||||||
<th className="text-[12px] font-semibold px-4 py-2">빗물</th>
|
<th className="text-[12px] font-semibold px-4 py-2">빗물(이용량,천𝓂³/년)</th>
|
||||||
<th className="text-[12px] font-semibold px-4 py-2">중수도</th>
|
<th className="text-[12px] font-semibold px-4 py-2">중수도(이용량,천𝓂³/년)</th>
|
||||||
<th className="text-[12px] font-semibold px-4 py-2">온배수</th>
|
<th className="text-[12px] font-semibold px-4 py-2">온배수(이용량,천𝓂³/년)</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y">
|
<tbody className="divide-y">
|
||||||
@@ -522,7 +522,7 @@ const WrStatusPage: React.FC = () => {
|
|||||||
<div className="bg-white rounded-xl shadow border border-[#DBE2E6] overflow-hidden">
|
<div className="bg-white rounded-xl shadow border border-[#DBE2E6] overflow-hidden">
|
||||||
<div className="border-b border-[#DBE2E6] px-4 py-3">
|
<div className="border-b border-[#DBE2E6] px-4 py-3">
|
||||||
<h3 className="text-[12px] font-semibold">재이용통계(2023)</h3>
|
<h3 className="text-[12px] font-semibold">재이용통계(2023)</h3>
|
||||||
<p className="text-[12px] text-[#999] font-medium">항목별 통계 보기</p>
|
<p className="text-[12px] text-[#999] font-medium">행정구역별 재이용시설 현황 개소수 그래프</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4 bg-[#f9fbfd]">
|
<div className="p-4 bg-[#f9fbfd]">
|
||||||
<div className="h-[305px] text-gray-500">
|
<div className="h-[305px] text-gray-500">
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
"use client";
|
||||||
|
import "../globals.css";
|
||||||
|
import { StoreProvider } from "../store";
|
||||||
|
|
||||||
|
export default function PopupLayout({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) {
|
||||||
|
return <StoreProvider>{children}</StoreProvider>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Gis from "@common/business_components/ui/map/Gis";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { mapService } from "../service";
|
||||||
|
import GisApp from "@common/business_components/ui/map/lib/gis.core";
|
||||||
|
import { SearchBar } from "@/components/ui/map/searchBar/searchBar";
|
||||||
|
import SelectedFcltList from "./selectedFcltList";
|
||||||
|
import type { SubsInfo } from "@/app/store/map/type";
|
||||||
|
|
||||||
|
const Map = () => {
|
||||||
|
const [treeData, setTreeData] = useState([]); // 초기값을 객체 또는 null로 설정
|
||||||
|
const gisMaps = useRef<any | null>(null);
|
||||||
|
const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태
|
||||||
|
const [isSearchModalOpen, setIsSearchModalOpen] = useState(true); // SearchModal 열림 상태
|
||||||
|
const [featureData, setFeatureData] = useState(null); // 선택된 피쳐 데이터
|
||||||
|
const [selectDataPk, setSelectDataPk] = useState<Record<string, any>>({});
|
||||||
|
const [selectedFclts, setSelectedFclts] = useState<any[]>([]);
|
||||||
|
|
||||||
|
//적용 버튼 클릭 이벤트
|
||||||
|
const onApply = () => {
|
||||||
|
if (selectedFclts.length > 3) {
|
||||||
|
alert("재이용시설 후보지는 최대 3개까지만 선택할 수 있습니다.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const subsInfoList: SubsInfo[] = selectedFclts.map((fclt) => ({
|
||||||
|
ruseUtztnFcltNm: fclt.ruseUtztnFcltNm,
|
||||||
|
ruseUtztnFcltCd: fclt.pbnstRuseFcltId,
|
||||||
|
}));
|
||||||
|
|
||||||
|
window.opener.postMessage(
|
||||||
|
{
|
||||||
|
type: "SET_SUBS_INFO",
|
||||||
|
payload: subsInfoList, // 또는 필요한 데이터
|
||||||
|
},
|
||||||
|
"*"
|
||||||
|
);
|
||||||
|
|
||||||
|
window.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
//트리 레이어 데이터 조회
|
||||||
|
const fetchData = async () => {
|
||||||
|
try {
|
||||||
|
const response = await mapService.getLyrStngs();
|
||||||
|
setTreeData(response); // API 응답 저장
|
||||||
|
} catch (error) {
|
||||||
|
console.error("데이터 불러오기 실패:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
//하수처리시설코드로 재이용시설 목록 조회
|
||||||
|
const reuseData = async (ruseFcltPk: any) => {
|
||||||
|
try {
|
||||||
|
const response = await mapService.getPbnstRuseFclts(ruseFcltPk);
|
||||||
|
const data = response.data || [];
|
||||||
|
setSelectedFclts((prev) => {
|
||||||
|
const newItems = data.filter(
|
||||||
|
(item: any) =>
|
||||||
|
item &&
|
||||||
|
Object.keys(item).length > 0 &&
|
||||||
|
!prev.some((f) => f.pbnstRuseFcltId === item.pbnstRuseFcltId)
|
||||||
|
);
|
||||||
|
console.log(newItems)
|
||||||
|
return [...prev, ...newItems];
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("데이터 불러오기 실패:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 컴포넌트 마운트 시 1회 실행
|
||||||
|
useEffect(() => {
|
||||||
|
fetchData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 레이어 생성 후 초기화 처리
|
||||||
|
const onCheckedLayersChange = () => {
|
||||||
|
//수요처 ID
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const dmdInfoId = urlParams.get("dmdInfoId");
|
||||||
|
|
||||||
|
//수요처 위치로 화면 이동
|
||||||
|
const maps = gisMaps.current;
|
||||||
|
const layer = GisApp.Module.layer as any;
|
||||||
|
const layerNm = "tb_wr_ruse_fclt_dmd_co";
|
||||||
|
const featureId = `${layerNm}.${dmdInfoId}`;
|
||||||
|
let foundFeature: any = null;
|
||||||
|
|
||||||
|
maps
|
||||||
|
.getLayers()
|
||||||
|
.getArray()
|
||||||
|
.forEach((layer: any) => {
|
||||||
|
if (layer.get("lyrTyNm") == layerNm) {
|
||||||
|
const source = layer.getSource?.();
|
||||||
|
if (source?.getFeatureById) {
|
||||||
|
const feature = source.getFeatureById(String(featureId).trim());
|
||||||
|
if (feature) {
|
||||||
|
foundFeature = feature;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (foundFeature) {
|
||||||
|
const geometry = foundFeature.getGeometry();
|
||||||
|
|
||||||
|
if (geometry) {
|
||||||
|
const extent = geometry.getExtent();
|
||||||
|
|
||||||
|
if (geometry.getType() === "Point") {
|
||||||
|
maps.getView().setCenter(geometry.getCoordinates());
|
||||||
|
maps.getView().setZoom(11);
|
||||||
|
} else {
|
||||||
|
maps.getView().fit(extent, {
|
||||||
|
duration: 300,
|
||||||
|
padding: [30, 30, 30, 30],
|
||||||
|
maxZoom: 16,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
layer.highLightFeature(
|
||||||
|
[foundFeature],
|
||||||
|
{
|
||||||
|
id: GisApp.hlLayerId,
|
||||||
|
zIndex: 10,
|
||||||
|
opacity: 1,
|
||||||
|
selectable: true,
|
||||||
|
},
|
||||||
|
{ width: 30 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatNumber = (val: any) => {
|
||||||
|
const num = Number(val);
|
||||||
|
return isNaN(num) ? "-" : num.toLocaleString();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 재이용시설 오버레이
|
||||||
|
maps
|
||||||
|
.getLayers()
|
||||||
|
.getArray()
|
||||||
|
.forEach((layer: any) => {
|
||||||
|
if (layer.get("lyrTyNm") === "tb_wr_bdst_ruse_fclt") {
|
||||||
|
const source = layer.getSource?.();
|
||||||
|
if (source?.getFeatures) {
|
||||||
|
const features = source.getFeatures();
|
||||||
|
|
||||||
|
features.forEach((feature: any) => {
|
||||||
|
// 필요한 고유 ID 또는 속성 가져오기 (예: ftrIdn 또는 feature.getId())
|
||||||
|
const featureId =
|
||||||
|
feature.getId() || feature.get("ftrIdn") || Math.random();
|
||||||
|
|
||||||
|
// HTML 또는 React 오버레이 컴포넌트 구성
|
||||||
|
const overlayContent = `
|
||||||
|
<div class="rounded-md border border-gray-300 bg-white shadow-md text-sm w-[120px]">
|
||||||
|
<!-- 헤더 -->
|
||||||
|
<div class="bg-cyan-700 text-white px-3 py-2 text-[11px] font-semibold text-center rounded-t-md">
|
||||||
|
${feature.get("swtrmt_fclt_nm") || "-"}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 테이블 -->
|
||||||
|
<table class="w-full text-sm text-gray-800 border-t border-gray-200">
|
||||||
|
<tbody>
|
||||||
|
<tr class="border-b border-gray-200">
|
||||||
|
<th class="bg-gray-100 text-gray-600 font-semibold text-[10px] text-left">용량(m³/일)</th>
|
||||||
|
<td class="text-right text-[10px] font-bold">${formatNumber(feature.get("fclt_cpct"))}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
feature.set("html", overlayContent);
|
||||||
|
feature.set("overLayId", `overLay.${featureId}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 검색해서 재이용시설 선택하면 해당 시설로 화면 이동 처리
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectDataPk || Object.keys(selectDataPk).length === 0) return;
|
||||||
|
const maps = gisMaps.current;
|
||||||
|
let foundFeature: any = null;
|
||||||
|
const layerNm = "tb_wr_bdst_ruse_fclt";
|
||||||
|
const featureId = `${layerNm}.${selectDataPk.bscFctCd}`;
|
||||||
|
maps
|
||||||
|
.getLayers()
|
||||||
|
.getArray()
|
||||||
|
.forEach((layer: any) => {
|
||||||
|
if (layer.get("lyrTyNm") == layerNm) {
|
||||||
|
const source = layer.getSource?.();
|
||||||
|
if (source?.getFeatureById) {
|
||||||
|
const feature = source.getFeatureById(String(featureId).trim());
|
||||||
|
if (feature) {
|
||||||
|
foundFeature = feature;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (foundFeature) {
|
||||||
|
const geometry = foundFeature.getGeometry();
|
||||||
|
if (geometry) {
|
||||||
|
const extent = geometry.getExtent();
|
||||||
|
|
||||||
|
maps.getView().fit(extent, {
|
||||||
|
duration: 300,
|
||||||
|
padding: [50, 50, 50, 50],
|
||||||
|
maxZoom: 16,
|
||||||
|
constrainResolution: true,
|
||||||
|
nearest: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn("OpenLayers Feature를 찾을 수 없습니다:", selectDataPk);
|
||||||
|
}
|
||||||
|
}, [selectDataPk]);
|
||||||
|
|
||||||
|
// 재이용시설 클릭 시 선택 시설목록에 추가
|
||||||
|
const onFeatureSelect = (data: any) => {
|
||||||
|
// 처리시설코드
|
||||||
|
const swtrmtFcltCd = data.get("swtrmt_fclt_cd");
|
||||||
|
reuseData(swtrmtFcltCd);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SearchBar
|
||||||
|
setHasuPk={(hasuPk: any) => {
|
||||||
|
if (hasuPk) {
|
||||||
|
setSelectDataPk(hasuPk);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onApply={onApply}
|
||||||
|
/>
|
||||||
|
<div className="bg-container">
|
||||||
|
<Gis
|
||||||
|
treeData={treeData}
|
||||||
|
isSearch={false}
|
||||||
|
gisMaps={gisMaps}
|
||||||
|
isMiniMap={false}
|
||||||
|
searchView={undefined}
|
||||||
|
detailView={undefined}
|
||||||
|
onFeatureSelect={onFeatureSelect}
|
||||||
|
setIsOpen={setIsOpen}
|
||||||
|
setFeatureData={setFeatureData}
|
||||||
|
featureData={featureData}
|
||||||
|
setIsSearchModalOpen={setIsSearchModalOpen}
|
||||||
|
isOpen={isOpen}
|
||||||
|
isSearchModalOpen={isSearchModalOpen}
|
||||||
|
onCheckedLayersChange={onCheckedLayersChange}
|
||||||
|
imgVisble={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<SelectedFcltList
|
||||||
|
data={selectedFclts}
|
||||||
|
onRemove={(updatedList) => setSelectedFclts(updatedList)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Map;
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
type FcltItem = {
|
||||||
|
pbnstRuseFcltId: string;
|
||||||
|
ruseUtztnFcltNm: string;
|
||||||
|
splyExtraCpct?: number;
|
||||||
|
ruseCpct?: number;
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SelectedFcltList({
|
||||||
|
data = [],
|
||||||
|
onRemove,
|
||||||
|
}: {
|
||||||
|
data: FcltItem[];
|
||||||
|
onRemove: (updatedList: FcltItem[]) => void;
|
||||||
|
}) {
|
||||||
|
const handleDelete = (code: string) => {
|
||||||
|
const updatedList = data.filter((item) => item.pbnstRuseFcltId !== code);
|
||||||
|
onRemove(updatedList);
|
||||||
|
};
|
||||||
|
|
||||||
|
const TableContent = (
|
||||||
|
<>
|
||||||
|
<thead className="bg-gray-100 text-gray-700 text-[13px]">
|
||||||
|
<tr>
|
||||||
|
<th className="px-3 py-2 text-left border-b">시설명</th>
|
||||||
|
<th className="px-3 py-2 text-right border-b">
|
||||||
|
하수처리량
|
||||||
|
<br />
|
||||||
|
(㎥/일)
|
||||||
|
</th>
|
||||||
|
<th className="px-3 py-2 text-right border-b">
|
||||||
|
재이용량
|
||||||
|
<br />
|
||||||
|
(㎥/일)
|
||||||
|
</th>
|
||||||
|
<th className="px-3 py-2 border-b">삭제</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{data.map((item) => (
|
||||||
|
<tr key={item.pbnstRuseFcltId} className="border-b last:border-none">
|
||||||
|
<td className="px-3 py-2 text-[12px]">{item.ruseUtztnFcltNm}</td>
|
||||||
|
<td className="px-3 py-2 text-right text-[12px]">
|
||||||
|
{Number(item.splyExtraCpct || 0).toLocaleString()}
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-2 text-right text-[12px]">
|
||||||
|
{Number(item.ruseCpct || 0).toLocaleString()}
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-2 text-center">
|
||||||
|
<button
|
||||||
|
onClick={() => handleDelete(item.pbnstRuseFcltId)}
|
||||||
|
className="text-red-500 hover:text-red-700 text-sm font-bold"
|
||||||
|
aria-label="삭제"
|
||||||
|
title="삭제"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (data.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed right-40 top-[8rem] w-[40rem] max-h-[130rem] overflow-auto bg-white shadow-lg rounded p-4 border border-gray-300 z-50">
|
||||||
|
<h3 className="bg-cyan-700 text-white text-xl font-bold mb-2 p-2">
|
||||||
|
선택된 시설
|
||||||
|
</h3>
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full text-sm text-gray-800 border border-gray-200">
|
||||||
|
{TableContent}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
+16
-1
@@ -3,6 +3,7 @@
|
|||||||
* 관망도 API
|
* 관망도 API
|
||||||
*/
|
*/
|
||||||
const API_URL = `${process.env.NEXT_PUBLIC_SEWERAGE_API_URL}/v1/map`
|
const API_URL = `${process.env.NEXT_PUBLIC_SEWERAGE_API_URL}/v1/map`
|
||||||
|
const REUSE_API_URL = `${process.env.NEXT_PUBLIC_REUSE_API_URL}`
|
||||||
|
|
||||||
export type LyrStngProps = {
|
export type LyrStngProps = {
|
||||||
lyr_sn: string
|
lyr_sn: string
|
||||||
@@ -30,7 +31,21 @@ export type LyrStngProps = {
|
|||||||
export const mapService = {
|
export const mapService = {
|
||||||
//레이어 설정 조회
|
//레이어 설정 조회
|
||||||
getLyrStngs: async () => {
|
getLyrStngs: async () => {
|
||||||
const response = await fetch(`${API_URL}/lyrStngs/WR_NETWORK`, {
|
const response = await fetch(`${API_URL}/lyrStngs/DM_NETWORK`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
},
|
||||||
|
getPbnstRuseFclts: async (ruseFcltPk:any) => {
|
||||||
|
const response = await fetch(`${REUSE_API_URL}/reuseFc/pubFc/${ruseFcltPk}`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
+10
-4
@@ -3,7 +3,7 @@
|
|||||||
import { BaseLayout } from "@common/components";
|
import { BaseLayout } from "@common/components";
|
||||||
import type { MenuItem } from "./store/common";
|
import type { MenuItem } from "./store/common";
|
||||||
import { useDmStore } from "./store";
|
import { useDmStore } from "./store";
|
||||||
import { useRouter } from "next/navigation";
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
|
|
||||||
export default function Base({
|
export default function Base({
|
||||||
children,
|
children,
|
||||||
@@ -13,10 +13,16 @@ export default function Base({
|
|||||||
const { getMenuAuth } = useDmStore((state) => state);
|
const { getMenuAuth } = useDmStore((state) => state);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const handleClick = (id:MenuItem) =>{
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
const handleClick = (id: MenuItem) => {
|
||||||
getMenuAuth(id.menuCd);
|
getMenuAuth(id.menuCd);
|
||||||
router.push(id.urlPath);
|
router.push(id.urlPath);
|
||||||
}
|
};
|
||||||
|
|
||||||
return <BaseLayout handleMenuClick={handleClick}>{children}</BaseLayout>;
|
return (
|
||||||
|
<BaseLayout currentPath={pathname ?? ""} handleMenuClick={handleClick}>
|
||||||
|
{children}
|
||||||
|
</BaseLayout>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import Base from "./base";
|
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import { StoreProvider } from "./store";
|
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
children,
|
children,
|
||||||
@@ -9,12 +7,11 @@ export default function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="ko">
|
||||||
<body>
|
<body>
|
||||||
<StoreProvider>
|
{children}
|
||||||
<Base>{children}</Base>
|
|
||||||
</StoreProvider>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
|
||||||
|
const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id";
|
||||||
|
const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization";
|
||||||
|
const SERVER_API_URL = process.env.NEXT_PUBLIC_DM_SERVER_API_URL || "";
|
||||||
|
const SEWERAGE_API_URL = process.env.NEXT_PUBLIC_SEWERAGE_API_URL || "";
|
||||||
|
const GEOSERVER_URL = process.env.NEXT_PUBLIC_GEOSERVER_URL || "";
|
||||||
|
const LX_GEOSERVER_URL = process.env.NEXT_PUBLIC_LX_GEOSERVER_URL || "";
|
||||||
|
const DC_URL = process.env.NEXT_PUBLIC_DC_URL || "";
|
||||||
|
|
||||||
|
export function middleware(req: NextRequest) {
|
||||||
|
console.log("Middleware 진입:", req.nextUrl.pathname);
|
||||||
|
const pathname = req.nextUrl.pathname;
|
||||||
|
const searchParams = req.nextUrl.searchParams;
|
||||||
|
|
||||||
|
if (pathname.startsWith("/server")) {
|
||||||
|
|
||||||
|
const decodedParams = Array.from(searchParams.entries())
|
||||||
|
.map(([key, value]) => `${key}=${decodeURIComponent(value)}`)
|
||||||
|
.join("&");
|
||||||
|
|
||||||
|
const newUrl = `${SERVER_API_URL}${pathname.replace("/server", "")}${
|
||||||
|
decodedParams ? `?${decodedParams}` : ""
|
||||||
|
}`;
|
||||||
|
return NextResponse.rewrite(new URL(newUrl));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathname.startsWith("/geoserver")) {
|
||||||
|
let queryParams = searchParams.toString();
|
||||||
|
const typename = searchParams.get("TYPENAME");
|
||||||
|
|
||||||
|
let newUrl = `${GEOSERVER_URL}${pathname.replace("/geoserver", "")}${queryParams ? `?${queryParams}` : ""}`;
|
||||||
|
|
||||||
|
if (
|
||||||
|
typename == "hasudo:tb_il_swage_prcs_fclt" //하수처리시설
|
||||||
|
|| typename == "hasudo:tb_ms_swage_prcs_zone" //하수처리구역
|
||||||
|
|| typename == "hasudo:tb_ms_sbmrg_dngr_map" //침수위험지도
|
||||||
|
|| typename == "hasudo:tb_ms_flood_dngr_map" //도시침수지도
|
||||||
|
) {
|
||||||
|
newUrl = `${GEOSERVER_URL}${pathname.replace("/geoserver", "")}${queryParams ? `?${queryParams}` : ""}`;
|
||||||
|
}
|
||||||
|
else if (
|
||||||
|
typename == "hasudo:tb_ms_swage_ppl" //관로
|
||||||
|
|| typename == "hasudo:tb_ms_swage_mhl" //맨홀
|
||||||
|
|| typename == "hasudo:tb_ms_swage_pmstn" //펌프장
|
||||||
|
|| typename == "hasudo:tb_ms_stofcbr" //우수토실
|
||||||
|
) {
|
||||||
|
newUrl = `${LX_GEOSERVER_URL}${pathname.replace("/geoserver", "")}${queryParams ? `?${queryParams}` : ""}`;
|
||||||
|
}
|
||||||
|
else if (
|
||||||
|
typename == "hasudo:om_dt_untrt_swg_rain" //중점우수토실
|
||||||
|
|| typename == "hasudo:om_wqlt_station" //관측소
|
||||||
|
|| typename == "hasudo:tb_wr_bdst_ruse_fclt" //재이용시설
|
||||||
|
|| typename == "hasudo:tb_wr_rnwtr_utztn_fclt" //빗물이용시설
|
||||||
|
|| typename == "hasudo:tb_wr_rswtr_fclt" //중수도시설
|
||||||
|
|| typename == "hasudo:tb_wr_htdfl_fclt" //온배수시설
|
||||||
|
|| typename == "hasudo:tb_wr_ruse_fclt_dmd_co" //수요처
|
||||||
|
|| typename == "hasudo:tb_wr_tmpr_dmd_co" //잠재수요처
|
||||||
|
){
|
||||||
|
const url = req.nextUrl.clone();
|
||||||
|
url.searchParams.set('TYPENAME', typename.replace("hasudo:",""));
|
||||||
|
queryParams = url.searchParams.toString();
|
||||||
|
newUrl = `${SEWERAGE_API_URL}/v1/map/geometry${queryParams ? `?${queryParams}` : ""}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[Middleware] Rewriting from '/geoserver' path to: ${newUrl}`);
|
||||||
|
return NextResponse.rewrite(new URL(newUrl));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
pathname.startsWith("/attach-service") ||
|
||||||
|
pathname.startsWith("/cms-service")
|
||||||
|
) {
|
||||||
|
const requestHeaders = new Headers(req.headers);
|
||||||
|
const token = req.cookies.get(CLAIM_NAME)?.value;
|
||||||
|
const userId = req.cookies.get(AUTH_USER_ID)?.value;
|
||||||
|
const newUrl = `${SERVER_API_URL}${pathname}`;
|
||||||
|
|
||||||
|
if (token) requestHeaders.set(CLAIM_NAME, token);
|
||||||
|
if (userId) requestHeaders.set(AUTH_USER_ID, userId);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`[Middleware] Rewriting from '/attach-service' path to: ${newUrl}`,
|
||||||
|
);
|
||||||
|
return NextResponse.rewrite(new URL(newUrl), {
|
||||||
|
headers: requestHeaders,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathname.startsWith("/user-service")) {
|
||||||
|
const requestHeaders = new Headers(req.headers);
|
||||||
|
const newUrl = `${SERVER_API_URL}${pathname}`;
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`[Middleware] Rewriting from '/user-service' path to: ${newUrl}`,
|
||||||
|
);
|
||||||
|
return NextResponse.rewrite(new URL(newUrl), {
|
||||||
|
headers: requestHeaders,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathname.startsWith("/dc-service")) {
|
||||||
|
const requestHeaders = new Headers(req.headers);
|
||||||
|
const decodedParams = Array.from(searchParams.entries())
|
||||||
|
.map(([key, value]) => `${key}=${decodeURIComponent(value)}`)
|
||||||
|
.join("&");
|
||||||
|
|
||||||
|
const newUrl = `${DC_URL}${pathname}${decodedParams ? `?${decodedParams}` : ""}`;
|
||||||
|
console.log(
|
||||||
|
`[Middleware] Rewriting from '/dc-service' path to: ${newUrl}`,
|
||||||
|
);
|
||||||
|
return NextResponse.rewrite(new URL(newUrl), {
|
||||||
|
headers: requestHeaders,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return NextResponse.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
matcher: [
|
||||||
|
"/server/:path*",
|
||||||
|
"/geoserver/:path*",
|
||||||
|
"/attach-service/:path*",
|
||||||
|
"/user-service/:path*",
|
||||||
|
"/cms-service/:path*",
|
||||||
|
"/dc-service/:path*",
|
||||||
|
],
|
||||||
|
|
||||||
|
};
|
||||||
@@ -30,9 +30,6 @@ export default function Home() {
|
|||||||
<Button className='ml-5 place-self-auto'>
|
<Button className='ml-5 place-self-auto'>
|
||||||
<Link href="/pages/sdmMng/sdmApplyList">수요처 신청 현황(개발중)</Link>
|
<Link href="/pages/sdmMng/sdmApplyList">수요처 신청 현황(개발중)</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<Button className='ml-5 place-self-auto'>
|
|
||||||
<Link href="/pages/map/mainMap">지도팝업 (임시)</Link>
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
"use client";
|
|
||||||
const Map = () => {
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
|
|
||||||
|
|
||||||
</>
|
|
||||||
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Map;
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
|
||||||
import { createPortal } from 'react-dom';
|
|
||||||
|
|
||||||
// 현재 문서의 스타일(CSS)을 새 창으로 복사하는 함수
|
|
||||||
const copyStyles = (sourceDoc: Document, targetDoc: Document) => {
|
|
||||||
Array.from(sourceDoc.styleSheets).forEach(styleSheet => {
|
|
||||||
try {
|
|
||||||
if (styleSheet.cssRules) { // <style> 태그로 주입된 CSS
|
|
||||||
const newStyleEl = sourceDoc.createElement('style');
|
|
||||||
Array.from(styleSheet.cssRules).forEach(cssRule => {
|
|
||||||
newStyleEl.appendChild(sourceDoc.createTextNode(cssRule.cssText));
|
|
||||||
});
|
|
||||||
targetDoc.head.appendChild(newStyleEl);
|
|
||||||
} else if (styleSheet.href) { // <link> 태그로 연결된 CSS 파일
|
|
||||||
const newLinkEl = sourceDoc.createElement('link');
|
|
||||||
newLinkEl.rel = 'stylesheet';
|
|
||||||
newLinkEl.href = styleSheet.href;
|
|
||||||
targetDoc.head.appendChild(newLinkEl);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('스타일시트를 복사할 수 없습니다:', e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
interface NewWindowPortalProps {
|
|
||||||
children: React.ReactNode;
|
|
||||||
onClose: () => void;
|
|
||||||
title?: string;
|
|
||||||
width?: number;
|
|
||||||
height?: number;
|
|
||||||
resizable?: string;
|
|
||||||
scrollbars?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const NewWindowPortal: React.FC<NewWindowPortalProps> = ({ children, onClose, title = "" , width = 900, height = 800, resizable = "yes", scrollbars= "yes" }) => {
|
|
||||||
const [container, setContainer] = useState<HTMLElement | null>(null);
|
|
||||||
const newWindowRef = useRef<Window | null>(null);
|
|
||||||
const onCloseRef = useRef(onClose);
|
|
||||||
|
|
||||||
// onClose 함수를 ref로 저장하여 의존성 문제 해결
|
|
||||||
useEffect(() => {
|
|
||||||
onCloseRef.current = onClose;
|
|
||||||
}, [onClose]);
|
|
||||||
|
|
||||||
const handleUnload = useCallback(() => {
|
|
||||||
onCloseRef.current();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// 이미 창이 열려있으면 새로 열지 않음
|
|
||||||
if (newWindowRef.current && !newWindowRef.current.closed) return;
|
|
||||||
|
|
||||||
// 1. 새 창을 열고 참조를 저장합니다.
|
|
||||||
const popupOptions = `width=${width},height=${height},resizable=${resizable},scrollbars=${scrollbars}`;
|
|
||||||
newWindowRef.current = window.open('', title, popupOptions);
|
|
||||||
|
|
||||||
if (newWindowRef.current) {
|
|
||||||
newWindowRef.current.document.title = title;
|
|
||||||
|
|
||||||
// 2. React 컴포넌트를 마운트할 컨테이너 div를 생성하여 새 창의 body에 추가합니다.
|
|
||||||
const div = newWindowRef.current.document.createElement('div');
|
|
||||||
newWindowRef.current.document.body.appendChild(div);
|
|
||||||
setContainer(div);
|
|
||||||
|
|
||||||
// 3. 메인 창의 스타일을 새 창으로 복사합니다. (매우 중요)
|
|
||||||
copyStyles(document, newWindowRef.current.document);
|
|
||||||
|
|
||||||
// 4. 사용자가 창을 닫을 때 부모에게 알리기 위한 이벤트 리스너를 설정합니다.
|
|
||||||
// const handleUnload = () => {
|
|
||||||
// onClose();
|
|
||||||
// };
|
|
||||||
newWindowRef.current.addEventListener('unload', handleUnload);
|
|
||||||
|
|
||||||
// 5. 컴포넌트가 사라질 때 창을 닫는 클린업 함수를 반환합니다.
|
|
||||||
return () => {
|
|
||||||
if (newWindowRef.current) {
|
|
||||||
newWindowRef.current.removeEventListener('unload', handleUnload);
|
|
||||||
if (!newWindowRef.current.closed) {
|
|
||||||
newWindowRef.current.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
alert('팝업이 차단되었습니다. 팝업 차단을 해제하고 다시 시도해주세요.');
|
|
||||||
onCloseRef.current();
|
|
||||||
}
|
|
||||||
}, [handleUnload, onClose, title]);
|
|
||||||
|
|
||||||
// `createPortal`을 사용해 children을 새 창의 container div에 렌더링합니다.
|
|
||||||
return container ? createPortal(children, container) : null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default NewWindowPortal;
|
|
||||||
@@ -1,335 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from 'react';
|
|
||||||
import Image from 'next/image';
|
|
||||||
|
|
||||||
const NAVS = [
|
|
||||||
{ id: 'card1', label: '물재이용이란' },
|
|
||||||
{ id: 'card2', label: '물재이용 필요성' },
|
|
||||||
{ id: 'card3', label: '빗물 재이용' },
|
|
||||||
{ id: 'card4', label: '중수 재이용' },
|
|
||||||
{ id: 'card5', label: '하수 재이용' },
|
|
||||||
{ id: 'card6', label: '물재이용 목표' },
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function WrAboutPage() {
|
|
||||||
const [activeIdx, setActiveIdx] = useState(0);
|
|
||||||
const navRefs = useRef<(HTMLLIElement | null)[]>([]);
|
|
||||||
const cardRefs = useRef<(HTMLElement | null)[]>([]);
|
|
||||||
|
|
||||||
// 클릭 시 이동
|
|
||||||
const handleNavClick = (idx: number) => {
|
|
||||||
setActiveIdx(idx);
|
|
||||||
const targetCard = cardRefs.current[idx];
|
|
||||||
if (targetCard) {
|
|
||||||
targetCard.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 키보드 이벤트 처리
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent, idx: number) => {
|
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
|
||||||
e.preventDefault();
|
|
||||||
handleNavClick(idx);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const observerOptions = {
|
|
||||||
root: null,
|
|
||||||
rootMargin: '-200px 0px 0px 0px', // 상단에서 200px 지점을 기준으로
|
|
||||||
threshold: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
const observer = new IntersectionObserver((entries) => {
|
|
||||||
entries.forEach((entry) => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
const idx = cardRefs.current.findIndex(ref => ref === entry.target);
|
|
||||||
if (idx !== -1) {
|
|
||||||
setActiveIdx(idx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, observerOptions);
|
|
||||||
|
|
||||||
// 각 카드 요소를 관찰
|
|
||||||
cardRefs.current.forEach((card) => {
|
|
||||||
if (card) {
|
|
||||||
observer.observe(card);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
cardRefs.current.forEach((card) => {
|
|
||||||
if (card) {
|
|
||||||
observer.unobserve(card);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<main className="relative w-[950px] mx-auto pt-[100px] font-sans">
|
|
||||||
{/* 사이드바 */}
|
|
||||||
<nav className="fixed left-[290px] top-[195px] w-[210px] z-50" aria-label="물재이용 정보 섹션 내비게이션">
|
|
||||||
<ul role="list" className="relative space-y-6">
|
|
||||||
{NAVS.map((nav, idx) => (
|
|
||||||
<li
|
|
||||||
key={nav.id}
|
|
||||||
ref={(el) => { navRefs.current[idx] = el; }}
|
|
||||||
role="listitem"
|
|
||||||
tabIndex={0}
|
|
||||||
aria-current={idx === activeIdx ? 'true' : 'false'}
|
|
||||||
aria-label={`현재 위치: ${nav.label}`}
|
|
||||||
onClick={() => handleNavClick(idx)}
|
|
||||||
onKeyDown={(e) => handleKeyDown(e, idx)}
|
|
||||||
className={`nav-item relative pl-6 text-2xl font-bold text-gray-700 cursor-pointer transition-all duration-300
|
|
||||||
after:content-[''] after:absolute after:top-6 after:left-[0.5rem] after:w-[2px] after:h-[24px] after:bg-[#C5C5C5]
|
|
||||||
${idx === activeIdx ? 'text-4xl font-bold text-gray-900' : ''}`}
|
|
||||||
>
|
|
||||||
<span className={`absolute left-[0.5rem] top-1 w-4 h-4 rounded-full -translate-x-1/2 transition-all duration-300
|
|
||||||
${idx === activeIdx ? 'bg-blue-500 border-blue-500' : 'bg-white border-gray-400 border'}`}></span>
|
|
||||||
{nav.label}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<article ref={(el) => { cardRefs.current[0] = el; }} className="card-wrap pl-[100px] mb-24 !pt-0" id="card1" aria-labelledby="card1-title">
|
|
||||||
<h2 id="card1-title" className="relative text-center text-4xl font-bold text-gray-900 mb-6">
|
|
||||||
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
|
||||||
물재이용이란
|
|
||||||
</h2>
|
|
||||||
<section aria-labelledby="card1-desc" className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px] ">
|
|
||||||
<h1 id="card1-desc" className="text-[#269DDA] text-4xl font-bold mb-8">물 재이용이 무엇인가요?</h1>
|
|
||||||
<p className="text-2xl font-bold mb-10 leading-relaxed">
|
|
||||||
물 재이용은 빗물, 오수, 하수처리수, 폐수처리수 및 발전소 온배수를<br />
|
|
||||||
물 재이용시설을 이용하여 처리하고, 그 처리된 물을 생활, 공업, 농업, 조경,<br />
|
|
||||||
하천 유지 등의 용도로 이용하는 것을 말합니다.
|
|
||||||
</p>
|
|
||||||
<Image
|
|
||||||
src="/img/wrAbout/img_01_1.png"
|
|
||||||
alt="이 이미지는 물 재이용 시스템의 개요를 나타냅니다. 가정, 공공기관, 하수처리장 등에서 발생한 생활하수, 빗물, 하수처리수를 물 재이용 시설에서 처리한 후, 청소나 화장실, 농업, 살수차, 하천 유지, 공장, 공원 및 골프장 등 다양한 용도로 활용하는 과정을 도식화한 것입니다."
|
|
||||||
width={800}
|
|
||||||
height={400}
|
|
||||||
className="w-[90%] mx-auto mb-8 rounded"
|
|
||||||
/>
|
|
||||||
<p className="text-center">
|
|
||||||
환경부는 2011년에 '물의 재이용 촉진 및 지원에 관한 법률'을 제정하여<br />
|
|
||||||
한국환경공단과 함께 물 재이용 확대 및 촉진을 위해 노력하고 있습니다.
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
{/* 카드2 */}
|
|
||||||
<article ref={(el) => { cardRefs.current[1] = el; }} className="card-wrap pl-[100px] mb-24" id="card2" aria-labelledby="card2-title">
|
|
||||||
<h2 id="card2-title" className="relative text-center text-4xl font-bold text-gray-900 mb-6">
|
|
||||||
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
|
||||||
물재이용 필요성
|
|
||||||
</h2>
|
|
||||||
<section aria-labelledby="card2-desc" className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px] ">
|
|
||||||
<h1 id="card2-desc" className="text-[#EE7B62] text-4xl font-bold mb-8">물 재이용, 왜 필요한가요?</h1>
|
|
||||||
<p className="text-2xl font-bold mb-10 leading-relaxed">
|
|
||||||
빗물, 중수, 하수처리수는 지속적으로 이용 가능한 순환자원입니다.<br />
|
|
||||||
물 재이용을 통한 효율적인 수자원 활용은 물 스트레스국가인 우리나라에서<br />가뭄대응능력 향상 및 하천수질보전을 위해 반드시 필요한 일입니다.
|
|
||||||
</p>
|
|
||||||
<Image
|
|
||||||
src="/img/wrAbout/img_02_1.png"
|
|
||||||
alt="이 이미지는 기후변화로 인한 온도 상승 추이를 설명한다. 왼쪽에는 지구 그림과 온도계가 함께 표시되어 있으며, 지구 평균 기온은 지난 100년간 약 0.9℃ 상승했음을 나타낸다. 오른쪽에는 대한민국 지도와 함께 온도계가 있으며, 지난 100년간 약 1.8℃ 상승했음을 보여준다. 이는 지구 평균보다 2배 높은 수치이며, 자료 출처는 「기후변화 평가보고서 2020, 환경부·기상청」이다."
|
|
||||||
width={800}
|
|
||||||
height={400}
|
|
||||||
className="w-[90%] mx-auto mb-8 w-[80%]"
|
|
||||||
/>
|
|
||||||
<p className="text-2xl font-bold mb-10 leading-relaxed">
|
|
||||||
- 우리나라의 기후변화 진행속도는 세계 평균을 상회하고 있습니다.<br />
|
|
||||||
- 기후변화로 인한 국지적인 가뭄은 더욱 자주, 더욱 심하게 일어나고 있으며, 우리의 물 안보를 위협하고 있습니다.
|
|
||||||
</p>
|
|
||||||
<Image
|
|
||||||
src="/img/wrAbout/img_02_2.png"
|
|
||||||
alt="세계 평균 1인당 강수량은 15,044m³이며, 대한민국은 2,546m³로 약 1/6 수준입니다. 계절별 강수량은 봄 10%, 여름 70%, 가을 15%, 겨울 5%입니다. 이는 여름철에 강수량이 집중되는 문제를 나타냅니다."
|
|
||||||
width={800}
|
|
||||||
height={400}
|
|
||||||
className="w-[90%] mx-auto mb-8"
|
|
||||||
/>
|
|
||||||
<p className="text-2xl font-bold mb-10 leading-relaxed">
|
|
||||||
- 우리나라의 1인당 강수량은 2,546㎥ 으로 세계 평균 15,044㎥의 약 1/6 수준입니다.<br />
|
|
||||||
- 우리나라 강수량의 70%는 여름철에 집중되어 있어 이용 가능한 수자원확보가 어렵습니다.
|
|
||||||
</p>
|
|
||||||
<Image
|
|
||||||
src="/img/wrAbout/img_02_3.png"
|
|
||||||
alt="물 재이용은 환경을 보호하며 지속 가능한 발전이 가능하게 하고, 안정적인 수자원 확보, 깨끗하고 경제적인 공업용수 공급, 하천 수질 개선을 통한 생태계 회복에 기여합니다."
|
|
||||||
width={800}
|
|
||||||
height={400}
|
|
||||||
className="w-[93%] mx-auto"
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
{/* 카드3 */}
|
|
||||||
<article ref={(el) => { cardRefs.current[2] = el; }} className="card-wrap pl-[100px] mb-24" id="card3" aria-labelledby="card3-title">
|
|
||||||
<h2 id="card3-title" className="relative text-center text-4xl font-bold text-gray-900 mb-6">
|
|
||||||
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
|
||||||
빗물 재이용
|
|
||||||
</h2>
|
|
||||||
<section aria-labelledby="card3-desc" className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px] ">
|
|
||||||
<h1 id="card3-desc" className="text-[#269DDA] text-4xl font-bold mb-6">친환경 수자원 빗물,<br />편리하게 사용할 수 있어요!</h1>
|
|
||||||
<p className="text-2xl font-bold mb-10 leading-relaxed">가뭄을 극복하고 홍수를 예방하기 위해<br />비가 내릴 때 빗물을 모아두었다가 사용합니다.</p>
|
|
||||||
<Image
|
|
||||||
src="/img/wrAbout/img_03_1.png"
|
|
||||||
alt="주택 지붕에 내린 빗물을 저장조에 모아 조경용수, 화장실용수, 세척·살수용수로 재활용하는 과정을 보여줍니다."
|
|
||||||
width={800}
|
|
||||||
height={400}
|
|
||||||
className="w-[90%] mx-auto mb-6"
|
|
||||||
/>
|
|
||||||
<Image
|
|
||||||
src="/img/wrAbout/img_03_2.png"
|
|
||||||
alt="빗물이용"
|
|
||||||
width={200}
|
|
||||||
height={100}
|
|
||||||
className="mb-5 w-[20%]"
|
|
||||||
/>
|
|
||||||
<p className="text-2xl font-bold leading-relaxed">
|
|
||||||
- 의무 설치대상: 지붕면적 1,000㎡ 이상의 운동장, 체육관, 공공업무시설 등<br />
|
|
||||||
- 설 치 기 준: 집수시설, 처리시설, 빗물 저류조(지붕 집수면적 x 0.05m 이상)<br />
|
|
||||||
- 활 용 용 도: 골프장 등의 조경용수, 화장실용수, 세척·살수용수 등
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
{/* 카드4 */}
|
|
||||||
<article ref={(el) => { cardRefs.current[3] = el; }} className="card-wrap pl-[100px] mb-24" id="card4" aria-labelledby="card4-title">
|
|
||||||
<h2 id="card4-title" className="relative text-center text-4xl font-bold text-gray-900 mb-6">
|
|
||||||
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
|
||||||
중수 재이용
|
|
||||||
</h2>
|
|
||||||
<section aria-labelledby="card4-desc" className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px] ">
|
|
||||||
<h1 id="card4-desc" className="text-[#9771B0] text-4xl font-bold mb-6">요모조모 쓸모 많은 중수,<br />알뜰하게 재이용할 수 있어요!</h1>
|
|
||||||
<p className="text-2xl font-bold mb-10 leading-relaxed">한번 사용한 물을 중수도 시설을 통해 재생하여<br />개별적 또는 지역적으로 재이용할 수 있습니다.</p>
|
|
||||||
<Image
|
|
||||||
src="/img/wrAbout/img_04_1.png"
|
|
||||||
alt="욕실, 세면대, 주방, 세탁기 등에서 발생한 생활용수를 중수도 시설에서 처리하여 화장실 변기용수와 조경용수로 재이용하는 시스템 흐름도입니다."
|
|
||||||
width={800}
|
|
||||||
height={400}
|
|
||||||
className="w-[90%] mx-auto mb-6"
|
|
||||||
/>
|
|
||||||
<Image
|
|
||||||
src="/img/wrAbout/img_04_2.png"
|
|
||||||
alt="중수재이용"
|
|
||||||
width={200}
|
|
||||||
height={100}
|
|
||||||
className="mb-5 w-[20%]"
|
|
||||||
/>
|
|
||||||
<p className="text-2xl font-bold leading-relaxed">
|
|
||||||
- 의무 설치대상: 일정규모 이상의 대규모점포, 숙박/목욕장, 공장, 각종 개발사업 등<br />
|
|
||||||
- 의무 재이용량: 물 사용량의 10% 이상<br />
|
|
||||||
- 활 용 용 도: 청소·화장실용수, 세척·살수용수, 조경용수, 공업용수 등
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
{/* 카드5 */}
|
|
||||||
<article ref={(el) => { cardRefs.current[4] = el; }} className="card-wrap pl-[100px] mb-24" id="card5" aria-labelledby="card5-title">
|
|
||||||
<h2 id="card5-title" className="relative text-center text-4xl font-bold text-gray-900 mb-6">
|
|
||||||
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
|
||||||
하수 재이용
|
|
||||||
</h2>
|
|
||||||
<section aria-labelledby="card5-desc" className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px] ">
|
|
||||||
<h1 id="card5-desc" className="text-[#00A5B1] text-4xl font-bold mb-6">아직 할 일이 많은 하수처리수,<br />풍족하게 재이용할 수 있어요!</h1>
|
|
||||||
<p className="text-2xl font-bold mb-10 leading-relaxed">사용하고 버려져 공공하수처리장으로 모여든 물은<br />용도에 맞는 처리 후 재이용 할 수 있습니다.</p>
|
|
||||||
<Image
|
|
||||||
src="/img/wrAbout/img_05_1.png"
|
|
||||||
alt="주택과 건물에서 발생한 하수가 공공하수처리장을 거쳐 처리된 후, 물 재이용 시설에서 정화되어 공업용수, 하천유지용수, 농업용수, 세척·살수용수로 재활용됩니다."
|
|
||||||
width={800}
|
|
||||||
height={400}
|
|
||||||
className="w-[90%] mx-auto mb-6"
|
|
||||||
/>
|
|
||||||
<Image
|
|
||||||
src="/img/wrAbout/img_05_2.png"
|
|
||||||
alt="하수처리 재이용"
|
|
||||||
width={200}
|
|
||||||
height={100}
|
|
||||||
className="mb-5 w-[20%]"
|
|
||||||
/>
|
|
||||||
<p className="text-2xl font-bold leading-relaxed">
|
|
||||||
- 의무 이용대상: 공공하수처리장(시설용량 5,000㎥/일 이상)<br />
|
|
||||||
- 의무 재이용량: 일 하수처리량의 10% 이상<br />
|
|
||||||
- 활 용 용 도: 공업용수, 하천유지용수, 농업용수 등
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
{/* 카드6 */}
|
|
||||||
<article ref={(el) => { cardRefs.current[5] = el; }} className="card-wrap pl-[100px] mb-24" id="card6" aria-labelledby="card6-title">
|
|
||||||
<h2 id="card6-title" className="relative text-center text-4xl font-bold text-gray-900 mb-6">
|
|
||||||
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
|
||||||
물재이용 목표
|
|
||||||
</h2>
|
|
||||||
<section aria-labelledby="card6-desc" className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px] ">
|
|
||||||
<h1 id="card6-desc" className="text-[#269DDA] text-4xl font-bold mb-6">2030년 물 재이용 목표를 위해<br />모두의 관심과 노력이 필요합니다.</h1>
|
|
||||||
<p className="text-2xl font-bold mb-6 leading-relaxed">
|
|
||||||
환경부는 2030년까지의 제2차 물 재이용 기본계획을 수립하여<br />한국환경공단과 함께 물 재이용 확대 및 촉진을 위해 노력하고 있습니다.
|
|
||||||
</p>
|
|
||||||
<Image
|
|
||||||
src="/img/wrAbout/img_06_1.png"
|
|
||||||
alt="지속가능한 물 재이용 정착으로 건전한 물순환 확산"
|
|
||||||
width={800}
|
|
||||||
height={400}
|
|
||||||
className="w-[80%] mx-auto mb-6"
|
|
||||||
/>
|
|
||||||
<div className="flex gap-3 flex-wrap justify-between bg-gray-100 p-6 rounded-lg mb-8">
|
|
||||||
<div className="flex-1">
|
|
||||||
<Image
|
|
||||||
src="/img/wrAbout/icon_01.png"
|
|
||||||
alt=""
|
|
||||||
width={70}
|
|
||||||
height={70}
|
|
||||||
className="mx-auto mb-4 w-[auto] h-[70px]"
|
|
||||||
/>
|
|
||||||
<h3 className="text-center font-semibold text-nowrap mb-2">도시 물 순환이용<br />체계로의 전환</h3>
|
|
||||||
<ul className="text-sm text-gray-600 leading-relaxed space-y-1">
|
|
||||||
<li className="text-nowrap">- 지속가능한 물 순환이용체계 구축</li>
|
|
||||||
<li>- 물 재이용시설 관리체계 강화</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 text-center">
|
|
||||||
<Image
|
|
||||||
src="/img/wrAbout/icon_02.png"
|
|
||||||
alt=""
|
|
||||||
width={70}
|
|
||||||
height={70}
|
|
||||||
className="mx-auto mb-4 w-[auto] h-[70px]"
|
|
||||||
/>
|
|
||||||
<h3 className="text-center font-semibold text-nowrap mb-2">유역기반 하수처리수 재이용<br />수요확대 및 공급체계 구축</h3>
|
|
||||||
<ul className="text-sm text-gray-600 leading-relaxed space-y-1">
|
|
||||||
<li>- 유역기반 통합물관리 연계 강화</li>
|
|
||||||
<li>- 하수처리수 재이용 공업용수 공급 활성화</li>
|
|
||||||
<li>- 하수처리수 재이용을 활용한 물순환 촉진</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 text-center">
|
|
||||||
<Image
|
|
||||||
src="/img/wrAbout/icon_03.png"
|
|
||||||
alt=""
|
|
||||||
width={70}
|
|
||||||
height={70}
|
|
||||||
className="mx-auto mb-4 w-[auto] h-[70px]"
|
|
||||||
/>
|
|
||||||
<h3 className="text-center font-semibold text-nowrap mb-2">물 재이용 인식제고<br />및 산업발전 기반 마련</h3>
|
|
||||||
<ul className="text-sm text-gray-600 leading-relaxed space-y-1">
|
|
||||||
<li>- 물 재이용 교육 및 대국민 홍보 강화</li>
|
|
||||||
<li>- 핵심기술 육성 및 산업발전 지원 강화</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Image
|
|
||||||
src="/img/wrAbout/img_06_2.png"
|
|
||||||
alt="물 재이용량은 2014년 13.3억톤에서 2030년 25.9억톤까지 증가할 것으로 예상됩니다. 그에 따른 기대 효과로는 연간 상수대체 17.9억톤, 물산업 시장성장 약 2조3,541억 원, 건천화된 73개 하천 해소, 약 20,550명의 고용 효과가 포함됩니다."
|
|
||||||
width={800}
|
|
||||||
height={400}
|
|
||||||
className="w-[90%] mx-auto"
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
</article>
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
import Image from "next/image";
|
|
||||||
|
|
||||||
export default function WrOverviewPage() {
|
|
||||||
return (
|
|
||||||
<div className="bg-[#F1F6F9]">
|
|
||||||
<div className="relative w-[950px] mx-auto pt-[50px] font-sans">
|
|
||||||
{/* 카드 1 - 물재이용 사업 추진 절차 */}
|
|
||||||
<div className="card-wrap mb-24" id="card1">
|
|
||||||
<h2 className="relative text-center text-4xl font-bold text-gray-900 mb-6">
|
|
||||||
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
|
||||||
물재이용 사업 추진 절차
|
|
||||||
</h2>
|
|
||||||
<div className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px]">
|
|
||||||
<table className="table-fixed w-full text-center text-sm">
|
|
||||||
<caption className="sr-only">물 재이용 추진 절차 상세 표</caption>
|
|
||||||
<colgroup>
|
|
||||||
<col style={{ width: "28%" }} />
|
|
||||||
<col style={{ width: "2%" }} />
|
|
||||||
<col style={{ width: "35%" }} />
|
|
||||||
<col style={{ width: "5%" }} />
|
|
||||||
<col style={{ width: "30%" }} />
|
|
||||||
</colgroup>
|
|
||||||
<tbody>
|
|
||||||
{/* 1단계 */}
|
|
||||||
<tr><td></td><td></td><td className="text-xl bg-gray-100 font-bold py-3">타당성 조사</td><td></td><td></td></tr>
|
|
||||||
<tr><td></td><td></td><td className="text-xl font-bold text-center">↓</td><td></td><td></td></tr>
|
|
||||||
{/* 2단계 */}
|
|
||||||
<tr><td className="text-xl bg-yellow-400 text-black font-bold py-3">매칭시스템 신청</td><td></td><td className="text-xl bg-gray-100 font-bold py-3">사업계획(안) 수립</td><td></td><td></td></tr>
|
|
||||||
<tr><td></td><td></td><td className="text-xl font-bold text-center">↓</td><td></td><td></td></tr>
|
|
||||||
{/* 3단계 */}
|
|
||||||
<tr><td className="text-xl bg-yellow-400 text-black font-bold py-3">하수도정보시스템 신청</td><td></td><td className="text-xl bg-gray-100 font-bold py-3">국고지원 신청</td><td className="text-lg">→</td><td className="text-xl bg-green-200 font-bold py-3">기술검토<br />(한국환경공단)</td></tr>
|
|
||||||
<tr><td></td><td></td><td className="text-xl font-bold text-center">↓</td><td></td><td></td></tr>
|
|
||||||
{/* 4단계 */}
|
|
||||||
<tr><td></td><td></td><td className="text-xl bg-gray-100 font-bold py-3">국고지원 확정 통보</td><td></td><td></td></tr>
|
|
||||||
<tr><td></td><td></td><td className="text-xl font-bold text-center">↓</td><td></td><td></td></tr>
|
|
||||||
{/* 5단계 */}
|
|
||||||
<tr><td></td><td></td><td className="text-xl bg-gray-100 font-bold py-3">사업계획 수립(실시설계)</td><td></td><td></td></tr>
|
|
||||||
<tr><td></td><td></td><td className="text-xl font-bold text-center">↓</td><td></td><td></td></tr>
|
|
||||||
{/* 6단계 */}
|
|
||||||
<tr><td className="text-xl bg-yellow-400 text-black font-bold py-3">(행정관) 기술검토 신청</td><td></td><td className="text-xl bg-gray-100 font-bold py-3">재원협의<br />(지방환경관서장)</td><td className="text-lg">→</td><td className="text-xl bg-green-200 font-bold py-3">기술검토<br />(한국환경공단)</td></tr>
|
|
||||||
<tr><td></td><td></td><td className="text-xl font-bold text-center">↓</td><td></td><td></td></tr>
|
|
||||||
{/* 7단계 */}
|
|
||||||
<tr><td className="text-xl bg-yellow-400 text-black font-bold py-3">(행정관) 기술검토 신청</td><td></td><td className="text-xl bg-gray-100 font-bold py-3">설치승인<br />(시도지사)</td><td className="text-lg">→</td><td className="text-xl bg-green-200 font-bold py-3">기술검토<br />(한국환경공단)</td></tr>
|
|
||||||
<tr><td></td><td></td><td className="text-xl font-bold text-center">↓</td><td></td><td></td></tr>
|
|
||||||
{/* 8단계 */}
|
|
||||||
<tr><td></td><td></td><td className="text-xl bg-gray-100 font-bold py-3">공사시행</td><td></td><td></td></tr>
|
|
||||||
<tr><td></td><td></td><td className="text-xl font-bold text-center">↓</td><td></td><td></td></tr>
|
|
||||||
{/* 9단계 */}
|
|
||||||
<tr><td className="text-xl bg-yellow-400 text-black font-bold py-3">(행정관) 기술검토 신청</td><td></td><td className="text-xl bg-gray-100 font-bold py-3">준공(국고정산)</td><td className="text-lg">→</td><td className="text-xl bg-green-200 font-bold py-3">기술검토<br />(한국환경공단)</td></tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 카드 2 - 물재이용 매칭 시스템 */}
|
|
||||||
<div className="card-wrap mb-24" id="card2">
|
|
||||||
<h2 className="relative text-center text-4xl font-bold text-gray-900 mb-6">
|
|
||||||
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
|
||||||
물재이용 매칭 시스템이란?
|
|
||||||
</h2>
|
|
||||||
<div className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px]">
|
|
||||||
{/* 개요 */}
|
|
||||||
<section>
|
|
||||||
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">개요</h2>
|
|
||||||
<p className="text-2xl font-bold text-gray-800 leading-relaxed">
|
|
||||||
기후변화 및 지자체의 개발속도 차이에 따라 물 자원의 수요-공급 불균형이 발생하여 빗물, 중수도, 하수처리 재처리수 등의 공급 여유가 있는 지자체의 재이용수를 부족한 지자체 수요처에 공급하여 불균형 해소를 통한 수자원 절약 및 물 자원 순환
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* 구성 */}
|
|
||||||
<section className="mt-8">
|
|
||||||
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">구성</h2>
|
|
||||||
<ul className="list-[square] pl-6 text-2xl font-bold text-gray-800 leading-relaxed space-y-2">
|
|
||||||
<li>공급자: 하수처리장, 빗물이용시설, 중수도 등</li>
|
|
||||||
<li>수요자: 산업체(냉각수), 공공시설(조경, 도로청소), 농민(농업용수) 등</li>
|
|
||||||
<li>시스템: 수질, 수량, 거리 기반 매칭, 중개</li>
|
|
||||||
<li>운영주체: 한국환경공단</li>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* 절차 */}
|
|
||||||
<section className="mt-8">
|
|
||||||
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 주요 내용</h2>
|
|
||||||
<p className="text-2xl font-bold text-gray-800 leading-relaxed">
|
|
||||||
① 공급자 등록 → ② 수요자 조건 입력 → ③ 매칭 검토 → ④ 시스템 매칭 → ⑤ 계약·공급 이행
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* 기대효과 */}
|
|
||||||
<section className="mt-8">
|
|
||||||
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">기대효과</h2>
|
|
||||||
<ul className="list-[square] pl-6 text-2xl font-bold text-gray-800 leading-relaxed space-y-2">
|
|
||||||
<li>물 자원 수요-공급 조정을 통한 효율적 분배</li>
|
|
||||||
<li>물 재이용시설 데이터 수집으로 체계적 관리</li>
|
|
||||||
<li>지자체, 사업시행자 등 재이용 정보 획득 용이</li>
|
|
||||||
</ul>
|
|
||||||
<div className="text-center my-6">
|
|
||||||
<Image
|
|
||||||
src="/img/wrOverview/img_ov_01_1.png"
|
|
||||||
alt="하수 재이용 시스템 흐름도. 저장탱크에서 재처리된 물이 손 씻기, 수도, 세정 등에 공급되며 재이용 활성화를 통해 최적의 물공급 방안이 마련되는 과정을 시각화함."
|
|
||||||
width={650}
|
|
||||||
height={500}
|
|
||||||
className="h-[500px] mx-auto mb-4 rounded"
|
|
||||||
/>
|
|
||||||
<p className="text-2xl font-bold text-gray-800 mt-10">(하수 재이용 시스템 개요도)</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import { getFetch } from "@/components/ui/api";
|
||||||
|
import { DEV_URL_FO } from "@/components/ui/config";
|
||||||
|
import type { FacilityInfo, SubsInfo } from "./type";
|
||||||
|
|
||||||
|
const checkWrRegList = async () => {
|
||||||
|
const wrRegList = await getFetch("wr", `/reuseFc/pubFc`);
|
||||||
|
return wrRegList;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRuseFclt = async (
|
||||||
|
page: string,
|
||||||
|
sggOrCity: string,
|
||||||
|
searchTxt?: string
|
||||||
|
) => {
|
||||||
|
const checkWrRegListRes = await checkWrRegList();
|
||||||
|
|
||||||
|
let url = `/prcsFclt/ruseInfo/${page ? page : 1}/5/A/${sggOrCity ? sggOrCity : "0"}/0/1/SW`;
|
||||||
|
|
||||||
|
const ruseList = checkWrRegListRes.data.filter((item: string) => item !== "");
|
||||||
|
|
||||||
|
if (searchTxt) {
|
||||||
|
url = url + `/${searchTxt}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(DEV_URL_FO + url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(ruseList),
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasuList = await response.json().then((res: any) => {
|
||||||
|
const data = res.data
|
||||||
|
.map((item: any) => {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
fctCpc: Number(item.fctCpc) || 0,
|
||||||
|
searchNm: item.fctNm,
|
||||||
|
searchPk: item.bscFctNm,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((hasu) => ruseList.find((ruse) => hasu.bscFctCd === ruse));
|
||||||
|
return { data: data, count: res.count };
|
||||||
|
});
|
||||||
|
|
||||||
|
return hasuList;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DmMapActions = {
|
||||||
|
findRuseFclt(sggOrCity: string, searchTxt: string): void;
|
||||||
|
setSubsInfo(subsInfo: SubsInfo[]): void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DmMapType = {
|
||||||
|
map: {
|
||||||
|
ruseFcltList: FacilityInfo[];
|
||||||
|
subsInfo: SubsInfo[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const dmMapState: DmMapType = {
|
||||||
|
map: {
|
||||||
|
ruseFcltList: [],
|
||||||
|
// subsInfo: [
|
||||||
|
// {
|
||||||
|
// ruseUtztnFcltNm: "수원제1,2 재이용1", // 재이용시설명
|
||||||
|
// ruseUtztnFcltCd: "41110PB001R1",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// ruseUtztnFcltNm: "낙양 재이용1", // 재이용시설명
|
||||||
|
// ruseUtztnFcltCd: "41150SW001R1",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// ruseUtztnFcltNm: "서남 재이용1", // 재이용시설명
|
||||||
|
// ruseUtztnFcltCd: "11500PB001R1",
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
subsInfo:[]
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export type DmMapStore = DmMapType & DmMapActions;
|
||||||
|
|
||||||
|
export const DmMapReducer = (set: any): DmMapActions => {
|
||||||
|
return {
|
||||||
|
findRuseFclt: async (sggOrCity: string, searchTxt: string) => {
|
||||||
|
const ruseFcltList = await getRuseFclt("1", sggOrCity, searchTxt);
|
||||||
|
|
||||||
|
set((state: DmMapType) => ({
|
||||||
|
...state,
|
||||||
|
map: {
|
||||||
|
...state.map,
|
||||||
|
ruseFcltList: ruseFcltList.data,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
setSubsInfo: async (subsInfo: SubsInfo[]) => {
|
||||||
|
set((state: DmMapType) => ({
|
||||||
|
...state,
|
||||||
|
map: {
|
||||||
|
...state.map,
|
||||||
|
subsInfo: subsInfo,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
export type FacilityInfo= {
|
||||||
|
bscFctCd: string; // 기본시설 코드
|
||||||
|
bscFctNm: string; // 기본시설명
|
||||||
|
rvbsnCd: string; // 유역 코드
|
||||||
|
rvbsn: string; // 유역명
|
||||||
|
lgbsn: string; // 대권역명
|
||||||
|
mdmbsn: string; // 중권역명
|
||||||
|
rgnCd: string; // 관할청 코드
|
||||||
|
rgnNm: string; // 관할청 명칭
|
||||||
|
adstDstrCd: string; // 행정구역 코드
|
||||||
|
sido: string; // 시도
|
||||||
|
gugun: string; // 구군
|
||||||
|
addr: string; // 주소
|
||||||
|
fctCpc: string; // 시설용량
|
||||||
|
efflWqltStndrdCd: string; // 방류수 수질기준 코드
|
||||||
|
efflWqltStndrdNm: string; // 방류수 수질기준명
|
||||||
|
trtFrfNm: string; // 처리공법명
|
||||||
|
trtDstr: string; // 처리구역
|
||||||
|
rnDt: string; // 사용개시일 (예: YYYYMMDD)
|
||||||
|
cmplDt: string; // 준공일자 (예: YYYYMMDD)
|
||||||
|
trstOprEtpNm: string; // 위탁운영기관명
|
||||||
|
bscFctClsNm: string; // 기본시설 구분명
|
||||||
|
sitArea: string; // 부지면적
|
||||||
|
trtDstrArea: string; // 처리구역 면적
|
||||||
|
swrTrtPopl: string; // 하수처리 인구
|
||||||
|
mnstr: string; // 총괄 유역
|
||||||
|
efflWtrrgn: string; // 방류수역
|
||||||
|
rvNm: string; // 하천명
|
||||||
|
bscFctClsCd: string; // 기본시설 구분 코드
|
||||||
|
rvbsnNm: string; // 유역명
|
||||||
|
tel: string; // 전화번호
|
||||||
|
rswtrYn: string; // 재이용수 여부 ("Y" 또는 "N")
|
||||||
|
ruse: boolean; // 재이용 여부 (boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SubsInfo={
|
||||||
|
ruseUtztnFcltNm: string; // 재이용시설명
|
||||||
|
ruseUtztnFcltCd: string; // 재이용시설 코드
|
||||||
|
}
|
||||||
@@ -38,8 +38,17 @@ export type DemandCompType = {
|
|||||||
reqDt : string | '';
|
reqDt : string | '';
|
||||||
dmdInfoSn : number | '';
|
dmdInfoSn : number | '';
|
||||||
status : string | '';
|
status : string | '';
|
||||||
|
splyCndtList:SupplyCandidate[]|[],
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 공급 후보자 타입 정의
|
||||||
|
export type SupplyCandidate = {
|
||||||
|
ruseUtztnFcltNm: string;
|
||||||
|
ruseUtztnFcltCd: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type DemandMatchings = {
|
export type DemandMatchings = {
|
||||||
splyFcltFk : string | '';
|
splyFcltFk : string | '';
|
||||||
splyFcltFlg : string | '';
|
splyFcltFlg : string | '';
|
||||||
@@ -387,11 +396,9 @@ export const SdmState: SdmType = {
|
|||||||
similarCaseList: similarCaseList,
|
similarCaseList: similarCaseList,
|
||||||
upHeadMoneyDetailList : moneyDetailModalHeader,
|
upHeadMoneyDetailList : moneyDetailModalHeader,
|
||||||
moneyDetailList: moneyDetailList,
|
moneyDetailList: moneyDetailList,
|
||||||
|
|
||||||
// 수요정보 신청현황
|
// 수요정보 신청현황
|
||||||
sdmInfoStatusList: [],
|
sdmInfoStatusList: [],
|
||||||
upHeadSdmInfoStatus: upHeadSdmInfoStatus,
|
upHeadSdmInfoStatus: upHeadSdmInfoStatus,
|
||||||
|
|
||||||
// 로그인관련
|
// 로그인관련
|
||||||
successNextPage : false,
|
successNextPage : false,
|
||||||
successLogin: false,
|
successLogin: false,
|
||||||
|
|||||||
@@ -1,19 +1,27 @@
|
|||||||
import { createStore } from 'zustand/vanilla';
|
import { createStore } from 'zustand/vanilla';
|
||||||
import { CommonReducer, CommonState, type CommonStore, type CommonType } from './common';
|
import { CommonReducer, CommonState, type CommonStore, type CommonType } from './common';
|
||||||
import { SdmReducer, SdmState, type SdmStore, type SdmType } from './sdmMng';
|
import { SdmReducer, SdmState, type SdmStore, type SdmType } from './sdmMng';
|
||||||
|
import { dmMapState, type DmMapType, type DmMapStore, DmMapReducer } from './map';
|
||||||
|
import { tokenManager } from '@/src/entities/auth/lib/token-manager';
|
||||||
|
|
||||||
export type DmStore = CommonStore & SdmStore;
|
export type DmStore = CommonStore & SdmStore &DmMapStore ;
|
||||||
export type DmState = CommonType & SdmType;
|
export type DmState = CommonType & SdmType &DmMapType;
|
||||||
|
|
||||||
export const StoreZus = (initState: DmState= {
|
export const StoreZus = (initState: DmState= {
|
||||||
...CommonState,
|
...CommonState,
|
||||||
...SdmState,
|
...SdmState,
|
||||||
|
...dmMapState,
|
||||||
}) => {
|
}) => {
|
||||||
return createStore<DmStore>()((set) => {
|
return createStore<DmStore>()((set) => {
|
||||||
|
tokenManager.setToken("site-id-key", 'DM');
|
||||||
|
tokenManager.setToken("site-theme-key", 'green');
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...initState,
|
...initState,
|
||||||
...CommonReducer(set),
|
...CommonReducer(set),
|
||||||
...SdmReducer(set)
|
...SdmReducer(set),
|
||||||
|
...DmMapReducer(set)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
type SearchType = {
|
||||||
|
bscFctNm: string;
|
||||||
|
bscFctCd: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type thisProps = {
|
||||||
|
label: string | undefined;
|
||||||
|
initValue: string | undefined;
|
||||||
|
setValue: (searchNm?: string) => void;
|
||||||
|
setRtnData: (rtnData: any) => void;
|
||||||
|
searchList?: SearchType[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SearchInputText({
|
||||||
|
label = undefined,
|
||||||
|
initValue = undefined,
|
||||||
|
setValue = () => {},
|
||||||
|
setRtnData = () => {},
|
||||||
|
searchList = [],
|
||||||
|
}: thisProps) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [textValue, setTextValue] = useState(initValue || "");
|
||||||
|
const [highlightIndex, setHighlightIndex] = useState<number>(-1);
|
||||||
|
|
||||||
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const handleInputChange = useCallback(
|
||||||
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
setTextValue(value);
|
||||||
|
setValue(value);
|
||||||
|
setHighlightIndex(-1);
|
||||||
|
setIsOpen(value !== "");
|
||||||
|
},
|
||||||
|
[setValue]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSuggestionClick = useCallback(
|
||||||
|
(searchNm: string) => {
|
||||||
|
setValue(searchNm);
|
||||||
|
setTextValue(searchNm);
|
||||||
|
setIsOpen(false);
|
||||||
|
setRtnData(searchList?.find((item) => item.bscFctNm === searchNm) || {});
|
||||||
|
},
|
||||||
|
[searchList, setRtnData, setValue]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleKeyDown = useCallback(
|
||||||
|
(e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (!isOpen || searchList.length === 0) return;
|
||||||
|
|
||||||
|
if (e.key === "ArrowDown") {
|
||||||
|
e.preventDefault();
|
||||||
|
setHighlightIndex((prev) => (prev + 1) % searchList.length);
|
||||||
|
} else if (e.key === "ArrowUp") {
|
||||||
|
e.preventDefault();
|
||||||
|
setHighlightIndex((prev) =>
|
||||||
|
prev <= 0 ? searchList.length - 1 : prev - 1
|
||||||
|
);
|
||||||
|
} else if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
if (searchList.length === 1 && searchList[0]) {
|
||||||
|
handleSuggestionClick(searchList[0].bscFctNm);
|
||||||
|
} else if (highlightIndex >= 0 && highlightIndex < searchList.length && searchList[highlightIndex]) {
|
||||||
|
handleSuggestionClick(searchList[highlightIndex].bscFctNm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[highlightIndex, isOpen, searchList, handleSuggestionClick]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (
|
||||||
|
wrapperRef.current &&
|
||||||
|
!wrapperRef.current.contains(event.target as Node)
|
||||||
|
) {
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("mousedown", handleClickOutside);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={wrapperRef} className="relative">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={textValue || ""}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
className="w-full h-[3.2rem] !text-xl pl-3 text-gray-600 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
placeholder={`${label}을(를) 입력하세요`}
|
||||||
|
onClick={() => {
|
||||||
|
setTextValue("");
|
||||||
|
setValue("");
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{isOpen && (
|
||||||
|
<div className="absolute top-full left-0 z-50 mb-2 max-h-72 w-full overflow-auto rounded-md border border-gray-300 bg-white shadow-lg">
|
||||||
|
<div className="max-h-72">
|
||||||
|
{searchList.length > 0 ? (
|
||||||
|
searchList.map((item, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={`w-full cursor-pointer overflow-auto px-5 py-3 text-left !text-xl ${
|
||||||
|
highlightIndex === index
|
||||||
|
? "bg-blue-100"
|
||||||
|
: "hover:bg-gray-100"
|
||||||
|
}`}
|
||||||
|
onClick={() => handleSuggestionClick(item.bscFctNm)}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
|
handleSuggestionClick(item.bscFctNm);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="float-left">{item.bscFctNm}</span>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="!text-xl w-full px-5 py-3 hover:bg-gray-100 cursor-pointer overflow-auto">
|
||||||
|
검색 결과가 없습니다.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
"use client";
|
||||||
|
import { useDmStore } from "@/app/store";
|
||||||
|
import {
|
||||||
|
SearchDiv,
|
||||||
|
SearchFormLeft,
|
||||||
|
SelectBox,
|
||||||
|
type SelectDataType,
|
||||||
|
} from "@common/business_components/ui";
|
||||||
|
import { Button, Label } from "@common/components/ui";
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import SearchInputText from "../SearchInputText/SearchInputText";
|
||||||
|
|
||||||
|
export function SearchBar({
|
||||||
|
setHasuPk,
|
||||||
|
onApply,
|
||||||
|
}: {
|
||||||
|
setHasuPk: (hasuPk: any) => void;
|
||||||
|
onApply: () => void;
|
||||||
|
}) {
|
||||||
|
const { map, Common, initializeCommonData, findRuseFclt } = useDmStore(
|
||||||
|
(state) => state
|
||||||
|
);
|
||||||
|
const [sidoValue, setSidoValue] = useState<string>("");
|
||||||
|
const [sigunValue, setSigunValue] = useState<string>("");
|
||||||
|
const [filteredSigunData, setFilteredSigunData] = useState<SelectDataType[]>([
|
||||||
|
{ text: "전체", val: "0" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initializeCommonData();
|
||||||
|
}, [initializeCommonData]);
|
||||||
|
|
||||||
|
const initializeSigunData = useCallback(
|
||||||
|
(sidoCd: string) => {
|
||||||
|
if (sidoCd === "0") {
|
||||||
|
setSigunValue("0");
|
||||||
|
setFilteredSigunData([{ text: "전체", val: "0" }]);
|
||||||
|
} else {
|
||||||
|
const newFilteredData = [
|
||||||
|
{ text: "전체", val: "0" },
|
||||||
|
...Common.selectSigunData.filter(
|
||||||
|
(item: SelectDataType) => item.val.substring(0, 2) === sidoCd
|
||||||
|
),
|
||||||
|
];
|
||||||
|
setSigunValue("0");
|
||||||
|
setFilteredSigunData(newFilteredData);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[Common.selectSigunData]
|
||||||
|
);
|
||||||
|
const onSelectValue = useCallback(
|
||||||
|
(val: string, type: string) => {
|
||||||
|
switch (type) {
|
||||||
|
case "ctpvCd":
|
||||||
|
initializeSigunData(val);
|
||||||
|
setSidoValue(val);
|
||||||
|
break;
|
||||||
|
case "sggCd":
|
||||||
|
setSigunValue(val);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[initializeSigunData]
|
||||||
|
);
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// console.log("map.ruseFcltList ", map.ruseFcltList);
|
||||||
|
// }, [map.ruseFcltList]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="grid grid-cols-4 gap-4 ">
|
||||||
|
<div className="col-span-4">
|
||||||
|
<SearchDiv>
|
||||||
|
<SearchFormLeft>
|
||||||
|
<div className="col-span-10 flex flex-row">
|
||||||
|
<SelectBox
|
||||||
|
className="col-span-2 w-1/6 h-full text-2xl"
|
||||||
|
label="시도"
|
||||||
|
selectClass={""}
|
||||||
|
selectArray={Common.selectSidoData || []}
|
||||||
|
onSelectValue={(val) =>
|
||||||
|
onSelectValue(val.toString(), "ctpvCd")
|
||||||
|
}
|
||||||
|
value={sidoValue || Common.selectSidoData[0]?.val || ""}
|
||||||
|
/>
|
||||||
|
<SelectBox
|
||||||
|
className="col-span-2 w-1/6 h-full text-2xl ml-10"
|
||||||
|
label="시군구"
|
||||||
|
selectClass={""}
|
||||||
|
selectArray={filteredSigunData || []}
|
||||||
|
onSelectValue={(val) =>
|
||||||
|
onSelectValue(val.toString(), "sggCd")
|
||||||
|
}
|
||||||
|
value={sigunValue}
|
||||||
|
/>
|
||||||
|
<div className="col-span-6 flex flex-row items-center min-w-[27rem] ml-10">
|
||||||
|
<Label className="text-xl justify-self-center mt-2 mx-3">
|
||||||
|
하수재이용시설
|
||||||
|
</Label>
|
||||||
|
<SearchInputText
|
||||||
|
label={"시설명"}
|
||||||
|
initValue={undefined}
|
||||||
|
setValue={(searchNm?: string) => {
|
||||||
|
let sggOrCity = "";
|
||||||
|
if (searchNm) {
|
||||||
|
if (sigunValue === "0" || sigunValue === "") {
|
||||||
|
sggOrCity = sidoValue;
|
||||||
|
} else {
|
||||||
|
sggOrCity = sigunValue;
|
||||||
|
}
|
||||||
|
findRuseFclt(sggOrCity, searchNm);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
setRtnData={(rtnData: any) => {
|
||||||
|
setHasuPk(rtnData);
|
||||||
|
}}
|
||||||
|
searchList={map.ruseFcltList || []}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row space-x-2">
|
||||||
|
<Button
|
||||||
|
color="red"
|
||||||
|
className="mx-2 mt-2"
|
||||||
|
size={"sm"}
|
||||||
|
onClick={() => {}}
|
||||||
|
>
|
||||||
|
후보지선택
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color="sky"
|
||||||
|
className="mt-2"
|
||||||
|
size={"sm"}
|
||||||
|
onClick={() => {
|
||||||
|
onApply();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
적용
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SearchFormLeft>
|
||||||
|
</SearchDiv>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -23,11 +23,11 @@ export function AccessibleFormField<T extends FieldValues>({
|
|||||||
<FormUi label={labelText}>
|
<FormUi label={labelText}>
|
||||||
<div>
|
<div>
|
||||||
{inputComponent}
|
{inputComponent}
|
||||||
{error?.message && Array.from(new Set(error.message.split('\n'))).map((msg, idx) => (
|
{/* {error?.message && Array.from(new Set(error.message.split('\n'))).map((msg, idx) => (
|
||||||
<p key={idx} className="text-red-500 text-sm mt-1" role="alert">
|
<p key={idx} className="text-red-500 text-sm mt-1" role="alert">
|
||||||
{msg}
|
{msg}
|
||||||
</p>
|
</p>
|
||||||
))}
|
))} */}
|
||||||
</div>
|
</div>
|
||||||
</FormUi>
|
</FormUi>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import ModalSimilarPage from "@/app/pages/sdmMng/sdmInfoInsert/modalSimilar/page";
|
import ModalSimilarPage from "@/app/(base-layout)/pages/sdmMng/sdmInfoInsert/modalSimilar/page";
|
||||||
import type { FormData } from "@/app/pages/sdmMng/sdmInfoInsert/page";
|
import type { FormData } from "@/app/(base-layout)/pages/sdmMng/sdmInfoInsert/page";
|
||||||
import { useDmStore } from "@/app/store";
|
import { useDmStore } from "@/app/store";
|
||||||
import {
|
import {
|
||||||
CANDIDATE,
|
CANDIDATE,
|
||||||
@@ -15,11 +15,9 @@ import { Button } from "@common/components/ui";
|
|||||||
import { useSearchParams } from "next/navigation";
|
import { useSearchParams } from "next/navigation";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { AccessibleFormField } from "./AccessibleFormField";
|
import { AccessibleFormField } from "./AccessibleFormField";
|
||||||
import type { ControllerRenderProps, UseFormReturn } from "react-hook-form";
|
import type { ControllerRenderProps, UseFormReturn, FieldError } from "react-hook-form";
|
||||||
import SearchInputText from "../common/searhDemandInput/component";
|
import SearchInputText from "../common/searhDemandInput/component";
|
||||||
import type { DemandCompType } from "@/app/store/sdmMng";
|
import type { DemandCompType } from "@/app/store/sdmMng";
|
||||||
import NewWindowPortal from "@/app/pages/map/popup/windowPotal";
|
|
||||||
import Map from "@/app/pages/map/popup/sdmInfoMap";
|
|
||||||
|
|
||||||
export interface SdmInfoRegFormProps {
|
export interface SdmInfoRegFormProps {
|
||||||
form: UseFormReturn<FormData>;
|
form: UseFormReturn<FormData>;
|
||||||
@@ -47,7 +45,6 @@ export function SdmInfoRegForm({
|
|||||||
const [isSimilarCase, setIsSimilarCase] = useState(false);
|
const [isSimilarCase, setIsSimilarCase] = useState(false);
|
||||||
const [key, setKey] = useState(0);
|
const [key, setKey] = useState(0);
|
||||||
// 후보지 팝업 오픈 상태
|
// 후보지 팝업 오픈 상태
|
||||||
const [isCandidatePopup, setIsCandidatePopup] = useState(false);
|
|
||||||
|
|
||||||
//모달 조회에 필요한 데이터
|
//모달 조회에 필요한 데이터
|
||||||
const [sply, setSply] = useState<number>(0);
|
const [sply, setSply] = useState<number>(0);
|
||||||
@@ -58,6 +55,9 @@ export function SdmInfoRegForm({
|
|||||||
initializeCommonData();
|
initializeCommonData();
|
||||||
}, [initializeCommonData]);
|
}, [initializeCommonData]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log("candidateTitle ", form.getValues("candidateTitle"));
|
||||||
|
}, [form.getValues("candidateTitle")]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (useTypesData && useTypesData.length > 0) {
|
if (useTypesData && useTypesData.length > 0) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -85,8 +85,17 @@ export function SdmInfoRegForm({
|
|||||||
const searchCandidate = (e: React.MouseEvent) => {
|
const searchCandidate = (e: React.MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
console.log(1);
|
const dmdInfoId = JSON.parse(
|
||||||
setIsCandidatePopup(true);
|
sessionStorage.getItem("user") || "{}"
|
||||||
|
)?.dmdInfoId?.toString();
|
||||||
|
|
||||||
|
const url = `/map/popup?dmdInfoId=${encodeURIComponent(dmdInfoId)}`;
|
||||||
|
const width = window.screen.availWidth;
|
||||||
|
const height = window.screen.availHeight;
|
||||||
|
const left = 0;
|
||||||
|
const top = 0;
|
||||||
|
const popupOptions = `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes`;
|
||||||
|
window.open(url, "candidatePopup", popupOptions);
|
||||||
};
|
};
|
||||||
|
|
||||||
const searchSimilarCase = (e: React.MouseEvent) => {
|
const searchSimilarCase = (e: React.MouseEvent) => {
|
||||||
@@ -208,32 +217,24 @@ export function SdmInfoRegForm({
|
|||||||
formIndex: 0,
|
formIndex: 0,
|
||||||
required: true,
|
required: true,
|
||||||
component: (field) => (
|
component: (field) => (
|
||||||
<AccessibleFormField
|
<FormUi
|
||||||
field={field}
|
onCheckboxChange={handleCheckboxChange}
|
||||||
label="희망단가(원/㎥)"
|
isDisabled={isBznmTotNoDisabled}
|
||||||
required={true}
|
>
|
||||||
error={form.formState.errors.money}
|
<Input
|
||||||
inputComponent={
|
placeholder="희망단가(원/㎥)을 입력하세요"
|
||||||
<FormUi
|
type="text"
|
||||||
onCheckboxChange={handleCheckboxChange}
|
className="!text-xl text-gray-600"
|
||||||
isDisabled={isBznmTotNoDisabled}
|
value={field.value as string}
|
||||||
>
|
onChange={(e) => {
|
||||||
<Input
|
field.onChange(e);
|
||||||
placeholder="희망단가(원/㎥)을 입력하세요"
|
setMoney(Number(e.target.value));
|
||||||
type="text"
|
}}
|
||||||
className="!text-xl text-gray-600"
|
onBlur={field.onBlur}
|
||||||
value={field.value as string}
|
name={field.name}
|
||||||
onChange={(e) => {
|
ref={field.ref}
|
||||||
field.onChange(e);
|
/>
|
||||||
setMoney(Number(e.target.value));
|
</FormUi>
|
||||||
}}
|
|
||||||
onBlur={field.onBlur}
|
|
||||||
name={field.name}
|
|
||||||
ref={field.ref}
|
|
||||||
/>
|
|
||||||
</FormUi>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -445,7 +446,6 @@ export function SdmInfoRegForm({
|
|||||||
name: "rmrk",
|
name: "rmrk",
|
||||||
label: "비고",
|
label: "비고",
|
||||||
formIndex: 1,
|
formIndex: 1,
|
||||||
required: true,
|
|
||||||
component: (field) => (
|
component: (field) => (
|
||||||
<Input
|
<Input
|
||||||
placeholder="비고를 입력하세요"
|
placeholder="비고를 입력하세요"
|
||||||
@@ -463,7 +463,7 @@ export function SdmInfoRegForm({
|
|||||||
name: "candidateTitle",
|
name: "candidateTitle",
|
||||||
label: CANDIDATE || "공급처 후보지 선택(필수아님)",
|
label: CANDIDATE || "공급처 후보지 선택(필수아님)",
|
||||||
formIndex: 2,
|
formIndex: 2,
|
||||||
component: (field) => (
|
component: () => (
|
||||||
<div>
|
<div>
|
||||||
<div className="-mt-14 float-right flex gap-10">
|
<div className="-mt-14 float-right flex gap-10">
|
||||||
<Button className={"bg-gray-200"} onClick={searchCandidate}>
|
<Button className={"bg-gray-200"} onClick={searchCandidate}>
|
||||||
@@ -474,36 +474,18 @@ export function SdmInfoRegForm({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-center gap-10 mt-5">
|
<div className="flex justify-center gap-10 mt-5">
|
||||||
<Input
|
{(form.getValues("candidateTitle") || []).map(
|
||||||
placeholder="후보지1"
|
(candidate: any, index: number) => (
|
||||||
type="text"
|
<Input
|
||||||
className="!text-xl w-[130px] text-gray-600"
|
key={index}
|
||||||
value={field.value as string}
|
placeholder={`후보지${index + 1}`}
|
||||||
onChange={field.onChange}
|
type="text"
|
||||||
onBlur={field.onBlur}
|
className="!text-xl w-[130px] text-gray-600"
|
||||||
name={field.name}
|
value={candidate.ruseUtztnFcltNm}
|
||||||
ref={field.ref}
|
readOnly
|
||||||
/>
|
/>
|
||||||
<Input
|
)
|
||||||
placeholder="후보지2"
|
)}
|
||||||
type="text"
|
|
||||||
className="!text-xl w-[130px] text-gray-600"
|
|
||||||
value={field.value as string}
|
|
||||||
onChange={field.onChange}
|
|
||||||
onBlur={field.onBlur}
|
|
||||||
name={field.name}
|
|
||||||
ref={field.ref}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
placeholder="후보지3"
|
|
||||||
type="text"
|
|
||||||
className="!text-xl w-[130px] text-gray-600"
|
|
||||||
value={field.value as string}
|
|
||||||
onChange={field.onChange}
|
|
||||||
onBlur={field.onBlur}
|
|
||||||
name={field.name}
|
|
||||||
ref={field.ref}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
@@ -539,7 +521,11 @@ export function SdmInfoRegForm({
|
|||||||
field={field}
|
field={field}
|
||||||
label={label}
|
label={label}
|
||||||
required={required}
|
required={required}
|
||||||
error={form.formState.errors[name]}
|
error={
|
||||||
|
typeof form.formState.errors[name]?.message === 'string'
|
||||||
|
? (form.formState.errors[name] as FieldError)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
inputComponent={component(field)}
|
inputComponent={component(field)}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -565,7 +551,11 @@ export function SdmInfoRegForm({
|
|||||||
field={field}
|
field={field}
|
||||||
label={label}
|
label={label}
|
||||||
required={required}
|
required={required}
|
||||||
error={form.formState.errors[name]}
|
error={
|
||||||
|
typeof form.formState.errors[name]?.message === 'string'
|
||||||
|
? (form.formState.errors[name] as FieldError)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
inputComponent={component?.(field)}
|
inputComponent={component?.(field)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -590,7 +580,11 @@ export function SdmInfoRegForm({
|
|||||||
field={field}
|
field={field}
|
||||||
label={label}
|
label={label}
|
||||||
required={required}
|
required={required}
|
||||||
error={form.formState.errors[name]}
|
error={
|
||||||
|
typeof form.formState.errors[name]?.message === 'string'
|
||||||
|
? (form.formState.errors[name] as FieldError)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
inputComponent={component?.(field)}
|
inputComponent={component?.(field)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -610,20 +604,6 @@ export function SdmInfoRegForm({
|
|||||||
noFooterYn="Y"
|
noFooterYn="Y"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 후보지 조회 새 창 포털 */}
|
|
||||||
{isCandidatePopup && (
|
|
||||||
<NewWindowPortal
|
|
||||||
onClose={() => setIsCandidatePopup(false)}
|
|
||||||
title="공급처 후보지 조회"
|
|
||||||
width={window.screen.availWidth}
|
|
||||||
height={window.screen.availHeight}
|
|
||||||
resizable="no"
|
|
||||||
scrollbars="yes"
|
|
||||||
>
|
|
||||||
<Map />
|
|
||||||
</NewWindowPortal>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,24 @@ import * as z from "zod";
|
|||||||
|
|
||||||
export function SdmLoginForm() {
|
export function SdmLoginForm() {
|
||||||
const { actionLogin, initializeCommonData } = useDmStore((state) => state);
|
const { actionLogin, initializeCommonData } = useDmStore((state) => state);
|
||||||
const [isBznmTotNoDisabled, setIsBznmTotNoDisabled] = useState(false);
|
const [isBznmTotNoDisabled, setIsBznmTotNoDisabled] = useState(true);
|
||||||
|
|
||||||
|
const formatBusinessNumber = (value: string) => {
|
||||||
|
// 숫자만 추출
|
||||||
|
const digitsOnly = value.replace(/\D/g, "");
|
||||||
|
|
||||||
|
// 10자리까지만 처리
|
||||||
|
const limitedDigits = digitsOnly.slice(0, 10);
|
||||||
|
|
||||||
|
// 3-2-5 형식으로 포맷팅
|
||||||
|
if (limitedDigits.length <= 3) {
|
||||||
|
return limitedDigits;
|
||||||
|
} else if (limitedDigits.length <= 5) {
|
||||||
|
return `${limitedDigits.slice(0, 3)}-${limitedDigits.slice(3)}`;
|
||||||
|
} else {
|
||||||
|
return `${limitedDigits.slice(0, 3)}-${limitedDigits.slice(3, 5)}-${limitedDigits.slice(5)}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 스키마를 useMemo로 최적화
|
// 스키마를 useMemo로 최적화
|
||||||
const sdmLoginFormSchema = useMemo(() => z.object({
|
const sdmLoginFormSchema = useMemo(() => z.object({
|
||||||
@@ -79,7 +96,12 @@ export function SdmLoginForm() {
|
|||||||
placeholder="사업자등록번호를 입력하세요"
|
placeholder="사업자등록번호를 입력하세요"
|
||||||
type="text"
|
type="text"
|
||||||
className="!mt-5 !mb-5 !text-xl text-gray-600"
|
className="!mt-5 !mb-5 !text-xl text-gray-600"
|
||||||
{...field}
|
{...field}
|
||||||
|
value={formatBusinessNumber(field.value as string)}
|
||||||
|
onChange={(e) => {
|
||||||
|
const dmdId = e.target.value.replace(/\D/g, "").slice(0, 10);
|
||||||
|
field.onChange(dmdId);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -27,13 +27,17 @@ export function middleware(req: NextRequest) {
|
|||||||
.map(([key, value]) => `${key}=${decodeURIComponent(value)}`)
|
.map(([key, value]) => `${key}=${decodeURIComponent(value)}`)
|
||||||
.join("&");
|
.join("&");
|
||||||
|
|
||||||
console.log(' sesesesesesevereveraserwereqwrewbrtkehjwrtg server')
|
|
||||||
const newUrl = `${serverApiUrl}${pathname.replace("/server", "")}${
|
const newUrl = `${serverApiUrl}${pathname.replace("/server", "")}${
|
||||||
decodedParams ? `?${decodedParams}` : ""
|
decodedParams ? `?${decodedParams}` : ""
|
||||||
}`;
|
}`;
|
||||||
return NextResponse.rewrite(new URL(newUrl));
|
return NextResponse.rewrite(new URL(newUrl));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pathname === "/") {
|
||||||
|
console.log(" maim 화면이동 ");
|
||||||
|
return NextResponse.redirect(new URL("/pages/wrStatus", req.url));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if (pathname.startsWith("/attach-service")) {
|
if (pathname.startsWith("/attach-service")) {
|
||||||
const requestHeaders = new Headers(req.headers);
|
const requestHeaders = new Headers(req.headers);
|
||||||
@@ -124,5 +128,5 @@ export function middleware(req: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
matcher: ["/server/:path*", "/user-service/:path*", "/attach-service/:path*", "/geoserver/:path*", "/sewerage-service/:path*"],
|
matcher: ["/server/:path*", "/user-service/:path*", "/attach-service/:path*", "/geoserver/:path*", "/sewerage-service/:path*", "/"],
|
||||||
};
|
};
|
||||||
|
|||||||
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
/// <reference types="next" />
|
||||||
|
/// <reference types="next/image-types/global" />
|
||||||
|
|
||||||
|
// NOTE: This file should not be edited
|
||||||
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
@@ -16,7 +16,6 @@ export class LoginApiService implements LoginService {
|
|||||||
|
|
||||||
public async login(payload: ILogin): Promise<void> {
|
public async login(payload: ILogin): Promise<void> {
|
||||||
try {
|
try {
|
||||||
console.log(' ENDPOINT.USER_SERVICE.LOGIN ', ENDPOINT.USER_SERVICE.LOGIN)
|
|
||||||
const response = await fetch( PROXY_HOST+ ENDPOINT.USER_SERVICE.LOGIN, {
|
const response = await fetch( PROXY_HOST+ ENDPOINT.USER_SERVICE.LOGIN, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
|||||||
@@ -19,8 +19,10 @@ export const ENDPOINT = {
|
|||||||
AUTHORIZATIONS: "/user-service/api/v1/authorizations",
|
AUTHORIZATIONS: "/user-service/api/v1/authorizations",
|
||||||
ROLE_AUTHORIZATIONS: "/user-service/api/v1/role-authorizations",
|
ROLE_AUTHORIZATIONS: "/user-service/api/v1/role-authorizations",
|
||||||
ROLES: "/user-service/api/v1/roles",
|
ROLES: "/user-service/api/v1/roles",
|
||||||
|
|
||||||
},
|
},
|
||||||
CMS_SERVICE: {
|
CMS_SERVICE: {
|
||||||
|
DMSITES:'cms-service/api/v1/menu/anonymous/user/DM',
|
||||||
SITES: "/cms-service/api/v1/sites",
|
SITES: "/cms-service/api/v1/sites",
|
||||||
IPS: "/cms-service/api/v1/site-ips",
|
IPS: "/cms-service/api/v1/site-ips",
|
||||||
DOMAINS: "/cms-service/api/v1/site-dmns",
|
DOMAINS: "/cms-service/api/v1/site-dmns",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export const SITE_ID = process.env.NEXT_PUBLIC_SITE_ID || "DM";
|
export const SITE_ID = process.env.NEXT_PUBLIC_SITE_ID || "DM";
|
||||||
export const PORT = process.env.NEXT_PUBLIC_PORT || "3000";
|
export const PORT = process.env.NEXT_PUBLIC_PORT || "3000";
|
||||||
export const PROXY_HOST = process.env.NEXT_PUBLIC_PROXY_HOST || `http://localhost:${PORT}`;
|
export const PROXY_HOST = process.env.NEXT_PUBLIC_PROXY_HOST || `http://localhost:${PORT}`;
|
||||||
export const SERVER_API_URL = process.env.NEXT_PUBLIC_SERVER_API_URL ||'http://con.mind-one.co.kr:11035/api/v1';
|
export const SERVER_API_DM_URL = process.env.NEXT_PUBLIC_DM_SERVER_API_URL ||'http://con.mind-one.co.kr:11035/api/v1';
|
||||||
export const SERVER_API_FO_URL = process.env.NEXT_PUBLIC_FO_SERVER_API_URL||' http://con.mind-one.co.kr:11082/api/v1';
|
export const SERVER_API_FO_URL = process.env.NEXT_PUBLIC_FO_SERVER_API_URL||' http://con.mind-one.co.kr:11082/api/v1';
|
||||||
export const SERVER_API_WR_URL = process.env.NEXT_PUBLIC_WR_SERVER_API_URL||' http://con.mind-one.co.kr:11080/api/v1';
|
export const SERVER_API_WR_URL = process.env.NEXT_PUBLIC_WR_SERVER_API_URL||' http://con.mind-one.co.kr:11080/api/v1';
|
||||||
export const SERVER_API_CS_URL = process.env.NEXT_PUBLIC_CS_SERVER_API_URL||'http://dev.onasys.co.kr:8000';
|
export const SERVER_API_CS_URL = process.env.NEXT_PUBLIC_CS_SERVER_API_URL||'http://dev.onasys.co.kr:8000';
|
||||||
|
|||||||
+63
-60
@@ -3,64 +3,67 @@ import baseConfig from "../../packages/common/components/tailwind.config";
|
|||||||
import type { Config } from "tailwindcss";
|
import type { Config } from "tailwindcss";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
darkMode: ["class"],
|
darkMode: ["class"],
|
||||||
presets: [baseConfig],
|
presets: [baseConfig],
|
||||||
content: [
|
content: [
|
||||||
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
"../../packages/common/components/src/ui/**/*.{js,ts,jsx,tsx,mdx}",
|
"../../packages/common/components/src/ui/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
"../../packages/common/business_components/src/ui/**/*.{js,ts,jsx,tsx,mdx}",
|
"../../packages/common/business_components/src/ui/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
],
|
],
|
||||||
plugins: [require("tailwindcss-animate")],
|
plugins: [require("tailwindcss-animate")],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
borderRadius: {
|
fontFamily: {
|
||||||
lg: 'var(--radius)',
|
NotoSansKR: ['NotoSansKR'],
|
||||||
md: 'calc(var(--radius) - 2px)',
|
},
|
||||||
sm: 'calc(var(--radius) - 4px)'
|
borderRadius: {
|
||||||
},
|
lg: 'var(--radius)',
|
||||||
colors: {
|
md: 'calc(var(--radius) - 2px)',
|
||||||
background: 'hsl(var(--background))',
|
sm: 'calc(var(--radius) - 4px)'
|
||||||
foreground: 'hsl(var(--foreground))',
|
},
|
||||||
card: {
|
colors: {
|
||||||
DEFAULT: 'hsl(var(--card))',
|
background: 'hsl(var(--background))',
|
||||||
foreground: 'hsl(var(--card-foreground))'
|
foreground: 'hsl(var(--foreground))',
|
||||||
},
|
card: {
|
||||||
popover: {
|
DEFAULT: 'hsl(var(--card))',
|
||||||
DEFAULT: 'hsl(var(--popover))',
|
foreground: 'hsl(var(--card-foreground))'
|
||||||
foreground: 'hsl(var(--popover-foreground))'
|
},
|
||||||
},
|
popover: {
|
||||||
primary: {
|
DEFAULT: 'hsl(var(--popover))',
|
||||||
DEFAULT: 'hsl(var(--primary))',
|
foreground: 'hsl(var(--popover-foreground))'
|
||||||
foreground: 'hsl(var(--primary-foreground))'
|
},
|
||||||
},
|
primary: {
|
||||||
secondary: {
|
DEFAULT: 'hsl(var(--primary))',
|
||||||
DEFAULT: 'hsl(var(--secondary))',
|
foreground: 'hsl(var(--primary-foreground))'
|
||||||
foreground: 'hsl(var(--secondary-foreground))'
|
},
|
||||||
},
|
secondary: {
|
||||||
muted: {
|
DEFAULT: 'hsl(var(--secondary))',
|
||||||
DEFAULT: 'hsl(var(--muted))',
|
foreground: 'hsl(var(--secondary-foreground))'
|
||||||
foreground: 'hsl(var(--muted-foreground))'
|
},
|
||||||
},
|
muted: {
|
||||||
accent: {
|
DEFAULT: 'hsl(var(--muted))',
|
||||||
DEFAULT: 'hsl(var(--accent))',
|
foreground: 'hsl(var(--muted-foreground))'
|
||||||
foreground: 'hsl(var(--accent-foreground))'
|
},
|
||||||
},
|
accent: {
|
||||||
destructive: {
|
DEFAULT: 'hsl(var(--accent))',
|
||||||
DEFAULT: 'hsl(var(--destructive))',
|
foreground: 'hsl(var(--accent-foreground))'
|
||||||
foreground: 'hsl(var(--destructive-foreground))'
|
},
|
||||||
},
|
destructive: {
|
||||||
border: 'hsl(var(--border))',
|
DEFAULT: 'hsl(var(--destructive))',
|
||||||
input: 'hsl(var(--input))',
|
foreground: 'hsl(var(--destructive-foreground))'
|
||||||
ring: 'hsl(var(--ring))',
|
},
|
||||||
chart: {
|
border: 'hsl(var(--border))',
|
||||||
'1': 'hsl(var(--chart-1))',
|
input: 'hsl(var(--input))',
|
||||||
'2': 'hsl(var(--chart-2))',
|
ring: 'hsl(var(--ring))',
|
||||||
'3': 'hsl(var(--chart-3))',
|
chart: {
|
||||||
'4': 'hsl(var(--chart-4))',
|
'1': 'hsl(var(--chart-1))',
|
||||||
'5': 'hsl(var(--chart-5))'
|
'2': 'hsl(var(--chart-2))',
|
||||||
}
|
'3': 'hsl(var(--chart-3))',
|
||||||
}
|
'4': 'hsl(var(--chart-4))',
|
||||||
}
|
'5': 'hsl(var(--chart-5))'
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} satisfies Config;
|
} satisfies Config;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user