From 92eda21e45d902663052575aaa4c4f80bfa2faea Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
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<typeof Sheet> {
+/** =============================================================
+ *  🔄  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<typeof Sheet> {
   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<string, z.ZodType<any>> = {};
+    const shape: Record<string, z.ZodTypeAny> = {};
     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<string, any> = {};
-      for (const col of columns) {
-        defaults[col.key] = rowData[col.key] ?? "";
-      }
-      return defaults;
+      return columns.reduce<Record<string, any>>((acc, col) => {
+        acc[col.key] = rowData[col.key] ?? "";
+        return acc;
+      }, {});
     }, [rowData, columns]),
   });
 
@@ -161,28 +152,67 @@ export function UpdateTagSheet({
       return;
     }
     const defaults: Record<string, any> = {};
-    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<string, any>) {
     startTransition(async () => {
       try {
-        // 제출 전에 읽기 전용 필드를 원본 값으로 복원
-        const finalValues = { ...values };
-        for (const col of columns) {
+        // Restore read‑only fields to their original value before saving
+        const finalValues: Record<string, any> = { ...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 (
     <Sheet open={open} onOpenChange={onOpenChange} {...props}>
       <SheetContent className="sm:max-w-xl md:max-w-3xl lg:max-w-4xl xl:max-w-5xl flex flex-col">
         <SheetHeader className="text-left">
-          <SheetTitle>Update Row - {rowData?.TAG_NO || 'Unknown TAG'}</SheetTitle>
+          <SheetTitle>
+            Update Row – {rowData?.TAG_NO || "Unknown TAG"}
+          </SheetTitle>
           <SheetDescription>
-            Modify the fields below and save changes. Fields with <LockIcon className="inline h-3 w-3" /> are read-only.
+            Modify the fields below and save changes. Fields with
+            <LockIcon className="inline h-3 w-3 mx-1" /> are read‑only.
             <br />
             <span className="text-sm text-green-600">
-              {editableFieldCount} of {columns.length} fields are editable for this TAG.
+              {editableFieldCount} of {columns.length} fields are editable for
+              this TAG.
             </span>
           </SheetDescription>
         </SheetHeader>
 
+        {/* ────────────────────────────────────────────── */}
         <Form {...form}>
-          <form
-            onSubmit={form.handleSubmit(onSubmit)}
-            className="flex flex-col gap-4"
-          >
+          <form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-4">
+            {/* Scroll wrapper */}
             <div className="overflow-y-auto max-h-[80vh] flex-1 pr-4 -mr-4">
