diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-24 11:06:32 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-24 11:06:32 +0000 |
| commit | 1dc24d48e52f2e490f5603ceb02842586ecae533 (patch) | |
| tree | 8fca2c5b5b52cc10557b5ba6e55b937ae3c57cf6 /components/form-data | |
| parent | ed0d6fcc98f671280c2ccde797b50693da88152e (diff) | |
(대표님) 정기평가 피드백 반영, 설계 피드백 반영, (최겸) 기술영업 피드백 반영
Diffstat (limited to 'components/form-data')
| -rw-r--r-- | components/form-data/add-formTag-dialog.tsx | 2 | ||||
| -rw-r--r-- | components/form-data/spreadJS-dialog.tsx | 469 | ||||
| -rw-r--r-- | components/form-data/update-form-sheet.tsx | 2 |
3 files changed, 340 insertions, 133 deletions
diff --git a/components/form-data/add-formTag-dialog.tsx b/components/form-data/add-formTag-dialog.tsx index a8e51c4d..2cd336a0 100644 --- a/components/form-data/add-formTag-dialog.tsx +++ b/components/form-data/add-formTag-dialog.tsx @@ -756,7 +756,7 @@ export function AddFormTagDialog({ title={opt.label} className="whitespace-normal py-2 break-words" > - {opt.label} + {opt.value} - {opt.label} </SelectItem> ))} </SelectContent> 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> diff --git a/components/form-data/update-form-sheet.tsx b/components/form-data/update-form-sheet.tsx index ecf42048..abc9bbf3 100644 --- a/components/form-data/update-form-sheet.tsx +++ b/components/form-data/update-form-sheet.tsx @@ -99,7 +99,7 @@ export function UpdateTagSheet({ // } // 4. editableFieldsMap 정보가 없으면 기본적으로 편집 불가 (안전한 기본값) - return false; + return true; }, []); // 읽기 전용 필드인지 판별하는 함수 (편집 가능의 반대) |
