From 48c1cced18bc13b3dfc543116622491d5bdd5c7e Mon Sep 17 00:00:00 2001 From: changwng Date: Tue, 8 Apr 2025 18:40:17 +0900 Subject: [PATCH] =?UTF-8?q?api=20=EA=B4=80=EB=A6=AC=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EB=9E=A8=20=EC=B6=94=EA=B0=80,=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=EC=8B=9C=20ApiQuery=20=EA=B0=92=EC=9D=B4=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=EB=90=98=EB=A9=B4=20=EB=B0=94=EB=A1=9C=20refetch=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",