-              <div className="flex flex-col gap-4 pt-2">
-                {columns.map((col) => {
-                  const isReadOnly = isFieldReadOnly(col);
-                  const readOnlyReason = isReadOnly ? getReadOnlyReason(col) : "";
-                  
-                  return (
-                    <FormField
-                      key={col.key}
-                      control={form.control}
-                      name={col.key}
-                      render={({ field }) => {
-                        switch (col.type) {
-                          case "NUMBER":
+              {/* ------------------------------------------------------------------
+               * Render groups
+               * ----------------------------------------------------------------*/}
+              {groupedColumns.map(({ head, cols }) => (
+                <div key={head ?? cols[0].key} className="flex flex-col gap-4 pt-2">
+                  {head && (
+                    <h3 className="text-sm font-semibold text-gray-700 uppercase tracking-wide pl-1">
+                      {head}
+                    </h3>
+                  )}
+
+                  {/* Fields inside the group */}
+                  {cols.map((col) => {
+                    const isReadOnly = isFieldReadOnly(col);
+                    const readOnlyReason = isReadOnly ? getReadOnlyReason(col) : "";
+                    return (
+                      <FormField
+                        key={col.key}
+                        control={form.control}
+                        name={col.key}
+                        render={({ field }) => {
+                          // ——————————————— Number ————————————————
+                          if (col.type === "NUMBER") {
                             return (
                               <FormItem>
                                 <FormLabel className="flex items-center">
@@ -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",
                                     )}
                                   />
                                 </FormControl>
@@ -284,8 +315,10 @@ export function UpdateTagSheet({
                                 <FormMessage />
                               </FormItem>
                             );
+                          }
 
-                          case "LIST":
+                          // ——————————————— List ————————————————
+                          if (col.type === "LIST") {
                             return (
                               <FormItem>
                                 <FormLabel className="flex items-center">
@@ -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"}
                                       <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
                                     </Button>
                                   </PopoverTrigger>
@@ -319,17 +351,11 @@ export function UpdateTagSheet({
                                       <CommandList>
                                         <CommandGroup>
                                           {col.options?.map((opt) => (
-                                            <CommandItem
-                                              key={opt}
-                                              value={opt}
-                                              onSelect={() => {
-                                                field.onChange(opt);
-                                              }}
-                                            >
+                                            <CommandItem key={opt} value={opt} onSelect={() => field.onChange(opt)}>
                                               <Check
                                                 className={cn(
                                                   "mr-2 h-4 w-4",
-                                                  field.value === opt ? "opacity-100" : "opacity-0"
+                                                  field.value === opt ? "opacity-100" : "opacity-0",
                                                 )}
                                               />
                                               {opt}
@@ -348,52 +374,52 @@ export function UpdateTagSheet({
                                 <FormMessage />
                               </FormItem>
                             );
+                          }
 
-                          case "STRING":
-                          default:
-                            return (
-                              <FormItem>
-                                <FormLabel className="flex items-center">
-                                  {col.label}
-                                  {isReadOnly && (
-                                    <LockIcon className="ml-1 h-3 w-3 text-gray-400" />
-                                  )}
-                                </FormLabel>
-                                <FormControl>
-                                  <Input
-                                    readOnly={isReadOnly}
-                                    {...field}
-                                    className={cn(
-                                      isReadOnly && "bg-gray-100 text-gray-600 cursor-not-allowed border-gray-300"
-                                    )}
-                                  />
-                                </FormControl>
+                          // ——————————————— String / default ————————————
+                          return (
+                            <FormItem>
+                              <FormLabel className="flex items-center">
+                                {col.label}
                                 {isReadOnly && (
-                                  <FormDescription className="text-xs text-gray-500">
-                                    {readOnlyReason}
-                                  </FormDescription>
+                                  <LockIcon className="ml-1 h-3 w-3 text-gray-400" />
                                 )}
-                                <FormMessage />
-                              </FormItem>
-                            );
-                        }
-                      }}
-                    />
-                  );
-                })}
-              </div>
+                              </FormLabel>
+                              <FormControl>
+                                <Input
+                                  readOnly={isReadOnly}
+                                  {...field}
+                                  className={cn(
+                                    isReadOnly &&
+                                      "bg-gray-100 text-gray-600 cursor-not-allowed border-gray-300",
+                                  )}
+                                />
+                              </FormControl>
+                              {isReadOnly && (
+                                <FormDescription className="text-xs text-gray-500">
+                                  {readOnlyReason}
+                                </FormDescription>
+                              )}
+                              <FormMessage />
+                            </FormItem>
+                          );
+                        }}
+                      />
+                    );
+                  })}
+                </div>
+              ))}
             </div>
 
+            {/* Footer */}
             <SheetFooter className="gap-2 pt-2">
               <SheetClose asChild>
                 <Button type="button" variant="outline">
                   Cancel
                 </Button>
               </SheetClose>
-
               <Button type="submit" disabled={isPending}>
-                {isPending && <Loader className="mr-2 h-4 w-4 animate-spin" />}
-                Save
+                {isPending && <Loader className="mr-2 h-4 w-4 animate-spin" />}Save
               </Button>
             </SheetFooter>
           </form>
@@ -401,4 +427,4 @@ export function UpdateTagSheet({
       </SheetContent>
     </Sheet>
   );
-}
\ No newline at end of file
+}
-- 
cgit v1.2.3

