summaryrefslogtreecommitdiff
path: root/components/form-data
diff options
context:
space:
mode:
Diffstat (limited to 'components/form-data')
-rw-r--r--components/form-data/add-formTag-dialog.tsx2
-rw-r--r--components/form-data/spreadJS-dialog.tsx469
-rw-r--r--components/form-data/update-form-sheet.tsx2
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;
}, []);
// 읽기 전용 필드인지 판별하는 함수 (편집 가능의 반대)