From 6f0e1a6e40ce769433dc429fa25e44af5bc70fcd Mon Sep 17 00:00:00 2001 From: bicochan Date: Wed, 19 Mar 2025 15:54:20 +0900 Subject: [PATCH 01/11] =?UTF-8?q?feat:=20=EC=BB=A8=ED=8E=8C=20&=20?= =?UTF-8?q?=EC=96=BC=EB=9F=BF=20=EB=8B=A4=EC=9D=B4=EC=96=BC=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=20=EC=8A=A4=ED=86=A0=EC=96=B4=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cs/src/app/providers/app-provider.tsx | 6 ++- apps/cs/src/shared/lib/use-alert-store.ts | 14 ++++++ apps/cs/src/shared/lib/use-user-store.ts | 2 +- .../src/shared/providers/store-provider.tsx | 5 +- apps/cs/src/shared/store/alert-store.ts | 22 +++++++++ apps/cs/src/shared/ui/alert-dialog.tsx | 28 +++++++++++ apps/cs/src/shared/ui/confirm-dialog.tsx | 46 +++++++++++++++++++ 7 files changed, 120 insertions(+), 3 deletions(-) create mode 100644 apps/cs/src/shared/lib/use-alert-store.ts create mode 100644 apps/cs/src/shared/store/alert-store.ts create mode 100644 apps/cs/src/shared/ui/alert-dialog.tsx create mode 100644 apps/cs/src/shared/ui/confirm-dialog.tsx diff --git a/apps/cs/src/app/providers/app-provider.tsx b/apps/cs/src/app/providers/app-provider.tsx index cf477dcc0..caf3b98f7 100644 --- a/apps/cs/src/app/providers/app-provider.tsx +++ b/apps/cs/src/app/providers/app-provider.tsx @@ -3,13 +3,17 @@ import { ConfirmProvider } from "@frontend-opensource/use-react-hooks"; import { AuthWrapper } from "@/entities/auth"; import { QueryProvider, StoreProvider } from "@/shared/providers"; +import { AlertDialog } from "@/shared/ui/alert-dialog"; const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => { return ( - {children} + + {children} + + diff --git a/apps/cs/src/shared/lib/use-alert-store.ts b/apps/cs/src/shared/lib/use-alert-store.ts new file mode 100644 index 000000000..c21a00ed3 --- /dev/null +++ b/apps/cs/src/shared/lib/use-alert-store.ts @@ -0,0 +1,14 @@ +import { useStore } from "zustand"; +import { useContext } from "react"; +import type { AlertStore } from "../store/alert-store"; +import { StoreContext } from "../providers/store-provider"; + +export const useAlertStore = (selector: (store: AlertStore) => T): T => { + const storeContext = useContext(StoreContext); + + if (!storeContext) { + throw new Error("useAlertStore must be used within StoreProvider"); + } + + return useStore(storeContext.alertStore, selector); +}; diff --git a/apps/cs/src/shared/lib/use-user-store.ts b/apps/cs/src/shared/lib/use-user-store.ts index f4bf9763a..748caed4c 100644 --- a/apps/cs/src/shared/lib/use-user-store.ts +++ b/apps/cs/src/shared/lib/use-user-store.ts @@ -1,6 +1,6 @@ import { useStore } from "zustand"; import { useContext } from "react"; -import { type UserStore } from "@/shared/store/user-store"; +import type { UserStore } from "../store/user-store"; import { StoreContext } from "../providers/store-provider"; export const useUserStore = (selector: (store: UserStore) => T): T => { diff --git a/apps/cs/src/shared/providers/store-provider.tsx b/apps/cs/src/shared/providers/store-provider.tsx index ec913f235..4814705ab 100644 --- a/apps/cs/src/shared/providers/store-provider.tsx +++ b/apps/cs/src/shared/providers/store-provider.tsx @@ -1,9 +1,11 @@ "use client"; import { createContext, useRef } from "react"; -import { createUserStore } from "@/shared/store/user-store"; +import { createAlertStore } from "../store/alert-store"; +import { createUserStore } from "../store/user-store"; type StoreContextType = { + alertStore: ReturnType; userStore: ReturnType; }; @@ -15,6 +17,7 @@ export const StoreProvider = ({ children }: { children: React.ReactNode }) => { const storeRef = useRef(null); if (storeRef.current === null) { storeRef.current = { + alertStore: createAlertStore(), userStore: createUserStore(), }; } diff --git a/apps/cs/src/shared/store/alert-store.ts b/apps/cs/src/shared/store/alert-store.ts new file mode 100644 index 000000000..a6e22d441 --- /dev/null +++ b/apps/cs/src/shared/store/alert-store.ts @@ -0,0 +1,22 @@ +import { createStore } from "zustand/vanilla"; + +export type AlertState = { + alertMessage: string; +}; + +export type AlertActions = { + setAlertMessage: (message: string) => void; +}; + +export type AlertStore = AlertState & AlertActions; + +export const defaultInitState: AlertState = { + alertMessage: "", +}; + +export const createAlertStore = (initState: AlertState = defaultInitState) => { + return createStore()((set) => ({ + ...initState, + setAlertMessage: (alertMessage: string) => set(() => ({ alertMessage })), + })); +}; diff --git a/apps/cs/src/shared/ui/alert-dialog.tsx b/apps/cs/src/shared/ui/alert-dialog.tsx new file mode 100644 index 000000000..68805ce2f --- /dev/null +++ b/apps/cs/src/shared/ui/alert-dialog.tsx @@ -0,0 +1,28 @@ +import { + Dialog, + DialogContent, + DialogFooter, + DialogClose, + Button, +} from "@common/components/ui"; +import { useAlertStore } from "../lib/use-alert-store"; + +function AlertDialog() { + const { alertMessage, setAlertMessage } = useAlertStore((state) => state); + return ( + setAlertMessage("")}> + +
+

{alertMessage}

+
+ + + + + +
+
+ ); +} + +export { AlertDialog }; diff --git a/apps/cs/src/shared/ui/confirm-dialog.tsx b/apps/cs/src/shared/ui/confirm-dialog.tsx new file mode 100644 index 000000000..3e1e90f9d --- /dev/null +++ b/apps/cs/src/shared/ui/confirm-dialog.tsx @@ -0,0 +1,46 @@ +import { useConfirm } from "@frontend-opensource/use-react-hooks"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogClose, + Button, + type ButtonProps, +} from "@common/components/ui"; + +type ConfirmDialogProps = { + open: boolean; + confirmButtonProps?: ButtonProps & { label?: string }; + cancelButtonProps?: ButtonProps & { label?: string }; +}; + +function ConfirmDialog({ + open, + confirmButtonProps, + cancelButtonProps, +}: ConfirmDialogProps) { + const { message, onConfirm, onCancel } = useConfirm(); + return ( + + +
+

{message}

+
+ + + + + + + + +
+
+ ); +} + +export { ConfirmDialog }; From 915543628db421b62dc5522c1ec9608119564cd3 Mon Sep 17 00:00:00 2001 From: bicochan Date: Wed, 19 Mar 2025 16:23:33 +0900 Subject: [PATCH 02/11] =?UTF-8?q?feat:=20ip=20=EA=B4=80=EB=A6=AC=20?= =?UTF-8?q?=EC=84=9C=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", }, }; From e49e3d3b36e0766d21730d1308dff128fb1e60dd Mon Sep 17 00:00:00 2001 From: bicochan Date: Wed, 19 Mar 2025 18:43:58 +0900 Subject: [PATCH 03/11] =?UTF-8?q?feat:=20useAlertStore=20=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cs/src/pages/site/ui/create-page.tsx | 26 +------ .../pages/site/ui/ip-management-dialog.tsx | 13 ++-- apps/cs/src/pages/site/ui/modify-page.tsx | 28 ++------ apps/cs/src/pages/site/ui/site-form.tsx | 67 +++++++------------ apps/cs/src/shared/store/alert-store.ts | 11 +-- apps/cs/src/shared/ui/alert-dialog.tsx | 10 ++- 6 files changed, 50 insertions(+), 105 deletions(-) diff --git a/apps/cs/src/pages/site/ui/create-page.tsx b/apps/cs/src/pages/site/ui/create-page.tsx index a79c16022..5170b85a9 100644 --- a/apps/cs/src/pages/site/ui/create-page.tsx +++ b/apps/cs/src/pages/site/ui/create-page.tsx @@ -1,6 +1,5 @@ "use client"; -import { useState } from "react"; import { useRouter } from "next/navigation"; import { useForm } from "react-hook-form"; import { useMutation } from "@tanstack/react-query"; @@ -9,25 +8,18 @@ import { SiteApi } from "../api/site-service"; import { siteFormSchema, type SiteFormData } from "../model/site-interface"; import { SitePageLayout } from "./layout"; import { SiteForm } from "./site-form"; -import { - Button, - Dialog, - DialogClose, - DialogContent, - DialogFooter, -} from "@common/components/ui"; +import { useAlertStore } from "@/shared/lib/use-alert-store"; function CreatePage() { const router = useRouter(); + const { setMessage: alert } = useAlertStore((state) => state); const { mutateAsync: createSite } = useMutation({ mutationFn: (data: SiteFormData) => SiteApi.createSite(data), onSuccess: () => { - setIsCreateSuccess(true); + alert("등록되었습니다.", router.back); }, }); - const [isCreateSuccess, setIsCreateSuccess] = useState(false); - const form = useForm({ resolver: zodResolver(siteFormSchema), }); @@ -35,18 +27,6 @@ function CreatePage() { return ( - router.back()}> - -
-

등록되었습니다.

-
- - - - - -
-
); } 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 35166d0de..c62d136b2 100644 --- a/apps/cs/src/pages/site/ui/ip-management-dialog.tsx +++ b/apps/cs/src/pages/site/ui/ip-management-dialog.tsx @@ -47,7 +47,7 @@ function IPManagementDialog({ ...props }: IPManagementDialogProps) { const { confirm } = useConfirm(); - const { setAlertMessage } = useAlertStore((state) => state); + const { setMessage: alert } = useAlertStore((state) => state); useQuery({ queryKey: [ENDPOINT.CMS_SERVICE.IPS, props.open], @@ -74,20 +74,15 @@ function IPManagementDialog({ name: "ips", }); - const [isOpen, setIsOpen] = useState(false); + 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("저장에 실패했습니다."); - } + await mutateAsync(data.ips); + alert("저장되었습니다."); } }; diff --git a/apps/cs/src/pages/site/ui/modify-page.tsx b/apps/cs/src/pages/site/ui/modify-page.tsx index 8c3bbd093..ebd8f36c5 100644 --- a/apps/cs/src/pages/site/ui/modify-page.tsx +++ b/apps/cs/src/pages/site/ui/modify-page.tsx @@ -1,11 +1,12 @@ "use client"; -import { useEffect, useState } from "react"; +import { useEffect } from "react"; import { useRouter } from "next/navigation"; import { useForm } from "react-hook-form"; import { useMutation, useQuery } from "@tanstack/react-query"; import { zodResolver } from "@hookform/resolvers/zod"; import { ENDPOINT } from "@/shared/config"; +import { useAlertStore } from "@/shared/lib/use-alert-store"; import { SiteApi } from "../api/site-service"; import { siteFormSchema, @@ -14,18 +15,12 @@ import { } from "../model/site-interface"; import { SitePageLayout } from "./layout"; import { SiteForm } from "./site-form"; -import { - Button, - Dialog, - DialogClose, - DialogContent, - DialogFooter, -} from "@common/components/ui"; function ModifyPage({ siteId }: { siteId: string }) { const isModifyMode = !!siteId; const router = useRouter(); + const { setMessage: alert } = useAlertStore((state) => state); const { data: siteDetail } = useQuery({ queryKey: [ENDPOINT.CMS_SERVICE.SITES, siteId], queryFn: () => SiteApi.siteDetail(siteId), @@ -40,17 +35,16 @@ function ModifyPage({ siteId }: { siteId: string }) { const { mutateAsync: modifySite } = useMutation({ mutationFn: (data: SiteFormData) => SiteApi.modifySite(siteId, data), onSuccess: () => { - setMessage("수정되었습니다."); + alert("수정되었습니다.", router.back); }, }); const { mutateAsync: deleteSite } = useMutation({ mutationFn: () => SiteApi.deleteSite(siteId), onSuccess: () => { - setMessage("삭제되었습니다."); + alert("삭제되었습니다.", router.back); }, }); - const [message, setMessage] = useState(""); const form = useForm({ resolver: zodResolver(siteFormSchema), @@ -76,18 +70,6 @@ function ModifyPage({ siteId }: { siteId: string }) { handleSave={modifySite} handleDelete={deleteSite} /> - router.back()}> - -
-

{message}

-
- - - - - -
-
); } diff --git a/apps/cs/src/pages/site/ui/site-form.tsx b/apps/cs/src/pages/site/ui/site-form.tsx index c554a868c..b1f8c3eed 100644 --- a/apps/cs/src/pages/site/ui/site-form.tsx +++ b/apps/cs/src/pages/site/ui/site-form.tsx @@ -4,10 +4,6 @@ import { useConfirm } from "@frontend-opensource/use-react-hooks"; import { type UseFormReturn } from "react-hook-form"; import { Button, - Dialog, - DialogClose, - DialogContent, - DialogFooter, Input, Label, RadioGroup, @@ -26,7 +22,8 @@ import { Form, FormMessage, } from "@/shared/ui/form"; -import { type ISiteDetail, type SiteFormData } from "../model/site-interface"; +import { ConfirmDialog } from "@/shared/ui/confirm-dialog"; +import { type SiteFormData } from "../model/site-interface"; import { IPManagementDialog } from "./ip-management-dialog"; import { DomainManagementDialog, @@ -36,22 +33,17 @@ import { type SiteFormProps = { form: UseFormReturn; siteId?: string; - handleSave: (data: SiteFormData) => void; - handleDelete?: () => void; + handleSave: (data: SiteFormData) => Promise; + handleDelete?: () => Promise; }; function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) { - const router = useRouter(); - const { message, confirm, onConfirm, onCancel } = useConfirm(); - - const [viewState, setViewState] = useState>({}); - const [representativeDomain, setRepresentativeDomain] = useState(""); - const isModifyMode = !!siteId; - const handleViewStateChange = (key: keyof ISiteDetail) => (value: string) => { - setViewState({ ...viewState, [key]: value }); - }; + const router = useRouter(); + const { confirm } = useConfirm(); + const [representativeDomain, setRepresentativeDomain] = useState(""); + const [isConfirmOpen, setIsConfirmOpen] = useState(false); const handleDomainSave = (rows: Domain[]) => { setRepresentativeDomain( @@ -59,12 +51,22 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) { ); }; - const onSubmit = (data: SiteFormData) => { - handleSave(data); + const onSubmit = async (data: SiteFormData) => { + const confirmMessage = isModifyMode + ? "수정하시겠습니까?" + : "저장하시겠습니까?"; + + setIsConfirmOpen(true); + if (await confirm(confirmMessage)) { + setIsConfirmOpen(false); + handleSave(data); + } }; const onDelete = async () => { - if (await confirm("정말 삭제하시겠습니까?")) { + setIsConfirmOpen(true); + if (await confirm("삭제하시겠습니까?")) { + setIsConfirmOpen(false); handleDelete?.(); } }; @@ -120,10 +122,7 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) { { - handleViewStateChange("siteKndCd")(value); - field.onChange(value); - }} + onValueChange={field.onChange} /> @@ -132,7 +131,7 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) { /> {isModifyMode && ( )} @@ -405,25 +404,7 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) { - - -
-

{message}

-
- - - - - - - - -
-
+ ); } diff --git a/apps/cs/src/shared/store/alert-store.ts b/apps/cs/src/shared/store/alert-store.ts index a6e22d441..3bba65848 100644 --- a/apps/cs/src/shared/store/alert-store.ts +++ b/apps/cs/src/shared/store/alert-store.ts @@ -1,22 +1,25 @@ import { createStore } from "zustand/vanilla"; export type AlertState = { - alertMessage: string; + message: string; + callback?: () => void; }; export type AlertActions = { - setAlertMessage: (message: string) => void; + setMessage: (message: string, callback?: () => void) => void; }; export type AlertStore = AlertState & AlertActions; export const defaultInitState: AlertState = { - alertMessage: "", + message: "", + callback: undefined, }; export const createAlertStore = (initState: AlertState = defaultInitState) => { return createStore()((set) => ({ ...initState, - setAlertMessage: (alertMessage: string) => set(() => ({ alertMessage })), + setMessage: (message: string, callback?: () => void) => + set(() => ({ message, callback })), })); }; diff --git a/apps/cs/src/shared/ui/alert-dialog.tsx b/apps/cs/src/shared/ui/alert-dialog.tsx index 68805ce2f..f7907e0e4 100644 --- a/apps/cs/src/shared/ui/alert-dialog.tsx +++ b/apps/cs/src/shared/ui/alert-dialog.tsx @@ -8,12 +8,16 @@ import { import { useAlertStore } from "../lib/use-alert-store"; function AlertDialog() { - const { alertMessage, setAlertMessage } = useAlertStore((state) => state); + const { message, callback, setMessage } = useAlertStore((state) => state); + const handleOpenChange = () => { + setMessage(""); + callback?.(); + }; return ( - setAlertMessage("")}> +
-

{alertMessage}

+

{message}

From b2141d97082b6b00839d1a8478558656b23d7961 Mon Sep 17 00:00:00 2001 From: bicochan Date: Thu, 20 Mar 2025 10:43:19 +0900 Subject: [PATCH 04/11] =?UTF-8?q?refactor:=20ip=20=EA=B4=80=EB=A6=AC=20?= =?UTF-8?q?=ED=8F=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/site/ui/ip-management-dialog.tsx | 10 +++--- apps/cs/src/pages/site/ui/modify-page.tsx | 36 +++++++------------ apps/cs/src/pages/site/ui/site-form.tsx | 15 ++++---- 3 files changed, 28 insertions(+), 33 deletions(-) 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 c62d136b2..cbf1625ad 100644 --- a/apps/cs/src/pages/site/ui/ip-management-dialog.tsx +++ b/apps/cs/src/pages/site/ui/ip-management-dialog.tsx @@ -57,6 +57,8 @@ function IPManagementDialog({ ...item, mode: "U", })); + + form.reset(); replace(ips); return data; }, @@ -74,13 +76,13 @@ function IPManagementDialog({ name: "ips", }); - const [isOpen, setIsOpen] = useState(false); + const [onConfirm, setOnConfirm] = useState(false); const [selectedRows, setSelectedRows] = useState([]); const onSubmit = async (data: IPFormData) => { - setIsOpen(true); + setOnConfirm(true); if (await confirm("저장하시겠습니까?")) { - setIsOpen(false); + setOnConfirm(false); await mutateAsync(data.ips); alert("저장되었습니다."); } @@ -247,7 +249,7 @@ function IPManagementDialog({
- + ); } diff --git a/apps/cs/src/pages/site/ui/modify-page.tsx b/apps/cs/src/pages/site/ui/modify-page.tsx index ebd8f36c5..90ab1ccc0 100644 --- a/apps/cs/src/pages/site/ui/modify-page.tsx +++ b/apps/cs/src/pages/site/ui/modify-page.tsx @@ -1,6 +1,5 @@ "use client"; -import { useEffect } from "react"; import { useRouter } from "next/navigation"; import { useForm } from "react-hook-form"; import { useMutation, useQuery } from "@tanstack/react-query"; @@ -8,11 +7,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { ENDPOINT } from "@/shared/config"; import { useAlertStore } from "@/shared/lib/use-alert-store"; import { SiteApi } from "../api/site-service"; -import { - siteFormSchema, - type ISiteDetail, - type SiteFormData, -} from "../model/site-interface"; +import { siteFormSchema, type SiteFormData } from "../model/site-interface"; import { SitePageLayout } from "./layout"; import { SiteForm } from "./site-form"; @@ -24,11 +19,17 @@ function ModifyPage({ siteId }: { siteId: string }) { const { data: siteDetail } = useQuery({ queryKey: [ENDPOINT.CMS_SERVICE.SITES, siteId], queryFn: () => SiteApi.siteDetail(siteId), - select: (data) => ({ - ...data, - faxNo: data.faxNumber, - bscSiteYn: data.basicSiteYn, - }), + select: (data) => { + const keys = Object.keys(siteFormSchema.shape); + const filteredData = Object.fromEntries( + Object.entries(data).filter(([key]) => keys.includes(key)), + ); + return { + ...filteredData, + faxNo: filteredData.faxNumber, + bscSiteYn: filteredData.basicSiteYn, + }; + }, enabled: isModifyMode, }); @@ -48,20 +49,9 @@ function ModifyPage({ siteId }: { siteId: string }) { const form = useForm({ resolver: zodResolver(siteFormSchema), + values: siteDetail as SiteFormData, }); - useEffect(() => { - if (!siteDetail) return; - - const keys = Object.keys(siteFormSchema.shape); - for (const key of keys) { - form.setValue( - key as keyof SiteFormData, - siteDetail[key as keyof ISiteDetail] ?? "", - ); - } - }, [siteDetail]); - return ( { setRepresentativeDomain( @@ -56,17 +57,17 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) { ? "수정하시겠습니까?" : "저장하시겠습니까?"; - setIsConfirmOpen(true); + setOnConfirm(true); if (await confirm(confirmMessage)) { - setIsConfirmOpen(false); + setOnConfirm(false); handleSave(data); } }; const onDelete = async () => { - setIsConfirmOpen(true); + setOnConfirm(true); if (await confirm("삭제하시겠습니까?")) { - setIsConfirmOpen(false); + setOnConfirm(false); handleDelete?.(); } }; @@ -132,6 +133,8 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) { {isModifyMode && ( )} @@ -404,7 +407,7 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) { - + ); } From dc01c897a907797b54c70dac8671214b00e6330c Mon Sep 17 00:00:00 2001 From: bicochan Date: Thu, 20 Mar 2025 10:48:54 +0900 Subject: [PATCH 05/11] =?UTF-8?q?chore:=20=EC=82=AC=EC=9D=B4=ED=8A=B8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=ED=8F=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cs/src/pages/site/ui/modify-page.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/cs/src/pages/site/ui/modify-page.tsx b/apps/cs/src/pages/site/ui/modify-page.tsx index 90ab1ccc0..d73d79ef6 100644 --- a/apps/cs/src/pages/site/ui/modify-page.tsx +++ b/apps/cs/src/pages/site/ui/modify-page.tsx @@ -26,8 +26,8 @@ function ModifyPage({ siteId }: { siteId: string }) { ); return { ...filteredData, - faxNo: filteredData.faxNumber, - bscSiteYn: filteredData.basicSiteYn, + faxNo: data.faxNumber, + bscSiteYn: data.basicSiteYn, }; }, enabled: isModifyMode, @@ -36,7 +36,7 @@ function ModifyPage({ siteId }: { siteId: string }) { const { mutateAsync: modifySite } = useMutation({ mutationFn: (data: SiteFormData) => SiteApi.modifySite(siteId, data), onSuccess: () => { - alert("수정되었습니다.", router.back); + alert("수정되었습니다."); }, }); From fadb57242d142c36b39074f5e2e8bd8a9aa10fb8 Mon Sep 17 00:00:00 2001 From: bicochan Date: Thu, 20 Mar 2025 17:36:16 +0900 Subject: [PATCH 06/11] =?UTF-8?q?feat:=20=EB=8F=84=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=20=EB=B0=8F=20IP=20=EA=B4=80=EB=A6=AC=20?= =?UTF-8?q?=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cs/middleware.ts | 7 +- apps/cs/src/pages/site/api/domain-service.ts | 28 ++ apps/cs/src/pages/site/api/ip-service.ts | 8 +- apps/cs/src/pages/site/api/site-service.ts | 6 +- .../src/pages/site/model/domain-interface.ts | 31 ++ apps/cs/src/pages/site/model/ip-interface.ts | 39 +- .../cs/src/pages/site/model/site-interface.ts | 58 +-- .../site/ui/domain-management-dialog.tsx | 459 +++++++++++------- .../pages/site/ui/ip-management-dialog.tsx | 58 ++- apps/cs/src/pages/site/ui/site-form.tsx | 32 +- apps/cs/src/pages/site/ui/site-page.tsx | 17 +- apps/cs/src/shared/config/api.ts | 1 + apps/cs/src/shared/model/api-interface.ts | 34 ++ apps/cs/src/shared/model/index.ts | 1 + 14 files changed, 484 insertions(+), 295 deletions(-) create mode 100644 apps/cs/src/pages/site/api/domain-service.ts create mode 100644 apps/cs/src/pages/site/model/domain-interface.ts create mode 100644 apps/cs/src/shared/model/api-interface.ts create mode 100644 apps/cs/src/shared/model/index.ts diff --git a/apps/cs/middleware.ts b/apps/cs/middleware.ts index e7b03dddc..4d6d816a7 100644 --- a/apps/cs/middleware.ts +++ b/apps/cs/middleware.ts @@ -5,10 +5,13 @@ const serverApiUrl = export function middleware(req: NextRequest) { const pathname = req.nextUrl.pathname; + const searchParams = req.nextUrl.searchParams; + if (pathname.startsWith("/server")) { - return NextResponse.rewrite( - `${serverApiUrl}${pathname.replace("/server", "")}`, + const newUrl = new URL( + `${serverApiUrl}${pathname.replace("/server", "")}?${searchParams.toString() ?? ""}`, ); + return NextResponse.rewrite(newUrl); } return NextResponse.next(); diff --git a/apps/cs/src/pages/site/api/domain-service.ts b/apps/cs/src/pages/site/api/domain-service.ts new file mode 100644 index 000000000..7cc36363f --- /dev/null +++ b/apps/cs/src/pages/site/api/domain-service.ts @@ -0,0 +1,28 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; +import type { + Domain, + DomainService, + DomainList, + IDomainParams, +} from "../model/domain-interface"; + +export class DomainApiService implements DomainService { + public async getDomainList(params: IDomainParams): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.DOMAINS}`, + { params }, + ); + return response.data; + } + + public async saveDomainList(body: Domain[]): Promise { + const response = await client.post( + `${ENDPOINT.CMS_SERVICE.DOMAINS}`, + body, + ); + return response.data; + } +} + +export const DomainApi = new DomainApiService(); diff --git a/apps/cs/src/pages/site/api/ip-service.ts b/apps/cs/src/pages/site/api/ip-service.ts index 2bfad0934..748676b21 100644 --- a/apps/cs/src/pages/site/api/ip-service.ts +++ b/apps/cs/src/pages/site/api/ip-service.ts @@ -1,10 +1,12 @@ import { client } from "@/shared/api"; import { ENDPOINT } from "@/shared/config"; -import type { IP, IpList, IpService } from "../model/ip-interface"; +import type { IIpParams, 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}`); + public async getIpList(params: IIpParams): Promise { + const response = await client.get(`${ENDPOINT.CMS_SERVICE.IPS}`, { + params, + }); return response.data; } diff --git a/apps/cs/src/pages/site/api/site-service.ts b/apps/cs/src/pages/site/api/site-service.ts index 22363de68..629bf5f01 100644 --- a/apps/cs/src/pages/site/api/site-service.ts +++ b/apps/cs/src/pages/site/api/site-service.ts @@ -2,16 +2,16 @@ import { client } from "@/shared/api"; import { ENDPOINT } from "@/shared/config"; import { siteFormSchema, - type ISiteInfo, type SiteService, + type SiteList, type ISiteDetail, type ISiteParams, type SiteFormData, } from "../model/site-interface"; export class SiteApiService implements SiteService { - public async siteInfo(params: ISiteParams): Promise { - const response = await client.get( + public async getSiteList(params: ISiteParams): Promise { + const response = await client.get( `${ENDPOINT.CMS_SERVICE.SITES}`, { params }, ); diff --git a/apps/cs/src/pages/site/model/domain-interface.ts b/apps/cs/src/pages/site/model/domain-interface.ts new file mode 100644 index 000000000..5f1076275 --- /dev/null +++ b/apps/cs/src/pages/site/model/domain-interface.ts @@ -0,0 +1,31 @@ +import { z } from "zod"; +import type { IPageableParams, PageableList } from "@/shared/model"; + +export interface IDomainParams extends IPageableParams { + keywordType?: string; + keyword?: string; + siteId?: string; + useYn?: string; +} + +export const domainSchema = z.object({ + siteDmnNo: z.number().nullish(), + siteId: z.string(), + dmnAddr: z.string().nonempty(), + useYn: z.string(), + rprsDmnYn: z.string(), + mode: z.string(), +}); + +export const domainFormSchema = z.object({ + domains: z.array(domainSchema), +}); + +export type Domain = z.infer; +export type DomainFormData = z.infer; +export type DomainList = PageableList; + +export interface DomainService { + getDomainList(params: IDomainParams): Promise; + saveDomainList(body: Domain[]): Promise; +} diff --git a/apps/cs/src/pages/site/model/ip-interface.ts b/apps/cs/src/pages/site/model/ip-interface.ts index f8cf9d5ea..db1d393f1 100644 --- a/apps/cs/src/pages/site/model/ip-interface.ts +++ b/apps/cs/src/pages/site/model/ip-interface.ts @@ -1,4 +1,11 @@ import { z } from "zod"; +import type { IPageableParams, PageableList } from "@/shared/model"; + +export interface IIpParams extends IPageableParams { + siteId: string; + ipAddr?: string; + prmYn?: string; +} export const ipSchema = z.object({ mngNo: z.number().nullish(), @@ -14,37 +21,9 @@ export const ipFormSchema = z.object({ 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 type IpList = PageableList; export interface IpService { - getIpList(): Promise; + getIpList(params: IIpParams): Promise; saveIpList(body: IP[]): Promise; } diff --git a/apps/cs/src/pages/site/model/site-interface.ts b/apps/cs/src/pages/site/model/site-interface.ts index 4d03e2c8b..6edc1e69e 100644 --- a/apps/cs/src/pages/site/model/site-interface.ts +++ b/apps/cs/src/pages/site/model/site-interface.ts @@ -1,12 +1,10 @@ import { z } from "zod"; +import type { IPageableParams, PageableList } from "@/shared/model"; -export interface ISiteParams { - siteNm: string; - siteKndCd: string; - useYn: string; - page: number; - size: number; - sort: string[]; +export interface ISiteParams extends IPageableParams { + siteNm?: string; + siteKndCd?: string; + useYn?: string; } export interface ISite { @@ -20,35 +18,6 @@ export interface ISite { rgTrNm: string; } -export interface ISiteInfo { - totalPages: number; - totalElements: number; - number: number; - sort: { - empty: boolean; - sorted: boolean; - unsorted: boolean; - }; - size: number; - content: ISite[]; - pageable: { - sort: { - empty: boolean; - sorted: boolean; - unsorted: boolean; - }; - offset: number; - pageNumber: number; - pageSize: number; - paged: boolean; - unpaged: boolean; - }; - numberOfElements: number; - first: boolean; - last: boolean; - empty: boolean; -} - export interface ISiteDetail extends SiteFormData { siteId: string; siteKindNm: string; @@ -65,22 +34,23 @@ export interface ISiteDetail extends SiteFormData { export const siteFormSchema = z.object({ siteId: z.string().max(2).nonempty(), siteNm: z.string().max(100).nonempty(), - siteExpln: z.string().max(1000).optional(), + siteExpln: z.string().max(1000).nullable(), siteKndCd: z.string().max(10).nonempty(), - siteAddr: z.string().max(2000).optional(), - faxNo: z.string().max(100).optional(), - telNo: z.string().max(1000).optional(), - lwndCn: z.string().max(2000).optional(), + siteAddr: z.string().max(2000).nullable(), + faxNo: z.string().max(100).nullable(), + telNo: z.string().max(1000).nullable(), + lwndCn: z.string().max(2000).nullable(), bscSiteYn: z.string(), - bkmkIcon: z.string().max(100).optional(), + bkmkIcon: z.string().max(100).nullable(), useYn: z.string(), - siteSkn: z.string().max(10).optional(), + siteSkn: z.string().max(10).nullable(), }); export type SiteFormData = z.infer; +export type SiteList = PageableList; export interface SiteService { - siteInfo(query: ISiteParams): Promise; + getSiteList(params: ISiteParams): Promise; siteDetail(siteId: string): Promise; modifySite(siteId: string, data: SiteFormData): Promise; createSite(data: SiteFormData): Promise; diff --git a/apps/cs/src/pages/site/ui/domain-management-dialog.tsx b/apps/cs/src/pages/site/ui/domain-management-dialog.tsx index c675f92d9..4a7e854ee 100644 --- a/apps/cs/src/pages/site/ui/domain-management-dialog.tsx +++ b/apps/cs/src/pages/site/ui/domain-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 } from "react-hook-form"; import { Dialog, DialogTrigger, @@ -13,197 +17,304 @@ import { TableHeader, TableRow, Input, - Checkbox, - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, + RadioGroup, + RadioGroupItem, DialogFooter, DialogClose, + Checkbox, } from "@common/components"; - -export type Domain = { - id: number; - domainUrl: string; - isRepresentative: boolean; - isActive: "true" | "false"; -}; +import { + Form, + FormControl, + FormField, + FormItem, + FormMessage, +} from "@/shared/ui/form"; +import { ENDPOINT } from "@/shared/config"; +import { DomainApi } from "../api/domain-service"; +import { + domainFormSchema, + type Domain, + type DomainFormData, +} from "../model/domain-interface"; +import { ConfirmDialog } from "@/shared/ui/confirm-dialog"; +import { useAlertStore } from "@/shared/lib/use-alert-store"; type DomainManagementDialogProps = { - triggerDisabled?: boolean; - handleSave: (rows: Domain[]) => void; + siteId: string; + open?: boolean; + onOpenChange?: (open: boolean) => void; + handleSaveDomain?: (rows: Domain[]) => void; }; function DomainManagementDialog({ - triggerDisabled, - handleSave, + siteId, + handleSaveDomain, + ...props }: DomainManagementDialogProps) { - const [rows, setRows] = useState([]); - const [rowSelection, setRowSelection] = useState([]); + const { confirm } = useConfirm(); + const { setMessage: alert } = useAlertStore((state) => state); - const onSave = () => { - handleSave(rows); + const { data: domainList } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.DOMAINS, props.open], + queryFn: () => DomainApi.getDomainList({ siteId }), + select: (data) => { + const domains = data.content.map((item) => ({ + ...item, + mode: "U", + })); + + initialForm(); + return domains; + }, + }); + + const { mutateAsync } = useMutation({ + mutationFn: (body: Domain[]) => DomainApi.saveDomainList(body), + }); + + const form = useForm({ + resolver: zodResolver(domainFormSchema), + values: { + domains: domainList ?? [], + }, + }); + const { fields, append, remove, update } = useFieldArray({ + control: form.control, + name: "domains", + }); + + const [onConfirm, setOnConfirm] = useState(false); + const [selectedRows, setSelectedRows] = useState([]); + + const initialForm = () => { + form.reset(); + setSelectedRows([]); + }; + + const onSubmit = async (data: DomainFormData) => { + setOnConfirm(true); + if (await confirm("저장하시겠습니까?")) { + setOnConfirm(false); + await mutateAsync(data.domains); + handleSaveDomain?.(data.domains); + alert("저장되었습니다."); + } + setOnConfirm(false); }; const handleAddDomain = () => { - const newDomain: Domain = { - id: Date.now(), - domainUrl: "", - isRepresentative: rows.length === 0, - isActive: "true", - }; - setRows([...rows, newDomain]); - }; - - const handleDeleteDomain = () => { - const filteredRows = rows.filter((row) => !rowSelection.includes(row.id)); - const hasRepresentative = filteredRows.some((row) => row.isRepresentative); - - let updatedRows = filteredRows; - if (!hasRepresentative && filteredRows.length > 0) { - updatedRows = filteredRows.map((row, index) => - index === 0 ? { ...row, isRepresentative: true } : row, - ); - } - - setRows(updatedRows); - setRowSelection([]); - }; - - const handleDomainUrlChange = (id: number, value: string) => { - setRows( - rows.map((row) => (row.id === id ? { ...row, domainUrl: value } : row)), - ); - }; - - const handleRepresentativeChange = (id: number) => { - setRows( - rows.map((row) => ({ - ...row, - isRepresentative: row.id === id, - })), - ); - }; - - const handleActiveChange = (id: number, value: "true" | "false") => { - setRows( - rows.map((row) => (row.id === id ? { ...row, isActive: value } : row)), - ); - }; - - const toggleRowSelection = (id: number) => { - setRowSelection((prev) => { - if (prev.includes(id)) { - return prev.filter((item) => item !== id); - } else { - return [...prev, id]; - } + append({ + siteDmnNo: null, + siteId, + dmnAddr: "", + useYn: "Y", + rprsDmnYn: "N", + mode: "C", }); }; + const handleSelectAll = () => { + if (selectedRows.length === fields.length) { + setSelectedRows([]); + } else { + setSelectedRows(fields.map((_, index) => index)); + } + }; + + const handleSelectRow = (i: number) => { + setSelectedRows((prev) => + prev.includes(i) ? prev.filter((j) => j !== i) : [...prev, i], + ); + }; + + const handleDeleteDomain = async () => { + if (selectedRows.length === 0) return; + + const sortedIndices = [...selectedRows].sort((a, b) => b - a); + sortedIndices.forEach((i) => { + const field = fields[i]; + if (field?.siteDmnNo) { + update(i, { + ...field, + mode: "D", + }); + } else { + remove(i); + } + }); + + setSelectedRows([]); + }; + + const handleChangeRprsDmnYn = (i: number) => { + fields.forEach((_, j) => { + if (j === i) return; + form.setValue(`domains.${j}.rprsDmnYn`, "N"); + }); + }; + + const isAllSelected = + fields.length > 0 && selectedRows.length === fields.length; + return ( - - - - - - - 도메인 관리 - -
- - -
-
- - - - - - - - - - - NO - 선택 - 도메인URL - 대표URL - 사용여부 - - - - {rows.map((row, idx) => ( - - {idx + 1} - - toggleRowSelection(row.id)} - className="cursor-pointer" - /> - - - - handleDomainUrlChange(row.id, e.target.value) - } - className="!text-[1.3rem]" - /> - - - handleRepresentativeChange(row.id)} - className="cursor-pointer" - /> - - - - - - ))} - -
-
- - - - - - - - -
-
+ <> + + + + + + + 도메인 관리 + +
+ { + e.preventDefault(); + e.stopPropagation(); + form.handleSubmit(onSubmit)(e); + }} + > +
+ + +
+
+ + + + + + + + + + + + + 도메인 주소 + 대표 URL + 사용여부 + + + + {fields.map((field, i) => { + if (field.mode === "D") return null; + return ( + + + handleSelectRow(i)} + /> + + + ( + + + + + + + )} + /> + + + ( + + + { + field.onChange(checked ? "Y" : "N"); + handleChangeRprsDmnYn(i); + }} + /> + + + + )} + /> + + + ( + + + +
+ + +
+
+ + +
+
+
+ +
+ )} + /> +
+
+ ); + })} +
+
+
+ + + + + + +
+ +
+
+ + ); } 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 cbf1625ad..97721a6d6 100644 --- a/apps/cs/src/pages/site/ui/ip-management-dialog.tsx +++ b/apps/cs/src/pages/site/ui/ip-management-dialog.tsx @@ -2,7 +2,7 @@ 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 { useForm, useFieldArray } from "react-hook-form"; import { Dialog, DialogTrigger, @@ -30,37 +30,38 @@ import { FormItem, FormMessage, } from "@/shared/ui/form"; -import { ENDPOINT, SITE_ID } from "@/shared/config"; +import { ENDPOINT } 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 = { + siteId: string; triggerDisabled?: boolean; open?: boolean; onOpenChange?: (open: boolean) => void; }; function IPManagementDialog({ + siteId, triggerDisabled, ...props }: IPManagementDialogProps) { const { confirm } = useConfirm(); const { setMessage: alert } = useAlertStore((state) => state); - useQuery({ + const { data: ipList } = useQuery({ queryKey: [ENDPOINT.CMS_SERVICE.IPS, props.open], - queryFn: () => IpApi.getIpList(), + queryFn: () => IpApi.getIpList({ siteId, sort: [] }), select: (data) => { const ips = data.content.map((item) => ({ ...item, mode: "U", })); - form.reset(); - replace(ips); - return data; + initialForm(); + return ips; }, }); @@ -70,8 +71,11 @@ function IPManagementDialog({ const form = useForm({ resolver: zodResolver(ipFormSchema), + values: { + ips: ipList ?? [], + }, }); - const { fields, append, remove, update, replace } = useFieldArray({ + const { fields, append, remove, update } = useFieldArray({ control: form.control, name: "ips", }); @@ -79,6 +83,11 @@ function IPManagementDialog({ const [onConfirm, setOnConfirm] = useState(false); const [selectedRows, setSelectedRows] = useState([]); + const initialForm = () => { + form.reset(); + setSelectedRows([]); + }; + const onSubmit = async (data: IPFormData) => { setOnConfirm(true); if (await confirm("저장하시겠습니까?")) { @@ -91,24 +100,34 @@ function IPManagementDialog({ const handleAddIP = () => { append({ mngNo: null, - siteId: SITE_ID, + siteId, ipAddr: "", prmYn: "Y", mode: "C", }); }; + const handleSelectAll = () => { + if (selectedRows.length === fields.length) { + setSelectedRows([]); + } else { + setSelectedRows(fields.map((_, index) => index)); + } + }; + 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) { + const handleDeleteIP = async () => { + if (selectedRows.length === 0) return; + + const sortedIndices = [...selectedRows].sort((a, b) => b - a); + sortedIndices.forEach((i) => { + const field = fields[i]; + if (field?.mngNo) { update(i, { ...field, mode: "D", @@ -117,9 +136,13 @@ function IPManagementDialog({ remove(i); } }); + setSelectedRows([]); }; + const isAllSelected = + fields.length > 0 && selectedRows.length === fields.length; + return ( <> @@ -161,7 +184,12 @@ function IPManagementDialog({ - 선택 + + + IP 주소 허용여부 diff --git a/apps/cs/src/pages/site/ui/site-form.tsx b/apps/cs/src/pages/site/ui/site-form.tsx index 4922031f4..7716d393e 100644 --- a/apps/cs/src/pages/site/ui/site-form.tsx +++ b/apps/cs/src/pages/site/ui/site-form.tsx @@ -23,12 +23,10 @@ import { FormMessage, } from "@/shared/ui/form"; import { ConfirmDialog } from "@/shared/ui/confirm-dialog"; -import { type SiteFormData } from "../model/site-interface"; +import type { SiteFormData } from "../model/site-interface"; +import type { Domain } from "../model/domain-interface"; import { IPManagementDialog } from "./ip-management-dialog"; -import { - DomainManagementDialog, - type Domain, -} from "./domain-management-dialog"; +import { DomainManagementDialog } from "./domain-management-dialog"; type SiteFormProps = { form: UseFormReturn; @@ -44,12 +42,11 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) { const { confirm } = useConfirm(); const [onConfirm, setOnConfirm] = useState(false); const [isIpManagementOpen, setIsIpManagementOpen] = useState(false); - const [representativeDomain, setRepresentativeDomain] = useState(""); + const [isDomainManagementOpen, setIsDomainManagementOpen] = useState(false); + const [domainUrl, setDomainUrl] = useState(""); - const handleDomainSave = (rows: Domain[]) => { - setRepresentativeDomain( - rows.find((row) => row.isRepresentative)?.domainUrl || "", - ); + const handleSaveDomain = (rows: Domain[]) => { + setDomainUrl(rows.find((row) => row.rprsDmnYn === "Y")?.dmnAddr || ""); }; const onSubmit = async (data: SiteFormData) => { @@ -132,6 +129,7 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) { /> {isModifyMode && ( @@ -193,6 +192,7 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) { @@ -209,10 +209,13 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) {
- {representativeDomain} + {domainUrl}
@@ -229,6 +232,7 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) { @@ -250,6 +254,7 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) { @@ -271,6 +276,7 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) { @@ -292,6 +298,7 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) { @@ -313,6 +320,7 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) { diff --git a/apps/cs/src/pages/site/ui/site-page.tsx b/apps/cs/src/pages/site/ui/site-page.tsx index feace6285..8a2bd5264 100644 --- a/apps/cs/src/pages/site/ui/site-page.tsx +++ b/apps/cs/src/pages/site/ui/site-page.tsx @@ -7,7 +7,7 @@ import { useEffect, useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { getCoreRowModel, useReactTable } from "@tanstack/react-table"; import { Button, DataTable, Input } from "@common/components"; -import { ENDPOINT } from "@/shared/config/api"; +import { ENDPOINT } from "@/shared/config"; import { SiteApi } from "../api/site-service"; import { columns } from "../model/table-columns"; import { type ISite, type ISiteParams } from "../model/site-interface"; @@ -15,23 +15,18 @@ import { SitePageLayout } from "./layout"; function SitePage() { const router = useRouter(); - const { data: siteInfo, refetch } = useQuery({ + const { data: siteList, refetch } = useQuery({ queryKey: [ENDPOINT.CMS_SERVICE.SITES], - queryFn: () => SiteApi.siteInfo(siteQuery), + queryFn: () => SiteApi.getSiteList(siteQuery), enabled: false, }); const [siteQuery, setSiteQuery] = useState({ siteNm: "", - siteKndCd: "", - useYn: "", - page: 0, - size: 0, - sort: [], }); const table = useReactTable({ - data: siteInfo?.content ?? [], + data: siteList?.content ?? [], columns, getCoreRowModel: getCoreRowModel(), }); @@ -61,9 +56,7 @@ function SitePage() { - setSiteQuery({ ...siteQuery, siteNm: e.target.value }) - } + onChange={(e) => setSiteQuery({ siteNm: e.target.value })} />