Compare commits
48
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c72fa450bc | ||
|
|
400d6cf732 | ||
|
|
39e2734a49 | ||
|
|
d92d4d3949 | ||
|
|
b93efa5503 | ||
|
|
0b8559891a | ||
|
|
ffa1781005 | ||
|
|
b47c0dff2b | ||
|
|
83f5d878e7 | ||
|
|
e815b9662f | ||
|
|
2e6cbad49c | ||
|
|
ede5ef6a21 | ||
|
|
621d222799 | ||
|
|
01df37d0b3 | ||
|
|
072a5337a3 | ||
|
|
2a0f5dce25 | ||
|
|
26f2c60f4c | ||
|
|
28d0e21815 | ||
|
|
49c475709f | ||
|
|
31b8152d37 | ||
|
|
b9f5eb50e5 | ||
|
|
c7772a0a23 | ||
|
|
392ccefb16 | ||
|
|
aeaf507c47 | ||
|
|
2f4cfa0d78 | ||
|
|
ebfc520d67 | ||
|
|
4df286b086 | ||
|
|
262cf21546 | ||
|
|
056b7bae3c | ||
|
|
428f617802 | ||
|
|
e797bdbdf7 | ||
|
|
cb11ec839e | ||
|
|
772dd1c1aa | ||
|
|
579e599eb1 | ||
|
|
3cc8fcb2eb | ||
|
|
f644bd6b7e | ||
|
|
f685884ed7 | ||
|
|
a2af60d52e | ||
|
|
395590bbe4 | ||
|
|
6c0d65a69a | ||
|
|
a6b34780ae | ||
|
|
a83ea455fb | ||
|
|
ab63cbe992 | ||
|
|
b2a6285475 | ||
|
|
6808430c9a | ||
|
|
dad8b9925e | ||
|
|
07c7947da1 | ||
|
|
35e1203168 |
Generated
+8
@@ -0,0 +1,8 @@
|
|||||||
|
# 디폴트 무시된 파일
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# 에디터 기반 HTTP 클라이언트 요청
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
@@ -185,15 +185,6 @@ function PostingDialog({ postingType, postingTargets }: PostingDialogProps) {
|
|||||||
...prev,
|
...prev,
|
||||||
[siteId]: value,
|
[siteId]: value,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// 상시 선택 시 게시 기간 미설정
|
|
||||||
if (value === "99") {
|
|
||||||
handleUpdateFields(siteId)({
|
|
||||||
pstgPrdSeCd: value,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setRememberPeriodDate((prev) => ({
|
setRememberPeriodDate((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[siteId]: { from, to },
|
[siteId]: { from, to },
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
Skeleton,
|
Skeleton,
|
||||||
} from "@common/components";
|
} from "@common/components";
|
||||||
import { AttachmentApi } from "@/entities/attachment";
|
import { AttachmentApi } from "@/entities/attachment";
|
||||||
import { PostApi, PostPageGuard, type PostParams } from "@/entities/post";
|
import { PostApi, type PostParams } from "@/entities/post";
|
||||||
import { useAlertStore, useMenuStore, useUserStore } from "@/shared/lib";
|
import { useAlertStore, useMenuStore, useUserStore } from "@/shared/lib";
|
||||||
import { ENDPOINT } from "@/shared/config";
|
import { ENDPOINT } from "@/shared/config";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
@@ -103,147 +103,143 @@ function FaqPage({ bbsId }: { bbsId: string }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PostPageGuard bbsId={bbsId}>
|
<div className="card card-border !mt-6">
|
||||||
<div className="card card-border !mt-6">
|
<div className="card card-border">
|
||||||
<div className="card card-border">
|
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
|
||||||
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
|
<Label className="mr-2 text-label">검색</Label>
|
||||||
<Label className="mr-2 text-label">검색</Label>
|
<Select
|
||||||
<Select
|
value={postParams.keywordType}
|
||||||
value={postParams.keywordType}
|
onValueChange={(keywordType) => {
|
||||||
onValueChange={(keywordType) => {
|
setPostParams({ ...postParams, keywordType });
|
||||||
setPostParams({ ...postParams, keywordType });
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<SelectTrigger className="h-[3.2rem] w-[10rem]">
|
||||||
<SelectTrigger className="h-[3.2rem] w-[10rem]">
|
<SelectValue placeholder="선택" />
|
||||||
<SelectValue placeholder="선택" />
|
</SelectTrigger>
|
||||||
</SelectTrigger>
|
<SelectContent>
|
||||||
<SelectContent>
|
<SelectItem value="title">제목</SelectItem>
|
||||||
<SelectItem value="title">제목</SelectItem>
|
<SelectItem value="all">제목+내용</SelectItem>
|
||||||
<SelectItem value="all">제목+내용</SelectItem>
|
<SelectItem value="writer">작성자</SelectItem>
|
||||||
<SelectItem value="writer">작성자</SelectItem>
|
</SelectContent>
|
||||||
</SelectContent>
|
</Select>
|
||||||
</Select>
|
<Input
|
||||||
<Input
|
value={postParams.keyword}
|
||||||
value={postParams.keyword}
|
onChange={(e) =>
|
||||||
onChange={(e) =>
|
setPostParams({ ...postParams, keyword: e.target.value })
|
||||||
setPostParams({ ...postParams, keyword: e.target.value })
|
}
|
||||||
}
|
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
|
||||||
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
|
/>
|
||||||
/>
|
<Button className="h-[3.2rem]" type="submit">
|
||||||
<Button className="h-[3.2rem]" type="submit">
|
조회
|
||||||
조회
|
</Button>
|
||||||
</Button>
|
</form>
|
||||||
</form>
|
</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">
|
||||||
|
{pageMenuAuthority?.regAuthrtYn === "Y" && (
|
||||||
|
<Button onClick={() => router.push(`${pathname}/register`)}>
|
||||||
|
등록
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{pageMenuAuthority?.delAuthrtYn === "Y" && (
|
||||||
|
<Button
|
||||||
|
color="red"
|
||||||
|
onClick={onSelectedPostDelete}
|
||||||
|
disabled={!selectedPostNumbers.length}
|
||||||
|
>
|
||||||
|
선택삭제
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="card card-border">
|
<div className="card !m-[1.2rem] h-full overflow-auto !p-0">
|
||||||
<div className="card-header">
|
<div className="mb-4 flex items-center gap-2 rounded-sm border border-border bg-background p-4">
|
||||||
<div className="flex w-full items-center justify-between">
|
<Checkbox
|
||||||
<h3 className="text-2xl font-medium text-label">콘텐츠 목록</h3>
|
id="all-select"
|
||||||
<div className="flex gap-2">
|
checked={selectedPostNumbers.length === postList?.content.length}
|
||||||
{pageMenuAuthority?.regAuthrtYn === "Y" && (
|
onClick={(e) => e.stopPropagation()}
|
||||||
<Button onClick={() => router.push(`${pathname}/register`)}>
|
onCheckedChange={handleAllCheckboxChange}
|
||||||
등록
|
/>
|
||||||
</Button>
|
<Label htmlFor="all-select" className="text-2xl">
|
||||||
)}
|
전체선택
|
||||||
{pageMenuAuthority?.delAuthrtYn === "Y" && (
|
</Label>
|
||||||
<Button
|
</div>
|
||||||
color="red"
|
{isFetched ? (
|
||||||
onClick={onSelectedPostDelete}
|
<Accordion
|
||||||
disabled={!selectedPostNumbers.length}
|
type="multiple"
|
||||||
|
className="secondary flex flex-col gap-4"
|
||||||
|
>
|
||||||
|
{postList?.content.map((item) => (
|
||||||
|
<div key={item.pstNo} className="flex items-start gap-2">
|
||||||
|
<Checkbox
|
||||||
|
id={item.pstNo.toString()}
|
||||||
|
checked={selectedPostNumbers.includes(item.pstNo)}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
handleCheckboxChange(item.pstNo, checked)
|
||||||
|
}
|
||||||
|
className="mx-2 my-6"
|
||||||
|
/>
|
||||||
|
<AccordionItem
|
||||||
|
value={item.pstNo.toString()}
|
||||||
|
className="w-full"
|
||||||
>
|
>
|
||||||
선택삭제
|
<AccordionTrigger>
|
||||||
</Button>
|
<div className="flex flex-1 items-center justify-between">
|
||||||
)}
|
<div className="flex items-center gap-4">
|
||||||
</div>
|
<span className="text-primary">Q</span>
|
||||||
</div>
|
<span>{item.pstTtl}</span>
|
||||||
</div>
|
|
||||||
<div className="card !m-[1.2rem] h-full overflow-auto !p-0">
|
|
||||||
<div className="mb-4 flex items-center gap-2 rounded-sm border border-border bg-background p-4">
|
|
||||||
<Checkbox
|
|
||||||
id="all-select"
|
|
||||||
checked={
|
|
||||||
selectedPostNumbers.length === postList?.content.length
|
|
||||||
}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
onCheckedChange={handleAllCheckboxChange}
|
|
||||||
/>
|
|
||||||
<Label htmlFor="all-select" className="text-2xl">
|
|
||||||
전체선택
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
{isFetched ? (
|
|
||||||
<Accordion
|
|
||||||
type="multiple"
|
|
||||||
className="secondary flex flex-col gap-4"
|
|
||||||
>
|
|
||||||
{postList?.content.map((item) => (
|
|
||||||
<div key={item.pstNo} className="flex items-start gap-2">
|
|
||||||
<Checkbox
|
|
||||||
id={item.pstNo.toString()}
|
|
||||||
checked={selectedPostNumbers.includes(item.pstNo)}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
onCheckedChange={(checked) =>
|
|
||||||
handleCheckboxChange(item.pstNo, checked)
|
|
||||||
}
|
|
||||||
className="mx-2 my-6"
|
|
||||||
/>
|
|
||||||
<AccordionItem
|
|
||||||
value={item.pstNo.toString()}
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
<AccordionTrigger>
|
|
||||||
<div className="flex flex-1 items-center justify-between">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<span className="text-primary">Q</span>
|
|
||||||
<span>{item.pstTtl}</span>
|
|
||||||
</div>
|
|
||||||
<span className="px-4 text-[1.2rem] font-normal text-label">
|
|
||||||
등록일: {dayjs(item.regDt).format("YYYY-MM-DD")}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</AccordionTrigger>
|
<span className="px-4 text-[1.2rem] font-normal text-label">
|
||||||
<AccordionContent>
|
등록일: {dayjs(item.regDt).format("YYYY-MM-DD")}
|
||||||
<div className="flex items-start gap-4">
|
</span>
|
||||||
<span className="text-[1.4rem] font-bold text-label">
|
</div>
|
||||||
A
|
</AccordionTrigger>
|
||||||
</span>
|
<AccordionContent>
|
||||||
<div className="flex-1">
|
<div className="flex items-start gap-4">
|
||||||
<pre className="whitespace-pre-wrap font-noto text-[1.4rem] tracking-tighter text-foreground">
|
<span className="text-[1.4rem] font-bold text-label">
|
||||||
{item.pstCn}
|
A
|
||||||
</pre>
|
</span>
|
||||||
<Attachment atflCd={item.atflCd} />
|
<div className="flex-1">
|
||||||
</div>
|
<pre className="whitespace-pre-wrap font-noto text-[1.4rem] tracking-tighter text-foreground">
|
||||||
<div className="flex justify-end gap-2">
|
{item.pstCn}
|
||||||
<Button
|
</pre>
|
||||||
size="icon"
|
<Attachment atflCd={item.atflCd} />
|
||||||
color="white"
|
|
||||||
onClick={() =>
|
|
||||||
router.push(`${pathname}/${item.pstNo}`)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<i className="diveicon di-pencil" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="icon"
|
|
||||||
color="white"
|
|
||||||
onClick={() => handleDeletePost(item.pstNo)}
|
|
||||||
>
|
|
||||||
<i className="diveicon di-trash" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</AccordionContent>
|
<div className="flex justify-end gap-2">
|
||||||
</AccordionItem>
|
<Button
|
||||||
</div>
|
size="icon"
|
||||||
))}
|
color="white"
|
||||||
</Accordion>
|
onClick={() =>
|
||||||
) : (
|
router.push(`${pathname}/${item.pstNo}`)
|
||||||
<Skeleton className="h-[36rem] w-full" />
|
}
|
||||||
)}
|
>
|
||||||
</div>
|
<i className="diveicon di-pencil" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
color="white"
|
||||||
|
onClick={() => handleDeletePost(item.pstNo)}
|
||||||
|
>
|
||||||
|
<i className="diveicon di-trash" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Accordion>
|
||||||
|
) : (
|
||||||
|
<Skeleton className="h-[36rem] w-full" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</PostPageGuard>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
import { LinkPage } from "@/pages/link";
|
|
||||||
export default LinkPage;
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
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} />;
|
|
||||||
};
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
import { RegistLinkPage } from "@/pages/link";
|
|
||||||
export default RegistLinkPage;
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
import { LinkLogPage } from "@/pages/linkLog";
|
|
||||||
export default LinkLogPage;
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
import { LinkParamsPage } from "@/pages/linkParams";
|
|
||||||
export default LinkParamsPage;
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
import { LinkStatsPage } from "@/pages/linkStats";
|
|
||||||
export default LinkStatsPage;
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
import { LinkTablePage } from "@/pages/linkTable";
|
|
||||||
export default LinkTablePage;
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
import { UserStatisticsPage } from "@/pages/user-statistics";
|
|
||||||
export default UserStatisticsPage;
|
|
||||||
+7
-26
@@ -1,12 +1,5 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import {
|
import { CLAIM_NAME, AUTH_USER_ID } from "./src/shared/config";
|
||||||
CLAIM_NAME,
|
|
||||||
AUTH_USER_ID,
|
|
||||||
PORT,
|
|
||||||
PROXY_HOST,
|
|
||||||
SERVER_API_URL,
|
|
||||||
SITE_ID,
|
|
||||||
} from "./src/shared/config";
|
|
||||||
|
|
||||||
const serverApiUrl =
|
const serverApiUrl =
|
||||||
process.env.NEXT_PUBLIC_SERVER_API_URL ?? "http://localhost:3000";
|
process.env.NEXT_PUBLIC_SERVER_API_URL ?? "http://localhost:3000";
|
||||||
@@ -15,22 +8,16 @@ export function middleware(req: NextRequest) {
|
|||||||
const pathname = req.nextUrl.pathname;
|
const pathname = req.nextUrl.pathname;
|
||||||
const searchParams = req.nextUrl.searchParams;
|
const searchParams = req.nextUrl.searchParams;
|
||||||
|
|
||||||
const logMessages: string[] = [
|
|
||||||
`[Middleware Request] ${new Date().toISOString()}`,
|
|
||||||
`[Environment]\n Mode: ${process.env.NODE_ENV}\n Port: ${PORT}\n Proxy Host: ${PROXY_HOST}\n Server API URL: ${SERVER_API_URL}\n Site ID: ${SITE_ID}`,
|
|
||||||
`[Request]\n Original Pathname: ${pathname}`,
|
|
||||||
];
|
|
||||||
|
|
||||||
if (pathname.startsWith("/server")) {
|
if (pathname.startsWith("/server")) {
|
||||||
const queryParams = searchParams.toString();
|
|
||||||
|
const decodedParams = Array.from(searchParams.entries())
|
||||||
|
.map(([key, value]) => `${key}=${decodeURIComponent(value)}`)
|
||||||
|
.join("&");
|
||||||
|
|
||||||
const newUrl = `${serverApiUrl}${pathname.replace("/server", "")}${
|
const newUrl = `${serverApiUrl}${pathname.replace("/server", "")}${
|
||||||
queryParams ? `?${queryParams}` : ""
|
decodedParams ? `?${decodedParams}` : ""
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
logMessages.push(` Rewritten URL: ${newUrl}`);
|
|
||||||
logMessages.push("\n--------------------------------\n");
|
|
||||||
console.log(logMessages.join("\n"));
|
|
||||||
|
|
||||||
return NextResponse.rewrite(new URL(newUrl));
|
return NextResponse.rewrite(new URL(newUrl));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,10 +30,6 @@ export function middleware(req: NextRequest) {
|
|||||||
if (token) requestHeaders.set(CLAIM_NAME, token);
|
if (token) requestHeaders.set(CLAIM_NAME, token);
|
||||||
if (userId) requestHeaders.set(AUTH_USER_ID, userId);
|
if (userId) requestHeaders.set(AUTH_USER_ID, userId);
|
||||||
|
|
||||||
logMessages.push(` Rewritten URL: ${newUrl}`);
|
|
||||||
logMessages.push("\n--------------------------------\n");
|
|
||||||
console.log(logMessages.join("\n"));
|
|
||||||
|
|
||||||
return NextResponse.rewrite(new URL(newUrl), {
|
return NextResponse.rewrite(new URL(newUrl), {
|
||||||
headers: requestHeaders,
|
headers: requestHeaders,
|
||||||
});
|
});
|
||||||
@@ -56,8 +39,6 @@ export function middleware(req: NextRequest) {
|
|||||||
return NextResponse.redirect(new URL("/dashboard", req.url));
|
return NextResponse.redirect(new URL("/dashboard", req.url));
|
||||||
}
|
}
|
||||||
|
|
||||||
logMessages.push("\n--------------------------------\n");
|
|
||||||
console.log(logMessages.join("\n"));
|
|
||||||
return NextResponse.next();
|
return NextResponse.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -67,22 +67,11 @@ export const contentsDetailSchema = z.object({
|
|||||||
useYn: z.string(),
|
useYn: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const contentsFormSchema = contentsDetailSchema
|
export const contentsFormSchema = contentsDetailSchema.omit({
|
||||||
.omit({
|
contsVer: true,
|
||||||
contsVer: true,
|
aprvDt: true,
|
||||||
aprvDt: true,
|
autzr: true,
|
||||||
autzr: true,
|
});
|
||||||
})
|
|
||||||
.extend({
|
|
||||||
contsSn: z.number().optional(),
|
|
||||||
aprvCd: z
|
|
||||||
.enum([
|
|
||||||
CONTENTS_APPROVE_STATUS.DRAFT,
|
|
||||||
CONTENTS_APPROVE_STATUS.REQUESTED,
|
|
||||||
CONTENTS_APPROVE_STATUS.APPROVED,
|
|
||||||
])
|
|
||||||
.optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type Contents = z.infer<typeof contentsSchema>;
|
export type Contents = z.infer<typeof contentsSchema>;
|
||||||
export type ContentsDetail = z.infer<typeof contentsDetailSchema>;
|
export type ContentsDetail = z.infer<typeof contentsDetailSchema>;
|
||||||
|
|||||||
@@ -1,191 +0,0 @@
|
|||||||
import axios from "axios";
|
|
||||||
import {
|
|
||||||
type LinkService,
|
|
||||||
type LinkSearchParam,
|
|
||||||
type LinkList,
|
|
||||||
type LinkFormData,
|
|
||||||
type LinkInfo,
|
|
||||||
type LinkLogList,
|
|
||||||
type LinkStatsList,
|
|
||||||
type LinkVrblList,
|
|
||||||
type LinkVrblFormData,
|
|
||||||
type LinkVrbl,
|
|
||||||
type LinkTable,
|
|
||||||
type LinkTableColumnsList,
|
|
||||||
type LinkTableColumns,
|
|
||||||
type LinkTableColData,
|
|
||||||
type LinkTableFormData,
|
|
||||||
} from "../model/link-interface";
|
|
||||||
|
|
||||||
export class LinkApiService implements LinkService {
|
|
||||||
public async getLinkList(params: LinkSearchParam): Promise<LinkList> {
|
|
||||||
const response = await axios.get<LinkList>(
|
|
||||||
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkList`,
|
|
||||||
{ params }
|
|
||||||
);
|
|
||||||
return response.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getLinkInfo(linkSn: string): Promise<LinkInfo> {
|
|
||||||
const response = await axios.get<LinkInfo>(
|
|
||||||
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkInfo/${linkSn}`,
|
|
||||||
);
|
|
||||||
return response.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async linkQuickStart(linkSn: string): Promise<string> {
|
|
||||||
const response = await axios.patch<string>(
|
|
||||||
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/linkQuickStart/${linkSn}`,
|
|
||||||
);
|
|
||||||
return response.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async updateLinkInfo(data: LinkFormData): Promise<void> {
|
|
||||||
await axios.put<void>(
|
|
||||||
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/updateLinkInfo`,
|
|
||||||
data,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async insertLinkInfo(data: LinkFormData): Promise<void> {
|
|
||||||
await axios.post<void>(
|
|
||||||
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/insertLinkInfo`,
|
|
||||||
data,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async deleteLink(linkSn: string): Promise<void> {
|
|
||||||
await axios.delete<void>(
|
|
||||||
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/deleteLink/${linkSn}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getLinkLogList(params: LinkSearchParam): Promise<LinkLogList> {
|
|
||||||
const response = await axios.get<LinkLogList>(
|
|
||||||
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/selectLinkLoglist`,
|
|
||||||
{ params }
|
|
||||||
);
|
|
||||||
return response.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getLinkStatsList(params: LinkSearchParam): Promise<LinkStatsList> {
|
|
||||||
const response = await axios.get<LinkStatsList>(
|
|
||||||
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/selectLinkStats`,
|
|
||||||
{ params }
|
|
||||||
);
|
|
||||||
return response.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getLinkVrblList(linkSn: string): Promise<LinkVrblList> {
|
|
||||||
const response = await axios.get<LinkVrblList>(
|
|
||||||
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkVrblList/${linkSn}`,
|
|
||||||
);
|
|
||||||
return response.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async insertLinkVrbl(data: LinkVrblFormData): Promise<number> {
|
|
||||||
const response = await axios.post<number>(
|
|
||||||
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/insertLinkVrbl`,
|
|
||||||
data,
|
|
||||||
);
|
|
||||||
|
|
||||||
return response.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async deleteLinkVrbl(data: string[]): Promise<void> {
|
|
||||||
await axios.delete<void>(
|
|
||||||
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/deleteLinkVrbl`,
|
|
||||||
{
|
|
||||||
data
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getLinkVrblInfo(linkVrblSn: string): Promise<LinkVrbl> {
|
|
||||||
const response = await axios.get<LinkVrbl>(
|
|
||||||
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkVrblInfo/${linkVrblSn}`,
|
|
||||||
);
|
|
||||||
return response.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async updateLinkVrblInfo(data: LinkVrblFormData): Promise<void> {
|
|
||||||
await axios.put<void>(
|
|
||||||
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/updateLinkVrblInfo`,
|
|
||||||
data,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getLinkTableInfo(linkSn: string): Promise<LinkTable> {
|
|
||||||
const response = await axios.get<LinkTable>(
|
|
||||||
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableInfo/${linkSn}`,
|
|
||||||
);
|
|
||||||
return response.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getLinkTableColList(linkSn: string): Promise<LinkTableColumnsList> {
|
|
||||||
const response = await axios.get<LinkTableColumnsList>(
|
|
||||||
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableColList/${linkSn}`,
|
|
||||||
);
|
|
||||||
return response.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getLinkTableColInfo(params: { colSn: string; tblSn:string; }): Promise<LinkTableColumns> {
|
|
||||||
const response = await axios.get<LinkTableColumns>(
|
|
||||||
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableColInfo`,
|
|
||||||
{ params }
|
|
||||||
);
|
|
||||||
return response.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async insertLinkTableCol(data: LinkTableColData): Promise<void> {
|
|
||||||
await axios.post<void>(
|
|
||||||
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/insertLinkTableCol`,
|
|
||||||
data
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async deleteLinkTableCol(data: string[]): Promise<void> {
|
|
||||||
await axios.delete<void>(
|
|
||||||
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/deleteLinkTableCol`,
|
|
||||||
{
|
|
||||||
data
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async updateLinkTableCol(data: LinkTableColData): Promise<void> {
|
|
||||||
axios.put<void>(
|
|
||||||
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/updateLinkTableCol`,
|
|
||||||
data,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async insertLinkTableInfo(data: LinkTableFormData): Promise<void> {
|
|
||||||
await axios.post<void>(
|
|
||||||
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/insertLinkTableInfo`,
|
|
||||||
data
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async updateLinkTableInfo(data: LinkTableFormData): Promise<void> {
|
|
||||||
await axios.put<void>(
|
|
||||||
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/updateLinkTableInfo`,
|
|
||||||
data
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async countLinkTblNm(tblNm: string): Promise<{count: number, tbl_sn: string}> {
|
|
||||||
const response = await axios.get<{count: number, tbl_sn: string}>(
|
|
||||||
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/countLinkTblNm/${tblNm}`,
|
|
||||||
);
|
|
||||||
return response.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async updateLinkResultCode(linkSn: string): Promise<void> {
|
|
||||||
await axios.patch<void>(
|
|
||||||
`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/updateLinkResultCode/${linkSn}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const LinkApi = new LinkApiService();
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
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"
|
|
||||||
@@ -1,235 +0,0 @@
|
|||||||
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>;
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,6 @@ import type {
|
|||||||
MenuItem,
|
MenuItem,
|
||||||
MenuFormData,
|
MenuFormData,
|
||||||
MenuTemporarilyStoredData,
|
MenuTemporarilyStoredData,
|
||||||
SiteMapFormData,
|
|
||||||
} from "../model/menu-interface";
|
} from "../model/menu-interface";
|
||||||
|
|
||||||
export class MenuApiService implements MenuService {
|
export class MenuApiService implements MenuService {
|
||||||
@@ -66,14 +65,6 @@ export class MenuApiService implements MenuService {
|
|||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async publishSiteMap(data: SiteMapFormData["siteMap"]): Promise<void> {
|
|
||||||
const response = await client.post<void>(
|
|
||||||
`${ENDPOINT.CMS_SERVICE.MENUS}/sitemap/publish`,
|
|
||||||
data,
|
|
||||||
);
|
|
||||||
return response.data;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MenuApi = new MenuApiService();
|
export const MenuApi = new MenuApiService();
|
||||||
|
|||||||
@@ -2,11 +2,8 @@ export { MenuApi } from "./api/menu-service";
|
|||||||
export {
|
export {
|
||||||
MENU_TYPE,
|
MENU_TYPE,
|
||||||
menuFormSchema,
|
menuFormSchema,
|
||||||
siteMapFormSchema,
|
|
||||||
type MenuType,
|
type MenuType,
|
||||||
type MenuItem,
|
type MenuItem,
|
||||||
type MenuFormData,
|
type MenuFormData,
|
||||||
type SiteMapFormData,
|
|
||||||
type SiteMapPayloadData,
|
|
||||||
} from "./model/menu-interface";
|
} from "./model/menu-interface";
|
||||||
export { MenuWrapper } from "./lib/menu-wrapper";
|
export { MenuWrapper } from "./lib/menu-wrapper";
|
||||||
|
|||||||
@@ -72,19 +72,8 @@ export const menuFormSchema = z.object({
|
|||||||
menuPicDsgnYmd: z.string().nullish(),
|
menuPicDsgnYmd: z.string().nullish(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const siteMapPayloadSchema = z.object({
|
|
||||||
menuCd: z.number(),
|
|
||||||
menuSubKornNm: z.string(),
|
|
||||||
otptYn: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const siteMapFormSchema = z.object({
|
|
||||||
siteMap: z.array(siteMapPayloadSchema),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type MenuFormData = z.infer<typeof menuFormSchema>;
|
export type MenuFormData = z.infer<typeof menuFormSchema>;
|
||||||
export type SiteMapFormData = z.infer<typeof siteMapFormSchema>;
|
|
||||||
export type SiteMapPayloadData = z.infer<typeof siteMapPayloadSchema>;
|
|
||||||
export type Menu = Omit<MenuFormData, "iconNm"> & {
|
export type Menu = Omit<MenuFormData, "iconNm"> & {
|
||||||
menuCd: number;
|
menuCd: number;
|
||||||
iconName?: string;
|
iconName?: string;
|
||||||
@@ -103,5 +92,4 @@ export interface MenuService {
|
|||||||
getMenuTemporarilyStoredData: (
|
getMenuTemporarilyStoredData: (
|
||||||
menuCd: number,
|
menuCd: number,
|
||||||
) => Promise<MenuTemporarilyStoredData[]>;
|
) => Promise<MenuTemporarilyStoredData[]>;
|
||||||
publishSiteMap: (data: SiteMapFormData["siteMap"]) => Promise<void>;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -194,15 +194,6 @@ function PostingDialog({
|
|||||||
...prev,
|
...prev,
|
||||||
[siteId]: value,
|
[siteId]: value,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// 상시 선택 시 게시 기간 미설정
|
|
||||||
if (value === "99") {
|
|
||||||
handleUpdateFields(siteId)({
|
|
||||||
pstgPrdSeCd: value,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setRememberPeriodDate((prev) => ({
|
setRememberPeriodDate((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[siteId]: { from, to },
|
[siteId]: { from, to },
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
export { TreeMenuApi } from "./api/tree-menu-service";
|
|
||||||
export type { TreeMenu, TreeNode } from "./model/tree-menu-interface";
|
|
||||||
@@ -4,7 +4,6 @@ export {
|
|||||||
userSaveSchema,
|
userSaveSchema,
|
||||||
userPasswordUpdateSchema,
|
userPasswordUpdateSchema,
|
||||||
userUpdateSchema,
|
userUpdateSchema,
|
||||||
type UserStateCode,
|
|
||||||
type UserSearchParams,
|
type UserSearchParams,
|
||||||
type UserListResponse,
|
type UserListResponse,
|
||||||
type UserSaveData,
|
type UserSaveData,
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ export const USER_STATE_CODE = {
|
|||||||
삭제: "09",
|
삭제: "09",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type UserStateCode =
|
type UserStateCode = (typeof USER_STATE_CODE)[keyof typeof USER_STATE_CODE];
|
||||||
(typeof USER_STATE_CODE)[keyof typeof USER_STATE_CODE];
|
|
||||||
|
|
||||||
// 사용자 검색 파라미터
|
// 사용자 검색 파라미터
|
||||||
export interface UserSearchParams extends PageableParams {
|
export interface UserSearchParams extends PageableParams {
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import {
|
|||||||
import { AttachmentApi } from "@/entities/attachment";
|
import { AttachmentApi } from "@/entities/attachment";
|
||||||
import {
|
import {
|
||||||
PostApi,
|
PostApi,
|
||||||
PostPageGuard,
|
|
||||||
deleteReasonMessages,
|
deleteReasonMessages,
|
||||||
type PostParams,
|
type PostParams,
|
||||||
} from "@/entities/post";
|
} from "@/entities/post";
|
||||||
@@ -121,147 +120,143 @@ function FaqPage({ bbsId }: { bbsId: string }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PostPageGuard bbsId={bbsId}>
|
<div className="card card-border !mt-6">
|
||||||
<div className="card card-border !mt-6">
|
<div className="card card-border">
|
||||||
<div className="card card-border">
|
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
|
||||||
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
|
<Label className="mr-2 text-label">검색</Label>
|
||||||
<Label className="mr-2 text-label">검색</Label>
|
<Select
|
||||||
<Select
|
value={postParams.keywordType}
|
||||||
value={postParams.keywordType}
|
onValueChange={(keywordType) => {
|
||||||
onValueChange={(keywordType) => {
|
setPostParams({ ...postParams, keywordType });
|
||||||
setPostParams({ ...postParams, keywordType });
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<SelectTrigger className="h-[3.2rem] w-[10rem]">
|
||||||
<SelectTrigger className="h-[3.2rem] w-[10rem]">
|
<SelectValue placeholder="선택" />
|
||||||
<SelectValue placeholder="선택" />
|
</SelectTrigger>
|
||||||
</SelectTrigger>
|
<SelectContent>
|
||||||
<SelectContent>
|
<SelectItem value="title">제목</SelectItem>
|
||||||
<SelectItem value="title">제목</SelectItem>
|
<SelectItem value="all">제목+내용</SelectItem>
|
||||||
<SelectItem value="all">제목+내용</SelectItem>
|
<SelectItem value="writer">작성자</SelectItem>
|
||||||
<SelectItem value="writer">작성자</SelectItem>
|
</SelectContent>
|
||||||
</SelectContent>
|
</Select>
|
||||||
</Select>
|
<Input
|
||||||
<Input
|
value={postParams.keyword}
|
||||||
value={postParams.keyword}
|
onChange={(e) =>
|
||||||
onChange={(e) =>
|
setPostParams({ ...postParams, keyword: e.target.value })
|
||||||
setPostParams({ ...postParams, keyword: e.target.value })
|
}
|
||||||
}
|
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
|
||||||
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
|
/>
|
||||||
/>
|
<Button className="h-[3.2rem]" type="submit">
|
||||||
<Button className="h-[3.2rem]" type="submit">
|
조회
|
||||||
조회
|
</Button>
|
||||||
</Button>
|
</form>
|
||||||
</form>
|
</div>
|
||||||
</div>
|
<div className="card card-border">
|
||||||
<div className="card card-border">
|
<div className="card-header">
|
||||||
<div className="card-header">
|
<div className="flex w-full items-center justify-between">
|
||||||
<div className="flex w-full items-center justify-between">
|
<h3 className="text-2xl font-medium text-label">콘텐츠 목록</h3>
|
||||||
<h3 className="text-2xl font-medium text-label">콘텐츠 목록</h3>
|
<div className="flex gap-2">
|
||||||
<div className="flex gap-2">
|
<AuthorityButton
|
||||||
<AuthorityButton
|
authority="regAuthrtYn"
|
||||||
authority="regAuthrtYn"
|
onClick={() => router.push(`${pathname}/register`)}
|
||||||
onClick={() => router.push(`${pathname}/register`)}
|
|
||||||
>
|
|
||||||
등록
|
|
||||||
</AuthorityButton>
|
|
||||||
<AuthorityButton
|
|
||||||
authority="delAuthrtYn"
|
|
||||||
color="red"
|
|
||||||
onClick={onSelectedPostDelete}
|
|
||||||
disabled={!selectedPostNumbers.length}
|
|
||||||
>
|
|
||||||
선택삭제
|
|
||||||
</AuthorityButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="card !m-[1.2rem] h-full overflow-auto !p-0">
|
|
||||||
<div className="mb-4 flex items-center gap-2 rounded-sm border border-border bg-background p-4">
|
|
||||||
<Checkbox
|
|
||||||
id="all-select"
|
|
||||||
checked={
|
|
||||||
selectedPostNumbers.length === postList?.content.length
|
|
||||||
}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
onCheckedChange={handleAllCheckboxChange}
|
|
||||||
/>
|
|
||||||
<Label htmlFor="all-select" className="text-2xl">
|
|
||||||
전체선택
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
{isFetched ? (
|
|
||||||
<Accordion
|
|
||||||
type="multiple"
|
|
||||||
className="secondary flex flex-col gap-4"
|
|
||||||
>
|
>
|
||||||
{postList?.content.map((item) => (
|
등록
|
||||||
<div key={item.pstNo} className="flex items-start gap-2">
|
</AuthorityButton>
|
||||||
<Checkbox
|
<AuthorityButton
|
||||||
id={item.pstNo.toString()}
|
authority="delAuthrtYn"
|
||||||
checked={selectedPostNumbers.includes(item.pstNo)}
|
color="red"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={onSelectedPostDelete}
|
||||||
onCheckedChange={(checked) =>
|
disabled={!selectedPostNumbers.length}
|
||||||
handleCheckboxChange(item.pstNo, checked)
|
>
|
||||||
}
|
선택삭제
|
||||||
className="mx-2 my-6"
|
</AuthorityButton>
|
||||||
/>
|
</div>
|
||||||
<AccordionItem
|
|
||||||
value={item.pstNo.toString()}
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
<AccordionTrigger>
|
|
||||||
<div className="flex flex-1 items-center justify-between">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<span className="text-primary">Q</span>
|
|
||||||
<span>{item.pstTtl}</span>
|
|
||||||
</div>
|
|
||||||
<span className="px-4 text-[1.2rem] font-normal text-label">
|
|
||||||
등록일: {dayjs(item.regDt).format("YYYY-MM-DD")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</AccordionTrigger>
|
|
||||||
<AccordionContent>
|
|
||||||
<div className="flex items-start gap-4">
|
|
||||||
<span className="text-[1.4rem] font-bold text-label">
|
|
||||||
A
|
|
||||||
</span>
|
|
||||||
<div className="flex-1">
|
|
||||||
<pre className="whitespace-pre-wrap font-noto text-[1.4rem] tracking-tighter text-foreground">
|
|
||||||
{item.pstCn}
|
|
||||||
</pre>
|
|
||||||
<Attachment atflCd={item.atflCd} />
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-end gap-2">
|
|
||||||
<Button
|
|
||||||
size="icon"
|
|
||||||
color="white"
|
|
||||||
onClick={() =>
|
|
||||||
router.push(`${pathname}/${item.pstNo}`)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<i className="diveicon di-pencil" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="icon"
|
|
||||||
color="white"
|
|
||||||
onClick={() => handleDeletePost(item.pstNo)}
|
|
||||||
>
|
|
||||||
<i className="diveicon di-trash" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</Accordion>
|
|
||||||
) : (
|
|
||||||
<Skeleton className="h-[36rem] w-full" />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="card !m-[1.2rem] h-full overflow-auto !p-0">
|
||||||
|
<div className="mb-4 flex items-center gap-2 rounded-sm border border-border bg-background p-4">
|
||||||
|
<Checkbox
|
||||||
|
id="all-select"
|
||||||
|
checked={selectedPostNumbers.length === postList?.content.length}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
onCheckedChange={handleAllCheckboxChange}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="all-select" className="text-2xl">
|
||||||
|
전체선택
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
{isFetched ? (
|
||||||
|
<Accordion
|
||||||
|
type="multiple"
|
||||||
|
className="secondary flex flex-col gap-4"
|
||||||
|
>
|
||||||
|
{postList?.content.map((item) => (
|
||||||
|
<div key={item.pstNo} className="flex items-start gap-2">
|
||||||
|
<Checkbox
|
||||||
|
id={item.pstNo.toString()}
|
||||||
|
checked={selectedPostNumbers.includes(item.pstNo)}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
handleCheckboxChange(item.pstNo, checked)
|
||||||
|
}
|
||||||
|
className="mx-2 my-6"
|
||||||
|
/>
|
||||||
|
<AccordionItem
|
||||||
|
value={item.pstNo.toString()}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<AccordionTrigger>
|
||||||
|
<div className="flex flex-1 items-center justify-between">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<span className="text-primary">Q</span>
|
||||||
|
<span>{item.pstTtl}</span>
|
||||||
|
</div>
|
||||||
|
<span className="px-4 text-[1.2rem] font-normal text-label">
|
||||||
|
등록일: {dayjs(item.regDt).format("YYYY-MM-DD")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent>
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<span className="text-[1.4rem] font-bold text-label">
|
||||||
|
A
|
||||||
|
</span>
|
||||||
|
<div className="flex-1">
|
||||||
|
<pre className="whitespace-pre-wrap font-noto text-[1.4rem] tracking-tighter text-foreground">
|
||||||
|
{item.pstCn}
|
||||||
|
</pre>
|
||||||
|
<Attachment atflCd={item.atflCd} />
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
color="white"
|
||||||
|
onClick={() =>
|
||||||
|
router.push(`${pathname}/${item.pstNo}`)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<i className="diveicon di-pencil" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
color="white"
|
||||||
|
onClick={() => handleDeletePost(item.pstNo)}
|
||||||
|
>
|
||||||
|
<i className="diveicon di-trash" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Accordion>
|
||||||
|
) : (
|
||||||
|
<Skeleton className="h-[36rem] w-full" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</PostPageGuard>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ function AddAdminDialog() {
|
|||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [userSearchQuery, setUserSearchQuery] = useState<UserSearchParams>({
|
const [userSearchQuery, setUserSearchQuery] = useState<UserSearchParams>({
|
||||||
keywordType: "userName",
|
keywordType: "",
|
||||||
keyword: "",
|
keyword: "",
|
||||||
});
|
});
|
||||||
const [selectedGroupId, setSelectedGroupId] = useState("");
|
const [selectedGroupId, setSelectedGroupId] = useState("");
|
||||||
@@ -193,7 +193,6 @@ function AddAdminDialog() {
|
|||||||
|
|
||||||
<form onSubmit={handleSearch} className="flex items-center gap-2">
|
<form onSubmit={handleSearch} className="flex items-center gap-2">
|
||||||
<Select
|
<Select
|
||||||
value={userSearchQuery.keywordType}
|
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
setUserSearchQuery({ ...userSearchQuery, keywordType: value })
|
setUserSearchQuery({ ...userSearchQuery, keywordType: value })
|
||||||
}
|
}
|
||||||
@@ -202,8 +201,8 @@ function AddAdminDialog() {
|
|||||||
<SelectValue placeholder="선택" />
|
<SelectValue placeholder="선택" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="userName">이름</SelectItem>
|
|
||||||
<SelectItem value="userId">아이디</SelectItem>
|
<SelectItem value="userId">아이디</SelectItem>
|
||||||
|
<SelectItem value="userName">이름</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ function AdminPage() {
|
|||||||
const alert = useAlertStore((state) => state.setMessage);
|
const alert = useAlertStore((state) => state.setMessage);
|
||||||
const { confirm } = useConfirm();
|
const { confirm } = useConfirm();
|
||||||
|
|
||||||
const [keywordType, setKeywordType] = useState("userId");
|
const [keywordType, setKeywordType] = useState("");
|
||||||
const [keyword, setKeyword] = useState("");
|
const [keyword, setKeyword] = useState("");
|
||||||
const [grpId, setGrpId] = useState("");
|
const [grpId, setGrpId] = useState("");
|
||||||
const [searchParams, setSearchParams] = useState<UserSearchParams>({
|
const [searchParams, setSearchParams] = useState<UserSearchParams>({
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
|||||||
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
DateRangePicker,
|
||||||
Input,
|
Input,
|
||||||
Label,
|
Label,
|
||||||
Select,
|
Select,
|
||||||
@@ -14,14 +15,14 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@common/components";
|
} from "@common/components";
|
||||||
import { columns } from "@/pages/apilog/model/table-columns";
|
import { ApiLogApi } from "@/entities/apilog";
|
||||||
import { ApiLogApi, type ApiLogParams } from "@/entities/apilog";
|
import { type ApiLogParams } from "@/entities/apilog/model/apilog-interface";
|
||||||
import { useDataTable } from "@/shared/lib";
|
import { useDataTable } from "@/shared/lib";
|
||||||
import { ENDPOINT } from "@/shared/config";
|
import { ENDPOINT } from "@/shared/config";
|
||||||
import { InputDateRangePicker } from "@/shared/ui/input-date-range-picker";
|
import { columns } from "@/pages/apilog/model/table-columns";
|
||||||
|
|
||||||
type DateRange = {
|
type DateRange = {
|
||||||
from?: Date | undefined;
|
from: Date | undefined;
|
||||||
to?: Date | undefined;
|
to?: Date | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -35,7 +36,6 @@ function ApiLogPage() {
|
|||||||
pageIndex: 0,
|
pageIndex: 0,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
});
|
});
|
||||||
const [presetDateRange, setPresetDateRange] = useState("");
|
|
||||||
const [dateRange, setDateRange] = useState<DateRange>(defaultDateRange);
|
const [dateRange, setDateRange] = useState<DateRange>(defaultDateRange);
|
||||||
const [userId, setUserId] = useState("");
|
const [userId, setUserId] = useState("");
|
||||||
const [apiHttpPmhd, setApiHttpPmhd] = useState("");
|
const [apiHttpPmhd, setApiHttpPmhd] = useState("");
|
||||||
@@ -85,10 +85,8 @@ function ApiLogPage() {
|
|||||||
...searchParams,
|
...searchParams,
|
||||||
userId,
|
userId,
|
||||||
apiHttpPmhd,
|
apiHttpPmhd,
|
||||||
dmndDtStart: dateRange.from
|
dmndDtStart: dayjs(dateRange.from).format("YYYY-MM-DD"),
|
||||||
? dayjs(dateRange.from).format("YYYY-MM-DD")
|
dmndDtEnd: dayjs(dateRange.to).format("YYYY-MM-DD"),
|
||||||
: "",
|
|
||||||
dmndDtEnd: dateRange.to ? dayjs(dateRange.to).format("YYYY-MM-DD") : "",
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -129,34 +127,31 @@ function ApiLogPage() {
|
|||||||
default:
|
default:
|
||||||
presetDateRange = undefined;
|
presetDateRange = undefined;
|
||||||
}
|
}
|
||||||
setPresetDateRange(value);
|
|
||||||
setDateRange(presetDateRange || defaultDateRange);
|
setDateRange(presetDateRange || defaultDateRange);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDateRangeChange = (dateRange: DateRange) => {
|
|
||||||
setDateRange(dateRange);
|
|
||||||
setPresetDateRange("");
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="card card-border">
|
<div className="card card-border">
|
||||||
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
|
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
|
||||||
<Label className="mr-2">조회기간</Label>
|
<Label className="mr-2">조회기간</Label>
|
||||||
<Select value={presetDateRange} onValueChange={handlePresetDateRange}>
|
<DateRangePicker
|
||||||
<SelectTrigger className="w-[12rem]">
|
value={dateRange}
|
||||||
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
|
onValueChange={(value) => setDateRange(value || defaultDateRange)}
|
||||||
</SelectTrigger>
|
>
|
||||||
<SelectContent>
|
<div className="card card-border flex items-center gap-2">
|
||||||
<SelectItem value="1W">일주일</SelectItem>
|
<Select onValueChange={handlePresetDateRange}>
|
||||||
<SelectItem value="1M">1개월</SelectItem>
|
<SelectTrigger>
|
||||||
<SelectItem value="3M">3개월</SelectItem>
|
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
|
||||||
</SelectContent>
|
</SelectTrigger>
|
||||||
</Select>
|
<SelectContent>
|
||||||
<InputDateRangePicker
|
<SelectItem value="1W">일주일</SelectItem>
|
||||||
dateRange={dateRange}
|
<SelectItem value="1M">1개월</SelectItem>
|
||||||
setDateRange={handleDateRangeChange}
|
<SelectItem value="3M">3개월</SelectItem>
|
||||||
/>
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</DateRangePicker>
|
||||||
<Label className="ml-4 mr-2 text-label">사용자ID</Label>
|
<Label className="ml-4 mr-2 text-label">사용자ID</Label>
|
||||||
<Input
|
<Input
|
||||||
value={userId}
|
value={userId}
|
||||||
|
|||||||
@@ -35,14 +35,9 @@ export const columns: ColumnDef<Post>[] = [
|
|||||||
accessorKey: "pstgPrdSeNm",
|
accessorKey: "pstgPrdSeNm",
|
||||||
header: "게시기간",
|
header: "게시기간",
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const pstgPrdSeCd = row.original.pstgPrdSeCd;
|
|
||||||
const pstgPrdSeNm = row.original.pstgPrdSeNm;
|
const pstgPrdSeNm = row.original.pstgPrdSeNm;
|
||||||
const pstgBgngDt = row.original.pstgBgngDt;
|
const pstgBgngDt = row.original.pstgBgngDt;
|
||||||
const pstgEndDt = row.original.pstgEndDt;
|
const pstgEndDt = row.original.pstgEndDt;
|
||||||
|
|
||||||
if (pstgPrdSeCd === "99") {
|
|
||||||
return pstgPrdSeNm;
|
|
||||||
}
|
|
||||||
return `${pstgPrdSeNm} (${dayjs(pstgBgngDt).format("YYYY-MM-DD")} ~ ${dayjs(pstgEndDt).format("YYYY-MM-DD")})`;
|
return `${pstgPrdSeNm} (${dayjs(pstgBgngDt).format("YYYY-MM-DD")} ~ ${dayjs(pstgEndDt).format("YYYY-MM-DD")})`;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ function CreatePage() {
|
|||||||
<div className="card-header">
|
<div className="card-header">
|
||||||
<h3 className="text-2xl font-medium text-label">콘텐츠 등록</h3>
|
<h3 className="text-2xl font-medium text-label">콘텐츠 등록</h3>
|
||||||
</div>
|
</div>
|
||||||
<ContentsForm isEditable form={form} handleSave={createContents} />
|
<ContentsForm form={form} handleSave={createContents} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||||
@@ -25,7 +24,6 @@ import { useAlertStore, useMutationWrapper } from "@/shared/lib";
|
|||||||
import { ContentsForm } from "./contents-form";
|
import { ContentsForm } from "./contents-form";
|
||||||
|
|
||||||
function ModifyPage({ sequential }: { sequential: string }) {
|
function ModifyPage({ sequential }: { sequential: string }) {
|
||||||
const router = useRouter();
|
|
||||||
const { confirm } = useConfirm();
|
const { confirm } = useConfirm();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const alert = useAlertStore((state) => state.setMessage);
|
const alert = useAlertStore((state) => state.setMessage);
|
||||||
@@ -53,11 +51,9 @@ function ModifyPage({ sequential }: { sequential: string }) {
|
|||||||
mutationFn: (data: ContentsFormData) => ContentsApi.createContents(data),
|
mutationFn: (data: ContentsFormData) => ContentsApi.createContents(data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setContsVer(0);
|
setContsVer(0);
|
||||||
alert("수정되었습니다.", () => {
|
alert("수정되었습니다.");
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS, contsSn],
|
queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS, contsSn],
|
||||||
});
|
|
||||||
router.back();
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -66,7 +62,7 @@ function ModifyPage({ sequential }: { sequential: string }) {
|
|||||||
mutationFn: (data: ContentsFormData) =>
|
mutationFn: (data: ContentsFormData) =>
|
||||||
ContentsApi.updateContents(contsSn, contsVer, data),
|
ContentsApi.updateContents(contsSn, contsVer, data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
alert("수정되었습니다.", router.back);
|
alert("수정되었습니다.");
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -74,11 +70,9 @@ function ModifyPage({ sequential }: { sequential: string }) {
|
|||||||
mutationFn: () => ContentsApi.deleteContents(contsSn, contsVer),
|
mutationFn: () => ContentsApi.deleteContents(contsSn, contsVer),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setContsVer(0);
|
setContsVer(0);
|
||||||
alert("삭제되었습니다.", () => {
|
alert("삭제되었습니다.");
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS, contsSn],
|
queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS, contsSn],
|
||||||
});
|
|
||||||
router.back();
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
import { client } from "@/shared/api";
|
|
||||||
import { ENDPOINT } from "@/shared/config";
|
|
||||||
import type { Dashboard, DashboardService } from "../model/dashboard-interface";
|
|
||||||
|
|
||||||
export class DashboardApiService implements DashboardService {
|
|
||||||
public async getDashboard(): Promise<Dashboard> {
|
|
||||||
const response = await client.get<Dashboard>(
|
|
||||||
ENDPOINT.CMS_SERVICE.DASHBOARD,
|
|
||||||
);
|
|
||||||
return response.data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DashboardApi = new DashboardApiService();
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import type { ChartData, ChartDataset } from "chart.js";
|
|
||||||
import { getCssVariableValue } from "@common/components";
|
|
||||||
import type {
|
|
||||||
MenuStatisticsContent,
|
|
||||||
SiteStatisticsContent,
|
|
||||||
} from "../model/dashboard-interface";
|
|
||||||
|
|
||||||
export function processSiteAccessData(
|
|
||||||
siteAccessList: SiteStatisticsContent[],
|
|
||||||
): ChartData<"line">["datasets"] {
|
|
||||||
const siteGroups: Record<string, ChartDataset> = siteAccessList.reduce(
|
|
||||||
(acc, item) => {
|
|
||||||
if (!acc[item.siteNm]) {
|
|
||||||
acc[item.siteNm] = {
|
|
||||||
label: item.siteNm,
|
|
||||||
data: Array(24).fill(0), // 24시간 데이터 초기화
|
|
||||||
borderColor: getCssVariableValue(
|
|
||||||
`--chart-${Math.floor(Math.random() * 5) + 1}`,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 시간대별 접속 카운트 누적
|
|
||||||
const hourIndex = parseInt(item.accessHour, 10);
|
|
||||||
if (!isNaN(hourIndex) && hourIndex >= 0 && hourIndex < 24) {
|
|
||||||
acc[item.siteNm]!.data[hourIndex] = item.accessCnt;
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{} as Record<string, ChartDataset>,
|
|
||||||
);
|
|
||||||
|
|
||||||
return Object.values(siteGroups) as ChartData<"line">["datasets"];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function processMenuAccessData(
|
|
||||||
menuAccessList: MenuStatisticsContent[],
|
|
||||||
): ChartData<"bar">["datasets"] {
|
|
||||||
const data = menuAccessList.map((item) => item.accessCnt);
|
|
||||||
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
label: "메뉴별 접속 횟수",
|
|
||||||
data,
|
|
||||||
backgroundColor: "rgb(32, 148, 250)",
|
|
||||||
},
|
|
||||||
] as ChartData<"bar">["datasets"];
|
|
||||||
}
|
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
import { type ChartData, type ChartOptions } from "chart.js";
|
|
||||||
|
|
||||||
const commonData = {
|
|
||||||
labels: ["1", "2", "3", "4", "5", "6", "7"],
|
|
||||||
datasets: [
|
|
||||||
{
|
|
||||||
fill: true,
|
|
||||||
pointRadius: 0,
|
|
||||||
tension: 0.5,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const commonOptions = {
|
|
||||||
responsive: true,
|
|
||||||
maintainAspectRatio: false,
|
|
||||||
plugins: {
|
|
||||||
legend: {
|
|
||||||
display: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
scales: {
|
|
||||||
x: {
|
|
||||||
display: false,
|
|
||||||
},
|
|
||||||
y: {
|
|
||||||
display: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const chartData: Record<
|
|
||||||
string,
|
|
||||||
{ data: ChartData<"line" | "bar">; options: ChartOptions<"line" | "bar"> }
|
|
||||||
> = {
|
|
||||||
memberChartData: {
|
|
||||||
data: {
|
|
||||||
...commonData,
|
|
||||||
datasets: [
|
|
||||||
{
|
|
||||||
...commonData.datasets[0],
|
|
||||||
data: [8, 10, 7, 12, 9, 13, 12],
|
|
||||||
borderColor: "rgba(255, 255, 255, 0.6)",
|
|
||||||
backgroundColor: "rgba(255, 255, 255, 0.1)",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
...commonOptions,
|
|
||||||
scales: {
|
|
||||||
...commonOptions.scales,
|
|
||||||
y: {
|
|
||||||
...commonOptions.scales.y,
|
|
||||||
min: 5,
|
|
||||||
max: 15,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
postChartData: {
|
|
||||||
data: {
|
|
||||||
...commonData,
|
|
||||||
datasets: [
|
|
||||||
{
|
|
||||||
...commonData.datasets[0],
|
|
||||||
data: [5200, 5350, 5450, 5520, 5600, 5650, 5678],
|
|
||||||
borderColor: "rgba(86, 138, 255, 1)",
|
|
||||||
backgroundColor: "rgba(86, 138, 255, 0.1)",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
...commonOptions,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
visitorChartData: {
|
|
||||||
data: {
|
|
||||||
...commonData,
|
|
||||||
datasets: [
|
|
||||||
{
|
|
||||||
...commonData.datasets[0],
|
|
||||||
data: [1800, 2100, 1950, 2300, 2150, 2250, 2345],
|
|
||||||
borderColor: "rgba(149, 131, 252)",
|
|
||||||
backgroundColor: "rgba(149, 131, 252, 0.1)",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
...commonOptions,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
apiChartData: {
|
|
||||||
data: {
|
|
||||||
...commonData,
|
|
||||||
datasets: [
|
|
||||||
{
|
|
||||||
...commonData.datasets[0],
|
|
||||||
data: [41000, 42500, 43200, 44100, 44800, 45200, 45678],
|
|
||||||
borderColor: "rgba(220, 103, 206)",
|
|
||||||
backgroundColor: "rgba(220, 103, 206, 0.1)",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
...commonOptions,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
siteAccessChartData: {
|
|
||||||
data: {
|
|
||||||
labels: Array.from({ length: 24 }, (_, i) =>
|
|
||||||
i.toString().padStart(2, "0"),
|
|
||||||
),
|
|
||||||
datasets: [],
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
...commonOptions,
|
|
||||||
plugins: {
|
|
||||||
legend: {
|
|
||||||
position: "top",
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
mode: "index",
|
|
||||||
intersect: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
scales: {
|
|
||||||
x: {
|
|
||||||
title: {
|
|
||||||
text: "시간대",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
y: {
|
|
||||||
title: {
|
|
||||||
text: "접속자 수",
|
|
||||||
},
|
|
||||||
beginAtZero: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
menuAccessChartData: {
|
|
||||||
data: {
|
|
||||||
labels: [],
|
|
||||||
datasets: [],
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
...commonOptions,
|
|
||||||
plugins: {
|
|
||||||
legend: {
|
|
||||||
position: "top",
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
mode: "index",
|
|
||||||
intersect: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
scales: {
|
|
||||||
x: {
|
|
||||||
title: {
|
|
||||||
text: "메뉴명",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
y: {
|
|
||||||
title: {
|
|
||||||
text: "접속 횟수",
|
|
||||||
},
|
|
||||||
beginAtZero: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
export interface SiteStatisticsContent {
|
|
||||||
siteId: string;
|
|
||||||
siteNm: string;
|
|
||||||
accessHour: string;
|
|
||||||
accessCnt: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MenuStatisticsContent {
|
|
||||||
menuCd: number;
|
|
||||||
menuNm: string;
|
|
||||||
accessCnt: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Dashboard {
|
|
||||||
memberTotCnt: number;
|
|
||||||
memberTodayCnt: number;
|
|
||||||
pstTotCnt: number;
|
|
||||||
pstTodayCnt: number;
|
|
||||||
accessTodayCnt: number;
|
|
||||||
accessBefDayPer: number;
|
|
||||||
apiTodayCnt: number;
|
|
||||||
waitMemberCnt: number;
|
|
||||||
waitCntnCnt: number;
|
|
||||||
notiList: [];
|
|
||||||
schdlList: [];
|
|
||||||
siteAccessList: SiteStatisticsContent[];
|
|
||||||
menuAccessList: MenuStatisticsContent[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DashboardService {
|
|
||||||
getDashboard(): Promise<Dashboard>;
|
|
||||||
}
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
import dayjs from "dayjs";
|
|
||||||
import type { ColumnDef } from "@tanstack/react-table";
|
|
||||||
import { Badge } from "@common/components";
|
|
||||||
import type { RecentPost } from "@/entities/post";
|
|
||||||
|
|
||||||
const noticePostTableColumns: ColumnDef<RecentPost>[] = [
|
|
||||||
{
|
|
||||||
header: "번호",
|
|
||||||
cell: ({ row, table }) => {
|
|
||||||
if (row.original.upndFixYn === "Y") {
|
|
||||||
return (
|
|
||||||
<Badge
|
|
||||||
color="orange"
|
|
||||||
className="flex size-8 items-center justify-center"
|
|
||||||
>
|
|
||||||
<i className="diveicon di-volume-up-fill" />
|
|
||||||
</Badge>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const totalRows = table.getRowCount();
|
|
||||||
const currentPage = table.getState().pagination.pageIndex;
|
|
||||||
const pageSize = table.getState().pagination.pageSize;
|
|
||||||
return totalRows - (currentPage * pageSize + row.index);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: "pstTtl",
|
|
||||||
header: "제목",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: "wrtrNm",
|
|
||||||
header: "작성자",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: "regDt",
|
|
||||||
header: "작성일",
|
|
||||||
cell: ({ row }) => dayjs(row.original.regDt).format("YYYY-MM-DD"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: "inqNocs",
|
|
||||||
header: "조회수",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const recentPostTableColumns: ColumnDef<RecentPost>[] = [
|
|
||||||
{
|
|
||||||
header: "번호",
|
|
||||||
cell: ({ row, table }) => {
|
|
||||||
const totalRows = table.getRowCount();
|
|
||||||
const currentPage = table.getState().pagination.pageIndex;
|
|
||||||
const pageSize = table.getState().pagination.pageSize;
|
|
||||||
return totalRows - (currentPage * pageSize + row.index);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: "pstTtl",
|
|
||||||
header: "제목",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: "wrtrNm",
|
|
||||||
header: "작성자",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: "regDt",
|
|
||||||
header: "작성일",
|
|
||||||
cell: ({ row }) => dayjs(row.original.regDt).format("YYYY-MM-DD"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: "inqNocs",
|
|
||||||
header: "조회수",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export { noticePostTableColumns, recentPostTableColumns };
|
|
||||||
@@ -1,433 +1,5 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import { getCoreRowModel } from "@tanstack/react-table";
|
|
||||||
import {
|
|
||||||
Chart as ChartJS,
|
|
||||||
CategoryScale,
|
|
||||||
LinearScale,
|
|
||||||
PointElement,
|
|
||||||
LineElement,
|
|
||||||
BarElement,
|
|
||||||
Filler,
|
|
||||||
Legend,
|
|
||||||
Tooltip,
|
|
||||||
type ChartData,
|
|
||||||
} from "chart.js";
|
|
||||||
import { Bar, Line } from "react-chartjs-2";
|
|
||||||
import { PostApi, type RecentPost } from "@/entities/post";
|
|
||||||
import { ENDPOINT } from "@/shared/config";
|
|
||||||
import { useDataTable } from "@/shared/lib";
|
|
||||||
import { DashboardApi } from "../api/dashboard-service";
|
|
||||||
import { chartData } from "../model/chart-data";
|
|
||||||
import {
|
|
||||||
noticePostTableColumns,
|
|
||||||
recentPostTableColumns,
|
|
||||||
} from "../model/table-columns";
|
|
||||||
import { processMenuAccessData, processSiteAccessData } from "../lib/utils";
|
|
||||||
|
|
||||||
ChartJS.register(
|
|
||||||
CategoryScale,
|
|
||||||
LinearScale,
|
|
||||||
PointElement,
|
|
||||||
LineElement,
|
|
||||||
BarElement,
|
|
||||||
Filler,
|
|
||||||
Legend,
|
|
||||||
Tooltip,
|
|
||||||
);
|
|
||||||
|
|
||||||
function DashboardPage() {
|
function DashboardPage() {
|
||||||
const router = useRouter();
|
return <></>;
|
||||||
const { data } = useQuery({
|
|
||||||
queryKey: [ENDPOINT.CMS_SERVICE.DASHBOARD],
|
|
||||||
queryFn: () => DashboardApi.getDashboard(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const noticePostQuery = useQuery({
|
|
||||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS],
|
|
||||||
queryFn: () =>
|
|
||||||
PostApi.getRecentPostList({
|
|
||||||
page: 0,
|
|
||||||
size: 5,
|
|
||||||
keywordType: "bbsTypeCd",
|
|
||||||
keyword: "BBSNOTICE",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const recentPostQuery = useQuery({
|
|
||||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS],
|
|
||||||
queryFn: () =>
|
|
||||||
PostApi.getRecentPostList({
|
|
||||||
page: 0,
|
|
||||||
size: 5,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleNoticePostRowClick = (row: RecentPost) => {
|
|
||||||
router.push(`/bbs/bbsnotice/${row.bbsId}/${row.pstNo}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRecentPostRowClick = (row: RecentPost) => {
|
|
||||||
router.push(
|
|
||||||
`/bbs/${row.bbsTypeCd.toLowerCase()}/${row.bbsId}/${row.pstNo}`,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const { tableComponent: noticePostTableComponent } = useDataTable({
|
|
||||||
query: noticePostQuery,
|
|
||||||
tableOptions: {
|
|
||||||
columns: noticePostTableColumns,
|
|
||||||
getCoreRowModel: getCoreRowModel(),
|
|
||||||
},
|
|
||||||
dataTableOptions: {
|
|
||||||
variant: "line",
|
|
||||||
onRowClick: handleNoticePostRowClick,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { tableComponent: recentPostTableComponent } = useDataTable({
|
|
||||||
query: recentPostQuery,
|
|
||||||
tableOptions: {
|
|
||||||
columns: recentPostTableColumns,
|
|
||||||
getCoreRowModel: getCoreRowModel(),
|
|
||||||
},
|
|
||||||
dataTableOptions: {
|
|
||||||
variant: "line",
|
|
||||||
onRowClick: handleRecentPostRowClick,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="p-6">
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
{/* 첫번째 라인 */}
|
|
||||||
<div className="grid grid-cols-4 gap-1">
|
|
||||||
{/* 총 회원수 카드 */}
|
|
||||||
<div className="card card-border h-80 !bg-primary !p-0">
|
|
||||||
<div className="relative h-full">
|
|
||||||
<div className="absolute left-0 top-0 p-6 tracking-tighter text-white">
|
|
||||||
<h3 className="text-2xl font-medium">총 회원수</h3>
|
|
||||||
<div className="mt-2 text-6xl font-bold">
|
|
||||||
{data?.memberTotCnt
|
|
||||||
? data.memberTotCnt.toLocaleString()
|
|
||||||
: "-"}
|
|
||||||
</div>
|
|
||||||
<p className="mt-1 text-2xl opacity-80">
|
|
||||||
금일 신규 가입자
|
|
||||||
{data?.memberTodayCnt
|
|
||||||
? data.memberTodayCnt.toLocaleString()
|
|
||||||
: "-"}
|
|
||||||
명
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{/* 차트 영역 */}
|
|
||||||
<div className="h-full">
|
|
||||||
<Line
|
|
||||||
data={chartData.memberChartData!.data as ChartData<"line">}
|
|
||||||
options={chartData.memberChartData!.options}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* 전체 게시물 카드 */}
|
|
||||||
<div className="card card-border h-80 !p-0">
|
|
||||||
<div className="relative h-full">
|
|
||||||
<div className="absolute left-0 top-0 p-6 tracking-tighter">
|
|
||||||
<h3 className="text-2xl font-medium">전체 게시물</h3>
|
|
||||||
<div className="mt-2 text-6xl font-bold">
|
|
||||||
{data?.pstTotCnt ? data.pstTotCnt.toLocaleString() : "-"}
|
|
||||||
</div>
|
|
||||||
<p className="mt-1 text-2xl opacity-80">
|
|
||||||
금일 +
|
|
||||||
{data?.pstTodayCnt ? data.pstTodayCnt.toLocaleString() : "-"}{" "}
|
|
||||||
등록
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{/* 차트 영역 */}
|
|
||||||
<div className="h-full">
|
|
||||||
<Line
|
|
||||||
data={chartData.postChartData!.data as ChartData<"line">}
|
|
||||||
options={chartData.postChartData!.options}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* 오늘 접속자 수 카드 */}
|
|
||||||
<div className="card card-border h-80 !p-0">
|
|
||||||
<div className="relative h-full">
|
|
||||||
<div className="absolute left-0 top-0 p-6 tracking-tighter">
|
|
||||||
<h3 className="text-2xl font-medium">오늘 접속자 수</h3>
|
|
||||||
<div className="mt-2 text-6xl font-bold">
|
|
||||||
{data?.accessTodayCnt
|
|
||||||
? data.accessTodayCnt.toLocaleString()
|
|
||||||
: "-"}
|
|
||||||
</div>
|
|
||||||
<p className="mt-1 text-2xl opacity-80">
|
|
||||||
전일 대비 +
|
|
||||||
{data?.accessBefDayPer
|
|
||||||
? data.accessBefDayPer.toLocaleString()
|
|
||||||
: "-"}
|
|
||||||
%
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{/* 차트 영역 */}
|
|
||||||
<div className="h-full">
|
|
||||||
<Line
|
|
||||||
data={chartData.visitorChartData!.data as ChartData<"line">}
|
|
||||||
options={chartData.visitorChartData!.options}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* API 호출 건수 카드 */}
|
|
||||||
<div className="card card-border h-80 !p-0">
|
|
||||||
<div className="relative h-full">
|
|
||||||
<div className="absolute left-0 top-0 p-6 tracking-tighter">
|
|
||||||
<h3 className="text-2xl font-medium">API 호출 건수</h3>
|
|
||||||
<div className="mt-2 text-6xl font-bold">
|
|
||||||
{data?.apiTodayCnt ? data.apiTodayCnt.toLocaleString() : "-"}
|
|
||||||
</div>
|
|
||||||
<p className="mt-1 text-2xl opacity-80">금일 누적</p>
|
|
||||||
</div>
|
|
||||||
{/* 차트 영역 */}
|
|
||||||
<div className="h-full">
|
|
||||||
<Line
|
|
||||||
data={chartData.apiChartData!.data as ChartData<"line">}
|
|
||||||
options={chartData.apiChartData!.options}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 두번째 라인 */}
|
|
||||||
<div className="grid grid-cols-2 gap-1">
|
|
||||||
{/* 승인 대기 알림 */}
|
|
||||||
<div className="card card-border">
|
|
||||||
<div className="card-header">
|
|
||||||
<h3 className="text-2xl font-medium">승인 대기 알림</h3>
|
|
||||||
</div>
|
|
||||||
<div className="p-4">
|
|
||||||
<div className="flex gap-4">
|
|
||||||
{/* 사용자 승인 */}
|
|
||||||
<div className="flex flex-1 flex-col items-start rounded-md bg-accent p-6">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<div className="mr-4 flex h-10 w-10 items-center justify-center rounded-lg bg-red-100">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
className="text-red-500"
|
|
||||||
>
|
|
||||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
|
|
||||||
<circle cx="12" cy="7" r="4"></circle>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<span className="text-2xl font-medium">사용자 승인</span>
|
|
||||||
</div>
|
|
||||||
<div className="mt-5">
|
|
||||||
<div className="text-5xl font-bold">
|
|
||||||
{data?.waitMemberCnt
|
|
||||||
? data.waitMemberCnt.toLocaleString()
|
|
||||||
: "0"}
|
|
||||||
건
|
|
||||||
</div>
|
|
||||||
<div className="mt-2 text-2xl text-gray-500">
|
|
||||||
신규 가입 승인 대기
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* 매뉴 승인 */}
|
|
||||||
<div className="flex flex-1 flex-col items-start rounded-md bg-accent p-6">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<div className="mr-4 flex h-10 w-10 items-center justify-center rounded-lg bg-blue-100">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
className="text-blue-500"
|
|
||||||
>
|
|
||||||
<rect x="3" y="3" width="7" height="7"></rect>
|
|
||||||
<rect x="14" y="3" width="7" height="7"></rect>
|
|
||||||
<rect x="14" y="14" width="7" height="7"></rect>
|
|
||||||
<rect x="3" y="14" width="7" height="7"></rect>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<span className="text-2xl font-medium">메뉴 승인</span>
|
|
||||||
</div>
|
|
||||||
<div className="mt-5">
|
|
||||||
<div className="text-5xl font-bold">
|
|
||||||
{(3).toLocaleString()}건
|
|
||||||
</div>
|
|
||||||
<div className="mt-2 text-2xl text-gray-500">
|
|
||||||
매뉴 수정 승인 대기
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* 컨텐츠 승인 */}
|
|
||||||
<div className="flex flex-1 flex-col items-start rounded-md bg-accent p-6">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<div className="mr-4 flex h-10 w-10 items-center justify-center rounded-lg bg-yellow-100">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
className="text-yellow-500"
|
|
||||||
>
|
|
||||||
<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"></path>
|
|
||||||
<polyline points="14 2 14 8 20 8"></polyline>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<span className="text-2xl font-medium">컨텐츠 승인</span>
|
|
||||||
</div>
|
|
||||||
<div className="mt-5">
|
|
||||||
<div className="text-5xl font-bold">
|
|
||||||
{data?.waitCntnCnt
|
|
||||||
? data.waitCntnCnt.toLocaleString()
|
|
||||||
: "0"}
|
|
||||||
건
|
|
||||||
</div>
|
|
||||||
<div className="mt-2 text-2xl text-gray-500">
|
|
||||||
컨텐츠 수정 승인 대기
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-1">
|
|
||||||
{/* 실시간 알림 */}
|
|
||||||
<div className="card card-border">
|
|
||||||
<div className="card-header">
|
|
||||||
<h3 className="text-2xl font-medium">실시간 알림</h3>
|
|
||||||
</div>
|
|
||||||
<div className="p-4">
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="flex items-center justify-between border-b border-border pb-4">
|
|
||||||
<span className="font-medium">신규 사용자 가입</span>
|
|
||||||
<span>{(1).toLocaleString()}분 전</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between border-b border-border pb-4">
|
|
||||||
<span className="font-medium">메뉴 수정 요청</span>
|
|
||||||
<span>{(5).toLocaleString()}분 전</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between border-b border-border pb-4">
|
|
||||||
<span className="font-medium">게시물 신고</span>
|
|
||||||
<span>{(10).toLocaleString()}분 전</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="font-medium">컨텐츠 승인 요청</span>
|
|
||||||
<span>{(15).toLocaleString()}분 전</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* 일정 알림 */}
|
|
||||||
<div className="card card-border">
|
|
||||||
<div className="card-header">
|
|
||||||
<h3 className="text-2xl font-medium">일정 알림</h3>
|
|
||||||
</div>
|
|
||||||
<div className="p-4">
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="flex items-center justify-between border-b border-border pb-4">
|
|
||||||
<span className="font-medium">
|
|
||||||
{(3).toLocaleString()}월 정기 시스템 점검
|
|
||||||
</span>
|
|
||||||
<span>내일</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between border-b border-border pb-4">
|
|
||||||
<span className="font-medium">서버 업데이트 예정</span>
|
|
||||||
<span>{(3).toLocaleString()}일 후</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between border-b border-border pb-4">
|
|
||||||
<span className="font-medium">백업 스케줄</span>
|
|
||||||
<span>{(5).toLocaleString()}일 후</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="font-medium">정기 보안 점검</span>
|
|
||||||
<span>{(7).toLocaleString()}일 후</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 세번째 라인 */}
|
|
||||||
<div className="grid grid-cols-2 gap-1">
|
|
||||||
{/* 접속자 통계 */}
|
|
||||||
<div className="card card-border">
|
|
||||||
<div className="card-header">
|
|
||||||
<h3 className="text-2xl font-medium">접속자 통계</h3>
|
|
||||||
</div>
|
|
||||||
<div className="h-80 p-4">
|
|
||||||
<Line
|
|
||||||
data={{
|
|
||||||
...chartData.siteAccessChartData!.data,
|
|
||||||
datasets: processSiteAccessData(data?.siteAccessList ?? []),
|
|
||||||
}}
|
|
||||||
options={chartData.siteAccessChartData!.options}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* 메뉴별 접속 통계 */}
|
|
||||||
<div className="card card-border">
|
|
||||||
<div className="card-header">
|
|
||||||
<h3 className="text-2xl font-medium">메뉴별 접속 통계</h3>
|
|
||||||
</div>
|
|
||||||
<div className="h-80 p-4">
|
|
||||||
<Bar
|
|
||||||
data={{
|
|
||||||
labels:
|
|
||||||
data?.menuAccessList?.map((item) => item.menuNm) || [],
|
|
||||||
datasets: processMenuAccessData(data?.menuAccessList || []),
|
|
||||||
}}
|
|
||||||
options={chartData.menuAccessChartData!.options}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 네번째 라인 */}
|
|
||||||
<div className="grid grid-cols-2 gap-1">
|
|
||||||
{/* 공지사항 */}
|
|
||||||
<div className="card card-border !p-0">
|
|
||||||
<div className="card-header !m-0 !border-none">
|
|
||||||
<h3 className="text-2xl font-medium">공지사항</h3>
|
|
||||||
</div>
|
|
||||||
{noticePostTableComponent}
|
|
||||||
</div>
|
|
||||||
{/* 신규 게시글 */}
|
|
||||||
<div className="card card-border !p-0">
|
|
||||||
<div className="card-header !m-0 !border-none">
|
|
||||||
<h3 className="text-2xl font-medium">신규 게시글</h3>
|
|
||||||
</div>
|
|
||||||
{recentPostTableComponent}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export { DashboardPage };
|
export { DashboardPage };
|
||||||
|
|||||||
@@ -39,7 +39,8 @@ function FilePage() {
|
|||||||
mutationFn: (atflId: string) =>
|
mutationFn: (atflId: string) =>
|
||||||
AttachmentApi.deleteAttachmentByAtflId(atflId),
|
AttachmentApi.deleteAttachmentByAtflId(atflId),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
alert("삭제되었습니다.", query.refetch);
|
query.refetch();
|
||||||
|
alert("삭제되었습니다.");
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState } from "react";
|
||||||
import {
|
import {
|
||||||
keepPreviousData,
|
keepPreviousData,
|
||||||
useQuery,
|
useQuery,
|
||||||
@@ -22,7 +22,6 @@ import {
|
|||||||
DialogFooter,
|
DialogFooter,
|
||||||
Input,
|
Input,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
Label,
|
|
||||||
} from "@common/components";
|
} from "@common/components";
|
||||||
import {
|
import {
|
||||||
UserApi,
|
UserApi,
|
||||||
@@ -42,8 +41,7 @@ function AddUserDialog({ groupId }: AdditionalUserDialogProps) {
|
|||||||
const alert = useAlertStore((state) => state.setMessage);
|
const alert = useAlertStore((state) => state.setMessage);
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [userName, setUserName] = useState("");
|
const [userSearchQuery, setUserSearchQuery] = useState<UserSearchParams>({
|
||||||
const [searchParam, setSearchParam] = useState<UserSearchParams>({
|
|
||||||
userName: "",
|
userName: "",
|
||||||
});
|
});
|
||||||
const [pagination, setPagination] = useState<PaginationState>({
|
const [pagination, setPagination] = useState<PaginationState>({
|
||||||
@@ -55,10 +53,10 @@ function AddUserDialog({ groupId }: AdditionalUserDialogProps) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const query = useQuery({
|
const query = useQuery({
|
||||||
queryKey: [ENDPOINT.USER_SERVICE.USERS, searchParam, pagination],
|
queryKey: [ENDPOINT.USER_SERVICE.USERS, userSearchQuery, pagination],
|
||||||
queryFn: () =>
|
queryFn: () =>
|
||||||
UserApi.findPageUser({
|
UserApi.findPageUser({
|
||||||
...searchParam,
|
...userSearchQuery,
|
||||||
page: pagination.pageIndex,
|
page: pagination.pageIndex,
|
||||||
size: pagination.pageSize,
|
size: pagination.pageSize,
|
||||||
}),
|
}),
|
||||||
@@ -120,9 +118,6 @@ function AddUserDialog({ groupId }: AdditionalUserDialogProps) {
|
|||||||
onRowSelectionChange: setUserRowSelection,
|
onRowSelectionChange: setUserRowSelection,
|
||||||
getRowId: (row) => row.userUnqId,
|
getRowId: (row) => row.userUnqId,
|
||||||
},
|
},
|
||||||
dataTableOptions: {
|
|
||||||
viewPagination: true,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleAddUsers = async () => {
|
const handleAddUsers = async () => {
|
||||||
@@ -149,30 +144,10 @@ function AddUserDialog({ groupId }: AdditionalUserDialogProps) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSearch = (e: React.FormEvent<HTMLFormElement>) => {
|
const handleSearch = () => {
|
||||||
e.preventDefault();
|
setPagination({ pageIndex: 0, pageSize: 10 });
|
||||||
e.stopPropagation();
|
|
||||||
setPagination({
|
|
||||||
...pagination,
|
|
||||||
pageIndex: 0,
|
|
||||||
});
|
|
||||||
setSearchParam({ userName });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetSearch = () => {
|
|
||||||
setPagination({
|
|
||||||
pageIndex: 0,
|
|
||||||
pageSize: 10,
|
|
||||||
});
|
|
||||||
setUserName("");
|
|
||||||
setSearchParam({ userName: "" });
|
|
||||||
setUserRowSelection({});
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (open) resetSearch();
|
|
||||||
}, [open]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
@@ -182,31 +157,20 @@ function AddUserDialog({ groupId }: AdditionalUserDialogProps) {
|
|||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>사용자 추가</DialogTitle>
|
<DialogTitle>사용자 추가</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<form onSubmit={handleSearch} className="mb-4 flex items-end gap-2">
|
<div className="mb-4 flex items-end gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<Input
|
||||||
<Label htmlFor="userName" className="mr-2 text-label">
|
value={userSearchQuery.userName}
|
||||||
사용자 이름
|
onChange={(e) =>
|
||||||
</Label>
|
setUserSearchQuery({
|
||||||
<Input
|
...userSearchQuery,
|
||||||
id="userName"
|
userName: e.target.value,
|
||||||
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
|
})
|
||||||
value={userName}
|
}
|
||||||
onChange={(e) => setUserName(e.target.value)}
|
className="!text-[1.3rem]"
|
||||||
placeholder="사용자 이름으로 검색"
|
placeholder="사용자 이름으로 검색"
|
||||||
/>
|
/>
|
||||||
</div>
|
<Button onClick={handleSearch}>검색</Button>
|
||||||
<Button className="h-[3.2rem]" type="submit">
|
</div>
|
||||||
조회
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
color="white"
|
|
||||||
className="h-[3.2rem]"
|
|
||||||
onClick={resetSearch}
|
|
||||||
>
|
|
||||||
초기화
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
{tableComponent}
|
{tableComponent}
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button color="white" onClick={() => setOpen(false)}>
|
<Button color="white" onClick={() => setOpen(false)}>
|
||||||
|
|||||||
@@ -161,11 +161,11 @@ function GroupForm({ form }: GroupFormProps) {
|
|||||||
>
|
>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<RadioGroupItem value="Y" id="mngrY" />
|
<RadioGroupItem value="Y" id="mngrY" />
|
||||||
<Label htmlFor="mngrY">예</Label>
|
<Label htmlFor="mngrY">Y</Label>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<RadioGroupItem value="N" id="mngrN" />
|
<RadioGroupItem value="N" id="mngrN" />
|
||||||
<Label htmlFor="mngrN">아니오</Label>
|
<Label htmlFor="mngrN">N</Label>
|
||||||
</div>
|
</div>
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { memo, useEffect, useState } from "react";
|
import { memo, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||||
@@ -43,7 +43,7 @@ const GroupsDialog = memo(
|
|||||||
const { data: group } = useQuery({
|
const { data: group } = useQuery({
|
||||||
queryKey: [ENDPOINT.CMS_SERVICE.GROUPS, groupId],
|
queryKey: [ENDPOINT.CMS_SERVICE.GROUPS, groupId],
|
||||||
queryFn: () => GroupApi.findGroupById(groupId!),
|
queryFn: () => GroupApi.findGroupById(groupId!),
|
||||||
enabled: !!groupId && open, // open 시점 groupId가 있을 때만 상세 조회 API 호출
|
enabled: !!groupId && open, // open 시점 groupId가 있을 때만 상세 조회 API 호출
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutateAsync: createGroup } = useMutationWrapper({
|
const { mutateAsync: createGroup } = useMutationWrapper({
|
||||||
@@ -77,7 +77,7 @@ const GroupsDialog = memo(
|
|||||||
delYn: group?.delYn ?? "",
|
delYn: group?.delYn ?? "",
|
||||||
authrtId: group?.authrtId ?? "",
|
authrtId: group?.authrtId ?? "",
|
||||||
siteId: group?.siteId ?? "",
|
siteId: group?.siteId ?? "",
|
||||||
mngrYn: group?.mngrYn ?? "Y",
|
mngrYn: group?.mngrYn ?? "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -91,10 +91,6 @@ const GroupsDialog = memo(
|
|||||||
if (await confirm(confirmMessage)) callback(data);
|
if (await confirm(confirmMessage)) callback(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (open) form.reset();
|
|
||||||
}, [open]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen} {...props}>
|
<Dialog open={open} onOpenChange={setOpen} {...props}>
|
||||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||||
|
|||||||
@@ -35,14 +35,9 @@ export const columns: ColumnDef<Post>[] = [
|
|||||||
accessorKey: "pstgPrdSeNm",
|
accessorKey: "pstgPrdSeNm",
|
||||||
header: "게시기간",
|
header: "게시기간",
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const pstgPrdSeCd = row.original.pstgPrdSeCd;
|
|
||||||
const pstgPrdSeNm = row.original.pstgPrdSeNm;
|
const pstgPrdSeNm = row.original.pstgPrdSeNm;
|
||||||
const pstgBgngDt = row.original.pstgBgngDt;
|
const pstgBgngDt = row.original.pstgBgngDt;
|
||||||
const pstgEndDt = row.original.pstgEndDt;
|
const pstgEndDt = row.original.pstgEndDt;
|
||||||
|
|
||||||
if (pstgPrdSeCd === "99") {
|
|
||||||
return pstgPrdSeNm;
|
|
||||||
}
|
|
||||||
return `${pstgPrdSeNm} (${dayjs(pstgBgngDt).format("YYYY-MM-DD")} ~ ${dayjs(pstgEndDt).format("YYYY-MM-DD")})`;
|
return `${pstgPrdSeNm} (${dayjs(pstgBgngDt).format("YYYY-MM-DD")} ~ ${dayjs(pstgEndDt).format("YYYY-MM-DD")})`;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ function CreatePage() {
|
|||||||
resolver: zodResolver(imageFormSchema),
|
resolver: zodResolver(imageFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
atflYn: "Y",
|
atflYn: "Y",
|
||||||
useYn: "Y",
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -124,7 +124,6 @@ function ImageForm({ form, imgSn, ...props }: ImageFormProps) {
|
|||||||
{...field}
|
{...field}
|
||||||
type="number"
|
type="number"
|
||||||
min={0}
|
min={0}
|
||||||
max={9999}
|
|
||||||
value={field.value ?? ""}
|
value={field.value ?? ""}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
field.onChange(parseInt(e.target.value))
|
field.onChange(parseInt(e.target.value))
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
export { ModifyLinkPage } from "./ui/modify-page";
|
|
||||||
export { RegistLinkPage } from "./ui/regist-page";
|
|
||||||
export { LinkPage } from "./ui/link-page";
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
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 };
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,590 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
"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 };
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
"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 };
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
"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 };
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { LinkLogPage } from './ui/link-log-page';
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
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 };
|
|
||||||
@@ -1,189 +0,0 @@
|
|||||||
"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 };
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { LinkParamsPage } from './ui/link-params-page';
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
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 };
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
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 };
|
|
||||||
@@ -1,351 +0,0 @@
|
|||||||
'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 };
|
|
||||||
@@ -1,405 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState, useMemo } from "react";
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
DataTable,
|
|
||||||
Input,
|
|
||||||
Label,
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableRow,
|
|
||||||
LoadingWithAnimation,
|
|
||||||
} from "@common/components";
|
|
||||||
import { getCoreRowModel, useReactTable, type PaginationState, type RowSelectionState, } from "@tanstack/react-table";
|
|
||||||
import { LinkApi, type Link, type LinkSearchParam, type LinkVrblFormData, linkVrblFormSchema } from "@/entities/link";
|
|
||||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
||||||
import { columns } from "../model/table-columns";
|
|
||||||
import { vrblColumns } from "../model/vrbl-table-columns";
|
|
||||||
import { type LinkVrbl } from "@/entities/link";
|
|
||||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
|
||||||
import { LinkParamsDialog } from "./link-params-dialog";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
|
|
||||||
import { AUTH_USER_ID } from "@/shared/config";
|
|
||||||
import { getCookie } from "@common/components";
|
|
||||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
|
||||||
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
|
||||||
import { useDataTable } from "@/shared/lib";
|
|
||||||
|
|
||||||
function LinkParamsPage() {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
const [linkSn, setLinkSn] = useState<string>('');
|
|
||||||
const [linkNm, setLinkNm] = useState<string>('');
|
|
||||||
|
|
||||||
const [linkSearchParam, setLinkSearchParam] = useState<LinkSearchParam>({
|
|
||||||
linkSn: '',
|
|
||||||
linkNm: ''
|
|
||||||
});
|
|
||||||
|
|
||||||
const [selectedLink, setSelectedLink] = useState<{
|
|
||||||
linkSn: string;
|
|
||||||
linkNm: string;
|
|
||||||
}>({
|
|
||||||
linkSn: '',
|
|
||||||
linkNm: ''
|
|
||||||
});
|
|
||||||
|
|
||||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
|
||||||
|
|
||||||
const [pagination, setPagination] = useState<PaginationState>({
|
|
||||||
pageIndex: 0,
|
|
||||||
pageSize: 100,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { confirm } = useConfirm();
|
|
||||||
|
|
||||||
const [selectedVrblIds, setSelectedVrblIds] = useState<string[]>([]);
|
|
||||||
const [isDBdata, setIsDBData] = useState<boolean>(false);
|
|
||||||
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
|
|
||||||
const [onConfirm, setOnConfirm] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const userUniqId = getCookie(AUTH_USER_ID) ?? "";
|
|
||||||
const { setMessage: alert } = useAlertStore((state) => state);
|
|
||||||
|
|
||||||
const dbIdList = ['link0001', 'link0002'];
|
|
||||||
|
|
||||||
// Link 목록 쿼리
|
|
||||||
const query = useQuery({
|
|
||||||
queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkList`, linkSearchParam],
|
|
||||||
queryFn: () => LinkApi.getLinkList({
|
|
||||||
...linkSearchParam,
|
|
||||||
page: pagination.pageIndex,
|
|
||||||
size: pagination.pageSize,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
// LinkVrbl 목록 쿼리 - 별도 관리
|
|
||||||
const linkVrblQuery = useQuery({
|
|
||||||
queryKey: ['linkVrblList', selectedLink.linkSn],
|
|
||||||
queryFn: () => LinkApi.getLinkVrblList(selectedLink.linkSn),
|
|
||||||
enabled: !!selectedLink.linkSn && !dbIdList.includes(selectedLink.linkSn),
|
|
||||||
staleTime: 0, // 항상 최신 데이터를 가져오도록 설정
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleResetSearchParam = () => {
|
|
||||||
setSelectedLink({
|
|
||||||
linkSn: '',
|
|
||||||
linkNm: ''
|
|
||||||
});
|
|
||||||
|
|
||||||
setLinkSn('');
|
|
||||||
setLinkNm('');
|
|
||||||
setLinkSearchParam({
|
|
||||||
...linkSearchParam,
|
|
||||||
linkSn: '',
|
|
||||||
linkNm: ''
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleConfirm = async (callback: () => Promise<void>) => {
|
|
||||||
setOnConfirm(true);
|
|
||||||
await callback();
|
|
||||||
setOnConfirm(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const { mutateAsync: insertLinkVrbl } = useMutation({
|
|
||||||
mutationFn: async (data: LinkVrblFormData) =>
|
|
||||||
await LinkApi.insertLinkVrbl({
|
|
||||||
...data,
|
|
||||||
link_sn: selectedLink.linkSn,
|
|
||||||
mdfr: userUniqId,
|
|
||||||
}),
|
|
||||||
onSuccess: async () => {
|
|
||||||
// 쿼리 무효화를 통한 최신 데이터 가져오기
|
|
||||||
await queryClient.invalidateQueries({
|
|
||||||
queryKey: ['linkVrblList', selectedLink.linkSn]
|
|
||||||
});
|
|
||||||
setDialogOpen(false);
|
|
||||||
alert('저장되었습니다.');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const { mutateAsync: updateLinkVrbl } = useMutation({
|
|
||||||
mutationFn: async (data: LinkVrblFormData) => await LinkApi.updateLinkVrblInfo({
|
|
||||||
...data,
|
|
||||||
link_sn: selectedLink.linkSn,
|
|
||||||
mdfr: userUniqId,
|
|
||||||
}),
|
|
||||||
onSuccess: async () => {
|
|
||||||
await queryClient.invalidateQueries({
|
|
||||||
queryKey: ['linkVrblList', selectedLink.linkSn]
|
|
||||||
});
|
|
||||||
setDialogOpen(false);
|
|
||||||
alert('저장되었습니다.');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const { mutateAsync: deleteLinkVrbl } = useMutation({
|
|
||||||
mutationFn: async (data: string[]) => await LinkApi.deleteLinkVrbl(data),
|
|
||||||
onSuccess: async () => {
|
|
||||||
await queryClient.invalidateQueries({
|
|
||||||
queryKey: ['linkVrblList', selectedLink.linkSn]
|
|
||||||
});
|
|
||||||
setSelectedVrblIds([]);
|
|
||||||
alert('삭제되었습니다.');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// linkVrblQuery의 데이터를 사용
|
|
||||||
const tableData = useMemo(() => {
|
|
||||||
return linkVrblQuery.data?.content ?? [];
|
|
||||||
}, [linkVrblQuery.data]);
|
|
||||||
|
|
||||||
const vrblCols = useMemo(
|
|
||||||
() =>
|
|
||||||
vrblColumns({
|
|
||||||
selectedVrblIds,
|
|
||||||
setSelectedVrblIds,
|
|
||||||
}),
|
|
||||||
[selectedVrblIds]
|
|
||||||
);
|
|
||||||
|
|
||||||
const vrblTable = useReactTable({
|
|
||||||
data: tableData,
|
|
||||||
columns: vrblCols,
|
|
||||||
getCoreRowModel: getCoreRowModel(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const form = useForm<LinkVrblFormData>({
|
|
||||||
resolver: zodResolver(linkVrblFormSchema),
|
|
||||||
defaultValues: {},
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleRowClick = async (link: Link) => {
|
|
||||||
setSelectedLink({
|
|
||||||
linkSn: link.link_sn,
|
|
||||||
linkNm: link.link_nm,
|
|
||||||
});
|
|
||||||
|
|
||||||
setSelectedVrblIds([]);
|
|
||||||
|
|
||||||
if (dbIdList.includes(link.link_clsf_cd)) {
|
|
||||||
setIsDBData(true);
|
|
||||||
} else {
|
|
||||||
setIsDBData(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
setSelectedLink({
|
|
||||||
linkSn: '',
|
|
||||||
linkNm: ''
|
|
||||||
});
|
|
||||||
|
|
||||||
setLinkSearchParam({
|
|
||||||
...linkSearchParam,
|
|
||||||
linkSn,
|
|
||||||
linkNm
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchUserInfo = async () => {
|
|
||||||
const userData = await getUserInfo();
|
|
||||||
return userData.userName;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteParam = async () => {
|
|
||||||
if (selectedVrblIds.length === 0) {
|
|
||||||
alert('삭제할 파라미터를 선택해 주세요.');
|
|
||||||
} else {
|
|
||||||
await handleConfirm(async () => {
|
|
||||||
if (await confirm('선택한 파라미터 정보를 삭제하시겠습니까?')) {
|
|
||||||
deleteLinkVrbl(selectedVrblIds);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleVrblRowClick = async (data: LinkVrbl) => {
|
|
||||||
const linkVrblInfo = await LinkApi.getLinkVrblInfo(data.link_vrbl_sn);
|
|
||||||
|
|
||||||
if (linkVrblInfo) {
|
|
||||||
let newData = { ...linkVrblInfo };
|
|
||||||
|
|
||||||
if (linkVrblInfo.mdfr) {
|
|
||||||
const userName = await fetchUserInfo();
|
|
||||||
newData = {
|
|
||||||
...linkVrblInfo,
|
|
||||||
mdfr: userName,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
form.reset(newData);
|
|
||||||
}
|
|
||||||
setDialogOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const { tableComponent } = useDataTable({
|
|
||||||
query,
|
|
||||||
tableOptions: {
|
|
||||||
columns,
|
|
||||||
state: {
|
|
||||||
pagination,
|
|
||||||
rowSelection,
|
|
||||||
},
|
|
||||||
manualPagination: true,
|
|
||||||
enableMultiRowSelection: true,
|
|
||||||
getCoreRowModel: getCoreRowModel(),
|
|
||||||
onPaginationChange: setPagination,
|
|
||||||
onRowSelectionChange: setRowSelection,
|
|
||||||
},
|
|
||||||
dataTableOptions: {
|
|
||||||
onRowClick: handleRowClick,
|
|
||||||
viewPagination: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// LinkVrbl 데이터 표시 로직
|
|
||||||
const renderLinkVrblContent = () => {
|
|
||||||
if (isDBdata) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-center mt-14 text-gray-500 text-3xl flex-1">
|
|
||||||
변수를 지원하지 않는 연계 데이터입니다.
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!selectedLink.linkSn) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-center mt-14 text-gray-500 text-3xl flex-1">
|
|
||||||
연계 데이터를 선택해주세요.
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (linkVrblQuery.isLoading) {
|
|
||||||
return (
|
|
||||||
<div className="[data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center h-full">
|
|
||||||
<div className="relative size-[5rem]">
|
|
||||||
<LoadingWithAnimation color="primary" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="mb-2 mt-14 flex justify-end items-center flex-none">
|
|
||||||
{tableData.length > 0 && (
|
|
||||||
<Button color="red" className="h-[3.2rem] mr-2" type="button" onClick={handleDeleteParam}>
|
|
||||||
행 삭제
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<LinkParamsDialog
|
|
||||||
selectedLink={selectedLink}
|
|
||||||
form={form}
|
|
||||||
insertLinkVrbl={insertLinkVrbl}
|
|
||||||
dialogOpen={dialogOpen}
|
|
||||||
onOpenChange={setDialogOpen}
|
|
||||||
updateLinkVrbl={updateLinkVrbl}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{tableData.length > 0 ? (
|
|
||||||
<div className="overflow-auto flex-1 min-h-[300px]">
|
|
||||||
<DataTable table={vrblTable} onRowClick={handleVrblRowClick} />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex items-center justify-center h-20 text-gray-500 text-3xl">
|
|
||||||
등록된 연계 변수가 없습니다.
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="card card-border !mt-6">
|
|
||||||
<div className="card card-border">
|
|
||||||
<form className="flex items-center gap-2 p-2" onSubmit={handleSubmit}>
|
|
||||||
<Label htmlFor="liSnSearchInput" className="mr-2 text-label">
|
|
||||||
일련번호
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem] mr-4"
|
|
||||||
id="liSnSearchInput"
|
|
||||||
value={linkSn}
|
|
||||||
onChange={(e) => setLinkSn(e.target.value)}
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
<Label htmlFor="liNmSearchInput" className="mr-2 text-label">
|
|
||||||
연계 명
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem] mr-2"
|
|
||||||
id="liNmSearchInput"
|
|
||||||
value={linkNm}
|
|
||||||
onChange={(e) => setLinkNm(e.target.value)}
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
<Button color="green" className="h-[3.2rem]" type="button" onClick={handleResetSearchParam}>
|
|
||||||
초기화
|
|
||||||
</Button>
|
|
||||||
<Button className="h-[3.2rem]" type="submit">
|
|
||||||
조회
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div className="card !mb-0 flex h-[calc(100vh-28rem)] gap-5 !p-0">
|
|
||||||
<div className="card card-border w-1/2 !mx-0 flex flex-col">
|
|
||||||
<div className="card-header">
|
|
||||||
<h3 className="text-2xl font-medium text-label">연계 목록</h3>
|
|
||||||
</div>
|
|
||||||
<div className="card !m-[1.2rem] overflow-auto !p-0 h-full">
|
|
||||||
{!query.isLoading ? (
|
|
||||||
<div>{tableComponent}</div>
|
|
||||||
) : (
|
|
||||||
<div className="[data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center h-full">
|
|
||||||
<div className="relative size-[5rem]">
|
|
||||||
<LoadingWithAnimation color="primary" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="card card-border w-1/2 !mx-0 flex flex-col">
|
|
||||||
<div className="card-header">
|
|
||||||
<h3 className="text-2xl font-medium text-label">연계 변수 정보</h3>
|
|
||||||
</div>
|
|
||||||
<div className="card !m-[1.2rem] !p-0 overflow-auto h-full flex flex-col">
|
|
||||||
<Table variant="secondary" className="flex-none">
|
|
||||||
<colgroup>
|
|
||||||
<col width="20%" />
|
|
||||||
<col width="80%" />
|
|
||||||
</colgroup>
|
|
||||||
<TableBody>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>연계 일련번호</TableHead>
|
|
||||||
<TableCell>
|
|
||||||
{selectedLink.linkSn ? selectedLink.linkSn : '-'}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>연계 명</TableHead>
|
|
||||||
<TableCell>
|
|
||||||
{selectedLink.linkNm ? selectedLink.linkNm : '-'}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
{renderLinkVrblContent()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ConfirmDialog isOpen={onConfirm} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { LinkParamsPage };
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { LinkStatsPage } from './ui/link-stats-page';
|
|
||||||
@@ -1,378 +0,0 @@
|
|||||||
"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 };
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { LinkTablePage} from "./ui/link-table-page";
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
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 };
|
|
||||||
@@ -1,503 +0,0 @@
|
|||||||
'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 };
|
|
||||||
@@ -1,710 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState, useMemo } from "react";
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
DataTable,
|
|
||||||
Input,
|
|
||||||
Label,
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableRow,
|
|
||||||
LoadingWithAnimation,
|
|
||||||
} from "@common/components";
|
|
||||||
import { getCoreRowModel, type PaginationState, type RowSelectionState, useReactTable } from "@tanstack/react-table";
|
|
||||||
import { LinkApi, type Link, type LinkSearchParam, type LinkTable, type LinkTableColumns, type LinkTableColData, type LinkTableFormData, LinkTableColumnsSchema, LinkTableSchema } from "@/entities/link";
|
|
||||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
||||||
import { columns } from "../../linkParams/model/table-columns";
|
|
||||||
import { tableColumns } from "../model/table-columns";
|
|
||||||
import { LinkTableDialog } from "./link-table-dialog";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
|
|
||||||
import { AUTH_USER_ID } from "@/shared/config";
|
|
||||||
import { getCookie } from "@common/components";
|
|
||||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
|
||||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
|
||||||
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
|
||||||
import { FormItem, FormControl, FormField, Form } from "@/shared/ui/form";
|
|
||||||
import { useDataTable } from "@/shared/lib";
|
|
||||||
|
|
||||||
function LinkTablePage() {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
const userUniqId = getCookie(AUTH_USER_ID) ?? "";
|
|
||||||
|
|
||||||
const [linkSn, setLinkSn] = useState<string>('');
|
|
||||||
const [linkNm, setLinkNm] = useState<string>('');
|
|
||||||
|
|
||||||
const [linkSearchParam, setLinkSearchParam] = useState<LinkSearchParam>({
|
|
||||||
linkSn: '',
|
|
||||||
linkNm: ''
|
|
||||||
});
|
|
||||||
|
|
||||||
const [selectedTableInfo, setSelectedTableInfo] = useState<LinkTable | null>(null);
|
|
||||||
const [tableColList, setTableColList] = useState<LinkTableColumns[]>([]);
|
|
||||||
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
|
|
||||||
const [selectedColIds, setSelectedColIds] = useState<string[]>([]);
|
|
||||||
|
|
||||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
|
||||||
|
|
||||||
const [pagination, setPagination] = useState<PaginationState>({
|
|
||||||
pageIndex: 0,
|
|
||||||
pageSize: 100,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { setMessage: alert } = useAlertStore((state) => state);
|
|
||||||
|
|
||||||
const { confirm } = useConfirm();
|
|
||||||
const [onConfirm, setOnConfirm] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const handleConfirm = async (callback: () => Promise<void>) => {
|
|
||||||
setOnConfirm(true);
|
|
||||||
await callback();
|
|
||||||
setOnConfirm(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const query = useQuery({
|
|
||||||
queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkList`, linkSearchParam],
|
|
||||||
queryFn: () => LinkApi.getLinkList({
|
|
||||||
...linkSearchParam,
|
|
||||||
page: pagination.pageIndex,
|
|
||||||
size: pagination.pageSize,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const { mutateAsync: insertLinkTableCol } = useMutation({
|
|
||||||
mutationFn: async (data: LinkTableColData) => await LinkApi.insertLinkTableCol({
|
|
||||||
...data,
|
|
||||||
mdfr: userUniqId,
|
|
||||||
}),
|
|
||||||
onSuccess: async () => {
|
|
||||||
if (selectedTableInfo) {
|
|
||||||
await queryClient.invalidateQueries({
|
|
||||||
queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableInfo`]
|
|
||||||
});
|
|
||||||
await queryClient.invalidateQueries({
|
|
||||||
queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableColList`]
|
|
||||||
});
|
|
||||||
|
|
||||||
const tableData = await LinkApi.getLinkTableInfo(selectedTableInfo.link_sn);
|
|
||||||
const tableColList = await LinkApi.getLinkTableColList(selectedTableInfo.link_sn);
|
|
||||||
|
|
||||||
setSelectedTableInfo(tableData);
|
|
||||||
setTableColList(tableColList.content);
|
|
||||||
setSelectedColIds([]);
|
|
||||||
setDialogOpen(false);
|
|
||||||
alert('저장되었습니다.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const { mutateAsync: updateLinkTableCol } = useMutation({
|
|
||||||
mutationFn: async (data: LinkTableColData) => await LinkApi.updateLinkTableCol({
|
|
||||||
...data,
|
|
||||||
mdfr: userUniqId,
|
|
||||||
}),
|
|
||||||
onSuccess: async () => {
|
|
||||||
if (selectedTableInfo) {
|
|
||||||
await queryClient.invalidateQueries({
|
|
||||||
queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableInfo`]
|
|
||||||
});
|
|
||||||
await queryClient.invalidateQueries({
|
|
||||||
queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableColList`]
|
|
||||||
});
|
|
||||||
|
|
||||||
const tableData = await LinkApi.getLinkTableInfo(selectedTableInfo.link_sn);
|
|
||||||
const tableColList = await LinkApi.getLinkTableColList(selectedTableInfo.link_sn);
|
|
||||||
|
|
||||||
setSelectedTableInfo(tableData);
|
|
||||||
setTableColList(tableColList.content);
|
|
||||||
setSelectedColIds([]);
|
|
||||||
setDialogOpen(false);
|
|
||||||
alert('수정되었습니다.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const { mutateAsync: deleteLinkTableCol } = useMutation({
|
|
||||||
mutationFn: async (data: string[]) => await LinkApi.deleteLinkTableCol(data),
|
|
||||||
onSuccess: async () => {
|
|
||||||
if (selectedTableInfo) {
|
|
||||||
await queryClient.invalidateQueries({
|
|
||||||
queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableInfo`]
|
|
||||||
});
|
|
||||||
await queryClient.invalidateQueries({
|
|
||||||
queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableColList`]
|
|
||||||
});
|
|
||||||
|
|
||||||
const tableData = await LinkApi.getLinkTableInfo(selectedTableInfo.link_sn);
|
|
||||||
const tableColList = await LinkApi.getLinkTableColList(selectedTableInfo.link_sn);
|
|
||||||
|
|
||||||
setSelectedTableInfo(tableData);
|
|
||||||
setTableColList(tableColList.content);
|
|
||||||
setSelectedColIds([]);
|
|
||||||
setDialogOpen(false);
|
|
||||||
alert('삭제되었습니다.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const { mutateAsync: insertLinkTableInfo } = useMutation({
|
|
||||||
mutationFn: async (data: LinkTableFormData) => await LinkApi.insertLinkTableInfo(data),
|
|
||||||
onSuccess: async () => {
|
|
||||||
if (selectedTableInfo) {
|
|
||||||
await queryClient.invalidateQueries({
|
|
||||||
queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableInfo`]
|
|
||||||
});
|
|
||||||
await queryClient.invalidateQueries({
|
|
||||||
queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableColList`]
|
|
||||||
});
|
|
||||||
|
|
||||||
const tableData = await LinkApi.getLinkTableInfo(selectedTableInfo.link_sn);
|
|
||||||
const tableColList = await LinkApi.getLinkTableColList(selectedTableInfo.link_sn);
|
|
||||||
|
|
||||||
tableForm.reset({
|
|
||||||
link_sn: tableData.link_sn,
|
|
||||||
link_nm: tableData.link_nm,
|
|
||||||
tbl_sn: tableData.tbl_sn ?? "",
|
|
||||||
sou_tbl_nm: tableData.sou_tbl_nm ?? "",
|
|
||||||
sou_tbl_korn_nm: tableData.sou_tbl_korn_nm ?? "",
|
|
||||||
sou_tbl_expln: tableData.sou_tbl_expln ?? "",
|
|
||||||
trsf_tbl_expln: tableData.trsf_tbl_expln ?? "",
|
|
||||||
trsf_tbl_nm: tableData.trsf_tbl_nm ?? "",
|
|
||||||
trsf_tbl_korn_nm: tableData.trsf_tbl_korn_nm ?? "",
|
|
||||||
});
|
|
||||||
setSelectedTableInfo(tableData);
|
|
||||||
setTableColList(tableColList.content);
|
|
||||||
setSelectedColIds([]);
|
|
||||||
alert('저장되었습니다.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const { mutateAsync: updateLinkTableInfo } = useMutation({
|
|
||||||
mutationFn: async (data: LinkTableFormData) => await LinkApi.updateLinkTableInfo(data),
|
|
||||||
onSuccess: async () => {
|
|
||||||
if (selectedTableInfo) {
|
|
||||||
await queryClient.invalidateQueries({
|
|
||||||
queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableInfo`]
|
|
||||||
});
|
|
||||||
await queryClient.invalidateQueries({
|
|
||||||
queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableColList`]
|
|
||||||
});
|
|
||||||
|
|
||||||
const tableData = await LinkApi.getLinkTableInfo(selectedTableInfo.link_sn);
|
|
||||||
const tableColList = await LinkApi.getLinkTableColList(selectedTableInfo.link_sn);
|
|
||||||
|
|
||||||
setSelectedTableInfo(tableData);
|
|
||||||
setTableColList(tableColList.content);
|
|
||||||
setSelectedColIds([]);
|
|
||||||
alert('수정되었습니다.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const tableCol = useMemo(
|
|
||||||
() =>
|
|
||||||
tableColumns({ selectedColIds, setSelectedColIds }),
|
|
||||||
[selectedColIds]
|
|
||||||
);
|
|
||||||
|
|
||||||
const colTable = useReactTable({
|
|
||||||
data: tableColList,
|
|
||||||
columns: tableCol,
|
|
||||||
getCoreRowModel: getCoreRowModel(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const form = useForm<LinkTableColData>({
|
|
||||||
resolver: zodResolver(LinkTableColumnsSchema),
|
|
||||||
defaultValues: {
|
|
||||||
pk_yn: "N"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const tableForm = useForm<LinkTableFormData>({
|
|
||||||
resolver: zodResolver(LinkTableSchema),
|
|
||||||
defaultValues: {},
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleResetSearchParam = async () => {
|
|
||||||
setSelectedTableInfo(null);
|
|
||||||
setTableColList([]);
|
|
||||||
setLinkSn('');
|
|
||||||
setLinkNm('');
|
|
||||||
setLinkSearchParam({
|
|
||||||
...linkSearchParam,
|
|
||||||
linkSn: '',
|
|
||||||
linkNm: '',
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setSelectedTableInfo(null);
|
|
||||||
setTableColList([]);
|
|
||||||
|
|
||||||
setLinkSearchParam({
|
|
||||||
...linkSearchParam,
|
|
||||||
linkSn,
|
|
||||||
linkNm
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRowClick = async (data: Link) => {
|
|
||||||
const tableData = await LinkApi.getLinkTableInfo(data.link_sn);
|
|
||||||
const tableColList = await LinkApi.getLinkTableColList(data.link_sn);
|
|
||||||
|
|
||||||
setSelectedTableInfo(tableData);
|
|
||||||
setTableColList(tableColList.content);
|
|
||||||
setSelectedColIds([]);
|
|
||||||
|
|
||||||
tableForm.reset({
|
|
||||||
link_sn: tableData.link_sn,
|
|
||||||
link_nm: tableData.link_nm,
|
|
||||||
tbl_sn: tableData.tbl_sn ?? "",
|
|
||||||
sou_tbl_nm: tableData.sou_tbl_nm ?? "",
|
|
||||||
sou_tbl_korn_nm: tableData.sou_tbl_korn_nm ?? "",
|
|
||||||
sou_tbl_expln: tableData.sou_tbl_expln ?? "",
|
|
||||||
trsf_tbl_expln: tableData.trsf_tbl_expln ?? "",
|
|
||||||
trsf_tbl_nm: tableData.trsf_tbl_nm ?? "",
|
|
||||||
trsf_tbl_korn_nm: tableData.trsf_tbl_korn_nm ?? "",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleColumnRowClick = async (data: LinkTableColumns) => {
|
|
||||||
const colInfo = await LinkApi.getLinkTableColInfo({ colSn: data.col_sn, tblSn: data.tbl_sn });
|
|
||||||
|
|
||||||
if (colInfo) {
|
|
||||||
let newData = { ...colInfo };
|
|
||||||
|
|
||||||
if (colInfo.mdfr) {
|
|
||||||
const userName = await fetchUserInfo();
|
|
||||||
newData = {
|
|
||||||
...colInfo,
|
|
||||||
mdfr: userName,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
form.reset(newData);
|
|
||||||
|
|
||||||
}
|
|
||||||
setDialogOpen(true)
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchUserInfo = async () => {
|
|
||||||
const userData = await getUserInfo();
|
|
||||||
return userData.userName;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteParam = async () => {
|
|
||||||
if (selectedColIds.length === 0) {
|
|
||||||
alert('삭제할 컬럼을 선택해 주세요.');
|
|
||||||
} else {
|
|
||||||
await handleConfirm(async () => {
|
|
||||||
if (await confirm('선택한 컬럼 정보를 삭제하시겠습니까?')) {
|
|
||||||
deleteLinkTableCol(selectedColIds);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTableSubmit = async (data: LinkTableFormData) => {
|
|
||||||
const regTblInfo = await LinkApi.countLinkTblNm(data.trsf_tbl_nm);
|
|
||||||
const confirmMsg = (regTblInfo.count > 0 && data.trsf_tbl_nm !== selectedTableInfo?.trsf_tbl_nm) ? `이미 사용중인 테이블 명입니다. 해당 테이블 명으로 ${!data.tbl_sn ? '저장' : '수정'}하시겠습니까?` : `${!data.tbl_sn ? '저장' : '수정'}하시겠습니까?`;
|
|
||||||
if (!data.tbl_sn) {
|
|
||||||
await handleConfirm(async () => {
|
|
||||||
if (await confirm(confirmMsg)) {
|
|
||||||
insertLinkTableInfo({
|
|
||||||
...data,
|
|
||||||
mdfr: userUniqId,
|
|
||||||
tbl_sn: (regTblInfo.count > 0) ? regTblInfo.tbl_sn : "",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await handleConfirm(async () => {
|
|
||||||
if (await confirm(confirmMsg)) {
|
|
||||||
updateLinkTableInfo({
|
|
||||||
...data,
|
|
||||||
tbl_sn: (regTblInfo.count > 0) ? regTblInfo.tbl_sn : "",
|
|
||||||
mdfr: userUniqId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
const { tableComponent } = useDataTable({
|
|
||||||
query,
|
|
||||||
tableOptions: {
|
|
||||||
columns,
|
|
||||||
state: {
|
|
||||||
pagination,
|
|
||||||
rowSelection,
|
|
||||||
},
|
|
||||||
manualPagination: true,
|
|
||||||
enableMultiRowSelection: true,
|
|
||||||
getCoreRowModel: getCoreRowModel(),
|
|
||||||
onPaginationChange: setPagination,
|
|
||||||
onRowSelectionChange: setRowSelection,
|
|
||||||
},
|
|
||||||
dataTableOptions: {
|
|
||||||
onRowClick: handleRowClick,
|
|
||||||
viewPagination: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="card card-border !mt-6">
|
|
||||||
<div className="card card-border">
|
|
||||||
<form className="flex items-center gap-2 p-2" onSubmit={handleSubmit}>
|
|
||||||
<Label htmlFor="liSnSearchInput" className="mr-2 text-label">
|
|
||||||
일련번호
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem] mr-4"
|
|
||||||
id="liSnSearchInput"
|
|
||||||
value={linkSn}
|
|
||||||
onChange={(e) => setLinkSn(e.target.value)}
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
<Label htmlFor="liNmSearchInput" className="mr-2 text-label">
|
|
||||||
연계 명
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem] mr-2"
|
|
||||||
id="liNmSearchInput"
|
|
||||||
value={linkNm}
|
|
||||||
onChange={(e) => setLinkNm(e.target.value)}
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
<Button color="green" className="h-[3.2rem]" type="button" onClick={handleResetSearchParam}>
|
|
||||||
초기화
|
|
||||||
</Button>
|
|
||||||
<Button className="h-[3.2rem]" type="submit">
|
|
||||||
조회
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div className="card !mb-0 flex h-[calc(100vh-28rem)] gap-5 !p-0">
|
|
||||||
<div className="card card-border w-1/2 !mx-0 flex flex-col">
|
|
||||||
<div className="card-header">
|
|
||||||
<h3 className="text-2xl font-medium text-label">연계 목록</h3>
|
|
||||||
</div>
|
|
||||||
<div className="card !m-[1.2rem] overflow-auto !p-0 h-full">
|
|
||||||
{!query.isLoading ?
|
|
||||||
<div>{tableComponent}</div>
|
|
||||||
:
|
|
||||||
<div className="[data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center h-full">
|
|
||||||
<div className="relative size-[5rem]">
|
|
||||||
<LoadingWithAnimation color="primary" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="card card-border w-1/2 !mx-0 flex flex-col">
|
|
||||||
<div className="card-header">
|
|
||||||
<h3 className="text-2xl font-medium text-label">연계 테이블 정보</h3>
|
|
||||||
</div>
|
|
||||||
<div className="card !m-[1.2rem] !p-0 h-full overflow-auto flex flex-col">
|
|
||||||
<Form {...tableForm}>
|
|
||||||
<form onSubmit={tableForm.handleSubmit(handleTableSubmit)}>
|
|
||||||
{selectedTableInfo &&
|
|
||||||
<div className="flex justify-end mb-2">
|
|
||||||
<Button className="h-[3.2rem]" type="submit" color={selectedTableInfo.tbl_sn ? "green" : "primary"}>
|
|
||||||
{selectedTableInfo.tbl_sn ? "수정" : "저장"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<Table variant="secondary" className="flex-none">
|
|
||||||
<colgroup>
|
|
||||||
<col width="20%" />
|
|
||||||
<col width="80%" />
|
|
||||||
</colgroup>
|
|
||||||
<TableBody>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>연계 일련번호</TableHead>
|
|
||||||
<TableCell className="!p-2">
|
|
||||||
{selectedTableInfo ?
|
|
||||||
<FormField
|
|
||||||
control={tableForm.control}
|
|
||||||
name="link_sn"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
value={field.value ?? ""}
|
|
||||||
className="!text-[1.3rem] w-[400px]"
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
:
|
|
||||||
<span className="px-2 py-[1.2rem]">-</span>
|
|
||||||
}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>연계 명</TableHead>
|
|
||||||
<TableCell className="!p-2">
|
|
||||||
{selectedTableInfo ?
|
|
||||||
<FormField
|
|
||||||
control={tableForm.control}
|
|
||||||
name="link_nm"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
value={field.value ?? ""}
|
|
||||||
className="!text-[1.3rem] w-[400px]"
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
:
|
|
||||||
<span className="px-2 py-[1.2rem]">-</span>
|
|
||||||
}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>원천 테이블 명</TableHead>
|
|
||||||
<TableCell className="!p-2">
|
|
||||||
{selectedTableInfo ?
|
|
||||||
<FormField
|
|
||||||
control={tableForm.control}
|
|
||||||
name="sou_tbl_nm"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
value={field.value ?? ""}
|
|
||||||
className="!text-[1.3rem] w-[400px]"
|
|
||||||
placeholder={!field.value ? "-" : ""}
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
{tableForm.formState.errors.sou_tbl_nm && (
|
|
||||||
<p className="text-red-500 text-base pt-2">
|
|
||||||
{tableForm.formState.errors.sou_tbl_nm.message}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
:
|
|
||||||
<span className="px-2 py-[1.2rem]">-</span>
|
|
||||||
}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>원천 테이블 한글명</TableHead>
|
|
||||||
<TableCell className="!p-2">
|
|
||||||
{selectedTableInfo ?
|
|
||||||
<FormField
|
|
||||||
control={tableForm.control}
|
|
||||||
name="sou_tbl_korn_nm"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
value={field.value ?? ""}
|
|
||||||
className="!text-[1.3rem] w-[400px]"
|
|
||||||
placeholder={!field.value ? "-" : ""}
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
{tableForm.formState.errors.sou_tbl_korn_nm && (
|
|
||||||
<p className="text-red-500 text-base pt-2">
|
|
||||||
{tableForm.formState.errors.sou_tbl_korn_nm.message}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
:
|
|
||||||
<span className="px-2 py-[1.2rem]">-</span>
|
|
||||||
}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>원천 테이블 설명</TableHead>
|
|
||||||
<TableCell className="!p-2">
|
|
||||||
{selectedTableInfo ?
|
|
||||||
<FormField
|
|
||||||
control={tableForm.control}
|
|
||||||
name="sou_tbl_expln"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
value={field.value ?? ""}
|
|
||||||
className="!text-[1.3rem] w-[400px]"
|
|
||||||
placeholder={!field.value ? "-" : ""}
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
{tableForm.formState.errors.sou_tbl_expln && (
|
|
||||||
<p className="text-red-500 text-base pt-2">
|
|
||||||
{tableForm.formState.errors.sou_tbl_expln.message}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
:
|
|
||||||
<span className="px-2 py-[1.2rem]">-</span>
|
|
||||||
}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>* 변환 테이블 명</TableHead>
|
|
||||||
<TableCell className="!p-2">
|
|
||||||
{selectedTableInfo ?
|
|
||||||
<FormField
|
|
||||||
control={tableForm.control}
|
|
||||||
name="trsf_tbl_nm"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
value={field.value ?? ""}
|
|
||||||
className="!text-[1.3rem] w-[400px]"
|
|
||||||
placeholder={!field.value ? "-" : ""}
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
{tableForm.formState.errors.trsf_tbl_nm && (
|
|
||||||
<p className="text-red-500 text-base pt-2">
|
|
||||||
{tableForm.formState.errors.trsf_tbl_nm.message}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
:
|
|
||||||
<span className="px-2 py-[1.2rem]">-</span>
|
|
||||||
}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>* 변환 테이블 한글명</TableHead>
|
|
||||||
<TableCell className="!p-2">
|
|
||||||
{selectedTableInfo ?
|
|
||||||
<FormField
|
|
||||||
control={tableForm.control}
|
|
||||||
name="trsf_tbl_korn_nm"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
value={field.value ?? ""}
|
|
||||||
className="!text-[1.3rem] w-[400px]"
|
|
||||||
placeholder={!field.value ? "-" : ""}
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
{tableForm.formState.errors.trsf_tbl_korn_nm && (
|
|
||||||
<p className="text-red-500 text-base pt-2">
|
|
||||||
{tableForm.formState.errors.trsf_tbl_korn_nm.message}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
:
|
|
||||||
<span className="px-2 py-[1.2rem]">-</span>
|
|
||||||
}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>변환 테이블 설명</TableHead>
|
|
||||||
<TableCell className="!p-2">
|
|
||||||
{selectedTableInfo ?
|
|
||||||
<FormField
|
|
||||||
control={tableForm.control}
|
|
||||||
name="trsf_tbl_expln"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
value={field.value ?? ""}
|
|
||||||
className="!text-[1.3rem] w-[400px]"
|
|
||||||
placeholder={!field.value ? "-" : ""}
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
{tableForm.formState.errors.trsf_tbl_expln && (
|
|
||||||
<p className="text-red-500 text-base pt-2">
|
|
||||||
{tableForm.formState.errors.trsf_tbl_expln.message}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
:
|
|
||||||
<span className="px-2 py-[1.2rem]">-</span>
|
|
||||||
}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
{selectedTableInfo ?
|
|
||||||
<>
|
|
||||||
<div className="mb-2 mt-14 flex justify-end items-center flex-none">
|
|
||||||
{tableColList.length > 0 &&
|
|
||||||
<Button color="red" className="h-[3.2rem] mr-2" type="button" onClick={handleDeleteParam}>
|
|
||||||
행 삭제
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
{selectedTableInfo.tbl_sn &&
|
|
||||||
<LinkTableDialog dialogOpen={dialogOpen} onOpenChange={setDialogOpen} form={form} tableInfo={selectedTableInfo} insertLinkTableCol={insertLinkTableCol} updateLinkTableCol={updateLinkTableCol} />
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
{selectedTableInfo.tbl_sn ?
|
|
||||||
(tableColList.length > 0 ?
|
|
||||||
<div className="flex-1 overflow-auto min-h-[300px]">
|
|
||||||
<DataTable table={colTable} onRowClick={handleColumnRowClick} />
|
|
||||||
</div>
|
|
||||||
:
|
|
||||||
<div className="flex items-center justify-center h-20 text-gray-500 text-3xl">
|
|
||||||
등록된 테이블 컬럼이 없습니다.
|
|
||||||
</div>)
|
|
||||||
:
|
|
||||||
<div className="flex items-center justify-center h-20 text-gray-500 text-3xl">
|
|
||||||
테이블 정보를 등록해주세요.
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</>
|
|
||||||
:
|
|
||||||
<div className="flex items-center justify-center text-gray-500 text-3xl mt-14 flex-1">
|
|
||||||
연계 데이터를 선택해주세요.
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ConfirmDialog isOpen={onConfirm} />
|
|
||||||
</div >
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
export { LinkTablePage };
|
|
||||||
@@ -6,6 +6,7 @@ import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
|||||||
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
DateRangePicker,
|
||||||
Input,
|
Input,
|
||||||
Label,
|
Label,
|
||||||
Select,
|
Select,
|
||||||
@@ -15,14 +16,14 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
LoadingWithImage,
|
LoadingWithImage,
|
||||||
} from "@common/components";
|
} from "@common/components";
|
||||||
import { columns } from "@/pages/loginlog/model/table-columns";
|
import { LoginlogApi } from "@/entities/loginlog";
|
||||||
import { LoginlogApi, type LoginlogParams } from "@/entities/loginlog";
|
import { type LoginlogParams } from "@/entities/loginlog/model/loginlog-interface";
|
||||||
import { useDataTable } from "@/shared/lib";
|
import { useDataTable } from "@/shared/lib";
|
||||||
import { ENDPOINT } from "@/shared/config";
|
import { ENDPOINT } from "@/shared/config";
|
||||||
import { InputDateRangePicker } from "@/shared/ui/input-date-range-picker";
|
import { columns } from "@/pages/loginlog/model/table-columns";
|
||||||
|
|
||||||
type DateRange = {
|
type DateRange = {
|
||||||
from?: Date | undefined;
|
from: Date | undefined;
|
||||||
to?: Date | undefined;
|
to?: Date | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -32,12 +33,13 @@ function LoginlogPage() {
|
|||||||
to: dayjs().toDate(),
|
to: dayjs().toDate(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [dateRange, setDateRange] = useState<DateRange>(defaultDateRange);
|
||||||
|
const [isManualLoading, setIsManualLoading] = useState(false);
|
||||||
|
|
||||||
const [pagination, setPagination] = useState<PaginationState>({
|
const [pagination, setPagination] = useState<PaginationState>({
|
||||||
pageIndex: 0,
|
pageIndex: 0,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
});
|
});
|
||||||
const [presetDateRange, setPresetDateRange] = useState("");
|
|
||||||
const [dateRange, setDateRange] = useState<DateRange>(defaultDateRange);
|
|
||||||
const [userId, setUserId] = useState("");
|
const [userId, setUserId] = useState("");
|
||||||
const [errOcrnYn, setErrOcrnYn] = useState("N");
|
const [errOcrnYn, setErrOcrnYn] = useState("N");
|
||||||
const [searchParams, setSearchParams] = useState<LoginlogParams>({
|
const [searchParams, setSearchParams] = useState<LoginlogParams>({
|
||||||
@@ -47,7 +49,6 @@ function LoginlogPage() {
|
|||||||
searchDateTo: dayjs().format("YYYY-MM-DD"),
|
searchDateTo: dayjs().format("YYYY-MM-DD"),
|
||||||
sort: "lgnDt,desc",
|
sort: "lgnDt,desc",
|
||||||
});
|
});
|
||||||
const [isManualLoading, setIsManualLoading] = useState(false);
|
|
||||||
|
|
||||||
const query = useQuery({
|
const query = useQuery({
|
||||||
queryKey: [ENDPOINT.USER_SERVICE.LOGINLOG, searchParams, pagination],
|
queryKey: [ENDPOINT.USER_SERVICE.LOGINLOG, searchParams, pagination],
|
||||||
@@ -88,12 +89,8 @@ function LoginlogPage() {
|
|||||||
...searchParams,
|
...searchParams,
|
||||||
userId,
|
userId,
|
||||||
errOcrnYn,
|
errOcrnYn,
|
||||||
searchDateFrom: dateRange.from
|
searchDateFrom: dayjs(dateRange.from).format("YYYY-MM-DD"),
|
||||||
? dayjs(dateRange.from).format("YYYY-MM-DD")
|
searchDateTo: dayjs(dateRange.to).format("YYYY-MM-DD"),
|
||||||
: "",
|
|
||||||
searchDateTo: dateRange.to
|
|
||||||
? dayjs(dateRange.to).format("YYYY-MM-DD")
|
|
||||||
: "",
|
|
||||||
});
|
});
|
||||||
query.refetch().finally(() => {
|
query.refetch().finally(() => {
|
||||||
setIsManualLoading(false);
|
setIsManualLoading(false);
|
||||||
@@ -137,34 +134,31 @@ function LoginlogPage() {
|
|||||||
default:
|
default:
|
||||||
presetDateRange = undefined;
|
presetDateRange = undefined;
|
||||||
}
|
}
|
||||||
setPresetDateRange(value);
|
|
||||||
setDateRange(presetDateRange || defaultDateRange);
|
setDateRange(presetDateRange || defaultDateRange);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDateRangeChange = (dateRange: DateRange) => {
|
|
||||||
setDateRange(dateRange);
|
|
||||||
setPresetDateRange("");
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="card card-border">
|
<div className="card card-border">
|
||||||
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
|
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
|
||||||
<Label className="mr-2">조회기간</Label>
|
<Label className="mr-2">조회기간</Label>
|
||||||
<Select value={presetDateRange} onValueChange={handlePresetDateRange}>
|
<DateRangePicker
|
||||||
<SelectTrigger className="w-[12rem]">
|
value={dateRange}
|
||||||
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
|
onValueChange={(value) => setDateRange(value || defaultDateRange)}
|
||||||
</SelectTrigger>
|
>
|
||||||
<SelectContent>
|
<div className="card card-border flex items-center gap-2">
|
||||||
<SelectItem value="1W">일주일</SelectItem>
|
<Select onValueChange={handlePresetDateRange}>
|
||||||
<SelectItem value="1M">1개월</SelectItem>
|
<SelectTrigger>
|
||||||
<SelectItem value="3M">3개월</SelectItem>
|
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
|
||||||
</SelectContent>
|
</SelectTrigger>
|
||||||
</Select>
|
<SelectContent>
|
||||||
<InputDateRangePicker
|
<SelectItem value="1W">일주일</SelectItem>
|
||||||
dateRange={dateRange}
|
<SelectItem value="1M">1개월</SelectItem>
|
||||||
setDateRange={handleDateRangeChange}
|
<SelectItem value="3M">3개월</SelectItem>
|
||||||
/>
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</DateRangePicker>
|
||||||
<Label className="ml-4 mr-2 text-label">사용자ID</Label>
|
<Label className="ml-4 mr-2 text-label">사용자ID</Label>
|
||||||
<Input
|
<Input
|
||||||
className="h-[3.2rem] w-[20rem] rounded-sm !text-[1.3rem]"
|
className="h-[3.2rem] w-[20rem] rounded-sm !text-[1.3rem]"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type {
|
|||||||
TreeUserOrganization,
|
TreeUserOrganization,
|
||||||
TreeUser,
|
TreeUser,
|
||||||
} from "@/entities/user-organization";
|
} from "@/entities/user-organization";
|
||||||
import type { TreeMenu, TreeNode } from "@/entities/tree-menu";
|
import type { TreeMenu, TreeNode } from "../model/tree-menu-interface";
|
||||||
|
|
||||||
export const generateMenuTreeData = (
|
export const generateMenuTreeData = (
|
||||||
data: TreeMenu[],
|
data: TreeMenu[],
|
||||||
|
|||||||
-1
@@ -19,7 +19,6 @@ export interface TreeMenu extends Omit<TreeNode, "name" | "icon" | "children"> {
|
|||||||
qrcd?: string;
|
qrcd?: string;
|
||||||
useYn: string;
|
useYn: string;
|
||||||
otptYn: string;
|
otptYn: string;
|
||||||
menuSubKornNm?: string;
|
|
||||||
requiredUrlPath?: boolean;
|
requiredUrlPath?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { ComponentProps } from "react";
|
import type { ComponentProps } from "react";
|
||||||
import { Button, Input } from "@common/components";
|
import { Button, Input } from "@common/components";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip";
|
|
||||||
|
|
||||||
type EditableNodeProps = {
|
type EditableNodeProps = {
|
||||||
depth?: number;
|
depth?: number;
|
||||||
@@ -21,34 +20,22 @@ function EditableNode({
|
|||||||
>
|
>
|
||||||
<Input {...props} className="h-[2.5rem] !text-[1.3rem] text-foreground" />
|
<Input {...props} className="h-[2.5rem] !text-[1.3rem] text-foreground" />
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Tooltip>
|
<Button
|
||||||
<TooltipTrigger asChild>
|
variant="ghost"
|
||||||
<Button
|
size="icon"
|
||||||
variant="ghost"
|
className="size-8 !bg-transparent"
|
||||||
size="icon"
|
onClick={onSave}
|
||||||
className="size-8 !bg-transparent"
|
>
|
||||||
onClick={onSave}
|
<i className="diveicon di-check text-2xl" />
|
||||||
aria-label="저장"
|
</Button>
|
||||||
>
|
<Button
|
||||||
<i className="diveicon di-check text-2xl" />
|
variant="ghost"
|
||||||
</Button>
|
size="icon"
|
||||||
</TooltipTrigger>
|
className="size-8 !bg-transparent"
|
||||||
<TooltipContent>저장</TooltipContent>
|
onClick={onCancel}
|
||||||
</Tooltip>
|
>
|
||||||
<Tooltip>
|
<i className="diveicon di-x" />
|
||||||
<TooltipTrigger asChild>
|
</Button>
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="size-8 !bg-transparent"
|
|
||||||
onClick={onCancel}
|
|
||||||
aria-label="취소"
|
|
||||||
>
|
|
||||||
<i className="diveicon di-x" />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>취소</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ import { useEffect, useState } from "react";
|
|||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import type { NodeModel, TreeMethods } from "@minoru/react-dnd-treeview";
|
import type { NodeModel, TreeMethods } from "@minoru/react-dnd-treeview";
|
||||||
import { Button, cn } from "@common/components";
|
import { Button, cn } from "@common/components";
|
||||||
import { TreeMenuApi, type TreeMenu } from "@/entities/tree-menu";
|
|
||||||
import { useMutationWrapper } from "@/shared/lib";
|
import { useMutationWrapper } from "@/shared/lib";
|
||||||
import { ENDPOINT } from "@/shared/config";
|
import { ENDPOINT } from "@/shared/config";
|
||||||
|
import { TreeMenuApi } from "../api/tree-menu-service";
|
||||||
|
import type { TreeMenu } from "../model/tree-menu-interface";
|
||||||
import { EditableNode } from "./editable-node";
|
import { EditableNode } from "./editable-node";
|
||||||
|
|
||||||
type MenuNodeProps = {
|
type MenuNodeProps = {
|
||||||
|
|||||||
@@ -21,10 +21,11 @@ import {
|
|||||||
type MenuFormData,
|
type MenuFormData,
|
||||||
} from "@/entities/menu";
|
} from "@/entities/menu";
|
||||||
import { SiteApi } from "@/entities/site";
|
import { SiteApi } from "@/entities/site";
|
||||||
import { TreeMenuApi, type TreeMenu } from "@/entities/tree-menu";
|
|
||||||
import { ENDPOINT } from "@/shared/config";
|
import { ENDPOINT } from "@/shared/config";
|
||||||
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
|
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
|
||||||
import { generateMenuTreeData } from "../lib/utils";
|
import { generateMenuTreeData } from "../lib/utils";
|
||||||
|
import { TreeMenuApi } from "../api/tree-menu-service";
|
||||||
|
import type { TreeMenu } from "../model/tree-menu-interface";
|
||||||
import { MenuTree } from "./menu-tree";
|
import { MenuTree } from "./menu-tree";
|
||||||
import { MenuForm } from "./menu-form";
|
import { MenuForm } from "./menu-form";
|
||||||
|
|
||||||
@@ -110,7 +111,6 @@ function MenuPage() {
|
|||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: [ENDPOINT.CMS_SERVICE.MENU_ROLES],
|
queryKey: [ENDPOINT.CMS_SERVICE.MENU_ROLES],
|
||||||
});
|
});
|
||||||
form.reset();
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -12,11 +12,8 @@ import { Button } from "@common/components";
|
|||||||
import { ENDPOINT } from "@/shared/config";
|
import { ENDPOINT } from "@/shared/config";
|
||||||
import { useMutationWrapper } from "@/shared/lib";
|
import { useMutationWrapper } from "@/shared/lib";
|
||||||
import type { MenuFormData } from "@/entities/menu";
|
import type { MenuFormData } from "@/entities/menu";
|
||||||
import {
|
import { TreeMenuApi } from "../api/tree-menu-service";
|
||||||
TreeMenuApi,
|
import type { TreeMenu, TreeNode } from "../model/tree-menu-interface";
|
||||||
type TreeMenu,
|
|
||||||
type TreeNode,
|
|
||||||
} from "@/entities/tree-menu";
|
|
||||||
import { handleDropNode, handleSelectNode } from "../lib/utils";
|
import { handleDropNode, handleSelectNode } from "../lib/utils";
|
||||||
import { MenuNode } from "./menu-node";
|
import { MenuNode } from "./menu-node";
|
||||||
import { EditableNode } from "./editable-node";
|
import { EditableNode } from "./editable-node";
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ function AddUserDialog({ onAddUsers, ...props }: AddUserDialogProps) {
|
|||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-w-[60rem]" aria-describedby={undefined}>
|
<DialogContent className="max-w-[60rem]" aria-describedby={undefined}>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>사용자 조회</DialogTitle>
|
<DialogTitle>콘텐츠 조회</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="card card-border !m-0 flex w-full items-center gap-2">
|
<div className="card card-border !m-0 flex w-full items-center gap-2">
|
||||||
|
|||||||
@@ -242,16 +242,6 @@ function UserOrganizationSection({ ognzSn }: UserOrganizationSectionProps) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="card max-h-[calc(100vh-58rem)] overflow-auto !p-0">
|
<div className="card max-h-[calc(100vh-58rem)] overflow-auto !p-0">
|
||||||
<Table>
|
<Table>
|
||||||
<colgroup>
|
|
||||||
<col width="5%" />
|
|
||||||
<col width="5%" />
|
|
||||||
<col width="auto" />
|
|
||||||
<col width="10%" />
|
|
||||||
<col width="10%" />
|
|
||||||
<col width="10%" />
|
|
||||||
<col width="15%" />
|
|
||||||
<col width="15%" />
|
|
||||||
</colgroup>
|
|
||||||
<TableHeader className="sticky top-0">
|
<TableHeader className="sticky top-0">
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
@@ -310,9 +300,6 @@ const UserCells = memo(
|
|||||||
jobPositionCodeData,
|
jobPositionCodeData,
|
||||||
jobGradeCodeData,
|
jobGradeCodeData,
|
||||||
}: UserCellsProps) => {
|
}: UserCellsProps) => {
|
||||||
const [jbpsSeCd, setJbpsSeCd] = useState(user.jbpsSeCd);
|
|
||||||
const [jbgdSeCd, setJbgdSeCd] = useState(user.jbgdSeCd);
|
|
||||||
|
|
||||||
const updateMode = (index: number) => {
|
const updateMode = (index: number) => {
|
||||||
const mode = form.watch(`userOrganization.${index}.mode`);
|
const mode = form.watch(`userOrganization.${index}.mode`);
|
||||||
if (mode === "R") {
|
if (mode === "R") {
|
||||||
@@ -335,33 +322,17 @@ const UserCells = memo(
|
|||||||
<CodeSelect
|
<CodeSelect
|
||||||
codeData={jobPositionCodeData}
|
codeData={jobPositionCodeData}
|
||||||
defaultValue={user.jbpsSeCd}
|
defaultValue={user.jbpsSeCd}
|
||||||
onValueChange={(value) => {
|
onValueChange={updateFormValue(index, "jbpsSeCd")}
|
||||||
setJbpsSeCd(value);
|
|
||||||
updateFormValue(index, "jbpsSeCd")(value);
|
|
||||||
}}
|
|
||||||
{...form.register(`userOrganization.${index}.jbpsSeCd`)}
|
{...form.register(`userOrganization.${index}.jbpsSeCd`)}
|
||||||
/>
|
/>
|
||||||
{!jbpsSeCd && (
|
|
||||||
<p className="pt-1 text-base text-destructive">
|
|
||||||
직위를 선택해주세요.
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell align="center">
|
<TableCell align="center">
|
||||||
<CodeSelect
|
<CodeSelect
|
||||||
codeData={jobGradeCodeData}
|
codeData={jobGradeCodeData}
|
||||||
defaultValue={user.jbgdSeCd}
|
defaultValue={user.jbgdSeCd}
|
||||||
onValueChange={(value) => {
|
onValueChange={updateFormValue(index, "jbgdSeCd")}
|
||||||
setJbgdSeCd(value);
|
|
||||||
updateFormValue(index, "jbgdSeCd")(value);
|
|
||||||
}}
|
|
||||||
{...form.register(`userOrganization.${index}.jbgdSeCd`)}
|
{...form.register(`userOrganization.${index}.jbgdSeCd`)}
|
||||||
/>
|
/>
|
||||||
{!jbgdSeCd && (
|
|
||||||
<p className="pt-1 text-base text-destructive">
|
|
||||||
직급을 선택해주세요.
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell align="center">
|
<TableCell align="center">
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
@@ -33,14 +33,9 @@ export const columns: ColumnDef<Post>[] = [
|
|||||||
accessorKey: "pstgPrdSeNm",
|
accessorKey: "pstgPrdSeNm",
|
||||||
header: "게시기간",
|
header: "게시기간",
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const pstgPrdSeCd = row.original.pstgPrdSeCd;
|
|
||||||
const pstgPrdSeNm = row.original.pstgPrdSeNm;
|
const pstgPrdSeNm = row.original.pstgPrdSeNm;
|
||||||
const pstgBgngDt = row.original.pstgBgngDt;
|
const pstgBgngDt = row.original.pstgBgngDt;
|
||||||
const pstgEndDt = row.original.pstgEndDt;
|
const pstgEndDt = row.original.pstgEndDt;
|
||||||
|
|
||||||
if (pstgPrdSeCd === "99") {
|
|
||||||
return pstgPrdSeNm;
|
|
||||||
}
|
|
||||||
return `${pstgPrdSeNm} (${dayjs(pstgBgngDt).format("YYYY-MM-DD")} ~ ${dayjs(pstgEndDt).format("YYYY-MM-DD")})`;
|
return `${pstgPrdSeNm} (${dayjs(pstgBgngDt).format("YYYY-MM-DD")} ~ ${dayjs(pstgEndDt).format("YYYY-MM-DD")})`;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
|||||||
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
DateRangePicker,
|
||||||
Input,
|
Input,
|
||||||
Label,
|
Label,
|
||||||
Select,
|
Select,
|
||||||
@@ -14,17 +15,14 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@common/components";
|
} from "@common/components";
|
||||||
import { columns } from "@/pages/privacylog/model/table-columns";
|
import { PrivacyLogPrivacy } from "@/entities/privacylog";
|
||||||
import {
|
import type { PrivacyLogParams } from "@/entities/privacylog/model/privacylog-interface";
|
||||||
PrivacyLogPrivacy,
|
|
||||||
type PrivacyLogParams,
|
|
||||||
} from "@/entities/privacylog";
|
|
||||||
import { useDataTable } from "@/shared/lib";
|
import { useDataTable } from "@/shared/lib";
|
||||||
import { ENDPOINT } from "@/shared/config";
|
import { ENDPOINT } from "@/shared/config";
|
||||||
import { InputDateRangePicker } from "@/shared/ui/input-date-range-picker";
|
import { columns } from "@/pages/privacylog/model/table-columns";
|
||||||
|
|
||||||
type DateRange = {
|
type DateRange = {
|
||||||
from?: Date | undefined;
|
from: Date | undefined;
|
||||||
to?: Date | undefined;
|
to?: Date | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -38,7 +36,6 @@ function PrivacyLogPage() {
|
|||||||
pageIndex: 0,
|
pageIndex: 0,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
});
|
});
|
||||||
const [presetDateRange, setPresetDateRange] = useState("");
|
|
||||||
const [dateRange, setDateRange] = useState<DateRange>(defaultDateRange);
|
const [dateRange, setDateRange] = useState<DateRange>(defaultDateRange);
|
||||||
const [inqInfoKeyword, setInqInfoKeyword] = useState("");
|
const [inqInfoKeyword, setInqInfoKeyword] = useState("");
|
||||||
const [searchParams, setSearchParams] = useState<PrivacyLogParams>({
|
const [searchParams, setSearchParams] = useState<PrivacyLogParams>({
|
||||||
@@ -86,10 +83,8 @@ function PrivacyLogPage() {
|
|||||||
setSearchParams({
|
setSearchParams({
|
||||||
...searchParams,
|
...searchParams,
|
||||||
inqInfoKeyword,
|
inqInfoKeyword,
|
||||||
inqDtStart: dateRange.from
|
inqDtStart: dayjs(dateRange.from).format("YYYY-MM-DD"),
|
||||||
? dayjs(dateRange.from).format("YYYY-MM-DD")
|
inqDtEnd: dayjs(dateRange.to).format("YYYY-MM-DD"),
|
||||||
: "",
|
|
||||||
inqDtEnd: dateRange.to ? dayjs(dateRange.to).format("YYYY-MM-DD") : "",
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -128,40 +123,38 @@ function PrivacyLogPage() {
|
|||||||
default:
|
default:
|
||||||
presetDateRange = undefined;
|
presetDateRange = undefined;
|
||||||
}
|
}
|
||||||
setPresetDateRange(value);
|
|
||||||
setDateRange(presetDateRange || defaultDateRange);
|
setDateRange(presetDateRange || defaultDateRange);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDateRangeChange = (dateRange: DateRange) => {
|
|
||||||
setDateRange(dateRange);
|
|
||||||
setPresetDateRange("");
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="card card-border">
|
<div className="card card-border">
|
||||||
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
|
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
|
||||||
<Label className="mr-2">조회기간</Label>
|
<Label className="mr-2">조회기간</Label>
|
||||||
<Select value={presetDateRange} onValueChange={handlePresetDateRange}>
|
<DateRangePicker
|
||||||
<SelectTrigger className="w-[12rem]">
|
value={dateRange}
|
||||||
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
|
onValueChange={(value) => setDateRange(value || defaultDateRange)}
|
||||||
</SelectTrigger>
|
>
|
||||||
<SelectContent>
|
<div className="card card-border flex items-center gap-2">
|
||||||
<SelectItem value="1W">일주일</SelectItem>
|
<Select onValueChange={handlePresetDateRange}>
|
||||||
<SelectItem value="1M">1개월</SelectItem>
|
<SelectTrigger>
|
||||||
<SelectItem value="3M">3개월</SelectItem>
|
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
|
||||||
</SelectContent>
|
</SelectTrigger>
|
||||||
</Select>
|
<SelectContent>
|
||||||
<InputDateRangePicker
|
<SelectItem value="1W">일주일</SelectItem>
|
||||||
dateRange={dateRange}
|
<SelectItem value="1M">1개월</SelectItem>
|
||||||
setDateRange={handleDateRangeChange}
|
<SelectItem value="3M">3개월</SelectItem>
|
||||||
/>
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</DateRangePicker>
|
||||||
<Label className="ml-4 mr-2 text-label">조회정보</Label>
|
<Label className="ml-4 mr-2 text-label">조회정보</Label>
|
||||||
<Input
|
<Input
|
||||||
className="h-[3.2rem] w-[20rem] rounded-sm !text-[1.3rem]"
|
className="h-[3.2rem] w-[20rem] rounded-sm !text-[1.3rem]"
|
||||||
value={inqInfoKeyword}
|
value={inqInfoKeyword}
|
||||||
onChange={(e) => setInqInfoKeyword(e.target.value)}
|
onChange={(e) => setInqInfoKeyword(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button className="h-[3.2rem]" type="submit">
|
<Button className="h-[3.2rem]" type="submit">
|
||||||
조회
|
조회
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
} from "@tanstack/react-table";
|
} from "@tanstack/react-table";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
DateRangePicker,
|
||||||
Input,
|
Input,
|
||||||
Label,
|
Label,
|
||||||
Select,
|
Select,
|
||||||
@@ -30,14 +31,13 @@ import {
|
|||||||
} from "@/entities/post";
|
} from "@/entities/post";
|
||||||
import { BoardApi } from "@/entities/board";
|
import { BoardApi } from "@/entities/board";
|
||||||
import { AuthorityButton } from "@/entities/menu-authority";
|
import { AuthorityButton } from "@/entities/menu-authority";
|
||||||
import { ENDPOINT } from "@/shared/config";
|
|
||||||
import { InputDateRangePicker } from "@/shared/ui/input-date-range-picker";
|
|
||||||
import { useAlertStore, useSelectPrompt, useDataTable } from "@/shared/lib";
|
import { useAlertStore, useSelectPrompt, useDataTable } from "@/shared/lib";
|
||||||
|
import { ENDPOINT } from "@/shared/config";
|
||||||
import { columns } from "../model/table-columns";
|
import { columns } from "../model/table-columns";
|
||||||
import { MovePostDialog } from "./move-post-dialog";
|
import { MovePostDialog } from "./move-post-dialog";
|
||||||
|
|
||||||
type DateRange = {
|
type DateRange = {
|
||||||
from?: Date | undefined;
|
from: Date | undefined;
|
||||||
to?: Date | undefined;
|
to?: Date | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -51,21 +51,22 @@ function RecentPage() {
|
|||||||
const { confirm } = useConfirm();
|
const { confirm } = useConfirm();
|
||||||
const alert = useAlertStore((state) => state.setMessage);
|
const alert = useAlertStore((state) => state.setMessage);
|
||||||
|
|
||||||
|
const [dateRange, setDateRange] = useState<DateRange | undefined>({
|
||||||
|
from: undefined,
|
||||||
|
to: undefined,
|
||||||
|
});
|
||||||
const [tableSort, setTableSort] = useState("regDt,desc");
|
const [tableSort, setTableSort] = useState("regDt,desc");
|
||||||
const [pagination, setPagination] = useState<PaginationState>({
|
const [pagination, setPagination] = useState<PaginationState>({
|
||||||
pageIndex: 0,
|
pageIndex: 0,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
});
|
});
|
||||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||||
const [presetDateRange, setPresetDateRange] = useState("");
|
|
||||||
const [dateRange, setDateRange] = useState<DateRange>({
|
|
||||||
from: undefined,
|
|
||||||
to: undefined,
|
|
||||||
});
|
|
||||||
const [keywordType, setKeywordType] = useState("");
|
const [keywordType, setKeywordType] = useState("");
|
||||||
const [keyword, setKeyword] = useState("");
|
const [keyword, setKeyword] = useState("");
|
||||||
const [bbsId, setBbsId] = useState<string | null>(null);
|
const [bbsId, setBbsId] = useState<string | null>(null);
|
||||||
const [delYn, setDelYn] = useState("N");
|
const [delYn, setDelYn] = useState("N");
|
||||||
|
|
||||||
const [searchParams, setSearchParams] = useState<RecentPostParams>({
|
const [searchParams, setSearchParams] = useState<RecentPostParams>({
|
||||||
keywordType: "",
|
keywordType: "",
|
||||||
keyword: "",
|
keyword: "",
|
||||||
@@ -169,7 +170,7 @@ function RecentPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handlePresetDateRange = (value: string) => {
|
const handlePresetDateRange = (value: string) => {
|
||||||
let presetDateRange: DateRange;
|
let presetDateRange: DateRange | undefined;
|
||||||
switch (value) {
|
switch (value) {
|
||||||
case "1W":
|
case "1W":
|
||||||
presetDateRange = {
|
presetDateRange = {
|
||||||
@@ -190,20 +191,11 @@ function RecentPage() {
|
|||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
presetDateRange = {
|
presetDateRange = undefined;
|
||||||
from: undefined,
|
|
||||||
to: undefined,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
setPresetDateRange(value);
|
|
||||||
setDateRange(presetDateRange);
|
setDateRange(presetDateRange);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDateRangeChange = (dateRange: DateRange) => {
|
|
||||||
setDateRange(dateRange);
|
|
||||||
setPresetDateRange("");
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSoftDeletePost = async () => {
|
const onSoftDeletePost = async () => {
|
||||||
const { resolved, value: delRsn } =
|
const { resolved, value: delRsn } =
|
||||||
await prompt("삭제 사유를 입력해주세요.");
|
await prompt("삭제 사유를 입력해주세요.");
|
||||||
@@ -258,20 +250,21 @@ function RecentPage() {
|
|||||||
<div className="card card-border">
|
<div className="card card-border">
|
||||||
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
|
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
|
||||||
<Label className="mr-2">날짜</Label>
|
<Label className="mr-2">날짜</Label>
|
||||||
<Select value={presetDateRange} onValueChange={handlePresetDateRange}>
|
<DateRangePicker value={dateRange} onValueChange={setDateRange}>
|
||||||
<SelectTrigger className="w-[12rem]">
|
<div className="card card-border flex items-center gap-2">
|
||||||
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
|
<Select onValueChange={handlePresetDateRange}>
|
||||||
</SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectContent>
|
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
|
||||||
<SelectItem value="1W">일주일</SelectItem>
|
</SelectTrigger>
|
||||||
<SelectItem value="1M">1개월</SelectItem>
|
<SelectContent>
|
||||||
<SelectItem value="3M">3개월</SelectItem>
|
<SelectItem value="1W">일주일</SelectItem>
|
||||||
</SelectContent>
|
<SelectItem value="1M">1개월</SelectItem>
|
||||||
</Select>
|
<SelectItem value="3M">3개월</SelectItem>
|
||||||
<InputDateRangePicker
|
</SelectContent>
|
||||||
dateRange={dateRange}
|
</Select>
|
||||||
setDateRange={handleDateRangeChange}
|
</div>
|
||||||
/>
|
</DateRangePicker>
|
||||||
|
|
||||||
<Label className="ml-4 mr-2 text-label">게시판</Label>
|
<Label className="ml-4 mr-2 text-label">게시판</Label>
|
||||||
<Select
|
<Select
|
||||||
value={bbsId?.toString() || ""}
|
value={bbsId?.toString() || ""}
|
||||||
@@ -288,6 +281,7 @@ function RecentPage() {
|
|||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
<Label className="ml-4 mr-2 text-label">상태</Label>
|
<Label className="ml-4 mr-2 text-label">상태</Label>
|
||||||
<Select value={delYn} onValueChange={(value) => setDelYn(value)}>
|
<Select value={delYn} onValueChange={(value) => setDelYn(value)}>
|
||||||
<SelectTrigger className="w-[10rem]">
|
<SelectTrigger className="w-[10rem]">
|
||||||
@@ -298,6 +292,7 @@ function RecentPage() {
|
|||||||
<SelectItem value="Y">삭제</SelectItem>
|
<SelectItem value="Y">삭제</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
<Label className="ml-4 mr-2 text-label">검색</Label>
|
<Label className="ml-4 mr-2 text-label">검색</Label>
|
||||||
<Select
|
<Select
|
||||||
value={keywordType}
|
value={keywordType}
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
import { client } from "@/shared/api";
|
|
||||||
import type {
|
|
||||||
SiteAuthorityCreatePayload,
|
|
||||||
SiteAuthorityList,
|
|
||||||
SiteAuthoritySearchParams,
|
|
||||||
SiteAuthorityService,
|
|
||||||
} from "../model/site-authority-interface";
|
|
||||||
import { ENDPOINT } from "@/shared/config";
|
|
||||||
|
|
||||||
export class SiteAuthorityApiService implements SiteAuthorityService {
|
|
||||||
public async getSiteAuthorityList(
|
|
||||||
params: SiteAuthoritySearchParams,
|
|
||||||
): Promise<SiteAuthorityList> {
|
|
||||||
const response = await client.get<SiteAuthorityList>(
|
|
||||||
ENDPOINT.CMS_SERVICE.SITE_AUTHORITY,
|
|
||||||
{ params },
|
|
||||||
);
|
|
||||||
return response.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async createSiteAuthority(
|
|
||||||
data: SiteAuthorityCreatePayload[],
|
|
||||||
): Promise<void> {
|
|
||||||
await client.post(ENDPOINT.CMS_SERVICE.SITE_AUTHORITY, data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SiteAuthorityApi = new SiteAuthorityApiService();
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import type { PageableList, PageableParams } from "@/shared/model";
|
|
||||||
|
|
||||||
export interface SiteAuthoritySearchParams extends PageableParams {
|
|
||||||
siteId?: string;
|
|
||||||
authrtId?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SiteAuthority {
|
|
||||||
siteId: string;
|
|
||||||
authrtId: string;
|
|
||||||
siteNm: string;
|
|
||||||
mode: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SiteAuthorityCreatePayload = Omit<SiteAuthority, "siteNm">;
|
|
||||||
|
|
||||||
export type SiteAuthorityList = PageableList<SiteAuthority>;
|
|
||||||
|
|
||||||
export interface SiteAuthorityService {
|
|
||||||
getSiteAuthorityList: (
|
|
||||||
params: SiteAuthoritySearchParams,
|
|
||||||
) => Promise<SiteAuthorityList>;
|
|
||||||
createSiteAuthority: (data: SiteAuthorityCreatePayload[]) => Promise<void>;
|
|
||||||
}
|
|
||||||
@@ -3,13 +3,8 @@ import { useState } from "react";
|
|||||||
import type { Role } from "@/entities/role";
|
import type { Role } from "@/entities/role";
|
||||||
import type { ColumnDef } from "@tanstack/react-table";
|
import type { ColumnDef } from "@tanstack/react-table";
|
||||||
import { MappingRoleDialog } from "../ui/mapping-role-dialog";
|
import { MappingRoleDialog } from "../ui/mapping-role-dialog";
|
||||||
import { Button } from "@common/components";
|
|
||||||
|
|
||||||
export const createColumns = ({
|
export const columns: ColumnDef<Role>[] = [
|
||||||
deleteRole,
|
|
||||||
}: {
|
|
||||||
deleteRole: (roleId: string) => void;
|
|
||||||
}): ColumnDef<Role>[] => [
|
|
||||||
{
|
{
|
||||||
id: "no",
|
id: "no",
|
||||||
header: "NO",
|
header: "NO",
|
||||||
@@ -44,18 +39,7 @@ export const createColumns = ({
|
|||||||
{
|
{
|
||||||
id: "action",
|
id: "action",
|
||||||
header: "관리",
|
header: "관리",
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => <MappingRoleButton roleId={row.original.roleId} />,
|
||||||
<div className="flex items-center justify-center gap-2">
|
|
||||||
<MappingRoleButton roleId={row.original.roleId} />
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
color="red"
|
|
||||||
onClick={() => deleteRole(row.original.roleId)}
|
|
||||||
>
|
|
||||||
삭제
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { useState, useRef } from "react";
|
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
||||||
import { getCoreRowModel, type ColumnDef } from "@tanstack/react-table";
|
import { getCoreRowModel, type ColumnDef } from "@tanstack/react-table";
|
||||||
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
@@ -9,7 +8,6 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
Button,
|
Button,
|
||||||
Switch,
|
Switch,
|
||||||
Input,
|
|
||||||
} from "@common/components";
|
} from "@common/components";
|
||||||
import {
|
import {
|
||||||
RoleAuthorizationsApi,
|
RoleAuthorizationsApi,
|
||||||
@@ -33,18 +31,12 @@ function MappingRoleDialog({
|
|||||||
}: MappingRoleDialogProps) {
|
}: MappingRoleDialogProps) {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const alert = useAlertStore((state) => state.setMessage);
|
const alert = useAlertStore((state) => state.setMessage);
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
|
||||||
const [searchParams, setSearchParams] = useState({
|
|
||||||
keyword: "",
|
|
||||||
keywordType: "authorizationName",
|
|
||||||
});
|
|
||||||
|
|
||||||
const query = useQuery({
|
const query = useQuery({
|
||||||
queryKey: [ENDPOINT.USER_SERVICE.ROLE_AUTHORIZATIONS, searchParams],
|
queryKey: [ENDPOINT.USER_SERVICE.AUTHORIZATIONS],
|
||||||
queryFn: () =>
|
queryFn: () =>
|
||||||
RoleAuthorizationsApi.getRoleAuthorizationsListByRoleId({
|
RoleAuthorizationsApi.getRoleAuthorizationsListByRoleId({
|
||||||
roleId,
|
roleId,
|
||||||
...searchParams,
|
|
||||||
}),
|
}),
|
||||||
enabled: open,
|
enabled: open,
|
||||||
});
|
});
|
||||||
@@ -54,7 +46,7 @@ function MappingRoleDialog({
|
|||||||
RoleAuthorizationsApi.createRoleAuthorizations(data),
|
RoleAuthorizationsApi.createRoleAuthorizations(data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: [ENDPOINT.USER_SERVICE.ROLE_AUTHORIZATIONS],
|
queryKey: [ENDPOINT.USER_SERVICE.AUTHORIZATIONS],
|
||||||
});
|
});
|
||||||
alert("권한 맵핑이 완료되었습니다.");
|
alert("권한 맵핑이 완료되었습니다.");
|
||||||
},
|
},
|
||||||
@@ -74,26 +66,6 @@ function MappingRoleDialog({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const formData = new FormData(e.target as HTMLFormElement);
|
|
||||||
const keyword = formData.get("keyword") as string;
|
|
||||||
setSearchParams({
|
|
||||||
...searchParams,
|
|
||||||
keyword,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleReset = () => {
|
|
||||||
if (inputRef.current) {
|
|
||||||
inputRef.current.value = "";
|
|
||||||
}
|
|
||||||
setSearchParams({
|
|
||||||
...searchParams,
|
|
||||||
keyword: "",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const { tableComponent } = useDataTable({
|
const { tableComponent } = useDataTable({
|
||||||
query,
|
query,
|
||||||
tableOptions: {
|
tableOptions: {
|
||||||
@@ -107,31 +79,21 @@ function MappingRoleDialog({
|
|||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button size="sm">권한맵핑</Button>
|
<Button size="sm">권한맵핑</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-w-[80rem]" aria-describedby={undefined}>
|
<DialogContent className="max-w-[60rem]" aria-describedby={undefined}>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>모듈 목록</DialogTitle>
|
<DialogTitle>모듈 목록</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="flex items-center justify-between">
|
<div className="max-h-[40rem] overflow-y-auto">
|
||||||
<form onSubmit={handleSubmit} className="flex items-center gap-2">
|
<div className="mb-4 flex justify-end gap-2">
|
||||||
<Input
|
<Button size="sm" onClick={() => handleApplyAll("Y")}>
|
||||||
ref={inputRef}
|
일괄적용
|
||||||
type="text"
|
|
||||||
name="keyword"
|
|
||||||
className="w-[32rem]"
|
|
||||||
/>
|
|
||||||
<Button type="submit">검색</Button>
|
|
||||||
<Button type="button" color="white" onClick={handleReset}>
|
|
||||||
초기화
|
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
<Button size="sm" color="gray" onClick={() => handleApplyAll("N")}>
|
||||||
<div className="flex items-center justify-end gap-2">
|
|
||||||
<Button onClick={() => handleApplyAll("Y")}>일괄적용</Button>
|
|
||||||
<Button color="gray" onClick={() => handleApplyAll("N")}>
|
|
||||||
일괄해제
|
일괄해제
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
{tableComponent}
|
||||||
</div>
|
</div>
|
||||||
<div className="max-h-[60rem] overflow-y-auto">{tableComponent}</div>
|
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useState } from "react";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
||||||
import type { UseFormReturn } from "react-hook-form";
|
import type { UseFormReturn } from "react-hook-form";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Checkbox,
|
|
||||||
Input,
|
Input,
|
||||||
Label,
|
Label,
|
||||||
RadioGroup,
|
RadioGroup,
|
||||||
@@ -18,9 +16,6 @@ import { FormWrapper } from "@/widgets/form-wrapper";
|
|||||||
import { RoleApi, type RoleFormData } from "@/entities/role";
|
import { RoleApi, type RoleFormData } from "@/entities/role";
|
||||||
import { FormField, FormItem, FormControl } from "@/shared/ui/form";
|
import { FormField, FormItem, FormControl } from "@/shared/ui/form";
|
||||||
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
|
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
|
||||||
import { ENDPOINT } from "@/shared/config";
|
|
||||||
import { SiteAuthorityApi } from "../api/site-authority-service";
|
|
||||||
import type { SiteAuthorityCreatePayload } from "../model/site-authority-interface";
|
|
||||||
|
|
||||||
type RoleFormProps = {
|
type RoleFormProps = {
|
||||||
roleId?: string;
|
roleId?: string;
|
||||||
@@ -29,24 +24,12 @@ type RoleFormProps = {
|
|||||||
handleDelete?: () => Promise<void>;
|
handleDelete?: () => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
function RoleForm({ roleId, form, handleSave, ...props }: RoleFormProps) {
|
function RoleForm({ roleId, form, ...props }: RoleFormProps) {
|
||||||
const isModifyMode = !!roleId;
|
const isModifyMode = !!roleId;
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const alert = useAlertStore((state) => state.setMessage);
|
|
||||||
const [isValidRoleId, setIsValidRoleId] = useState(false);
|
|
||||||
const [checkedSiteAuthority, setCheckedSiteAuthority] = useState<string[]>(
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: siteAuthorityList } = useQuery({
|
const alert = useAlertStore((state) => state.setMessage);
|
||||||
queryKey: [ENDPOINT.CMS_SERVICE.SITE_AUTHORITY, roleId],
|
|
||||||
queryFn: () =>
|
const [isValidRoleId, setIsValidRoleId] = useState(false);
|
||||||
SiteAuthorityApi.getSiteAuthorityList({
|
|
||||||
authrtId: roleId,
|
|
||||||
page: 0,
|
|
||||||
size: 100,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const { mutateAsync: checkDuplicateRoleId } = useMutationWrapper({
|
const { mutateAsync: checkDuplicateRoleId } = useMutationWrapper({
|
||||||
mutationFn: (roleId: string) => RoleApi.checkDuplicateRoleId(roleId),
|
mutationFn: (roleId: string) => RoleApi.checkDuplicateRoleId(roleId),
|
||||||
@@ -60,49 +43,8 @@ function RoleForm({ roleId, form, handleSave, ...props }: RoleFormProps) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSiteAuthorityChecked = (checked: boolean, siteId: string) => {
|
|
||||||
if (checked) {
|
|
||||||
setCheckedSiteAuthority((prev) => [...prev, siteId]);
|
|
||||||
} else {
|
|
||||||
setCheckedSiteAuthority((prev) => prev.filter((id) => id !== siteId));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const saveInterceptor = async (data: RoleFormData) => {
|
|
||||||
const siteAuthorityPayload: SiteAuthorityCreatePayload[] =
|
|
||||||
checkedSiteAuthority.map((siteId) => ({
|
|
||||||
siteId,
|
|
||||||
authrtId: data.roleId,
|
|
||||||
mode: "C",
|
|
||||||
}));
|
|
||||||
|
|
||||||
try {
|
|
||||||
await handleSave(data);
|
|
||||||
if (data.mngrYn === "N") return;
|
|
||||||
await SiteAuthorityApi.createSiteAuthority(siteAuthorityPayload);
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: [ENDPOINT.CMS_SERVICE.SITE_AUTHORITY],
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!siteAuthorityList?.content.length) return;
|
|
||||||
const checkedSiteAuthority = siteAuthorityList.content.filter(
|
|
||||||
(item) => item.mode === "R",
|
|
||||||
);
|
|
||||||
setCheckedSiteAuthority(checkedSiteAuthority.map((item) => item.siteId));
|
|
||||||
}, [siteAuthorityList]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormWrapper
|
<FormWrapper form={form} isModifyMode={isModifyMode} {...props}>
|
||||||
form={form}
|
|
||||||
isModifyMode={isModifyMode}
|
|
||||||
handleSave={saveInterceptor}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<Table variant="secondary">
|
<Table variant="secondary">
|
||||||
<colgroup>
|
<colgroup>
|
||||||
@@ -223,29 +165,6 @@ function RoleForm({ roleId, form, handleSave, ...props }: RoleFormProps) {
|
|||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
{form.watch("mngrYn") === "Y" && (
|
|
||||||
<TableRow>
|
|
||||||
<TableHead data-required>관리사이트</TableHead>
|
|
||||||
<TableCell className="border">
|
|
||||||
<div className="flex flex-wrap items-center gap-4">
|
|
||||||
{siteAuthorityList?.content.map((item) => (
|
|
||||||
<div
|
|
||||||
key={item.siteId}
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<Checkbox
|
|
||||||
defaultChecked={item.mode === "R"}
|
|
||||||
onCheckedChange={(checked: boolean) => {
|
|
||||||
handleSiteAuthorityChecked(checked, item.siteId);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<span className="whitespace-nowrap">{item.siteNm}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
)}
|
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead data-required>사용여부</TableHead>
|
<TableHead data-required>사용여부</TableHead>
|
||||||
<TableCell className="border">
|
<TableCell className="border">
|
||||||
|
|||||||
@@ -7,12 +7,11 @@ import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
|||||||
import { Button, Input, Label } from "@common/components";
|
import { Button, Input, Label } from "@common/components";
|
||||||
import { RoleApi, type Role, type RoleParams } from "@/entities/role";
|
import { RoleApi, type Role, type RoleParams } from "@/entities/role";
|
||||||
import { ENDPOINT } from "@/shared/config";
|
import { ENDPOINT } from "@/shared/config";
|
||||||
import { useAlertStore, useDataTable, useMutationWrapper } from "@/shared/lib";
|
import { useDataTable } from "@/shared/lib";
|
||||||
import { createColumns } from "../model/table-columns";
|
import { columns } from "../model/table-columns";
|
||||||
|
|
||||||
function RolePage() {
|
function RolePage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const alert = useAlertStore((state) => state.setMessage);
|
|
||||||
|
|
||||||
const [keyword, setKeyword] = useState("");
|
const [keyword, setKeyword] = useState("");
|
||||||
const [searchParams, setSearchParams] = useState<RoleParams>({
|
const [searchParams, setSearchParams] = useState<RoleParams>({
|
||||||
@@ -34,13 +33,6 @@ function RolePage() {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutateAsync: deleteRole } = useMutationWrapper({
|
|
||||||
mutationFn: (roleId: string) => RoleApi.deleteRole(roleId),
|
|
||||||
onSuccess: () => {
|
|
||||||
alert("삭제되었습니다.", query.refetch);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleRowClick = (row: Role) => {
|
const handleRowClick = (row: Role) => {
|
||||||
router.push(`/role/register/${row.roleId}`);
|
router.push(`/role/register/${row.roleId}`);
|
||||||
};
|
};
|
||||||
@@ -48,9 +40,7 @@ function RolePage() {
|
|||||||
const { tableComponent } = useDataTable({
|
const { tableComponent } = useDataTable({
|
||||||
query,
|
query,
|
||||||
tableOptions: {
|
tableOptions: {
|
||||||
columns: createColumns({
|
columns,
|
||||||
deleteRole,
|
|
||||||
}),
|
|
||||||
state: {
|
state: {
|
||||||
pagination,
|
pagination,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
import type { NodeModel } from "@minoru/react-dnd-treeview";
|
|
||||||
import type { TreeMenu } from "@/entities/tree-menu";
|
|
||||||
|
|
||||||
export const generateMenuTreeData = (
|
|
||||||
data: TreeMenu[],
|
|
||||||
): NodeModel<TreeMenu>[] => {
|
|
||||||
return data.reduce<NodeModel<TreeMenu>[]>((acc, menu) => {
|
|
||||||
const node: NodeModel<TreeMenu> = {
|
|
||||||
id: menu.menuCd ?? 0,
|
|
||||||
parent: menu.parentId ?? 0,
|
|
||||||
text: menu.menuSubKornNm ?? "",
|
|
||||||
droppable: true,
|
|
||||||
data: menu,
|
|
||||||
};
|
|
||||||
|
|
||||||
acc.push(node);
|
|
||||||
|
|
||||||
if (menu.children?.length) {
|
|
||||||
acc.push(...generateMenuTreeData(menu.children));
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, []);
|
|
||||||
};
|
|
||||||
@@ -1,32 +1,6 @@
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { memo, useState } from "react";
|
|
||||||
import { useFieldArray, useForm } from "react-hook-form";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import { type ColumnDef } from "@tanstack/react-table";
|
import { type ColumnDef } from "@tanstack/react-table";
|
||||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Checkbox,
|
|
||||||
Dialog,
|
|
||||||
DialogClose,
|
|
||||||
DialogContent,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from "@common/components";
|
|
||||||
import type { Site } from "@/entities/site";
|
import type { Site } from "@/entities/site";
|
||||||
import { TreeMenuApi } from "@/entities/tree-menu";
|
|
||||||
import {
|
|
||||||
MenuApi,
|
|
||||||
siteMapFormSchema,
|
|
||||||
type SiteMapFormData,
|
|
||||||
} from "@/entities/menu";
|
|
||||||
import { ENDPOINT } from "@/shared/config";
|
|
||||||
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
|
|
||||||
import { MenuTree } from "../ui/menu-tree";
|
|
||||||
import { generateMenuTreeData } from "../lib/utils";
|
|
||||||
|
|
||||||
export const columns: ColumnDef<Site>[] = [
|
export const columns: ColumnDef<Site>[] = [
|
||||||
{
|
{
|
||||||
@@ -71,81 +45,4 @@ export const columns: ColumnDef<Site>[] = [
|
|||||||
return date.format("YYYY-MM-DD");
|
return date.format("YYYY-MM-DD");
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: "action",
|
|
||||||
header: "관리",
|
|
||||||
cell: ({ row }) => {
|
|
||||||
return (
|
|
||||||
<div onClick={(e) => e.stopPropagation()}>
|
|
||||||
<ActionCell data={row.original} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const ActionCell = memo(({ data }: { data: Site }) => {
|
|
||||||
const { confirm } = useConfirm();
|
|
||||||
const alert = useAlertStore((state) => state.setMessage);
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
|
|
||||||
const { data: menuTreeData, refetch } = useQuery({
|
|
||||||
queryKey: [ENDPOINT.CMS_SERVICE.MENUS, data.siteId],
|
|
||||||
queryFn: () => TreeMenuApi.getTreeMenuList(data.siteId),
|
|
||||||
select: (data) => generateMenuTreeData(data),
|
|
||||||
enabled: !!data.siteId && open,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { mutate: publishSiteMap } = useMutationWrapper({
|
|
||||||
mutationFn: (data: SiteMapFormData["siteMap"]) =>
|
|
||||||
MenuApi.publishSiteMap(data),
|
|
||||||
onSuccess: () => {
|
|
||||||
alert("사이트맵이 저장되었습니다.", () => {
|
|
||||||
setOpen(false);
|
|
||||||
refetch();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const form = useForm<SiteMapFormData>({
|
|
||||||
resolver: zodResolver(siteMapFormSchema),
|
|
||||||
});
|
|
||||||
const fieldArray = useFieldArray({
|
|
||||||
control: form.control,
|
|
||||||
name: "siteMap",
|
|
||||||
});
|
|
||||||
|
|
||||||
const handlePublishSiteMap = async () => {
|
|
||||||
if (await confirm("저장하시겠습니까?")) {
|
|
||||||
publishSiteMap(form.getValues("siteMap"));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Button size="sm">사이트맵</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>{data.siteNm} - 사이트맵 관리</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
<div>
|
|
||||||
<div className="flex items-center justify-end gap-2 p-2">
|
|
||||||
<span className="text-xl">표시여부</span>
|
|
||||||
<Checkbox checked />
|
|
||||||
</div>
|
|
||||||
<MenuTree tree={menuTreeData ?? []} fieldArray={fieldArray} />
|
|
||||||
</div>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button onClick={handlePublishSiteMap}>저장</Button>
|
|
||||||
<DialogClose asChild>
|
|
||||||
<Button color="white">취소</Button>
|
|
||||||
</DialogClose>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
ActionCell.displayName = "ActionCell";
|
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
import type { ComponentProps } from "react";
|
|
||||||
import { Button, Input } from "@common/components";
|
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip";
|
|
||||||
|
|
||||||
type EditableNodeProps = {
|
|
||||||
depth?: number;
|
|
||||||
onSave: () => void;
|
|
||||||
onCancel: () => void;
|
|
||||||
} & ComponentProps<typeof Input>;
|
|
||||||
|
|
||||||
function EditableNode({
|
|
||||||
depth,
|
|
||||||
onSave,
|
|
||||||
onCancel,
|
|
||||||
...props
|
|
||||||
}: EditableNodeProps) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="flex items-center gap-2 rounded-sm bg-accent p-2"
|
|
||||||
style={{ marginInlineStart: !depth ? 0 : depth * 5 + 5 }}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<Input {...props} className="h-[2.5rem] !text-[1.3rem] text-foreground" />
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="size-8 !bg-transparent"
|
|
||||||
onClick={onSave}
|
|
||||||
aria-label="저장"
|
|
||||||
>
|
|
||||||
<i className="diveicon di-check text-2xl" />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>저장</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="size-8 !bg-transparent"
|
|
||||||
onClick={onCancel}
|
|
||||||
aria-label="취소"
|
|
||||||
>
|
|
||||||
<i className="diveicon di-x" />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>취소</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { EditableNode };
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import type { NodeModel } from "@minoru/react-dnd-treeview";
|
|
||||||
import type { UseFieldArrayReturn } from "react-hook-form";
|
|
||||||
import { Button, Checkbox, cn } from "@common/components";
|
|
||||||
import { type TreeMenu } from "@/entities/tree-menu";
|
|
||||||
import type { SiteMapFormData, SiteMapPayloadData } from "@/entities/menu";
|
|
||||||
import { EditableNode } from "./editable-node";
|
|
||||||
|
|
||||||
type MenuNodeProps = {
|
|
||||||
node: NodeModel<TreeMenu>;
|
|
||||||
depth: number;
|
|
||||||
isOpen: boolean;
|
|
||||||
fieldArray: UseFieldArrayReturn<SiteMapFormData>;
|
|
||||||
onToggle: (id: NodeModel["id"]) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
function MenuNode({
|
|
||||||
node,
|
|
||||||
depth,
|
|
||||||
isOpen,
|
|
||||||
fieldArray,
|
|
||||||
onToggle,
|
|
||||||
}: MenuNodeProps) {
|
|
||||||
const { id, text, data } = node;
|
|
||||||
const indent = depth * 10 + 5;
|
|
||||||
|
|
||||||
const [editMode, setEditMode] = useState(false);
|
|
||||||
const [nodeName, setNodeName] = useState(text);
|
|
||||||
const [otptYn, setOtptYn] = useState(data?.otptYn ?? "N");
|
|
||||||
|
|
||||||
const updateField = (id: number, newField: SiteMapPayloadData) => {
|
|
||||||
const existingFieldIndex = fieldArray.fields.findIndex(
|
|
||||||
(field) => field.menuCd === id,
|
|
||||||
);
|
|
||||||
if (existingFieldIndex !== -1) {
|
|
||||||
fieldArray.update(existingFieldIndex, newField);
|
|
||||||
} else {
|
|
||||||
fieldArray.append(newField);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleToggle = () => onToggle(id);
|
|
||||||
|
|
||||||
const handleSave = async () => {
|
|
||||||
const newField = {
|
|
||||||
menuCd: id as number,
|
|
||||||
menuSubKornNm: nodeName,
|
|
||||||
otptYn,
|
|
||||||
};
|
|
||||||
updateField(id as number, newField);
|
|
||||||
setEditMode(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
setEditMode(false);
|
|
||||||
setNodeName(text);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOtptYnChange = (checked: boolean) => {
|
|
||||||
const newField = {
|
|
||||||
menuCd: id as number,
|
|
||||||
menuSubKornNm: nodeName,
|
|
||||||
otptYn: checked ? "Y" : "N",
|
|
||||||
};
|
|
||||||
updateField(id as number, newField);
|
|
||||||
setOtptYn(checked ? "Y" : "N");
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{ paddingInlineStart: indent }}
|
|
||||||
className={cn(
|
|
||||||
"group flex cursor-pointer items-center gap-2 rounded-sm p-2 hover:bg-accent",
|
|
||||||
editMode && "p-0 pr-2 hover:bg-transparent",
|
|
||||||
)}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
handleToggle();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{editMode ? (
|
|
||||||
<EditableNode
|
|
||||||
value={nodeName}
|
|
||||||
onChange={(e) => setNodeName(e.target.value)}
|
|
||||||
onSave={handleSave}
|
|
||||||
onCancel={handleCancel}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="flex w-full items-center justify-between">
|
|
||||||
<div className="flex items-center">
|
|
||||||
{data?.children?.length && (
|
|
||||||
<i
|
|
||||||
className={cn(
|
|
||||||
"diveicon di-arrow-forward mr-2 text-base text-label",
|
|
||||||
isOpen && "rotate-90",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<span className="text-[1.4rem] font-medium text-label">
|
|
||||||
{nodeName}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="invisible !bg-transparent group-hover:visible"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setEditMode(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<i className="diveicon di-pencil" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<Checkbox
|
|
||||||
className="ml-auto"
|
|
||||||
checked={otptYn === "Y"}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
}}
|
|
||||||
onCheckedChange={handleOtptYnChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { MenuNode };
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useRef } from "react";
|
|
||||||
import type { UseFieldArrayReturn } from "react-hook-form";
|
|
||||||
import {
|
|
||||||
Tree,
|
|
||||||
type NodeModel,
|
|
||||||
type TreeMethods,
|
|
||||||
} from "@minoru/react-dnd-treeview";
|
|
||||||
import { Button } from "@common/components";
|
|
||||||
import type { SiteMapFormData } from "@/entities/menu";
|
|
||||||
import { type TreeMenu } from "@/entities/tree-menu";
|
|
||||||
import { MenuNode } from "./menu-node";
|
|
||||||
|
|
||||||
type MenuTreeProps = {
|
|
||||||
tree: NodeModel<TreeMenu>[];
|
|
||||||
fieldArray: UseFieldArrayReturn<SiteMapFormData>;
|
|
||||||
};
|
|
||||||
|
|
||||||
function MenuTree({ tree, fieldArray }: MenuTreeProps) {
|
|
||||||
const treeMethods = useRef<TreeMethods>(null);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="max-h-[calc(100vh-33.5rem)] min-h-[40vh] overflow-auto">
|
|
||||||
<Tree
|
|
||||||
tree={tree}
|
|
||||||
rootId={0}
|
|
||||||
ref={treeMethods}
|
|
||||||
render={(node: NodeModel<TreeMenu>, props) => {
|
|
||||||
return <MenuNode node={node} fieldArray={fieldArray} {...props} />;
|
|
||||||
}}
|
|
||||||
onDrop={() => {}}
|
|
||||||
canDrag={() => false}
|
|
||||||
canDrop={() => false}
|
|
||||||
classes={{
|
|
||||||
container: "flex flex-col",
|
|
||||||
dropTarget: "!bg-[rgba(32,148,250,0.5)] rounded-sm",
|
|
||||||
placeholder: "relative",
|
|
||||||
}}
|
|
||||||
sort={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button
|
|
||||||
color="lightGray"
|
|
||||||
className="flex-1 items-center px-0"
|
|
||||||
onClick={() => {
|
|
||||||
treeMethods.current?.openAll();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<i className="diveicon di-arrows-angle-expand" />
|
|
||||||
전체펼침
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
color="lightGray"
|
|
||||||
className="flex-1 items-center px-0"
|
|
||||||
onClick={() => {
|
|
||||||
treeMethods.current?.closeAll();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<i className="diveicon di-arrows-angle-contract" />
|
|
||||||
전체접기
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { MenuTree };
|
|
||||||
@@ -36,14 +36,9 @@ export const columns: ColumnDef<Post>[] = [
|
|||||||
accessorKey: "pstgPrdSeNm",
|
accessorKey: "pstgPrdSeNm",
|
||||||
header: "게시기간",
|
header: "게시기간",
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const pstgPrdSeCd = row.original.pstgPrdSeCd;
|
|
||||||
const pstgPrdSeNm = row.original.pstgPrdSeNm;
|
const pstgPrdSeNm = row.original.pstgPrdSeNm;
|
||||||
const pstgBgngDt = row.original.pstgBgngDt;
|
const pstgBgngDt = row.original.pstgBgngDt;
|
||||||
const pstgEndDt = row.original.pstgEndDt;
|
const pstgEndDt = row.original.pstgEndDt;
|
||||||
|
|
||||||
if (pstgPrdSeCd === "99") {
|
|
||||||
return pstgPrdSeNm;
|
|
||||||
}
|
|
||||||
return `${pstgPrdSeNm} (${dayjs(pstgBgngDt).format("YYYY-MM-DD")} ~ ${dayjs(pstgEndDt).format("YYYY-MM-DD")})`;
|
return `${pstgPrdSeNm} (${dayjs(pstgBgngDt).format("YYYY-MM-DD")} ~ ${dayjs(pstgEndDt).format("YYYY-MM-DD")})`;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export { UserStatisticsApi } from "./user-statistics-service";
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
import {
|
|
||||||
type AccessLog,
|
|
||||||
type AccessLogParams,
|
|
||||||
type AccessLogService,
|
|
||||||
type PageableList,
|
|
||||||
} from "../model/user-statistics-interface";
|
|
||||||
|
|
||||||
// 목 데이터
|
|
||||||
const mockAccessLogs: AccessLog[] = [
|
|
||||||
{
|
|
||||||
no: 1,
|
|
||||||
accessTime: "2024-01-15 14:30:25",
|
|
||||||
accessIp: "192.168.1.100",
|
|
||||||
accessRegion: "서울특별시",
|
|
||||||
urlPath: "/index",
|
|
||||||
browser: "Chrome",
|
|
||||||
site: "SITE1",
|
|
||||||
accessType: "메인",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
no: 2,
|
|
||||||
accessTime: "2024-01-15 14:28:15",
|
|
||||||
accessIp: "203.241.50.123",
|
|
||||||
accessRegion: "부산광역시",
|
|
||||||
urlPath: "/board/list",
|
|
||||||
browser: "Edge",
|
|
||||||
site: "SITE2",
|
|
||||||
accessType: "게시판",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
no: 3,
|
|
||||||
accessTime: "2024-01-15 14:25:42",
|
|
||||||
accessIp: "175.223.45.67",
|
|
||||||
accessRegion: "대구광역시",
|
|
||||||
urlPath: "/statistics",
|
|
||||||
browser: "Safari",
|
|
||||||
site: "SITE1",
|
|
||||||
accessType: "통계",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
no: 4,
|
|
||||||
accessTime: "2024-01-15 14:22:18",
|
|
||||||
accessIp: "211.252.87.45",
|
|
||||||
accessRegion: "인천광역시",
|
|
||||||
urlPath: "/contents/view",
|
|
||||||
browser: "Firefox",
|
|
||||||
site: "SITE3",
|
|
||||||
accessType: "컨텐츠",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
no: 5,
|
|
||||||
accessTime: "2024-01-15 14:20:33",
|
|
||||||
accessIp: "121.254.12.89",
|
|
||||||
accessRegion: "광주광역시",
|
|
||||||
urlPath: "/search",
|
|
||||||
browser: "Chrome",
|
|
||||||
site: "SITE1",
|
|
||||||
accessType: "검색",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// 목 API 서비스
|
|
||||||
class UserStatisticsApiImpl implements AccessLogService {
|
|
||||||
async getAccessLogList(
|
|
||||||
params: AccessLogParams,
|
|
||||||
): Promise<PageableList<AccessLog>> {
|
|
||||||
// 실제 API 호출 대신 목 데이터 반환
|
|
||||||
const { page = 0, size = 10 } = params;
|
|
||||||
|
|
||||||
// 필터링 로직 (실제로는 서버에서 처리)
|
|
||||||
let filteredLogs = [...mockAccessLogs];
|
|
||||||
|
|
||||||
if (params.urlPath) {
|
|
||||||
filteredLogs = filteredLogs.filter((log) =>
|
|
||||||
log.urlPath.toLowerCase().includes(params.urlPath!.toLowerCase()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (params.accessIp) {
|
|
||||||
filteredLogs = filteredLogs.filter((log) =>
|
|
||||||
log.accessIp.includes(params.accessIp!),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (params.accessRegion) {
|
|
||||||
filteredLogs = filteredLogs.filter((log) =>
|
|
||||||
log.accessRegion.includes(params.accessRegion!),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (params.browser) {
|
|
||||||
filteredLogs = filteredLogs.filter(
|
|
||||||
(log) => log.browser.toLowerCase() === params.browser!.toLowerCase(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (params.siteId) {
|
|
||||||
filteredLogs = filteredLogs.filter((log) => log.site === params.siteId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 페이지네이션
|
|
||||||
const start = page * size;
|
|
||||||
const end = start + size;
|
|
||||||
const paginatedLogs = filteredLogs.slice(start, end);
|
|
||||||
|
|
||||||
// 목 응답 생성
|
|
||||||
return {
|
|
||||||
content: paginatedLogs,
|
|
||||||
pageable: {
|
|
||||||
pageNumber: page,
|
|
||||||
pageSize: size,
|
|
||||||
sort: {
|
|
||||||
empty: true,
|
|
||||||
sorted: false,
|
|
||||||
unsorted: true,
|
|
||||||
},
|
|
||||||
offset: page * size,
|
|
||||||
paged: true,
|
|
||||||
unpaged: false,
|
|
||||||
},
|
|
||||||
last: (page + 1) * size >= filteredLogs.length,
|
|
||||||
totalPages: Math.ceil(filteredLogs.length / size),
|
|
||||||
totalElements: filteredLogs.length,
|
|
||||||
size,
|
|
||||||
number: page,
|
|
||||||
sort: {
|
|
||||||
empty: true,
|
|
||||||
sorted: false,
|
|
||||||
unsorted: true,
|
|
||||||
},
|
|
||||||
first: page === 0,
|
|
||||||
numberOfElements: paginatedLogs.length,
|
|
||||||
empty: paginatedLogs.length === 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const UserStatisticsApi = new UserStatisticsApiImpl();
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { UserStatisticsPage } from "./ui/user-statistics-page";
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
import { type ChartData, type ChartOptions } from "chart.js";
|
|
||||||
|
|
||||||
// 일별 접속 현황 차트 데이터
|
|
||||||
export const monthlyStatisticsData = {
|
|
||||||
labels: [
|
|
||||||
"1일",
|
|
||||||
"2일",
|
|
||||||
"3일",
|
|
||||||
"4일",
|
|
||||||
"5일",
|
|
||||||
"6일",
|
|
||||||
"7일",
|
|
||||||
"8일",
|
|
||||||
"9일",
|
|
||||||
"10일",
|
|
||||||
"11일",
|
|
||||||
"12일",
|
|
||||||
"13일",
|
|
||||||
"14일",
|
|
||||||
"15일",
|
|
||||||
],
|
|
||||||
datasets: [
|
|
||||||
{
|
|
||||||
type: "bar" as const,
|
|
||||||
label: "페이지뷰",
|
|
||||||
data: [
|
|
||||||
850, 1000, 750, 750, 700, 750, 780, 800, 850, 900, 1200, 1700, 2300,
|
|
||||||
2100, 2600,
|
|
||||||
],
|
|
||||||
backgroundColor: "rgba(54, 162, 235, 0.8)",
|
|
||||||
order: 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "line" as const,
|
|
||||||
label: "신규 접속자",
|
|
||||||
data: [
|
|
||||||
700, 600, 500, 550, 500, 550, 500, 550, 600, 850, 1200, 1500, 1700,
|
|
||||||
2300, 2200,
|
|
||||||
],
|
|
||||||
borderColor: "rgba(255, 99, 132, 0.8)",
|
|
||||||
borderWidth: 2,
|
|
||||||
tension: 0.4,
|
|
||||||
pointRadius: 3,
|
|
||||||
order: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "line" as const,
|
|
||||||
label: "총 접속자",
|
|
||||||
data: [
|
|
||||||
200, 300, 400, 500, 600, 500, 550, 500, 400, 500, 350, 800, 900, 1200,
|
|
||||||
1300,
|
|
||||||
],
|
|
||||||
borderColor: "rgba(75, 192, 192, 0.8)",
|
|
||||||
borderWidth: 2,
|
|
||||||
tension: 0.4,
|
|
||||||
pointRadius: 3,
|
|
||||||
order: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const monthlyStatisticsOptions: ChartOptions<"bar"> = {
|
|
||||||
responsive: true,
|
|
||||||
maintainAspectRatio: false,
|
|
||||||
plugins: {
|
|
||||||
legend: {
|
|
||||||
position: "top" as const,
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
mode: "index" as const,
|
|
||||||
intersect: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// 사이트별 접속자 차트 데이터
|
|
||||||
export const siteVisitorsData: ChartData<"bar"> = {
|
|
||||||
labels: ["SITE1", "SITE2", "SITE3", "SITE4", "SITE5"],
|
|
||||||
datasets: [
|
|
||||||
{
|
|
||||||
label: "사이트별 접속자",
|
|
||||||
data: [2500, 2000, 1500, 1200, 800],
|
|
||||||
backgroundColor: [
|
|
||||||
"rgba(54, 162, 235, 0.8)",
|
|
||||||
"rgba(153, 102, 255, 0.8)",
|
|
||||||
"rgba(255, 99, 132, 0.8)",
|
|
||||||
"rgba(255, 159, 64, 0.8)",
|
|
||||||
"rgba(255, 205, 86, 0.8)",
|
|
||||||
],
|
|
||||||
borderWidth: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const siteVisitorsOptions: ChartOptions<"bar"> = {
|
|
||||||
responsive: true,
|
|
||||||
maintainAspectRatio: false,
|
|
||||||
indexAxis: "y" as const,
|
|
||||||
plugins: {
|
|
||||||
legend: {
|
|
||||||
display: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// 브라우저별 접속자 차트 데이터
|
|
||||||
export const browserStatisticsData: ChartData<"doughnut"> = {
|
|
||||||
labels: ["Chrome", "Edge", "Safari", "Firefox", "Internet Explorer"],
|
|
||||||
datasets: [
|
|
||||||
{
|
|
||||||
data: [45, 25, 15, 10, 5],
|
|
||||||
backgroundColor: [
|
|
||||||
"rgba(54, 162, 235, 0.8)",
|
|
||||||
"rgba(153, 102, 255, 0.8)",
|
|
||||||
"rgba(255, 99, 132, 0.8)",
|
|
||||||
"rgba(255, 159, 64, 0.8)",
|
|
||||||
"rgba(255, 205, 86, 0.8)",
|
|
||||||
],
|
|
||||||
borderColor: [
|
|
||||||
"rgba(54, 162, 235, 1)",
|
|
||||||
"rgba(153, 102, 255, 1)",
|
|
||||||
"rgba(255, 99, 132, 1)",
|
|
||||||
"rgba(255, 159, 64, 1)",
|
|
||||||
"rgba(255, 205, 86, 1)",
|
|
||||||
],
|
|
||||||
borderWidth: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const browserStatisticsOptions: ChartOptions<"doughnut"> = {
|
|
||||||
responsive: true,
|
|
||||||
maintainAspectRatio: false,
|
|
||||||
plugins: {
|
|
||||||
legend: {
|
|
||||||
position: "bottom" as const,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const chartData = {
|
|
||||||
monthlyStatisticsData,
|
|
||||||
monthlyStatisticsOptions,
|
|
||||||
siteVisitorsData,
|
|
||||||
siteVisitorsOptions,
|
|
||||||
browserStatisticsData,
|
|
||||||
browserStatisticsOptions,
|
|
||||||
};
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import { createColumnHelper } from "@tanstack/react-table";
|
|
||||||
import { type AccessLog } from "./user-statistics-interface";
|
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<AccessLog>();
|
|
||||||
|
|
||||||
export const columns = [
|
|
||||||
columnHelper.accessor("no", {
|
|
||||||
header: "No",
|
|
||||||
cell: (info) => info.getValue(),
|
|
||||||
}),
|
|
||||||
columnHelper.accessor("accessTime", {
|
|
||||||
header: "접속일시",
|
|
||||||
cell: (info) => info.getValue(),
|
|
||||||
}),
|
|
||||||
columnHelper.accessor("accessIp", {
|
|
||||||
header: "접속IP",
|
|
||||||
cell: (info) => info.getValue(),
|
|
||||||
}),
|
|
||||||
columnHelper.accessor("accessRegion", {
|
|
||||||
header: "접속지역",
|
|
||||||
cell: (info) => info.getValue(),
|
|
||||||
}),
|
|
||||||
columnHelper.accessor("urlPath", {
|
|
||||||
header: "URL경로",
|
|
||||||
cell: (info) => info.getValue(),
|
|
||||||
}),
|
|
||||||
columnHelper.accessor("browser", {
|
|
||||||
header: "브라우저",
|
|
||||||
cell: (info) => info.getValue(),
|
|
||||||
}),
|
|
||||||
columnHelper.accessor("site", {
|
|
||||||
header: "사이트",
|
|
||||||
cell: (info) => info.getValue(),
|
|
||||||
}),
|
|
||||||
columnHelper.accessor("accessType", {
|
|
||||||
header: "메뉴명",
|
|
||||||
cell: (info) => info.getValue(),
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
export interface AccessLog {
|
|
||||||
no: number;
|
|
||||||
accessTime: string;
|
|
||||||
accessIp: string;
|
|
||||||
accessRegion: string;
|
|
||||||
urlPath: string;
|
|
||||||
browser: string;
|
|
||||||
site: string;
|
|
||||||
accessType: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AccessLogParams {
|
|
||||||
page?: number;
|
|
||||||
size?: number;
|
|
||||||
urlPath?: string;
|
|
||||||
accessIp?: string;
|
|
||||||
accessRegion?: string;
|
|
||||||
browser?: string;
|
|
||||||
accessDateStart?: string;
|
|
||||||
accessDateEnd?: string;
|
|
||||||
siteId?: string;
|
|
||||||
sort?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AccessLogService {
|
|
||||||
getAccessLogList(params: AccessLogParams): Promise<PageableList<AccessLog>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PageableList<T> {
|
|
||||||
content: T[];
|
|
||||||
pageable: {
|
|
||||||
pageNumber: number;
|
|
||||||
pageSize: number;
|
|
||||||
sort: {
|
|
||||||
empty: boolean;
|
|
||||||
sorted: boolean;
|
|
||||||
unsorted: boolean;
|
|
||||||
};
|
|
||||||
offset: number;
|
|
||||||
paged: boolean;
|
|
||||||
unpaged: boolean;
|
|
||||||
};
|
|
||||||
last: boolean;
|
|
||||||
totalPages: number;
|
|
||||||
totalElements: number;
|
|
||||||
size: number;
|
|
||||||
number: number;
|
|
||||||
sort: {
|
|
||||||
empty: boolean;
|
|
||||||
sorted: boolean;
|
|
||||||
unsorted: boolean;
|
|
||||||
};
|
|
||||||
first: boolean;
|
|
||||||
numberOfElements: number;
|
|
||||||
empty: boolean;
|
|
||||||
}
|
|
||||||
@@ -1,324 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import dayjs from "dayjs";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
|
||||||
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
|
||||||
import {
|
|
||||||
Chart as ChartJS,
|
|
||||||
CategoryScale,
|
|
||||||
LinearScale,
|
|
||||||
PointElement,
|
|
||||||
LineElement,
|
|
||||||
BarElement,
|
|
||||||
ArcElement,
|
|
||||||
Filler,
|
|
||||||
Legend,
|
|
||||||
Tooltip,
|
|
||||||
type ChartData,
|
|
||||||
} from "chart.js";
|
|
||||||
import { Bar, Doughnut } from "react-chartjs-2";
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Input,
|
|
||||||
Label,
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@common/components";
|
|
||||||
import { SiteSelect } from "@/entities/site";
|
|
||||||
import { useDataTable } from "@/shared/lib";
|
|
||||||
import { InputDateRangePicker } from "@/shared/ui/input-date-range-picker";
|
|
||||||
import { UserStatisticsApi } from "../api";
|
|
||||||
import { chartData } from "../model/chart-data";
|
|
||||||
import { columns } from "../model/table-columns";
|
|
||||||
import { type AccessLogParams } from "../model/user-statistics-interface";
|
|
||||||
|
|
||||||
ChartJS.register(
|
|
||||||
CategoryScale,
|
|
||||||
LinearScale,
|
|
||||||
PointElement,
|
|
||||||
LineElement,
|
|
||||||
BarElement,
|
|
||||||
ArcElement,
|
|
||||||
Filler,
|
|
||||||
Legend,
|
|
||||||
Tooltip,
|
|
||||||
);
|
|
||||||
|
|
||||||
type DateRange = {
|
|
||||||
from?: Date | undefined;
|
|
||||||
to?: Date | undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
function UserStatisticsPage() {
|
|
||||||
const defaultDateRange: DateRange = {
|
|
||||||
from: dayjs().subtract(7, "day").toDate(),
|
|
||||||
to: dayjs().toDate(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const [pagination, setPagination] = useState<PaginationState>({
|
|
||||||
pageIndex: 0,
|
|
||||||
pageSize: 10,
|
|
||||||
});
|
|
||||||
const [keywordType, setKeywordType] = useState("");
|
|
||||||
const [keyword, setKeyword] = useState("");
|
|
||||||
const [presetDateRange, setPresetDateRange] = useState("1W");
|
|
||||||
const [dateRange, setDateRange] = useState<DateRange>(defaultDateRange);
|
|
||||||
const [searchParams, setSearchParams] = useState<AccessLogParams>({
|
|
||||||
urlPath: "",
|
|
||||||
accessIp: "",
|
|
||||||
accessRegion: "",
|
|
||||||
browser: "",
|
|
||||||
accessDateStart: dayjs().subtract(7, "day").format("YYYY-MM-DD"),
|
|
||||||
accessDateEnd: dayjs().format("YYYY-MM-DD"),
|
|
||||||
sort: "accessTime,desc",
|
|
||||||
});
|
|
||||||
|
|
||||||
const query = useQuery({
|
|
||||||
queryKey: ["user-statistics", searchParams, pagination],
|
|
||||||
queryFn: () =>
|
|
||||||
UserStatisticsApi.getAccessLogList({
|
|
||||||
...searchParams,
|
|
||||||
page: pagination.pageIndex,
|
|
||||||
size: pagination.pageSize,
|
|
||||||
}),
|
|
||||||
placeholderData: keepPreviousData,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { tableComponent, tableInstance: table } = useDataTable({
|
|
||||||
query,
|
|
||||||
tableOptions: {
|
|
||||||
columns,
|
|
||||||
state: {
|
|
||||||
pagination,
|
|
||||||
},
|
|
||||||
manualPagination: true,
|
|
||||||
getCoreRowModel: getCoreRowModel(),
|
|
||||||
onPaginationChange: setPagination,
|
|
||||||
},
|
|
||||||
dataTableOptions: {
|
|
||||||
viewPagination: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const handlePresetDateRange = (value: string) => {
|
|
||||||
let presetDateRange: DateRange;
|
|
||||||
switch (value) {
|
|
||||||
case "1W":
|
|
||||||
presetDateRange = {
|
|
||||||
from: dayjs().subtract(7, "day").toDate(),
|
|
||||||
to: dayjs().toDate(),
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
case "1M":
|
|
||||||
presetDateRange = {
|
|
||||||
from: dayjs().subtract(30, "day").toDate(),
|
|
||||||
to: dayjs().toDate(),
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
case "3M":
|
|
||||||
presetDateRange = {
|
|
||||||
from: dayjs().subtract(90, "day").toDate(),
|
|
||||||
to: dayjs().toDate(),
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
presetDateRange = defaultDateRange;
|
|
||||||
}
|
|
||||||
setPresetDateRange(value);
|
|
||||||
setDateRange(presetDateRange);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDateRangeChange = (dateRange: DateRange) => {
|
|
||||||
setDateRange(dateRange);
|
|
||||||
setPresetDateRange("");
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setPagination({
|
|
||||||
...pagination,
|
|
||||||
pageIndex: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 검색 조건에 따라 검색 파라미터 설정
|
|
||||||
const params: AccessLogParams = {
|
|
||||||
sort: "accessTime,desc",
|
|
||||||
accessDateStart: dateRange.from
|
|
||||||
? dayjs(dateRange.from).format("YYYY-MM-DD")
|
|
||||||
: "",
|
|
||||||
accessDateEnd: dateRange.to
|
|
||||||
? dayjs(dateRange.to).format("YYYY-MM-DD")
|
|
||||||
: "",
|
|
||||||
};
|
|
||||||
|
|
||||||
// 검색어 타입에 따라 파라미터 설정
|
|
||||||
switch (keywordType) {
|
|
||||||
case "urlPath":
|
|
||||||
params.urlPath = keyword;
|
|
||||||
break;
|
|
||||||
case "ip":
|
|
||||||
params.accessIp = keyword;
|
|
||||||
break;
|
|
||||||
case "region":
|
|
||||||
params.accessRegion = keyword;
|
|
||||||
break;
|
|
||||||
case "browser":
|
|
||||||
params.browser = keyword;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
setSearchParams(params);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleReset = () => {
|
|
||||||
setKeywordType("");
|
|
||||||
setKeyword("");
|
|
||||||
setDateRange(defaultDateRange);
|
|
||||||
setPresetDateRange("1W");
|
|
||||||
setSearchParams({
|
|
||||||
urlPath: "",
|
|
||||||
accessIp: "",
|
|
||||||
accessRegion: "",
|
|
||||||
browser: "",
|
|
||||||
accessDateStart: dayjs().subtract(7, "day").format("YYYY-MM-DD"),
|
|
||||||
accessDateEnd: dayjs().format("YYYY-MM-DD"),
|
|
||||||
sort: "accessTime,desc",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="card card-border !mt-6">
|
|
||||||
<div className="card card-border">
|
|
||||||
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
|
|
||||||
<Label className="mr-2">날짜</Label>
|
|
||||||
<Select value={presetDateRange} onValueChange={handlePresetDateRange}>
|
|
||||||
<SelectTrigger className="w-[12rem]">
|
|
||||||
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="1W">일주일</SelectItem>
|
|
||||||
<SelectItem value="1M">1개월</SelectItem>
|
|
||||||
<SelectItem value="3M">3개월</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<InputDateRangePicker
|
|
||||||
dateRange={dateRange}
|
|
||||||
setDateRange={handleDateRangeChange}
|
|
||||||
/>
|
|
||||||
<Label className="ml-4 mr-2 text-label">사이트</Label>
|
|
||||||
<SiteSelect enabledAllSite className="w-[16rem]" />
|
|
||||||
<Label className="ml-4 mr-2 text-label">검색</Label>
|
|
||||||
<Select
|
|
||||||
value={keywordType}
|
|
||||||
onValueChange={(value) => setKeywordType(value)}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-[3.2rem] w-[10rem]">
|
|
||||||
<SelectValue placeholder="선택" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="urlPath">URL경로</SelectItem>
|
|
||||||
<SelectItem value="ip">접속IP</SelectItem>
|
|
||||||
<SelectItem value="region">접속지역</SelectItem>
|
|
||||||
<SelectItem value="browser">브라우저</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<Input
|
|
||||||
value={keyword}
|
|
||||||
onChange={(e) => setKeyword(e.target.value)}
|
|
||||||
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
|
|
||||||
/>
|
|
||||||
<Button className="h-[3.2rem]" type="submit">
|
|
||||||
조회
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
color="white"
|
|
||||||
className="h-[3.2rem]"
|
|
||||||
onClick={handleReset}
|
|
||||||
>
|
|
||||||
초기화
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 차트 영역 */}
|
|
||||||
<div className="grid grid-cols-2">
|
|
||||||
{/* 일별 접속 현황 */}
|
|
||||||
<div className="card card-border">
|
|
||||||
<div className="card-header">
|
|
||||||
<h3 className="text-xl font-medium">일별 접속 현황</h3>
|
|
||||||
</div>
|
|
||||||
<div className="h-96 p-4">
|
|
||||||
<Bar
|
|
||||||
data={chartData.monthlyStatisticsData as ChartData<"bar">}
|
|
||||||
options={chartData.monthlyStatisticsOptions}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2">
|
|
||||||
{/* 사이트별 접속자 */}
|
|
||||||
<div className="card card-border">
|
|
||||||
<div className="card-header">
|
|
||||||
<h3 className="text-xl font-medium">사이트별 접속자</h3>
|
|
||||||
</div>
|
|
||||||
<div className="h-96 p-4">
|
|
||||||
<Bar
|
|
||||||
data={chartData.siteVisitorsData}
|
|
||||||
options={chartData.siteVisitorsOptions}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 브라우저별 접속자 */}
|
|
||||||
<div className="card card-border">
|
|
||||||
<div className="card-header">
|
|
||||||
<h3 className="text-xl font-medium">브라우저별 접속자</h3>
|
|
||||||
</div>
|
|
||||||
<div className="h-96 p-4">
|
|
||||||
<Doughnut
|
|
||||||
data={chartData.browserStatisticsData}
|
|
||||||
options={chartData.browserStatisticsOptions}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 접속 로그 상세 */}
|
|
||||||
<div className="card card-border">
|
|
||||||
<div className="card-header">
|
|
||||||
<div className="flex w-full items-center justify-between">
|
|
||||||
<h3 className="text-2xl font-medium text-label">접속 로그 상세</h3>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Select
|
|
||||||
value={table.getState().pagination.pageSize.toString()}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
table.setPageSize(parseInt(value));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-[3.2rem] w-[12rem]">
|
|
||||||
<SelectValue placeholder="페이지 크기" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{[10, 20, 30, 40, 50].map((pageSize) => (
|
|
||||||
<SelectItem key={pageSize} value={pageSize.toString()}>
|
|
||||||
{pageSize}개씩 보기
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="card !m-[1.2rem] !p-0">{tableComponent}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { UserStatisticsPage };
|
|
||||||
@@ -1,11 +1,6 @@
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import type { ColumnDef } from "@tanstack/react-table";
|
import type { ColumnDef } from "@tanstack/react-table";
|
||||||
import {
|
import type { UserListResponse } from "@/entities/user";
|
||||||
USER_STATE_CODE,
|
|
||||||
type UserListResponse,
|
|
||||||
type UserStateCode,
|
|
||||||
} from "@/entities/user";
|
|
||||||
import { Badge } from "@common/components";
|
|
||||||
|
|
||||||
export const columns: ColumnDef<UserListResponse>[] = [
|
export const columns: ColumnDef<UserListResponse>[] = [
|
||||||
{
|
{
|
||||||
@@ -47,11 +42,8 @@ export const columns: ColumnDef<UserListResponse>[] = [
|
|||||||
header: "기관/부서",
|
header: "기관/부서",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "userStateCode",
|
accessorKey: "userStateCodeName",
|
||||||
header: "회원상태",
|
header: "회원상태",
|
||||||
cell: ({ row }) => {
|
|
||||||
return getUserStateBadge(row.original.userStateCode);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "aprvYmd",
|
accessorKey: "aprvYmd",
|
||||||
@@ -71,18 +63,3 @@ export const columns: ColumnDef<UserListResponse>[] = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const getUserStateBadge = (userStateCode: UserStateCode) => {
|
|
||||||
switch (userStateCode) {
|
|
||||||
case USER_STATE_CODE.대기:
|
|
||||||
return <Badge color="gray">대기</Badge>;
|
|
||||||
case USER_STATE_CODE.정상:
|
|
||||||
return <Badge color="green">정상</Badge>;
|
|
||||||
case USER_STATE_CODE.정지:
|
|
||||||
return <Badge color="yellow">정지</Badge>;
|
|
||||||
case USER_STATE_CODE.탈퇴:
|
|
||||||
return <Badge color="purple">탈퇴</Badge>;
|
|
||||||
case USER_STATE_CODE.삭제:
|
|
||||||
return <Badge color="red">삭제</Badge>;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -16,11 +16,20 @@ function CreatePage() {
|
|||||||
defaultValues: {
|
defaultValues: {
|
||||||
userId: "",
|
userId: "",
|
||||||
userName: "",
|
userName: "",
|
||||||
|
email: "",
|
||||||
password: "",
|
password: "",
|
||||||
pswdHnt: "",
|
pswdHnt: "",
|
||||||
|
pswdAns: "",
|
||||||
|
mphn: "",
|
||||||
|
telno: "",
|
||||||
|
zip: "",
|
||||||
|
addr: "",
|
||||||
|
daddr: "",
|
||||||
laeSeCd: "",
|
laeSeCd: "",
|
||||||
ogdpSeCd: "",
|
ogdpSeCd: "",
|
||||||
expryYmd: "",
|
expryYmd: "",
|
||||||
|
instSeCd: "",
|
||||||
|
enoCd: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
|
DateRangePicker,
|
||||||
Input,
|
Input,
|
||||||
Label,
|
Label,
|
||||||
Select,
|
Select,
|
||||||
@@ -24,11 +25,10 @@ import {
|
|||||||
} from "@/entities/user";
|
} from "@/entities/user";
|
||||||
import { ENDPOINT } from "@/shared/config";
|
import { ENDPOINT } from "@/shared/config";
|
||||||
import { useDataTable } from "@/shared/lib";
|
import { useDataTable } from "@/shared/lib";
|
||||||
import { InputDateRangePicker } from "@/shared/ui/input-date-range-picker";
|
|
||||||
import { columns } from "../model/table-columns";
|
import { columns } from "../model/table-columns";
|
||||||
|
|
||||||
type DateRange = {
|
type DateRange = {
|
||||||
from?: Date | undefined;
|
from: Date | undefined;
|
||||||
to?: Date | undefined;
|
to?: Date | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -39,8 +39,7 @@ function UsersPage() {
|
|||||||
pageIndex: 0,
|
pageIndex: 0,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
});
|
});
|
||||||
const [presetDateRange, setPresetDateRange] = useState("");
|
const [dateRange, setDateRange] = useState<DateRange | undefined>({
|
||||||
const [dateRange, setDateRange] = useState<DateRange>({
|
|
||||||
from: undefined,
|
from: undefined,
|
||||||
to: undefined,
|
to: undefined,
|
||||||
});
|
});
|
||||||
@@ -62,7 +61,7 @@ function UsersPage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { data: userSttsStats } = useQuery({
|
const { data: userSttsStats } = useQuery({
|
||||||
queryKey: [ENDPOINT.USER_SERVICE.USERS, "userStatusStatistics"],
|
queryKey: [ENDPOINT.USER_SERVICE.USERS],
|
||||||
queryFn: () => UserApi.selectUserSttsStats(),
|
queryFn: () => UserApi.selectUserSttsStats(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -145,7 +144,7 @@ function UsersPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handlePresetDateRange = (value: string) => {
|
const handlePresetDateRange = (value: string) => {
|
||||||
let presetDateRange: DateRange;
|
let presetDateRange: DateRange | undefined;
|
||||||
switch (value) {
|
switch (value) {
|
||||||
case "1M":
|
case "1M":
|
||||||
presetDateRange = {
|
presetDateRange = {
|
||||||
@@ -166,24 +165,14 @@ function UsersPage() {
|
|||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
presetDateRange = {
|
presetDateRange = undefined;
|
||||||
from: undefined,
|
|
||||||
to: undefined,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
setPresetDateRange(value);
|
|
||||||
setDateRange(presetDateRange);
|
setDateRange(presetDateRange);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDateRangeChange = (dateRange: DateRange) => {
|
const getUserSttsStatsCnt = (code: string): number => {
|
||||||
setDateRange(dateRange);
|
if (!Array.isArray(userSttsStats)) return 0;
|
||||||
setPresetDateRange("");
|
return userSttsStats.find((item) => item.sttsCd === code)?.cnt ?? 0;
|
||||||
};
|
|
||||||
|
|
||||||
const getUserSttsStatsCnt = (code: string): string => {
|
|
||||||
if (!Array.isArray(userSttsStats)) return "0";
|
|
||||||
const count = userSttsStats.find((item) => item.sttsCd === code)?.cnt ?? 0;
|
|
||||||
return count.toLocaleString();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUserStateCodeChange = (code: string, checked: boolean) => {
|
const handleUserStateCodeChange = (code: string, checked: boolean) => {
|
||||||
@@ -288,23 +277,23 @@ function UsersPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Label className="mr-2 text-label">기간</Label>
|
<Label className="mr-2 text-label">기간</Label>
|
||||||
<Select
|
<DateRangePicker value={dateRange} onValueChange={setDateRange}>
|
||||||
value={presetDateRange}
|
<div className="card card-border flex items-center gap-2">
|
||||||
onValueChange={handlePresetDateRange}
|
<Select onValueChange={handlePresetDateRange}>
|
||||||
>
|
<SelectTrigger>
|
||||||
<SelectTrigger className="w-[12rem]">
|
<SelectValue
|
||||||
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
|
placeholder="기간 선택"
|
||||||
</SelectTrigger>
|
className="w-[24rem]"
|
||||||
<SelectContent>
|
/>
|
||||||
<SelectItem value="1M">1개월</SelectItem>
|
</SelectTrigger>
|
||||||
<SelectItem value="3M">3개월</SelectItem>
|
<SelectContent>
|
||||||
<SelectItem value="6M">6개월</SelectItem>
|
<SelectItem value="1M">1개월</SelectItem>
|
||||||
</SelectContent>
|
<SelectItem value="3M">3개월</SelectItem>
|
||||||
</Select>
|
<SelectItem value="6M">6개월</SelectItem>
|
||||||
<InputDateRangePicker
|
</SelectContent>
|
||||||
dateRange={dateRange}
|
</Select>
|
||||||
setDateRange={handleDateRangeChange}
|
</div>
|
||||||
/>
|
</DateRangePicker>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ export const ENDPOINT = {
|
|||||||
PRIVACYLOG: "/user-service//api/v1/prvc-inq-logs",
|
PRIVACYLOG: "/user-service//api/v1/prvc-inq-logs",
|
||||||
},
|
},
|
||||||
CMS_SERVICE: {
|
CMS_SERVICE: {
|
||||||
DASHBOARD: "/cms-service/api/v1/dashboard",
|
|
||||||
SITES: "/cms-service/api/v1/sites",
|
SITES: "/cms-service/api/v1/sites",
|
||||||
IPS: "/cms-service/api/v1/site-ips",
|
IPS: "/cms-service/api/v1/site-ips",
|
||||||
DOMAINS: "/cms-service/api/v1/site-dmns",
|
DOMAINS: "/cms-service/api/v1/site-dmns",
|
||||||
@@ -54,7 +53,6 @@ export const ENDPOINT = {
|
|||||||
GROUP_USERS: "/cms-service/api/v1/group-users",
|
GROUP_USERS: "/cms-service/api/v1/group-users",
|
||||||
MANAGER_IP: "/cms-service/api/v1/mgrip",
|
MANAGER_IP: "/cms-service/api/v1/mgrip",
|
||||||
APILOG: "/cms-service/api/v1/api-logs",
|
APILOG: "/cms-service/api/v1/api-logs",
|
||||||
SITE_AUTHORITY: "/cms-service/api/v1/site-authrt",
|
|
||||||
},
|
},
|
||||||
BOARD_SERVICE: {
|
BOARD_SERVICE: {
|
||||||
BBS: "/board-service/api/v1/bbs",
|
BBS: "/board-service/api/v1/bbs",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user