Compare commits
275
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb9dacd60c | ||
|
|
1952bd5c2c | ||
|
|
765ca59e35 | ||
|
|
fd55d2e3cf | ||
|
|
3a3eae84ae | ||
|
|
a24d9f42d6 | ||
|
|
96ef328fbf | ||
|
|
7ee7a0fc69 | ||
|
|
4c66dacdb0 | ||
|
|
024a69b1fd | ||
|
|
8a2bbfe220 | ||
|
|
d954675824 | ||
|
|
5712bcedaa | ||
|
|
deaf428ed9 | ||
|
|
0c16b9d14d | ||
|
|
41686670a4 | ||
|
|
82019c86bf | ||
|
|
637929ea26 | ||
|
|
c94b33b1b8 | ||
|
|
80156bd349 | ||
|
|
2e7b06d970 | ||
|
|
c23fe68675 | ||
|
|
b5c7f09bc9 | ||
|
|
4cfa032be2 | ||
|
|
886561535e | ||
|
|
83b63901ff | ||
|
|
96624140e0 | ||
|
|
6c6b5e662c | ||
|
|
bbfd7fd201 | ||
|
|
340814439a | ||
|
|
5cc86d81f1 | ||
|
|
a05b0e1cfe | ||
|
|
c89c588487 | ||
|
|
df18a79528 | ||
|
|
18b8a6edab | ||
|
|
fcfaa23dc3 | ||
|
|
5382328f94 | ||
|
|
3bd22d83b7 | ||
|
|
5405af7ff3 | ||
|
|
0abc7eaaac | ||
|
|
0805fbcc26 | ||
|
|
2f553a0dff | ||
|
|
c713b55480 | ||
|
|
6dd48a3054 | ||
|
|
afa129f20b | ||
|
|
6dae81eb05 | ||
|
|
e5f2cb2182 | ||
|
|
0fe84dba22 | ||
|
|
b2e0780654 | ||
|
|
81d589acb0 | ||
|
|
3ba11b5508 | ||
|
|
eafe131df1 | ||
|
|
97b0a2e5bf | ||
|
|
65f74987f8 | ||
|
|
aaed8fe971 | ||
|
|
c222e809b6 | ||
|
|
cba305ab9b | ||
|
|
913256cac4 | ||
|
|
e1fc6f210f | ||
|
|
1bec6a3787 | ||
|
|
3ed203c597 | ||
|
|
12f7fe53ad | ||
|
|
75d14782f4 | ||
|
|
387362d42b | ||
|
|
4e04676a57 | ||
|
|
c30de7f40d | ||
|
|
ca96e55c07 | ||
|
|
64698fb97e | ||
|
|
662a461cfa | ||
|
|
2c5ed5f6cf | ||
|
|
484d90e84f | ||
|
|
f550a62f99 | ||
|
|
9759a5250a | ||
|
|
890e2af79b | ||
|
|
895adeef0a | ||
|
|
2a15138014 | ||
|
|
a2b2d94581 | ||
|
|
1083be517d | ||
|
|
dad8b9925e | ||
|
|
aeb4f5f8ca | ||
|
|
fc54f6253c | ||
|
|
07c7947da1 | ||
|
|
af035aa649 | ||
|
|
35e1203168 | ||
|
|
97727908fa | ||
|
|
ebde6d433a | ||
|
|
556d3f33b6 | ||
|
|
f826f0fb3f | ||
|
|
11010099b4 | ||
|
|
17886b0f9d | ||
|
|
46c166a87c | ||
|
|
5b96245e7d | ||
|
|
81a3e6324e | ||
|
|
6ba78d0923 | ||
|
|
15b6520ee1 | ||
|
|
ed53ad249c | ||
|
|
b67d1261ff | ||
|
|
cfde5d695f | ||
|
|
13ce985fe5 | ||
|
|
3e634fa575 | ||
|
|
16b204f83b | ||
|
|
dfabee3e08 | ||
|
|
16ebb8eb3a | ||
|
|
f8c902a138 | ||
|
|
be5ffb60f6 | ||
|
|
01c01dedbb | ||
|
|
007a39ebfa | ||
|
|
bec3132417 | ||
|
|
8c2bdc65eb | ||
|
|
befb3f1c84 | ||
|
|
87ee8c827d | ||
|
|
e7a46ac3a2 | ||
|
|
bd7debbe3e | ||
|
|
1850ab2aa8 | ||
|
|
0f9d7a115f | ||
|
|
05802896a8 | ||
|
|
a44e8722c6 | ||
|
|
690e671a25 | ||
|
|
46b98b99e2 | ||
|
|
9e339cd451 | ||
|
|
95085cbdeb | ||
|
|
c1b0f32609 | ||
|
|
f9b328278a | ||
|
|
8a5b9ba2c3 | ||
|
|
c976b5d6ee | ||
|
|
ad7a633bc9 | ||
|
|
b91298f3d9 | ||
|
|
763e0ff5d8 | ||
|
|
93e8ec1df3 | ||
|
|
1207754092 | ||
|
|
f087e55c3c | ||
|
|
53b3cc9817 | ||
|
|
b2bbdb8ad4 | ||
|
|
2fc4f9512f | ||
|
|
417a24bb16 | ||
|
|
72c1c01e47 | ||
|
|
aea6924015 | ||
|
|
6fb39d6384 | ||
|
|
4be7354b36 | ||
|
|
55de8cc31e | ||
|
|
ab23e4ba2c | ||
|
|
e688a703ca | ||
|
|
49961db9a9 | ||
|
|
56f8d3700a | ||
|
|
c68e1d017e | ||
|
|
a09e7fc709 | ||
|
|
024f25f003 | ||
|
|
f18eaa8f01 | ||
|
|
9d101666ab | ||
|
|
04bc7d3a3a | ||
|
|
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 | ||
|
|
6bdef28532 | ||
|
|
314e6ed9ad | ||
|
|
99ffa1d6d4 | ||
|
|
d23c9ca4b1 | ||
|
|
cc67d707d9 | ||
|
|
583559ec60 | ||
|
|
e7195f7db0 | ||
|
|
04bb3657bb | ||
|
|
f1563aa1fc | ||
|
|
0795299b48 | ||
|
|
3380addd78 | ||
|
|
2862fdc9fd | ||
|
|
c9e413a3d3 | ||
|
|
6413bc6b18 | ||
|
|
fbb47c2593 | ||
|
|
a446739432 | ||
|
|
ec7655a71d | ||
|
|
c8d2c4e17e | ||
|
|
b513fddbd5 | ||
|
|
c0c7bdfa5b | ||
|
|
55b8e91298 | ||
|
|
ea125605d1 | ||
|
|
8353800dac | ||
|
|
cd08b5dad8 | ||
|
|
8a2ed7df58 | ||
|
|
00d6ff90c6 | ||
|
|
5a43b33a1d | ||
|
|
c5f4f6ddd0 | ||
|
|
f332d64aa6 | ||
|
|
bb4d7b9d56 | ||
|
|
32b16055fb | ||
|
|
1daa925019 | ||
|
|
543fbb70cc | ||
|
|
bcdefaa828 | ||
|
|
0d48a0b579 | ||
|
|
fc3ef2c59e | ||
|
|
accc3e0ef1 | ||
|
|
e30a7d184b |
@@ -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;
|
||||
+27
-1
@@ -11,6 +11,8 @@ import {
|
||||
const serverApiUrl =
|
||||
process.env.NEXT_PUBLIC_SERVER_API_URL ?? "http://localhost:3000";
|
||||
|
||||
const linkBackendApiHost = process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST ?? "http://localhost:8080";
|
||||
|
||||
export function middleware(req: NextRequest) {
|
||||
const pathname = req.nextUrl.pathname;
|
||||
const searchParams = req.nextUrl.searchParams;
|
||||
@@ -52,6 +54,30 @@ export function middleware(req: NextRequest) {
|
||||
});
|
||||
}
|
||||
|
||||
if (pathname.startsWith("/api/link")) {
|
||||
const requestHeaders = new Headers(req.headers);
|
||||
const token = req.cookies.get(CLAIM_NAME)?.value;
|
||||
const userId = req.cookies.get(AUTH_USER_ID)?.value;
|
||||
|
||||
// /api/link 경로를 제거하고 백엔드 API 경로로 변환
|
||||
const apiPath = pathname.replace("/api/link", "");
|
||||
const queryParams = searchParams.toString();
|
||||
const newUrl = `${linkBackendApiHost}${apiPath}${
|
||||
queryParams ? `?${queryParams}` : ""
|
||||
}`;
|
||||
|
||||
if (token) requestHeaders.set(CLAIM_NAME, token);
|
||||
if (userId) requestHeaders.set(AUTH_USER_ID, userId);
|
||||
|
||||
logMessages.push(` Link API Rewritten URL: ${newUrl}`);
|
||||
logMessages.push("\n--------------------------------\n");
|
||||
console.log(logMessages.join("\n"));
|
||||
|
||||
return NextResponse.rewrite(new URL(newUrl), {
|
||||
headers: requestHeaders,
|
||||
});
|
||||
}
|
||||
|
||||
if (pathname === "/") {
|
||||
return NextResponse.redirect(new URL("/dashboard", req.url));
|
||||
}
|
||||
@@ -62,5 +88,5 @@ export function middleware(req: NextRequest) {
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ["/server/:path*", "/attach-service/:path*", "/"],
|
||||
matcher: ["/server/:path*", "/attach-service/:path*", "/api/link/:path*", "/"],
|
||||
};
|
||||
|
||||
@@ -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>(
|
||||
`/api/link/getLinkList`,
|
||||
{ params }
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getLinkInfo(linkSn: string): Promise<LinkInfo> {
|
||||
const response = await axios.get<LinkInfo>(
|
||||
`/api/link/getLinkInfo/${linkSn}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async linkQuickStart(linkSn: string): Promise<string> {
|
||||
const response = await axios.patch<string>(
|
||||
`/api/link/linkQuickStart/${linkSn}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async updateLinkInfo(data: LinkFormData): Promise<void> {
|
||||
await axios.put<void>(
|
||||
'/api/link/updateLinkInfo',
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
public async insertLinkInfo(data: LinkFormData): Promise<void> {
|
||||
await axios.post<void>(
|
||||
'/api/link/insertLinkInfo',
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
public async deleteLink(linkSn: string): Promise<void> {
|
||||
await axios.delete<void>(
|
||||
`/api/link/deleteLink/${linkSn}`,
|
||||
);
|
||||
}
|
||||
|
||||
public async getLinkLogList(params: LinkSearchParam): Promise<LinkLogList> {
|
||||
const response = await axios.get<LinkLogList>(
|
||||
'/api/link/selectLinkLoglist',
|
||||
{ params }
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getLinkStatsList(params: LinkSearchParam): Promise<LinkStatsList> {
|
||||
const response = await axios.get<LinkStatsList>(
|
||||
'/api/link/selectLinkStats',
|
||||
{ params }
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getLinkVrblList(linkSn: string): Promise<LinkVrblList> {
|
||||
const response = await axios.get<LinkVrblList>(
|
||||
`/api/link/getLinkVrblList/${linkSn}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async insertLinkVrbl(data: LinkVrblFormData): Promise<number> {
|
||||
const response = await axios.post<number>(
|
||||
'/api/link/insertLinkVrbl',
|
||||
data,
|
||||
);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async deleteLinkVrbl(data: string[]): Promise<void> {
|
||||
await axios.delete<void>(
|
||||
'/api/link/deleteLinkVrbl',
|
||||
{
|
||||
data
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public async getLinkVrblInfo(linkVrblSn: string): Promise<LinkVrbl> {
|
||||
const response = await axios.get<LinkVrbl>(
|
||||
`/api/link/getLinkVrblInfo/${linkVrblSn}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async updateLinkVrblInfo(data: LinkVrblFormData): Promise<void> {
|
||||
await axios.put<void>(
|
||||
'/api/link/updateLinkVrblInfo',
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
public async getLinkTableInfo(linkSn: string): Promise<LinkTable> {
|
||||
const response = await axios.get<LinkTable>(
|
||||
`/api/link/getLinkTableInfo/${linkSn}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getLinkTableColList(linkSn: string): Promise<LinkTableColumnsList> {
|
||||
const response = await axios.get<LinkTableColumnsList>(
|
||||
`/api/link/getLinkTableColList/${linkSn}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getLinkTableColInfo(params: { colSn: string; tblSn:string; }): Promise<LinkTableColumns> {
|
||||
const response = await axios.get<LinkTableColumns>(
|
||||
'/api/link/getLinkTableColInfo',
|
||||
{ params }
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async insertLinkTableCol(data: LinkTableColData): Promise<void> {
|
||||
await axios.post<void>(
|
||||
'/api/link/insertLinkTableCol',
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
public async deleteLinkTableCol(data: string[]): Promise<void> {
|
||||
await axios.delete<void>(
|
||||
'/api/link/deleteLinkTableCol',
|
||||
{
|
||||
data
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public async updateLinkTableCol(data: LinkTableColData): Promise<void> {
|
||||
axios.put<void>(
|
||||
'/api/link/updateLinkTableCol',
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
public async insertLinkTableInfo(data: LinkTableFormData): Promise<void> {
|
||||
await axios.post<void>(
|
||||
'/api/link/insertLinkTableInfo',
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
public async updateLinkTableInfo(data: LinkTableFormData): Promise<void> {
|
||||
await axios.put<void>(
|
||||
'/api/link/updateLinkTableInfo',
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
public async countLinkTblNm(tblNm: string): Promise<{count: number, tbl_sn: string}> {
|
||||
const response = await axios.get<{count: number, tbl_sn: string}>(
|
||||
`/api/link/countLinkTblNm/${tblNm}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async updateLinkResultCode(linkSn: string): Promise<void> {
|
||||
await axios.patch<void>(
|
||||
`/api/link/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 [userSearchQuery, setUserSearchQuery] = useState<UserSearchParams>({
|
||||
keywordType: "",
|
||||
keywordType: "userName",
|
||||
keyword: "",
|
||||
});
|
||||
const [selectedGroupId, setSelectedGroupId] = useState("");
|
||||
@@ -193,6 +193,7 @@ function AddAdminDialog() {
|
||||
|
||||
<form onSubmit={handleSearch} className="flex items-center gap-2">
|
||||
<Select
|
||||
value={userSearchQuery.keywordType}
|
||||
onValueChange={(value) =>
|
||||
setUserSearchQuery({ ...userSearchQuery, keywordType: value })
|
||||
}
|
||||
@@ -201,8 +202,8 @@ function AddAdminDialog() {
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="userId">아이디</SelectItem>
|
||||
<SelectItem value="userName">이름</SelectItem>
|
||||
<SelectItem value="userId">아이디</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input
|
||||
|
||||
@@ -7,7 +7,6 @@ import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
InputDatePicker,
|
||||
Label,
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -15,10 +14,11 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@common/components";
|
||||
import { columns } from "@/pages/apilog/model/table-columns";
|
||||
import { ApiLogApi, type ApiLogParams } from "@/entities/apilog";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { columns } from "@/pages/apilog/model/table-columns";
|
||||
import { InputDateRangePicker } from "@/shared/ui/input-date-range-picker";
|
||||
|
||||
type DateRange = {
|
||||
from?: Date | undefined;
|
||||
@@ -133,6 +133,11 @@ function ApiLogPage() {
|
||||
setDateRange(presetDateRange || defaultDateRange);
|
||||
};
|
||||
|
||||
const handleDateRangeChange = (dateRange: DateRange) => {
|
||||
setDateRange(dateRange);
|
||||
setPresetDateRange("");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex-1">
|
||||
<div className="card card-border">
|
||||
@@ -148,20 +153,9 @@ function ApiLogPage() {
|
||||
<SelectItem value="3M">3개월</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<InputDatePicker
|
||||
value={dateRange.from}
|
||||
onChange={(date) => {
|
||||
setDateRange({ ...dateRange, from: date });
|
||||
setPresetDateRange("");
|
||||
}}
|
||||
/>
|
||||
~
|
||||
<InputDatePicker
|
||||
value={dateRange.to}
|
||||
onChange={(date) => {
|
||||
setDateRange({ ...dateRange, to: date });
|
||||
setPresetDateRange("");
|
||||
}}
|
||||
<InputDateRangePicker
|
||||
dateRange={dateRange}
|
||||
setDateRange={handleDateRangeChange}
|
||||
/>
|
||||
<Label className="ml-4 mr-2 text-label">사용자ID</Label>
|
||||
<Input
|
||||
|
||||
@@ -110,10 +110,16 @@ function DashboardPage() {
|
||||
<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
|
||||
? data.memberTotCnt.toLocaleString()
|
||||
: "-"}
|
||||
</div>
|
||||
<p className="mt-1 text-2xl opacity-80">
|
||||
금일 신규 가입자 {data?.memberTodayCnt ?? "-"}명
|
||||
금일 신규 가입자
|
||||
{data?.memberTodayCnt
|
||||
? data.memberTodayCnt.toLocaleString()
|
||||
: "0"}
|
||||
명
|
||||
</p>
|
||||
</div>
|
||||
{/* 차트 영역 */}
|
||||
@@ -131,10 +137,12 @@ function DashboardPage() {
|
||||
<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 ? data.pstTotCnt.toLocaleString() : "-"}
|
||||
</div>
|
||||
<p className="mt-1 text-2xl opacity-80">
|
||||
금일 +{data?.pstTodayCnt ?? "-"} 등록
|
||||
금일 +
|
||||
{data?.pstTodayCnt ? data.pstTodayCnt.toLocaleString() : "0"}
|
||||
등록
|
||||
</p>
|
||||
</div>
|
||||
{/* 차트 영역 */}
|
||||
@@ -152,10 +160,16 @@ function DashboardPage() {
|
||||
<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
|
||||
? data.accessTodayCnt.toLocaleString()
|
||||
: "-"}
|
||||
</div>
|
||||
<p className="mt-1 text-2xl opacity-80">
|
||||
전일 대비 +{data?.accessBefDayPer ?? "-"}%
|
||||
전일 대비 +
|
||||
{data?.accessBefDayPer
|
||||
? data.accessBefDayPer.toLocaleString()
|
||||
: "0"}
|
||||
%
|
||||
</p>
|
||||
</div>
|
||||
{/* 차트 영역 */}
|
||||
@@ -173,7 +187,7 @@ function DashboardPage() {
|
||||
<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 ? data.apiTodayCnt.toLocaleString() : "-"}
|
||||
</div>
|
||||
<p className="mt-1 text-2xl opacity-80">금일 누적</p>
|
||||
</div>
|
||||
@@ -221,7 +235,10 @@ function DashboardPage() {
|
||||
</div>
|
||||
<div className="mt-5">
|
||||
<div className="text-5xl font-bold">
|
||||
{data?.waitMemberCnt}건
|
||||
{data?.waitMemberCnt
|
||||
? data.waitMemberCnt.toLocaleString()
|
||||
: "0"}
|
||||
건
|
||||
</div>
|
||||
<div className="mt-2 text-2xl text-gray-500">
|
||||
신규 가입 승인 대기
|
||||
@@ -253,7 +270,9 @@ function DashboardPage() {
|
||||
<span className="text-2xl font-medium">메뉴 승인</span>
|
||||
</div>
|
||||
<div className="mt-5">
|
||||
<div className="text-5xl font-bold">3건</div>
|
||||
<div className="text-5xl font-bold">
|
||||
{(3).toLocaleString()}건
|
||||
</div>
|
||||
<div className="mt-2 text-2xl text-gray-500">
|
||||
매뉴 수정 승인 대기
|
||||
</div>
|
||||
@@ -283,7 +302,10 @@ function DashboardPage() {
|
||||
</div>
|
||||
<div className="mt-5">
|
||||
<div className="text-5xl font-bold">
|
||||
{data?.waitCntnCnt}건
|
||||
{data?.waitCntnCnt
|
||||
? data.waitCntnCnt.toLocaleString()
|
||||
: "0"}
|
||||
건
|
||||
</div>
|
||||
<div className="mt-2 text-2xl text-gray-500">
|
||||
컨텐츠 수정 승인 대기
|
||||
@@ -303,19 +325,19 @@ function DashboardPage() {
|
||||
<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분 전</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분 전</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분 전</span>
|
||||
<span>{(10).toLocaleString()}분 전</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-medium">컨텐츠 승인 요청</span>
|
||||
<span>15분 전</span>
|
||||
<span>{(15).toLocaleString()}분 전</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -328,20 +350,22 @@ function DashboardPage() {
|
||||
<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월 정기 시스템 점검</span>
|
||||
<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일 후</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일 후</span>
|
||||
<span>{(5).toLocaleString()}일 후</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-medium">정기 보안 점검</span>
|
||||
<span>7일 후</span>
|
||||
<span>{(7).toLocaleString()}일 후</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -161,11 +161,11 @@ function GroupForm({ form }: GroupFormProps) {
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="mngrY" />
|
||||
<Label htmlFor="mngrY">Y</Label>
|
||||
<Label htmlFor="mngrY">예</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="mngrN" />
|
||||
<Label htmlFor="mngrN">N</Label>
|
||||
<Label htmlFor="mngrN">아니오</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { memo, useState } from "react";
|
||||
import { memo, useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
@@ -43,7 +43,7 @@ const GroupsDialog = memo(
|
||||
const { data: group } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.GROUPS, groupId],
|
||||
queryFn: () => GroupApi.findGroupById(groupId!),
|
||||
enabled: !!groupId && open, // open 시점 groupId가 있을 때만 상세 조회 API 호출
|
||||
enabled: !!groupId && open, // open 시점 groupId가 있을 때만 상세 조회 API 호출
|
||||
});
|
||||
|
||||
const { mutateAsync: createGroup } = useMutationWrapper({
|
||||
@@ -77,7 +77,7 @@ const GroupsDialog = memo(
|
||||
delYn: group?.delYn ?? "",
|
||||
authrtId: group?.authrtId ?? "",
|
||||
siteId: group?.siteId ?? "",
|
||||
mngrYn: group?.mngrYn ?? "",
|
||||
mngrYn: group?.mngrYn ?? "Y",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -91,6 +91,10 @@ const GroupsDialog = memo(
|
||||
if (await confirm(confirmMessage)) callback(data);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (open) form.reset();
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen} {...props}>
|
||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
|
||||
@@ -23,6 +23,7 @@ function CreatePage() {
|
||||
resolver: zodResolver(imageFormSchema),
|
||||
defaultValues: {
|
||||
atflYn: "Y",
|
||||
useYn: "Y",
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -124,6 +124,7 @@ function ImageForm({ form, imgSn, ...props }: ImageFormProps) {
|
||||
{...field}
|
||||
type="number"
|
||||
min={0}
|
||||
max={9999}
|
||||
value={field.value ?? ""}
|
||||
onChange={(e) =>
|
||||
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,406 @@
|
||||
'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: ['api/link/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.refetchQueries({
|
||||
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: ['/api/link/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 };
|
||||
@@ -7,7 +7,6 @@ import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
InputDatePicker,
|
||||
Label,
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -16,10 +15,11 @@ import {
|
||||
SelectValue,
|
||||
LoadingWithImage,
|
||||
} from "@common/components";
|
||||
import { columns } from "@/pages/loginlog/model/table-columns";
|
||||
import { LoginlogApi, type LoginlogParams } from "@/entities/loginlog";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { columns } from "@/pages/loginlog/model/table-columns";
|
||||
import { InputDateRangePicker } from "@/shared/ui/input-date-range-picker";
|
||||
|
||||
type DateRange = {
|
||||
from?: Date | undefined;
|
||||
@@ -141,6 +141,11 @@ function LoginlogPage() {
|
||||
setDateRange(presetDateRange || defaultDateRange);
|
||||
};
|
||||
|
||||
const handleDateRangeChange = (dateRange: DateRange) => {
|
||||
setDateRange(dateRange);
|
||||
setPresetDateRange("");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex-1">
|
||||
<div className="card card-border">
|
||||
@@ -156,20 +161,9 @@ function LoginlogPage() {
|
||||
<SelectItem value="3M">3개월</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<InputDatePicker
|
||||
value={dateRange.from}
|
||||
onChange={(date) => {
|
||||
setDateRange({ ...dateRange, from: date });
|
||||
setPresetDateRange("");
|
||||
}}
|
||||
/>
|
||||
~
|
||||
<InputDatePicker
|
||||
value={dateRange.to}
|
||||
onChange={(date) => {
|
||||
setDateRange({ ...dateRange, to: date });
|
||||
setPresetDateRange("");
|
||||
}}
|
||||
<InputDateRangePicker
|
||||
dateRange={dateRange}
|
||||
setDateRange={handleDateRangeChange}
|
||||
/>
|
||||
<Label className="ml-4 mr-2 text-label">사용자ID</Label>
|
||||
<Input
|
||||
|
||||
@@ -7,7 +7,6 @@ import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
InputDatePicker,
|
||||
Label,
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -15,13 +14,14 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@common/components";
|
||||
import { columns } from "@/pages/privacylog/model/table-columns";
|
||||
import {
|
||||
PrivacyLogPrivacy,
|
||||
type PrivacyLogParams,
|
||||
} from "@/entities/privacylog";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { columns } from "@/pages/privacylog/model/table-columns";
|
||||
import { InputDateRangePicker } from "@/shared/ui/input-date-range-picker";
|
||||
|
||||
type DateRange = {
|
||||
from?: Date | undefined;
|
||||
@@ -132,6 +132,11 @@ function PrivacyLogPage() {
|
||||
setDateRange(presetDateRange || defaultDateRange);
|
||||
};
|
||||
|
||||
const handleDateRangeChange = (dateRange: DateRange) => {
|
||||
setDateRange(dateRange);
|
||||
setPresetDateRange("");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex-1">
|
||||
<div className="card card-border">
|
||||
@@ -147,20 +152,9 @@ function PrivacyLogPage() {
|
||||
<SelectItem value="3M">3개월</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<InputDatePicker
|
||||
value={dateRange.from}
|
||||
onChange={(date) => {
|
||||
setDateRange({ ...dateRange, from: date });
|
||||
setPresetDateRange("");
|
||||
}}
|
||||
/>
|
||||
~
|
||||
<InputDatePicker
|
||||
value={dateRange.to}
|
||||
onChange={(date) => {
|
||||
setDateRange({ ...dateRange, to: date });
|
||||
setPresetDateRange("");
|
||||
}}
|
||||
<InputDateRangePicker
|
||||
dateRange={dateRange}
|
||||
setDateRange={handleDateRangeChange}
|
||||
/>
|
||||
<Label className="ml-4 mr-2 text-label">조회정보</Label>
|
||||
<Input
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
InputDatePicker,
|
||||
Label,
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -31,8 +30,9 @@ import {
|
||||
} from "@/entities/post";
|
||||
import { BoardApi } from "@/entities/board";
|
||||
import { AuthorityButton } from "@/entities/menu-authority";
|
||||
import { useAlertStore, useSelectPrompt, useDataTable } from "@/shared/lib";
|
||||
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 { MovePostDialog } from "./move-post-dialog";
|
||||
|
||||
@@ -58,7 +58,7 @@ function RecentPage() {
|
||||
});
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
const [presetDateRange, setPresetDateRange] = useState("");
|
||||
const [dateRange, setDateRange] = useState<DateRange | undefined>({
|
||||
const [dateRange, setDateRange] = useState<DateRange>({
|
||||
from: undefined,
|
||||
to: undefined,
|
||||
});
|
||||
@@ -169,7 +169,7 @@ function RecentPage() {
|
||||
};
|
||||
|
||||
const handlePresetDateRange = (value: string) => {
|
||||
let presetDateRange: DateRange | undefined;
|
||||
let presetDateRange: DateRange;
|
||||
switch (value) {
|
||||
case "1W":
|
||||
presetDateRange = {
|
||||
@@ -190,12 +190,20 @@ function RecentPage() {
|
||||
};
|
||||
break;
|
||||
default:
|
||||
presetDateRange = undefined;
|
||||
presetDateRange = {
|
||||
from: undefined,
|
||||
to: undefined,
|
||||
};
|
||||
}
|
||||
setPresetDateRange(value);
|
||||
setDateRange(presetDateRange);
|
||||
};
|
||||
|
||||
const handleDateRangeChange = (dateRange: DateRange) => {
|
||||
setDateRange(dateRange);
|
||||
setPresetDateRange("");
|
||||
};
|
||||
|
||||
const onSoftDeletePost = async () => {
|
||||
const { resolved, value: delRsn } =
|
||||
await prompt("삭제 사유를 입력해주세요.");
|
||||
@@ -260,20 +268,9 @@ function RecentPage() {
|
||||
<SelectItem value="3M">3개월</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<InputDatePicker
|
||||
value={dateRange?.from}
|
||||
onChange={(date) => {
|
||||
setDateRange({ ...dateRange, from: date });
|
||||
setPresetDateRange("");
|
||||
}}
|
||||
/>
|
||||
~
|
||||
<InputDatePicker
|
||||
value={dateRange?.to}
|
||||
onChange={(date) => {
|
||||
setDateRange({ ...dateRange, to: date });
|
||||
setPresetDateRange("");
|
||||
}}
|
||||
<InputDateRangePicker
|
||||
dateRange={dateRange}
|
||||
setDateRange={handleDateRangeChange}
|
||||
/>
|
||||
<Label className="ml-4 mr-2 text-label">게시판</Label>
|
||||
<Select
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import {
|
||||
type ConnectionLogParams,
|
||||
type ConnectionLogList,
|
||||
type ConnectionStatisticsParams,
|
||||
type ConnectionStatistics,
|
||||
type ConnectionLogService,
|
||||
} from "../model/connection-log-interface";
|
||||
|
||||
class ConnectionLogApiService implements ConnectionLogService {
|
||||
async getConnectionLog(
|
||||
params: ConnectionLogParams,
|
||||
): Promise<ConnectionLogList> {
|
||||
const response = await client.get<ConnectionLogList>(
|
||||
`${ENDPOINT.CMS_SERVICE.CONNECTION_LOG}`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async getConnectionStatistics(
|
||||
params: ConnectionStatisticsParams,
|
||||
): Promise<ConnectionStatistics> {
|
||||
const response = await client.get<ConnectionStatistics>(
|
||||
`${ENDPOINT.CMS_SERVICE.CONNECTION_LOG}/stat`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const ConnectionLogApi = new ConnectionLogApiService();
|
||||
@@ -0,0 +1 @@
|
||||
export { UserStatisticsPage } from "./ui/user-statistics-page";
|
||||
@@ -0,0 +1,103 @@
|
||||
import type { ChartData, ChartOptions } from "chart.js";
|
||||
import { getCssVariableValue } from "@common/components";
|
||||
import type { ConnectionStatistics } from "./connection-log-interface";
|
||||
|
||||
export const chartData = {
|
||||
day: (
|
||||
data: ConnectionStatistics,
|
||||
): {
|
||||
data: ChartData<"line">;
|
||||
options: ChartOptions<"line">;
|
||||
} => {
|
||||
return {
|
||||
data: {
|
||||
labels: data.dayList.map((item) => item.dayYmd),
|
||||
datasets: [
|
||||
{
|
||||
type: "line",
|
||||
label: "접속자 수",
|
||||
data: data.dayList.map((item) => item.accessCnt),
|
||||
borderColor: getCssVariableValue("--chart-1"),
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
y: {
|
||||
ticks: {
|
||||
precision: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
site: (
|
||||
data: ConnectionStatistics,
|
||||
): {
|
||||
data: ChartData<"bar">;
|
||||
options: ChartOptions<"bar">;
|
||||
} => {
|
||||
return {
|
||||
data: {
|
||||
labels: data.siteList.map((item) => item.siteNm),
|
||||
datasets: [
|
||||
{
|
||||
type: "bar",
|
||||
label: "사이트별 접속자",
|
||||
data: data.siteList.map((item) => item.accessCnt),
|
||||
backgroundColor: data.siteList.map((_, index) =>
|
||||
getCssVariableValue(`--chart-${(index % 5) + 1}`),
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
indexAxis: "y",
|
||||
scales: {
|
||||
x: {
|
||||
ticks: {
|
||||
precision: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
browser: (
|
||||
data: ConnectionStatistics,
|
||||
): {
|
||||
data: ChartData<"doughnut">;
|
||||
options: ChartOptions<"doughnut">;
|
||||
} => {
|
||||
return {
|
||||
data: {
|
||||
labels: data.browserList.map((item) => item.wbsrKndNm),
|
||||
datasets: [
|
||||
{
|
||||
type: "doughnut",
|
||||
data: data.browserList.map((item) => item.accessCnt),
|
||||
backgroundColor: data.browserList.map((_, index) =>
|
||||
getCssVariableValue(`--chart-${(index % 5) + 1}`),
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: "bottom",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
import type { PageableList, PageableParams } from "@/shared/model";
|
||||
|
||||
export interface ConnectionLogParams extends PageableParams {
|
||||
keywordType?: string;
|
||||
keyword?: string;
|
||||
siteId?: string;
|
||||
cntnIp?: string;
|
||||
cntnRgn?: string;
|
||||
wbsrKnd?: string;
|
||||
urlPath?: string;
|
||||
cntnDtStart?: string;
|
||||
cntnDtEnd?: string;
|
||||
}
|
||||
|
||||
export interface ConnectionLog {
|
||||
cntnSn: number;
|
||||
cntnDt: string;
|
||||
cntnId: string;
|
||||
cntnIp: string;
|
||||
cntnRgn: string;
|
||||
wbsrKnd: string;
|
||||
menuCd: number;
|
||||
siteId: string;
|
||||
urlPath: string;
|
||||
siteNm: string;
|
||||
menuNm: string;
|
||||
wbsrKndNm: string;
|
||||
}
|
||||
|
||||
export interface ConnectionStatisticsParams {
|
||||
keywordType?: string;
|
||||
keyword?: string;
|
||||
siteId?: string;
|
||||
cntnIp?: string;
|
||||
cntnRgn?: string;
|
||||
wbsrKnd?: string;
|
||||
urlPath?: string;
|
||||
cntnDtStart?: string;
|
||||
cntnDtEnd?: string;
|
||||
}
|
||||
|
||||
interface DayConnectionStatistics {
|
||||
dayYmd: string;
|
||||
accessCnt: number;
|
||||
}
|
||||
|
||||
interface SiteConnectionStatistics {
|
||||
siteId: string;
|
||||
siteNm: string;
|
||||
accessCnt: number;
|
||||
}
|
||||
|
||||
interface BrowserConnectionStatistics {
|
||||
wbsrKnd: string;
|
||||
wbsrKndNm: string;
|
||||
accessCnt: number;
|
||||
}
|
||||
|
||||
export interface ConnectionStatistics {
|
||||
dayList: DayConnectionStatistics[];
|
||||
siteList: SiteConnectionStatistics[];
|
||||
browserList: BrowserConnectionStatistics[];
|
||||
}
|
||||
|
||||
export type ConnectionLogList = PageableList<ConnectionLog>;
|
||||
|
||||
export interface ConnectionLogService {
|
||||
getConnectionLog(params: ConnectionLogParams): Promise<ConnectionLogList>;
|
||||
getConnectionStatistics(
|
||||
params: ConnectionStatisticsParams,
|
||||
): Promise<ConnectionStatistics>;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import type { ConnectionLog } from "./connection-log-interface";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
export const columns: ColumnDef<ConnectionLog>[] = [
|
||||
{
|
||||
header: "No",
|
||||
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: "cntnDt",
|
||||
header: "접속일시",
|
||||
cell: ({ row }) => {
|
||||
return dayjs(row.original.cntnDt).format("YYYY-MM-DD HH:mm:ss");
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "cntnIp",
|
||||
header: "접속IP",
|
||||
},
|
||||
{
|
||||
accessorKey: "cntnRgn",
|
||||
header: "접속지역",
|
||||
},
|
||||
{
|
||||
accessorKey: "urlPath",
|
||||
header: "URL경로",
|
||||
},
|
||||
{
|
||||
accessorKey: "wbsrKndNm",
|
||||
header: "브라우저",
|
||||
},
|
||||
{
|
||||
accessorKey: "siteNm",
|
||||
header: "사이트",
|
||||
},
|
||||
{
|
||||
accessorKey: "menuNm",
|
||||
header: "메뉴명",
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,310 @@
|
||||
"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,
|
||||
} from "chart.js";
|
||||
import { Bar, Doughnut, Line } 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 { ENDPOINT } from "@/shared/config";
|
||||
import { NoData } from "@/shared/ui/no-data";
|
||||
import { InputDateRangePicker } from "@/shared/ui/input-date-range-picker";
|
||||
import { ConnectionLogApi } from "../api/connection-log-service";
|
||||
import { chartData } from "../model/chart-data";
|
||||
import { columns } from "../model/table-columns";
|
||||
import type { ConnectionStatisticsParams } from "../model/connection-log-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 [siteId, setSiteId] = useState("");
|
||||
const [presetDateRange, setPresetDateRange] = useState("1W");
|
||||
const [dateRange, setDateRange] = useState<DateRange>(defaultDateRange);
|
||||
const [searchParams, setSearchParams] = useState<ConnectionStatisticsParams>({
|
||||
keywordType: "",
|
||||
keyword: "",
|
||||
siteId: "",
|
||||
cntnDtStart: dayjs(defaultDateRange.from).format("YYYY-MM-DD"),
|
||||
cntnDtEnd: dayjs(defaultDateRange.to).format("YYYY-MM-DD"),
|
||||
});
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.CONNECTION_LOG, searchParams],
|
||||
queryFn: () =>
|
||||
ConnectionLogApi.getConnectionLog({
|
||||
...searchParams,
|
||||
}),
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
const { data: connectionStatisticsData } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.CONNECTION_LOG, searchParams],
|
||||
queryFn: () =>
|
||||
ConnectionLogApi.getConnectionStatistics({
|
||||
...searchParams,
|
||||
}),
|
||||
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();
|
||||
const params: ConnectionStatisticsParams = {
|
||||
keywordType,
|
||||
keyword,
|
||||
siteId,
|
||||
cntnDtStart: dateRange.from
|
||||
? dayjs(dateRange.from).format("YYYY-MM-DD")
|
||||
: "",
|
||||
cntnDtEnd: dateRange.to ? dayjs(dateRange.to).format("YYYY-MM-DD") : "",
|
||||
};
|
||||
setSearchParams(params);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setKeywordType("");
|
||||
setKeyword("");
|
||||
setSiteId("");
|
||||
setDateRange(defaultDateRange);
|
||||
setPresetDateRange("1W");
|
||||
setSearchParams({
|
||||
keywordType: "",
|
||||
keyword: "",
|
||||
siteId: "",
|
||||
cntnDtStart: dayjs(defaultDateRange.from).format("YYYY-MM-DD"),
|
||||
cntnDtEnd: dayjs(defaultDateRange.to).format("YYYY-MM-DD"),
|
||||
});
|
||||
};
|
||||
|
||||
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
|
||||
className="w-[16rem]"
|
||||
value={siteId}
|
||||
onValueChange={(value) => setSiteId(value)}
|
||||
/>
|
||||
<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="cntnIp">접속IP</SelectItem>
|
||||
<SelectItem value="cntnRgn">접속지역</SelectItem>
|
||||
<SelectItem value="wbsrKnd">브라우저</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-2xl font-medium">일별 접속 현황</h3>
|
||||
</div>
|
||||
<div className="h-96 p-4">
|
||||
{connectionStatisticsData?.dayList?.length ? (
|
||||
<Line {...chartData.day(connectionStatisticsData)} />
|
||||
) : (
|
||||
<NoData />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2">
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium">사이트별 접속자</h3>
|
||||
</div>
|
||||
<div className="h-96 p-4">
|
||||
{connectionStatisticsData?.siteList?.length ? (
|
||||
<Bar {...chartData.site(connectionStatisticsData)} />
|
||||
) : (
|
||||
<NoData />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium">브라우저별 접속자</h3>
|
||||
</div>
|
||||
<div className="h-96 p-4">
|
||||
{connectionStatisticsData?.browserList?.length ? (
|
||||
<Doughnut {...chartData.browser(connectionStatisticsData)} />
|
||||
) : (
|
||||
<NoData />
|
||||
)}
|
||||
</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 };
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Input,
|
||||
InputDatePicker,
|
||||
Label,
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -25,6 +24,7 @@ import {
|
||||
} from "@/entities/user";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import { InputDateRangePicker } from "@/shared/ui/input-date-range-picker";
|
||||
import { columns } from "../model/table-columns";
|
||||
|
||||
type DateRange = {
|
||||
@@ -40,7 +40,7 @@ function UsersPage() {
|
||||
pageSize: 10,
|
||||
});
|
||||
const [presetDateRange, setPresetDateRange] = useState("");
|
||||
const [dateRange, setDateRange] = useState<DateRange | undefined>({
|
||||
const [dateRange, setDateRange] = useState<DateRange>({
|
||||
from: undefined,
|
||||
to: undefined,
|
||||
});
|
||||
@@ -62,7 +62,7 @@ function UsersPage() {
|
||||
});
|
||||
|
||||
const { data: userSttsStats } = useQuery({
|
||||
queryKey: [ENDPOINT.USER_SERVICE.USERS],
|
||||
queryKey: [ENDPOINT.USER_SERVICE.USERS, "userStatusStatistics"],
|
||||
queryFn: () => UserApi.selectUserSttsStats(),
|
||||
});
|
||||
|
||||
@@ -145,7 +145,7 @@ function UsersPage() {
|
||||
};
|
||||
|
||||
const handlePresetDateRange = (value: string) => {
|
||||
let presetDateRange: DateRange | undefined;
|
||||
let presetDateRange: DateRange;
|
||||
switch (value) {
|
||||
case "1M":
|
||||
presetDateRange = {
|
||||
@@ -166,15 +166,24 @@ function UsersPage() {
|
||||
};
|
||||
break;
|
||||
default:
|
||||
presetDateRange = undefined;
|
||||
presetDateRange = {
|
||||
from: undefined,
|
||||
to: undefined,
|
||||
};
|
||||
}
|
||||
setPresetDateRange(value);
|
||||
setDateRange(presetDateRange);
|
||||
};
|
||||
|
||||
const getUserSttsStatsCnt = (code: string): number => {
|
||||
if (!Array.isArray(userSttsStats)) return 0;
|
||||
return userSttsStats.find((item) => item.sttsCd === code)?.cnt ?? 0;
|
||||
const handleDateRangeChange = (dateRange: DateRange) => {
|
||||
setDateRange(dateRange);
|
||||
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) => {
|
||||
@@ -292,20 +301,9 @@ function UsersPage() {
|
||||
<SelectItem value="6M">6개월</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<InputDatePicker
|
||||
value={dateRange?.from}
|
||||
onChange={(date) => {
|
||||
setDateRange({ ...dateRange, from: date });
|
||||
setPresetDateRange("");
|
||||
}}
|
||||
/>
|
||||
~
|
||||
<InputDatePicker
|
||||
value={dateRange?.to}
|
||||
onChange={(date) => {
|
||||
setDateRange({ ...dateRange, to: date });
|
||||
setPresetDateRange("");
|
||||
}}
|
||||
<InputDateRangePicker
|
||||
dateRange={dateRange}
|
||||
setDateRange={handleDateRangeChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -55,6 +55,7 @@ export const ENDPOINT = {
|
||||
MANAGER_IP: "/cms-service/api/v1/mgrip",
|
||||
APILOG: "/cms-service/api/v1/api-logs",
|
||||
SITE_AUTHORITY: "/cms-service/api/v1/site-authrt",
|
||||
CONNECTION_LOG: "/cms-service/api/v1/cntnlog",
|
||||
},
|
||||
BOARD_SERVICE: {
|
||||
BBS: "/board-service/api/v1/bbs",
|
||||
|
||||
@@ -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 };
|
||||
@@ -25,6 +25,7 @@ function PostAction({ children, handleDelete }: PostActionProps) {
|
||||
const { prompt } = useSelectPrompt(selectData);
|
||||
const bbsInfo = useMenuStore((state) => state.bbsInfo);
|
||||
const userInfo = useUserStore((state) => state.userInfo);
|
||||
const postPageLink = `/bbs/${bbsInfo?.bbsTypeCd.toLowerCase()}/${bbsInfo?.bbsId}`;
|
||||
|
||||
const onDelete = async () => {
|
||||
const { resolved, value } = await prompt("삭제 사유를 입력해주세요.");
|
||||
@@ -64,7 +65,7 @@ function PostAction({ children, handleDelete }: PostActionProps) {
|
||||
size="lg"
|
||||
color="white"
|
||||
type="button"
|
||||
onClick={() => router.back()}
|
||||
onClick={() => router.push(postPageLink)}
|
||||
>
|
||||
목록
|
||||
</Button>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
NEXT_PUBLIC_SITE_ID = DM
|
||||
NEXT_PUBLIC_PORT = 3000
|
||||
NEXT_PUBLIC_PROXY_HOST = http://localhost:3000
|
||||
NEXT_PUBLIC_SERVER_API_URL = http://con.mind-one.co.kr:11080/api/v1
|
||||
NEXT_PUBLIC_PORT = 11035
|
||||
NEXT_PUBLIC_PROXY_HOST = http://con.mind-one.co.kr:11035
|
||||
NEXT_PUBLIC_SERVER_API_URL = http://dev.onasys.co.kr:8000
|
||||
NEXT_PUBLIC_DM_SERVER_API_URL = http://con.mind-one.co.kr:11080/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_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_ACCESS_TOKEN = access-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://192.168.1.128:8883/sewerage-service/api # 최용성
|
||||
#NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.66:8883/sewerage-service/api # 박이정
|
||||
|
||||
+7
-3
@@ -3,13 +3,17 @@ NEXT_PUBLIC_CLAIM_NAME = Authorization
|
||||
NEXT_PUBLIC_AUTH_USER_ID = token-id
|
||||
NEXT_PUBLIC_ACCESS_TOKEN = access-token
|
||||
NEXT_PUBLIC_REFRESH_TOKEN = refresh-token
|
||||
NEXT_PUBLIC_PORT = 11035
|
||||
NEXT_PUBLIC_PORT = 3005
|
||||
#NEXT_PUBLIC_PROXY_HOST = http://con.mind-one.co.kr:11035
|
||||
NEXT_PUBLIC_PROXY_HOST = http://localhost:3000/
|
||||
NEXT_PUBLIC_SERVER_API_URL = http://con.mind-one.co.kr:11080/api/v1
|
||||
NEXT_PUBLIC_PROXY_HOST = http://localhost:3005/
|
||||
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_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_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://192.168.1.128: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_ACCESS_TOKEN = access-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
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
+30
-19
@@ -7,8 +7,9 @@ import ListDiv from '@common/business_components/ui/list_div';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Toaster } from 'react-hot-toast';
|
||||
import SdmLogin from '../sdmLogin/page';
|
||||
import type { DemandCompType } from '@/app/store/sdmMng';
|
||||
import type { DemandCompBizType } from '@/app/store/sdmMng';
|
||||
import TransValueFormat from '@/app/common/transValueFormat';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
const TABLE_STYLES = {
|
||||
header: "text-center border border-[var(--border)] bg-[rgb(234,243,251)] text-[rgb(100,119,132)] whitespace-pre-line",
|
||||
@@ -22,13 +23,16 @@ type SdmApplyProps = {
|
||||
}
|
||||
|
||||
export default function SdmApplyList({}: SdmApplyProps) {
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const {getSdmInfoStatusList, Sdm, Common} = useDmStore((state) => state);
|
||||
const {successLogin, sdmInfoStatusList, upHeadSdmInfoStatus} = Sdm;
|
||||
const {selectSidoData, selectSigunData} = Common;
|
||||
const [isLogin, setIsLogin] = useState(false);
|
||||
const [isLoginStorageInfo, setIsLoginStorageInfo] = useState(false);
|
||||
const [sdmApplyDetail, setSdmApplyDetail] = useState<DemandCompType>({} as DemandCompType);
|
||||
const [transformedList, setTransformedList] = useState<DemandCompType[]>([]);
|
||||
const [sdmApplyDetail, setSdmApplyDetail] = useState<DemandCompBizType>({} as DemandCompBizType);
|
||||
const [transformedList, setTransformedList] = useState<DemandCompBizType[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('sessionStorage : ',sessionStorage.getItem('user'));
|
||||
@@ -56,10 +60,10 @@ export default function SdmApplyList({}: SdmApplyProps) {
|
||||
// 데이터 변환을 위한 별도의 useEffect
|
||||
useEffect(() => {
|
||||
if (sdmInfoStatusList.length > 0) {
|
||||
const newTransformedList = sdmInfoStatusList.map((item: DemandCompType) => ({
|
||||
const newTransformedList = sdmInfoStatusList.map((item: DemandCompBizType) => ({
|
||||
...item,
|
||||
ctpvCd: selectSidoData.find(sido => sido.val === item.ctpvCd?.trim())?.text || '',
|
||||
sggCd: selectSigunData.find(sigun => sigun.val === item.sggCd?.trim())?.text || ''
|
||||
sggCd: selectSigunData.find(sigun => sigun.val === item.sggCd?.trim())?.text || '',
|
||||
}));
|
||||
setTransformedList(newTransformedList);
|
||||
}
|
||||
@@ -69,6 +73,8 @@ export default function SdmApplyList({}: SdmApplyProps) {
|
||||
if (index !== undefined) {
|
||||
const selectedStatus = transformedList[index];
|
||||
if (selectedStatus) {
|
||||
|
||||
|
||||
setSdmApplyDetail(selectedStatus);
|
||||
}
|
||||
}
|
||||
@@ -117,33 +123,33 @@ export default function SdmApplyList({}: SdmApplyProps) {
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>총대장균군수(개/100ml)</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.grssClfg} format="number"/>}</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.grssClfg ?? ''} format="number"/>}</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>결합잔류염소(mg/L)</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.combResdch} format="number"/>}</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.combResdch ?? ''} format="number"/>}</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>탁도(NTU)</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.trbt} format="number"/>}</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.trbt ?? ''} format="number"/>}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>생물학적 산소요구량(BOD)(mg/L)</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.blgcOxygDmndQnt} format="number"/>}</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.blgcOxygDmndQnt ?? ''} format="number"/>}</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>냄새</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.smll} format="number"/>}</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>색도(도)</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.crmty} format="number"/>}</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.crmty ?? ''} format="number"/>}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>총질소(T-N)(mg/L)</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.tn} format="number"/>}</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.tn ?? ''} format="number"/>}</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>총인(T-P)(mg/L)</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.tph} format="number"/>}</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.tph ?? ''} format="number"/>}</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>수소이온농도(pH)</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.ph} format="number"/>}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>염화물(mgCL/L)</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.clIonQnt} format="number"/>}</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.clIonQnt ?? ''} format="number"/>}</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>전기전도도(㎲/cm)</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.cdtnv} format="number"/>}</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.cdtnv ?? ''} format="number"/>}</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>비고</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{sdmApplyDetail?.rmrk}</TableCell>
|
||||
</TableRow>
|
||||
@@ -152,11 +158,11 @@ export default function SdmApplyList({}: SdmApplyProps) {
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>후보지1</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}></TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{ sdmApplyDetail?.splyCndtList?.[0]?.ruseUtztnFcltNm ?? ""}</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>후보지2</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}></TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{sdmApplyDetail?.splyCndtList?.[1]?.ruseUtztnFcltNm ?? ""}</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>후보지3</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}></TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{sdmApplyDetail?.splyCndtList?.[2]?.ruseUtztnFcltNm ?? ""}</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
@@ -173,7 +179,7 @@ export default function SdmApplyList({}: SdmApplyProps) {
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>진행상태</TableCell>
|
||||
<TableCell colSpan={2} className={TABLE_STYLES.cell}>{sdmApplyDetail?.status?.toString()}</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>공급처매칭결과</TableCell>
|
||||
<TableCell colSpan={2} className={TABLE_STYLES.cell}></TableCell>
|
||||
<TableCell colSpan={2} className={TABLE_STYLES.cell}>{sdmApplyDetail?.ruseUtztnFcltNm??''}</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
@@ -185,7 +191,12 @@ export default function SdmApplyList({}: SdmApplyProps) {
|
||||
body={<SdmLogin/>}
|
||||
bodyClass="sm:max-w-[90vw] w-[530px] h-[430px]"
|
||||
open={!isLogin}
|
||||
onOpenChange={(open) => !open && setIsLogin(false)}
|
||||
onOpenChange={(open) => {
|
||||
if(!open){
|
||||
setIsLogin(false)
|
||||
router.push('/');
|
||||
}
|
||||
}}
|
||||
noHeaderYn="Y"
|
||||
noFooterYn="Y"
|
||||
/>
|
||||
@@ -0,0 +1,312 @@
|
||||
"use client";
|
||||
|
||||
import { useDmStore } from "@/app/store";
|
||||
import type { DemandCompBizType } from "@/app/store/sdmMng";
|
||||
import { SdmInfoRegForm } from "@/components/ui/sdmInfoInsertForm/component";
|
||||
import { UiModal } from "@common/business_components/ui/dialog_ui";
|
||||
import { Button } from "@common/components/ui";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { Suspense, useEffect, useState } from "react";
|
||||
import { FormProvider, useForm, type SubmitHandler } from "react-hook-form";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
import * as z from "zod";
|
||||
import SdmLogin from "../sdmLogin/page";
|
||||
|
||||
const sdmInfoFormSchema = z.object({
|
||||
sdmNm: z.string().min(1, "수요처를 선택해주세요"),
|
||||
demandYmd: z.date().optional(),
|
||||
demandQnty: z
|
||||
.string()
|
||||
.min(1, "요구수량을 입력해주세요")
|
||||
.regex(/^[0-9]+$/, "요구수량은 숫자만 입력 가능합니다"),
|
||||
usePurpose: z.string().optional(),
|
||||
money: z.string().regex(/^[0-9]+$/, "희망단가는 숫자만 입력 가능합니다"),
|
||||
/**요구수질 */
|
||||
reqWaterQlty: z.string().optional(),
|
||||
grssClfg: z.number().nullish(),
|
||||
tn: z.number().nullish(),
|
||||
combResdch: z.number().nullish(),
|
||||
tph: z.number().nullish(),
|
||||
trbt: z.number().nullish(),
|
||||
ph: z.string().nullish(),
|
||||
bod: z.number().nullish(),
|
||||
/**컬럼확인필요 */
|
||||
clIonQnt: z.number().nullish(),
|
||||
smll: z.string(),
|
||||
cdtnv: z.number().nullish(),
|
||||
crmty: z.number().nullish(),
|
||||
rmrk: z.string().optional(),
|
||||
/* telno: z.string()
|
||||
.min(8, "전화번호는 8자 이상 입력해주세요")
|
||||
.max(12, "전화번호는 12자 이내로 입력해주세요")
|
||||
.regex(/^[0-9-]+$/, "전화번호는 숫자와 하이픈(-)만 입력 가능합니다"), */
|
||||
candidateTitle: z
|
||||
.array(
|
||||
z.object({
|
||||
ruseUtztnFcltNm: z.string(),
|
||||
ruseUtztnFcltCd: z.string(),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export type FormData = z.infer<typeof sdmInfoFormSchema>;
|
||||
|
||||
function SdmInfoInsertContent() {
|
||||
const searchParams = useSearchParams();
|
||||
const { insertSdmInfoData, Sdm, setSubsInfo } = useDmStore((state) => state);
|
||||
const { successLogin } = Sdm;
|
||||
const [ismoneyDisabled, setIsmoneyDisabled] = useState(false);
|
||||
const formSchema = sdmInfoFormSchema;
|
||||
|
||||
const [isLogin, setIsLogin] = useState(false);
|
||||
const [facilityName, setFacilityName] = useState<string>("");
|
||||
const [defaultValues, setDefaultValues] = useState<FormData>({
|
||||
sdmNm: "",
|
||||
demandYmd: new Date(),
|
||||
demandQnty: "",
|
||||
usePurpose: "",
|
||||
money: "",
|
||||
/**요구수질 */
|
||||
reqWaterQlty: "",
|
||||
grssClfg: null,
|
||||
tn: null,
|
||||
combResdch: null,
|
||||
tph: null,
|
||||
trbt: null,
|
||||
ph: null,
|
||||
bod: null,
|
||||
/**컬럼확인필요 */
|
||||
clIonQnt: null,
|
||||
smll: "",
|
||||
cdtnv: null,
|
||||
crmty: null,
|
||||
rmrk: "",
|
||||
/** 후보지 제목 */
|
||||
candidateTitle: [
|
||||
{ ruseUtztnFcltNm: "", ruseUtztnFcltCd: "" },
|
||||
{ ruseUtztnFcltNm: "", ruseUtztnFcltCd: "" },
|
||||
{ ruseUtztnFcltNm: "", ruseUtztnFcltCd: "" },
|
||||
],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const user = sessionStorage.getItem("user");
|
||||
setDefaultValues((prev) => ({
|
||||
...prev,
|
||||
sdmNm: searchParams.get("dmdCoNm") || "",
|
||||
}));
|
||||
sessionStorage.setItem("user", searchParams.get("dmdInfoId") || "");
|
||||
setIsLogin(user || searchParams.get("dmdInfoId") ? true : false);
|
||||
}, [searchParams]);
|
||||
|
||||
useEffect(() => {
|
||||
const local = localStorage.getItem("user");
|
||||
if (local) {
|
||||
setIsLogin(true);
|
||||
}
|
||||
setSubsInfo([]); // subsInfo 초기화 후보지 처음 화면 들어올때 초기화(스토어내 객체만)
|
||||
}, [setSubsInfo]);
|
||||
|
||||
const form = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues,
|
||||
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(() => {
|
||||
if (successLogin) {
|
||||
setIsLogin(true);
|
||||
const user = sessionStorage.getItem("user");
|
||||
if (user) {
|
||||
try {
|
||||
const userData = JSON.parse(user);
|
||||
if (userData.dmdCoNm) {
|
||||
setFacilityName(userData.dmdCoNm);
|
||||
form.setValue("sdmNm", userData.dmdCoNm);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error parsing user data:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [successLogin, form]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log("Form state changed:", form.formState.errors);
|
||||
}, [form.formState.errors]);
|
||||
|
||||
// 체크박스 상태가 변경될 때마다 money 필드의 에러를 초기화
|
||||
useEffect(() => {
|
||||
if (ismoneyDisabled) {
|
||||
form.clearErrors("money");
|
||||
form.setValue("money", "");
|
||||
}
|
||||
}, [ismoneyDisabled, form]);
|
||||
const router = useRouter();
|
||||
const onSubmit: SubmitHandler<FormData> = async (data) => {
|
||||
try {
|
||||
const dmSubsInfo = data.candidateTitle?.filter(
|
||||
(item) => item.ruseUtztnFcltNm && item.ruseUtztnFcltCd
|
||||
);
|
||||
// Transform form data to madata.candidateTitletch API schema
|
||||
const apiData: DemandCompBizType = {
|
||||
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") || ""),
|
||||
dmdCoNm:
|
||||
JSON.parse(
|
||||
sessionStorage.getItem("user") || "{}"
|
||||
)?.dmdCoNm?.toString() || data.sdmNm,
|
||||
ctpvCd:
|
||||
JSON.parse(
|
||||
sessionStorage.getItem("user") || "{}"
|
||||
)?.ctpvCd?.toString() ||
|
||||
searchParams.get("ctpvCd") ||
|
||||
"",
|
||||
sggCd:
|
||||
JSON.parse(
|
||||
sessionStorage.getItem("user") || "{}"
|
||||
)?.sggCd?.toString() ||
|
||||
searchParams.get("sggCd") ||
|
||||
"",
|
||||
daddr:
|
||||
JSON.parse(
|
||||
sessionStorage.getItem("user") || "{}"
|
||||
)?.daddr?.toString() ||
|
||||
searchParams.get("daddr") ||
|
||||
"",
|
||||
telno:
|
||||
JSON.parse(
|
||||
sessionStorage.getItem("user") || "{}"
|
||||
)?.telno?.toString() ||
|
||||
searchParams.get("telno") ||
|
||||
"",
|
||||
rgtr:
|
||||
JSON.parse(
|
||||
sessionStorage.getItem("user") || "{}"
|
||||
)?.rgtr?.toString() ||
|
||||
searchParams.get("rgtr") ||
|
||||
"",
|
||||
useUsgCd: data.usePurpose || "",
|
||||
wntUntprc: Number(data.money) || "",
|
||||
dmndTmng: data.demandYmd || new Date(),
|
||||
dmndQty: Number(data.demandQnty) || "",
|
||||
grssClfg: data.grssClfg === null ? -1 : Number(data.grssClfg),
|
||||
tn: Number(data.tn) || null,
|
||||
combResdch: Number(data.combResdch) || null,
|
||||
tph: Number(data.tph) || null,
|
||||
trbt: Number(data.trbt) || null,
|
||||
ph: data.ph || "",
|
||||
blgcOxygDmndQnt: Number(data.bod) ||null,
|
||||
clIonQnt: Number(data.clIonQnt) || null,
|
||||
smll: data.smll || "",
|
||||
cdtnv: Number(data.cdtnv) ||null,
|
||||
crmty: Number(data.crmty) || null,
|
||||
rmrk: data.rmrk || "",
|
||||
bzmnNo: "",
|
||||
dmdAplyPswd: "",
|
||||
index: 0,
|
||||
reqDt: "",
|
||||
dmdInfoSn: 0,
|
||||
status: "",
|
||||
splyCndtList: dmSubsInfo || [],
|
||||
};
|
||||
|
||||
await insertSdmInfoData(apiData);
|
||||
setTimeout(() => {
|
||||
router.push("/");
|
||||
}, 1000); // 0.8초 후 이동
|
||||
} catch (error) {
|
||||
console.error("Error submitting form:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F2F2F2]">
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-4xl font-bold text-gray-900 underline">
|
||||
수요정보 등록
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className="max-w-3xl mx-auto bg-white rounded-lg shadow-md p-6">
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||
<SdmInfoRegForm
|
||||
form={form}
|
||||
onCheckboxChange={setIsmoneyDisabled}
|
||||
facilityName={facilityName}
|
||||
/>
|
||||
<div className="flex justify-center pt-4">
|
||||
<Button
|
||||
color="sky"
|
||||
size={"sm"}
|
||||
type="submit"
|
||||
className="w-40 font-semibold py-2 px-4 rounded-md "
|
||||
>
|
||||
수요정보 등록
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isLogin ? (
|
||||
<UiModal
|
||||
body={<SdmLogin />}
|
||||
bodyClass="sm:max-w-[90vw] w-[530px] h-[430px]"
|
||||
open={!isLogin}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
router.push("/");
|
||||
}
|
||||
}}
|
||||
noHeaderYn="Y"
|
||||
noFooterYn="Y"
|
||||
/>
|
||||
) : null}
|
||||
<Toaster position="top-center" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function MyForm() {
|
||||
return (
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<SdmInfoInsertContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
+16
-20
@@ -1,14 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { useDmStore } from "@/app/store";
|
||||
import type { DemandCompType } from "@/app/store/sdmMng";
|
||||
import SdmInsertForm from '@common/business_components/ui/sdmInsertForm';
|
||||
import type { DemandCompType } from "@common/business_components/ui/sdmInsertForm/form";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
|
||||
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 { demandCompList, successNextPage } = Sdm;
|
||||
const router = useRouter();
|
||||
@@ -33,10 +33,10 @@ export default function MyForm() {
|
||||
router.push(`/pages/sdmMng/sdmInfoInsert?${queryParams.toString()}`);
|
||||
}
|
||||
}, [successNextPage, router]);
|
||||
|
||||
|
||||
const callback = useCallback((searchText: string, pageCnt: number) => {
|
||||
searchText = searchText.trim();
|
||||
if(searchText.length > 0) {
|
||||
if (searchText.length > 0) {
|
||||
getDemandCompList(searchText, pageCnt);
|
||||
}
|
||||
}, [getDemandCompList]);
|
||||
@@ -44,27 +44,23 @@ export default function MyForm() {
|
||||
const handleInsertSdmData = useCallback((data: import('@common/business_components/ui/sdmInsertForm/form').DemandCompType) => {
|
||||
const convertedData: DemandCompType = {
|
||||
...data,
|
||||
dmdInfoSn: '',
|
||||
dmdInfoFk: '',
|
||||
status: '',
|
||||
};
|
||||
formDataRef.current = convertedData;
|
||||
insertSdmData(convertedData).catch(error => {
|
||||
console.error('Error inserting SDM data:', error);
|
||||
});
|
||||
insertSdmData(convertedData)
|
||||
}, [insertSdmData]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<SdmInsertForm
|
||||
selectSidoData={selectSidoData}
|
||||
selectSigunData={selectSigunData}
|
||||
dataList={demandCompList}
|
||||
rtnValue={callback}
|
||||
rtnInputData={handleInsertSdmData}
|
||||
/>
|
||||
<Toaster position="top-center" />
|
||||
<div className="min-h-screen bg-[#F2F2F2]">
|
||||
<div className="pt-12">
|
||||
<SdmInsertForm
|
||||
selectSidoData={selectSidoData}
|
||||
selectSigunData={selectSigunData}
|
||||
dataList={demandCompList}
|
||||
rtnValue={callback}
|
||||
rtnInputData={handleInsertSdmData}
|
||||
/>
|
||||
<Toaster position="top-center" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+5
-3
@@ -53,14 +53,16 @@ export default function WrAboutPage() {
|
||||
}, observerOptions);
|
||||
|
||||
// 각 카드 요소를 관찰
|
||||
cardRefs.current.forEach((card) => {
|
||||
const cardsSnapshot = [...cardRefs.current];
|
||||
|
||||
cardsSnapshot.forEach((card) => {
|
||||
if (card) {
|
||||
observer.observe(card);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return () => {
|
||||
cardRefs.current.forEach((card) => {
|
||||
cardsSnapshot.forEach((card) => {
|
||||
if (card) {
|
||||
observer.unobserve(card);
|
||||
}
|
||||
+40
-40
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { CHART_FORM, MainChart, type ChartConfig } from '@common/business_components';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import React, { useEffect, useRef, useState, useMemo, useCallback } from 'react';
|
||||
|
||||
interface RegionData {
|
||||
name: string;
|
||||
@@ -12,10 +12,10 @@ interface RegionData {
|
||||
|
||||
interface TableData {
|
||||
region: string;
|
||||
sewage: number;
|
||||
rainwater: number;
|
||||
reclaimed: number;
|
||||
thermal: number;
|
||||
sewage: string;
|
||||
rainwater: string;
|
||||
reclaimed: string;
|
||||
thermal: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -161,7 +161,7 @@ const WrStatusPage: React.FC = () => {
|
||||
chartTitle : '',
|
||||
chartClassName : "h-[300px] w-full",
|
||||
graphSize : 40,
|
||||
yFieldText : '개소수(건)',
|
||||
yFieldText : '시설(개소)',
|
||||
showLegend : true
|
||||
}
|
||||
|
||||
@@ -193,27 +193,27 @@ const WrStatusPage: React.FC = () => {
|
||||
};
|
||||
|
||||
const tableData: TableData[] = [
|
||||
{ region: '서울특별시', sewage: 4, rainwater: 816, reclaimed: 371, thermal: 0 },
|
||||
{ region: '부산광역시', sewage: 13, rainwater: 68, reclaimed: 20, thermal: 1 },
|
||||
{ region: '대구광역시', sewage: 9, rainwater: 294, reclaimed: 14, thermal: 2 },
|
||||
{ region: '인천광역시', sewage: 17, rainwater: 165, reclaimed: 35, thermal: 15 },
|
||||
{ region: '광주광역시', sewage: 3, rainwater: 74, reclaimed: 15, thermal: 15 },
|
||||
{ region: '대전광역시', sewage: 2, rainwater: 52, reclaimed: 11, thermal: 20 },
|
||||
{ region: '울산광역시', sewage: 8, rainwater: 111, reclaimed: 19, thermal: 30 },
|
||||
{ region: '세종특별자치시', sewage: 9, rainwater: 65, reclaimed: 0, thermal: 0 },
|
||||
{ region: '경기도', sewage: 176, rainwater: 1009, reclaimed: 252, thermal: 40 },
|
||||
{ region: '강원특별자치도', sewage: 69, rainwater: 69, reclaimed: 22, thermal: 0 },
|
||||
{ region: '충청북도', sewage: 38, rainwater: 17, reclaimed: 91, thermal: 0 },
|
||||
{ region: '충청남도', sewage: 67, rainwater: 54, reclaimed: 188, thermal: 0 },
|
||||
{ region: '전라북도', sewage: 55, rainwater: 33, reclaimed: 220, thermal: 0 },
|
||||
{ region: '전라남도', sewage: 83, rainwater: 16, reclaimed: 75, thermal: 0 },
|
||||
{ region: '경상북도', sewage: 81, rainwater: 37, reclaimed: 70, thermal: 0 },
|
||||
{ region: '경상남도', sewage: 75, rainwater: 27, reclaimed: 140, thermal: 0 },
|
||||
{ region: '제주특별자치도', sewage: 8, rainwater: 47, reclaimed: 64, thermal: 0 },
|
||||
|
||||
{ region: '전국', sewage: '717(1,137,538)', rainwater: '3544(13,945)', reclaimed: '1018(222,476)', thermal: '123(492)' },
|
||||
{ region: '서울특별시', sewage: '4(114,908)', rainwater: '816(430)', reclaimed: '371(341)', thermal: '0' },
|
||||
{ region: '부산광역시', sewage: '13(146,197)', rainwater: '68(335)', reclaimed: '20(512)', thermal: '1(18)' },
|
||||
{ region: '대구광역시', sewage: '9(102,380)', rainwater: '294(133)', reclaimed: '14(44)', thermal: '2(12)' },
|
||||
{ region: '인천광역시', sewage: '17(57,691)', rainwater: '165(642)', reclaimed: '35(7,859)', thermal: '15(32)' },
|
||||
{ region: '광주광역시', sewage: '3(28,738)', rainwater: '74(23)', reclaimed: '15(450)', thermal: '15(6)' },
|
||||
{ region: '대전광역시', sewage: '2(11,966)', rainwater: '52(2)', reclaimed: '11(492)', thermal: '20(100)' },
|
||||
{ region: '울산광역시', sewage: '8(36,076)', rainwater: '111(39)', reclaimed: '19(28,904)', thermal: '30(130)' },
|
||||
{ region: '세종특별자치시', sewage: '9(2,555)', rainwater: '65(16)', reclaimed: '0(0)', thermal: '0' },
|
||||
{ region: '경기도', sewage: '176(313,298)', rainwater: '1009(4,735)', reclaimed: '252(77,096)', thermal: '40(143)' },
|
||||
{ region: '강원특별자치도', sewage: '69(19,677)', rainwater: '69(4)', reclaimed: '22(447)', thermal: '0' },
|
||||
{ region: '충청북도', sewage: '38(15,607)', rainwater: '17(91)', reclaimed: '91(2,211)', thermal: '0' },
|
||||
{ region: '충청남도', sewage: '67(41,368)', rainwater: '54(77)', reclaimed: '188(50,434)', thermal: '0' },
|
||||
{ region: '전라북도', sewage: '55(42,671)', rainwater: '33(199)', reclaimed: '220(9,453)', thermal: '0' },
|
||||
{ region: '전라남도', sewage: '83(23,560)', rainwater: '16(885)', reclaimed: '75(15,412)', thermal: '0' },
|
||||
{ region: '경상북도', sewage: '81(129,081)', rainwater: '37(341)', reclaimed: '70(27,818)', 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[] = useMemo(() => [
|
||||
{ name: '서울', x: 170, y: 140, values: [0.3, 0.3, 0.2, 0.2] },
|
||||
{ name: '인천', x: 120, y: 150, values: [0.3, 0.3, 0.2, 0.2] },
|
||||
{ name: '경기', x: 210, y: 210, values: [0.3, 0.3, 0.2, 0.2] },
|
||||
@@ -231,7 +231,7 @@ const WrStatusPage: React.FC = () => {
|
||||
{ name: '울산', x: 450, y: 500, values: [0.3, 0.3, 0.2, 0.2] },
|
||||
{ name: '부산', x: 420, y: 550, values: [0.3, 0.3, 0.2, 0.2] },
|
||||
{ name: '제주', x: 60, y: 750, values: [0.3, 0.3, 0.2, 0.2] },
|
||||
];
|
||||
], []);
|
||||
|
||||
const handleMarkerClick = (regionId: string) => {
|
||||
if (selectedRegion === regionId) {
|
||||
@@ -242,7 +242,7 @@ const WrStatusPage: React.FC = () => {
|
||||
};
|
||||
|
||||
|
||||
const createDonutMarker = (svg: SVGSVGElement, x: number, y: number, name: string, values: number[]) => {
|
||||
const createDonutMarker = useCallback((svg: SVGSVGElement, x: number, y: number, name: string, values: number[]) => {
|
||||
const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#FED16A'];
|
||||
const radius = 26;
|
||||
const innerRadius = 18;
|
||||
@@ -334,7 +334,7 @@ const WrStatusPage: React.FC = () => {
|
||||
});
|
||||
|
||||
svg.appendChild(g);
|
||||
};
|
||||
}, [handleMarkerClick, regionIdMap]);
|
||||
|
||||
useEffect(() => {
|
||||
const loadSVG = async () => {
|
||||
@@ -374,7 +374,7 @@ const WrStatusPage: React.FC = () => {
|
||||
};
|
||||
|
||||
loadSVG();
|
||||
}, []);
|
||||
}, [createDonutMarker, regionData]);
|
||||
|
||||
useEffect(() => {
|
||||
const svg = svgContainerRef.current?.querySelector('svg');
|
||||
@@ -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="border-b border-[#DBE2E6] px-4 py-3">
|
||||
<h3 className="text-[12px] font-semibold">재이용시설개소수(2023)</h3>
|
||||
<p className="text-[12px] text-[#999] font-medium">행정구역별 재이용시설 개소수 조회</p>
|
||||
<h3 className="text-[12px] font-semibold">재이용시설현황(2023)</h3>
|
||||
<p className="text-[12px] text-[#999] font-medium">행정구역별 재이용시설현황(개소)</p>
|
||||
</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="flex items-center space-x-2">
|
||||
<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 className="flex items-center space-x-2">
|
||||
<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 className="flex items-center space-x-2">
|
||||
<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 className="flex items-center space-x-2">
|
||||
<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>
|
||||
@@ -482,7 +482,7 @@ const WrStatusPage: React.FC = () => {
|
||||
<div className="flex justify-between items-center border-b border-[#DBE2E6] px-4 py-3">
|
||||
<div>
|
||||
<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">
|
||||
@@ -493,10 +493,10 @@ const WrStatusPage: React.FC = () => {
|
||||
<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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y">
|
||||
@@ -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,272 @@
|
||||
"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 (
|
||||
<>
|
||||
<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}
|
||||
searchBar={(props: any) => (
|
||||
<SearchBar
|
||||
setHasuPk={(hasuPk: any) => {
|
||||
if (hasuPk) {
|
||||
setSelectDataPk(hasuPk);
|
||||
}
|
||||
}}
|
||||
onApply={onApply}
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</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
|
||||
*/
|
||||
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 = {
|
||||
lyr_sn: string
|
||||
@@ -30,7 +31,21 @@ export type LyrStngProps = {
|
||||
export const mapService = {
|
||||
//레이어 설정 조회
|
||||
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',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
+10
-4
@@ -3,7 +3,7 @@
|
||||
import { BaseLayout } from "@common/components";
|
||||
import type { MenuItem } from "./store/common";
|
||||
import { useDmStore } from "./store";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
|
||||
export default function Base({
|
||||
children,
|
||||
@@ -13,10 +13,16 @@ export default function Base({
|
||||
const { getMenuAuth } = useDmStore((state) => state);
|
||||
|
||||
const router = useRouter();
|
||||
const handleClick = (id:MenuItem) =>{
|
||||
const pathname = usePathname();
|
||||
|
||||
const handleClick = (id: MenuItem) => {
|
||||
getMenuAuth(id.menuCd);
|
||||
router.push(id.urlPath);
|
||||
}
|
||||
};
|
||||
|
||||
return <BaseLayout handleMenuClick={handleClick}>{children}</BaseLayout>;
|
||||
return (
|
||||
<BaseLayout currentPath={pathname ?? ""} handleMenuClick={handleClick}>
|
||||
{children}
|
||||
</BaseLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"use client"
|
||||
import Base from "./base";
|
||||
import "./globals.css";
|
||||
import { StoreProvider } from "./store";
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
@@ -9,12 +7,11 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<html lang="ko">
|
||||
<body>
|
||||
<StoreProvider>
|
||||
<Base>{children}</Base>
|
||||
</StoreProvider>
|
||||
{children}
|
||||
</body>
|
||||
</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'>
|
||||
<Link href="/pages/sdmMng/sdmApplyList">수요처 신청 현황(개발중)</Link>
|
||||
</Button>
|
||||
<Button className='ml-5 place-self-auto'>
|
||||
<Link href="/pages/map/mainMap">수요처 신청 현황(개발중)</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { SearchBar } from "@/components/ui/map/searchBar/searchBar";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const Map = () => {
|
||||
|
||||
const [selectDataPk, setSelectDataPk] = useState <string>('')
|
||||
|
||||
// 삭제요망 데이터 선ㅌ
|
||||
useEffect(() =>{
|
||||
console.log("Selected Data Pk: ", selectDataPk);
|
||||
},[selectDataPk])
|
||||
return (
|
||||
<>
|
||||
<SearchBar
|
||||
setHasuPk={(hasuPk: string) => {
|
||||
console.log("Selected HasuPk: ", hasuPk);
|
||||
if(hasuPk !==""){
|
||||
setSelectDataPk(hasuPk);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Map;
|
||||
@@ -1,250 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useDmStore } from "@/app/store";
|
||||
import type { DemandCompType } from "@/app/store/sdmMng";
|
||||
import { SdmInfoRegForm } from "@/components/ui/sdmInfoInsertForm/component";
|
||||
import { UiModal } from "@common/business_components/ui/dialog_ui";
|
||||
import { Button } from "@common/components/ui";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { Suspense, useEffect, useState } from "react";
|
||||
import { FormProvider, useForm, type SubmitHandler } from "react-hook-form";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
import * as z from "zod";
|
||||
import SdmLogin from "../sdmLogin/page";
|
||||
|
||||
const sdmInfoFormSchema = z.object({
|
||||
sdmNm: z.string()
|
||||
.min(1, "수요처를 선택해주세요"),
|
||||
demandYmd: z.date().optional(),
|
||||
demandQnty: z.string()
|
||||
.min(1, "요구수량을 입력해주세요")
|
||||
.regex(/^[0-9]+$/, "요구수량은 숫자만 입력 가능합니다"),
|
||||
usePurpose: z.string().optional(),
|
||||
money: z.string()
|
||||
.regex(/^[0-9]+$/, "희망단가는 숫자만 입력 가능합니다"),
|
||||
/**요구수질 */
|
||||
reqWaterQlty : z.string().optional(),
|
||||
grssClfg: z.string()
|
||||
.min(1, "총대장균군수를 입력해주세요")
|
||||
.regex(/^[0-9]+$/, "총대장균군수는 숫자만 입력 가능합니다"),
|
||||
tn : z.string()
|
||||
.min(1, "총질소를 입력해주세요")
|
||||
.regex(/^[0-9]+$/, "총질소는 숫자만 입력 가능합니다"),
|
||||
combResdch : z.string()
|
||||
.min(1, "결합잔류염소를 입력해주세요")
|
||||
.regex(/^[0-9]+$/, "결합잔류염소는 숫자만 입력 가능합니다"),
|
||||
tph : z.string()
|
||||
.min(1, "총인산물을 입력해주세요")
|
||||
.regex(/^[0-9]+$/, "총인산물은 숫자만 입력 가능합니다"),
|
||||
trbt : z.string()
|
||||
.min(1, "탁도를 입력해주세요")
|
||||
.regex(/^[0-9]+$/, "탁도는 숫자만 입력 가능합니다"),
|
||||
ph : z.string()
|
||||
.min(1, "수소이온농도를 입력해주세요")
|
||||
.regex(/^[0-9]+$/, "수소이온농도는 숫자만 입력 가능합니다"),
|
||||
bod : z.string()
|
||||
.min(1, "생물학적 산소요구량을 입력해주세요")
|
||||
.regex(/^[0-9]+$/, "생물학적 산소요구량은 숫자만 입력 가능합니다"),
|
||||
/**컬럼확인필요 */
|
||||
clIonQnt : z.string()
|
||||
.min(1, "염화물을 입력해주세요")
|
||||
.regex(/^[0-9]+$/, "염화물은 숫자만 입력 가능합니다"),
|
||||
smll : z.string()
|
||||
.min(1, "냄새를 입력해주세요")
|
||||
.regex(/^[0-9]+$/, "냄새는 숫자만 입력 가능합니다"),
|
||||
cdtnv : z.string()
|
||||
.min(1, "전기전도도를 입력해주세요")
|
||||
.regex(/^[0-9]+$/, "전기전도도는 숫자만 입력 가능합니다"),
|
||||
crmty : z.string()
|
||||
.min(1, "색도를 입력해주세요")
|
||||
.regex(/^[0-9]+$/, "색도는 숫자만 입력 가능합니다"),
|
||||
rmrk : z.string().optional(),
|
||||
/* telno: z.string()
|
||||
.min(8, "전화번호는 8자 이상 입력해주세요")
|
||||
.max(12, "전화번호는 12자 이내로 입력해주세요")
|
||||
.regex(/^[0-9-]+$/, "전화번호는 숫자와 하이픈(-)만 입력 가능합니다"), */
|
||||
candidateTitle: z.string().optional(),
|
||||
});
|
||||
|
||||
export type FormData = z.infer<typeof sdmInfoFormSchema>;
|
||||
|
||||
function SdmInfoInsertContent() {
|
||||
const searchParams = useSearchParams();
|
||||
const {insertSdmInfoData, Sdm} = useDmStore((state) => state);
|
||||
const {successLogin} = Sdm;
|
||||
const [ismoneyDisabled, setIsmoneyDisabled] = useState(false);
|
||||
const formSchema = sdmInfoFormSchema;
|
||||
|
||||
const [isLogin, setIsLogin] = useState(false);
|
||||
const [facilityName, setFacilityName] = useState<string>('');
|
||||
const [defaultValues, setDefaultValues] = useState<FormData>({
|
||||
sdmNm: '',
|
||||
demandYmd: new Date(),
|
||||
demandQnty: '',
|
||||
usePurpose: '',
|
||||
money: '',
|
||||
/**요구수질 */
|
||||
reqWaterQlty: '',
|
||||
grssClfg: '',
|
||||
tn: '',
|
||||
combResdch: '',
|
||||
tph: '',
|
||||
trbt: '',
|
||||
ph: '',
|
||||
bod: '',
|
||||
/**컬럼확인필요 */
|
||||
clIonQnt: '',
|
||||
smll: '',
|
||||
cdtnv: '',
|
||||
crmty: '',
|
||||
rmrk: '',
|
||||
/** 후보지 제목 */
|
||||
candidateTitle: '',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const user = sessionStorage.getItem('user');
|
||||
setDefaultValues(prev => ({
|
||||
...prev,
|
||||
sdmNm: searchParams.get('dmdCoNm') || '',
|
||||
}));
|
||||
sessionStorage.setItem('user', searchParams.get('dmdInfoId') || '');
|
||||
setIsLogin(user || searchParams.get('dmdInfoId') ? true : false);
|
||||
}, [searchParams]);
|
||||
|
||||
useEffect(() => {
|
||||
const local = localStorage.getItem('user');
|
||||
if(local) {
|
||||
setIsLogin(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const form = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues,
|
||||
mode: "onChange",
|
||||
});
|
||||
|
||||
// 로그인 성공 시 모달 닫기 및 폼 업데이트
|
||||
useEffect(() => {
|
||||
if (successLogin) {
|
||||
setIsLogin(true);
|
||||
const user = sessionStorage.getItem('user');
|
||||
if (user) {
|
||||
try {
|
||||
const userData = JSON.parse(user);
|
||||
if (userData.dmdCoNm) {
|
||||
setFacilityName(userData.dmdCoNm);
|
||||
form.setValue('sdmNm', userData.dmdCoNm);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing user data:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [successLogin, form]);
|
||||
|
||||
// 체크박스 상태가 변경될 때마다 money 필드의 에러를 초기화
|
||||
useEffect(() => {
|
||||
if (ismoneyDisabled) {
|
||||
form.clearErrors('money');
|
||||
form.setValue('money', '');
|
||||
}
|
||||
}, [ismoneyDisabled, form]);
|
||||
|
||||
const onSubmit: SubmitHandler<FormData> = async (data) => {
|
||||
try {
|
||||
// Transform form data to match API schema
|
||||
const apiData : DemandCompType = {
|
||||
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') || ''),
|
||||
dmdCoNm: JSON.parse(sessionStorage.getItem('user') || '{}')?.dmdCoNm?.toString() || data.sdmNm,
|
||||
ctpvCd: JSON.parse(sessionStorage.getItem('user') || '{}')?.ctpvCd?.toString() || searchParams.get('ctpvCd') || '',
|
||||
sggCd: JSON.parse(sessionStorage.getItem('user') || '{}')?.sggCd?.toString() || searchParams.get('sggCd') || '',
|
||||
daddr: JSON.parse(sessionStorage.getItem('user') || '{}')?.daddr?.toString() || searchParams.get('daddr') || '',
|
||||
telno: JSON.parse(sessionStorage.getItem('user') || '{}')?.telno?.toString() || searchParams.get('telno') || '',
|
||||
rgtr: JSON.parse(sessionStorage.getItem('user') || '{}')?.rgtr?.toString() || searchParams.get('rgtr') || '',
|
||||
useUsgCd: data.usePurpose || '',
|
||||
wntUntprc: Number(data.money) || '',
|
||||
dmndTmng: data.demandYmd || new Date(),
|
||||
dmndQty: Number(data.demandQnty) || '',
|
||||
grssClfg: Number(data.grssClfg) || '',
|
||||
tn: Number(data.tn) || '',
|
||||
combResdch: Number(data.combResdch) || '',
|
||||
tph: Number(data.tph) || '',
|
||||
trbt: Number(data.trbt) || '',
|
||||
ph: Number(data.ph) || '',
|
||||
blgcOxygDmndQnt: Number(data.bod) || '',
|
||||
clIonQnt: Number(data.clIonQnt) || '',
|
||||
smll: Number(data.smll) || '',
|
||||
cdtnv: Number(data.cdtnv) || '',
|
||||
crmty: Number(data.crmty) || '',
|
||||
rmrk: data.rmrk || '',
|
||||
bzmnNo: "",
|
||||
dmdAplyPswd: "",
|
||||
index: 0,
|
||||
reqDt: "",
|
||||
dmdInfoSn: 0,
|
||||
status : '',
|
||||
};
|
||||
|
||||
await insertSdmInfoData(apiData);
|
||||
} catch (error) {
|
||||
console.error('Error submitting form:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-4xl font-bold text-gray-900 underline">
|
||||
수요정보 등록
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className="max-w-3xl mx-auto bg-white rounded-lg shadow-md p-6">
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||
<SdmInfoRegForm
|
||||
form={form}
|
||||
onCheckboxChange={setIsmoneyDisabled}
|
||||
facilityName={facilityName}
|
||||
/>
|
||||
<div className="flex justify-center pt-4">
|
||||
<Button
|
||||
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"
|
||||
>
|
||||
수요정보 등록
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isLogin?
|
||||
<UiModal
|
||||
body={<SdmLogin/>}
|
||||
bodyClass="sm:max-w-[90vw] w-[530px] h-[430px]"
|
||||
open={!isLogin}
|
||||
onOpenChange={(open) => !open && setIsLogin(false)}
|
||||
noHeaderYn="Y"
|
||||
noFooterYn="Y"
|
||||
/>
|
||||
:null}
|
||||
<Toaster position="top-center" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function MyForm() {
|
||||
return (
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<SdmInfoInsertContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getFetch } from "@/components/ui/api";
|
||||
import { DEV_URL_FO } from "@/components/ui/config";
|
||||
import type { FacilityInfo } from "./type";
|
||||
import type { FacilityInfo, SubsInfo } from "./type";
|
||||
|
||||
const checkWrRegList = async () => {
|
||||
const wrRegList = await getFetch("wr", `/reuseFc/pubFc`);
|
||||
@@ -14,9 +14,8 @@ const getRuseFclt = async (
|
||||
) => {
|
||||
const checkWrRegListRes = await checkWrRegList();
|
||||
|
||||
let url = `/prcsFclt/ruseInfo/${page ? page : 1}/11/A/${sggOrCity ? sggOrCity : "11"}/0/1/SW`;
|
||||
let url = `/prcsFclt/ruseInfo/${page ? page : 1}/5/A/${sggOrCity ? sggOrCity : "0"}/0/1/SW`;
|
||||
|
||||
console.log(' checkWrRegListRes ', checkWrRegListRes)
|
||||
const ruseList = checkWrRegListRes.data.filter((item: string) => item !== "");
|
||||
|
||||
if (searchTxt) {
|
||||
@@ -30,54 +29,77 @@ const getRuseFclt = async (
|
||||
},
|
||||
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 };
|
||||
|
||||
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
|
||||
|
||||
return hasuList;
|
||||
};
|
||||
|
||||
export type DmMapActions = {
|
||||
findRuseFclt(sggOrCity:string, searchTxt: string): void;
|
||||
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);
|
||||
findRuseFclt: async (sggOrCity: string, searchTxt: string) => {
|
||||
const ruseFcltList = await getRuseFclt("1", sggOrCity, searchTxt);
|
||||
|
||||
set((state: DmMapType) => ({
|
||||
...state,
|
||||
map: {
|
||||
...state.map,
|
||||
ruseFcltList :ruseFcltList.data
|
||||
ruseFcltList: ruseFcltList.data,
|
||||
},
|
||||
}));
|
||||
},
|
||||
setSubsInfo: async (subsInfo: SubsInfo[]) => {
|
||||
set((state: DmMapType) => ({
|
||||
...state,
|
||||
map: {
|
||||
...state.map,
|
||||
subsInfo: subsInfo,
|
||||
},
|
||||
}));
|
||||
},
|
||||
|
||||
@@ -31,4 +31,9 @@ export type FacilityInfo= {
|
||||
tel: string; // 전화번호
|
||||
rswtrYn: string; // 재이용수 여부 ("Y" 또는 "N")
|
||||
ruse: boolean; // 재이용 여부 (boolean)
|
||||
}
|
||||
|
||||
export type SubsInfo={
|
||||
ruseUtztnFcltNm: string; // 재이용시설명
|
||||
ruseUtztnFcltCd: string; // 재이용시설 코드
|
||||
}
|
||||
@@ -2,12 +2,13 @@
|
||||
import type { SelectDataType } from "@common/business_components/ui";
|
||||
import type { TableUpperProps } from "../common";
|
||||
import toast from "react-hot-toast";
|
||||
import type { DemandCompType } from "@common/business_components/ui/sdmInsertForm/form";
|
||||
|
||||
/**====================================
|
||||
* Store - 타입
|
||||
=====================================*/
|
||||
// 수요처 정보
|
||||
export type DemandCompType = {
|
||||
export type DemandCompBizType = {
|
||||
index : number | '';
|
||||
dmdInfoId : string | '';
|
||||
dmdInfoFk : string | '';
|
||||
@@ -23,23 +24,33 @@ export type DemandCompType = {
|
||||
wntUntprc : number | '';
|
||||
dmndTmng : Date | '';
|
||||
dmndQty : number | '';
|
||||
grssClfg : number | '';
|
||||
tn : number | '';
|
||||
combResdch : number | '';
|
||||
tph : number | '';
|
||||
trbt : number | '';
|
||||
ph : number | '';
|
||||
blgcOxygDmndQnt : number | '';
|
||||
clIonQnt : number | '';
|
||||
smll : number | '';
|
||||
cdtnv : number | '';
|
||||
crmty : number | '';
|
||||
grssClfg : number | null //총대장균군수
|
||||
tn : number | null //총질소
|
||||
combResdch : number | null //결합잔류염소
|
||||
tph : number | null //총인
|
||||
trbt : number | null //탁도
|
||||
ph : string | ''; //수소이온농
|
||||
blgcOxygDmndQnt : number | null //생물학적산소요구량
|
||||
clIonQnt : number | null //염화물
|
||||
smll : string | ''; //냄새
|
||||
cdtnv : number | null; //전기전도도
|
||||
crmty : number | null //색도
|
||||
rmrk : string | '';
|
||||
reqDt : string | '';
|
||||
dmdInfoSn : number | '';
|
||||
status : string | '';
|
||||
splyCndtList:SupplyCandidate[]|[],
|
||||
ruseUtztnFcltNm?:string|''
|
||||
|
||||
}
|
||||
|
||||
// 공급 후보자 타입 정의
|
||||
export type SupplyCandidate = {
|
||||
ruseUtztnFcltNm: string;
|
||||
ruseUtztnFcltCd: string;
|
||||
};
|
||||
|
||||
|
||||
export type DemandMatchings = {
|
||||
splyFcltFk : string | '';
|
||||
splyFcltFlg : string | '';
|
||||
@@ -143,7 +154,8 @@ export type MoneyDetailType = {
|
||||
clclnBrdnCst : string | '';
|
||||
clclnEcqrRt : string | '';
|
||||
}
|
||||
|
||||
// 규칙 타입 선언
|
||||
export type DisableRulesType = Record<string, string[]>;
|
||||
/**====================================
|
||||
* Store - 데이터
|
||||
====================================*/
|
||||
@@ -151,6 +163,131 @@ export type MoneyDetailType = {
|
||||
const demandCompList : DemandCompType[] = [
|
||||
]
|
||||
|
||||
|
||||
const disableRules: DisableRulesType = {
|
||||
"청소": ["tn", 'tph',"clIonQnt" ,'cdtnv'],
|
||||
"화장실용수": ["tn", 'tph',"clIonQnt" ,'cdtnv'],
|
||||
"세척": ["combResdch", "crmty",'tph',"clIonQnt",'cdtnv'],
|
||||
"살수": ["combResdch", "crmty",'tph',"clIonQnt",'cdtnv'],
|
||||
"조경": ["combResdch", "crmty",'tph','cdtnv'],
|
||||
"농업_간접용수": ["combResdch", "crmty",'tn','tph','clIonQnt'],
|
||||
"농업_직접용수": ["combResdch", "crmty",'tn','tph','clIonQnt'],
|
||||
"하천": ["combResdch", "trbt", "crmty" ,'clIonQnt','cdtnv'],
|
||||
'공업':[]
|
||||
};
|
||||
|
||||
const defaultRules: Record<string, object> = {
|
||||
"청소": {
|
||||
grssClfg: null,
|
||||
combResdch:0.2,
|
||||
trbt: 2,
|
||||
bod: 5,
|
||||
crmty: 20,
|
||||
ph: "5.8~8.5",
|
||||
tn:null,
|
||||
tph: null,
|
||||
clIonQnt: null,
|
||||
cdtnv: null,
|
||||
|
||||
},
|
||||
"화장실용수": {
|
||||
grssClfg: null,
|
||||
combResdch:0.2,
|
||||
trbt: 2,
|
||||
bod: 5,
|
||||
crmty: 20,
|
||||
ph: "5.8~8.5",
|
||||
tn:null,
|
||||
tph: null,
|
||||
clIonQnt: null,
|
||||
cdtnv: null,
|
||||
},
|
||||
"세척": {
|
||||
grssClfg: null,
|
||||
trbt: 2,
|
||||
bod: 5,
|
||||
ph: "5.8~8.5",
|
||||
combResdch:null,
|
||||
crmty:null,
|
||||
tn:null,
|
||||
tph: null,
|
||||
clIonQnt: null,
|
||||
cdtnv: null,
|
||||
},
|
||||
"살수": {
|
||||
grssClfg: null,
|
||||
trbt: 2,
|
||||
bod: 5,
|
||||
ph: "5.8~8.5",
|
||||
combResdch:null,
|
||||
crmty:null,
|
||||
tn:null,
|
||||
tph: null,
|
||||
clIonQnt: null,
|
||||
cdtnv: null,
|
||||
},
|
||||
"조경": {
|
||||
grssClfg: null,
|
||||
trbt: 2,
|
||||
bod: 5,
|
||||
ph: "5.8~8.5",
|
||||
clIonQnt:250,
|
||||
combResdch:null,
|
||||
crmty:null,
|
||||
tn:null,
|
||||
tph: null,
|
||||
cdtnv: null,
|
||||
},
|
||||
"농업_간접용수": {
|
||||
grssClfg: null,
|
||||
trbt: 5,
|
||||
bod: 8,
|
||||
ph: "5.8~8.5",
|
||||
cdtnv:2000,
|
||||
combResdch:null,
|
||||
crmty:null,
|
||||
tn:null,
|
||||
tph: null,
|
||||
clIonQnt: null,
|
||||
},
|
||||
"농업_직접용수": {
|
||||
grssClfg: null,
|
||||
trbt: 2,
|
||||
bod: 8,
|
||||
ph: "5.8~8.5",
|
||||
cdtnv:700,
|
||||
combResdch:null,
|
||||
crmty:null,
|
||||
tn:null,
|
||||
tph: null,
|
||||
clIonQnt: null,
|
||||
},
|
||||
"하천": {
|
||||
grssClfg: 1000,
|
||||
bod: 5,
|
||||
tn: 10,
|
||||
tph: 0.5,
|
||||
ph: "5.8~8.5",
|
||||
combResdch:null,
|
||||
trbt:null,
|
||||
crmty:null,
|
||||
clIonQnt: null,
|
||||
cdtnv:null
|
||||
},
|
||||
공업:{
|
||||
grssClfg: null,
|
||||
bod: null,
|
||||
tn: null,
|
||||
tph: null,
|
||||
ph: "~",
|
||||
combResdch:null,
|
||||
trbt:null,
|
||||
crmty:null,
|
||||
clIonQnt: null,
|
||||
cdtnv:null
|
||||
}
|
||||
};
|
||||
|
||||
// 사용용도
|
||||
const useTypesData : SelectDataType[] = [
|
||||
{ text: '청소', val: "청소" },
|
||||
@@ -158,7 +295,8 @@ const useTypesData : SelectDataType[] = [
|
||||
{ text: "세척", val: "세척" },
|
||||
{ text: "살수", val: "살수" },
|
||||
{ text: "조경", val: "조경" },
|
||||
{ text: "농업", val: "농업" },
|
||||
{ text: "농업_간접용수", val: "농업_간접용수" },
|
||||
{ text: "농업_직접용수", val: "농업_직접용수" },
|
||||
{ text: "하천", val: "하천" },
|
||||
{ text: "지하수", val: "지하수" },
|
||||
{ text: "공업", val: "공업" },
|
||||
@@ -271,7 +409,7 @@ const postSdmData = async (data: DemandCompType) => {
|
||||
}
|
||||
|
||||
// 수요정보 등록
|
||||
const postSdmInfoData = async (data: DemandCompType) => {
|
||||
const postSdmInfoData = async (data: DemandCompBizType) => {
|
||||
try {
|
||||
await postFetch("/reuseFc/request/demandInfo", data);
|
||||
toast.success('수요 정보 등록에 성공하였습니다.');
|
||||
@@ -351,7 +489,7 @@ const fetchGetSimilarCaseList = async (part: string, value: number|string) => {
|
||||
export type SdmActions = {
|
||||
initializeSdmData: () => void;
|
||||
insertSdmData: (data: DemandCompType) => Promise<void>;
|
||||
insertSdmInfoData: (data: DemandCompType) => Promise<void>;
|
||||
insertSdmInfoData: (data: DemandCompBizType) => Promise<void>;
|
||||
getDemandCompList: (searchText: string, pageCnt: number) => Promise<void>;
|
||||
actionLogin: (data: DemandCompType) => Promise<void>;
|
||||
getSdmInfoStatusList: (dmdInfoId: string) => Promise<void>;
|
||||
@@ -369,9 +507,10 @@ export type SdmType ={
|
||||
moneyDetailList: MoneyDetailType[];
|
||||
|
||||
// 수요정보 신청현황
|
||||
sdmInfoStatusList: DemandCompType[];
|
||||
sdmInfoStatusList: DemandCompBizType[];
|
||||
upHeadSdmInfoStatus: TableUpperProps[];
|
||||
|
||||
defaultRules: Record<string, object>
|
||||
disableRules: DisableRulesType;
|
||||
// 로그인관련
|
||||
successNextPage : boolean;
|
||||
successLogin: boolean;
|
||||
@@ -389,7 +528,10 @@ export const SdmState: SdmType = {
|
||||
moneyDetailList: moneyDetailList,
|
||||
// 수요정보 신청현황
|
||||
sdmInfoStatusList: [],
|
||||
defaultRules:defaultRules,
|
||||
disableRules:disableRules,
|
||||
upHeadSdmInfoStatus: upHeadSdmInfoStatus,
|
||||
|
||||
// 로그인관련
|
||||
successNextPage : false,
|
||||
successLogin: false,
|
||||
@@ -419,7 +561,7 @@ export const SdmReducer = (set: any): SdmActions => {
|
||||
}
|
||||
},
|
||||
|
||||
insertSdmInfoData: async (data: DemandCompType) => {
|
||||
insertSdmInfoData: async (data: DemandCompBizType) => {
|
||||
postSdmInfoData(data);
|
||||
},
|
||||
|
||||
@@ -456,11 +598,13 @@ export const SdmReducer = (set: any): SdmActions => {
|
||||
}))
|
||||
return;
|
||||
}
|
||||
const sdmInfoStatusList : DemandCompType[] = [];
|
||||
const sdmInfoStatusList : DemandCompBizType[] = [];
|
||||
|
||||
res.demandInfo.map((el: DemandCompType, index: number) => {
|
||||
res.demandInfo.map((el: DemandCompBizType, index: number) => {
|
||||
const matchingInfo = res.demandMatchings.find((matching: DemandMatchings) => matching.dmdInfoSn === el.dmdInfoSn);
|
||||
const status = matchingInfo?.splyFcltFlg?.trim() === 'Sub' ? '검토' : matchingInfo?.splyFcltFlg?.trim() === 'matched' ? '매칭완료' : '신청';
|
||||
const ruseUtztnFcltNm = matchingInfo?.ruseUtztnFcltNm ? matchingInfo.ruseUtztnFcltNm : '';
|
||||
|
||||
|
||||
const tmpData = {
|
||||
...el,
|
||||
@@ -476,7 +620,9 @@ export const SdmReducer = (set: any): SdmActions => {
|
||||
useUsgCd: el.useUsgCd,
|
||||
dmndTmng: el.dmndTmng,
|
||||
dmdInfoSn : el.dmdInfoSn,
|
||||
status : status
|
||||
status : status,
|
||||
splyCndtList: el.splyCndtList,
|
||||
ruseUtztnFcltNm: ruseUtztnFcltNm || ''
|
||||
}
|
||||
sdmInfoStatusList.push(tmpData);
|
||||
})
|
||||
|
||||
@@ -13,8 +13,10 @@ export const StoreZus = (initState: DmState= {
|
||||
...dmMapState,
|
||||
}) => {
|
||||
return createStore<DmStore>()((set) => {
|
||||
tokenManager.setToken("site-id-key", 'DM');
|
||||
tokenManager.setToken("site-theme-key", 'green');
|
||||
if (typeof window !== "undefined") {
|
||||
tokenManager.setToken("site-id-key", 'DM');
|
||||
tokenManager.setToken("site-theme-key", 'green');
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useDmStore } from '@/app/store';
|
||||
import type { DemandCompType } from '@/app/store/sdmMng';
|
||||
import type { DemandCompType } from '@common/business_components/ui/sdmInsertForm/form';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
type thisProps = {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
export const sanitizeInput = (val: string, allowDecimal = false) => {
|
||||
if (val === "") return ""; // 빈 값 허용
|
||||
|
||||
// 소숫점 허용
|
||||
if (allowDecimal) {
|
||||
// 숫자와 점만 허용
|
||||
let cleaned = val.replace(/[^0-9.]/g, "");
|
||||
// 소숫점 여러 개일 경우 첫 번째만 유지
|
||||
const parts = cleaned.split(".");
|
||||
if (parts.length > 2) {
|
||||
cleaned = parts[0] + "." + parts.slice(1).join("");
|
||||
}
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
// 정수만 허용
|
||||
return val.replace(/[^0-9]/g, "");
|
||||
};
|
||||
@@ -1,53 +1,76 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
type SearchType = {
|
||||
searchNm: string;
|
||||
searchPk: string;
|
||||
bscFctNm: string;
|
||||
bscFctCd: string;
|
||||
};
|
||||
|
||||
type thisProps = {
|
||||
label: string | undefined;
|
||||
initValue: string | undefined;
|
||||
setValue: (searchNm?: string) => void;
|
||||
setRtnData: (rtnData: any) => void;
|
||||
searchList?: any[];
|
||||
searchList?: SearchType[];
|
||||
};
|
||||
|
||||
export default function SearchInputText({
|
||||
label = undefined,
|
||||
initValue = undefined,
|
||||
setValue = () => {},
|
||||
setRtnData = () => {},
|
||||
searchList = undefined,
|
||||
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);
|
||||
if (value && value!=='') {
|
||||
setIsOpen(true);
|
||||
} else {
|
||||
setIsOpen(false);
|
||||
}
|
||||
setHighlightIndex(-1);
|
||||
setIsOpen(value !== "");
|
||||
},
|
||||
[setValue]
|
||||
);
|
||||
|
||||
const handleSuggestionClick = useCallback(
|
||||
(searchNm: string) => {
|
||||
// Do not update textValue to keep input unchanged on suggestion click
|
||||
setValue(searchNm);
|
||||
setTextValue(searchNm);
|
||||
setIsOpen(false);
|
||||
setRtnData(searchList?.find((item) => item.searchNm === searchNm) || {});
|
||||
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 (
|
||||
@@ -68,31 +91,39 @@ export default function SearchInputText({
|
||||
<div ref={wrapperRef} className="relative">
|
||||
<input
|
||||
type="text"
|
||||
value={textValue ||''}
|
||||
value={textValue || ""}
|
||||
onChange={handleInputChange}
|
||||
/* onFocus={() => setIsOpen(true)} */
|
||||
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 && searchList.length > 0 ? (
|
||||
searchList.map((item: SearchType, index: number) => (
|
||||
{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 hover:bg-gray-100"
|
||||
onClick={() => handleSuggestionClick(item.searchNm)}
|
||||
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.searchNm);
|
||||
handleSuggestionClick(item.bscFctNm);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{item.searchPk?.substring(0, 10)}
|
||||
<span className="float-right">{item.searchNm}</span>
|
||||
<span className="float-left">{item.bscFctNm}</span>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
|
||||
@@ -3,28 +3,29 @@ import { useDmStore } from "@/app/store";
|
||||
import {
|
||||
SearchDiv,
|
||||
SearchFormLeft,
|
||||
SearchFormRight,
|
||||
SelectBox,
|
||||
type SelectDataType,
|
||||
} from "@common/business_components/ui";
|
||||
import { Label } from "@common/components/ui";
|
||||
import { Button, Label } from "@common/components/ui";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import SearchInputText from "../SearchInputText/SearchInputText";
|
||||
|
||||
export function SearchBar({
|
||||
|
||||
setHasuPk
|
||||
}:{
|
||||
setHasuPk: (hasuPk: string) => void,
|
||||
}
|
||||
|
||||
) {
|
||||
const {map, Common, initializeCommonData ,findRuseFclt} = useDmStore((state) => state);
|
||||
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]);
|
||||
@@ -59,12 +60,11 @@ export function SearchBar({
|
||||
}
|
||||
},
|
||||
[initializeSigunData]
|
||||
);
|
||||
);
|
||||
|
||||
|
||||
useEffect(()=>{
|
||||
console.log('map.ruseFcltList ', map.ruseFcltList)
|
||||
},[map.ruseFcltList])
|
||||
// useEffect(() => {
|
||||
// console.log("map.ruseFcltList ", map.ruseFcltList);
|
||||
// }, [map.ruseFcltList]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -81,10 +81,10 @@ useEffect(()=>{
|
||||
onSelectValue={(val) =>
|
||||
onSelectValue(val.toString(), "ctpvCd")
|
||||
}
|
||||
value={sidoValue|| Common.selectSidoData[0]?.val || ""}
|
||||
value={sidoValue || Common.selectSidoData[0]?.val || ""}
|
||||
/>
|
||||
<SelectBox
|
||||
className="col-span-2 w-1/6 h-full text-2xl ml-2"
|
||||
className="col-span-2 w-1/6 h-full text-2xl ml-10"
|
||||
label="시군구"
|
||||
selectClass={""}
|
||||
selectArray={filteredSigunData || []}
|
||||
@@ -93,39 +93,44 @@ useEffect(()=>{
|
||||
}
|
||||
value={sigunValue}
|
||||
/>
|
||||
<div className="col-span-6 flex flex-row items-center min-w-[40rem]">
|
||||
<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;
|
||||
<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);
|
||||
}
|
||||
findRuseFclt(sggOrCity, searchNm);
|
||||
}
|
||||
}}
|
||||
setRtnData={(rtnData: any)=> {
|
||||
setHasuPk(rtnData.bscFctCd || "");
|
||||
}}
|
||||
searchList={map.ruseFcltList || []}
|
||||
/>
|
||||
}}
|
||||
setRtnData={(rtnData: any) => {
|
||||
setHasuPk(rtnData);
|
||||
}}
|
||||
searchList={map.ruseFcltList || []}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-row space-x-2">
|
||||
<Button
|
||||
color="sky"
|
||||
className="mt-2"
|
||||
size={"sm"}
|
||||
onClick={() => {
|
||||
onApply();
|
||||
}}
|
||||
>
|
||||
적용
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</SearchFormLeft>
|
||||
<SearchFormRight className="w-2/12 mx-5 flex flex-col items-center justify-center space-x-2 ">
|
||||
<div className="flex flex-row space-x-2">
|
||||
{/* <Button className="mt-2" size={"sm"} onClick={() => {}}>
|
||||
조회
|
||||
</Button> */}
|
||||
</div>
|
||||
</SearchFormRight>
|
||||
</SearchDiv>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,33 +1,34 @@
|
||||
import { FormUi } from "@common/business_components/ui";
|
||||
import type { ReactNode } from "react";
|
||||
import type { ControllerRenderProps, FieldError, FieldValues, Path } from "react-hook-form";
|
||||
|
||||
|
||||
import { cloneElement, isValidElement } from "react";
|
||||
interface AccessibleFormFieldProps<T extends FieldValues> {
|
||||
field: ControllerRenderProps<T, Path<T>>;
|
||||
label: string;
|
||||
required?: boolean;
|
||||
error?: FieldError;
|
||||
inputComponent: ReactNode;
|
||||
isDisabled?: boolean;
|
||||
inputComponent: React.ReactElement<{ disabled?: boolean }>;
|
||||
}
|
||||
|
||||
export function AccessibleFormField<T extends FieldValues>({
|
||||
label,
|
||||
required = false,
|
||||
error,
|
||||
inputComponent,
|
||||
isDisabled = false,
|
||||
}: AccessibleFormFieldProps<T>) {
|
||||
const labelText = required ? `${label} *` : label;
|
||||
|
||||
return (
|
||||
<FormUi label={labelText}>
|
||||
<FormUi label={labelText} isDisabled={isDisabled}>
|
||||
<div>
|
||||
{inputComponent}
|
||||
{error?.message && Array.from(new Set(error.message.split('\n'))).map((msg, idx) => (
|
||||
{isValidElement(inputComponent)
|
||||
? cloneElement(inputComponent, { disabled: isDisabled } as Partial<typeof inputComponent.props>)
|
||||
: inputComponent}
|
||||
{/* {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">
|
||||
{msg}
|
||||
</p>
|
||||
))}
|
||||
))} */}
|
||||
</div>
|
||||
</FormUi>
|
||||
);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
import { useDmStore } from "@/app/store";
|
||||
import type { DemandCompType } from "@/app/store/sdmMng";
|
||||
import { BZNM_TOT_NO, FormField, FormUi } from "@common/business_components";
|
||||
import type { DemandCompType } from "@common/business_components/ui/sdmInsertForm/form";
|
||||
import { Button, Input } from "@common/components";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { sha512 } from 'js-sha512';
|
||||
@@ -10,7 +10,7 @@ import * as z from "zod";
|
||||
|
||||
export function SdmLoginForm() {
|
||||
const { actionLogin, initializeCommonData } = useDmStore((state) => state);
|
||||
const [isBznmTotNoDisabled, setIsBznmTotNoDisabled] = useState(false);
|
||||
const [isBznmTotNoDisabled, setIsBznmTotNoDisabled] = useState(true);
|
||||
|
||||
const formatBusinessNumber = (value: string) => {
|
||||
// 숫자만 추출
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"dev": "next dev -p 3005",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"start:dm": "next start -p 3005",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -16,7 +16,6 @@ export class LoginApiService implements LoginService {
|
||||
|
||||
public async login(payload: ILogin): Promise<void> {
|
||||
try {
|
||||
console.log(' ENDPOINT.USER_SERVICE.LOGIN ', ENDPOINT.USER_SERVICE.LOGIN)
|
||||
const response = await fetch( PROXY_HOST+ ENDPOINT.USER_SERVICE.LOGIN, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export const SITE_ID = process.env.NEXT_PUBLIC_SITE_ID || "DM";
|
||||
export const PORT = process.env.NEXT_PUBLIC_PORT || "3000";
|
||||
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_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';
|
||||
|
||||
@@ -2,6 +2,7 @@ NEXT_PUBLIC_SITE_ID = FO
|
||||
NEXT_PUBLIC_PORT = 11300
|
||||
NEXT_PUBLIC_PROXY_HOST = http://con.mind-one.co.kr:11300
|
||||
NEXT_PUBLIC_SERVER_API_URL = http://con.mind-one.co.kr:11082/api/v1
|
||||
#NEXT_PUBLIC_SERVER_API_URL = http://dev.onasys.co.kr:8000
|
||||
NEXT_PUBLIC_WR_SERVER_API_URL = http://con.mind-one.co.kr:11080/api/v1
|
||||
NEXT_PUBLIC_CS_SERVER_API_URL = http://dev.onasys.co.kr:8000
|
||||
NEXT_PUBLIC_GEOSERVER_URL = http://rsone.iptime.org:10010/geoserver
|
||||
|
||||
+4
-3
@@ -1,7 +1,8 @@
|
||||
NEXT_PUBLIC_SITE_ID = FO
|
||||
NEXT_PUBLIC_PORT = 11300
|
||||
NEXT_PUBLIC_PROXY_HOST = http://con.mind-one.co.kr:11300
|
||||
NEXT_PUBLIC_SERVER_API_URL = http://con.mind-one.co.kr:11082/api/v1
|
||||
NEXT_PUBLIC_PORT = 3000
|
||||
NEXT_PUBLIC_PROXY_HOST = http://localhost:3000
|
||||
NEXT_PUBLIC_SERVER_API_URL = http://con.mind-one.co.kr:11082/api/v1
|
||||
#NEXT_PUBLIC_SERVER_API_URL = http://dev.onasys.co.kr:8000
|
||||
NEXT_PUBLIC_WR_SERVER_API_URL = http://con.mind-one.co.kr:11080/api/v1
|
||||
NEXT_PUBLIC_CS_SERVER_API_URL = http://dev.onasys.co.kr:8000
|
||||
NEXT_PUBLIC_GEOSERVER_URL = http://rsone.iptime.org:10010/geoserver
|
||||
|
||||
@@ -15,6 +15,7 @@ NEXT_PUBLIC_SITE_ID = FO
|
||||
NEXT_PUBLIC_PORT = 11300
|
||||
NEXT_PUBLIC_PROXY_HOST = http://con.mind-one.co.kr:11300
|
||||
NEXT_PUBLIC_SERVER_API_URL = http://con.mind-one.co.kr:11082/api/v1
|
||||
#NEXT_PUBLIC_SERVER_API_URL = http://dev.onasys.co.kr:8000
|
||||
NEXT_PUBLIC_WR_SERVER_API_URL = http://con.mind-one.co.kr:11080/api/v1
|
||||
NEXT_PUBLIC_CS_SERVER_API_URL = http://dev.onasys.co.kr:8000
|
||||
NEXT_PUBLIC_GEOSERVER_URL = http://rsone.iptime.org:10010/geoserver
|
||||
|
||||
+10
-5
@@ -3,7 +3,7 @@
|
||||
import { BaseLayout } from "@common/components";
|
||||
import type { MenuItem } from "./store/common";
|
||||
import { useFoStore } from "./store";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
|
||||
export default function Base({
|
||||
children,
|
||||
@@ -11,12 +11,17 @@ export default function Base({
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const { getMenuAuth } = useFoStore((state) => state);
|
||||
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const handleClick = (id:MenuItem) =>{
|
||||
|
||||
const handleClick = (id: MenuItem) => {
|
||||
getMenuAuth(id.menuCd);
|
||||
router.push(id.urlPath);
|
||||
}
|
||||
};
|
||||
|
||||
return <BaseLayout handleMenuClick={handleClick}>{children}</BaseLayout>;
|
||||
return (
|
||||
<BaseLayout currentPath={pathname ?? ""} handleMenuClick={handleClick}>
|
||||
{children}
|
||||
</BaseLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -97,10 +97,7 @@ export default function FacilityTermModalHeader<T extends { msrDt: string }>({
|
||||
const startDateOnly = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate());
|
||||
const endDateOnly = new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate());
|
||||
const rainEffectDay = (item as T & { rainEffectDay: number }).rainEffectDay;
|
||||
if(flowPanel === '1') {
|
||||
const infoRnDay = (item as T & { infoRnDay: number }).infoRnDay;
|
||||
return (itemDateOnly >= startDateOnly && itemDateOnly <= endDateOnly) && infoRnDay>0 && infoRnDay < 3;
|
||||
} else if(flowPanel === '2'){
|
||||
if(flowPanel === '2'){
|
||||
const infoRnDay = (item as T & { infoRnDay: number }).infoRnDay;
|
||||
return (itemDateOnly >= startDateOnly && itemDateOnly <= endDateOnly) && infoRnDay >= 3 && infoRnDay < 10;
|
||||
} else if(flowPanel === '3'){
|
||||
@@ -122,8 +119,7 @@ export default function FacilityTermModalHeader<T extends { msrDt: string }>({
|
||||
const infoRnDay = (item as T & { infoRnDay: number }).infoRnDay;
|
||||
return (itemDateOnly >= startDateOnly && itemDateOnly <= endDateOnly) && infoRnDay >= 60;
|
||||
} else {
|
||||
const infoRnDay = (item as T & { infoRnDay: number }).infoRnDay;
|
||||
return (itemDateOnly >= startDateOnly && itemDateOnly <= endDateOnly) && (infoRnDay>0 || rainEffectDay ===1);
|
||||
return (itemDateOnly >= startDateOnly && itemDateOnly <= endDateOnly) && (rainEffectDay ===1);
|
||||
}
|
||||
});
|
||||
} else if(tabName.includes("_chuncheon")) {
|
||||
@@ -132,8 +128,8 @@ export default function FacilityTermModalHeader<T extends { msrDt: string }>({
|
||||
const itemDateOnly = new Date(itemDate.getFullYear(), itemDate.getMonth(), itemDate.getDate());
|
||||
const startDateOnly = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate());
|
||||
const endDateOnly = new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate());
|
||||
const infoRnDay = (item as T & { infoRnDay: number }).infoRnDay;
|
||||
return (itemDateOnly >= startDateOnly && itemDateOnly <= endDateOnly) && infoRnDay==0;
|
||||
const rainEffectDay = (item as T & { rainEffectDay: number }).rainEffectDay;
|
||||
return (itemDateOnly >= startDateOnly && itemDateOnly <= endDateOnly) && rainEffectDay==0;
|
||||
});
|
||||
} else {
|
||||
filteredData = searchList.filter((item) => {
|
||||
@@ -144,7 +140,6 @@ export default function FacilityTermModalHeader<T extends { msrDt: string }>({
|
||||
return itemDateOnly >= startDateOnly && itemDateOnly <= endDateOnly;
|
||||
});
|
||||
}
|
||||
|
||||
setCopySearchList(filteredData);
|
||||
setStandSearchList(filteredData);
|
||||
}, [searchList, searchYear, tabName, flowPanel]);
|
||||
|
||||
@@ -15,7 +15,6 @@ export default function DrainageEquipment({searchYear = '', fcltyList = []}: Dra
|
||||
const {
|
||||
Common,
|
||||
DrainageEquipment,
|
||||
searchDrainageEquipment,
|
||||
} = useFoStore((state) => state);
|
||||
|
||||
const {
|
||||
@@ -35,11 +34,10 @@ export default function DrainageEquipment({searchYear = '', fcltyList = []}: Dra
|
||||
|
||||
useEffect(() => {
|
||||
if (!isInit && tableData[0]?.bscFctCd && (tableData[0].bscFctCd !== prevFacilityCode || !hasSearched)) {
|
||||
searchDrainageEquipment(searchYear, tableData[0].bscFctCd, false);
|
||||
setHasSearched(true);
|
||||
setPrevFacilityCode(tableData[0].bscFctCd);
|
||||
}
|
||||
}, [fcltyList, isInit, searchDrainageEquipment, searchYear, tableData, hasSearched, prevFacilityCode]);
|
||||
}, [fcltyList, isInit, searchYear, tableData, hasSearched, prevFacilityCode]);
|
||||
|
||||
// 연도가 바뀌면 hasSearched를 false로 리셋
|
||||
useEffect(() => {
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
import { useFoStore } from "@/app/store";
|
||||
import type { TableUpperProps } from "@/app/store/processFacility";
|
||||
import { ContentsDiv, UiModal, UiTable } from "@common/business_components";
|
||||
import { cn, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
|
||||
import { useEffect, useState } from "react";
|
||||
import DashBoardChartModal from "./modal/page";
|
||||
import LoadingOverlay from "@common/business_components/ui/loadingBar";
|
||||
|
||||
export type facilityInfoProps = {
|
||||
searchYear?: string;
|
||||
@@ -30,20 +31,20 @@ const headMakeColSpanPlanFclty = (upHeadList: TableUpperProps[], planYearList: s
|
||||
const headMakeColSpanOperationStatus = (upHeadList: TableUpperProps[], searchYear: string) => {
|
||||
return ( <>
|
||||
<TableRow>
|
||||
<TableHead rowSpan={1} colSpan={16} style={{ whiteSpace: 'pre-line' }}>{searchYear+'년도 운영실태'}</TableHead>
|
||||
<TableHead rowSpan={1} colSpan={17} style={{ whiteSpace: 'pre-line' }}>{searchYear+'년도 운영실태'}</TableHead>
|
||||
</TableRow>
|
||||
<TableRow>{
|
||||
upHeadList.map((head: TableUpperProps) =>
|
||||
(head.upName === 'upChangeRe1'&&head.upSequnce===1?<TableHead key={head.id} rowSpan={2}colSpan={2} style={{ whiteSpace: 'pre-line' }}>{head.title}</TableHead>:
|
||||
!head.upSequnce?<TableHead rowSpan={2}key={head.id}>{head.title}</TableHead>:
|
||||
!head.upSequnce?<TableHead style={{ whiteSpace: 'pre-line' }} rowSpan={2}key={head.id}>{head.title}</TableHead>:
|
||||
head.upName === 'upChangeRe2'&&head.upSequnce===1?<TableHead key={head.id} colSpan={3} style={{ whiteSpace: 'pre-line' }}>{'연간 운영실태'}</TableHead>:
|
||||
head.upName === 'upChangeRe3'&&head.upSequnce===1?<TableHead key={head.id} colSpan={3} style={{ whiteSpace: 'pre-line' }}>{'하절기 운영실태\n(6~8월)'}</TableHead>:
|
||||
head.upName === 'upChangeRe4'&&head.upSequnce===1?<TableHead key={head.id} colSpan={4} style={{ whiteSpace: 'pre-line' }}>{'동절기 운영실태\n(1~2월)'}</TableHead>:
|
||||
head.upName === 'upChangeRe5'&&head.upSequnce===1?<TableHead key={head.id} colSpan={3} style={{ whiteSpace: 'pre-line' }}>{'지하수유입량'}</TableHead>:null))}
|
||||
head.upName === 'upChangeRe5'&&head.upSequnce===1?<TableHead key={head.id} colSpan={3} style={{ whiteSpace: 'pre-line' }}>{'지하수 유입량 추정'}</TableHead>:null))}
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
{upHeadList.map((head: TableUpperProps) =>
|
||||
(head.upName === 'upChangeRe2'||head.upName === 'upChangeRe3'||head.upName === 'upChangeRe4'||head.upName === 'upChangeRe5' ?<TableHead key={head.id}>{head.title}</TableHead>:null))}
|
||||
(head.upName === 'upChangeRe2'||head.upName === 'upChangeRe3'||head.upName === 'upChangeRe4'||head.upName === 'upChangeRe5'?<TableHead style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead>:null))}
|
||||
</TableRow>
|
||||
</> )
|
||||
}
|
||||
@@ -56,7 +57,7 @@ export default function ProcessFacilitySearch({searchYear = '', fcltyList = []}:
|
||||
PlanFclty,
|
||||
OperationStatus,
|
||||
searchOperationStatus,
|
||||
searchPlanInfo
|
||||
searchPlanInfo,
|
||||
} = useFoStore((state) => state);
|
||||
|
||||
const [modalItem, setModalItem] = useState<{item: string, period: string, title: string, waterQlty: string} | null>(null);
|
||||
@@ -77,13 +78,18 @@ export default function ProcessFacilitySearch({searchYear = '', fcltyList = []}:
|
||||
const {
|
||||
upHeadListPlanFclty,
|
||||
planFcltyList,
|
||||
planYearList
|
||||
planYearList,
|
||||
waterUsage1,
|
||||
waterUsage2,
|
||||
waterUsage3,
|
||||
isLoadingPlanFclty
|
||||
} = PlanFclty;
|
||||
|
||||
// 그리드 관련
|
||||
const {
|
||||
upHeadListOperationStatus,
|
||||
operationalStatusList
|
||||
operationalStatusList,
|
||||
isLoadingOperationStatus
|
||||
} = OperationStatus;
|
||||
|
||||
const closeModal = () => {
|
||||
@@ -147,20 +153,27 @@ export default function ProcessFacilitySearch({searchYear = '', fcltyList = []}:
|
||||
searchPlanInfo(searchYear, fcltyList[0].bscFctCd);
|
||||
setHasSearched(true);
|
||||
setPrevFacilityCode(fcltyList[0].bscFctCd);
|
||||
}
|
||||
}, [isInit, searchOperationStatus, searchYear, fcltyList, hasSearched, prevFacilityCode, searchPlanInfo]);
|
||||
|
||||
// waterUsage 값들이 로드된 후에 테이블 데이터 업데이트
|
||||
useEffect(() => {
|
||||
if (waterUsage1 !== '' && waterUsage2 !== '' && waterUsage3 !== '') {
|
||||
planFcltyList.forEach((el: { [key: string]: string | number | boolean }, index) => {
|
||||
if (index === 6) {
|
||||
el['item'] = (Number(searchYear)-2).toString();
|
||||
el['provisionalApproval'] = Number(fcltyList[0]?.fcltyCpc)* 0.6;
|
||||
el['provisionalApproval'] = waterUsage3;
|
||||
} else if (index === 7) {
|
||||
el['item'] = (Number(searchYear)-1).toString();
|
||||
el['provisionalApproval'] = Number(fcltyList[0]?.fcltyCpc)* 0.6;
|
||||
el['provisionalApproval'] = waterUsage2;
|
||||
} else if (index === 8) {
|
||||
el['item'] = (Number(searchYear)-0).toString();
|
||||
el['provisionalApproval'] = Number(fcltyList[0]?.fcltyCpc)* 0.6;
|
||||
el['provisionalApproval'] = waterUsage1;
|
||||
}
|
||||
});
|
||||
console.log('planFcltyList :', planFcltyList);
|
||||
}
|
||||
}, [isInit, searchOperationStatus, searchYear, fcltyList, hasSearched, prevFacilityCode, searchPlanInfo, planFcltyList]);
|
||||
}, [waterUsage1, waterUsage2, waterUsage3, searchYear, planFcltyList]);
|
||||
// 연도가 바뀌면 hasSearched를 false로 리셋
|
||||
useEffect(() => {
|
||||
setHasSearched(false);
|
||||
@@ -190,6 +203,7 @@ export default function ProcessFacilitySearch({searchYear = '', fcltyList = []}:
|
||||
<>
|
||||
<UiTable headName={""} tableData={mappedTableData} headlist={upHeadListFclty} pageSize={0} total={0} cellClick={ undefined } onPageChange={() => { } } />
|
||||
<ContentsDiv>
|
||||
<LoadingOverlay isLoading={isLoadingPlanFclty} />
|
||||
<div className="grid grid-cols-12 gap-1">
|
||||
<div className="col-span-5">
|
||||
<Table variant="secondary">
|
||||
@@ -211,8 +225,8 @@ export default function ProcessFacilitySearch({searchYear = '', fcltyList = []}:
|
||||
:(index < 5 || index > 5)&& (index!= 4) && (head.id ==='provisionalApproval'||head.id ==='now'||head.id ==='stepOne'||head.id ==='stepTwo'||head.id ==='stepThree'||head.id ==='stepFour')
|
||||
?<TableCell className="text-right" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell>
|
||||
:(head.id === "provisionalApproval" && index === 4)
|
||||
?<TableCell colSpan={6} className="text-right border border-[var(--border)] text-[rgb(100,119,132)]" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{(Number(fcltyList[0]?.fcltyCpc)* 0.6).toLocaleString('en-US', { minimumFractionDigits: 1, maximumFractionDigits: 1 })} </TableCell>
|
||||
:((head.id ==='now'||head.id ==='stepOne'||head.id ==='stepTwo'||head.id ==='stepThree'||head.id ==='stepFour') && index === 4)
|
||||
?<TableCell colSpan={6} className="text-right border border-[var(--border)] text-[rgb(100,119,132)]" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{Math.round((Number(waterUsage1)+Number(waterUsage2)+Number(waterUsage3))/3 * 100)/100}</TableCell>
|
||||
:((head.id ==='now'||head.id ==='stepOne'||head.id ==='stepTwo'||head.id ==='stepThree'||head.id ==='stepFour') )
|
||||
?null
|
||||
:(head.id === 'item' && index != 5)?
|
||||
<TableCell className="text-center text-[rgb(100,119,132)]" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell>
|
||||
@@ -225,39 +239,40 @@ export default function ProcessFacilitySearch({searchYear = '', fcltyList = []}:
|
||||
</div>
|
||||
<div className="col-span-7 h-[600px] overflow-x-auto">
|
||||
<div className="w-[200%]">
|
||||
<Table variant="secondary">
|
||||
<TableHeader>
|
||||
{upChangeHeadListOperationStatus}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{operationalStatusList.map((el: { [key: string]: string | number | boolean },index) => (
|
||||
<TableRow key={index+"row"} >
|
||||
{upHeadListOperationStatus.map((head) => (
|
||||
(head.id === 'flowRate' && index === 0)?<TableCell rowSpan={5} className="text-center" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell>:
|
||||
(head.id === 'flowRate' && index === 5)?<TableCell rowSpan={6} className="text-center" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell>:
|
||||
(head.id === 'flowRate' && index === 11)?<TableCell rowSpan={2} className="text-center" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell>:
|
||||
(head.id === 'groundWaterAvg'||head.id === 'groundWaterSummer'||head.id === 'groundWaterWinter')&&(index === 0)?<TableCell rowSpan={7} className="text-right" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell>:
|
||||
(head.id === 'groundWaterAvg'||head.id === 'groundWaterSummer'||head.id === 'groundWaterWinter')&&(index === 7)?<TableCell rowSpan={6} className="text-right" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell>:
|
||||
(head.id === 'rdi'&&index === 0)?<TableCell rowSpan={2} className="text-right" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell>:
|
||||
(head.id === 'rdi'&&index === 2)?<TableCell rowSpan={3} className={cn("sticky top-0 bg-secondary [&_tr]:border-b", "text-center" )} style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{'RDII추정시\n일강수량(mm)'}</TableCell>:
|
||||
(head.id === 'rdi'&&index === 5)?<TableCell rowSpan={2} className="text-right" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el['rdiRain']}</TableCell>:
|
||||
(head.id === 'rdi'&&index === 7)?<TableCell rowSpan={6} className="text-right" style={{ whiteSpace: 'pre-line' }} key={head.id+index}></TableCell>:
|
||||
(head.id === 'waterQlty')?<TableCell className="text-center" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell>:
|
||||
((head.id === 'yearAvg' || head.id === 'yearMax' || head.id === 'yearMin') && (index>=0 && index<=4))?<TableCell className="bg-yellow-50 text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('flowRate', 'year', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
|
||||
((head.id === 'yearAvg' || head.id === 'yearMax' || head.id === 'yearMin') && (index>=5 && index<=10))?<TableCell className="bg-green-50 text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('waterQlty', 'year', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
|
||||
((head.id === 'yearAvg' || head.id === 'yearMax' || head.id === 'yearMin') && (index>=11 && index<=12))?<TableCell className="bg-blue-50 text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('chongin', 'year', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
|
||||
((head.id === 'summerAll' || head.id === 'summerThree' || head.id === 'summerRain') && (index>=0 && index<=4))?<TableCell className="bg-yellow-100 text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('flowRate', 'summer', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
|
||||
((head.id === 'summerAll' || head.id === 'summerThree' || head.id === 'summerRain') && (index>=5 && index<=10))?<TableCell className="bg-green-100 text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('waterQlty', 'summer', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
|
||||
((head.id === 'summerAll' || head.id === 'summerThree' || head.id === 'summerRain') && (index>=11 && index<=12))?<TableCell className="bg-blue-100 text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('chongin', 'summer', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
|
||||
((head.id === 'winterAll' || head.id === 'winterSunnyAvg' || head.id === 'winterSunnyMax' || head.id === 'winterSunnyMin') && (index>=0 && index<=4))?<TableCell className="bg-yellow-200 text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('flowRate', 'winter', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
|
||||
((head.id === 'winterAll' || head.id === 'winterSunnyAvg' || head.id === 'winterSunnyMax' || head.id === 'winterSunnyMin') && (index>=5 && index<=10))?<TableCell className="bg-green-200 text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('waterQlty', 'winter', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
|
||||
((head.id === 'winterAll' || head.id === 'winterSunnyAvg' || head.id === 'winterSunnyMax' || head.id === 'winterSunnyMin') && (index>=11 && index<=12))?<TableCell className="bg-blue-200 text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('chongin', 'winter', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
|
||||
null
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<LoadingOverlay isLoading={isLoadingOperationStatus} />
|
||||
<Table variant="secondary">
|
||||
<TableHeader>
|
||||
{upChangeHeadListOperationStatus}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{operationalStatusList.map((el: { [key: string]: string | number | boolean },index) => (
|
||||
<TableRow key={index+"row"} >
|
||||
{upHeadListOperationStatus.map((head) => (
|
||||
(head.id === 'flowRate' && index === 0)?<TableCell rowSpan={5} className="text-center" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell>:
|
||||
(head.id === 'flowRate' && index === 5)?<TableCell rowSpan={6} className="text-center" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell>:
|
||||
(head.id === 'flowRate' && index === 11)?<TableCell rowSpan={2} className="text-center" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell>:
|
||||
(head.id === 'groundWaterAvg'||head.id === 'groundWaterSummer'||head.id === 'groundWaterWinter')&&(index === 0)?<TableCell rowSpan={7} className="text-right" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell>:
|
||||
(head.id === 'groundWaterAvg'||head.id === 'groundWaterSummer'||head.id === 'groundWaterWinter')&&(index === 7)?<TableCell rowSpan={6} className="text-right" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell>:
|
||||
(head.id === 'rdi'&&index === 0)?<TableCell rowSpan={7} className="text-right" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell>:
|
||||
(head.id === 'rdi'&&index === 7)?<TableCell rowSpan={6} className="text-right" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell>:
|
||||
(head.id === 'rdiRain'&&index === 0)?<TableCell rowSpan={7} className="text-right" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el['rdiRain']}</TableCell>:
|
||||
(head.id === 'rdiRain'&&index === 7)?<TableCell rowSpan={6} className="text-right" style={{ whiteSpace: 'pre-line' }} key={head.id+index}></TableCell>:
|
||||
(head.id === 'waterQlty')?<TableCell className="text-center" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell>:
|
||||
((head.id === 'yearAvg' || head.id === 'yearMax' || head.id === 'yearMin') && (index>=0 && index<=4))?<TableCell className="bg-yellow-50 text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('flowRate', 'year', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
|
||||
((head.id === 'yearAvg' || head.id === 'yearMax' || head.id === 'yearMin') && (index>=5 && index<=10))?<TableCell className="bg-green-50 text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('waterQlty', 'year', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
|
||||
((head.id === 'yearAvg' || head.id === 'yearMax' || head.id === 'yearMin') && (index>=11 && index<=12))?<TableCell className="bg-blue-50 text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('chongin', 'year', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
|
||||
((head.id === 'summerAll' || head.id === 'summerThree' || head.id === 'summerRain') && (index>=0 && index<=4))?<TableCell className="bg-yellow-100 text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('flowRate', 'summer', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
|
||||
((head.id === 'summerAll' || head.id === 'summerThree' || head.id === 'summerRain') && (index>=5 && index<=10))?<TableCell className="bg-green-100 text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('waterQlty', 'summer', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
|
||||
((head.id === 'summerAll' || head.id === 'summerThree' || head.id === 'summerRain') && (index>=11 && index<=12))?<TableCell className="bg-blue-100 text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('chongin', 'summer', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
|
||||
((head.id === 'winterAll' || head.id === 'winterSunnyAvg' || head.id === 'winterSunnyMax' || head.id === 'winterSunnyMin') && (index>=0 && index<=4))?<TableCell className="bg-yellow-200 text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('flowRate', 'winter', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
|
||||
((head.id === 'winterAll' || head.id === 'winterSunnyAvg' || head.id === 'winterSunnyMax' || head.id === 'winterSunnyMin') && (index>=5 && index<=10))?<TableCell className="bg-green-200 text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('waterQlty', 'winter', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
|
||||
((head.id === 'winterAll' || head.id === 'winterSunnyAvg' || head.id === 'winterSunnyMax' || head.id === 'winterSunnyMin') && (index>=11 && index<=12))?<TableCell className="bg-blue-200 text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('chongin', 'winter', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
|
||||
null
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { CHART_FORM, type ChartConfig, ContentsDiv, MainChart } from "@common/bu
|
||||
import { Card, CardContent } from "@common/business_components/ui/chart/lib/card";
|
||||
import ListDiv from "@common/business_components/ui/list_div";
|
||||
import { useEffect, useState } from "react";
|
||||
import LoadingOverlay from "@common/business_components/ui/loadingBar";
|
||||
|
||||
type facilityInfoProps = {
|
||||
searchYear?: string;
|
||||
@@ -44,6 +45,7 @@ export default function DashBoardChart({searchYear = '', fcltyList = []}: facili
|
||||
chart_inflTn,
|
||||
chart_efflTn,
|
||||
chart_inflBod,
|
||||
isLoadingMainGraph,
|
||||
} = MainGraph;
|
||||
|
||||
/**
|
||||
@@ -179,6 +181,7 @@ export default function DashBoardChart({searchYear = '', fcltyList = []}: facili
|
||||
return (
|
||||
<>
|
||||
<ContentsDiv>
|
||||
<LoadingOverlay isLoading={isLoadingMainGraph} />
|
||||
<ListDiv className="h-[300px]">
|
||||
<Card className="h-full">
|
||||
<CardContent className="h-full">
|
||||
|
||||
@@ -14,7 +14,7 @@ const tabList = [
|
||||
{ value: 'facilityInfo', label: '시설정보 및 운영현황' },
|
||||
{ value: 'mainGraph', label: '주요그래프' },
|
||||
{ value: 'rainManage', label: '강우일 운영현황' },
|
||||
{ value: 'drainageEquipment', label: '배수설비 정비현황' }
|
||||
//{ value: 'drainageEquipment', label: '배수설비 정비현황' }
|
||||
];
|
||||
|
||||
export default function DashBoard() {
|
||||
@@ -60,6 +60,14 @@ export default function DashBoard() {
|
||||
const [searchYear, setSearchYear] = useState<string>(selectSearchYear[0]?.val || '0');
|
||||
// 시설명
|
||||
const [fcltyList, setFcltyList] = useState<{bscFctCd: string, bscFctNm: string, fcltyCpc: number | '', trtFrfNm: string | '', efflWqltStndrdNm: string | ''}>({
|
||||
bscFctCd: '26260PB001R',
|
||||
bscFctNm: '수영',
|
||||
fcltyCpc: 452000,
|
||||
trtFrfNm: '표준활성슬러지법, MLE, MBR',
|
||||
efflWqltStndrdNm: '500톤 이상(Ⅳ지역)'
|
||||
});
|
||||
|
||||
const [searchFcltyList, setSearchFcltyList] = useState<{bscFctCd: string, bscFctNm: string, fcltyCpc: number | '', trtFrfNm: string | '', efflWqltStndrdNm: string | ''}>({
|
||||
bscFctCd: '',
|
||||
bscFctNm: '',
|
||||
fcltyCpc: '',
|
||||
@@ -70,12 +78,12 @@ export default function DashBoard() {
|
||||
useEffect(() => {
|
||||
if (!isInit) {
|
||||
initializeCommonData();
|
||||
setFcltyList({
|
||||
bscFctCd: '11000SW001R',
|
||||
bscFctNm: '난지',
|
||||
fcltyCpc: 860000,
|
||||
trtFrfNm: '표준활성슬러지법, MLE, A2O',
|
||||
efflWqltStndrdNm: '500톤 이상(Ⅲ지역)'
|
||||
setSearchFcltyList({
|
||||
bscFctCd: '26260PB001R',
|
||||
bscFctNm: '수영',
|
||||
fcltyCpc: 452000,
|
||||
trtFrfNm: '표준활성슬러지법, MLE, MBR',
|
||||
efflWqltStndrdNm: '500톤 이상(Ⅳ지역)'
|
||||
});
|
||||
}
|
||||
}, [isInit, initializeCommonData]);
|
||||
@@ -257,12 +265,32 @@ export default function DashBoard() {
|
||||
|
||||
const initVal = () => {
|
||||
setFcltyList({
|
||||
bscFctCd: '11000SW001R',
|
||||
bscFctNm: '난지',
|
||||
fcltyCpc: 860000,
|
||||
trtFrfNm: '표준활성슬러지법, MLE, A2O',
|
||||
efflWqltStndrdNm: '500톤 이상(Ⅲ지역)'
|
||||
bscFctCd: '26260PB001R',
|
||||
bscFctNm: '수영',
|
||||
fcltyCpc: 452000,
|
||||
trtFrfNm: '표준활성슬러지법, MLE, MBR',
|
||||
efflWqltStndrdNm: '500톤 이상(Ⅳ지역)'
|
||||
});
|
||||
|
||||
setSearchFcltyList({
|
||||
bscFctCd: '26260PB001R',
|
||||
bscFctNm: '수영',
|
||||
fcltyCpc: 452000,
|
||||
trtFrfNm: '표준활성슬러지법, MLE, MBR',
|
||||
efflWqltStndrdNm: '500톤 이상(Ⅳ지역)'
|
||||
});
|
||||
setSearchYear(selectSearchYear[0]?.val || '0');
|
||||
searchFcltyList.bscFctNm = '수영';
|
||||
|
||||
setSidoValue('0');
|
||||
setSigunValue('0');
|
||||
}
|
||||
|
||||
const searchVal = () => {
|
||||
setSearchFcltyList(fcltyList);
|
||||
if(searchFcltyList.bscFctCd) {
|
||||
searchOperationStatus(false, searchYear, fcltyList.bscFctCd);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -274,10 +302,13 @@ export default function DashBoard() {
|
||||
<SelectBox className="col-span-3" label={'시도'} selectArray={selectSidoData} onSelectValue={(val) => onSelectValue(val.toString(), 'sido')} value={sidoValue} selectClass={undefined}/>
|
||||
<SelectBox className="col-span-3" label={'시군구'} selectArray={filteredSigunData} onSelectValue={(val) => onSelectValue(val.toString(), 'sigun')} value={sigunValue} selectClass={undefined}/>
|
||||
<div className="col-span-4 flex">
|
||||
<DashBoardFclty onFacilityChange={handleFacilityChange} sidoSigun={sigunValue!='0'?sigunValue:sidoValue!='0'?sidoValue:''} facilityNm={fcltyList.bscFctNm}></DashBoardFclty>
|
||||
<DashBoardFclty onFacilityChange={handleFacilityChange} sidoSigun={sigunValue!='0'?sigunValue:sidoValue!='0'?sidoValue:''} facilityNm={searchFcltyList.bscFctNm}></DashBoardFclty>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-4/12 mx-5 flex items-center justify-end space-x-2">
|
||||
<Button className={btnClass} onClick={searchVal}>
|
||||
조회
|
||||
</Button>
|
||||
<Button className={btnClass} onClick={initVal}>
|
||||
초기화
|
||||
</Button>
|
||||
@@ -288,16 +319,16 @@ export default function DashBoard() {
|
||||
</div>
|
||||
<UITab tabList={tabList} defaultValue="facilityInfo" onValueChange={setActiveTab}>
|
||||
<TabsContent value="facilityInfo">
|
||||
<ProcessFacilitySearch searchYear={searchYear} fcltyList={[fcltyList]} onFacilityChange={handleFacilityChange} />
|
||||
<ProcessFacilitySearch searchYear={searchYear} fcltyList={[searchFcltyList]} onFacilityChange={handleFacilityChange} />
|
||||
</TabsContent>
|
||||
<TabsContent value="mainGraph">
|
||||
<DashBoardChart searchYear={searchYear} fcltyList={[fcltyList]}/>
|
||||
<DashBoardChart searchYear={searchYear} fcltyList={[searchFcltyList]}/>
|
||||
</TabsContent>
|
||||
<TabsContent value="rainManage">
|
||||
<RainManageTab searchYear={searchYear} fcltyList={[fcltyList]}/>
|
||||
<RainManageTab searchYear={searchYear} fcltyList={[searchFcltyList]}/>
|
||||
</TabsContent>
|
||||
<TabsContent value="drainageEquipment">
|
||||
<DrainageEquipment searchYear={searchYear} fcltyList={[fcltyList]}/>
|
||||
<DrainageEquipment searchYear={searchYear} fcltyList={[searchFcltyList]}/>
|
||||
</TabsContent>
|
||||
</UITab>
|
||||
</ContentsDiv>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user