From 4eb7532f822c821fb6b69bf103bd075fefba769b Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
Date: Tue, 15 Jul 2025 10:07:09 +0000
Subject: (대표님) 20250715 협력사 정기평가, spreadJS, roles 서비스에 함수 추가
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 components/form-data/spreadJS-dialog.tsx | 654 +++++++++++++++++++------------
 1 file changed, 396 insertions(+), 258 deletions(-)

(limited to 'components/form-data/spreadJS-dialog.tsx')

diff --git a/components/form-data/spreadJS-dialog.tsx b/components/form-data/spreadJS-dialog.tsx
index 8be9d175..c106f926 100644
--- a/components/form-data/spreadJS-dialog.tsx
+++ b/components/form-data/spreadJS-dialog.tsx
@@ -2,45 +2,31 @@
 
 import * as React from "react";
 import dynamic from "next/dynamic";
-import {
-  Dialog,
-  DialogContent,
-  DialogHeader,
-  DialogTitle,
-  DialogFooter,
-  DialogDescription,
-} from "@/components/ui/dialog";
+import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from "@/components/ui/dialog";
 import { Button } from "@/components/ui/button";
 import { GenericData } from "./export-excel-form";
 import * as GC from "@mescius/spread-sheets";
 import { toast } from "sonner";
 import { updateFormDataInDB } from "@/lib/forms/services";
 import { Loader, Save } from "lucide-react";
-import {
-  Select,
-  SelectContent,
-  SelectItem,
-  SelectTrigger,
-  SelectValue,
-} from "@/components/ui/select";
-import "@mescius/spread-sheets/styles/gc.spread.sheets.excel2016colorful.css";
-
-// Dynamically load the SpreadSheets component (disable SSR)
+import '@mescius/spread-sheets/styles/gc.spread.sheets.excel2016colorful.css';
+
+// SpreadSheets를 동적으로 import (SSR 비활성화)
 const SpreadSheets = dynamic(
-  () => import("@mescius/spread-sheets-react").then((mod) => mod.SpreadSheets),
-  {
+  () => import("@mescius/spread-sheets-react").then(mod => mod.SpreadSheets),
+  { 
     ssr: false,
     loading: () => (
       <div className="flex items-center justify-center h-full">
         <Loader className="mr-2 h-4 w-4 animate-spin" />
         Loading SpreadSheets...
       </div>
-    ),
+    )
   }
 );
 
