From e7f2b3e3ee97fd61513ab84ca6832361936c5411 Mon Sep 17 00:00:00 2001 From: chanii8 Date: Mon, 17 Mar 2025 19:14:32 +0900 Subject: [PATCH 1/9] Update README.md --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index b305a95a3..798425682 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", // 제거 From c31a6ea8bf443b3aa9391f7fba3010b12ffd0512 Mon Sep 17 00:00:00 2001 From: yschoi Date: Wed, 19 Mar 2025 09:24:36 +0900 Subject: [PATCH 2/9] =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reuseFacilityInsert/publicReuseFaciltyInsert/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 +} From 131ceec61e22d77723a6ccdc1a66fe9ab1c487da Mon Sep 17 00:00:00 2001 From: jcchoi Date: Wed, 26 Mar 2025 14:14:23 +0900 Subject: [PATCH 3/9] Update README.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit components.css 추가 --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 798425682..e74e7e815 100644 --- a/README.md +++ b/README.md @@ -149,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; From e3fc4e4dbff5af7c1572d8668a05955c3e00c9ff Mon Sep 17 00:00:00 2001 From: jcchoi Date: Mon, 31 Mar 2025 10:45:43 +0900 Subject: [PATCH 4/9] Update README.md --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index e74e7e815..11d5bcdbf 100644 --- a/README.md +++ b/README.md @@ -192,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 데이터 전달 처리 From 16511de37db1547049966faf1b3cd38b580c616f Mon Sep 17 00:00:00 2001 From: jcchoi Date: Mon, 31 Mar 2025 10:46:13 +0900 Subject: [PATCH 5/9] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 11d5bcdbf..b4e78b538 100644 --- a/README.md +++ b/README.md @@ -194,6 +194,6 @@ http://dev.onasys.co.kr:8000 http://dev.onasys.co.kr:8000/webjars/swagger-ui/index.html?urls.primaryName=attach-service ## 개발 가이드 -사이드바 메뉴 클릭 시 "menuCd" 데이터 쿠키 저장 처리 -쿠키명 : menu-code-key -해당 메뉴 쿠키 데이터 가져와 페이지 권한 및 상당 Breadcrumb 데이터 전달 처리 +- 사이드바 메뉴 클릭 시 "menuCd" 데이터 쿠키 저장 처리 +- 쿠키명 : menu-code-key +- 해당 메뉴 쿠키 데이터 가져와 페이지 권한 및 상당 Breadcrumb 데이터 전달 처리 From 010ced20b525e8f0bbb8bb13584aacd234e89c0b Mon Sep 17 00:00:00 2001 From: changwng Date: Mon, 7 Apr 2025 17:14:12 +0900 Subject: [PATCH 6/9] =?UTF-8?q?group=20=EA=B4=80=EB=A6=AC=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + apps/cs/app/(with-base-layout)/group/page.tsx | 5 + .../group/register/[id]/page.tsx | 10 + .../group/register/page.tsx | 4 + apps/cs/src/entities/group/index.ts | 6 + apps/cs/src/entities/group/model/types.ts | 29 ++ apps/cs/src/pages/group/api/group-api.ts | 64 +++++ apps/cs/src/pages/group/index.ts | 1 + .../pages/group/model/group-table-columns.tsx | 62 +++++ apps/cs/src/pages/group/ui/create-page.tsx | 71 +++++ apps/cs/src/pages/group/ui/group-form.tsx | 258 ++++++++++++++++++ apps/cs/src/pages/group/ui/group-page.tsx | 145 ++++++++++ apps/cs/src/pages/group/ui/modify-page.tsx | 95 +++++++ apps/cs/src/shared/config/api.ts | 1 + packages/.gitignore | 1 + packages/common/components/src/ui/index.ts | 1 + .../components/src/ui/pagination/index.tsx | 90 ++++++ 17 files changed, 844 insertions(+) create mode 100644 apps/cs/app/(with-base-layout)/group/page.tsx create mode 100644 apps/cs/app/(with-base-layout)/group/register/[id]/page.tsx create mode 100644 apps/cs/app/(with-base-layout)/group/register/page.tsx create mode 100644 apps/cs/src/entities/group/index.ts create mode 100644 apps/cs/src/entities/group/model/types.ts create mode 100644 apps/cs/src/pages/group/api/group-api.ts create mode 100644 apps/cs/src/pages/group/index.ts create mode 100644 apps/cs/src/pages/group/model/group-table-columns.tsx create mode 100644 apps/cs/src/pages/group/ui/create-page.tsx create mode 100644 apps/cs/src/pages/group/ui/group-form.tsx create mode 100644 apps/cs/src/pages/group/ui/group-page.tsx create mode 100644 apps/cs/src/pages/group/ui/modify-page.tsx create mode 100644 packages/.gitignore create mode 100644 packages/common/components/src/ui/pagination/index.tsx 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/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/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..7cfd42348 --- /dev/null +++ b/apps/cs/src/pages/group/model/group-table-columns.tsx @@ -0,0 +1,62 @@ +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, + }, */ + { + accessorKey: "groupId", + header: "그룹 ID", + }, + { + accessorKey: "groupNm", + header: "그룹명", + }, + { + accessorKey: "groupSeNm", + header: "그룹구분", + }, + { + accessorKey: "delYn", + header: "삭제여부", + cell: ({ row }) => ( + + ) + }, + { + 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..9c6d1b410 --- /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.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 4430285d8..6ea755a71 100644 --- a/apps/cs/src/shared/config/api.ts +++ b/apps/cs/src/shared/config/api.ts @@ -21,6 +21,7 @@ export const ENDPOINT = { CONTENTS: "/cms-service/api/v1/contents", USER_OGNZ: "/cms-service/api/v1/user-ognz", IMAGES: "/cms-service/api/v1/images", + GROUP: "/cms-service/api/v1/groups", }, BOARD_SERVICE: { BBS: "/board-service/api/v1/bbs", 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/index.ts b/packages/common/components/src/ui/index.ts index c3aad6220..d80d9bc9f 100644 --- a/packages/common/components/src/ui/index.ts +++ b/packages/common/components/src/ui/index.ts @@ -22,3 +22,4 @@ export * from "./slider"; export * from "./switch"; export * from "./table"; export * from "./tabs"; +export * from "./pagination"; \ No newline at end of file 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 From 92b5341cd1107bf62a39ed318f68c42d2d33674e Mon Sep 17 00:00:00 2001 From: changwng Date: Mon, 7 Apr 2025 17:30:41 +0900 Subject: [PATCH 7/9] =?UTF-8?q?=EA=B7=B8=EB=A3=B9=EA=B4=80=EB=A6=AC=20?= =?UTF-8?q?=EC=83=81=EB=8B=A8=EC=97=90=20No=20=EB=B6=99=EC=9D=B4=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cs/src/pages/group/model/group-table-columns.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/apps/cs/src/pages/group/model/group-table-columns.tsx b/apps/cs/src/pages/group/model/group-table-columns.tsx index 7cfd42348..cd12ef6e8 100644 --- a/apps/cs/src/pages/group/model/group-table-columns.tsx +++ b/apps/cs/src/pages/group/model/group-table-columns.tsx @@ -23,9 +23,15 @@ export const columns: ColumnDef[] = [ enableSorting: false, enableHiding: false, }, */ + { + id: "no", + header: "NO", + accessorFn: (_, index) => index + 1, + }, { accessorKey: "groupId", header: "그룹 ID", + enableSorting: true, }, { accessorKey: "groupNm", @@ -46,6 +52,11 @@ export const columns: ColumnDef[] = [ /> ) }, + { + accessorKey: "siteId", + header: "사이트 ID", + enableSorting: true, + }, { accessorKey: "roleNm", header: "권한", From 48c1cced18bc13b3dfc543116622491d5bdd5c7e Mon Sep 17 00:00:00 2001 From: changwng Date: Tue, 8 Apr 2025 18:40:17 +0900 Subject: [PATCH 8/9] =?UTF-8?q?api=20=EA=B4=80=EB=A6=AC=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EB=9E=A8=20=EC=B6=94=EA=B0=80,=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=EC=8B=9C=20ApiQuery=20=EA=B0=92=EC=9D=B4=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=EB=90=98=EB=A9=B4=20=EB=B0=94=EB=A1=9C=20refetch?= =?UTF-8?q?=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cs/app/(with-base-layout)/apiman/page.tsx | 5 + .../apiman/register/[id]/page.tsx | 10 + .../apiman/register/page.tsx | 4 + apps/cs/src/pages/apiman/api/apiman-api.ts | 63 +++++ apps/cs/src/pages/apiman/index.ts | 5 + .../apiman/model/apiman-table-columns.tsx | 82 ++++++ apps/cs/src/pages/apiman/ui/apiman-form.tsx | 256 ++++++++++++++++++ apps/cs/src/pages/apiman/ui/apiman-page.tsx | 211 +++++++++++++++ apps/cs/src/pages/apiman/ui/create-page.tsx | 76 ++++++ apps/cs/src/pages/apiman/ui/modify-page.tsx | 101 +++++++ apps/cs/src/pages/group/ui/group-page.tsx | 2 +- apps/cs/src/shared/config/api.ts | 2 + 12 files changed, 816 insertions(+), 1 deletion(-) create mode 100644 apps/cs/app/(with-base-layout)/apiman/page.tsx create mode 100644 apps/cs/app/(with-base-layout)/apiman/register/[id]/page.tsx create mode 100644 apps/cs/app/(with-base-layout)/apiman/register/page.tsx create mode 100644 apps/cs/src/pages/apiman/api/apiman-api.ts create mode 100644 apps/cs/src/pages/apiman/index.ts create mode 100644 apps/cs/src/pages/apiman/model/apiman-table-columns.tsx create mode 100644 apps/cs/src/pages/apiman/ui/apiman-form.tsx create mode 100644 apps/cs/src/pages/apiman/ui/apiman-page.tsx create mode 100644 apps/cs/src/pages/apiman/ui/create-page.tsx create mode 100644 apps/cs/src/pages/apiman/ui/modify-page.tsx 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..167a7e868 --- /dev/null +++ b/apps/cs/app/(with-base-layout)/apiman/register/[id]/page.tsx @@ -0,0 +1,10 @@ +import { ModifyPage } from "@/pages/apiman/ui/modify-page"; + +export default async function Page({ + params, +}: { + params: { id: string }; +}) { + const authorizationNo = parseInt(params.id); + return ; +} 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/src/pages/apiman/api/apiman-api.ts b/apps/cs/src/pages/apiman/api/apiman-api.ts new file mode 100644 index 000000000..6f1d50256 --- /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..f6817015c --- /dev/null +++ b/apps/cs/src/pages/apiman/model/apiman-table-columns.tsx @@ -0,0 +1,82 @@ +import { ColumnDef } from "@tanstack/react-table"; +import { Badge } from "@common/components"; +import { Api } from "../api/apiman-api"; +import dayjs from "dayjs"; + +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"), + }, + +]; 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..d52725668 --- /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 패턴 + + ( + + + + + + )} + /> + + + + HTTP 메소드 + + ( + + + + + + )} + /> + + + + 정렬 순서 + + ( + + + 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..d43d01168 --- /dev/null +++ b/apps/cs/src/pages/apiman/ui/apiman-page.tsx @@ -0,0 +1,211 @@ +"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 { ENDPOINT } from "@/shared/config"; +import { columns } from "../model/apiman-table-columns"; + +function ApimanPage() { + const router = useRouter(); + const [apiQuery, setApiQuery] = useState({ + authorizationName: "", + httpMethodCode: "", + useYn: "", + page: 0, + size: 10, + }); + + const { data: apiList, refetch, isLoading } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.API, 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, + }, + }, + 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 handleReset = () => { + setApiQuery({ + authorizationName: "", + httpMethodCode: "", + useYn: "", + page: 0, + size: 10, + }); + refetch(); + }; + + const handleRowClick = (api: Api) => { + router.push(`/apiman/register/${api.authorizationNo}`); + }; + + return ( +
+
+
+
+ + setApiQuery( prev => ({ ...prev, authorizationName: e.target.value }))} + placeholder="API 명 입력" + /> + +
+ +
+ + + + +
+ +
+ + +
+ +
+ + +
+
+
+ +
+
+
+ 총 {apiList?.totalElements || 0}건 +
+ +
+ + + + {apiList && apiList.totalElements > 0 && ( +
+ setApiQuery({ ...apiQuery, page })} + /> + +
+ )} +
+
+ ); +} + +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..2e93b6143 --- /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..242aeccf8 --- /dev/null +++ b/apps/cs/src/pages/apiman/ui/modify-page.tsx @@ -0,0 +1,101 @@ +"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 수정

