From 915543628db421b62dc5522c1ec9608119564cd3 Mon Sep 17 00:00:00 2001 From: bicochan Date: Wed, 19 Mar 2025 16:23:33 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20ip=20=EA=B4=80=EB=A6=AC=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=20=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cs/src/pages/site/api/ip-service.ts | 20 + apps/cs/src/pages/site/model/ip-interface.ts | 50 +++ .../pages/site/ui/ip-management-dialog.tsx | 365 +++++++++++------- apps/cs/src/shared/config/api.ts | 1 + 4 files changed, 295 insertions(+), 141 deletions(-) create mode 100644 apps/cs/src/pages/site/api/ip-service.ts create mode 100644 apps/cs/src/pages/site/model/ip-interface.ts diff --git a/apps/cs/src/pages/site/api/ip-service.ts b/apps/cs/src/pages/site/api/ip-service.ts new file mode 100644 index 000000000..2bfad0934 --- /dev/null +++ b/apps/cs/src/pages/site/api/ip-service.ts @@ -0,0 +1,20 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; +import type { IP, IpList, IpService } from "../model/ip-interface"; + +export class IpApiService implements IpService { + public async getIpList(): Promise { + const response = await client.get(`${ENDPOINT.CMS_SERVICE.IPS}`); + return response.data; + } + + public async saveIpList(body: IP[]): Promise { + const response = await client.post( + `${ENDPOINT.CMS_SERVICE.IPS}`, + body, + ); + return response.data; + } +} + +export const IpApi = new IpApiService(); diff --git a/apps/cs/src/pages/site/model/ip-interface.ts b/apps/cs/src/pages/site/model/ip-interface.ts new file mode 100644 index 000000000..f8cf9d5ea --- /dev/null +++ b/apps/cs/src/pages/site/model/ip-interface.ts @@ -0,0 +1,50 @@ +import { z } from "zod"; + +export const ipSchema = z.object({ + mngNo: z.number().nullish(), + siteId: z.string(), + ipAddr: z.string().nonempty(), + prmYn: z.string(), + mode: z.string(), +}); + +export const ipFormSchema = z.object({ + ips: z.array(ipSchema), +}); + +export type IP = z.infer; +export type IPFormData = z.infer; + +export interface IpList { + content: IP[]; + pageable: { + sort: { + empty: boolean; + sorted: boolean; + unsorted: boolean; + }; + offset: number; + pageNumber: number; + pageSize: number; + paged: boolean; + unpaged: boolean; + }; + last: boolean; + totalPages: number; + totalElements: number; + number: number; + sort: { + empty: boolean; + sorted: boolean; + unsorted: boolean; + }; + size: number; + first: boolean; + numberOfElements: number; + empty: boolean; +} + +export interface IpService { + getIpList(): Promise; + saveIpList(body: IP[]): Promise; +} diff --git a/apps/cs/src/pages/site/ui/ip-management-dialog.tsx b/apps/cs/src/pages/site/ui/ip-management-dialog.tsx index 06d9638b3..35166d0de 100644 --- a/apps/cs/src/pages/site/ui/ip-management-dialog.tsx +++ b/apps/cs/src/pages/site/ui/ip-management-dialog.tsx @@ -1,4 +1,8 @@ import { useState } from "react"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useConfirm } from "@frontend-opensource/use-react-hooks"; +import { useForm, useFieldArray, type FieldArrayWithId } from "react-hook-form"; import { Dialog, DialogTrigger, @@ -15,162 +19,241 @@ import { Input, RadioGroup, RadioGroupItem, - Checkbox, DialogFooter, DialogClose, + Checkbox, } from "@common/components"; - -type IP = { - id: number; - ipAddress: string; - allowed: boolean; -}; +import { + Form, + FormControl, + FormField, + FormItem, + FormMessage, +} from "@/shared/ui/form"; +import { ENDPOINT, SITE_ID } from "@/shared/config"; +import { IpApi } from "../api/ip-service"; +import { ipFormSchema, type IP, type IPFormData } from "../model/ip-interface"; +import { ConfirmDialog } from "@/shared/ui/confirm-dialog"; +import { useAlertStore } from "@/shared/lib/use-alert-store"; type IPManagementDialogProps = { triggerDisabled?: boolean; + open?: boolean; + onOpenChange?: (open: boolean) => void; }; -function IPManagementDialog({ triggerDisabled }: IPManagementDialogProps) { - const [rows, setRows] = useState([]); - const [rowSelection, setRowSelection] = useState([]); +function IPManagementDialog({ + triggerDisabled, + ...props +}: IPManagementDialogProps) { + const { confirm } = useConfirm(); + const { setAlertMessage } = useAlertStore((state) => state); - const onSave = () => {}; + useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.IPS, props.open], + queryFn: () => IpApi.getIpList(), + select: (data) => { + const ips = data.content.map((item) => ({ + ...item, + mode: "U", + })); + replace(ips); + return data; + }, + }); + + const { mutateAsync } = useMutation({ + mutationFn: (body: IP[]) => IpApi.saveIpList(body), + }); + + const form = useForm({ + resolver: zodResolver(ipFormSchema), + }); + const { fields, append, remove, update, replace } = useFieldArray({ + control: form.control, + name: "ips", + }); + + const [isOpen, setIsOpen] = useState(false); + const [selectedRows, setSelectedRows] = useState([]); + + const onSubmit = async (data: IPFormData) => { + setIsOpen(true); + if (await confirm("저장하시겠습니까?")) { + setIsOpen(false); + try { + await mutateAsync(data.ips); + setAlertMessage("저장되었습니다."); + } catch (error) { + console.error(error); + setAlertMessage("저장에 실패했습니다."); + } + } + }; const handleAddIP = () => { - const newIP: IP = { - id: Date.now(), - ipAddress: "", - allowed: true, - }; - setRows([...rows, newIP]); - }; - - const handleDeleteIP = () => { - const filteredRows = rows.filter((row) => !rowSelection.includes(row.id)); - setRows(filteredRows); - setRowSelection([]); - }; - - const handleIPAddressChange = (id: number, value: string) => { - setRows( - rows.map((row) => (row.id === id ? { ...row, ipAddress: value } : row)), - ); - }; - - const handleAllowedChange = (id: number, value: boolean) => { - setRows( - rows.map((row) => (row.id === id ? { ...row, allowed: value } : row)), - ); - }; - - const toggleRowSelection = (id: number) => { - setRowSelection((prev) => { - if (prev.includes(id)) { - return prev.filter((item) => item !== id); - } else { - return [...prev, id]; - } + append({ + mngNo: null, + siteId: SITE_ID, + ipAddr: "", + prmYn: "Y", + mode: "C", }); }; + const handleSelectRow = (i: number) => { + setSelectedRows((prev) => + prev.includes(i) ? prev.filter((j) => j !== i) : [...prev, i], + ); + }; + + const handleDeleteIP = () => { + selectedRows.forEach((i) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { id, ...field } = fields[i] as FieldArrayWithId; + if (field.mngNo) { + update(i, { + ...field, + mode: "D", + }); + } else { + remove(i); + } + }); + setSelectedRows([]); + }; + return ( - - - - - - - IP 관리 - -
- - - -
-
- - - - - - - - - - NO - 선택 - IP 주소 - 허용여부 - - - - {rows.map((row, idx) => ( - - {idx + 1} - - toggleRowSelection(row.id)} - className="cursor-pointer" - /> - - - - handleIPAddressChange(row.id, e.target.value) - } - className="!text-[1.3rem]" - /> - - - - handleAllowedChange(row.id, value === "true") - } - className="flex justify-center" - > -
- - -
-
- - -
-
-
-
- ))} -
-
-
- - - - - - - - -
-
+ <> + + + + + + + IP 관리 + +
+ { + e.preventDefault(); + e.stopPropagation(); + form.handleSubmit(onSubmit)(e); + }} + > +
+ + +
+
+ + + + + + + + + 선택 + IP 주소 + 허용여부 + + + + {fields.map((field, i) => { + if (field.mode === "D") return null; + return ( + + + handleSelectRow(i)} + /> + + + ( + + + + + + + )} + /> + + + ( + + + +
+ + +
+
+ + +
+
+
+ +
+ )} + /> +
+
+ ); + })} +
+
+
+ + + + + + +
+ +
+
+ + ); } diff --git a/apps/cs/src/shared/config/api.ts b/apps/cs/src/shared/config/api.ts index bab8cac7b..b78018e65 100644 --- a/apps/cs/src/shared/config/api.ts +++ b/apps/cs/src/shared/config/api.ts @@ -11,6 +11,7 @@ export const ENDPOINT = { CMS_SERVICE: { SITES: "/cms-service/api/v1/sites", CODE: "/cms-service/api/v1/code/cache", + IPS: "/cms-service/api/v1/site-ips", }, };