From 1dc24d48e52f2e490f5603ceb02842586ecae533 Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
Date: Thu, 24 Jul 2025 11:06:32 +0000
Subject: (대표님) 정기평가 피드백 반영, 설계 피드백 반영, (최겸) 기술영업
 피드백 반영
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 components/form-data/spreadJS-dialog.tsx | 469 ++++++++++++++++++++++---------
 1 file changed, 338 insertions(+), 131 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 1cf23369..7ed861c2 100644
--- a/components/form-data/spreadJS-dialog.tsx
+++ b/components/form-data/spreadJS-dialog.tsx
@@ -4,13 +4,14 @@ import * as React from "react";
 import dynamic from "next/dynamic";
 import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from "@/components/ui/dialog";
 import { Button } from "@/components/ui/button";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
 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 { Loader, Save, AlertTriangle } from "lucide-react";
 import '@mescius/spread-sheets/styles/gc.spread.sheets.excel2016colorful.css';
-import { DataTableColumnJSON } from "./form-data-table-columns";
+import { DataTableColumnJSON, ColumnType } from "./form-data-table-columns";
 
 // SpreadSheets를 동적으로 import (SSR 비활성화)
 const SpreadSheets = dynamic(
@@ -68,6 +69,14 @@ interface TemplateItem {
   };
 }
 