-// Apply license key on the client only
-if (typeof window !== "undefined" && process.env.NEXT_PUBLIC_SPREAD_LICENSE) {
+// 라이센스 키 설정을 클라이언트에서만 실행
+if (typeof window !== 'undefined' && process.env.NEXT_PUBLIC_SPREAD_LICENSE) {
   GC.Spread.Sheets.LicenseKey = process.env.NEXT_PUBLIC_SPREAD_LICENSE;
 }
 
@@ -85,11 +71,12 @@ interface TemplateViewDialogProps {
   isOpen: boolean;
   onClose: () => void;
   templateData: TemplateItem[] | any;
-  selectedRow: GenericData;
+  selectedRow?: GenericData; // SPR_ITM_LST_SETUP용
+  tableData?: GenericData[]; // SPR_LST_SETUP용
   formCode: string;
   contractItemId: number;
-  editableFieldsMap?: Map<string, string[]>; // editable field info per tag
-  onUpdateSuccess?: (updatedValues: Record<string, any>) => void;
+  editableFieldsMap?: Map<string, string[]>;
+  onUpdateSuccess?: (updatedValues: Record<string, any> | GenericData[]) => void;
 }
 
 export function TemplateViewDialog({
@@ -97,345 +84,496 @@ export function TemplateViewDialog({
   onClose,
   templateData,
   selectedRow,
+  tableData = [],
   formCode,
   contractItemId,
   editableFieldsMap = new Map(),
-  onUpdateSuccess,
+  onUpdateSuccess
 }: TemplateViewDialogProps) {
-  /* ------------------------- local state ------------------------- */
-  const [hostStyle] = React.useState({ width: "100%", height: "100%" });
+  const [hostStyle, setHostStyle] = React.useState({
+    width: '100%',
+    height: '100%'
+  });
+
   const [isPending, setIsPending] = React.useState(false);
   const [hasChanges, setHasChanges] = React.useState(false);
-  const [currentSpread, setCurrentSpread] = React.useState<GC.Spread.Sheets.Workbook | null>(
-    null
-  );
-  const [selectedTemplateId, setSelectedTemplateId] = React.useState<string>("");
-  const [cellMappings, setCellMappings] = React.useState<
-    Array<{ attId: string; cellAddress: string; isEditable: boolean }>
-  >([]);
+  const [currentSpread, setCurrentSpread] = React.useState<any>(null);
+  const [cellMappings, setCellMappings] = React.useState<Array<{attId: string, cellAddress: string, isEditable: boolean}>>([]);
   const [isClient, setIsClient] = React.useState(false);
+  const [templateType, setTemplateType] = React.useState<'SPR_LST_SETUP' | 'SPR_ITM_LST_SETUP' | null>(null);
 
-  // Render only on client side
+  // 클라이언트 사이드에서만 렌더링되도록 보장
   React.useEffect(() => {
     setIsClient(true);
   }, []);
 
-  /* ------------------------- helpers ------------------------- */
-  // Normalize template list and keep only those with CONTENT
-  const normalizedTemplates = React.useMemo((): TemplateItem[] => {
-    if (!templateData) return [];
-
-    const list = Array.isArray(templateData)
-      ? (templateData as TemplateItem[])
-      : ([templateData] as TemplateItem[]);
-
-    return list.filter(
-      (t) => t.SPR_LST_SETUP?.CONTENT || t.SPR_ITM_LST_SETUP?.CONTENT
-    );
+  // 템플릿 데이터를 배열로 정규화하고 CONTENT가 있는 것 찾기
+  const { normalizedTemplate, detectedTemplateType } = React.useMemo(() => {
+    if (!templateData) return { normalizedTemplate: null, detectedTemplateType: null };
+    
+    let templates: TemplateItem[];
+    if (Array.isArray(templateData)) {
+      templates = templateData as TemplateItem[];
+    } else {
+      templates = [templateData as TemplateItem];
+    }
+    
+    // CONTENT가 있는 템플릿 찾기
+    for (const template of templates) {
+      if (template.SPR_LST_SETUP?.CONTENT) {
+        return { normalizedTemplate: template, detectedTemplateType: 'SPR_LST_SETUP' as const };
+      }
+      if (template.SPR_ITM_LST_SETUP?.CONTENT) {
+        return { normalizedTemplate: template, detectedTemplateType: 'SPR_ITM_LST_SETUP' as const };
+      }
+    }
+    
+    return { normalizedTemplate: null, detectedTemplateType: null };
   }, [templateData]);
 
-  // Choose currently selected template
-  const selectedTemplate = React.useMemo(() => {
-    if (!selectedTemplateId) return normalizedTemplates[0];
-    return (
-      normalizedTemplates.find((t) => t.TMPL_ID === selectedTemplateId) ||
-      normalizedTemplates[0]
-    );
-  }, [normalizedTemplates, selectedTemplateId]);
-
-  // Editable fields for the current TAG_NO
-  const editableFields = React.useMemo(() => {
-    if (!selectedRow?.TAG_NO) return [];
-    return editableFieldsMap.get(selectedRow.TAG_NO) || [];
-  }, [selectedRow?.TAG_NO, editableFieldsMap]);
-
-  const isFieldEditable = React.useCallback(
-    (attId: string) => {
-      // TAG_NO and TAG_DESC are always editable
-      if (attId === "TAG_NO" || attId === "TAG_DESC") return true;
-      if (!selectedRow?.TAG_NO) return false;
-      return editableFields.includes(attId);
-    },
-    [selectedRow?.TAG_NO, editableFields]
-  );
-
-  /** Convert a cell address like "M1" into {row:0,col:12}. */
-  const parseCellAddress = (addr: string): { row: number; col: number } | null => {
-    if (!addr) return null;
-    const match = addr.match(/^([A-Z]+)(\d+)$/);
+  // 템플릿 타입 설정
+  React.useEffect(() => {
+    setTemplateType(detectedTemplateType);
+  }, [detectedTemplateType]);
+
+  // 필드가 편집 가능한지 판별하는 함수
+  const isFieldEditable = React.useCallback((attId: string) => {
+    // TAG_NO와 TAG_DESC는 기본적으로 편집 가능
+    if (attId === "TAG_NO" || attId === "TAG_DESC") {
+      return true;
+    }
+    
+    // SPR_ITM_LST_SETUP인 경우 selectedRow.shi 확인
+    if (templateType === 'SPR_ITM_LST_SETUP' && selectedRow) {
+      return selectedRow.shi !== true;
+    }
+    
+    // SPR_LST_SETUP인 경우는 기본적으로 편집 가능 (개별 행의 shi 상태는 저장시 확인)
+    return true;
+  }, [templateType, selectedRow]);
+
+  // 편집 가능한 필드 개수 계산
+  const editableFieldsCount = React.useMemo(() => {
+    return cellMappings.filter(m => m.isEditable).length;
+  }, [cellMappings]);
+
+  // 셀 주소를 행과 열로 변환하는 함수
+  const parseCellAddress = (address: string): {row: number, col: number} | null => {
+    if (!address || address.trim() === "") return null;
+    
+    const match = address.match(/^([A-Z]+)(\d+)$/);
     if (!match) return null;
+    
     const [, colStr, rowStr] = match;
+    
     let col = 0;
     for (let i = 0; i < colStr.length; i++) {
       col = col * 26 + (colStr.charCodeAt(i) - 65 + 1);
     }
     col -= 1;
-    const row = parseInt(rowStr, 10) - 1;
+    
+    const row = parseInt(rowStr) - 1;
+    
     return { row, col };
   };
 
-  // Auto‑select first template
-  React.useEffect(() => {
-    if (normalizedTemplates.length && !selectedTemplateId) {
-      setSelectedTemplateId(normalizedTemplates[0].TMPL_ID);
-    }
-  }, [normalizedTemplates, selectedTemplateId]);
-
-  /* ------------------------- init spread ------------------------- */
-  const initSpread = React.useCallback(
-    (spread: GC.Spread.Sheets.Workbook | undefined) => {
-      if (!spread || !selectedTemplate || !selectedRow) return;
+  const initSpread = React.useCallback((spread: any) => {
+    if (!spread || !normalizedTemplate) return;
 
+    try {
       setCurrentSpread(spread);
       setHasChanges(false);
 
-      // Pick content JSON and data‑sheet mapping
-      const contentJson =
-        selectedTemplate.SPR_LST_SETUP?.CONTENT ??
-        selectedTemplate.SPR_ITM_LST_SETUP?.CONTENT;
-      const dataSheets =
-        selectedTemplate.SPR_LST_SETUP?.DATA_SHEETS ??
-        selectedTemplate.SPR_ITM_LST_SETUP?.DATA_SHEETS;
-      if (!contentJson) return;
-
-      // Prepare shared styles once
-      const editableStyle = new GC.Spread.Sheets.Style();
-      editableStyle.backColor = "#f0fdf4";
-      editableStyle.locked = false;
+      // 템플릿 타입에 따라 CONTENT와 DATA_SHEETS 가져오기
+      let contentJson = null;
+      let dataSheets = null;
+      
+      if (templateType === 'SPR_LST_SETUP') {
+        contentJson = normalizedTemplate.SPR_LST_SETUP.CONTENT;
+        dataSheets = normalizedTemplate.SPR_LST_SETUP.DATA_SHEETS;
+        console.log('Using SPR_LST_SETUP.CONTENT for template:', normalizedTemplate.NAME);
+      } else if (templateType === 'SPR_ITM_LST_SETUP') {
+        contentJson = normalizedTemplate.SPR_ITM_LST_SETUP.CONTENT;
+        dataSheets = normalizedTemplate.SPR_ITM_LST_SETUP.DATA_SHEETS;
+        console.log('Using SPR_ITM_LST_SETUP.CONTENT for template:', normalizedTemplate.NAME);
+      }
 
-      const readOnlyStyle = new GC.Spread.Sheets.Style();
-      readOnlyStyle.backColor = "#f9fafb";
-      readOnlyStyle.foreColor = "#6b7280";
-      readOnlyStyle.locked = true;
+      if (!contentJson) {
+        console.warn('No CONTENT found in template:', normalizedTemplate.NAME);
+        return;
+      }
 
-      const jsonObj = typeof contentJson === "string" ? JSON.parse(contentJson) : contentJson;
+      console.log(`Loading template content for: ${normalizedTemplate.NAME} (Type: ${templateType})`);
+      
+      const jsonData = typeof contentJson === 'string' 
+        ? JSON.parse(contentJson) 
+        : contentJson;
 
-      const sheet = spread.getActiveSheet();
+      // 렌더링 일시 중단
+      spread.suspendPaint();
 
-      /* -------- batch load + style -------- */
-      sheet.suspendPaint();
-      sheet.suspendCalcService(true);
       try {
-        spread.fromJSON(jsonObj);
-        sheet.options.isProtected = false;
-
-        const mappings: Array<{ attId: string; cellAddress: string; isEditable: boolean }> = [];
-
-        if (dataSheets?.length) {
-          dataSheets.forEach((ds) => {
-            ds.MAP_CELL_ATT?.forEach(({ ATT_ID, IN }) => {
-              if (!IN) return;
-              const pos = parseCellAddress(IN);
-              if (!pos) return;
-              const editable = isFieldEditable(ATT_ID);
-              mappings.push({ attId: ATT_ID, cellAddress: IN, isEditable: editable });
-            });
+        // fromJSON으로 템플릿 구조 로드
+        spread.fromJSON(jsonData);
+
+        // 활성 시트 가져오기
+        const activeSheet = spread.getActiveSheet();
+        
+        // 시트 보호 먼저 해제
+        activeSheet.options.isProtected = false;
+
+        // MAP_CELL_ATT 정보를 사용해서 데이터 매핑
+        if (dataSheets && dataSheets.length > 0) {
+          const mappings: Array<{attId: string, cellAddress: string, isEditable: boolean}> = [];
+          
+          dataSheets.forEach(dataSheet => {
+            if (dataSheet.MAP_CELL_ATT) {
+              dataSheet.MAP_CELL_ATT.forEach(mapping => {
+                const { ATT_ID, IN } = mapping;
+                
+                if (IN && IN.trim() !== "") {
+                  const cellPos = parseCellAddress(IN);
+                  if (cellPos) {
+                    const isEditable = isFieldEditable(ATT_ID);
+                    mappings.push({
+                      attId: ATT_ID,
+                      cellAddress: IN,
+                      isEditable: isEditable
+                    });
+                    
+                    // 템플릿 타입에 따라 다른 데이터 처리
+                    if (templateType === 'SPR_ITM_LST_SETUP' && selectedRow) {
+                      // 단일 행 처리 (기존 로직)
+                      const cell = activeSheet.getCell(cellPos.row, cellPos.col);
+                      const value = selectedRow[ATT_ID];
+                      if (value !== undefined && value !== null) {
+                        cell.value(value);
+                      }
+                      
+                      // 스타일 적용
+                      cell.locked(!isEditable);
+                      const existingStyle = activeSheet.getStyle(cellPos.row, cellPos.col);
+                      const newStyle = existingStyle ? Object.assign(new GC.Spread.Sheets.Style(), existingStyle) : new GC.Spread.Sheets.Style();
+                      
+                      if (isEditable) {
+                        newStyle.backColor = "#f0fdf4";
+                      } else {
+                        newStyle.backColor = "#f9fafb";
+                        newStyle.foreColor = "#6b7280";
+                      }
+                      
+                      activeSheet.setStyle(cellPos.row, cellPos.col, newStyle);
+                      
+                    } else if (templateType === 'SPR_LST_SETUP' && tableData.length > 0) {
+                      // 복수 행 처리 - 첫 번째 행부터 시작해서 아래로 채움
+                      tableData.forEach((rowData, index) => {
+                        const targetRow = cellPos.row + index;
+                        const cell = activeSheet.getCell(targetRow, cellPos.col);
+                        const value = rowData[ATT_ID];
+                        
+                        if (value !== undefined && value !== null) {
+                          cell.value(value);
+                        }
+                        
+                        // 개별 행의 편집 가능 여부 확인 (shi 필드 기준)
+                        const rowEditable = isEditable && (rowData.shi !== true);
+                        cell.locked(!rowEditable);
+                        
+                        // 스타일 적용
+                        const existingStyle = activeSheet.getStyle(targetRow, cellPos.col);
+                        const newStyle = existingStyle ? Object.assign(new GC.Spread.Sheets.Style(), existingStyle) : new GC.Spread.Sheets.Style();
+                        
+                        if (rowEditable) {
+                          newStyle.backColor = "#f0fdf4";
+                        } else {
+                          newStyle.backColor = "#f9fafb";
+                          newStyle.foreColor = "#6b7280";
+                        }
+                        
+                        activeSheet.setStyle(targetRow, cellPos.col, newStyle);
+                      });
+                    }
+                    
+                    console.log(`Mapped ${ATT_ID} to cell ${IN} - ${isEditable ? 'Editable' : 'Read-only'}`);
+                  }
+                }
+              });
+            }
+          });
+          
+          setCellMappings(mappings);
+          
+          // 시트 보호 설정
+          activeSheet.options.isProtected = true;
+          activeSheet.options.protectionOptions = {
+            allowSelectLockedCells: true,
+            allowSelectUnlockedCells: true,
+            allowSort: false,
+            allowFilter: false,
+            allowEditObjects: false,
+            allowResizeRows: false,
+            allowResizeColumns: false
+          };
+
+          // 이벤트 리스너 추가
+          activeSheet.bind(GC.Spread.Sheets.Events.CellChanged, (event: any, info: any) => {
+            console.log('Cell changed:', info);
+            setHasChanges(true);
           });
-        }
 
-        // Apply values + style in chunks for large templates
-        const CHUNK = 500;
-        let idx = 0;
-        const applyChunk = () => {
-          const end = Math.min(idx + CHUNK, mappings.length);
-          for (; idx < end; idx++) {
-            const { attId, cellAddress, isEditable } = mappings[idx];
-            const pos = parseCellAddress(cellAddress)!;
-            if (selectedRow[attId] !== undefined && selectedRow[attId] !== null) {
-              sheet.setValue(pos.row, pos.col, selectedRow[attId]);
-            }
-            sheet.setStyle(pos.row, pos.col, isEditable ? editableStyle : readOnlyStyle);
-          }
-          if (idx < mappings.length) {
-            requestAnimationFrame(applyChunk);
-          } else {
-            // enable protection & events after styling done
-            sheet.options.isProtected = true;
-            sheet.options.protectionOptions = {
-              allowSelectLockedCells: true,
-              allowSelectUnlockedCells: true,
-            } as any;
-
-            // Cell/value change events
-            sheet.bind(GC.Spread.Sheets.Events.ValueChanged, () => setHasChanges(true));
-            sheet.bind(GC.Spread.Sheets.Events.CellChanged, () => setHasChanges(true));
-
-            // Prevent editing read‑only fields
-            sheet.bind(
-              GC.Spread.Sheets.Events.EditStarting,
-              (event: any, info: any) => {
-                const map = mappings.find((m) => {
-                  const pos = parseCellAddress(m.cellAddress);
-                  return pos && pos.row === info.row && pos.col === info.col;
-                });
-                if (map && !map.isEditable) {
-                  toast.warning(`${map.attId} field is read‑only`);
+          activeSheet.bind(GC.Spread.Sheets.Events.ValueChanged, (event: any, info: any) => {
+            console.log('Value changed:', info);
+            setHasChanges(true);
+          });
+
+          // 편집 시작 시 읽기 전용 셀 확인
+          activeSheet.bind(GC.Spread.Sheets.Events.EditStarting, (event: any, info: any) => {
+            const mapping = mappings.find(m => {
+              const cellPos = parseCellAddress(m.cellAddress);
+              return cellPos && cellPos.row <= info.row && cellPos.col === info.col;
+            });
+            
+            if (mapping) {
+              // SPR_LST_SETUP인 경우 해당 행의 데이터에서 shi 확인
+              if (templateType === 'SPR_LST_SETUP') {
+                const dataRowIndex = info.row - parseCellAddress(mapping.cellAddress)!.row;
+                const rowData = tableData[dataRowIndex];
+                if (rowData && rowData.shi === true) {
+                  toast.warning(`Row ${dataRowIndex + 1}: ${mapping.attId} field is read-only (SHI mode)`);
                   info.cancel = true;
+                  return;
                 }
               }
-            );
-
-            setCellMappings(mappings);
-            sheet.resumeCalcService(false);
-            sheet.resumePaint();
-          }
-        };
-        applyChunk();
-      } catch (err) {
-        console.error(err);
-        toast.error("Failed to load template");
-        sheet.resumeCalcService(false);
-        sheet.resumePaint();
+              
+              if (!mapping.isEditable) {
+                toast.warning(`${mapping.attId} field is read-only`);
+                info.cancel = true;
+              }
+            }
+          });
+        }
+      } finally {
+        spread.resumePaint();
       }
-    },
-    [selectedTemplate, selectedRow, isFieldEditable]
-  );
 
-  /* ------------------------- handlers ------------------------- */
-  const handleTemplateChange = (id: string) => {
-    setSelectedTemplateId(id);
-    setHasChanges(false);
-    if (currentSpread) {
-      // re‑init after a short tick so component remounts SpreadSheets
-      setTimeout(() => initSpread(currentSpread), 50);
+    } catch (error) {
+      console.error('Error initializing spread:', error);
+      toast.error('Failed to load template');
+      if (spread && spread.resumePaint) {
+        spread.resumePaint();
+      }
     }
-  };
+  }, [normalizedTemplate, templateType, selectedRow, tableData, isFieldEditable]);
 
+  // 변경사항 저장 함수
   const handleSaveChanges = React.useCallback(async () => {
-    if (!currentSpread || !hasChanges || !selectedRow) {
+    if (!currentSpread || !hasChanges) {
       toast.info("No changes to save");
       return;
     }
 
-    setIsPending(true);
-
     try {
-      const sheet = currentSpread.getActiveSheet();
-      const payload: Record<string, any> = { ...selectedRow };
+      setIsPending(true);
+
+      const activeSheet = currentSpread.getActiveSheet();
 
-      cellMappings.forEach((m) => {
-        if (m.isEditable) {
-          const pos = parseCellAddress(m.cellAddress);
-          if (pos) payload[m.attId] = sheet.getValue(pos.row, pos.col);
+      if (templateType === 'SPR_ITM_LST_SETUP' && selectedRow) {
+        // 단일 행 저장 (기존 로직)
+        const dataToSave = { ...selectedRow };
+
+        cellMappings.forEach(mapping => {
+          if (mapping.isEditable) {
+            const cellPos = parseCellAddress(mapping.cellAddress);
+            if (cellPos) {
+              const cellValue = activeSheet.getValue(cellPos.row, cellPos.col);
+              dataToSave[mapping.attId] = cellValue;
+            }
+          }
+        });
+
+        dataToSave.TAG_NO = selectedRow.TAG_NO;
+
+        const { success, message } = await updateFormDataInDB(
+          formCode,
+          contractItemId,
+          dataToSave
+        );
+
+        if (!success) {
+          toast.error(message);
+          return;
         }
-      });
 
-      payload.TAG_NO = selectedRow.TAG_NO; // never change TAG_NO
+        toast.success("Changes saved successfully!");
+        onUpdateSuccess?.(dataToSave);
+
+      } else if (templateType === 'SPR_LST_SETUP' && tableData.length > 0) {
+        // 복수 행 저장
+        const updatedRows: GenericData[] = [];
+        let saveCount = 0;
+
+        for (let i = 0; i < tableData.length; i++) {
+          const originalRow = tableData[i];
+          const dataToSave = { ...originalRow };
+          let hasRowChanges = false;
+
+          // 각 매핑에 대해 해당 행의 값 확인
+          cellMappings.forEach(mapping => {
+            if (mapping.isEditable && originalRow.shi !== true) { // shi가 true인 행은 편집 불가
+              const cellPos = parseCellAddress(mapping.cellAddress);
+              if (cellPos) {
+                const targetRow = cellPos.row + i;
+                const cellValue = activeSheet.getValue(targetRow, cellPos.col);
+                
+                // 값이 변경되었는지 확인
+                if (cellValue !== originalRow[mapping.attId]) {
+                  dataToSave[mapping.attId] = cellValue;
+                  hasRowChanges = true;
+                }
+              }
+            }
+          });
 
-      const { success, message } = await updateFormDataInDB(
-        formCode,
-        contractItemId,
-        payload
-      );
+          // 변경사항이 있는 행만 저장
+          if (hasRowChanges) {
+            dataToSave.TAG_NO = originalRow.TAG_NO; // TAG_NO는 절대 변경되지 않도록
 
-      if (!success) {
-        toast.error(message);
-        return;
+            const { success } = await updateFormDataInDB(
+              formCode,
+              contractItemId,
+              dataToSave
+            );
+
+            if (success) {
+              updatedRows.push(dataToSave);
+              saveCount++;
+            }
+          } else {
+            updatedRows.push(originalRow); // 변경사항이 없으면 원본 유지
+          }
+        }
+
+        if (saveCount > 0) {
+          toast.success(`${saveCount} rows saved successfully!`);
+          onUpdateSuccess?.(updatedRows);
+        } else {
+          toast.info("No changes to save");
+        }
       }
 
-      toast.success("Changes saved successfully!");
-      onUpdateSuccess?.({ ...selectedRow, ...payload });
       setHasChanges(false);
-    } catch (err) {
-      console.error(err);
+
+    } catch (error) {
+      console.error("Error saving changes:", error);
       toast.error("An unexpected error occurred while saving");
     } finally {
       setIsPending(false);
     }
-  }, [currentSpread, hasChanges, selectedRow, cellMappings, formCode, contractItemId, onUpdateSuccess]);
+  }, [currentSpread, hasChanges, templateType, selectedRow, tableData, formCode, contractItemId, onUpdateSuccess, cellMappings]);
 
-  /* ------------------------- render ------------------------- */
   if (!isOpen) return null;
 
+  // 데이터 유효성 검사
+  const isDataValid = templateType === 'SPR_ITM_LST_SETUP' ? !!selectedRow : tableData.length > 0;
+  const dataCount = templateType === 'SPR_ITM_LST_SETUP' ? 1 : tableData.length;
+
   return (
     <Dialog open={isOpen} onOpenChange={onClose}>
-      <DialogContent className="w-[80%] max-w-none h-[80vh] flex flex-col" style={{ maxWidth: "80vw" }}>
+      <DialogContent 
+        className="w-[80%] max-w-none h-[80vh] flex flex-col" 
+        style={{maxWidth:"80vw"}}
+      >
         <DialogHeader className="flex-shrink-0">
-          <DialogTitle>SEDP Template – {formCode}</DialogTitle>
+          <DialogTitle>SEDP Template - {formCode}</DialogTitle>
           <DialogDescription>
-            {selectedRow && `Selected TAG_NO: ${selectedRow.TAG_NO || "N/A"}`}
-            {hasChanges && <span className="ml-2 text-orange-600 font-medium">• Unsaved changes</span>}
+            {templateType && (
+              <span className="font-medium text-blue-600">
+                Template Type: {templateType === 'SPR_LST_SETUP' ? 'List View' : 'Item View'}
+              </span>
+            )}
+            {templateType === 'SPR_ITM_LST_SETUP' && selectedRow && (
+              <span className="ml-2">• Selected TAG_NO: {selectedRow.TAG_NO || 'N/A'}</span>
+            )}
+            {templateType === 'SPR_LST_SETUP' && (
+              <span className="ml-2">• {dataCount} rows</span>
+            )}
+            {hasChanges && (
+              <span className="ml-2 text-orange-600 font-medium">
+                • Unsaved changes
+              </span>
+            )}
             <br />
             <div className="flex items-center gap-4 mt-2">
               <span className="text-xs text-muted-foreground">
-                <span className="inline-block w-3 h-3 bg-green-100 border border-green-400 mr-1" />
+                <span className="inline-block w-3 h-3 bg-green-100 border border-green-400 mr-1"></span>
                 Editable fields
               </span>
               <span className="text-xs text-muted-foreground">
-                <span className="inline-block w-3 h-3 bg-gray-100 border border-gray-300 mr-1" />
-                Read‑only fields
+                <span className="inline-block w-3 h-3 bg-gray-100 border border-gray-300 mr-1"></span>
+                Read-only fields
               </span>
-              {!!cellMappings.length && (
+              {cellMappings.length > 0 && (
                 <span className="text-xs text-blue-600">
-                  {cellMappings.filter((m) => m.isEditable).length} of {cellMappings.length} fields editable
+                  {editableFieldsCount} of {cellMappings.length} fields editable
                 </span>
               )}
             </div>
           </DialogDescription>
         </DialogHeader>
-
-        {/* Template selector */}
-        {normalizedTemplates.length > 1 && (
-          <div className="flex-shrink-0 px-4 py-2 border-b">
-            <div className="flex items-center gap-2">
-              <label className="text-sm font-medium">Template:</label>
-              <Select value={selectedTemplateId} onValueChange={handleTemplateChange}>
-                <SelectTrigger className="w-64">
-                  <SelectValue placeholder="Select a template" />
-                </SelectTrigger>
-                <SelectContent>
-                  {normalizedTemplates.map((t) => (
-                    <SelectItem key={t.TMPL_ID} value={t.TMPL_ID}>
-                      <div className="flex flex-col">
-                        <span>{t.NAME || `Template ${t.TMPL_ID.slice(0, 8)}`}</span>
-                        <span className="text-xs text-muted-foreground">{t.TMPL_TYPE}</span>
-                      </div>
-                    </SelectItem>
-                  ))}
-                </SelectContent>
-              </Select>
-              <span className="text-xs text-muted-foreground">({normalizedTemplates.length} templates available)</span>
-            </div>
-          </div>
-        )}
-
-        {/* Spreadsheet */}
+        
+        {/* SpreadSheets 컴포넌트 영역 */}
         <div className="flex-1 overflow-hidden">
-          {selectedTemplate && isClient ? (
-            <SpreadSheets key={selectedTemplateId} workbookInitialized={initSpread} hostStyle={hostStyle} />
+          {normalizedTemplate && isClient && isDataValid ? (
+            <SpreadSheets 
+              key={`${templateType}-${normalizedTemplate.TMPL_ID}`}
+              workbookInitialized={initSpread}
+              hostStyle={hostStyle}
+            />
           ) : (
             <div className="flex items-center justify-center h-full text-muted-foreground">
               {!isClient ? (
                 <>
-                  <Loader className="mr-2 h-4 w-4 animate-spin" /> Loading...
+                  <Loader className="mr-2 h-4 w-4 animate-spin" />
+                  Loading...
                 </>
-              ) : (
+              ) : !normalizedTemplate ? (
                 "No template available"
+              ) : !isDataValid ? (
+                `No ${templateType === 'SPR_ITM_LST_SETUP' ? 'selected row' : 'data'} available`
+              ) : (
+                "Template not ready"
               )}
             </div>
           )}
         </div>
 
-        {/* footer */}
         <DialogFooter className="flex-shrink-0">
           <Button variant="outline" onClick={onClose}>
             Close
           </Button>
+          
           {hasChanges && (
-            <Button variant="default" onClick={handleSaveChanges} disabled={isPending}>
+            <Button 
+              variant="default" 
+              onClick={handleSaveChanges}
+              disabled={isPending}
+            >
               {isPending ? (
                 <>
-                  <Loader className="mr-2 h-4 w-4 animate-spin" /> Saving...
+                  <Loader className="mr-2 h-4 w-4 animate-spin" />
+                  Saving...
                 </>
               ) : (
                 <>
-                  <Save className="mr-2 h-4 w-4" /> Save Changes
+                  <Save className="mr-2 h-4 w-4" />
+                  Save Changes
                 </>
               )}
             </Button>
           )}
+          
         </DialogFooter>
       </DialogContent>
     </Dialog>
   );
-}
+}
\ No newline at end of file
-- 
cgit v1.2.3

