From 11b0070fe14e07f56fd8b222acc03dfe9b68cd46 Mon Sep 17 00:00:00 2001 From: bicochan Date: Wed, 15 Apr 2026 13:30:05 +0900 Subject: [PATCH] =?UTF-8?q?[cs]=20feat:=20=EC=95=8C=EB=A6=BC=ED=86=A1?= =?UTF-8?q?=ED=85=9C=ED=94=8C=EB=A6=BF=EA=B4=80=EB=A6=AC=20>=20=EB=B3=80?= =?UTF-8?q?=EC=88=98=EA=B4=80=EB=A6=AC=20>=20=EB=B3=80=EC=88=98=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=A1=9C=EC=A7=81=20=EB=8B=A4=EC=9D=B4=EC=96=BC?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ntftalk/model/ntftalk-var-interface.ts | 2 +- .../model/ntftalk-var-additional-schema.ts | 17 ++ .../model/ntftalk-var-batch-payload.ts | 16 +- .../ntftalk-temp/model/ntftalk-var-grid.ts | 6 - .../ntftalk-temp/model/var-table-columns.tsx | 66 +----- .../ui/ntftalk-temp-regist-dialog.tsx | 9 +- .../ui/ntftalk-temp-template-section.tsx | 14 +- .../ui/ntftalk-temp-var-section.tsx | 100 ++------- .../ui/ntftalk-var-add-dialog.tsx | 197 ++++++++++++++++++ 9 files changed, 252 insertions(+), 175 deletions(-) create mode 100644 apps/cs/src/pages/ntftalk-temp/model/ntftalk-var-additional-schema.ts create mode 100644 apps/cs/src/pages/ntftalk-temp/ui/ntftalk-var-add-dialog.tsx diff --git a/apps/cs/src/entities/ntftalk/model/ntftalk-var-interface.ts b/apps/cs/src/entities/ntftalk/model/ntftalk-var-interface.ts index 8367ea87e..7bfc1d0e6 100644 --- a/apps/cs/src/entities/ntftalk/model/ntftalk-var-interface.ts +++ b/apps/cs/src/entities/ntftalk/model/ntftalk-var-interface.ts @@ -11,7 +11,7 @@ export interface NtftalkVar { sender: string; varNm: string; varDesc: string; - regDt: string; + regDt?: string; regr: string; } diff --git a/apps/cs/src/pages/ntftalk-temp/model/ntftalk-var-additional-schema.ts b/apps/cs/src/pages/ntftalk-temp/model/ntftalk-var-additional-schema.ts new file mode 100644 index 000000000..e2368fe77 --- /dev/null +++ b/apps/cs/src/pages/ntftalk-temp/model/ntftalk-var-additional-schema.ts @@ -0,0 +1,17 @@ +import { z } from "zod"; + +export const ntftalkVarAdditionalSchema = z.object({ + draftVars: z.array( + z.object({ + varCd: z.string().min(1, "변수코드를 입력하세요."), + varNm: z.string().min(1, "변수명을 입력하세요."), + varDesc: z.string().default(""), + sender: z.string().default(""), + regr: z.string().default(""), + }), + ), +}); + +export type NtftalkVarAdditionalFormData = z.infer< + typeof ntftalkVarAdditionalSchema +>; diff --git a/apps/cs/src/pages/ntftalk-temp/model/ntftalk-var-batch-payload.ts b/apps/cs/src/pages/ntftalk-temp/model/ntftalk-var-batch-payload.ts index 23af045f6..91d715188 100644 --- a/apps/cs/src/pages/ntftalk-temp/model/ntftalk-var-batch-payload.ts +++ b/apps/cs/src/pages/ntftalk-temp/model/ntftalk-var-batch-payload.ts @@ -12,15 +12,21 @@ export const varDraftRowSchema = z.object({ }); export function buildNtftalkVarBatchPayload(input: { - draftVars: NtftalkVarGridRow[]; pendingDeleteVarCds: ReadonlySet; - regr: string | undefined; + draftVars?: NtftalkVarGridRow[]; + regr?: string; }): NtftalkVarSavePayload[] { const { draftVars, pendingDeleteVarCds, regr } = input; const payloads: NtftalkVarSavePayload[] = []; + if (!draftVars) { + for (const varCd of pendingDeleteVarCds) { + payloads.push({ mode: "D", varCd }); + } + return payloads; + } + for (const d of draftVars) { - if (!d.isDraft) continue; const parsed = varDraftRowSchema.safeParse({ varCd: d.varCd, varNm: d.varNm, @@ -42,9 +48,5 @@ export function buildNtftalkVarBatchPayload(input: { }); } - for (const varCd of pendingDeleteVarCds) { - payloads.push({ mode: "D", varCd }); - } - return payloads; } diff --git a/apps/cs/src/pages/ntftalk-temp/model/ntftalk-var-grid.ts b/apps/cs/src/pages/ntftalk-temp/model/ntftalk-var-grid.ts index b946305a5..914130e09 100644 --- a/apps/cs/src/pages/ntftalk-temp/model/ntftalk-var-grid.ts +++ b/apps/cs/src/pages/ntftalk-temp/model/ntftalk-var-grid.ts @@ -2,17 +2,11 @@ import type { NtftalkVar } from "@/entities/ntftalk"; export interface NtftalkVarGridRow extends NtftalkVar { clientId?: string; - isDraft?: boolean; -} - -export interface VarDraftFormValues { - draftVars: NtftalkVarGridRow[]; } export function createEmptyVarDraft(clientId: string): NtftalkVarGridRow { return { clientId, - isDraft: true, varCd: "", varNm: "", varDesc: "", diff --git a/apps/cs/src/pages/ntftalk-temp/model/var-table-columns.tsx b/apps/cs/src/pages/ntftalk-temp/model/var-table-columns.tsx index 3514bd7eb..19e02e78d 100644 --- a/apps/cs/src/pages/ntftalk-temp/model/var-table-columns.tsx +++ b/apps/cs/src/pages/ntftalk-temp/model/var-table-columns.tsx @@ -1,6 +1,6 @@ import type { ColumnDef, Row, Table } from "@tanstack/react-table"; -import { Checkbox, Input } from "@common/components"; +import { Checkbox } from "@common/components"; import type { NtftalkVarGridRow } from "./ntftalk-var-grid"; @@ -12,9 +12,7 @@ export interface VarColumnHandlers { ) => void; } -export function createVarColumns( - handlers: VarColumnHandlers, -): ColumnDef[] { +export function createVarColumns(): ColumnDef[] { return [ { id: "select", @@ -65,74 +63,14 @@ export function createVarColumns( { accessorKey: "varCd", header: "변수코드", - cell: ({ row }) => { - const r = row.original; - if (r.isDraft && r.clientId) { - return ( - - handlers.onDraftFieldChange( - r.clientId!, - "varCd", - e.target.value, - ) - } - onClick={(e) => e.stopPropagation()} - /> - ); - } - return r.varCd; - }, }, { accessorKey: "varNm", header: "변수명", - cell: ({ row }) => { - const r = row.original; - if (r.isDraft && r.clientId) { - return ( - - handlers.onDraftFieldChange( - r.clientId!, - "varNm", - e.target.value, - ) - } - onClick={(e) => e.stopPropagation()} - /> - ); - } - return r.varNm; - }, }, { accessorKey: "varDesc", header: "설명", - cell: ({ row }) => { - const r = row.original; - if (r.isDraft && r.clientId) { - return ( - - handlers.onDraftFieldChange( - r.clientId!, - "varDesc", - e.target.value, - ) - } - onClick={(e) => e.stopPropagation()} - /> - ); - } - return r.varDesc ?? "—"; - }, }, { accessorKey: "regDt", diff --git a/apps/cs/src/pages/ntftalk-temp/ui/ntftalk-temp-regist-dialog.tsx b/apps/cs/src/pages/ntftalk-temp/ui/ntftalk-temp-regist-dialog.tsx index 8a537e7a2..f989a0165 100644 --- a/apps/cs/src/pages/ntftalk-temp/ui/ntftalk-temp-regist-dialog.tsx +++ b/apps/cs/src/pages/ntftalk-temp/ui/ntftalk-temp-regist-dialog.tsx @@ -109,17 +109,10 @@ function NtftalkTempRegistDialog({ enabled: isVarDialogOpen, }); - const varColumns = useMemo( - () => - createVarColumns({ - onDraftFieldChange: () => {}, - }), - [], - ); const { tableComponent, tableInstance } = useDataTable({ query: varQuery, tableOptions: { - columns: varColumns, + columns: createVarColumns(), state: { pagination: varPagination, rowSelection: varRowSelection }, manualPagination: true, enableRowSelection: true, diff --git a/apps/cs/src/pages/ntftalk-temp/ui/ntftalk-temp-template-section.tsx b/apps/cs/src/pages/ntftalk-temp/ui/ntftalk-temp-template-section.tsx index 4903a5f73..fea00f6d9 100644 --- a/apps/cs/src/pages/ntftalk-temp/ui/ntftalk-temp-template-section.tsx +++ b/apps/cs/src/pages/ntftalk-temp/ui/ntftalk-temp-template-section.tsx @@ -60,8 +60,10 @@ function NtftalkTempTablePanel({ }: NtftalkTempTablePanelProps) { if (query.isLoading) { return ( -
- +
+
+ +
); } @@ -217,8 +219,7 @@ function NtftalkTempTemplateSection() { alert("삭제할 템플릿을 선택해 주세요."); return; } - if (!(await confirm(`선택한 ${rows.length}건을 삭제하시겠습니까?`))) - return; + if (!(await confirm(`선택한 ${rows.length}건을 삭제하시겠습니까?`))) return; const codes = rows.map((r) => r.original.templateCode); setPendingDeleteTemplateCodes((prev) => { const next = new Set(prev); @@ -420,10 +421,7 @@ function NtftalkTempTemplateSection() { diff --git a/apps/cs/src/pages/ntftalk-temp/ui/ntftalk-temp-var-section.tsx b/apps/cs/src/pages/ntftalk-temp/ui/ntftalk-temp-var-section.tsx index 7e517afc7..6077be8ac 100644 --- a/apps/cs/src/pages/ntftalk-temp/ui/ntftalk-temp-var-section.tsx +++ b/apps/cs/src/pages/ntftalk-temp/ui/ntftalk-temp-var-section.tsx @@ -1,7 +1,6 @@ "use client"; import { useCallback, useMemo, useState } from "react"; -import { useFieldArray, useForm, useWatch } from "react-hook-form"; import { useConfirm } from "@frontend-opensource/use-react-hooks"; import type { UseQueryResult } from "@tanstack/react-query"; @@ -19,7 +18,7 @@ import { } from "@tanstack/react-table"; import { ENDPOINT } from "@/shared/config"; -import { useAlertStore, useMutationWrapper, useUserStore } from "@/shared/lib"; +import { useAlertStore, useMutationWrapper } from "@/shared/lib"; import { NoData } from "@/shared/ui/no-data"; import { NtftalkVarApi, @@ -30,13 +29,10 @@ import { import { Button, DataTable, LoadingWithAnimation } from "@common/components"; import { buildNtftalkVarBatchPayload } from "../model/ntftalk-var-batch-payload"; -import { - createEmptyVarDraft, - type NtftalkVarGridRow, - type VarDraftFormValues, -} from "../model/ntftalk-var-grid"; +import { type NtftalkVarGridRow } from "../model/ntftalk-var-grid"; import { createVarColumns } from "../model/var-table-columns"; +import { NtftalkVarAddDialog } from "./ntftalk-var-add-dialog"; import { NtftalkVarEditDialog } from "./ntftalk-var-edit-dialog"; interface NtftalkVarTablePanelProps { @@ -67,7 +63,6 @@ function NtftalkVarSection() { const queryClient = useQueryClient(); const alert = useAlertStore((s) => s.setMessage); const { confirm } = useConfirm(); - const userInfo = useUserStore((s) => s.userInfo); const [varPagination, setVarPagination] = useState({ pageIndex: 0, @@ -81,16 +76,7 @@ function NtftalkVarSection() { () => new Set(), ); const [editTargetVarCd, setEditTargetVarCd] = useState(null); - - const varDraftForm = useForm({ - defaultValues: { draftVars: [] }, - }); - const { control, setValue, getValues } = varDraftForm; - const { append, remove, replace } = useFieldArray({ - control, - name: "draftVars", - }); - const draftVars = useWatch({ control, name: "draftVars" }) ?? []; + const [addVarDialogOpen, setAddVarDialogOpen] = useState(false); const varQuery = useQuery({ queryKey: [ @@ -107,48 +93,15 @@ function NtftalkVarSection() { placeholderData: keepPreviousData, }); - const draftIndexByClientId = useMemo(() => { - const indexByClientId = new Map(); - draftVars.forEach((row, index) => { - if (row.clientId) indexByClientId.set(row.clientId, index); - }); - return indexByClientId; - }, [draftVars]); - - const onDraftFieldChange = useCallback( - ( - clientId: string, - field: "varCd" | "varNm" | "varDesc" | "sender", - value: string, - ) => { - const drafts = getValues("draftVars") ?? []; - const draftIndex = drafts.findIndex( - (r) => r.isDraft && r.clientId === clientId, - ); - if (draftIndex === -1) return; - setValue(`draftVars.${draftIndex}.${field}`, value, { - shouldDirty: true, - }); - }, - [getValues, setValue], - ); - - const varColumns = useMemo( - () => createVarColumns({ onDraftFieldChange }), - [onDraftFieldChange], - ); - const varTableRows = useMemo((): NtftalkVarGridRow[] => { - const server = (varQuery.data?.content ?? []).filter( + return (varQuery.data?.content ?? []).filter( (row) => !pendingDeleteVarCds.has(row.varCd), ); - if (varPagination.pageIndex === 0) return [...server, ...draftVars]; - return server; - }, [varQuery.data, draftVars, varPagination.pageIndex, pendingDeleteVarCds]); + }, [varQuery.data, pendingDeleteVarCds]); const varTable = useReactTable({ data: varTableRows, - columns: varColumns, + columns: createVarColumns(), state: { pagination: varPagination, rowSelection: varRowSelection }, manualPagination: true, rowCount: varQuery.data?.totalElements ?? 0, @@ -158,17 +111,9 @@ function NtftalkVarSection() { getCoreRowModel: getCoreRowModel(), onPaginationChange: setVarPagination, onRowSelectionChange: setVarRowSelection, - getRowId: (row) => - row.isDraft && row.clientId - ? `draft:${row.clientId}` - : `srv:${row.varCd}`, + getRowId: (row) => `srv:${row.varCd}`, }); - const handleAddVarRow = () => { - append(createEmptyVarDraft(crypto.randomUUID())); - setVarPagination((p) => ({ ...p, pageIndex: 0 })); - }; - const handleDeleteSelectedVars = async () => { const selected = varTable.getSelectedRowModel().rows; if (selected.length === 0) { @@ -178,18 +123,9 @@ function NtftalkVarSection() { if (!(await confirm(`선택한 ${selected.length}건을 삭제하시겠습니까?`))) return; - const serverCodes: string[] = []; - const draftIndexes: number[] = []; - selected.forEach((r) => { - const o = r.original; - if (o.isDraft && o.clientId) { - const draftIndex = draftIndexByClientId.get(o.clientId); - if (draftIndex !== undefined) draftIndexes.push(draftIndex); - } else if (o.varCd) serverCodes.push(o.varCd); - }); - - if (draftIndexes.length) - remove([...new Set(draftIndexes)].sort((a, b) => b - a)); + const serverCodes = selected + .map((r) => r.original.varCd) + .filter((varCd): varCd is string => Boolean(varCd)); if (serverCodes.length) { setPendingDeleteVarCds((prev) => { @@ -208,29 +144,26 @@ function NtftalkVarSection() { await queryClient.invalidateQueries({ queryKey: [ENDPOINT.NTFTALK_SERVICE.NTFTALK_VAR], }); - replace([]); setPendingDeleteVarCds(new Set()); setVarRowSelection({}); + setAddVarDialogOpen(false); alert("저장되었습니다."); }, }); const handleVarRowClick = useCallback((row: NtftalkVarGridRow) => { - if (row.isDraft) return; setEditTargetVarCd(row.varCd); }, []); const handleSaveVars = () => { - if (draftVars.length === 0 && pendingDeleteVarCds.size === 0) { + if (pendingDeleteVarCds.size === 0) { alert("저장할 변경 사항이 없습니다."); return; } let payload: NtftalkVarSavePayload[]; try { payload = buildNtftalkVarBatchPayload({ - draftVars, pendingDeleteVarCds, - regr: userInfo?.userName ?? "", }); } catch (e) { alert(e instanceof Error ? e.message : "유효하지 않은 입력입니다."); @@ -252,7 +185,7 @@ function NtftalkVarSection() { type="button" color="green" className="h-[3.2rem]" - onClick={handleAddVarRow} + onClick={() => setAddVarDialogOpen(true)} > 변수추가 @@ -282,6 +215,11 @@ function NtftalkVarSection() { onRowClick={handleVarRowClick} />
+ void; + onSave: (payload: NtftalkVarSavePayload[]) => void; +} + +function NtftalkVarAddDialog({ + open, + onOpenChange, + onSave, +}: NtftalkVarAddDialogProps) { + const alert = useAlertStore((s) => s.setMessage); + const userInfo = useUserStore((s) => s.userInfo); + + const form = useForm({ + resolver: zodResolver(ntftalkVarAdditionalSchema), + defaultValues: { draftVars: [] }, + }); + const { control, reset } = form; + const { fields, append } = useFieldArray({ + control, + name: "draftVars", + }); + + useEffect(() => { + if (!open) return; + reset({ + draftVars: [createEmptyVarDraft(crypto.randomUUID())], + }); + }, [open, reset]); + + const onSubmit = form.handleSubmit((data) => { + const draftVars = data.draftVars ?? []; + let payload: NtftalkVarSavePayload[]; + try { + payload = buildNtftalkVarBatchPayload({ + draftVars, + pendingDeleteVarCds: new Set(), + regr: userInfo?.userName, + }); + } catch (e) { + alert(e instanceof Error ? e.message : "유효하지 않은 입력입니다."); + return; + } + if (payload.length === 0) { + alert("저장할 변경 사항이 없습니다."); + return; + } + onSave(payload); + }); + + return ( + + e.preventDefault()} + > + + 변수 추가 + +
+
+ + + + + + 변수코드 + + + 변수명 + + + 설명 + + + + + {fields.map((field, index) => ( + + + ( + + + + + + + )} + /> + + + ( + + + + + + + )} + /> + + + ( + + + + + + + )} + /> + + + ))} + +
+
+ +
+ +
+
+ + + + +
+
+ ); +} + +export { NtftalkVarAddDialog };