+interface ValidationError {
+  cellAddress: string;
+  attId: string;
+  value: any;
+  expectedType: ColumnType;
+  message: string;
+}
+
 interface TemplateViewDialogProps {
   isOpen: boolean;
   onClose: () => void;
@@ -104,15 +113,18 @@ export function TemplateViewDialog({
   const [cellMappings, setCellMappings] = React.useState<Array<{attId: string, cellAddress: string, isEditable: boolean}>>([]);
   const [isClient, setIsClient] = React.useState(false);
   const [templateType, setTemplateType] = React.useState<'SPREAD_LIST' | 'SPREAD_ITEM' | null>(null);
+  const [validationErrors, setValidationErrors] = React.useState<ValidationError[]>([]);
+  const [selectedTemplateId, setSelectedTemplateId] = React.useState<string>("");
+  const [availableTemplates, setAvailableTemplates] = React.useState<TemplateItem[]>([]);
 
   // 클라이언트 사이드에서만 렌더링되도록 보장
   React.useEffect(() => {
     setIsClient(true);
   }, []);
 
-  // 템플릿 데이터를 배열로 정규화하고 TMPL_TYPE에 따라 템플릿 타입 결정
-  const { normalizedTemplate, detectedTemplateType } = React.useMemo(() => {
-    if (!templateData) return { normalizedTemplate: null, detectedTemplateType: null };
+  // 사용 가능한 템플릿들을 필터링하고 설정
+  React.useEffect(() => {
+    if (!templateData) return;
     
     let templates: TemplateItem[];
     if (Array.isArray(templateData)) {
@@ -121,26 +133,47 @@ export function TemplateViewDialog({
       templates = [templateData as TemplateItem];
     }
     
-    // TMPL_TYPE이 SPREAD_LIST 또는 SPREAD_ITEM인 템플릿 찾기
-    for (const template of templates) {
-      if (template.TMPL_TYPE === "SPREAD_LIST" || template.TMPL_TYPE === "SPREAD_ITEM") {
-        // SPR_LST_SETUP.CONTENT 또는 SPR_ITM_LST_SETUP.CONTENT 중 하나라도 있으면 해당 템플릿 사용
-        if (template.SPR_LST_SETUP?.CONTENT || template.SPR_ITM_LST_SETUP?.CONTENT) {
-          return { 
-            normalizedTemplate: template, 
-            detectedTemplateType: template.TMPL_TYPE as 'SPREAD_LIST' | 'SPREAD_ITEM'
-          };
+    // CONTENT가 있는 템플릿들 필터링
+    const validTemplates = templates.filter(template => {
+      const hasSpreadListContent = template.SPR_LST_SETUP?.CONTENT;
+      const hasSpreadItemContent = template.SPR_ITM_LST_SETUP?.CONTENT;
+      const isValidType = template.TMPL_TYPE === "SPREAD_LIST" || template.TMPL_TYPE === "SPREAD_ITEM";
+      
+      return isValidType && (hasSpreadListContent || hasSpreadItemContent);
+    });
+    
+    setAvailableTemplates(validTemplates);
+    
+    // 첫 번째 유효한 템플릿을 기본으로 선택
+    if (validTemplates.length > 0 && !selectedTemplateId) {
+      setSelectedTemplateId(validTemplates[0].TMPL_ID);
+      setTemplateType(validTemplates[0].TMPL_TYPE as 'SPREAD_LIST' | 'SPREAD_ITEM');
+    }
+  }, [templateData, selectedTemplateId]);
+
+  // 선택된 템플릿 변경 처리
+  const handleTemplateChange = (templateId: string) => {
+    const template = availableTemplates.find(t => t.TMPL_ID === templateId);
+    if (template) {
+      setSelectedTemplateId(templateId);
+      setTemplateType(template.TMPL_TYPE as 'SPREAD_LIST' | 'SPREAD_ITEM');
+      setHasChanges(false);
+      setValidationErrors([]);
+      
+      // SpreadSheets 재초기화
+      if (currentSpread) {
+        const template = availableTemplates.find(t => t.TMPL_ID === templateId);
+        if (template) {
+          initSpread(currentSpread, template);
         }
       }
     }
-    
-    return { normalizedTemplate: null, detectedTemplateType: null };
-  }, [templateData]);
+  };
 
-  // 템플릿 타입 설정
-  React.useEffect(() => {
-    setTemplateType(detectedTemplateType);
-  }, [detectedTemplateType]);
+  // 현재 선택된 템플릿 가져오기
+  const selectedTemplate = React.useMemo(() => {
+    return availableTemplates.find(t => t.TMPL_ID === selectedTemplateId);
+  }, [availableTemplates, selectedTemplateId]);
 
   const editableFields = React.useMemo(() => {
     if (!selectedRow?.TAG_NO || !editableFieldsMap.has(selectedRow.TAG_NO)) {
@@ -150,28 +183,6 @@ export function TemplateViewDialog({
   }, [selectedRow?.TAG_NO, editableFieldsMap]);
 
   // 필드가 편집 가능한지 판별하는 함수
-  // const isFieldEditable = React.useCallback((attId: string) => {
-  //   // columnsJSON에서 해당 attId의 shi 값 확인
-  //   const columnConfig = columnsJSON.find(col => col.key === attId);
-  //   if (columnConfig?.shi === true) {
-  //     return false; // columnsJSON에서 shi가 true이면 편집 불가
-  //   }
-    
-  //   // TAG_NO와 TAG_DESC는 기본적으로 편집 가능 (columnsJSON의 shi가 false인 경우)
-  //   if (attId === "TAG_NO" || attId === "TAG_DESC") {
-  //     return true;
-  //   }
-
-  //   if (selectedRow?.TAG_NO && editableFieldsMap.has(selectedRow.TAG_NO)) {
-  //     return editableFields.includes(attId);
-  //   }
-    
-    
-      
-  //   // SPREAD_LIST인 경우는 기본적으로 편집 가능 (개별 행의 shi 상태는 저장시 확인)
-  //   return true;
-  // }, [templateType, selectedRow, columnsJSON, editableFields]);
-
   const isFieldEditable = React.useCallback((attId: string, rowData?: GenericData) => {
     // columnsJSON에서 해당 attId의 shi 값 확인
     const columnConfig = columnsJSON.find(col => col.key === attId);
@@ -183,23 +194,10 @@ export function TemplateViewDialog({
     if (attId === "TAG_NO" || attId === "TAG_DESC") {
       return true;
     }
-  
-    // SPREAD_ITEM 모드일 때는 selectedRow 사용
-    // if (templateType === 'SPREAD_ITEM' && selectedRow?.TAG_NO && editableFieldsMap.has(selectedRow.TAG_NO)) {
-    //   const editableFields = editableFieldsMap.get(selectedRow.TAG_NO) || [];
-    //   return editableFields.includes(attId);
-    // }
-    
-    // // SPREAD_LIST 모드일 때는 각 행의 데이터 사용
-    // if (templateType === 'SPREAD_LIST' && rowData?.TAG_NO && editableFieldsMap.has(rowData.TAG_NO)) {
-    //   const editableFields = editableFieldsMap.get(rowData.TAG_NO) || [];
-    //   return editableFields.includes(attId);
-    // }
     
     // SPREAD_LIST인 경우는 기본적으로 편집 가능 (개별 행의 shi 상태는 저장시 확인)
     return true;
   }, [templateType, selectedRow, columnsJSON, editableFieldsMap]);
-  
 
   // 편집 가능한 필드 개수 계산
   const editableFieldsCount = React.useMemo(() => {
@@ -226,36 +224,136 @@ export function TemplateViewDialog({
     return { row, col };
   };
 
-  const initSpread = React.useCallback((spread: any) => {
-    if (!spread || !normalizedTemplate) return;
+  // 데이터 타입 검증 함수
+  const validateCellValue = (value: any, columnType: ColumnType, options?: string[]): string | null => {
+    if (value === undefined || value === null || value === "") {
+      return null; // 빈 값은 별도 required 검증에서 처리
+    }
+
+    switch (columnType) {
+      case "NUMBER":
+        if (isNaN(Number(value))) {
+          return "Value must be a valid number";
+        }
+        break;
+      case "LIST":
+        if (options && !options.includes(String(value))) {
+          return `Value must be one of: ${options.join(", ")}`;
+        }
+        break;
+      case "STRING":
+        // STRING 타입은 대부분의 값을 허용
+        break;
+      default:
+        // 커스텀 타입의 경우 추가 검증 로직이 필요할 수 있음
+        break;
+    }
+    
+    return null;
+  };
+
+  // 전체 데이터 검증 함수
+  const validateAllData = React.useCallback(() => {
+    if (!currentSpread || !selectedTemplate) return [];
+
+    const activeSheet = currentSpread.getActiveSheet();
+    const errors: ValidationError[] = [];
+
+    cellMappings.forEach(mapping => {
+      const columnConfig = columnsJSON.find(col => col.key === mapping.attId);
+      if (!columnConfig) return;
+
+      const cellPos = parseCellAddress(mapping.cellAddress);
+      if (!cellPos) return;
+
+      if (templateType === 'SPREAD_ITEM') {
+        // 단일 행 검증
+        const cellValue = activeSheet.getValue(cellPos.row, cellPos.col);
+        const errorMessage = validateCellValue(cellValue, columnConfig.type, columnConfig.options);
+        
+        if (errorMessage) {
+          errors.push({
+            cellAddress: mapping.cellAddress,
+            attId: mapping.attId,
+            value: cellValue,
+            expectedType: columnConfig.type,
+            message: errorMessage
+          });
+        }
+      } else if (templateType === 'SPREAD_LIST') {
+        // 복수 행 검증
+        for (let i = 0; i < tableData.length; i++) {
+          const targetRow = cellPos.row + i;
+          const cellValue = activeSheet.getValue(targetRow, cellPos.col);
+          const errorMessage = validateCellValue(cellValue, columnConfig.type, columnConfig.options);
+          
+          if (errorMessage) {
+            errors.push({
+              cellAddress: `${String.fromCharCode(65 + cellPos.col)}${targetRow + 1}`,
+              attId: mapping.attId,
+              value: cellValue,
+              expectedType: columnConfig.type,
+              message: errorMessage
+            });
+          }
+        }
+      }
+    });
+
+    setValidationErrors(errors);
+    return errors;
+  }, [currentSpread, selectedTemplate, cellMappings, columnsJSON, templateType, tableData]);
+
+  // LIST 타입 컬럼에 드롭다운 설정
+  const setupListValidation = React.useCallback((activeSheet: any, cellPos: {row: number, col: number}, options: string[], rowCount: number = 1) => {
+    // ComboBox 셀 타입 생성
+    const comboBoxCellType = new GC.Spread.Sheets.CellTypes.ComboBox();
+    comboBoxCellType.items(options);
+    comboBoxCellType.editorValueType(GC.Spread.Sheets.CellTypes.EditorValueType.text);
+
+    // 단일 셀 또는 범위에 적용
+    for (let i = 0; i < rowCount; i++) {
+      const targetRow = cellPos.row + i;
+      activeSheet.setCellType(targetRow, cellPos.col, comboBoxCellType);
+      
+      // 추가로 데이터 검증도 설정
+      const validator = GC.Spread.Sheets.DataValidation.createListValidator(options);
+      activeSheet.setDataValidator(targetRow, cellPos.col, validator);
+    }
+  }, []);
+
+  const initSpread = React.useCallback((spread: any, template?: TemplateItem) => {
+    const workingTemplate = template || selectedTemplate;
+    if (!spread || !workingTemplate) return;
 
     try {
       setCurrentSpread(spread);
       setHasChanges(false);
+      setValidationErrors([]);
 
       // SPR_LST_SETUP.CONTENT와 SPR_ITM_LST_SETUP.CONTENT 중에서 값이 있는 것을 찾아서 사용
       let contentJson = null;
       let dataSheets = null;
       
       // SPR_LST_SETUP.CONTENT가 있으면 우선 사용
-      if (normalizedTemplate.SPR_LST_SETUP?.CONTENT) {
-        contentJson = normalizedTemplate.SPR_LST_SETUP.CONTENT;
-        dataSheets = normalizedTemplate.SPR_LST_SETUP.DATA_SHEETS;
-        console.log('Using SPR_LST_SETUP.CONTENT for template:', normalizedTemplate.NAME, '(TMPL_TYPE:', normalizedTemplate.TMPL_TYPE, ')');
+      if (workingTemplate.SPR_LST_SETUP?.CONTENT) {
+        contentJson = workingTemplate.SPR_LST_SETUP.CONTENT;
+        dataSheets = workingTemplate.SPR_LST_SETUP.DATA_SHEETS;
+        console.log('Using SPR_LST_SETUP.CONTENT for template:', workingTemplate.NAME, '(TMPL_TYPE:', workingTemplate.TMPL_TYPE, ')');
       } 
       // SPR_ITM_LST_SETUP.CONTENT가 있으면 사용
-      else if (normalizedTemplate.SPR_ITM_LST_SETUP?.CONTENT) {
-        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, '(TMPL_TYPE:', normalizedTemplate.TMPL_TYPE, ')');
+      else if (workingTemplate.SPR_ITM_LST_SETUP?.CONTENT) {
+        contentJson = workingTemplate.SPR_ITM_LST_SETUP.CONTENT;
+        dataSheets = workingTemplate.SPR_ITM_LST_SETUP.DATA_SHEETS;
+        console.log('Using SPR_ITM_LST_SETUP.CONTENT for template:', workingTemplate.NAME, '(TMPL_TYPE:', workingTemplate.TMPL_TYPE, ')');
       }
 
       if (!contentJson) {
-        console.warn('No CONTENT found in template:', normalizedTemplate.NAME);
+        console.warn('No CONTENT found in template:', workingTemplate.NAME);
         return;
       }
 
-      console.log(`Loading template content for: ${normalizedTemplate.NAME} (Type: ${normalizedTemplate.TMPL_TYPE})`);
+      console.log(`Loading template content for: ${workingTemplate.NAME} (Type: ${workingTemplate.TMPL_TYPE})`);
       
       const jsonData = typeof contentJson === 'string' 
         ? JSON.parse(contentJson) 
@@ -287,6 +385,8 @@ export function TemplateViewDialog({
                   const cellPos = parseCellAddress(IN);
                   if (cellPos) {
                     const isEditable = isFieldEditable(ATT_ID);
+                    const columnConfig = columnsJSON.find(col => col.key === ATT_ID);
+                    
                     mappings.push({
                       attId: ATT_ID,
                       cellAddress: IN,
@@ -306,6 +406,11 @@ export function TemplateViewDialog({
                         cell.value(null);
                       }
 
+                      // LIST 타입 컬럼에 드롭다운 설정
+                      if (columnConfig?.type === "LIST" && columnConfig.options && isEditable) {
+                        setupListValidation(activeSheet, cellPos, columnConfig.options, 1);
+                      }
+
                       // 스타일 적용
                       cell.locked(!isEditable);
                       const existingStyle = activeSheet.getStyle(cellPos.row, cellPos.col);
@@ -322,6 +427,19 @@ export function TemplateViewDialog({
                       
                     } else if (templateType === 'SPREAD_LIST' && tableData.length > 0) {
                       // 복수 행 처리 - 첫 번째 행부터 시작해서 아래로 채움
+                      
+                      // LIST 타입 컬럼에 드롭다운 설정 (모든 행에 대해)
+                      if (columnConfig?.type === "LIST" && columnConfig.options && isEditable) {
+                        setupListValidation(activeSheet, cellPos, columnConfig.options, tableData.length);
+                      }
+
+                      // 필요한 경우 행 추가 (tableData 길이만큼 충분히 확보)
+                      const currentRowCount = activeSheet.getRowCount();
+                      const requiredRowCount = cellPos.row + tableData.length;
+                      if (requiredRowCount > currentRowCount) {
+                        activeSheet.setRowCount(requiredRowCount + 10); // 여유분 추가
+                      }
+
                       tableData.forEach((rowData, index) => {
                         const targetRow = cellPos.row + index;
                         const cell = activeSheet.getCell(targetRow, cellPos.col);
@@ -335,11 +453,7 @@ export function TemplateViewDialog({
                           cell.value(null);
                         }
                         
-                        // 개별 행의 편집 가능 여부 확인 (행의 shi + columnsJSON의 shi 모두 확인)
-                        // const columnConfig = columnsJSON.find(col => col.key === ATT_ID);
-                        // const cellEditable = columnConfig?.shi !== true; // columnsJSON에서 shi가 true이면 편집 불가
-                        
-                        const cellEditable = isFieldEditable(ATT_ID, rowData); // 각 행의 데이터를 전달
+                        const cellEditable = isFieldEditable(ATT_ID, rowData);
                         cell.locked(!cellEditable);
                         
                         // 스타일 적용
@@ -389,6 +503,12 @@ export function TemplateViewDialog({
             setHasChanges(true);
           });
 
+          // 복사 붙여넣기 이벤트 추가
+          activeSheet.bind(GC.Spread.Sheets.Events.ClipboardPasted, (event: any, info: any) => {
+            console.log('Clipboard pasted:', info);
+            setHasChanges(true);
+          });
+
           // 편집 시작 시 읽기 전용 셀 확인
           activeSheet.bind(GC.Spread.Sheets.Events.EditStarting, (event: any, info: any) => {
             const mapping = mappings.find(m => {
@@ -422,6 +542,38 @@ export function TemplateViewDialog({
               }
             }
           });
+
+          // 편집 종료 시 데이터 검증
+          activeSheet.bind(GC.Spread.Sheets.Events.EditEnded, (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) {
+              const columnConfig = columnsJSON.find(col => col.key === mapping.attId);
+              if (columnConfig) {
+                const cellValue = activeSheet.getValue(info.row, info.col);
+                const errorMessage = validateCellValue(cellValue, columnConfig.type, columnConfig.options);
+                
+                if (errorMessage) {
+                  toast.warning(`Invalid value in ${mapping.attId}: ${errorMessage}`);
+                  // 스타일을 오류 상태로 변경
+                  const errorStyle = new GC.Spread.Sheets.Style();
+                  errorStyle.backColor = "#fef2f2";
+                  errorStyle.foreColor = "#dc2626";
+                  activeSheet.setStyle(info.row, info.col, errorStyle);
+                } else {
+                  // 정상 스타일로 복원
+                  const cellEditable = isFieldEditable(mapping.attId);
+                  const normalStyle = new GC.Spread.Sheets.Style();
+                  normalStyle.backColor = cellEditable ? "#f0fdf4" : "#f9fafb";
+                  normalStyle.foreColor = cellEditable ? "#000000" : "#6b7280";
+                  activeSheet.setStyle(info.row, info.col, normalStyle);
+                }
+              }
+            }
+          });
         }
       } finally {
         spread.resumePaint();
@@ -434,7 +586,7 @@ export function TemplateViewDialog({
         spread.resumePaint();
       }
     }
-  }, [normalizedTemplate, templateType, selectedRow, tableData, isFieldEditable, columnsJSON]);
+  }, [selectedTemplate, templateType, selectedRow, tableData, isFieldEditable, columnsJSON, setupListValidation, validateCellValue]);
 
   // 변경사항 저장 함수
   const handleSaveChanges = React.useCallback(async () => {
@@ -443,6 +595,13 @@ export function TemplateViewDialog({
       return;
     }
 
+    // 저장 전 데이터 검증
+    const errors = validateAllData();
+    if (errors.length > 0) {
+      toast.error(`Cannot save: ${errors.length} validation errors found. Please fix them first.`);
+      return;
+    }
+
     try {
       setIsPending(true);
 
@@ -538,6 +697,7 @@ export function TemplateViewDialog({
       }
 
       setHasChanges(false);
+      setValidationErrors([]);
 
     } catch (error) {
       console.error("Error saving changes:", error);
@@ -545,7 +705,7 @@ export function TemplateViewDialog({
     } finally {
       setIsPending(false);
     }
-  }, [currentSpread, hasChanges, templateType, selectedRow, tableData, formCode, contractItemId, onUpdateSuccess, cellMappings, columnsJSON]);
+  }, [currentSpread, hasChanges, templateType, selectedRow, tableData, formCode, contractItemId, onUpdateSuccess, cellMappings, columnsJSON, validateAllData]);
 
   if (!isOpen) return null;
 
@@ -562,46 +722,81 @@ export function TemplateViewDialog({
         <DialogHeader className="flex-shrink-0">
           <DialogTitle>SEDP Template - {formCode}</DialogTitle>
           <DialogDescription>
-            {normalizedTemplate && (
-              <span className="font-medium text-blue-600">
-                Template Type: {normalizedTemplate.TMPL_TYPE === 'SPREAD_LIST' ? 'List View (SPREAD_LIST)' : 'Item View (SPREAD_ITEM)'}
-              </span>
-            )}
-            {templateType === 'SPREAD_ITEM' && selectedRow && (
-              <span className="ml-2">• Selected TAG_NO: {selectedRow.TAG_NO || 'N/A'}</span>
-            )}
-            {templateType === 'SPREAD_LIST' && (
-              <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>
-                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"></span>
-                Read-only fields
-              </span>
-              {cellMappings.length > 0 && (
-                <span className="text-xs text-blue-600">
-                  {editableFieldsCount} of {cellMappings.length} fields editable
-                </span>
+            <div className="space-y-3">
+              {/* 템플릿 선택 */}
+              {availableTemplates.length > 1 && (
+                <div className="flex items-center gap-4">
+                  <span className="text-sm font-medium">Template:</span>
+                  <Select value={selectedTemplateId} onValueChange={handleTemplateChange}>
+                    <SelectTrigger className="w-64">
+                      <SelectValue placeholder="Select a template" />
+                    </SelectTrigger>
+                    <SelectContent>
+                      {availableTemplates.map(template => (
+                        <SelectItem key={template.TMPL_ID} value={template.TMPL_ID}>
+                          {template.NAME} ({template.TMPL_TYPE})
+                        </SelectItem>
+                      ))}
+                    </SelectContent>
+                  </Select>
+                </div>
               )}
+
+              {/* 템플릿 정보 */}
+              {selectedTemplate && (
+                <div className="flex items-center gap-4 text-sm">
+                  <span className="font-medium text-blue-600">
+                    Template Type: {selectedTemplate.TMPL_TYPE === 'SPREAD_LIST' ? 'List View (SPREAD_LIST)' : 'Item View (SPREAD_ITEM)'}
+                  </span>
+                  {templateType === 'SPREAD_ITEM' && selectedRow && (
+                    <span>• Selected TAG_NO: {selectedRow.TAG_NO || 'N/A'}</span>
+                  )}
+                  {templateType === 'SPREAD_LIST' && (
+                    <span>• {dataCount} rows</span>
+                  )}
+                  {hasChanges && (
+                    <span className="text-orange-600 font-medium">
+                      • Unsaved changes
+                    </span>
+                  )}
+                  {validationErrors.length > 0 && (
+                    <span className="text-red-600 font-medium flex items-center">
+                      <AlertTriangle className="w-4 h-4 mr-1" />
+                      {validationErrors.length} validation errors
+                    </span>
+                  )}
+                </div>
+              )}
+
+              {/* 범례 */}
+              <div className="flex items-center gap-4 text-xs">
+                <span className="text-muted-foreground">
+                  <span className="inline-block w-3 h-3 bg-green-100 border border-green-400 mr-1"></span>
+                  Editable fields
+                </span>
+                <span className="text-muted-foreground">
+                  <span className="inline-block w-3 h-3 bg-gray-100 border border-gray-300 mr-1"></span>
+                  Read-only fields
+                </span>
+                <span className="text-muted-foreground">
+                  <span className="inline-block w-3 h-3 bg-red-100 border border-red-300 mr-1"></span>
+                  Validation errors
+                </span>
+                {cellMappings.length > 0 && (
+                  <span className="text-blue-600">
+                    {editableFieldsCount} of {cellMappings.length} fields editable
+                  </span>
+                )}
+              </div>
             </div>
           </DialogDescription>
         </DialogHeader>
         
         {/* SpreadSheets 컴포넌트 영역 */}
         <div className="flex-1 overflow-hidden">
-          {normalizedTemplate && isClient && isDataValid ? (
+          {selectedTemplate && isClient && isDataValid ? (
             <SpreadSheets 
-              key={`${normalizedTemplate.TMPL_TYPE}-${normalizedTemplate.TMPL_ID}`}
+              key={`${selectedTemplate.TMPL_TYPE}-${selectedTemplate.TMPL_ID}-${selectedTemplateId}`}
               workbookInitialized={initSpread}
               hostStyle={hostStyle}
             />
@@ -612,7 +807,7 @@ export function TemplateViewDialog({
                   <Loader className="mr-2 h-4 w-4 animate-spin" />
                   Loading...
                 </>
-              ) : !normalizedTemplate ? (
+              ) : !selectedTemplate ? (
                 "No template available"
               ) : !isDataValid ? (
                 `No ${templateType === 'SPREAD_ITEM' ? 'selected row' : 'data'} available`
@@ -624,30 +819,42 @@ export function TemplateViewDialog({
         </div>
 
         <DialogFooter className="flex-shrink-0">
-          <Button variant="outline" onClick={onClose}>
-            Close
-          </Button>
-          
-          {hasChanges && (
-            <Button 
-              variant="default" 
-              onClick={handleSaveChanges}
-              disabled={isPending}
-            >
-              {isPending ? (
-                <>
-                  <Loader className="mr-2 h-4 w-4 animate-spin" />
-                  Saving...
-                </>
-              ) : (
-                <>
-                  <Save className="mr-2 h-4 w-4" />
-                  Save Changes
-                </>
-              )}
+          <div className="flex items-center gap-2">
+            <Button variant="outline" onClick={onClose}>
+              Close
             </Button>
-          )}
-          
+            
+            {hasChanges && (
+              <Button 
+                variant="default" 
+                onClick={handleSaveChanges}
+                disabled={isPending || validationErrors.length > 0}
+              >
+                {isPending ? (
+                  <>
+                    <Loader className="mr-2 h-4 w-4 animate-spin" />
+                    Saving...
+                  </>
+                ) : (
+                  <>
+                    <Save className="mr-2 h-4 w-4" />
+                    Save Changes
+                  </>
+                )}
+              </Button>
+            )}
+
+            {validationErrors.length > 0 && (
+              <Button 
+                variant="outline" 
+                onClick={validateAllData}
+                className="text-red-600 border-red-300 hover:bg-red-50"
+              >
+                <AlertTriangle className="mr-2 h-4 w-4" />
+                Check Errors ({validationErrors.length})
+              </Button>
+            )}
+          </div>
         </DialogFooter>
       </DialogContent>
     </Dialog>
-- 
cgit v1.2.3

