Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2751232e1 | ||
|
|
48c1cced18 | ||
|
|
92b5341cd1 | ||
|
|
010ced20b5 | ||
|
|
c6cf493b51 | ||
|
|
16511de37d | ||
|
|
e3fc4e4dbf | ||
|
|
0cfe98f4b7 | ||
|
|
131ceec61e | ||
|
|
e92f36f739 | ||
|
|
18e9708af1 | ||
|
|
deeb670150 | ||
|
|
4e0e05ea78 | ||
|
|
c31a6ea8bf | ||
|
|
e7f2b3e3ee | ||
|
|
4933fc164a |
@@ -17,3 +17,4 @@ dist
|
||||
|
||||
# env
|
||||
.env*
|
||||
.qodo
|
||||
|
||||
@@ -44,6 +44,12 @@ npx create-next-app@latest ./apps/as --typescript --eslint --tailwind --app
|
||||
```json
|
||||
{
|
||||
// 기타 설정 ...
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@common/components": "workspace:*", // 추가
|
||||
"react": "^19.0.0", // 제거
|
||||
@@ -143,7 +149,9 @@ export default nextConfig;
|
||||
### 6. globals.css(또는 전역 css) 수정
|
||||
|
||||
```css
|
||||
@import "@common/assets/index.css";
|
||||
@import "@common/assets/styles/base.css";
|
||||
@import "@common/assets/styles/diveicon.css";
|
||||
@import "@common/assets/styles/components.css";
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@@ -184,3 +192,8 @@ http://dev.onasys.co.kr:8000
|
||||
|
||||
## Swagger
|
||||
http://dev.onasys.co.kr:8000/webjars/swagger-ui/index.html?urls.primaryName=attach-service
|
||||
|
||||
## 개발 가이드
|
||||
- 사이드바 메뉴 클릭 시 "menuCd" 데이터 쿠키 저장 처리
|
||||
- 쿠키명 : menu-code-key
|
||||
- 해당 메뉴 쿠키 데이터 가져와 페이지 권한 및 상당 Breadcrumb 데이터 전달 처리
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { ApimanPage } from "@/pages/apiman";
|
||||
|
||||
export default function Page() {
|
||||
return <ApimanPage />;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { ModifyPage as VModifyPage } from "@/pages/apiman/ui/modify-page";
|
||||
|
||||
|
||||
export default async function ModifyPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: number}>;
|
||||
}) {
|
||||
const { id } = await params;
|
||||
return <VModifyPage authorizationNo={id} />;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
"use client";
|
||||
|
||||
import { CreatePage } from "@/pages/apiman/ui/create-page";
|
||||
export default CreatePage;
|
||||
@@ -0,0 +1,5 @@
|
||||
import { GroupPage } from "@/pages/group";
|
||||
|
||||
export default function Page() {
|
||||
return <GroupPage />;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { ModifyPage as VModifyPage } from "@/pages/group/ui/modify-page";
|
||||
|
||||
export default async function ModifyPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>;
|
||||
}) {
|
||||
const { id } = await params;
|
||||
return <VModifyPage groupId={id} />;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
"use client";
|
||||
|
||||
import { CreatePage } from "@/pages/group/ui/create-page";
|
||||
export default CreatePage;
|
||||
@@ -1,2 +0,0 @@
|
||||
import { SkeletonPage } from "@/shared/ui/skeleton-page";
|
||||
export default SkeletonPage;
|
||||
+1
-11
@@ -1,5 +1,4 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { CLAIM_NAME, AUTH_USER_ID } from "./src/shared/config";
|
||||
|
||||
const serverApiUrl =
|
||||
process.env.NEXT_PUBLIC_SERVER_API_URL ?? "http://localhost:3000";
|
||||
@@ -17,17 +16,8 @@ export function middleware(req: NextRequest) {
|
||||
}
|
||||
|
||||
if (pathname.startsWith("/attach-service")) {
|
||||
const requestHeaders = new Headers(req.headers);
|
||||
const token = req.cookies.get(CLAIM_NAME)?.value;
|
||||
const userId = req.cookies.get(AUTH_USER_ID)?.value;
|
||||
const newUrl = `${serverApiUrl}${pathname}`;
|
||||
|
||||
if (token) requestHeaders.set(CLAIM_NAME, token);
|
||||
if (userId) requestHeaders.set(AUTH_USER_ID, userId);
|
||||
|
||||
return NextResponse.rewrite(new URL(newUrl), {
|
||||
headers: requestHeaders,
|
||||
});
|
||||
return NextResponse.rewrite(new URL(newUrl));
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
|
||||
@@ -6,7 +6,6 @@ import { AuthWrapper } from "@/entities/auth";
|
||||
import { MenuWrapper } from "@/entities/menu";
|
||||
import { StoreProvider } from "@/shared/providers";
|
||||
import { AlertDialog } from "@/shared/ui/alert-dialog";
|
||||
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
||||
|
||||
const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => {
|
||||
const queryClient = new QueryClient();
|
||||
@@ -17,7 +16,6 @@ const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => {
|
||||
<AuthWrapper>
|
||||
<MenuWrapper>{children}</MenuWrapper>
|
||||
<AlertDialog />
|
||||
<ConfirmDialog />
|
||||
</AuthWrapper>
|
||||
</ConfirmProvider>
|
||||
</StoreProvider>
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type {
|
||||
Attachment,
|
||||
AttachmentService,
|
||||
} from "../model/attachment-interface";
|
||||
|
||||
export class AttachmentApiService implements AttachmentService {
|
||||
public async getAttachments(atflCd: string): Promise<Attachment[]> {
|
||||
const response = await client.get<Attachment[]>(
|
||||
`${ENDPOINT.ATTACH_SERVICE.ATTACHMENTS}/${atflCd}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const AttachmentApi = new AttachmentApiService();
|
||||
@@ -1 +0,0 @@
|
||||
export { AttachmentApi } from "./api/attachment-service";
|
||||
@@ -1,18 +0,0 @@
|
||||
export interface Attachment {
|
||||
atflCd: string;
|
||||
atflSn: number;
|
||||
atflId: string;
|
||||
physFileNm: string;
|
||||
orgnlFileNm: string;
|
||||
atflFlieSz: number;
|
||||
atflType: string;
|
||||
dwnldNocs: number;
|
||||
delYn: string;
|
||||
lnkgDmnId: string;
|
||||
lnkgDmnNm: string;
|
||||
regDt: string;
|
||||
}
|
||||
|
||||
export interface AttachmentService {
|
||||
getAttachments: (atflCd: string) => Promise<Attachment[]>;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export { GroupApi } from "./api/group-api";
|
||||
export {
|
||||
type GroupParams,
|
||||
type Group,
|
||||
type GroupFormData,
|
||||
} from "./model/types";
|
||||
@@ -0,0 +1,29 @@
|
||||
export interface Group {
|
||||
groupId: string;
|
||||
groupNm: string;
|
||||
groupSeCd: string;
|
||||
groupExpln: string;
|
||||
delYn: string;
|
||||
regDt: string;
|
||||
rgtr: string;
|
||||
mdfcnDt: string | null;
|
||||
mdfr: string | null;
|
||||
authrtId: string;
|
||||
siteId: string | null;
|
||||
}
|
||||
|
||||
export interface GroupParams {
|
||||
groupNm?: string;
|
||||
groupSeCd?: string;
|
||||
siteId?: string;
|
||||
}
|
||||
|
||||
export interface GroupFormData {
|
||||
groupId: string;
|
||||
groupNm: string;
|
||||
groupSeCd: string;
|
||||
groupExpln: string;
|
||||
delYn: string;
|
||||
authrtId: string;
|
||||
siteId: string;
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type { PageableList } from "@/shared/model";
|
||||
import type {
|
||||
PostService,
|
||||
PostingParams,
|
||||
Post,
|
||||
WithModePost,
|
||||
} from "../model/posting-interface";
|
||||
|
||||
export class PostingApiService implements PostService {
|
||||
public async getPostingHistory(
|
||||
params: PostingParams,
|
||||
): Promise<PageableList<Post>> {
|
||||
const response = await client.get<PageableList<Post>>(
|
||||
`${ENDPOINT.CMS_SERVICE.SITE_PSTGS}`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async requestPosting(data: WithModePost[]): Promise<Post[]> {
|
||||
const response = await client.post<Post[]>(
|
||||
`${ENDPOINT.CMS_SERVICE.SITE_PSTGS}`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const PostingApi = new PostingApiService();
|
||||
@@ -1,7 +0,0 @@
|
||||
export { PostingApi } from "./api/posting-service";
|
||||
export {
|
||||
POSTING_TYPE,
|
||||
type Post,
|
||||
type WithModePost,
|
||||
} from "./model/posting-interface";
|
||||
export { PostingDialog } from "./ui/posting-dialog";
|
||||
@@ -1,50 +0,0 @@
|
||||
import type { PageableList, PageableParams } from "@/shared/model";
|
||||
import { z } from "zod";
|
||||
|
||||
export const POSTING_TYPE = {
|
||||
POPUP: "POPUP",
|
||||
BANNER: "BANNER",
|
||||
IMAGE: "IMAGE",
|
||||
} as const;
|
||||
|
||||
export type PostingType = (typeof POSTING_TYPE)[keyof typeof POSTING_TYPE];
|
||||
|
||||
export interface PostingParams extends PageableParams {
|
||||
pstgTrgtNo: number;
|
||||
}
|
||||
|
||||
export const postSchema = z.object({
|
||||
siteId: z.string(),
|
||||
siteNm: z.string(),
|
||||
pstgTypeCd: z.string(),
|
||||
pstgTypeNm: z.string(),
|
||||
pstgTrgtNo: z.number(),
|
||||
pstgBgngDt: z.string().optional(),
|
||||
pstgEndDt: z.string().optional(),
|
||||
pstgPrdSeCd: z.string(),
|
||||
pstgPrdSeNm: z.string(),
|
||||
delYn: z.string(),
|
||||
});
|
||||
|
||||
export const withModePostSchema = postSchema
|
||||
.extend({
|
||||
mode: z.string(),
|
||||
})
|
||||
.omit({
|
||||
siteNm: true,
|
||||
pstgTypeNm: true,
|
||||
pstgPrdSeNm: true,
|
||||
});
|
||||
|
||||
export const postingFormSchema = z.object({
|
||||
pstg: z.array(withModePostSchema),
|
||||
});
|
||||
|
||||
export type Post = z.infer<typeof postSchema>;
|
||||
export type WithModePost = z.infer<typeof withModePostSchema>;
|
||||
export type PostingFormData = z.infer<typeof postingFormSchema>;
|
||||
|
||||
export interface PostService {
|
||||
getPostingHistory: (params: PostingParams) => Promise<PageableList<Post>>;
|
||||
requestPosting: (data: WithModePost[]) => Promise<Post[]>;
|
||||
}
|
||||
@@ -1,323 +0,0 @@
|
||||
import dayjs from "dayjs";
|
||||
import { useState } from "react";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useFieldArray, useForm } from "react-hook-form";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import {
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
Button,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
Badge,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
Checkbox,
|
||||
Select,
|
||||
DateRangePicker,
|
||||
DialogClose,
|
||||
DialogFooter,
|
||||
} from "@common/components/ui";
|
||||
import { cn } from "@common/components/lib";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import { SiteApi, type Site } from "@/entities/site";
|
||||
import type { PageableList } from "@/shared/model";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { PostingApi } from "../api/posting-service";
|
||||
import {
|
||||
postingFormSchema,
|
||||
type PostingFormData,
|
||||
type PostingType,
|
||||
type WithModePost,
|
||||
} from "../model/posting-interface";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
|
||||
type DateRange = {
|
||||
from: string;
|
||||
to?: string;
|
||||
};
|
||||
|
||||
type PostingTarget = {
|
||||
pstgTrgtNm: string;
|
||||
pstgTrgtNo: number;
|
||||
};
|
||||
|
||||
type PostingDialogProps = {
|
||||
postingType: PostingType;
|
||||
postingTargets: PostingTarget[];
|
||||
};
|
||||
|
||||
const POSTING_TYPE_LABEL = {
|
||||
POPUP: "팝업",
|
||||
BANNER: "배너",
|
||||
IMAGE: "이미지",
|
||||
} as const;
|
||||
|
||||
function PostingDialog({ postingType, postingTargets }: PostingDialogProps) {
|
||||
const { confirm } = useConfirm();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const [selectedRows, setSelectedRows] = useState<number[]>([]);
|
||||
const [rememberPeriodCode, setRememberPeriodCode] = useState<
|
||||
Record<string, string>
|
||||
>({});
|
||||
const [rememberPeriodDate, setRememberPeriodDate] = useState<
|
||||
Record<string, DateRange | undefined>
|
||||
>({});
|
||||
|
||||
const { data: siteList } = useQuery<PageableList<Site>>({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.SITES],
|
||||
queryFn: () => SiteApi.getSiteList({}),
|
||||
});
|
||||
|
||||
const { mutateAsync: requestPosting } = useMutation({
|
||||
mutationFn: (data: WithModePost[]) => PostingApi.requestPosting(data),
|
||||
onSuccess: () => {
|
||||
alert("저장되었습니다.");
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<PostingFormData>({
|
||||
resolver: zodResolver(postingFormSchema),
|
||||
});
|
||||
const { fields, append, replace } = useFieldArray({
|
||||
control: form.control,
|
||||
name: "pstg",
|
||||
});
|
||||
|
||||
const isFormValid =
|
||||
fields.length && fields.every((field) => field.pstgPrdSeCd !== "");
|
||||
|
||||
const isAllSelected =
|
||||
siteList && selectedRows.length === siteList.content.length;
|
||||
|
||||
const postingPeriod = (siteId: string) => {
|
||||
const periodCode = Number(rememberPeriodCode[siteId]);
|
||||
|
||||
if (periodCode === 99) return "상시";
|
||||
|
||||
return `${dayjs().format("YYYY-MM-DD")} ~ ${dayjs()
|
||||
.add(periodCode, "day")
|
||||
.format("YYYY-MM-DD")}`;
|
||||
};
|
||||
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
if (open) return true;
|
||||
replace([]);
|
||||
setSelectedRows([]);
|
||||
setRememberPeriodCode({});
|
||||
setRememberPeriodDate({});
|
||||
};
|
||||
|
||||
const handleAppendFields = (i: number) => {
|
||||
const siteId = siteList?.content[i]?.siteId ?? "";
|
||||
const newFields = postingTargets.map((target) => ({
|
||||
siteId,
|
||||
pstgTypeCd: postingType,
|
||||
pstgTrgtNo: target.pstgTrgtNo,
|
||||
pstgBgngDt: rememberPeriodDate[siteId]?.from ?? "",
|
||||
pstgEndDt: rememberPeriodDate[siteId]?.to ?? "",
|
||||
pstgPrdSeCd: rememberPeriodCode[siteId] ?? "",
|
||||
delYn: "N",
|
||||
mode: "C",
|
||||
}));
|
||||
append(newFields);
|
||||
};
|
||||
|
||||
const handleUpdateFields =
|
||||
(siteId: string) => (obj: Record<string, unknown>) => {
|
||||
const updatedFields = fields.map((field) =>
|
||||
"siteId" in field && field.siteId === siteId
|
||||
? { ...field, ...obj }
|
||||
: field,
|
||||
);
|
||||
replace(updatedFields);
|
||||
};
|
||||
|
||||
const handleRemoveFields = (siteId: string) => {
|
||||
const filteredFields = fields.filter(
|
||||
(field) => "siteId" in field && field.siteId !== siteId,
|
||||
);
|
||||
replace(filteredFields);
|
||||
};
|
||||
|
||||
const handleSelectRow = (i: number) => {
|
||||
const isSelected = selectedRows.includes(i);
|
||||
setSelectedRows((prev) =>
|
||||
isSelected ? prev.filter((j) => j !== i) : [...prev, i],
|
||||
);
|
||||
|
||||
if (isSelected) {
|
||||
const siteId = siteList?.content[i]?.siteId;
|
||||
if (!siteId) return;
|
||||
handleRemoveFields(siteId);
|
||||
return;
|
||||
}
|
||||
|
||||
handleAppendFields(i);
|
||||
};
|
||||
|
||||
const handleSelectAll = () => {
|
||||
if (selectedRows.length === (siteList?.content.length ?? 0)) {
|
||||
replace([]);
|
||||
setSelectedRows([]);
|
||||
} else {
|
||||
replace([]);
|
||||
setSelectedRows(siteList?.content.map((_, i) => i) ?? []);
|
||||
siteList?.content.forEach((_, i) => handleAppendFields(i));
|
||||
}
|
||||
};
|
||||
|
||||
const handlePeriodCodeChange = (siteId: string) => (value: string) => {
|
||||
const from = dayjs().format("YYYY-MM-DD 00:00:00");
|
||||
const to = dayjs().add(Number(value), "day").format("YYYY-MM-DD 23:59:59");
|
||||
|
||||
setRememberPeriodCode((prev) => ({
|
||||
...prev,
|
||||
[siteId]: value,
|
||||
}));
|
||||
setRememberPeriodDate((prev) => ({
|
||||
...prev,
|
||||
[siteId]: { from, to },
|
||||
}));
|
||||
handleUpdateFields(siteId)({
|
||||
pstgPrdSeCd: value,
|
||||
pstgBgngDt: from,
|
||||
pstgEndDt: to,
|
||||
});
|
||||
};
|
||||
|
||||
const handlePeriodDateChange =
|
||||
(siteId: string) => (value: DateRange | undefined) => {
|
||||
const from = dayjs(value?.from).format("YYYY-MM-DD 00:00:00");
|
||||
const to = dayjs(value?.to).format("YYYY-MM-DD 23:59:59");
|
||||
setRememberPeriodDate((prev) => ({
|
||||
...prev,
|
||||
[siteId]: { from, to },
|
||||
}));
|
||||
handleUpdateFields(siteId)({
|
||||
pstgBgngDt: from,
|
||||
pstgEndDt: to,
|
||||
});
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (await confirm("저장하시겠습니까?")) {
|
||||
const data = form.getValues("pstg");
|
||||
await requestPosting(data);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog onOpenChange={handleOpenChange}>
|
||||
<DialogTrigger asChild>
|
||||
<Button color="emerald" disabled={!postingTargets.length}>
|
||||
<i className="diveicon di-layer" />
|
||||
사이트반영
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-[68rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>사이트 반영</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="card bg-accent !p-4">
|
||||
<h3 className="font-semibold">
|
||||
선택된 {POSTING_TYPE_LABEL[postingType]}
|
||||
</h3>
|
||||
<div className="mt-4 flex flex-wrap gap-3">
|
||||
{postingTargets.map((target) => (
|
||||
<Badge key={target.pstgTrgtNo}>
|
||||
<span className="text-xl">{target.pstgTrgtNm}</span>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="m-2 max-h-[30rem] overflow-auto">
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="10%" />
|
||||
<col width="25%" />
|
||||
<col width="65%" />
|
||||
</colgroup>
|
||||
<TableHeader className="sticky top-0 z-10 bg-accent">
|
||||
<TableRow>
|
||||
<TableHead className="!px-0">
|
||||
<Checkbox
|
||||
checked={isAllSelected}
|
||||
onCheckedChange={handleSelectAll}
|
||||
/>
|
||||
</TableHead>
|
||||
<TableHead>사이트명</TableHead>
|
||||
<TableHead>게시 구분</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{siteList?.content.map((site, i) => (
|
||||
<TableRow key={site.siteId}>
|
||||
<TableCell align="center" className="!px-0">
|
||||
<Checkbox
|
||||
checked={selectedRows.includes(i)}
|
||||
onCheckedChange={() => handleSelectRow(i)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="center">{site.siteNm}</TableCell>
|
||||
<TableCell align="left">
|
||||
<div className="flex items-center gap-4">
|
||||
<PostingPeriodSelect
|
||||
onValueChange={handlePeriodCodeChange(site.siteId)}
|
||||
disabled={!selectedRows.includes(i)}
|
||||
/>
|
||||
{!!Number(rememberPeriodCode[site.siteId]) && (
|
||||
<span
|
||||
className={cn(
|
||||
"text-lg text-label",
|
||||
!selectedRows.includes(i) && "text-muted",
|
||||
)}
|
||||
>
|
||||
게시 기간: <u>{postingPeriod(site.siteId)}</u>
|
||||
</span>
|
||||
)}
|
||||
{Number(rememberPeriodCode[site.siteId]) === 0 && (
|
||||
<DateRangePicker
|
||||
handleDateChange={(date) =>
|
||||
handlePeriodDateChange(site.siteId)(
|
||||
date as DateRange | undefined,
|
||||
)
|
||||
}
|
||||
disabled={!selectedRows.includes(i)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<DialogFooter className="!justify-center">
|
||||
<Button size="lg" onClick={handleSave} disabled={!isFormValid}>
|
||||
저장
|
||||
</Button>
|
||||
<DialogClose asChild>
|
||||
<Button size="lg" color="white">
|
||||
취소
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function PostingPeriodSelect({
|
||||
...props
|
||||
}: React.ComponentPropsWithoutRef<typeof Select>) {
|
||||
return <CodeSelect upCd="PSTGPRDCD" {...props} />;
|
||||
}
|
||||
|
||||
export { PostingDialog };
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from "../model/site-interface";
|
||||
|
||||
export class SiteApiService implements SiteService {
|
||||
public async getSiteList(params?: SiteParams): Promise<SiteList> {
|
||||
public async getSiteList(params: SiteParams): Promise<SiteList> {
|
||||
const response = await client.get<SiteList>(
|
||||
`${ENDPOINT.CMS_SERVICE.SITES}`,
|
||||
{ params },
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import type { Page } from "@/shared/types";
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
export interface Api {
|
||||
authorizationNo: number; // API 일련번호
|
||||
authorizationName: string; // API 명
|
||||
urlPatternValue: string; // URL 패턴
|
||||
httpMethodCode: string; // 하이퍼텍스트 전송 프로토콜 메쏘드 값 (예: GET, POST)
|
||||
sortSeq: number; // 정렬 순서
|
||||
useYn: string; // 사용 여부 (Y/N)
|
||||
regDt: string; // 등록 일시
|
||||
rgtr: string; // 등록자 UUID
|
||||
mdfcnDt: string | null; // 갱신 일시
|
||||
mdfr: string | null; // 갱신자 UUID
|
||||
}
|
||||
|
||||
export interface ApiParams {
|
||||
authorizationName?: string; // API 명 검색
|
||||
httpMethodCode?: string; // HTTP 메소드 검색
|
||||
useYn?: string; // 사용 여부 검색
|
||||
page?: number; // 페이지 번호
|
||||
size?: number; // 페이지 크기
|
||||
}
|
||||
|
||||
export interface ApiFormData {
|
||||
authorizationNo: number | null; // API 일련번호 (신규 등록 시 null)
|
||||
authorizationName: string; // API 명
|
||||
urlPatternValue: string; // URL 패턴
|
||||
httpMethodCode: string; // HTTP 메소드
|
||||
sortSeq: number; // 정렬 순서
|
||||
useYn: string; // 사용 여부
|
||||
}
|
||||
|
||||
export const ApimanApi = {
|
||||
getApiList: async (params: ApiParams) => {
|
||||
const { data } = await client.get<Page<Api>>(
|
||||
`${ENDPOINT.USER_SERVICE.AUTHRTS}`,
|
||||
{ params }
|
||||
);
|
||||
return data;
|
||||
},
|
||||
|
||||
getApi: async (authorizationNo: number) => {
|
||||
const { data } = await client.get<Api>(`${ENDPOINT.USER_SERVICE.AUTHRTS}/${authorizationNo}`);
|
||||
return data;
|
||||
},
|
||||
|
||||
createApi: async (apiData: ApiFormData) => {
|
||||
const { data } = await client.post<Api>(ENDPOINT.USER_SERVICE.AUTHRTS, apiData);
|
||||
return data;
|
||||
},
|
||||
|
||||
updateApi: async (authorizationNo: number, apiData: ApiFormData) => {
|
||||
const { data } = await client.put<Api>(`${ENDPOINT.USER_SERVICE.AUTHRTS}/${authorizationNo}`, apiData);
|
||||
return data;
|
||||
},
|
||||
|
||||
deleteApi: async (authorizationNo: number) => {
|
||||
const { data } = await client.delete<void>(`${ENDPOINT.USER_SERVICE.AUTHRTS}/${authorizationNo}`);
|
||||
return data;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
export { ApimanPage } from "./ui/apiman-page";
|
||||
export { ApimanForm } from "./ui/apiman-form";
|
||||
export { CreatePage } from "./ui/create-page";
|
||||
export { ModifyPage } from "./ui/modify-page";
|
||||
export { ApimanApi } from "./api/apiman-api";
|
||||
@@ -0,0 +1,111 @@
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { Badge, Button } from "@common/components";
|
||||
import type { Api } from "../api/apiman-api";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
// API 권한 관리를 위한 타입 정의
|
||||
type ApiTableMeta = {
|
||||
openRoleDialog: (api: Api) => void;
|
||||
};
|
||||
|
||||
export const columns: ColumnDef<Api>[] = [
|
||||
{
|
||||
accessorKey: "authorizationNo",
|
||||
header: "번호",
|
||||
cell: ({ row }) => <div>{row.original.authorizationNo}</div>,
|
||||
},
|
||||
{
|
||||
accessorKey: "authorizationName",
|
||||
header: "API 명",
|
||||
cell: ({ row }) => <div>{row.original.authorizationName}</div>,
|
||||
},
|
||||
{
|
||||
accessorKey: "urlPatternValue",
|
||||
header: "URL 패턴",
|
||||
cell: ({ row }) => (
|
||||
<div className="max-w-md truncate text-left" title={row.original.urlPatternValue}>
|
||||
{row.original.urlPatternValue}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "httpMethodCode",
|
||||
header: "HTTP 메소드",
|
||||
cell: ({ row }) => {
|
||||
const method = row.original.httpMethodCode;
|
||||
let color = "bg-gray-500";
|
||||
|
||||
switch (method) {
|
||||
case "GET":
|
||||
color = "bg-blue-500";
|
||||
break;
|
||||
case "POST":
|
||||
color = "bg-green-500";
|
||||
break;
|
||||
case "PUT":
|
||||
color = "bg-orange-500";
|
||||
break;
|
||||
case "DELETE":
|
||||
color = "bg-red-500";
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<Badge className={`${color} text-white`}>
|
||||
{method}
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "sortSeq",
|
||||
header: "정렬 순서",
|
||||
cell: ({ row }) => <div className="text-center">{row.original.sortSeq}</div>,
|
||||
},
|
||||
{
|
||||
accessorKey: "useYn",
|
||||
header: "사용 여부",
|
||||
cell: ({ row }) => {
|
||||
const useYn = row.original.useYn;
|
||||
return (
|
||||
<Badge className={useYn === "Y" ? "bg-green-100 text-green-800" : "bg-red-100 text-red-800"}>
|
||||
{useYn === "Y" ? "사용" : "미사용"}
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "mdfrNm",
|
||||
header: "수정자",
|
||||
},
|
||||
{
|
||||
accessorKey: "mdfcnDt",
|
||||
header: "수정일시",
|
||||
cell: ({ row }) => dayjs(row.original.mdfcnDt).format("YYYY-MM-DD"),
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: "권한",
|
||||
cell: ({ row, table }) => {
|
||||
// 메타 데이터에서 권한 다이얼로그 열기 함수 가져오기
|
||||
const meta = table.options.meta as ApiTableMeta | undefined;
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-8 w-16 bg-blue-500 text-white hover:bg-blue-600"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation(); // 행 클릭 이벤트 전파 방지
|
||||
if (meta?.openRoleDialog) {
|
||||
// console.log(row.original);
|
||||
meta.openRoleDialog(row.original);
|
||||
}
|
||||
}}
|
||||
>
|
||||
권한
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,201 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
Button,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
Input,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
DialogFooter,
|
||||
DialogClose,
|
||||
Checkbox,
|
||||
Label,
|
||||
} from "@common/components";
|
||||
import { ApiRoleApi, type ApiRoleByApi } from "../../apirole/api/apirole-api";
|
||||
|
||||
// 페이지네이션 응답을 위한 인터페이스
|
||||
interface Page<T> {
|
||||
content: T[];
|
||||
pageable: {
|
||||
pageNumber: number;
|
||||
pageSize: number;
|
||||
};
|
||||
totalElements: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
// API 역할 인터페이스 정의
|
||||
interface Role extends ApiRoleByApi {
|
||||
selected?: boolean;
|
||||
}
|
||||
|
||||
interface ApiRoleDialogProps {
|
||||
api: ApiRoleByApi | null;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (roleIds: string[]) => Promise<void>;
|
||||
}
|
||||
|
||||
export function ApiRoleDialog({ api, isOpen, onClose, onSave }: ApiRoleDialogProps): JSX.Element {
|
||||
const [roles, setRoles] = useState<Role[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// API에서 역할 데이터 가져오기
|
||||
useEffect(() => {
|
||||
if (isOpen && api && api.authorizationNo) {
|
||||
// 실제 API 호출로 데이터 가져오기
|
||||
ApiRoleApi.getApiRoleByApiList({ authorizationNo: api.authorizationNo })
|
||||
.then((data: Page<ApiRoleByApi>) => {
|
||||
// 데이터를 Role 인터페이스에 맞게 변환
|
||||
const rolesWithSelected = data.content.map(role => ({
|
||||
...role,
|
||||
mode: role.useYn ==null?"C":"R",
|
||||
selected: role.useYn === "Y"
|
||||
}));
|
||||
setRoles(rolesWithSelected);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("역할 데이터 가져오기 오류:", error);
|
||||
// 오류 발생 시 빈 배열 설정
|
||||
setRoles([]);
|
||||
});
|
||||
}
|
||||
}, [isOpen, api]);
|
||||
|
||||
const handleToggleRole = (roleId: string) => {
|
||||
setRoles(
|
||||
roles.map((role) =>
|
||||
role.roleId === roleId ? { ...role, useYn: role.useYn === "Y" ? "N" : "Y", mode: role.mode === "R" ? "U" : "C" } : role
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
//const selectedRoleIds = roles.filter((role) => role.useYn === "Y").map((role) => role.roleId ?? "");
|
||||
await onSave(roles);
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error("권한 저장 중 오류 발생:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Dialog가 닫힐 때 호출되는 함수
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
if (!open) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
|
||||
<DialogContent className="sm:max-w-[600px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl font-bold">
|
||||
API 권한 설정 - {api?.authorizationName}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="card mb-4">
|
||||
<Table>
|
||||
<colgroup>
|
||||
<col style={{ width: "20%" }} />
|
||||
<col style={{ width: "30%" }} />
|
||||
<col style={{ width: "20%" }} />
|
||||
<col style={{ width: "30%" }} />
|
||||
</colgroup>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell as="th">API 명</TableCell>
|
||||
<TableCell className="border">
|
||||
{api?.authorizationName || ""}
|
||||
</TableCell>
|
||||
<TableCell as="th">HTTP 메소드</TableCell>
|
||||
<TableCell className="border">
|
||||
{api?.httpMethodCode || ""}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell as="th">URL 패턴</TableCell>
|
||||
<TableCell colSpan={3} className="border">
|
||||
{api?.urlPatternValue || ""}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<div className="max-h-[400px] overflow-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[50px]">허용</TableHead>
|
||||
<TableHead className="w-[150px]">권한 ID</TableHead>
|
||||
<TableHead>권한명</TableHead>
|
||||
<TableHead>권한내용</TableHead>
|
||||
<TableHead className="w-[80px]">사용여부</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{roles.map((role) => (
|
||||
<TableRow key={role.roleId}>
|
||||
<TableCell className="text-center">
|
||||
<Checkbox
|
||||
checked={role.useYn === "Y"}
|
||||
onCheckedChange={() => handleToggleRole(role.roleId)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>{role.roleId}</TableCell>
|
||||
<TableCell>{role.roleName}</TableCell>
|
||||
<TableCell>{role.roleContent}</TableCell>
|
||||
<TableCell>
|
||||
<span className={role.useYn === "Y" ? "text-green-600" : "text-red-600"}>
|
||||
{role.useYn === "Y" ? "사용" : "미사용"}
|
||||
</span>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{roles.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} className="text-center py-4">
|
||||
권한 정보가 없습니다.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="flex justify-end gap-2 mt-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onClose}
|
||||
disabled={loading}
|
||||
>
|
||||
취소
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? "저장 중..." : "저장"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
// apps/cs/src/pages/apiman/ui/apiman-form.tsx
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { type UseFormReturn } from "react-hook-form";
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Label,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@common/components";
|
||||
import { FormItem, FormControl, FormField, Form } from "@/shared/ui/form";
|
||||
import type { ApiFormData } from "../api/apiman-api";
|
||||
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
||||
|
||||
interface ApimanFormProps {
|
||||
form: UseFormReturn<ApiFormData>;
|
||||
authorizationNo?: number;
|
||||
handleSave: (data: ApiFormData) => Promise<void>;
|
||||
handleDelete?: () => Promise<void>;
|
||||
}
|
||||
|
||||
function ApimanForm({ form, authorizationNo, handleSave, handleDelete }: ApimanFormProps) {
|
||||
const isModifyMode = !!authorizationNo;
|
||||
const router = useRouter();
|
||||
const { confirm } = useConfirm();
|
||||
const [onConfirm, setOnConfirm] = useState(false);
|
||||
|
||||
const handleConfirm = async (callback: () => Promise<void>) => {
|
||||
setOnConfirm(true);
|
||||
await callback();
|
||||
setOnConfirm(false);
|
||||
};
|
||||
|
||||
const onSubmit = async (data: ApiFormData) => {
|
||||
const confirmMessage = isModifyMode
|
||||
? "수정하시겠습니까?"
|
||||
: "저장하시겠습니까?";
|
||||
|
||||
await handleConfirm(async () => {
|
||||
if (await confirm(confirmMessage)) {
|
||||
await handleSave(data);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onDelete = async () => {
|
||||
await handleConfirm(async () => {
|
||||
if (await confirm("삭제하시겠습니까?")) {
|
||||
handleDelete?.();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<div className="card">
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="20%" />
|
||||
<col width="80%" />
|
||||
</colgroup>
|
||||
<TableBody>
|
||||
{isModifyMode && (
|
||||
<TableRow>
|
||||
<TableHead>API 일련번호</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="authorizationNo"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value?.toString() ?? ""}
|
||||
className="w-[12rem] !text-[1.3rem]"
|
||||
disabled
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
<TableRow>
|
||||
<TableHead>API 명</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="authorizationName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="API 명을 입력하세요"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>URL 패턴</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="urlPatternValue"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="URL 패턴을 입력하세요 (예: /api/users/{id})"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>요청 메소드</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="httpMethodCode"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Select
|
||||
value={field.value || undefined}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<SelectTrigger className="!text-[1.3rem]">
|
||||
<SelectValue placeholder="요청 메소드를 선택하세요" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="GET">GET</SelectItem>
|
||||
<SelectItem value="POST">POST</SelectItem>
|
||||
<SelectItem value="PUT">PUT</SelectItem>
|
||||
<SelectItem value="DELETE">DELETE</SelectItem>
|
||||
<SelectItem value="PATCH">PATCH</SelectItem>
|
||||
<SelectItem value="OPTIONS">OPTIONS</SelectItem>
|
||||
<SelectItem value="HEAD">TRACE</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>정렬 순서</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="sortSeq"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
{...field}
|
||||
value={field.value?.toString() ?? ""}
|
||||
className="w-[12rem] !text-[1.3rem]"
|
||||
onChange={(e) => field.onChange(parseInt(e.target.value) || 0)}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사용 여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="useYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
{...field}
|
||||
name="useYn"
|
||||
onValueChange={field.onChange}
|
||||
value={field.value}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="useY" />
|
||||
<Label htmlFor="useY">예</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="useN" />
|
||||
<Label htmlFor="useN">아니오</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<div className="flex justify-center gap-4 pb-12 pt-10">
|
||||
<Button
|
||||
size="lg"
|
||||
variant="outline"
|
||||
type="button"
|
||||
onClick={() => router.back()}
|
||||
>
|
||||
목록
|
||||
</Button>
|
||||
<Button size="lg" type="submit">
|
||||
저장
|
||||
</Button>
|
||||
{isModifyMode && (
|
||||
<Button size="lg" variant="destructive" type="button" onClick={onDelete}>
|
||||
삭제
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
<ConfirmDialog open={onConfirm} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { ApimanForm };
|
||||
@@ -0,0 +1,213 @@
|
||||
"use client";
|
||||
import "@common/assets/styles/grid.css";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
getPaginationRowModel,
|
||||
useReactTable
|
||||
} from "@tanstack/react-table";
|
||||
import {
|
||||
Button,
|
||||
DataTable,
|
||||
Input,
|
||||
Label,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
Pagination
|
||||
} from "@common/components";
|
||||
|
||||
import { ApimanApi, type Api, type ApiParams } from "../api/apiman-api";
|
||||
import { type ApiRoleByApi } from "../../apirole/api/apirole-api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { columns } from "../model/apiman-table-columns";
|
||||
import { ApiRoleDialog } from "./api-role-dialog";
|
||||
|
||||
function ApimanPage() {
|
||||
const router = useRouter();
|
||||
const [apiQuery, setApiQuery] = useState<ApiParams>({
|
||||
authorizationName: "",
|
||||
httpMethodCode: "",
|
||||
useYn: "",
|
||||
page: 0,
|
||||
size: 10,
|
||||
});
|
||||
|
||||
// API 역할 다이얼로그 상태 관리
|
||||
const [apiRoleDialog, setApiRoleDialog] = useState<{
|
||||
isOpen: boolean;
|
||||
api: ApiRoleByApi | null;
|
||||
}>({
|
||||
isOpen: false,
|
||||
api: null,
|
||||
});
|
||||
|
||||
const { data: apiList, refetch, isLoading } = useQuery({
|
||||
queryKey: [ENDPOINT.USER_SERVICE.AUTHRTS, apiQuery],
|
||||
queryFn: () => ApimanApi.getApiList(apiQuery),
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
const table = useReactTable({
|
||||
data: apiList?.content ?? [],
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
pageCount: apiList?.totalPages ?? -1,
|
||||
state: {
|
||||
pagination: {
|
||||
pageIndex: apiQuery.page ?? 0,
|
||||
pageSize: apiQuery.size ?? 10,
|
||||
},
|
||||
},
|
||||
meta: {
|
||||
openRoleDialog: (api: ApiRoleByApi) => {
|
||||
setApiRoleDialog({
|
||||
isOpen: true,
|
||||
api,
|
||||
});
|
||||
},
|
||||
},
|
||||
onPaginationChange: (updater) => {
|
||||
const newPagination =
|
||||
typeof updater === "function"
|
||||
? updater({ pageIndex: apiQuery.page ?? 0, pageSize: apiQuery.size ?? 10 })
|
||||
: updater;
|
||||
|
||||
setApiQuery((prev) => ({
|
||||
...prev,
|
||||
page: newPagination.pageIndex,
|
||||
size: newPagination.pageSize,
|
||||
}));
|
||||
},
|
||||
manualPagination: true,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
refetch();
|
||||
}, [apiQuery, refetch]);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setApiQuery(prev => ({ ...prev, page: 0 }));
|
||||
refetch();
|
||||
};
|
||||
|
||||
|
||||
const handleRowClick = (api: Api) => {
|
||||
router.push(`/apiman/register/${api.authorizationNo}`);
|
||||
};
|
||||
|
||||
// API 역할 저장 핸들러
|
||||
const handleSaveRoles = async (roles: ApiRoleByApi[]) => {
|
||||
try {
|
||||
// 실제 구현에서는 API 호출로 대체
|
||||
console.log(`API ${apiRoleDialog.api?.authorizationNo}에 권한 저장:`, roles);
|
||||
// 성공 시 다이얼로그 닫기
|
||||
setApiRoleDialog({ isOpen: false, api: null });
|
||||
// 목록 새로고침
|
||||
refetch();
|
||||
} catch (error) {
|
||||
console.error("권한 저장 중 오류 발생:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
{/* API 역할 다이얼로그 */}
|
||||
<ApiRoleDialog
|
||||
api={apiRoleDialog.api}
|
||||
isOpen={apiRoleDialog.isOpen}
|
||||
onClose={() => setApiRoleDialog({ isOpen: false, api: null })}
|
||||
onSave={handleSaveRoles}
|
||||
/>
|
||||
|
||||
<div className="card card-border">
|
||||
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
|
||||
|
||||
<Label htmlFor="httpMethodCode" className="mr-2 text-label">요청 메소드</Label>
|
||||
<Select
|
||||
value={apiQuery.httpMethodCode || undefined}
|
||||
onValueChange={(value) => setApiQuery( prev => ({ ...prev, httpMethodCode: value === "ALL" ? "" : value }))}
|
||||
>
|
||||
<SelectTrigger id="httpMethodCode" className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]">
|
||||
<SelectValue placeholder="요청 메소드 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ALL">전체</SelectItem>
|
||||
<SelectItem value="GET">GET</SelectItem>
|
||||
<SelectItem value="POST">POST</SelectItem>
|
||||
<SelectItem value="PUT">PUT</SelectItem>
|
||||
<SelectItem value="DELETE">DELETE</SelectItem>
|
||||
<SelectItem value="OPTIONS">OPTIONS</SelectItem>
|
||||
<SelectItem value="PATCH">PATCH</SelectItem>
|
||||
<SelectItem value="HEAD">HEAD</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Label htmlFor="authorizationName" className="mr-2 text-label">API 명</Label>
|
||||
<Input
|
||||
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
|
||||
id="authorizationName"
|
||||
value={apiQuery.authorizationName || ""}
|
||||
onChange={(e) => setApiQuery( prev => ({ ...prev, authorizationName: e.target.value }))}
|
||||
placeholder="API 명 입력"
|
||||
/>
|
||||
<Button className="h-[3.2rem]" type="submit">
|
||||
조회
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">모듈API 목록</h3>
|
||||
<Button onClick={() => router.push("/apiman/register")}>+등록</Button>
|
||||
</div>
|
||||
|
||||
<DataTable
|
||||
table={table}
|
||||
isLoading={isLoading}
|
||||
onRowClick={handleRowClick}
|
||||
/>
|
||||
|
||||
<div className="flex items-center justify-end space-x-2 py-4">
|
||||
<div className="flex-1 text-lm text-muted-foreground">
|
||||
총 {apiList?.totalElements ?? 0} 건
|
||||
</div>
|
||||
<Pagination
|
||||
currentPage={apiQuery.page + 1}
|
||||
totalPages={apiList?.totalPages ?? 0}
|
||||
onPageChange={(page) =>
|
||||
setApiQuery(prev => ({ ...prev, page: page - 1 }))
|
||||
}
|
||||
/>
|
||||
<Select
|
||||
value={String(apiQuery.size)}
|
||||
onValueChange={(value) =>
|
||||
setApiQuery(prev => ({ ...prev, page: 0, size: Number(value) }))
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-8 w-[70px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{[2,10, 20, 30, 40, 50].map((pageSize) => (
|
||||
<SelectItem key={pageSize} value={String(pageSize)}>
|
||||
{pageSize}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { ApimanPage };
|
||||
@@ -0,0 +1,76 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { ApimanApi, type ApiFormData } from "../api/apiman-api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
import { ApimanForm } from "./apiman-form";
|
||||
import { z } from "zod";
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
// API 스키마 정의
|
||||
const apiFormSchema = z.object({
|
||||
authorizationNo: z.number().nullable(),
|
||||
authorizationName: z.string().min(1, "API 명을 입력해주세요"),
|
||||
urlPatternValue: z.string().min(1, "URL 패턴을 입력해주세요"),
|
||||
httpMethodCode: z.string().min(1, "HTTP 메소드를 선택해주세요"),
|
||||
sortSeq: z.number().min(0, "정렬 순서는 0 이상이어야 합니다"),
|
||||
useYn: z.string(),
|
||||
});
|
||||
|
||||
function CreatePage() {
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const { mutateAsync: createApi } = useMutation({
|
||||
mutationFn: (data: ApiFormData) => ApimanApi.createApi(data),
|
||||
onSuccess: () => {
|
||||
alert("등록되었습니다.", () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.USER_SERVICE.AUTHRTS],
|
||||
});
|
||||
router.push("/apiman");
|
||||
});
|
||||
},
|
||||
onError: (error: unknown) => {
|
||||
const axiosError = error as AxiosError<{ message: string }>;
|
||||
if (axiosError.response?.data) {
|
||||
alert(axiosError.response.data.message);
|
||||
} else if (error instanceof Error) {
|
||||
alert(error.message);
|
||||
} else {
|
||||
alert("알 수 없는 오류가 발생했습니다.");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<ApiFormData>({
|
||||
resolver: zodResolver(apiFormSchema),
|
||||
defaultValues: {
|
||||
authorizationNo: null,
|
||||
authorizationName: "",
|
||||
urlPatternValue: "",
|
||||
httpMethodCode: "",
|
||||
sortSeq: 0,
|
||||
useYn: "Y"
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card card-border !mb-0 !mt-6">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">API 등록</h3>
|
||||
</div>
|
||||
<ApimanForm
|
||||
form={form}
|
||||
handleSave={createApi}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { CreatePage };
|
||||
@@ -0,0 +1,104 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { ApimanApi, type ApiFormData } from "../api/apiman-api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
import { ApimanForm } from "./apiman-form";
|
||||
import { z } from "zod";
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
// API 스키마 정의
|
||||
const apiFormSchema = z.object({
|
||||
authorizationNo: z.number().nullable(),
|
||||
authorizationName: z.string().min(1, "API 명을 입력해주세요"),
|
||||
urlPatternValue: z.string().min(1, "URL 패턴을 입력해주세요"),
|
||||
httpMethodCode: z.string().min(1, "HTTP 메소드를 선택해주세요"),
|
||||
sortSeq: z.number(),
|
||||
useYn: z.string(),
|
||||
});
|
||||
|
||||
function ModifyPage({ authorizationNo }: { authorizationNo: number }) {
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const { data: apiDetail } = useQuery({
|
||||
queryKey: [ENDPOINT.USER_SERVICE.AUTHRTS, authorizationNo],
|
||||
queryFn: () => ApimanApi.getApi(authorizationNo),
|
||||
select: (data) => {
|
||||
const keys = Object.keys(apiFormSchema.shape);
|
||||
const filteredData = Object.fromEntries(
|
||||
Object.entries(data).filter(([key]) => keys.includes(key)),
|
||||
);
|
||||
return filteredData;
|
||||
},
|
||||
enabled: !!authorizationNo,
|
||||
});
|
||||
|
||||
const { mutateAsync: updateApi } = useMutation({
|
||||
mutationFn: (data: ApiFormData) => ApimanApi.updateApi(authorizationNo, data),
|
||||
onSuccess: () => {
|
||||
alert("수정되었습니다.", () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.USER_SERVICE.AUTHRTS],
|
||||
});
|
||||
router.back();
|
||||
});
|
||||
},
|
||||
onError: (error: unknown) => {
|
||||
const axiosError = error as AxiosError<{ message: string }>;
|
||||
if (axiosError.response?.data) {
|
||||
alert(axiosError.response.data.message);
|
||||
} else if (error instanceof Error) {
|
||||
alert(error.message);
|
||||
} else {
|
||||
alert("알 수 없는 오류가 발생했습니다.");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: deleteApi } = useMutation({
|
||||
mutationFn: () => ApimanApi.deleteApi(authorizationNo),
|
||||
onSuccess: () => {
|
||||
alert("삭제되었습니다.", router.back);
|
||||
},
|
||||
onError: (error: unknown) => {
|
||||
const axiosError = error as AxiosError<{ message: string }>;
|
||||
if (axiosError.response?.data) {
|
||||
alert(axiosError.response.data.message);
|
||||
} else if (error instanceof Error) {
|
||||
alert(error.message);
|
||||
} else {
|
||||
alert("알 수 없는 오류가 발생했습니다.");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<ApiFormData>({
|
||||
resolver: zodResolver(apiFormSchema),
|
||||
values: apiDetail as ApiFormData,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card card-border !mb-0 !mt-6">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">API 수정</h3>
|
||||
</div>
|
||||
<ApimanForm
|
||||
form={form}
|
||||
authorizationNo={authorizationNo}
|
||||
handleSave={async (data) => {
|
||||
await updateApi(data);
|
||||
return;
|
||||
}}
|
||||
handleDelete={deleteApi}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { ModifyPage };
|
||||
@@ -0,0 +1,77 @@
|
||||
import type { Page } from "@/shared/types";
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
export interface ApiRole {
|
||||
roleId?: string;
|
||||
/** @format int64 */
|
||||
authorizationNo?: number;
|
||||
authorizationName?: string;
|
||||
urlPatternValue?: string;
|
||||
httpMethodCode?: string;
|
||||
/** @format int32 */
|
||||
sortSeq?: number;
|
||||
createdAt?: boolean;
|
||||
}
|
||||
/** API 에서 역할 추가 (API에 대한 역할) */
|
||||
export interface ApiRoleByApi {
|
||||
roleId?: string;
|
||||
roleName?: string;
|
||||
roleContent?: string;
|
||||
useYn?: string;
|
||||
/** @format int64 */
|
||||
authorizationNo?: number;
|
||||
mode?:string;
|
||||
}
|
||||
|
||||
export interface ApiRoleParams {
|
||||
authorizationNo?: number; // API 일련번호 검색
|
||||
roleId?: string; // API 역할 검색
|
||||
page?: number; // 페이지 번호
|
||||
size?: number; // 페이지 크기
|
||||
}
|
||||
|
||||
export interface ApiRoleFormData {
|
||||
authorizationNo: number | null; // API 일련번호 (신규 등록 시 null)
|
||||
authorizationName: string; // API 명
|
||||
urlPatternValue: string; // URL 패턴
|
||||
httpMethodCode: string; // HTTP 메소드
|
||||
sortSeq: number; // 정렬 순서
|
||||
useYn: string; // 사용 여부
|
||||
}
|
||||
|
||||
export const ApiRoleApi = {
|
||||
getApiRoleList: async (params: ApiRoleParams) => {
|
||||
const { data } = await client.get<Page<ApiRole>>(
|
||||
`${ENDPOINT.USER_SERVICE.ROLES_AUTHRTS}`,
|
||||
{ params }
|
||||
);
|
||||
return data;
|
||||
},
|
||||
getApiRoleByApiList: async (params: ApiRoleParams) => {
|
||||
const { data } = await client.get<Page<ApiRoleByApi>>(
|
||||
`${ENDPOINT.USER_SERVICE.ROLES_AUTHRTS}/api`,
|
||||
{ params }
|
||||
);
|
||||
return data;
|
||||
},
|
||||
getApiRole: async (authorizationNo: number) => {
|
||||
const { data } = await client.get<ApiRole>(`${ENDPOINT.USER_SERVICE.ROLES_AUTHRTS}/${authorizationNo}`);
|
||||
return data;
|
||||
},
|
||||
|
||||
createApiRole: async (apiData: ApiRoleFormData) => {
|
||||
const { data } = await client.post<ApiRole>(ENDPOINT.USER_SERVICE.ROLES_AUTHRTS, apiData);
|
||||
return data;
|
||||
},
|
||||
|
||||
updateApiRole: async (authorizationNo: number, apiData: ApiRoleFormData) => {
|
||||
const { data } = await client.put<ApiRole>(`${ENDPOINT.USER_SERVICE.ROLES_AUTHRTS}/${authorizationNo}`, apiData);
|
||||
return data;
|
||||
},
|
||||
|
||||
deleteApiRole: async (authorizationNo: number) => {
|
||||
const { data } = await client.delete<void>(`${ENDPOINT.USER_SERVICE.ROLES_AUTHRTS}/${authorizationNo}`);
|
||||
return data;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
//import { http } from "@/libs/axios";
|
||||
import type { Page } from "@/shared/types";
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
export interface Group {
|
||||
groupId: string;
|
||||
groupNm: string;
|
||||
groupSeCd: string;
|
||||
groupExpln: string;
|
||||
delYn: string;
|
||||
regDt: string;
|
||||
rgtr: string;
|
||||
mdfcnDt: string | null;
|
||||
mdfr: string | null;
|
||||
authrtId: string;
|
||||
siteId: string | null;
|
||||
}
|
||||
|
||||
export interface GroupParams {
|
||||
groupNm?: string;
|
||||
groupSeCd?: string;
|
||||
siteId?: string;
|
||||
}
|
||||
|
||||
export interface GroupFormData {
|
||||
groupId: string;
|
||||
groupNm: string;
|
||||
groupSeCd: string;
|
||||
groupExpln: string;
|
||||
delYn: string;
|
||||
authrtId: string;
|
||||
siteId: string;
|
||||
}
|
||||
|
||||
|
||||
export const GroupApi = {
|
||||
getGroupList: async (params: GroupParams) => {
|
||||
const { data } = await client.get<Page<Group>>(
|
||||
`${ENDPOINT.CMS_SERVICE.GROUP}`
|
||||
, { params });
|
||||
return data;
|
||||
},
|
||||
|
||||
getGroup: async (groupId: string) => {
|
||||
const { data } = await client.get<Group>(`${ENDPOINT.CMS_SERVICE.GROUP}/${groupId}`);
|
||||
return data;
|
||||
},
|
||||
|
||||
createGroup: async (groupData: GroupFormData) => {
|
||||
const { data } = await client.post<Group>(ENDPOINT.CMS_SERVICE.GROUP, groupData);
|
||||
return data;
|
||||
},
|
||||
|
||||
updateGroup: async (groupId: string, groupData: GroupFormData) => {
|
||||
const { data } = await client.put<Group>(`${ENDPOINT.CMS_SERVICE.GROUP}/${groupId}`, groupData);
|
||||
return data;
|
||||
},
|
||||
|
||||
deleteGroup: async (groupId: string) => {
|
||||
const { data } = await client.delete<void>(`${ENDPOINT.CMS_SERVICE.GROUP}/${groupId}`);
|
||||
return data;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { GroupPage } from "./ui/group-page";
|
||||
@@ -0,0 +1,73 @@
|
||||
import { type ColumnDef } from "@tanstack/react-table";
|
||||
import { Checkbox } from "@common/components/ui";
|
||||
import { type Group } from "../api/group-api";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
export const columns: ColumnDef<Group>[] = [
|
||||
/* {
|
||||
id: "select",
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
checked={table.getIsAllPageRowsSelected()}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
aria-label="전체 선택"
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
aria-label="행 선택"
|
||||
/>
|
||||
),
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
}, */
|
||||
{
|
||||
id: "no",
|
||||
header: "NO",
|
||||
accessorFn: (_, index) => index + 1,
|
||||
},
|
||||
{
|
||||
accessorKey: "groupId",
|
||||
header: "그룹 ID",
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "groupNm",
|
||||
header: "그룹명",
|
||||
},
|
||||
{
|
||||
accessorKey: "groupSeNm",
|
||||
header: "그룹구분",
|
||||
},
|
||||
{
|
||||
accessorKey: "delYn",
|
||||
header: "삭제여부",
|
||||
cell: ({ row }) => (
|
||||
<Checkbox
|
||||
checked={row.original.delYn === "Y"}
|
||||
disabled
|
||||
aria-label="삭제여부"
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
accessorKey: "siteId",
|
||||
header: "사이트 ID",
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "roleNm",
|
||||
header: "권한",
|
||||
},
|
||||
{
|
||||
accessorKey: "mdfrNm",
|
||||
header: "수정자",
|
||||
},
|
||||
{
|
||||
accessorKey: "mdfcnDt",
|
||||
header: "수정일시",
|
||||
cell: ({ row }) => dayjs(row.original.mdfcnDt).format("YYYY-MM-DD"),
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,71 @@
|
||||
// apps/cs/src/pages/group/ui/create-page.tsx
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { GroupApi, type GroupFormData } from "../api/group-api";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
import { GroupForm } from "./group-form";
|
||||
import { z } from "zod";
|
||||
|
||||
const groupFormSchema = z.object({
|
||||
groupId: z.string(),
|
||||
groupNm: z.string().min(1, "그룹명을 입력해주세요"),
|
||||
groupSeCd: z.string().min(1, "그룹구분을 선택해주세요"),
|
||||
groupExpln: z.string(),
|
||||
delYn: z.string(),
|
||||
authrtId: z.string(),
|
||||
siteId: z.string(),
|
||||
});
|
||||
|
||||
function CreatePage() {
|
||||
const router = useRouter();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const { mutateAsync: createGroup } = useMutation({
|
||||
mutationFn: (data: GroupFormData) => GroupApi.createGroup(data),
|
||||
onSuccess: () => {
|
||||
alert("등록되었습니다.", router.back);
|
||||
},
|
||||
onError: (error) => {
|
||||
/*{
|
||||
"timestamp": "2025-04-07T14:30:15.471581",
|
||||
"message": "해당 권한이 존재하지 않습니다. ID = sd",
|
||||
"status": 400,
|
||||
"code": "B001",
|
||||
"errors": []
|
||||
}*/
|
||||
if(error.response.data){
|
||||
alert(error.response.data.message);
|
||||
}else{
|
||||
alert(error.message);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<GroupFormData>({
|
||||
resolver: zodResolver(groupFormSchema),
|
||||
defaultValues: {
|
||||
groupId: "",
|
||||
groupNm: "",
|
||||
groupSeCd: "",
|
||||
groupExpln: "",
|
||||
delYn: "N",
|
||||
authrtId: "",
|
||||
siteId: "",
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card card-border !mb-0 !mt-6">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">그룹 등록</h3>
|
||||
</div>
|
||||
<GroupForm form={form} handleSave={createGroup} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { CreatePage };
|
||||
@@ -0,0 +1,258 @@
|
||||
// apps/cs/src/pages/group/ui/group-form.tsx
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { type UseFormReturn } from "react-hook-form";
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Label,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import { FormItem, FormControl, FormField, Form } from "@/shared/ui/form";
|
||||
import type { GroupFormData } from "../api/group-api";
|
||||
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
||||
|
||||
interface GroupFormProps {
|
||||
form: UseFormReturn<GroupFormData>;
|
||||
groupId?: string;
|
||||
handleSave: (data: GroupFormData) => Promise<void>;
|
||||
handleDelete?: () => Promise<void>;
|
||||
}
|
||||
|
||||
function GroupForm({ form, groupId, handleSave, handleDelete }: GroupFormProps) {
|
||||
const isModifyMode = !!groupId;
|
||||
const router = useRouter();
|
||||
const { confirm } = useConfirm();
|
||||
const [onConfirm, setOnConfirm] = useState(false);
|
||||
|
||||
const handleConfirm = async (callback: () => Promise<void>) => {
|
||||
setOnConfirm(true);
|
||||
await callback();
|
||||
setOnConfirm(false);
|
||||
};
|
||||
|
||||
const onSubmit = async (data: GroupFormData) => {
|
||||
const confirmMessage = isModifyMode
|
||||
? "수정하시겠습니까?"
|
||||
: "저장하시겠습니까?";
|
||||
|
||||
await handleConfirm(async () => {
|
||||
if (await confirm(confirmMessage)) {
|
||||
handleSave(data);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onDelete = async () => {
|
||||
await handleConfirm(async () => {
|
||||
if (await confirm("삭제하시겠습니까?")) {
|
||||
handleDelete?.();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<div className="card">
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="20%" />
|
||||
<col width="80%" />
|
||||
</colgroup>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableHead>그룹 ID</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="groupId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="w-[12rem] !text-[1.3rem]"
|
||||
disabled={isModifyMode}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>그룹명</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="groupNm"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="그룹명을 입력하세요"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>그룹구분</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="groupSeCd"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<CodeSelect
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
upCd="GROUPSECD"
|
||||
onValueChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>그룹설명</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="groupExpln"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="그룹설명을 입력하세요"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사이트 ID</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="siteId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="사이트 ID를 입력하세요"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>권한 ID</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="authrtId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="권한 ID를 입력하세요"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>삭제여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="delYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
{...field}
|
||||
name="delYn"
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="delY" />
|
||||
<Label htmlFor="delY">예</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="delN" />
|
||||
<Label htmlFor="delN">아니오</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</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={onDelete}>
|
||||
삭제
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
<ConfirmDialog open={onConfirm} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { GroupForm };
|
||||
@@ -0,0 +1,145 @@
|
||||
"use client";
|
||||
import "@common/assets/styles/grid.css";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
getPaginationRowModel,
|
||||
useReactTable
|
||||
} from "@tanstack/react-table";
|
||||
import {
|
||||
Button,
|
||||
DataTable,
|
||||
Input,
|
||||
Label,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
Pagination
|
||||
} from "@common/components";
|
||||
|
||||
import { GroupApi, type Group, type GroupParams } from "../api/group-api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { columns } from "../model/group-table-columns";
|
||||
|
||||
function GroupPage() {
|
||||
const router = useRouter();
|
||||
const [groupQuery, setGroupQuery] = useState<GroupParams>({
|
||||
groupNm: "",
|
||||
groupSeCd: "",
|
||||
page: 0,
|
||||
size: 2,
|
||||
});
|
||||
|
||||
const { data: groupList, refetch } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.GROUP, groupQuery],
|
||||
queryFn: () => GroupApi.getGroupList(groupQuery),
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
const table = useReactTable({
|
||||
data: groupList?.content ?? [],
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
pageCount: groupList?.totalPages ?? -1,
|
||||
state: {
|
||||
pagination: {
|
||||
pageIndex: groupQuery.page,
|
||||
pageSize: groupQuery.size,
|
||||
},
|
||||
},
|
||||
onPaginationChange: (updater) => {
|
||||
if (typeof updater === 'function') {
|
||||
const newPagination = updater({
|
||||
pageIndex: groupQuery.page,
|
||||
pageSize: groupQuery.size,
|
||||
});
|
||||
setGroupQuery(prev => ({
|
||||
...prev,
|
||||
page: newPagination.pageIndex,
|
||||
size: newPagination.pageSize,
|
||||
}));
|
||||
}
|
||||
},
|
||||
manualPagination: true,
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setGroupQuery(prev => ({ ...prev, page: 0 })); // 검색 시 첫 페이지로 이동
|
||||
refetch();
|
||||
};
|
||||
|
||||
const handleRowClick = (row: Group) => {
|
||||
router.push(`/group/register/${row.groupId}`);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
refetch();
|
||||
}, [groupQuery, refetch]); //[groupQuery.page, groupQuery.size]); // 페이지나 사이즈 변경 시 자동 조회
|
||||
|
||||
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 htmlFor="" className="mr-2 text-label">
|
||||
그룹명
|
||||
</Label>
|
||||
<Input
|
||||
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
|
||||
value={groupQuery.groupNm}
|
||||
onChange={(e) => setGroupQuery(prev => ({ ...prev, groupNm: e.target.value }))}
|
||||
/>
|
||||
|
||||
<Button className="h-[3.2rem]" type="submit">
|
||||
조회
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">그룹 목록</h3>
|
||||
<Button onClick={() => router.push("/group/register")}>+등록</Button>
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] h-[49rem] overflow-auto !p-0">
|
||||
<DataTable table={table} onRowClick={handleRowClick} />
|
||||
<div className="flex items-center justify-end space-x-2 py-4">
|
||||
<div className="flex-1 text-lm text-muted-foreground">
|
||||
총 {groupList?.totalElements ?? 0} 건
|
||||
</div>
|
||||
<Pagination
|
||||
currentPage={groupQuery.page + 1}
|
||||
totalPages={groupList?.totalPages ?? 0}
|
||||
onPageChange={(page) =>
|
||||
setGroupQuery(prev => ({ ...prev, page: page - 1 }))
|
||||
}
|
||||
/>
|
||||
<Select
|
||||
value={String(groupQuery.size)}
|
||||
onValueChange={(value) =>
|
||||
setGroupQuery(prev => ({ ...prev, page: 0, size: Number(value) }))
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-8 w-[70px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{[2,10, 20, 30, 40, 50].map((pageSize) => (
|
||||
<SelectItem key={pageSize} value={String(pageSize)}>
|
||||
{pageSize}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { GroupPage };
|
||||
@@ -0,0 +1,95 @@
|
||||
// apps/cs/src/pages/group/ui/modify-page.tsx
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { GroupApi, type GroupFormData } from "../api/group-api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
import { GroupForm } from "./group-form";
|
||||
import { z } from "zod";
|
||||
|
||||
// Group 스키마 정의
|
||||
const groupFormSchema = z.object({
|
||||
groupId: z.string(),
|
||||
groupNm: z.string().min(1, "그룹명을 입력해주세요"),
|
||||
groupSeCd: z.string().min(1, "그룹구분을 선택해주세요"),
|
||||
groupExpln: z.string(),
|
||||
delYn: z.string(),
|
||||
authrtId: z.string(),
|
||||
siteId: z.string(),
|
||||
});
|
||||
|
||||
function ModifyPage({ groupId }: { groupId: string }) {
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const { data: groupDetail } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.GROUP, groupId],
|
||||
queryFn: () => GroupApi.getGroup(groupId),
|
||||
select: (data) => {
|
||||
const keys = Object.keys(groupFormSchema.shape);
|
||||
const filteredData = Object.fromEntries(
|
||||
Object.entries(data).filter(([key]) => keys.includes(key)),
|
||||
);
|
||||
return filteredData;
|
||||
},
|
||||
enabled: !!groupId,
|
||||
});
|
||||
|
||||
const { mutateAsync: updateGroup } = useMutation({
|
||||
mutationFn: (data: GroupFormData) => GroupApi.updateGroup(groupId, data),
|
||||
onSuccess: () => {
|
||||
alert("수정되었습니다.", () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.GROUP],
|
||||
});
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
if(error.response.data){
|
||||
alert(error.response.data.message);
|
||||
}else{
|
||||
alert(error.message);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: deleteGroup } = useMutation({
|
||||
mutationFn: () => GroupApi.deleteGroup(groupId),
|
||||
onSuccess: () => {
|
||||
alert("삭제되었습니다.", router.back);
|
||||
},
|
||||
onError: (error) => {
|
||||
if(error.response.data){
|
||||
alert(error.response.data.message);
|
||||
}else{
|
||||
alert(error.message);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<GroupFormData>({
|
||||
resolver: zodResolver(groupFormSchema),
|
||||
values: groupDetail as GroupFormData,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card card-border !mb-0 !mt-6">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">그룹 수정</h3>
|
||||
</div>
|
||||
<GroupForm
|
||||
form={form}
|
||||
groupId={groupId}
|
||||
handleSave={updateGroup}
|
||||
handleDelete={deleteGroup}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { ModifyPage };
|
||||
@@ -11,8 +11,8 @@ export type ImageType = (typeof IMAGE_TYPE)[keyof typeof IMAGE_TYPE];
|
||||
|
||||
export interface ImageParams extends PageableParams {
|
||||
keyword: string;
|
||||
imgTypeCd?: string;
|
||||
useYn?: string;
|
||||
imgTypeCd: string;
|
||||
useYn: string;
|
||||
}
|
||||
|
||||
export interface Image {
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import dayjs from "dayjs";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import { Checkbox } from "@common/components/ui";
|
||||
import type { Post } from "@/entities/posting";
|
||||
|
||||
export const columns: ColumnDef<Post>[] = [
|
||||
{
|
||||
id: "select",
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
checked={table.getIsAllPageRowsSelected()}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onCheckedChange={(checked) =>
|
||||
table.toggleAllPageRowsSelected(!!checked)
|
||||
}
|
||||
className="h-4 w-4 rounded border-gray-300"
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onCheckedChange={(checked) => row.toggleSelected(!!checked)}
|
||||
className="h-4 w-4 rounded border-gray-300"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "siteNm",
|
||||
header: "사이트",
|
||||
},
|
||||
{
|
||||
accessorKey: "pstgPrdSeNm",
|
||||
header: "게시기간",
|
||||
cell: ({ row }) => {
|
||||
const pstgPrdSeNm = row.original.pstgPrdSeNm;
|
||||
const pstgBgngDt = row.original.pstgBgngDt;
|
||||
const pstgEndDt = row.original.pstgEndDt;
|
||||
return `${pstgPrdSeNm} (${dayjs(pstgBgngDt).format("YYYY-MM-DD")} ~ ${dayjs(pstgEndDt).format("YYYY-MM-DD")})`;
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,9 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
import type { HttpResponse, Upload } from "tus-js-client";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
@@ -22,11 +20,10 @@ import {
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import { AttachmentApi } from "@/entities/attachment";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
||||
import { Form, FormControl, FormField, FormItem } from "@/shared/ui/form";
|
||||
import type { ImageFormData } from "../model/image-interface";
|
||||
import { handleUpload } from "../lib/utils";
|
||||
import type { ImageFormData } from "../model/image-interface";
|
||||
|
||||
type ImageFormProps = {
|
||||
form: UseFormReturn<ImageFormData>;
|
||||
@@ -41,50 +38,47 @@ function ImageForm({ form, imgSn, handleSave, handleDelete }: ImageFormProps) {
|
||||
|
||||
const router = useRouter();
|
||||
const { confirm } = useConfirm();
|
||||
const [attachmentFileId, setAttachmentFileId] = useState("");
|
||||
const [onConfirm, setOnConfirm] = useState(false);
|
||||
const [upload, setUpload] = useState<Upload | null>(null);
|
||||
const [progress, setProgress] = useState(0);
|
||||
|
||||
const { data: atflId } = useQuery({
|
||||
queryKey: [ENDPOINT.ATTACH_SERVICE.ATTACHMENTS],
|
||||
queryFn: () => AttachmentApi.getAttachments(form.getValues("atflCd")!),
|
||||
enabled: !!form.getValues("atflCd"),
|
||||
select: (data) => data?.[data.length - 1]?.atflId,
|
||||
});
|
||||
const handleConfirm = async (callback: () => Promise<void>) => {
|
||||
setOnConfirm(true);
|
||||
await callback();
|
||||
setOnConfirm(false);
|
||||
};
|
||||
|
||||
const onSubmit = async (data: ImageFormData) => {
|
||||
const confirmMessage = isModifyMode
|
||||
? "수정하시겠습니까?"
|
||||
: "저장하시겠습니까?";
|
||||
|
||||
if (await confirm(confirmMessage)) {
|
||||
await handleSave(data);
|
||||
}
|
||||
await handleConfirm(async () => {
|
||||
if (await confirm(confirmMessage)) {
|
||||
await handleSave(data);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onDelete = async () => {
|
||||
if (await confirm("삭제하시겠습니까?")) {
|
||||
await handleDelete?.();
|
||||
}
|
||||
await handleConfirm(async () => {
|
||||
if (await confirm("삭제하시겠습니까?")) {
|
||||
await handleDelete?.();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleProgress = (percentage: string) => {
|
||||
setProgress(Number(percentage));
|
||||
};
|
||||
|
||||
const handleUploadResponse = async (res: HttpResponse) => {
|
||||
const handleUploadResponse = (res: HttpResponse) => {
|
||||
const fileInfo = res.getHeader("X-File-Info");
|
||||
if (fileInfo === undefined || fileInfo === null) return;
|
||||
|
||||
const { atflCd, atflId } = JSON.parse(fileInfo) ?? {};
|
||||
if (atflCd) form.setValue("atflCd", atflCd);
|
||||
if (atflId) setAttachmentFileId(atflId);
|
||||
form.setValue("atflCd", JSON.parse(fileInfo)?.atflCd);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (atflId) setAttachmentFileId(atflId);
|
||||
}, [atflId]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form {...form}>
|
||||
@@ -208,7 +202,6 @@ function ImageForm({ form, imgSn, handleSave, handleDelete }: ImageFormProps) {
|
||||
<FileUploader
|
||||
id="atflCd"
|
||||
className="!h-auto !max-w-none flex-col !items-start"
|
||||
accept=".jpeg,.jpg,.png"
|
||||
onUpload={(e) => {
|
||||
const files = e.target.files;
|
||||
if (!files) return;
|
||||
@@ -220,36 +213,18 @@ function ImageForm({ form, imgSn, handleSave, handleDelete }: ImageFormProps) {
|
||||
});
|
||||
}}
|
||||
customPreview={
|
||||
<>
|
||||
<Label htmlFor="atflCd">
|
||||
<div className="relative h-[27rem] w-[48rem] overflow-hidden rounded-sm border border-border bg-accent">
|
||||
<p className="absolute bottom-3 left-0 p-2 text-base leading-none">
|
||||
{(upload?.file as File)?.name}
|
||||
</p>
|
||||
<div className="relative h-full w-full">
|
||||
{attachmentFileId && (
|
||||
<Image
|
||||
src={`${ENDPOINT.ATTACH_SERVICE.IMAGES}/${attachmentFileId}`}
|
||||
alt="업로드된 이미지"
|
||||
fill
|
||||
objectFit="contain"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Progress
|
||||
value={progress}
|
||||
className="absolute bottom-0 left-0"
|
||||
/>
|
||||
</div>
|
||||
</Label>
|
||||
<div className="mb-2 text-lg font-medium text-label">
|
||||
<ul className="ml-10 list-disc">
|
||||
<li>권장 사이즈: 1920x500</li>
|
||||
<li>파일 최대 크기: 2MB</li>
|
||||
<li>확장자: jpg, jpeg, png 가능</li>
|
||||
</ul>
|
||||
<Label htmlFor="atflCd">
|
||||
<div className="relative h-[27rem] w-[48rem] rounded-sm border border-border">
|
||||
<p className="absolute left-0 top-0 p-2 text-base leading-none">
|
||||
{(upload?.file as File)?.name}
|
||||
</p>
|
||||
<div></div>
|
||||
<Progress
|
||||
value={progress}
|
||||
className="absolute bottom-0 left-0"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
</Label>
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
@@ -334,6 +309,7 @@ function ImageForm({ form, imgSn, handleSave, handleDelete }: ImageFormProps) {
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
<ConfirmDialog open={onConfirm} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,23 +9,22 @@ import {
|
||||
type RowSelectionState,
|
||||
} from "@tanstack/react-table";
|
||||
import { Button, DataTable, Input, Label } from "@common/components";
|
||||
import { POSTING_TYPE, PostingDialog } from "@/entities/posting";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { ImageApi } from "../api/image-service";
|
||||
import { columns } from "../model/image-table-columns";
|
||||
import { columns } from "../model/table-columns";
|
||||
import { type Image, type ImageParams } from "../model/image-interface";
|
||||
|
||||
type PostingTarget = {
|
||||
pstgTrgtNm: string;
|
||||
pstgTrgtNo: number;
|
||||
};
|
||||
|
||||
function ImagePage() {
|
||||
const router = useRouter();
|
||||
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
const [postingTargets, setPostingTargets] = useState<PostingTarget[]>([]);
|
||||
const [imageQuery, setImageQuery] = useState<ImageParams>({ keyword: "" });
|
||||
const [imageQuery, setImageQuery] = useState<ImageParams>({
|
||||
keyword: "",
|
||||
imgTypeCd: "",
|
||||
useYn: "",
|
||||
page: 0,
|
||||
size: 100,
|
||||
});
|
||||
|
||||
const { data: imageList, refetch } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.IMAGES],
|
||||
@@ -53,22 +52,6 @@ function ImagePage() {
|
||||
router.push(`/image/register/${row.imgSn}`);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const postingTargets = table
|
||||
.getFilteredSelectedRowModel()
|
||||
.rows.map((row) => ({
|
||||
pstgTrgtNm: row.original.imgTtl,
|
||||
pstgTrgtNo: row.original.imgSn,
|
||||
}));
|
||||
|
||||
if (!postingTargets.length) {
|
||||
setPostingTargets([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setPostingTargets(postingTargets);
|
||||
}, [rowSelection]);
|
||||
|
||||
useEffect(() => {
|
||||
refetch();
|
||||
}, []);
|
||||
@@ -94,18 +77,8 @@ function ImagePage() {
|
||||
</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">
|
||||
<PostingDialog
|
||||
postingType={POSTING_TYPE.IMAGE}
|
||||
postingTargets={postingTargets}
|
||||
/>
|
||||
<Button onClick={() => router.push("/image/register")}>
|
||||
등록
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<h3 className="text-2xl font-medium text-label">이미지 목록</h3>
|
||||
<Button onClick={() => router.push("/image/register")}>등록</Button>
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] h-[49rem] overflow-auto !p-0">
|
||||
<DataTable table={table} onRowClick={handleRowClick} />
|
||||
|
||||
@@ -1,45 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type RowSelectionState,
|
||||
} from "@tanstack/react-table";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { Button, DataTable } from "@common/components/ui";
|
||||
import { PostingApi, type WithModePost } from "@/entities/posting";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { imageFormSchema, type ImageFormData } from "../model/image-interface";
|
||||
import { columns } from "../model/posting-table-columns";
|
||||
import { ImageApi } from "../api/image-service";
|
||||
import { ImageForm } from "./image-form";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
|
||||
function ModifyPage({ serial }: { serial: string }) {
|
||||
const router = useRouter();
|
||||
const { confirm } = useConfirm();
|
||||
const queryClient = useQueryClient();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const [onConfirm, setOnConfirm] = useState(false);
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
|
||||
const { data: imageDetail } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.IMAGES, serial],
|
||||
queryFn: () => ImageApi.getImageInfo(serial),
|
||||
});
|
||||
|
||||
const { data: postHistory } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.SITE_PSTGS, serial],
|
||||
queryFn: () => PostingApi.getPostingHistory({ pstgTrgtNo: Number(serial) }),
|
||||
});
|
||||
|
||||
const { mutateAsync: updateImage } = useMutation({
|
||||
mutationFn: (data: ImageFormData) => ImageApi.updateImage(serial, data),
|
||||
onSuccess: () => {
|
||||
@@ -54,83 +33,23 @@ function ModifyPage({ serial }: { serial: string }) {
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: requestPosting } = useMutation({
|
||||
mutationFn: (data: WithModePost[]) => PostingApi.requestPosting(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.SITE_PSTGS, serial],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<ImageFormData>({
|
||||
resolver: zodResolver(imageFormSchema),
|
||||
values: imageDetail,
|
||||
});
|
||||
|
||||
const table = useReactTable({
|
||||
data: postHistory?.content ?? [],
|
||||
columns,
|
||||
state: {
|
||||
rowSelection,
|
||||
},
|
||||
enableMultiRowSelection: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onRowSelectionChange: setRowSelection,
|
||||
});
|
||||
|
||||
const handleDeletePostingHistory = async () => {
|
||||
const data = table.getFilteredSelectedRowModel().rows.map((row) => ({
|
||||
...row.original,
|
||||
mode: "D",
|
||||
}));
|
||||
|
||||
setOnConfirm(true);
|
||||
if (await confirm("삭제하시겠습니까?")) {
|
||||
setOnConfirm(false);
|
||||
await requestPosting(data);
|
||||
alert("삭제되었습니다.");
|
||||
}
|
||||
setOnConfirm(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mt-6 flex flex-col">
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">이미지 수정</h3>
|
||||
</div>
|
||||
<ImageForm
|
||||
form={form}
|
||||
imgSn={serial}
|
||||
handleSave={updateImage}
|
||||
handleDelete={deleteImage}
|
||||
/>
|
||||
</div>
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">
|
||||
사이트 게시 이력
|
||||
</h3>
|
||||
</div>
|
||||
<div className="card">
|
||||
<div className="mb-4 flex justify-end">
|
||||
<Button
|
||||
size="sm"
|
||||
color="red"
|
||||
disabled={!table.getFilteredSelectedRowModel().rows.length}
|
||||
onClick={handleDeletePostingHistory}
|
||||
>
|
||||
이력삭제
|
||||
</Button>
|
||||
</div>
|
||||
<DataTable table={table} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">이미지 수정</h3>
|
||||
</div>
|
||||
<ConfirmDialog open={onConfirm} />
|
||||
</>
|
||||
<ImageForm
|
||||
form={form}
|
||||
imgSn={serial}
|
||||
handleSave={updateImage}
|
||||
handleDelete={deleteImage}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,8 @@ function ContentSearchDialog({
|
||||
|
||||
const { data: contentList, refetch } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS],
|
||||
queryFn: () => ContentApi.getContentList({ contsTtl }),
|
||||
queryFn: () =>
|
||||
ContentApi.getContentList({ contsTtl, useYn: "Y", page: 0, size: 100 }),
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
|
||||
@@ -52,7 +52,6 @@ function MenuForm({
|
||||
const onCreate = async (data: MenuFormData) => {
|
||||
await handleConfirm(async () => {
|
||||
if (await confirm("저장하시겠습니까?")) {
|
||||
setOnConfirm(false);
|
||||
handleCreate(data);
|
||||
}
|
||||
});
|
||||
@@ -61,7 +60,6 @@ function MenuForm({
|
||||
const onUpdate = async (data: MenuFormData) => {
|
||||
await handleConfirm(async () => {
|
||||
if (await confirm("수정하시겠습니까?")) {
|
||||
setOnConfirm(false);
|
||||
handleUpdate({ menuCd: menuCd!, data });
|
||||
}
|
||||
});
|
||||
@@ -70,7 +68,6 @@ function MenuForm({
|
||||
const onDelete = () => {
|
||||
handleConfirm(async () => {
|
||||
if (await confirm("삭제하시겠습니까?")) {
|
||||
setOnConfirm(false);
|
||||
handleDelete?.(menuCd!);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -62,7 +62,7 @@ function MenuPage() {
|
||||
|
||||
const { data: options } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.SITES],
|
||||
queryFn: () => SiteApi.getSiteList(),
|
||||
queryFn: () => SiteApi.getSiteList({}),
|
||||
select: (data) =>
|
||||
data.content.map((site) => ({
|
||||
label: site.siteNm,
|
||||
@@ -179,7 +179,6 @@ function MenuPage() {
|
||||
setOnConfirm(false);
|
||||
deleteMenu(selectedMenu!.menuCd!);
|
||||
}
|
||||
setOnConfirm(false);
|
||||
};
|
||||
|
||||
const handleAddMenu = () => {
|
||||
|
||||
@@ -90,7 +90,6 @@ function IPManagementDialog({
|
||||
await mutateAsync(data.ips);
|
||||
alert("저장되었습니다.");
|
||||
}
|
||||
setOnConfirm(false);
|
||||
};
|
||||
|
||||
const handleAddIP = () => {
|
||||
|
||||
@@ -58,7 +58,6 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) {
|
||||
|
||||
await handleConfirm(async () => {
|
||||
if (await confirm(confirmMessage)) {
|
||||
setOnConfirm(false);
|
||||
handleSave(data);
|
||||
}
|
||||
});
|
||||
@@ -67,7 +66,6 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) {
|
||||
const onDelete = async () => {
|
||||
await handleConfirm(async () => {
|
||||
if (await confirm("삭제하시겠습니까?")) {
|
||||
setOnConfirm(false);
|
||||
handleDelete?.();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5,13 +5,13 @@ export const BASE_URL = `${PROXY_HOST}/server`;
|
||||
export const ENDPOINT = {
|
||||
ATTACH_SERVICE: {
|
||||
TUS_UPLOAD: "/attach-service/api/v1/tus/upload",
|
||||
IMAGES: "/attach-service/api/v1/images",
|
||||
ATTACHMENTS: "/attach-service/api/v1/atfl/attachments",
|
||||
},
|
||||
USER_SERVICE: {
|
||||
LOGIN: "/user-service/login",
|
||||
USERS: "/user-service/api/v1/users",
|
||||
SILENT_REFRESH: "/user-service/api/v1/users/token/refresh",
|
||||
AUTHRTS: "/user-service/api/v1/authorizations",
|
||||
ROLES_AUTHRTS: "/user-service/api/v1/role-authorizations",
|
||||
},
|
||||
CMS_SERVICE: {
|
||||
SITES: "/cms-service/api/v1/sites",
|
||||
@@ -23,7 +23,7 @@ export const ENDPOINT = {
|
||||
CONTENTS: "/cms-service/api/v1/contents",
|
||||
USER_OGNZ: "/cms-service/api/v1/user-ognz",
|
||||
IMAGES: "/cms-service/api/v1/images",
|
||||
SITE_PSTGS: "/cms-service/api/v1/site-pstgs",
|
||||
GROUP: "/cms-service/api/v1/groups",
|
||||
},
|
||||
BOARD_SERVICE: {
|
||||
BBS: "/board-service/api/v1/bbs",
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
} from "@common/components/ui";
|
||||
|
||||
type ConfirmDialogProps = {
|
||||
open?: boolean;
|
||||
open: boolean;
|
||||
confirmButtonProps?: ButtonProps & { label?: string };
|
||||
cancelButtonProps?: ButtonProps & { label?: string };
|
||||
};
|
||||
@@ -19,9 +19,9 @@ function ConfirmDialog({
|
||||
confirmButtonProps,
|
||||
cancelButtonProps,
|
||||
}: ConfirmDialogProps) {
|
||||
const { message, isOpen, onConfirm, onCancel } = useConfirm();
|
||||
const { message, onConfirm, onCancel } = useConfirm();
|
||||
return (
|
||||
<Dialog open={open ?? isOpen} onOpenChange={onCancel}>
|
||||
<Dialog open={open} onOpenChange={onCancel}>
|
||||
<DialogContent aria-describedby={undefined}>
|
||||
<div className="py-10">
|
||||
<p className="text-center text-[1.4rem] text-foreground">{message}</p>
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { Skeleton } from "@common/components/ui/skeleton";
|
||||
|
||||
function SkeletonPage() {
|
||||
return (
|
||||
<div className="flex flex-col gap-4 p-6">
|
||||
<Skeleton className="h-[6rem] rounded-xl" />
|
||||
<Skeleton className="h-[60rem] rounded-xl" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { SkeletonPage };
|
||||
+1
-1
@@ -49,4 +49,4 @@ import { useWrStore } from '../../../store';
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
.qodo
|
||||
@@ -34,21 +34,12 @@ const config: StorybookConfig = {
|
||||
AUTH_USER_ID: JSON.stringify(process.env.AUTH_USER_ID || "token-id"),
|
||||
},
|
||||
};
|
||||
|
||||
config.optimizeDeps = {
|
||||
...config.optimizeDeps,
|
||||
include: ["react", "react-dom", "@storybook/react"],
|
||||
exclude: ["@storybook/react-vite"],
|
||||
};
|
||||
|
||||
config.plugins?.push(
|
||||
tsconfigPaths({
|
||||
projects: [path.resolve(path.dirname(__dirname), "tsconfig.json")],
|
||||
}),
|
||||
);
|
||||
|
||||
config.server = {
|
||||
...config.server,
|
||||
proxy: {
|
||||
"/server": {
|
||||
target: process.env.SERVER_API_URL,
|
||||
|
||||
@@ -44,7 +44,7 @@ export function Calendar({
|
||||
),
|
||||
day_range_end: "day-range-end",
|
||||
day_selected:
|
||||
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
|
||||
"!bg-primary !text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
|
||||
day_today: "bg-accent text-accent-foreground",
|
||||
day_outside:
|
||||
"day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground",
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
|
||||
@layer components {
|
||||
.date-picker-trigger {
|
||||
@apply h-[3rem] w-[12rem] justify-start border-input !bg-background px-4 py-2 text-left text-[1.3rem] font-normal text-muted-foreground hover:!bg-accent;
|
||||
@apply h-[3rem] w-[12rem] justify-between border-input !bg-background px-4 py-2 text-left text-[1.3rem] font-normal leading-none text-muted-foreground hover:!bg-accent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from "react";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { DatePicker } from ".";
|
||||
import { useState } from "react";
|
||||
|
||||
const meta: Meta<typeof DatePicker> = {
|
||||
title: "Elements/DatePicker",
|
||||
|
||||
@@ -19,12 +19,12 @@ export function DatePicker({ className, ...props }: DatePickerProps) {
|
||||
variant="outline"
|
||||
className={cn("date-picker-trigger", className)}
|
||||
>
|
||||
<CalendarIcon className="!size-[1.6rem]" />
|
||||
{props.selected ? (
|
||||
format(props.selected, "yyyy-MM-dd")
|
||||
) : (
|
||||
<span>날짜 선택</span>
|
||||
)}
|
||||
<CalendarIcon className="!size-[1.6rem]" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0">
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { format } from "date-fns";
|
||||
import { CalendarIcon } from "lucide-react";
|
||||
import type { DateRange } from "react-day-picker";
|
||||
|
||||
import { cn } from "../../lib/utils";
|
||||
import { Button } from "../button";
|
||||
import { Calendar } from "../calendar";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "../popover";
|
||||
|
||||
type DateRangePickerProps = React.HTMLAttributes<HTMLDivElement> & {
|
||||
disabled?: boolean;
|
||||
handleDateChange?: (date: DateRange | undefined) => void;
|
||||
};
|
||||
|
||||
export function DateRangePicker({
|
||||
disabled,
|
||||
className,
|
||||
handleDateChange,
|
||||
}: DateRangePickerProps) {
|
||||
const [date, setDate] = useState<DateRange>();
|
||||
|
||||
const handleSelect = (date: DateRange | undefined) => {
|
||||
setDate(date);
|
||||
handleDateChange?.(date);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn("grid gap-2", className)}>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
id="date"
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"h-[3.2rem] min-w-[12rem] justify-start border-input !bg-background px-4 py-2 text-left text-[1.3rem] font-normal text-muted-foreground hover:!bg-accent",
|
||||
!date && "text-muted-foreground",
|
||||
)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<CalendarIcon className="!size-[1.6rem]" />
|
||||
{date?.from ? (
|
||||
date.to ? (
|
||||
<>
|
||||
{format(date.from, "yyyy-MM-dd")} ~{" "}
|
||||
{format(date.to, "yyyy-MM-dd")}
|
||||
</>
|
||||
) : (
|
||||
format(date.from, "yyyy-MM-dd")
|
||||
)
|
||||
) : (
|
||||
<span>기간 선택</span>
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
initialFocus
|
||||
mode="range"
|
||||
defaultMonth={date?.from}
|
||||
selected={date}
|
||||
onSelect={handleSelect}
|
||||
numberOfMonths={2}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -26,8 +26,8 @@ export function FileUploader({
|
||||
id={id}
|
||||
type="file"
|
||||
className="sr-only"
|
||||
onChange={onUpload}
|
||||
{...props}
|
||||
onChange={onUpload}
|
||||
/>
|
||||
{customPreview || (
|
||||
<div className="file-uploader-preview">
|
||||
|
||||
@@ -5,7 +5,6 @@ export * from "./calendar";
|
||||
export * from "./checkbox";
|
||||
export * from "./data-table";
|
||||
export * from "./date-picker";
|
||||
export * from "./date-range-picker";
|
||||
export * from "./dialog";
|
||||
export * from "./dropdown-menu";
|
||||
export * from "./file-uploader";
|
||||
@@ -23,3 +22,4 @@ export * from "./slider";
|
||||
export * from "./switch";
|
||||
export * from "./table";
|
||||
export * from "./tabs";
|
||||
export * from "./pagination";
|
||||
@@ -15,7 +15,7 @@ export function VBaseLayout({
|
||||
queryClient?: QueryClient;
|
||||
}) {
|
||||
return (
|
||||
<QueryProvider queryClient={queryClient}>
|
||||
<QueryProvider client={queryClient}>
|
||||
<SidebarProvider>
|
||||
<div className="flex-1">
|
||||
<Header viewOnly />
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
// apps/cs/src/common/components/ui/pagination.tsx
|
||||
import { Button } from "../button";
|
||||
|
||||
interface PaginationProps {
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
onPageChange: (page: number) => void;
|
||||
}
|
||||
|
||||
function Pagination({ currentPage, totalPages, onPageChange }: PaginationProps) {
|
||||
// 표시할 페이지 번호 범위 계산
|
||||
const getPageRange = () => {
|
||||
const range: number[] = [];
|
||||
const maxButtons = 5; // 한 번에 보여줄 최대 페이지 버튼 수
|
||||
const halfMaxButtons = Math.floor(maxButtons / 2);
|
||||
|
||||
let startPage = Math.max(currentPage - halfMaxButtons, 1);
|
||||
let endPage = Math.min(startPage + maxButtons - 1, totalPages);
|
||||
|
||||
if (endPage - startPage + 1 < maxButtons) {
|
||||
startPage = Math.max(endPage - maxButtons + 1, 1);
|
||||
}
|
||||
|
||||
for (let i = startPage; i <= endPage; i++) {
|
||||
range.push(i);
|
||||
}
|
||||
|
||||
return range;
|
||||
};
|
||||
|
||||
if (totalPages <= 1) return null;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
{/* 첫 페이지 버튼 */}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onPageChange(1)}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
{"<<"}
|
||||
</Button>
|
||||
|
||||
{/* 이전 페이지 버튼 */}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onPageChange(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
{"<"}
|
||||
</Button>
|
||||
|
||||
{/* 페이지 번호 버튼들 */}
|
||||
{getPageRange().map((pageNum) => (
|
||||
<Button
|
||||
key={pageNum}
|
||||
variant={currentPage === pageNum ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => onPageChange(pageNum)}
|
||||
>
|
||||
{pageNum}
|
||||
</Button>
|
||||
))}
|
||||
|
||||
{/* 다음 페이지 버튼 */}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onPageChange(currentPage + 1)}
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
{">"}
|
||||
</Button>
|
||||
|
||||
{/* 마지막 페이지 버튼 */}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onPageChange(totalPages)}
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
{">>"}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { Pagination };
|
||||
@@ -6,7 +6,7 @@ function Skeleton({
|
||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
className={cn("animate-pulse rounded-md bg-[rgba(0,0,0,0.1)]", className)}
|
||||
className={cn("animate-pulse rounded-md bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user