+
+ +
+ ); +} + +export { ModifyPage }; diff --git a/apps/cs/src/pages/group/ui/group-page.tsx b/apps/cs/src/pages/group/ui/group-page.tsx index 9c6d1b410..a9b198f3d 100644 --- a/apps/cs/src/pages/group/ui/group-page.tsx +++ b/apps/cs/src/pages/group/ui/group-page.tsx @@ -80,7 +80,7 @@ function GroupPage() { useEffect(() => { refetch(); - }, [groupQuery.page, groupQuery.size]); // 페이지나 사이즈 변경 시 자동 조회 + }, [groupQuery, refetch]); //[groupQuery.page, groupQuery.size]); // 페이지나 사이즈 변경 시 자동 조회 return (
diff --git a/apps/cs/src/shared/config/api.ts b/apps/cs/src/shared/config/api.ts index 6ea755a71..50f280edc 100644 --- a/apps/cs/src/shared/config/api.ts +++ b/apps/cs/src/shared/config/api.ts @@ -10,6 +10,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", + }, CMS_SERVICE: { SITES: "/cms-service/api/v1/sites", From b2751232e1d3faa519185bb542262d7932d06bb4 Mon Sep 17 00:00:00 2001 From: changwng Date: Wed, 9 Apr 2025 18:17:06 +0900 Subject: [PATCH 9/9] =?UTF-8?q?api-role=20Dialog=EA=B0=80=20=ED=98=B8?= =?UTF-8?q?=EC=B6=9C=EB=90=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../apiman/register/[id]/page.tsx | 12 +- apps/cs/src/pages/apiman/api/apiman-api.ts | 4 +- .../apiman/model/apiman-table-columns.tsx | 35 ++- .../src/pages/apiman/ui/api-role-dialog.tsx | 201 ++++++++++++++++++ apps/cs/src/pages/apiman/ui/apiman-form.tsx | 6 +- apps/cs/src/pages/apiman/ui/apiman-page.tsx | 194 ++++++++--------- apps/cs/src/pages/apiman/ui/create-page.tsx | 4 +- apps/cs/src/pages/apiman/ui/modify-page.tsx | 7 +- apps/cs/src/pages/apirole/api/apirole-api.ts | 77 +++++++ apps/cs/src/pages/group/ui/group-page.tsx | 4 +- apps/cs/src/shared/config/api.ts | 2 +- 11 files changed, 430 insertions(+), 116 deletions(-) create mode 100644 apps/cs/src/pages/apiman/ui/api-role-dialog.tsx create mode 100644 apps/cs/src/pages/apirole/api/apirole-api.ts 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 index 167a7e868..6d382c878 100644 --- a/apps/cs/app/(with-base-layout)/apiman/register/[id]/page.tsx +++ b/apps/cs/app/(with-base-layout)/apiman/register/[id]/page.tsx @@ -1,10 +1,12 @@ -import { ModifyPage } from "@/pages/apiman/ui/modify-page"; +import { ModifyPage as VModifyPage } from "@/pages/apiman/ui/modify-page"; -export default async function Page({ + +export default async function ModifyPage({ params, }: { - params: { id: string }; + params: Promise<{ id: number}>; }) { - const authorizationNo = parseInt(params.id); - return ; + const { id } = await params; + return ; } + \ 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 index 6f1d50256..ce9a8e189 100644 --- a/apps/cs/src/pages/apiman/api/apiman-api.ts +++ b/apps/cs/src/pages/apiman/api/apiman-api.ts @@ -6,7 +6,7 @@ export interface Api { authorizationNo: number; // API 일련번호 authorizationName: string; // API 명 urlPatternValue: string; // URL 패턴 - httpMethodCode: string; // 하이퍼텍스트 전송 프로토콜 메쏘드 값 (예: GET, POST) + httpMethodCode: string; // 하이퍼텍스트 전송 프로토콜 메쏘드 값 (예: GET, POST) sortSeq: number; // 정렬 순서 useYn: string; // 사용 여부 (Y/N) regDt: string; // 등록 일시 @@ -25,7 +25,7 @@ export interface ApiParams { export interface ApiFormData { authorizationNo: number | null; // API 일련번호 (신규 등록 시 null) - authorizationName: string; // API 명 + authorizationName: string; // API 명 urlPatternValue: string; // URL 패턴 httpMethodCode: string; // HTTP 메소드 sortSeq: number; // 정렬 순서 diff --git a/apps/cs/src/pages/apiman/model/apiman-table-columns.tsx b/apps/cs/src/pages/apiman/model/apiman-table-columns.tsx index f6817015c..22c0ba701 100644 --- a/apps/cs/src/pages/apiman/model/apiman-table-columns.tsx +++ b/apps/cs/src/pages/apiman/model/apiman-table-columns.tsx @@ -1,8 +1,13 @@ import { ColumnDef } from "@tanstack/react-table"; -import { Badge } from "@common/components"; -import { Api } from "../api/apiman-api"; +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", @@ -78,5 +83,29 @@ export const columns: ColumnDef[] = [ 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 index d52725668..f27502f4e 100644 --- a/apps/cs/src/pages/apiman/ui/apiman-form.tsx +++ b/apps/cs/src/pages/apiman/ui/apiman-form.tsx @@ -140,9 +140,9 @@ function ApimanForm({ form, authorizationNo, handleSave, handleDelete }: ApimanF )} /> - + - HTTP 메소드 + 요청 메소드 - + GET diff --git a/apps/cs/src/pages/apiman/ui/apiman-page.tsx b/apps/cs/src/pages/apiman/ui/apiman-page.tsx index d43d01168..fb0b82511 100644 --- a/apps/cs/src/pages/apiman/ui/apiman-page.tsx +++ b/apps/cs/src/pages/apiman/ui/apiman-page.tsx @@ -22,8 +22,10 @@ import { } 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(); @@ -34,9 +36,18 @@ function ApimanPage() { 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.CMS_SERVICE.API, apiQuery], + queryKey: [ENDPOINT.USER_SERVICE.AUTHRTS, apiQuery], queryFn: () => ApimanApi.getApiList(apiQuery), enabled: false, }); @@ -53,6 +64,14 @@ function ApimanPage() { pageSize: apiQuery.size ?? 10, }, }, + meta: { + openRoleDialog: (api: ApiRoleByApi) => { + setApiRoleDialog({ + isOpen: true, + api, + }); + }, + }, onPaginationChange: (updater) => { const newPagination = typeof updater === "function" @@ -77,48 +96,46 @@ function ApimanPage() { setApiQuery(prev => ({ ...prev, page: 0 })); refetch(); }; - - const handleReset = () => { - setApiQuery({ - authorizationName: "", - httpMethodCode: "", - useYn: "", - page: 0, - size: 10, - }); - 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 ( -
-
-
-
- - setApiQuery( prev => ({ ...prev, authorizationName: e.target.value }))} - placeholder="API 명 입력" - /> +
+ {/* API 역할 다이얼로그 */} + setApiRoleDialog({ isOpen: false, api: null })} + onSave={handleSaveRoles} + /> + +
+ -
- -
- - - - setApiQuery( prev => ({ ...prev, httpMethodCode: value === "ALL" ? "" : value }))} > - - + + 전체 @@ -131,78 +148,63 @@ function ApimanPage() { HEAD -
- -
- - -
- -
- - -
+ + setApiQuery( prev => ({ ...prev, authorizationName: e.target.value }))} + placeholder="API 명 입력" + /> +
+ + -
-
-
- 총 {apiList?.totalElements || 0}건 -
- -
+
+
+

