Merge remote-tracking branch 'origin/main' into iipbs_cs

This commit is contained in:
2025-04-11 15:56:43 +09:00
30 changed files with 1997 additions and 2 deletions
+1
View File
@@ -17,3 +17,4 @@ dist
# env # env
.env* .env*
.qodo
+14 -1
View File
@@ -44,6 +44,12 @@ npx create-next-app@latest ./apps/as --typescript --eslint --tailwind --app
```json ```json
{ {
// 기타 설정 ... // 기타 설정 ...
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": { "dependencies": {
"@common/components": "workspace:*", // 추가 "@common/components": "workspace:*", // 추가
"react": "^19.0.0", // 제거 "react": "^19.0.0", // 제거
@@ -143,7 +149,9 @@ export default nextConfig;
### 6. globals.css(또는 전역 css) 수정 ### 6. globals.css(또는 전역 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 base;
@tailwind components; @tailwind components;
@@ -184,3 +192,8 @@ http://dev.onasys.co.kr:8000
## Swagger ## Swagger
http://dev.onasys.co.kr:8000/webjars/swagger-ui/index.html?urls.primaryName=attach-service 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;
+6
View File
@@ -0,0 +1,6 @@
export { GroupApi } from "./api/group-api";
export {
type GroupParams,
type Group,
type GroupFormData,
} from "./model/types";
+29
View File
@@ -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;
}
@@ -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;
},
};
+5
View File
@@ -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>
);
}
+256
View File
@@ -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 };
+213
View File
@@ -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 };
+104
View File
@@ -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;
},
};
+64
View File
@@ -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;
},
};
+1
View File
@@ -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 };
+258
View File
@@ -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 };
+145
View File
@@ -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 };
+2
View File
@@ -13,6 +13,8 @@ export const ENDPOINT = {
LOGIN: "/user-service/login", LOGIN: "/user-service/login",
USERS: "/user-service/api/v1/users", USERS: "/user-service/api/v1/users",
SILENT_REFRESH: "/user-service/api/v1/users/token/refresh", 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: { CMS_SERVICE: {
SITES: "/cms-service/api/v1/sites", SITES: "/cms-service/api/v1/sites",
@@ -49,4 +49,4 @@ import { useWrStore } from '../../../store';
</div> </div>
</> </>
) )
} }
+1
View File
@@ -0,0 +1 @@
.qodo
@@ -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 };