From 010ced20b525e8f0bbb8bb13584aacd234e89c0b Mon Sep 17 00:00:00 2001 From: changwng Date: Mon, 7 Apr 2025 17:14:12 +0900 Subject: [PATCH] =?UTF-8?q?group=20=EA=B4=80=EB=A6=AC=EC=B6=94=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