From 92eda21e45d902663052575aaa4c4f80bfa2faea Mon Sep 17 00:00:00 2001 From: dujinkim Date: Mon, 4 Aug 2025 09:36:14 +0000 Subject: (대표님) 벤더 문서 변경사항, data-table 변경, sync 변경 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/form-data/update-form-sheet.tsx | 310 ++++++++++++++++------------- 1 file changed, 168 insertions(+), 142 deletions(-) (limited to 'components/form-data/update-form-sheet.tsx') diff --git a/components/form-data/update-form-sheet.tsx b/components/form-data/update-form-sheet.tsx index abc9bbf3..5666a116 100644 --- a/components/form-data/update-form-sheet.tsx +++ b/components/form-data/update-form-sheet.tsx @@ -32,7 +32,7 @@ import { Popover, PopoverTrigger, PopoverContent, -} from "@/components/ui/popover" +} from "@/components/ui/popover"; import { Command, CommandInput, @@ -40,14 +40,24 @@ import { CommandGroup, CommandItem, CommandEmpty, -} from "@/components/ui/command" +} from "@/components/ui/command"; import { DataTableColumnJSON } from "./form-data-table-columns"; import { updateFormDataInDB } from "@/lib/forms/services"; import { cn } from "@/lib/utils"; -interface UpdateTagSheetProps - extends React.ComponentPropsWithoutRef { +/** ============================================================= + * 🔄 UpdateTagSheet with grouped fields by `head` property + * ----------------------------------------------------------- + * - Consecutive columns that share the same `head` value will be + * rendered under a section title (the head itself). + * - Columns without a head still appear normally. + * + * NOTE: Only rendering logic is touched – all validation, + * read‑only checks, and mutation logic stay the same. + * ============================================================*/ + +interface UpdateTagSheetProps extends React.ComponentPropsWithoutRef { open: boolean; onOpenChange: (open: boolean) => void; columns: DataTableColumnJSON[]; @@ -66,74 +76,57 @@ export function UpdateTagSheet({ rowData, formCode, contractItemId, - editableFieldsMap = new Map(), // 기본값 설정 + editableFieldsMap = new Map(), onUpdateSuccess, ...props }: UpdateTagSheetProps) { + // ─────────────────────────────────────────────────────────────── + // hooks & helpers + // ─────────────────────────────────────────────────────────────── const [isPending, startTransition] = React.useTransition(); const router = useRouter(); - // 현재 TAG의 편집 가능한 필드 목록 가져오기 + /* ---------------------------------------------------------------- + * 1️⃣ Editable‑field helpers (unchanged) + * --------------------------------------------------------------*/ const editableFields = React.useMemo(() => { if (!rowData?.TAG_NO || !editableFieldsMap.has(rowData.TAG_NO)) { - return []; + return [] as string[]; } return editableFieldsMap.get(rowData.TAG_NO) || []; }, [rowData?.TAG_NO, editableFieldsMap]); - // 필드가 편집 가능한지 판별하는 함수 const isFieldEditable = React.useCallback((column: DataTableColumnJSON) => { - // 1. SHI-only 필드는 편집 불가 - if (column.shi === true) { - return false; - } - - // 2. TAG_NO와 TAG_DESC는 기본적으로 편집 가능 (필요에 따라 수정 가능) - if (column.key === "TAG_NO" || column.key === "TAG_DESC") { - return true; - } + if (column.shi === true) return false; // SHI‑only + if (column.key === "TAG_NO" || column.key === "TAG_DESC") return true; + return editableFields.includes(column.key); + }, [editableFields]); - //3. editableFieldsMap이 있으면 해당 리스트에 있는지 확인 - // if (rowData?.TAG_NO && editableFieldsMap.has(rowData.TAG_NO)) { - // return editableFields.includes(column.key); - // } + const isFieldReadOnly = React.useCallback((column: DataTableColumnJSON) => !isFieldEditable(column), [isFieldEditable]); - // 4. editableFieldsMap 정보가 없으면 기본적으로 편집 불가 (안전한 기본값) - return true; - }, []); - - // 읽기 전용 필드인지 판별하는 함수 (편집 가능의 반대) - const isFieldReadOnly = React.useCallback((column: DataTableColumnJSON) => { - return !isFieldEditable(column); - }, [isFieldEditable]); - - // 읽기 전용 사유를 반환하는 함수 const getReadOnlyReason = React.useCallback((column: DataTableColumnJSON) => { - if (column.shi === true) { - return "SHI-only field (managed by SHI system)"; - } - + if (column.shi) return "SHI‑only field (managed by SHI system)"; if (column.key !== "TAG_NO" && column.key !== "TAG_DESC") { if (!rowData?.TAG_NO || !editableFieldsMap.has(rowData.TAG_NO)) { return "No editable fields information for this TAG"; } - if (!editableFields.includes(column.key)) { return "Not editable for this TAG class"; } } - - return "Read-only field"; + return "Read‑only field"; }, [rowData?.TAG_NO, editableFieldsMap, editableFields]); - // 1) zod 스키마 + /* ---------------------------------------------------------------- + * 2️⃣ Zod dynamic schema & form state (unchanged) + * --------------------------------------------------------------*/ const dynamicSchema = React.useMemo(() => { - const shape: Record> = {}; + const shape: Record = {}; for (const col of columns) { if (col.type === "NUMBER") { shape[col.key] = z .union([z.coerce.number(), z.nan()]) - .transform((val) => (isNaN(val) ? undefined : val)) + .transform((val) => (isNaN(val as number) ? undefined : val)) .optional(); } else { shape[col.key] = z.string().optional(); @@ -142,16 +135,14 @@ export function UpdateTagSheet({ return z.object(shape); }, [columns]); - // 2) form init const form = useForm({ resolver: zodResolver(dynamicSchema), defaultValues: React.useMemo(() => { if (!rowData) return {}; - const defaults: Record = {}; - for (const col of columns) { - defaults[col.key] = rowData[col.key] ?? ""; - } - return defaults; + return columns.reduce>((acc, col) => { + acc[col.key] = rowData[col.key] ?? ""; + return acc; + }, {}); }, [rowData, columns]), }); @@ -161,28 +152,67 @@ export function UpdateTagSheet({ return; } const defaults: Record = {}; - for (const col of columns) { + columns.forEach((col) => { defaults[col.key] = rowData[col.key] ?? ""; - } + }); form.reset(defaults); }, [rowData, columns, form]); + /* ---------------------------------------------------------------- + * 3️⃣ Grouping logic – figure out consecutive columns that share + * the same `head` value. This mirrors `groupColumnsByHead` that + * you already use for the table view. + * --------------------------------------------------------------*/ + const groupedColumns = React.useMemo(() => { + // Ensure original ordering by `seq` where present + const sorted = [...columns].sort((a, b) => { + const seqA = a.seq ?? 999999; + const seqB = b.seq ?? 999999; + return seqA - seqB; + }); + + const groups: { head: string | null; cols: DataTableColumnJSON[] }[] = []; + let i = 0; + while (i < sorted.length) { + const curr = sorted[i]; + const head = curr.head?.trim() || null; + if (!head) { + groups.push({ head: null, cols: [curr] }); + i += 1; + continue; + } + + // Collect consecutive columns with the same head + const cols: DataTableColumnJSON[] = [curr]; + let j = i + 1; + while (j < sorted.length && sorted[j].head?.trim() === head) { + cols.push(sorted[j]); + j += 1; + } + groups.push({ head, cols }); + i = j; + } + return groups; + }, [columns]); + + /* ---------------------------------------------------------------- + * 4️⃣ Submission handler (unchanged) + * --------------------------------------------------------------*/ async function onSubmit(values: Record) { startTransition(async () => { try { - // 제출 전에 읽기 전용 필드를 원본 값으로 복원 - const finalValues = { ...values }; - for (const col of columns) { + // Restore read‑only fields to their original value before saving + const finalValues: Record = { ...values }; + columns.forEach((col) => { if (isFieldReadOnly(col)) { - // 읽기 전용 필드는 원본 값으로 복원 finalValues[col.key] = rowData?.[col.key] ?? ""; } - } + }); const { success, message } = await updateFormDataInDB( formCode, contractItemId, - finalValues + finalValues, ); if (!success) { @@ -190,25 +220,12 @@ export function UpdateTagSheet({ return; } - // Success handling toast.success("Updated successfully!"); - // Create a merged object of original rowData and new values - const updatedData = { - ...rowData, - ...finalValues, - TAG_NO: rowData?.TAG_NO, - }; - - // Call the success callback + const updatedData = { ...rowData, ...finalValues, TAG_NO: rowData?.TAG_NO }; onUpdateSuccess?.(updatedData); - - // Refresh the entire route to get fresh data router.refresh(); - - // Close the sheet onOpenChange(false); - } catch (error) { console.error("Error updating form data:", error); toast.error("An unexpected error occurred while updating"); @@ -216,44 +233,57 @@ export function UpdateTagSheet({ }); } - // 편집 가능한 필드 개수 계산 - const editableFieldCount = React.useMemo(() => { - return columns.filter(col => isFieldEditable(col)).length; - }, [columns, isFieldEditable]); + /* ---------------------------------------------------------------- + * 5️⃣ UI + * --------------------------------------------------------------*/ + const editableFieldCount = React.useMemo(() => columns.filter(isFieldEditable).length, [columns, isFieldEditable]); return ( - Update Row - {rowData?.TAG_NO || 'Unknown TAG'} + + Update Row – {rowData?.TAG_NO || "Unknown TAG"} + - Modify the fields below and save changes. Fields with are read-only. + Modify the fields below and save changes. Fields with + are read‑only.
- {editableFieldCount} of {columns.length} fields are editable for this TAG. + {editableFieldCount} of {columns.length} fields are editable for + this TAG.
+ {/* ────────────────────────────────────────────── */}
- + + {/* Scroll wrapper */}
-
- {columns.map((col) => { - const isReadOnly = isFieldReadOnly(col); - const readOnlyReason = isReadOnly ? getReadOnlyReason(col) : ""; - - return ( - { - switch (col.type) { - case "NUMBER": + {/* ------------------------------------------------------------------ + * Render groups + * ----------------------------------------------------------------*/} + {groupedColumns.map(({ head, cols }) => ( +
+ {head && ( +

+ {head} +

+ )} + + {/* Fields inside the group */} + {cols.map((col) => { + const isReadOnly = isFieldReadOnly(col); + const readOnlyReason = isReadOnly ? getReadOnlyReason(col) : ""; + return ( + { + // ——————————————— Number ———————————————— + if (col.type === "NUMBER") { return ( @@ -272,7 +302,8 @@ export function UpdateTagSheet({ }} value={field.value ?? ""} className={cn( - isReadOnly && "bg-gray-100 text-gray-600 cursor-not-allowed border-gray-300" + isReadOnly && + "bg-gray-100 text-gray-600 cursor-not-allowed border-gray-300", )} /> @@ -284,8 +315,10 @@ export function UpdateTagSheet({ ); + } - case "LIST": + // ——————————————— List ———————————————— + if (col.type === "LIST") { return ( @@ -303,12 +336,11 @@ export function UpdateTagSheet({ className={cn( "w-full justify-between", !field.value && "text-muted-foreground", - isReadOnly && "bg-gray-100 text-gray-600 cursor-not-allowed border-gray-300" + isReadOnly && + "bg-gray-100 text-gray-600 cursor-not-allowed border-gray-300", )} > - {field.value - ? col.options?.find((opt) => opt === field.value) - : "Select an option"} + {field.value ? col.options?.find((o) => o === field.value) : "Select an option"} @@ -319,17 +351,11 @@ export function UpdateTagSheet({ {col.options?.map((opt) => ( - { - field.onChange(opt); - }} - > + field.onChange(opt)}> {opt} @@ -348,52 +374,52 @@ export function UpdateTagSheet({ ); + } - case "STRING": - default: - return ( - - - {col.label} - {isReadOnly && ( - - )} - - - - + // ——————————————— String / default ———————————— + return ( + + + {col.label} {isReadOnly && ( - - {readOnlyReason} - + )} - - - ); - } - }} - /> - ); - })} -
+ + + + + {isReadOnly && ( + + {readOnlyReason} + + )} + + + ); + }} + /> + ); + })} +
+ ))}
+ {/* Footer */} -
@@ -401,4 +427,4 @@ export function UpdateTagSheet({
); -} \ No newline at end of file +} -- cgit v1.2.3