From fca5e37d43543ed1e6e680a779d340b1cc4427cd Mon Sep 17 00:00:00 2001
From: joonhoekim <26rote@gmail.com>
Date: Fri, 3 Oct 2025 13:55:08 +0900
Subject: (김준회) spreadjs dialog client error 처리
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 components/form-data/spreadJS-dialog.tsx | 99 +++++++++++++++++++++-----------
 1 file changed, 65 insertions(+), 34 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 375c097c..272d99be 100644
--- a/components/form-data/spreadJS-dialog.tsx
+++ b/components/form-data/spreadJS-dialog.tsx
@@ -189,6 +189,11 @@ export function TemplateViewDialog({
   }, [columnsJSON]);
 
   const isValidTemplate = React.useCallback((template: TemplateItem): boolean => {
+    // 🔍 TMPL_ID 필수 검증 추가
+    if (!template || !template.TMPL_ID || typeof template.TMPL_ID !== 'string') {
+      console.warn('⚠️ Invalid template: missing or invalid TMPL_ID', template);
+      return false;
+    }
     return determineTemplateType(template) !== null;
   }, [determineTemplateType]);
 
@@ -270,29 +275,43 @@ export function TemplateViewDialog({
 
     if (validTemplates.length > 0 && !selectedTemplateId) {
       const firstTemplate = validTemplates[0];
-      const templateTypeToSet = determineTemplateType(firstTemplate);
-      setSelectedTemplateId(firstTemplate.TMPL_ID);
-      setTemplateType(templateTypeToSet);
+      // 🔍 TMPL_ID 검증 (isValidTemplate로 필터링했으므로 존재해야 하지만 안전장치)
+      if (firstTemplate?.TMPL_ID) {
+        const templateTypeToSet = determineTemplateType(firstTemplate);
+        setSelectedTemplateId(firstTemplate.TMPL_ID);
+        setTemplateType(templateTypeToSet);
+      } else {
+        console.error('❌ First valid template has no TMPL_ID:', firstTemplate);
+      }
     }
   }, [templateData, selectedTemplateId, isValidTemplate, determineTemplateType, columnsJSON]);
 
   const handleTemplateChange = (templateId: string) => {
-    const template = availableTemplates.find(t => t.TMPL_ID === templateId);
-    if (template) {
-      const templateTypeToSet = determineTemplateType(template);
-      setSelectedTemplateId(templateId);
-      setTemplateType(templateTypeToSet);
-      setHasChanges(false);
-      setValidationErrors([]);
-
-      if (currentSpread && template) {
-        initSpread(currentSpread, template);
-      }
+    const template = availableTemplates.find(t => t?.TMPL_ID === templateId);
+    
+    // 🔍 템플릿과 TMPL_ID 검증
+    if (!template || !template.TMPL_ID) {
+      console.error('❌ Template not found or invalid TMPL_ID:', templateId);
+      return;
+    }
+    
+    const templateTypeToSet = determineTemplateType(template);
+    setSelectedTemplateId(templateId);
+    setTemplateType(templateTypeToSet);
+    setHasChanges(false);
+    setValidationErrors([]);
+
+    if (currentSpread) {
+      initSpread(currentSpread, template);
     }
   };
 
   const selectedTemplate = React.useMemo(() => {
-    return availableTemplates.find(t => t.TMPL_ID === selectedTemplateId);
+    const found = availableTemplates.find(t => t?.TMPL_ID === selectedTemplateId);
+    if (!found && selectedTemplateId) {
+      console.warn('⚠️ Selected template not found:', selectedTemplateId);
+    }
+    return found;
   }, [availableTemplates, selectedTemplateId]);
 
   const editableFields = React.useMemo(() => {
@@ -1293,17 +1312,29 @@ export function TemplateViewDialog({
   }, [selectedTemplate, templateType, selectedRow, tableData, updateProgress, getSafeActiveSheet, createGrdListTableOptimized, setBatchValues, setBatchStyles, setupSheetProtectionAndEvents, setCellMappings, createCellStyle, isFieldEditable, columnsJSON, setupOptimizedListValidation, parseCellAddress, ensureRowCapacity, getCellAddress]);
 
   React.useEffect(() => {
-    if (!selectedTemplateId) {
+    // 🔍 안전성 검증: availableTemplates가 있고, selectedTemplateId가 없을 때만 실행
+    if (!selectedTemplateId && availableTemplates.length > 0) {
       const only = availableTemplates[0];
+      
+      // 🔍 TMPL_ID 검증
+      if (!only || !only.TMPL_ID) {
+        console.error('❌ First template has no TMPL_ID:', only);
+        return;
+      }
+
       const type = determineTemplateType(only);
+      
+      // 🔍 type이 null이 아닐 때만 진행
+      if (!type) {
+        console.warn('⚠️ Could not determine template type for:', only);
+        return;
+      }
   
       // 선택되어 있지 않다면 자동 선택
-      if (selectedTemplateId !== only.TMPL_ID) {
-        setSelectedTemplateId(only.TMPL_ID);
-        setTemplateType(type);
-      }
+      setSelectedTemplateId(only.TMPL_ID);
+      setTemplateType(type);
   
-      // 이미 스프레드가 마운트되어 있다면 즉시 초기화(선택 변경만으로도 리렌더되지만 안전하게 보강)
+      // 이미 스프레드가 마운트되어 있다면 즉시 초기화
       if (currentSpread) {
         initSpread(currentSpread, only);
       }
@@ -1313,9 +1344,7 @@ export function TemplateViewDialog({
     selectedTemplateId,
     currentSpread,
     determineTemplateType,
-    initSpread,
-    setTemplateType,
-    setSelectedTemplateId
+    initSpread
   ]);
 
   const handleSaveChanges = React.useCallback(async () => {
@@ -1491,8 +1520,8 @@ export function TemplateViewDialog({
           <DialogTitle>SEDP Template - {formCode}</DialogTitle>
           <DialogDescription>
             <div className="space-y-3">
-              {availableTemplates.length > 0 ? (
-                // 템플릿이 2개 이상일 때: Select 박스 표시
+              {availableTemplates.length > 1 ? (
+                // 🔍 템플릿이 2개 이상일 때: Select 박스 표시
                 <div className="flex items-center gap-4">
                   <span className="text-sm font-medium">Template:</span>
                   <Select value={selectedTemplateId} onValueChange={handleTemplateChange}>
@@ -1500,20 +1529,22 @@ export function TemplateViewDialog({
                       <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>
-                      ))}
+                      {availableTemplates
+                        .filter(template => template?.TMPL_ID) // 🔍 TMPL_ID가 있는 것만 표시
+                        .map(template => (
+                          <SelectItem key={template.TMPL_ID} value={template.TMPL_ID}>
+                            {template.NAME || 'Unnamed'} ({template.TMPL_TYPE || 'Unknown'})
+                          </SelectItem>
+                        ))}
                     </SelectContent>
                   </Select>
                 </div>
               ) : availableTemplates.length === 1 ? (
-                // 템플릿이 정확히 1개일 때: 템플릿 이름을 텍스트로 표시
+                // 🔍 템플릿이 정확히 1개일 때: 템플릿 이름을 텍스트로 표시
                 <div className="flex items-center gap-4">
                   <span className="text-sm font-medium">Template:</span>
                   <span className="text-sm text-blue-600 font-medium">
-                    {availableTemplates[0].NAME} ({availableTemplates[0].TMPL_TYPE})
+                    {availableTemplates[0]?.NAME || 'Unnamed'} ({availableTemplates[0]?.TMPL_TYPE || 'Unknown'})
                   </span>
                 </div>
               ) : null}  
@@ -1581,7 +1612,7 @@ export function TemplateViewDialog({
 
           {selectedTemplate && isClient && isDataValid ? (
             <SpreadSheets
-              key={`${templateType}-${selectedTemplate.TMPL_ID}-${selectedTemplateId}`}
+              key={`${templateType}-${selectedTemplate?.TMPL_ID || 'unknown'}-${selectedTemplateId}`}
               workbookInitialized={initSpread}
               hostStyle={hostStyle}
             />
-- 
cgit v1.2.3