모듈API 목록

+ +
- - {apiList && apiList.totalElements > 0 && ( -
- setApiQuery({ ...apiQuery, page })} - /> - -
- )} + +
+
+ 총 {apiList?.totalElements ?? 0} 건 +
+ + setApiQuery(prev => ({ ...prev, page: page - 1 })) + } + /> + +
); diff --git a/apps/cs/src/pages/apiman/ui/create-page.tsx b/apps/cs/src/pages/apiman/ui/create-page.tsx index 2e93b6143..69f774765 100644 --- a/apps/cs/src/pages/apiman/ui/create-page.tsx +++ b/apps/cs/src/pages/apiman/ui/create-page.tsx @@ -18,7 +18,7 @@ const apiFormSchema = z.object({ urlPatternValue: z.string().min(1, "URL 패턴을 입력해주세요"), httpMethodCode: z.string().min(1, "HTTP 메소드를 선택해주세요"), sortSeq: z.number().min(0, "정렬 순서는 0 이상이어야 합니다"), - useYn: z.string(), + useYn: z.string(), }); function CreatePage() { @@ -56,7 +56,7 @@ function CreatePage() { urlPatternValue: "", httpMethodCode: "", sortSeq: 0, - useYn: "Y" + useYn: "Y" } }); diff --git a/apps/cs/src/pages/apiman/ui/modify-page.tsx b/apps/cs/src/pages/apiman/ui/modify-page.tsx index 242aeccf8..a6185e8d0 100644 --- a/apps/cs/src/pages/apiman/ui/modify-page.tsx +++ b/apps/cs/src/pages/apiman/ui/modify-page.tsx @@ -18,7 +18,7 @@ const apiFormSchema = z.object({ urlPatternValue: z.string().min(1, "URL 패턴을 입력해주세요"), httpMethodCode: z.string().min(1, "HTTP 메소드를 선택해주세요"), sortSeq: z.number(), - useYn: z.string(), + useYn: z.string(), }); function ModifyPage({ authorizationNo }: { authorizationNo: number }) { @@ -91,7 +91,10 @@ function ModifyPage({ authorizationNo }: { authorizationNo: number }) { { + await updateApi(data); + return; + }} handleDelete={deleteApi} />
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/ui/group-page.tsx b/apps/cs/src/pages/group/ui/group-page.tsx index a9b198f3d..99459c185 100644 --- a/apps/cs/src/pages/group/ui/group-page.tsx +++ b/apps/cs/src/pages/group/ui/group-page.tsx @@ -103,13 +103,13 @@ function GroupPage() {

그룹 목록

- +
- 총 {groupList?.totalElements ?? 0} 개 + 총 {groupList?.totalElements ?? 0} 건