diff --git a/.gitignore b/.gitignore index 40f0d864b..c40b4dc6e 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ dist # env .env* +.qodo diff --git a/README.md b/README.md index b305a95a3..b4e78b538 100644 --- a/README.md +++ b/README.md @@ -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 데이터 전달 처리 diff --git a/apps/cs/app/(with-base-layout)/apiman/page.tsx b/apps/cs/app/(with-base-layout)/apiman/page.tsx new file mode 100644 index 000000000..1e3b023df --- /dev/null +++ b/apps/cs/app/(with-base-layout)/apiman/page.tsx @@ -0,0 +1,5 @@ +import { ApimanPage } from "@/pages/apiman"; + +export default function Page() { + return ; +} diff --git a/apps/cs/app/(with-base-layout)/apiman/register/[id]/page.tsx b/apps/cs/app/(with-base-layout)/apiman/register/[id]/page.tsx new file mode 100644 index 000000000..6d382c878 --- /dev/null +++ b/apps/cs/app/(with-base-layout)/apiman/register/[id]/page.tsx @@ -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 ; +} + \ No newline at end of file diff --git a/apps/cs/app/(with-base-layout)/apiman/register/page.tsx b/apps/cs/app/(with-base-layout)/apiman/register/page.tsx new file mode 100644 index 000000000..cfd796314 --- /dev/null +++ b/apps/cs/app/(with-base-layout)/apiman/register/page.tsx @@ -0,0 +1,4 @@ +"use client"; + +import { CreatePage } from "@/pages/apiman/ui/create-page"; +export default CreatePage; diff --git a/apps/cs/app/(with-base-layout)/group/page.tsx b/apps/cs/app/(with-base-layout)/group/page.tsx new file mode 100644 index 000000000..97c32aeb7 --- /dev/null +++ b/apps/cs/app/(with-base-layout)/group/page.tsx @@ -0,0 +1,5 @@ +import { GroupPage } from "@/pages/group"; + +export default function Page() { + return ; +} \ No newline at end of file diff --git a/apps/cs/app/(with-base-layout)/group/register/[id]/page.tsx b/apps/cs/app/(with-base-layout)/group/register/[id]/page.tsx new file mode 100644 index 000000000..9b77a8408 --- /dev/null +++ b/apps/cs/app/(with-base-layout)/group/register/[id]/page.tsx @@ -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 ; +} diff --git a/apps/cs/app/(with-base-layout)/group/register/page.tsx b/apps/cs/app/(with-base-layout)/group/register/page.tsx new file mode 100644 index 000000000..d1ed3f7a4 --- /dev/null +++ b/apps/cs/app/(with-base-layout)/group/register/page.tsx @@ -0,0 +1,4 @@ +"use client"; + +import { CreatePage } from "@/pages/group/ui/create-page"; +export default CreatePage; diff --git a/apps/cs/src/entities/group/index.ts b/apps/cs/src/entities/group/index.ts new file mode 100644 index 000000000..6bfb42bf1 --- /dev/null +++ b/apps/cs/src/entities/group/index.ts @@ -0,0 +1,6 @@ +export { GroupApi } from "./api/group-api"; +export { + type GroupParams, + type Group, + type GroupFormData, +} from "./model/types"; diff --git a/apps/cs/src/entities/group/model/types.ts b/apps/cs/src/entities/group/model/types.ts new file mode 100644 index 000000000..79ba1211b --- /dev/null +++ b/apps/cs/src/entities/group/model/types.ts @@ -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; +} \ No newline at end of file diff --git a/apps/cs/src/pages/apiman/api/apiman-api.ts b/apps/cs/src/pages/apiman/api/apiman-api.ts new file mode 100644 index 000000000..ce9a8e189 --- /dev/null +++ b/apps/cs/src/pages/apiman/api/apiman-api.ts @@ -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>( + `${ENDPOINT.USER_SERVICE.AUTHRTS}`, + { params } + ); + return data; + }, + + getApi: async (authorizationNo: number) => { + const { data } = await client.get(`${ENDPOINT.USER_SERVICE.AUTHRTS}/${authorizationNo}`); + return data; + }, + + createApi: async (apiData: ApiFormData) => { + const { data } = await client.post(ENDPOINT.USER_SERVICE.AUTHRTS, apiData); + return data; + }, + + updateApi: async (authorizationNo: number, apiData: ApiFormData) => { + const { data } = await client.put(`${ENDPOINT.USER_SERVICE.AUTHRTS}/${authorizationNo}`, apiData); + return data; + }, + + deleteApi: async (authorizationNo: number) => { + const { data } = await client.delete(`${ENDPOINT.USER_SERVICE.AUTHRTS}/${authorizationNo}`); + return data; + }, +}; diff --git a/apps/cs/src/pages/apiman/index.ts b/apps/cs/src/pages/apiman/index.ts new file mode 100644 index 000000000..3d28d26f1 --- /dev/null +++ b/apps/cs/src/pages/apiman/index.ts @@ -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"; diff --git a/apps/cs/src/pages/apiman/model/apiman-table-columns.tsx b/apps/cs/src/pages/apiman/model/apiman-table-columns.tsx new file mode 100644 index 000000000..22c0ba701 --- /dev/null +++ b/apps/cs/src/pages/apiman/model/apiman-table-columns.tsx @@ -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[] = [ + { + accessorKey: "authorizationNo", + header: "번호", + cell: ({ row }) =>
{row.original.authorizationNo}
, + }, + { + accessorKey: "authorizationName", + header: "API 명", + cell: ({ row }) =>
{row.original.authorizationName}
, + }, + { + accessorKey: "urlPatternValue", + header: "URL 패턴", + cell: ({ row }) => ( +
+ {row.original.urlPatternValue} +
+ ), + }, + { + 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 ( + + {method} + + ); + }, + }, + { + accessorKey: "sortSeq", + header: "정렬 순서", + cell: ({ row }) =>
{row.original.sortSeq}
, + }, + { + accessorKey: "useYn", + header: "사용 여부", + cell: ({ row }) => { + const useYn = row.original.useYn; + return ( + + {useYn === "Y" ? "사용" : "미사용"} + + ); + }, + }, + { + 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 ( + + ); + }, + }, +]; diff --git a/apps/cs/src/pages/apiman/ui/api-role-dialog.tsx b/apps/cs/src/pages/apiman/ui/api-role-dialog.tsx new file mode 100644 index 000000000..8c78fc5c0 --- /dev/null +++ b/apps/cs/src/pages/apiman/ui/api-role-dialog.tsx @@ -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 { + 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; +} + +export function ApiRoleDialog({ api, isOpen, onClose, onSave }: ApiRoleDialogProps): JSX.Element { + const [roles, setRoles] = useState([]); + const [loading, setLoading] = useState(false); + + // API에서 역할 데이터 가져오기 + useEffect(() => { + if (isOpen && api && api.authorizationNo) { + // 실제 API 호출로 데이터 가져오기 + ApiRoleApi.getApiRoleByApiList({ authorizationNo: api.authorizationNo }) + .then((data: Page) => { + // 데이터를 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 ( + + + + + API 권한 설정 - {api?.authorizationName} + + + +
+ + + + + + + + + + API 명 + + {api?.authorizationName || ""} + + HTTP 메소드 + + {api?.httpMethodCode || ""} + + + + URL 패턴 + + {api?.urlPatternValue || ""} + + + +
+
+ +
+ + + + 허용 + 권한 ID + 권한명 + 권한내용 + 사용여부 + + + + {roles.map((role) => ( + + + handleToggleRole(role.roleId)} + /> + + {role.roleId} + {role.roleName} + {role.roleContent} + + + {role.useYn === "Y" ? "사용" : "미사용"} + + + + ))} + {roles.length === 0 && ( + + + 권한 정보가 없습니다. + + + )} + +
+
+ + + + + +
+
+ ); +} diff --git a/apps/cs/src/pages/apiman/ui/apiman-form.tsx b/apps/cs/src/pages/apiman/ui/apiman-form.tsx new file mode 100644 index 000000000..f27502f4e --- /dev/null +++ b/apps/cs/src/pages/apiman/ui/apiman-form.tsx @@ -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; + authorizationNo?: number; + handleSave: (data: ApiFormData) => Promise; + handleDelete?: () => Promise; +} + +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) => { + 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 ( + <> +
+ +
+ + + + + + + {isModifyMode && ( + + API 일련번호 + + ( + + + + + + )} + /> + + + )} + + API 명 + + ( + + + + + + )} + /> + + + + URL 패턴 + + ( + + + + + + )} + /> + + + + 요청 메소드 + + ( + + + + + + )} + /> + + + + 정렬 순서 + + ( + + + field.onChange(parseInt(e.target.value) || 0)} + /> + + + )} + /> + + + + 사용 여부 + + ( + + + +
+ + +
+
+ + +
+
+
+
+ )} + /> +
+
+
+
+
+
+ + + {isModifyMode && ( + + )} +
+
+ + + + ); +} + +export { ApimanForm }; diff --git a/apps/cs/src/pages/apiman/ui/apiman-page.tsx b/apps/cs/src/pages/apiman/ui/apiman-page.tsx new file mode 100644 index 000000000..fb0b82511 --- /dev/null +++ b/apps/cs/src/pages/apiman/ui/apiman-page.tsx @@ -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({ + 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) => { + 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 ( +
+ {/* API 역할 다이얼로그 */} + setApiRoleDialog({ isOpen: false, api: null })} + onSave={handleSaveRoles} + /> + +
+
+ + + + + setApiQuery( prev => ({ ...prev, authorizationName: e.target.value }))} + placeholder="API 명 입력" + /> + +
+
+ + + +
+
+

모듈API 목록

+ +
+ + + +
+
+ 총 {apiList?.totalElements ?? 0} 건 +
+ + setApiQuery(prev => ({ ...prev, page: page - 1 })) + } + /> + +
+
+
+ ); +} + +export { ApimanPage }; diff --git a/apps/cs/src/pages/apiman/ui/create-page.tsx b/apps/cs/src/pages/apiman/ui/create-page.tsx new file mode 100644 index 000000000..69f774765 --- /dev/null +++ b/apps/cs/src/pages/apiman/ui/create-page.tsx @@ -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({ + resolver: zodResolver(apiFormSchema), + defaultValues: { + authorizationNo: null, + authorizationName: "", + urlPatternValue: "", + httpMethodCode: "", + sortSeq: 0, + useYn: "Y" + } + }); + + return ( +
+
+

API 등록

+
+ +
+ ); +} + +export { CreatePage }; diff --git a/apps/cs/src/pages/apiman/ui/modify-page.tsx b/apps/cs/src/pages/apiman/ui/modify-page.tsx new file mode 100644 index 000000000..a6185e8d0 --- /dev/null +++ b/apps/cs/src/pages/apiman/ui/modify-page.tsx @@ -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({ + resolver: zodResolver(apiFormSchema), + values: apiDetail as ApiFormData, + }); + + return ( +
+
+

API 수정

+
+ { + await updateApi(data); + return; + }} + handleDelete={deleteApi} + /> +
+ ); +} + +export { ModifyPage }; diff --git a/apps/cs/src/pages/apirole/api/apirole-api.ts b/apps/cs/src/pages/apirole/api/apirole-api.ts new file mode 100644 index 000000000..c525c9a6a --- /dev/null +++ b/apps/cs/src/pages/apirole/api/apirole-api.ts @@ -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>( + `${ENDPOINT.USER_SERVICE.ROLES_AUTHRTS}`, + { params } + ); + return data; + }, + getApiRoleByApiList: async (params: ApiRoleParams) => { + const { data } = await client.get>( + `${ENDPOINT.USER_SERVICE.ROLES_AUTHRTS}/api`, + { params } + ); + return data; + }, + getApiRole: async (authorizationNo: number) => { + const { data } = await client.get(`${ENDPOINT.USER_SERVICE.ROLES_AUTHRTS}/${authorizationNo}`); + return data; + }, + + createApiRole: async (apiData: ApiRoleFormData) => { + const { data } = await client.post(ENDPOINT.USER_SERVICE.ROLES_AUTHRTS, apiData); + return data; + }, + + updateApiRole: async (authorizationNo: number, apiData: ApiRoleFormData) => { + const { data } = await client.put(`${ENDPOINT.USER_SERVICE.ROLES_AUTHRTS}/${authorizationNo}`, apiData); + return data; + }, + + deleteApiRole: async (authorizationNo: number) => { + const { data } = await client.delete(`${ENDPOINT.USER_SERVICE.ROLES_AUTHRTS}/${authorizationNo}`); + return data; + }, +}; diff --git a/apps/cs/src/pages/group/api/group-api.ts b/apps/cs/src/pages/group/api/group-api.ts new file mode 100644 index 000000000..ffffa958d --- /dev/null +++ b/apps/cs/src/pages/group/api/group-api.ts @@ -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>( + `${ENDPOINT.CMS_SERVICE.GROUP}` + , { params }); + return data; + }, + + getGroup: async (groupId: string) => { + const { data } = await client.get(`${ENDPOINT.CMS_SERVICE.GROUP}/${groupId}`); + return data; + }, + + createGroup: async (groupData: GroupFormData) => { + const { data } = await client.post(ENDPOINT.CMS_SERVICE.GROUP, groupData); + return data; + }, + + updateGroup: async (groupId: string, groupData: GroupFormData) => { + const { data } = await client.put(`${ENDPOINT.CMS_SERVICE.GROUP}/${groupId}`, groupData); + return data; + }, + + deleteGroup: async (groupId: string) => { + const { data } = await client.delete(`${ENDPOINT.CMS_SERVICE.GROUP}/${groupId}`); + return data; + }, +}; \ No newline at end of file diff --git a/apps/cs/src/pages/group/index.ts b/apps/cs/src/pages/group/index.ts new file mode 100644 index 000000000..8ba97789f --- /dev/null +++ b/apps/cs/src/pages/group/index.ts @@ -0,0 +1 @@ +export { GroupPage } from "./ui/group-page"; \ No newline at end of file diff --git a/apps/cs/src/pages/group/model/group-table-columns.tsx b/apps/cs/src/pages/group/model/group-table-columns.tsx new file mode 100644 index 000000000..cd12ef6e8 --- /dev/null +++ b/apps/cs/src/pages/group/model/group-table-columns.tsx @@ -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[] = [ + /* { + id: "select", + header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!value)} + aria-label="전체 선택" + /> + ), + cell: ({ row }) => ( + 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 }) => ( + + ) + }, + { + 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"), + } +]; \ No newline at end of file diff --git a/apps/cs/src/pages/group/ui/create-page.tsx b/apps/cs/src/pages/group/ui/create-page.tsx new file mode 100644 index 000000000..1970147dd --- /dev/null +++ b/apps/cs/src/pages/group/ui/create-page.tsx @@ -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({ + resolver: zodResolver(groupFormSchema), + defaultValues: { + groupId: "", + groupNm: "", + groupSeCd: "", + groupExpln: "", + delYn: "N", + authrtId: "", + siteId: "", + }, + }); + + return ( +
+
+

그룹 등록

+
+ +
+ ); +} + +export { CreatePage }; \ No newline at end of file diff --git a/apps/cs/src/pages/group/ui/group-form.tsx b/apps/cs/src/pages/group/ui/group-form.tsx new file mode 100644 index 000000000..2a88a0bd7 --- /dev/null +++ b/apps/cs/src/pages/group/ui/group-form.tsx @@ -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; + groupId?: string; + handleSave: (data: GroupFormData) => Promise; + handleDelete?: () => Promise; +} + +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) => { + 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 ( + <> +
+ +
+ + + + + + + + 그룹 ID + + ( + + + + + + )} + /> + + + + 그룹명 + + ( + + + + + + )} + /> + + + + 그룹구분 + + ( + + + + + + )} + /> + + + + 그룹설명 + + ( + + + + + + )} + /> + + + + 사이트 ID + + ( + + + + + + )} + /> + + + + 권한 ID + + ( + + + + + + )} + /> + + + + 삭제여부 + + ( + + + +
+ + +
+
+ + +
+
+
+
+ )} + /> +
+
+
+
+
+
+ + + {isModifyMode && ( + + )} +
+
+ + + + ); +} + +export { GroupForm }; \ No newline at end of file diff --git a/apps/cs/src/pages/group/ui/group-page.tsx b/apps/cs/src/pages/group/ui/group-page.tsx new file mode 100644 index 000000000..99459c185 --- /dev/null +++ b/apps/cs/src/pages/group/ui/group-page.tsx @@ -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({ + 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) => { + 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 ( +
+
+
+ + setGroupQuery(prev => ({ ...prev, groupNm: e.target.value }))} + /> + + +
+
+
+
+

그룹 목록

+ +
+
+ +
+
+ 총 {groupList?.totalElements ?? 0} 건 +
+ + setGroupQuery(prev => ({ ...prev, page: page - 1 })) + } + /> + +
+
+
+
+ ); +} + +export { GroupPage }; \ No newline at end of file diff --git a/apps/cs/src/pages/group/ui/modify-page.tsx b/apps/cs/src/pages/group/ui/modify-page.tsx new file mode 100644 index 000000000..0c3482384 --- /dev/null +++ b/apps/cs/src/pages/group/ui/modify-page.tsx @@ -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({ + resolver: zodResolver(groupFormSchema), + values: groupDetail as GroupFormData, + }); + + return ( +
+
+

그룹 수정

+
+ +
+ ); +} + +export { ModifyPage }; \ No newline at end of file diff --git a/apps/cs/src/shared/config/api.ts b/apps/cs/src/shared/config/api.ts index 86478ac19..94d2e728d 100644 --- a/apps/cs/src/shared/config/api.ts +++ b/apps/cs/src/shared/config/api.ts @@ -13,6 +13,8 @@ export const ENDPOINT = { 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", diff --git a/apps/wr/app/reuseFaciltyManage/reuseFacilityInsert/publicReuseFaciltyInsert/page.tsx b/apps/wr/app/reuseFaciltyManage/reuseFacilityInsert/publicReuseFaciltyInsert/page.tsx index a122d99c3..57c48a84d 100644 --- a/apps/wr/app/reuseFaciltyManage/reuseFacilityInsert/publicReuseFaciltyInsert/page.tsx +++ b/apps/wr/app/reuseFaciltyManage/reuseFacilityInsert/publicReuseFaciltyInsert/page.tsx @@ -49,4 +49,4 @@ import { useWrStore } from '../../../store'; ) -} \ No newline at end of file +} diff --git a/packages/.gitignore b/packages/.gitignore new file mode 100644 index 000000000..45784b06e --- /dev/null +++ b/packages/.gitignore @@ -0,0 +1 @@ +.qodo diff --git a/packages/common/components/src/ui/pagination/index.tsx b/packages/common/components/src/ui/pagination/index.tsx new file mode 100644 index 000000000..5557727f5 --- /dev/null +++ b/packages/common/components/src/ui/pagination/index.tsx @@ -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 ( +
+ {/* 첫 페이지 버튼 */} + + + {/* 이전 페이지 버튼 */} + + + {/* 페이지 번호 버튼들 */} + {getPageRange().map((pageNum) => ( + + ))} + + {/* 다음 페이지 버튼 */} + + + {/* 마지막 페이지 버튼 */} + +
+ ); +} + +export { Pagination }; \ No newline at end of file