summaryrefslogtreecommitdiff
path: root/components/form-data
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-08-13 11:05:09 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-08-13 11:05:09 +0000
commit33be47506f0aa62b969d82521580a29e95080268 (patch)
tree6b7e232f2d78ef8775944ea085a36b3ccbce7d95 /components/form-data
parent2ac95090157c355ea1bd0b8eb1e1e5e2bd56faf4 (diff)
(대표님) 입찰, 법무검토, EDP 변경사항 대응, dolce 개선, form-data 개선, 정규업체 등록관리 추가
(최겸) pq 미사용 컴포넌트 및 페이지 제거, 파일 라우트에 pq 적용
Diffstat (limited to 'components/form-data')
-rw-r--r--components/form-data/add-formTag-dialog.tsx4
-rw-r--r--components/form-data/form-data-table.tsx25
-rw-r--r--components/form-data/sedp-compare-dialog.tsx21
-rw-r--r--components/form-data/spreadJS-dialog.tsx1157
-rw-r--r--components/form-data/update-form-sheet.tsx6
5 files changed, 671 insertions, 542 deletions
diff --git a/components/form-data/add-formTag-dialog.tsx b/components/form-data/add-formTag-dialog.tsx
index 9d80de8c..5a462d2b 100644
--- a/components/form-data/add-formTag-dialog.tsx
+++ b/components/form-data/add-formTag-dialog.tsx
@@ -95,6 +95,7 @@ interface AddFormTagDialogProps {
formCode: string;
formName?: string;
contractItemId: number;
+ packageCode: string;
open?: boolean;
onOpenChange?: (open: boolean) => void;
}
@@ -104,6 +105,7 @@ export function AddFormTagDialog({
formCode,
formName,
contractItemId,
+ packageCode,
open: externalOpen,
onOpenChange: externalOnOpenChange
}: AddFormTagDialogProps) {
@@ -361,7 +363,7 @@ export function AddFormTagDialog({
};
try {
- const res = await createTagInForm(tagData, contractItemId, formCode);
+ const res = await createTagInForm(tagData, contractItemId, formCode, packageCode);
if ("error" in res) {
failedTags.push({ tag: row.tagNo, error: res.error });
} else {
diff --git a/components/form-data/form-data-table.tsx b/components/form-data/form-data-table.tsx
index be37de7a..b2fadacf 100644
--- a/components/form-data/form-data-table.tsx
+++ b/components/form-data/form-data-table.tsx
@@ -33,6 +33,8 @@ import {
} from "lucide-react";
import { toast } from "sonner";
import {
+ getPackageCodeById,
+ getProjectById,
getProjectCodeById,
getReportTempList,
sendFormDataToSEDP,
@@ -220,6 +222,8 @@ export default function DynamicTable({
// SEDP compare dialog state
const [sedpCompareOpen, setSedpCompareOpen] = React.useState(false);
const [projectCode, setProjectCode] = React.useState<string>('');
+ const [projectType, setProjectType] = React.useState<string>('plant');
+ const [packageCode, setPackageCode] = React.useState<string>('');
// 새로 추가된 Template 다이얼로그 상태
const [templateDialogOpen, setTemplateDialogOpen] = React.useState(false);
@@ -255,12 +259,26 @@ export default function DynamicTable({
getTempCount();
}, [contractItemId, formId, tempUpDialog]);
+ React.useEffect(() => {
+ const getPackageCode = async () => {
+ try {
+ const packageCode = await getPackageCodeById(contractItemId);
+ setPackageCode(packageCode || ''); // 빈 문자열이나 다른 기본값
+ } catch (error) {
+ console.error('패키지 조회 실패:', error);
+ setPackageCode('');
+ }
+ };
+
+ getPackageCode();
+ }, [contractItemId])
// Get project code when component mounts
React.useEffect(() => {
const getProjectCode = async () => {
try {
- const code = await getProjectCodeById(projectId);
- setProjectCode(code);
+ const project = await getProjectById(projectId);
+ setProjectCode(project.code);
+ setProjectType(project.type);
} catch (error) {
console.error("Error fetching project code:", error);
toast.error("Failed to fetch project code");
@@ -911,6 +929,7 @@ export default function DynamicTable({
formCode={formCode}
formName={`Form ${formCode}`}
contractItemId={contractItemId}
+ packageCode={packageCode}
open={addTagDialogOpen}
onOpenChange={setAddTagDialogOpen}
/>
@@ -982,6 +1001,8 @@ export default function DynamicTable({
projectCode={projectCode}
formCode={formCode}
fetchTagDataFromSEDP={fetchTagDataFromSEDP}
+ projectType={projectType}
+ packageCode={packageCode}
/>
{/* Other dialogs */}
diff --git a/components/form-data/sedp-compare-dialog.tsx b/components/form-data/sedp-compare-dialog.tsx
index 1a9938bd..9a6c8098 100644
--- a/components/form-data/sedp-compare-dialog.tsx
+++ b/components/form-data/sedp-compare-dialog.tsx
@@ -22,6 +22,8 @@ interface SEDPCompareDialogProps {
projectCode: string;
formCode: string;
fetchTagDataFromSEDP: (projectCode: string, formCode: string) => Promise<any>;
+ projectType:string;
+ packageCode:string;
}
interface ComparisonResult {
@@ -67,6 +69,8 @@ export function SEDPCompareDialog({
projectCode,
formCode,
fetchTagDataFromSEDP,
+ projectType,
+ packageCode
}: SEDPCompareDialogProps) {
const params = useParams() || {}
@@ -256,7 +260,22 @@ export function SEDPCompareDialog({
// Create a map of SEDP data by TAG_NO for quick lookup
const sedpTagMap = new Map();
- sedpTagEntries.forEach((entry: any) => {
+
+ const packageCodeAttId = projectType === "ship" ? "CM3003" : "ME5074";
+
+
+ const tagEntries = sedpTagEntries.filter(entry => {
+ if (Array.isArray(entry.ATTRIBUTES)) {
+ const packageCodeAttr = entry.ATTRIBUTES.find(attr => attr.ATT_ID === packageCodeAttId);
+ if (packageCodeAttr && packageCodeAttr.VALUE === packageCode) {
+ return true;
+ }
+ }
+ return false;
+ });
+
+
+ tagEntries.forEach((entry: any) => {
const tagNo = entry.TAG_NO;
const attributesMap = new Map();
diff --git a/components/form-data/spreadJS-dialog.tsx b/components/form-data/spreadJS-dialog.tsx
index 11d37911..54a70d9d 100644
--- a/components/form-data/spreadJS-dialog.tsx
+++ b/components/form-data/spreadJS-dialog.tsx
@@ -13,7 +13,6 @@ import { Loader, Save, AlertTriangle } from "lucide-react";
import '@mescius/spread-sheets/styles/gc.spread.sheets.excel2016colorful.css';
import { DataTableColumnJSON, ColumnType } from "./form-data-table-columns";
-// SpreadSheets를 동적으로 import (SSR 비활성화)
const SpreadSheets = dynamic(
() => import("@mescius/spread-sheets-react").then(mod => mod.SpreadSheets),
{
@@ -27,7 +26,6 @@ const SpreadSheets = dynamic(
}
);
-// 라이센스 키 설정을 클라이언트에서만 실행
if (typeof window !== 'undefined' && process.env.NEXT_PUBLIC_SPREAD_LICENSE) {
GC.Spread.Sheets.LicenseKey = process.env.NEXT_PUBLIC_SPREAD_LICENSE;
}
@@ -88,8 +86,8 @@ interface TemplateViewDialogProps {
isOpen: boolean;
onClose: () => void;
templateData: TemplateItem[] | any;
- selectedRow?: GenericData; // SPREAD_ITEM용
- tableData?: GenericData[]; // SPREAD_LIST용
+ selectedRow?: GenericData;
+ tableData?: GenericData[];
formCode: string;
columnsJSON: DataTableColumnJSON[]
contractItemId: number;
@@ -97,6 +95,44 @@ interface TemplateViewDialogProps {
onUpdateSuccess?: (updatedValues: Record<string, any> | GenericData[]) => void;
}
+// 🚀 로딩 프로그레스 컴포넌트
+interface LoadingProgressProps {
+ phase: string;
+ progress: number;
+ total: number;
+ isVisible: boolean;
+}
+
+const LoadingProgress: React.FC<LoadingProgressProps> = ({ phase, progress, total, isVisible }) => {
+ const percentage = total > 0 ? Math.round((progress / total) * 100) : 0;
+
+ if (!isVisible) return null;
+
+ return (
+ <div className="absolute inset-0 bg-white/90 flex items-center justify-center z-50">
+ <div className="bg-white rounded-lg shadow-lg border p-6 min-w-[300px]">
+ <div className="flex items-center space-x-3 mb-4">
+ <Loader className="h-5 w-5 animate-spin text-blue-600" />
+ <span className="font-medium text-gray-900">Loading Template</span>
+ </div>
+
+ <div className="space-y-2">
+ <div className="text-sm text-gray-600">{phase}</div>
+ <div className="w-full bg-gray-200 rounded-full h-2">
+ <div
+ className="bg-blue-600 h-2 rounded-full transition-all duration-300 ease-out"
+ style={{ width: `${percentage}%` }}
+ />
+ </div>
+ <div className="text-xs text-gray-500 text-right">
+ {progress.toLocaleString()} / {total.toLocaleString()} ({percentage}%)
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+};
+
export function TemplateViewDialog({
isOpen,
onClose,
@@ -123,13 +159,41 @@ export function TemplateViewDialog({
const [validationErrors, setValidationErrors] = React.useState<ValidationError[]>([]);
const [selectedTemplateId, setSelectedTemplateId] = React.useState<string>("");
const [availableTemplates, setAvailableTemplates] = React.useState<TemplateItem[]>([]);
+
+ // 🆕 로딩 상태 추가
+ const [loadingProgress, setLoadingProgress] = React.useState<{
+ phase: string;
+ progress: number;
+ total: number;
+ } | null>(null);
+ const [isInitializing, setIsInitializing] = React.useState(false);
+
+ // 🔄 진행상황 업데이트 함수
+ const updateProgress = React.useCallback((phase: string, progress: number, total: number) => {
+ setLoadingProgress({ phase, progress, total });
+ }, []);
+
+ const determineTemplateType = React.useCallback((template: TemplateItem): 'SPREAD_LIST' | 'SPREAD_ITEM' | 'GRD_LIST' | null => {
+ if (template.TMPL_TYPE === "SPREAD_LIST" && (template.SPR_LST_SETUP?.CONTENT || template.SPR_ITM_LST_SETUP?.CONTENT)) {
+ return 'SPREAD_LIST';
+ }
+ if (template.TMPL_TYPE === "SPREAD_ITEM" && (template.SPR_LST_SETUP?.CONTENT || template.SPR_ITM_LST_SETUP?.CONTENT)) {
+ return 'SPREAD_ITEM';
+ }
+ if (template.GRD_LST_SETUP && columnsJSON.length > 0) {
+ return 'GRD_LIST';
+ }
+ return null;
+ }, [columnsJSON]);
+
+ const isValidTemplate = React.useCallback((template: TemplateItem): boolean => {
+ return determineTemplateType(template) !== null;
+ }, [determineTemplateType]);
- // 클라이언트 사이드에서만 렌더링되도록 보장
React.useEffect(() => {
setIsClient(true);
}, []);
- // 사용 가능한 템플릿들을 필터링하고 설정
React.useEffect(() => {
if (!templateData) return;
@@ -140,134 +204,158 @@ export function TemplateViewDialog({
templates = [templateData as TemplateItem];
}
- // 유효한 템플릿들 필터링
- const validTemplates = templates.filter(template => {
- const hasSpreadListContent = template.SPR_LST_SETUP?.CONTENT;
- const hasSpreadItemContent = template.SPR_ITM_LST_SETUP?.CONTENT;
- const hasGrdListSetup = template.GRD_LST_SETUP && columnsJSON.length > 0; // GRD_LIST 조건: GRD_LST_SETUP 존재 + columnsJSON 있음
-
- const isValidType = template.TMPL_TYPE === "SPREAD_LIST" ||
- template.TMPL_TYPE === "SPREAD_ITEM" ||
- template.TMPL_TYPE === "GRD_LIST"; // GRD_LIST 타입 추가
-
- return isValidType && (hasSpreadListContent || hasSpreadItemContent || hasGrdListSetup);
- });
-
+ const validTemplates = templates.filter(isValidTemplate);
setAvailableTemplates(validTemplates);
- // 첫 번째 유효한 템플릿을 기본으로 선택
if (validTemplates.length > 0 && !selectedTemplateId) {
const firstTemplate = validTemplates[0];
+ const templateTypeToSet = determineTemplateType(firstTemplate);
setSelectedTemplateId(firstTemplate.TMPL_ID);
-
- // 템플릿 타입 결정
- let templateTypeToSet: 'SPREAD_LIST' | 'SPREAD_ITEM' | 'GRD_LIST';
- if (firstTemplate.GRD_LST_SETUP && columnsJSON.length > 0) {
- templateTypeToSet = 'GRD_LIST';
- } else {
- templateTypeToSet = firstTemplate.TMPL_TYPE as 'SPREAD_LIST' | 'SPREAD_ITEM';
- }
-
setTemplateType(templateTypeToSet);
}
- }, [templateData, selectedTemplateId]);
+ }, [templateData, selectedTemplateId, isValidTemplate, determineTemplateType]);
- // 선택된 템플릿 변경 처리
const handleTemplateChange = (templateId: string) => {
const template = availableTemplates.find(t => t.TMPL_ID === templateId);
if (template) {
+ const templateTypeToSet = determineTemplateType(template);
setSelectedTemplateId(templateId);
-
- // 템플릿 타입 결정
- let templateTypeToSet: 'SPREAD_LIST' | 'SPREAD_ITEM' | 'GRD_LIST';
- if (template.GRD_LST_SETUP && columnsJSON.length > 0) {
- templateTypeToSet = 'GRD_LIST';
- } else {
- templateTypeToSet = template.TMPL_TYPE as 'SPREAD_LIST' | 'SPREAD_ITEM';
- }
-
setTemplateType(templateTypeToSet);
setHasChanges(false);
setValidationErrors([]);
- // SpreadSheets 재초기화
- if (currentSpread) {
- const template = availableTemplates.find(t => t.TMPL_ID === templateId);
- if (template) {
- initSpread(currentSpread, template);
- }
+ if (currentSpread && template) {
+ initSpread(currentSpread, template);
}
}
};
- // 현재 선택된 템플릿 가져오기
const selectedTemplate = React.useMemo(() => {
return availableTemplates.find(t => t.TMPL_ID === selectedTemplateId);
}, [availableTemplates, selectedTemplateId]);
- // 편집 가능한 필드 목록 계산
const editableFields = React.useMemo(() => {
- // SPREAD_ITEM인 경우: selectedRow의 TAG_NO로 확인
if (templateType === 'SPREAD_ITEM' && selectedRow?.TAG_NO) {
if (!editableFieldsMap.has(selectedRow.TAG_NO)) {
return [];
}
return editableFieldsMap.get(selectedRow.TAG_NO) || [];
}
-
- // SPREAD_LIST 또는 GRD_LIST인 경우: 첫 번째 행의 TAG_NO를 기준으로 처리
+
if ((templateType === 'SPREAD_LIST' || templateType === 'GRD_LIST') && tableData.length > 0) {
const firstRowTagNo = tableData[0]?.TAG_NO;
if (firstRowTagNo && editableFieldsMap.has(firstRowTagNo)) {
return editableFieldsMap.get(firstRowTagNo) || [];
}
}
-
+
return [];
}, [templateType, selectedRow?.TAG_NO, tableData, editableFieldsMap]);
- // 필드가 편집 가능한지 판별하는 함수
const isFieldEditable = React.useCallback((attId: string, rowData?: GenericData) => {
- // columnsJSON에서 해당 attId의 shi 값 확인
const columnConfig = columnsJSON.find(col => col.key === attId);
if (columnConfig?.shi === true) {
- return false; // columnsJSON에서 shi가 true이면 편집 불가
+ return false;
}
- // TAG_NO와 TAG_DESC는 기본적으로 편집 가능 (columnsJSON의 shi가 false인 경우)
- if (attId === "TAG_NO" || attId === "TAG_DESC") {
- return true;
+ if (attId === "TAG_NO" || attId === "TAG_DESC" || attId === "status") {
+ return false;
}
- // SPREAD_ITEM인 경우: editableFields 체크
- if (templateType === 'SPREAD_ITEM') {
- return editableFields.includes(attId);
- }
-
- // SPREAD_LIST 또는 GRD_LIST인 경우: 개별 행의 편집 가능성도 고려
if (templateType === 'SPREAD_LIST' || templateType === 'GRD_LIST') {
- // 기본적으로 editableFields에 포함되어야 함
- if (!editableFields.includes(attId)) {
- return false;
- }
-
- // rowData가 제공된 경우 해당 행의 shi 상태도 확인
if (rowData && rowData.shi === true) {
return false;
}
-
return true;
}
-
+
return true;
- }, [templateType, columnsJSON, editableFields]);
+ }, [templateType, columnsJSON]);
- // 편집 가능한 필드 개수 계산
const editableFieldsCount = React.useMemo(() => {
return cellMappings.filter(m => m.isEditable).length;
}, [cellMappings]);
- // 셀 주소를 행과 열로 변환하는 함수
+ // 🚀 배치 처리 함수들
+ const setBatchValues = React.useCallback((
+ activeSheet: any,
+ valuesToSet: Array<{row: number, col: number, value: any}>
+ ) => {
+ console.log(`🚀 Setting ${valuesToSet.length} values in batch`);
+
+ const columnGroups = new Map<number, Array<{row: number, value: any}>>();
+
+ valuesToSet.forEach(({row, col, value}) => {
+ if (!columnGroups.has(col)) {
+ columnGroups.set(col, []);
+ }
+ columnGroups.get(col)!.push({row, value});
+ });
+
+ columnGroups.forEach((values, col) => {
+ values.sort((a, b) => a.row - b.row);
+
+ let start = 0;
+ while (start < values.length) {
+ let end = start;
+ while (end + 1 < values.length && values[end + 1].row === values[end].row + 1) {
+ end++;
+ }
+
+ const rangeValues = values.slice(start, end + 1).map(v => v.value);
+ const startRow = values[start].row;
+
+ try {
+ if (rangeValues.length === 1) {
+ activeSheet.setValue(startRow, col, rangeValues[0]);
+ } else {
+ const dataArray = rangeValues.map(v => [v]);
+ activeSheet.setArray(startRow, col, dataArray);
+ }
+ } catch (error) {
+ for (let i = start; i <= end; i++) {
+ try {
+ activeSheet.setValue(values[i].row, col, values[i].value);
+ } catch (cellError) {
+ console.warn(`⚠️ Individual value setting failed [${values[i].row}, ${col}]:`, cellError);
+ }
+ }
+ }
+
+ start = end + 1;
+ }
+ });
+ }, []);
+
+ const setBatchStyles = React.useCallback((
+ activeSheet: any,
+ stylesToSet: Array<{row: number, col: number, isEditable: boolean}>
+ ) => {
+ console.log(`🎨 Setting ${stylesToSet.length} styles in batch`);
+
+ const editableStyle = createCellStyle(true);
+ const readonlyStyle = createCellStyle(false);
+
+ // 🔧 개별 셀별로 스타일과 잠금 상태 설정 (편집 권한 보장)
+ stylesToSet.forEach(({row, col, isEditable}) => {
+ try {
+ const cell = activeSheet.getCell(row, col);
+ const style = isEditable ? editableStyle : readonlyStyle;
+
+ activeSheet.setStyle(row, col, style);
+ cell.locked(!isEditable); // 편집 가능하면 잠금 해제
+
+ // 🆕 편집 가능한 셀에 기본 텍스트 에디터 설정
+ if (isEditable) {
+ const textCellType = new GC.Spread.Sheets.CellTypes.Text();
+ activeSheet.setCellType(row, col, textCellType);
+ }
+ } catch (error) {
+ console.warn(`⚠️ Failed to set style for cell [${row}, ${col}]:`, error);
+ }
+ });
+ }, [createCellStyle]);
+
const parseCellAddress = (address: string): { row: number, col: number } | null => {
if (!address || address.trim() === "") return null;
@@ -283,11 +371,9 @@ export function TemplateViewDialog({
col -= 1;
const row = parseInt(rowStr) - 1;
-
return { row, col };
};
- // 행과 열을 셀 주소로 변환하는 함수 (GRD_LIST용)
const getCellAddress = (row: number, col: number): string => {
let colStr = '';
let colNum = col;
@@ -298,10 +384,9 @@ export function TemplateViewDialog({
return colStr + (row + 1);
};
- // 데이터 타입 검증 함수
const validateCellValue = (value: any, columnType: ColumnType, options?: string[]): string | null => {
if (value === undefined || value === null || value === "") {
- return null; // 빈 값은 별도 required 검증에서 처리
+ return null;
}
switch (columnType) {
@@ -316,17 +401,14 @@ export function TemplateViewDialog({
}
break;
case "STRING":
- // STRING 타입은 대부분의 값을 허용
break;
default:
- // 커스텀 타입의 경우 추가 검증 로직이 필요할 수 있음
break;
}
return null;
};
- // 전체 데이터 검증 함수
const validateAllData = React.useCallback(() => {
if (!currentSpread || !selectedTemplate) return [];
@@ -340,68 +422,44 @@ export function TemplateViewDialog({
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' || templateType === 'GRD_LIST') {
- // 복수 행 검증 - 각 매핑은 이미 개별 행을 가리킴
- const cellValue = activeSheet.getValue(cellPos.row, cellPos.col);
- const errorMessage = validateCellValue(cellValue, columnConfig.type, columnConfig.options);
+ 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
- });
- }
+ if (errorMessage) {
+ errors.push({
+ cellAddress: mapping.cellAddress,
+ attId: mapping.attId,
+ value: cellValue,
+ expectedType: columnConfig.type,
+ message: errorMessage
+ });
}
});
setValidationErrors(errors);
return errors;
- }, [currentSpread, selectedTemplate, cellMappings, columnsJSON, templateType]);
-
- // ═══════════════════════════════════════════════════════════════════════════════
- // 🛠️ 헬퍼 함수들
- // ═══════════════════════════════════════════════════════════════════════════════
+ }, [currentSpread, selectedTemplate, cellMappings, columnsJSON]);
- // 🎨 셀 스타일 생성
const createCellStyle = React.useCallback((isEditable: boolean) => {
const style = new GC.Spread.Sheets.Style();
if (isEditable) {
- style.backColor = "#f0fdf4"; // 연한 초록 (편집 가능)
+ style.backColor = "#f0fdf4";
} else {
- style.backColor = "#f9fafb"; // 연한 회색 (읽기 전용)
+ style.backColor = "#f9fafb";
style.foreColor = "#6b7280";
}
return style;
}, []);
- // 🎯 드롭다운 설정
const setupOptimizedListValidation = React.useCallback((activeSheet: any, cellPos: { row: number, col: number }, options: string[], rowCount: number) => {
try {
console.log(`🎯 Setting up dropdown for ${rowCount} rows with options:`, options);
- // ✅ options 정규화
const safeOptions = options
- .filter(opt => opt !== null && opt !== undefined && opt !== '')
+ .filter(opt => opt !== null && opt !== undefined && opt !== '')
.map(opt => String(opt).trim())
.filter(opt => opt.length > 0)
- .filter((opt, index, arr) => arr.indexOf(opt) === index)
+ .filter((opt, index, arr) => arr.indexOf(opt) === index)
.slice(0, 20);
if (safeOptions.length === 0) {
@@ -409,280 +467,333 @@ export function TemplateViewDialog({
return;
}
- console.log(`📋 Safe options:`, safeOptions);
-
- // ✅ DataValidation용 문자열 준비
const optionsString = safeOptions.join(',');
- // 🔑 핵심 수정: 각 셀마다 개별 ComboBox 인스턴스 생성!
for (let i = 0; i < rowCount; i++) {
try {
const targetRow = cellPos.row + i;
- // ✅ 각 셀마다 새로운 ComboBox 인스턴스 생성
+ // 🔧 각 셀마다 새로운 ComboBox 인스턴스 생성
const comboBoxCellType = new GC.Spread.Sheets.CellTypes.ComboBox();
- comboBoxCellType.items(safeOptions); // 배열로 전달
+ comboBoxCellType.items(safeOptions);
comboBoxCellType.editorValueType(GC.Spread.Sheets.CellTypes.EditorValueType.text);
-
- // ✅ 각 셀마다 새로운 DataValidation 인스턴스 생성
+
+ // 🔧 DataValidation 설정
const cellValidator = GC.Spread.Sheets.DataValidation.createListValidator(optionsString);
-
- // ComboBox + DataValidation 둘 다 적용
+ cellValidator.showInputMessage(false);
+ cellValidator.showErrorMessage(false);
+
+ // ComboBox와 Validator 적용
activeSheet.setCellType(targetRow, cellPos.col, comboBoxCellType);
activeSheet.setDataValidator(targetRow, cellPos.col, cellValidator);
- // 셀 잠금 해제
+ // 🚀 중요: 셀 잠금 해제 및 편집 가능 설정
const cell = activeSheet.getCell(targetRow, cellPos.col);
cell.locked(false);
-
- console.log(`✅ Individual dropdown applied to [${targetRow}, ${cellPos.col}]`);
-
+
+ console.log(`✅ Dropdown applied to [${targetRow}, ${cellPos.col}] with ${safeOptions.length} options`);
+
} catch (cellError) {
- console.warn(`⚠️ Failed to apply to row ${cellPos.row + i}:`, cellError);
+ console.warn(`⚠️ Failed to apply dropdown to row ${cellPos.row + i}:`, cellError);
}
}
- console.log(`✅ Safe dropdown setup completed for ${rowCount} cells`);
+ console.log(`✅ Dropdown setup completed for ${rowCount} cells`);
} catch (error) {
console.error('❌ Dropdown setup failed:', error);
}
}, []);
- // 🚀 행 용량 확보
+ const getSafeActiveSheet = React.useCallback((spread: any, functionName: string = 'unknown') => {
+ if (!spread) return null;
+
+ try {
+ let activeSheet = spread.getActiveSheet();
+ if (!activeSheet) {
+ const sheetCount = spread.getSheetCount();
+ if (sheetCount > 0) {
+ activeSheet = spread.getSheet(0);
+ if (activeSheet) {
+ spread.setActiveSheetIndex(0);
+ }
+ }
+ }
+ return activeSheet;
+ } catch (error) {
+ console.error(`❌ Error getting activeSheet in ${functionName}:`, error);
+ return null;
+ }
+ }, []);
+
const ensureRowCapacity = React.useCallback((activeSheet: any, requiredRowCount: number) => {
- const currentRowCount = activeSheet.getRowCount();
- if (requiredRowCount > currentRowCount) {
- activeSheet.setRowCount(requiredRowCount + 10); // 여유분 추가
- console.log(`📈 Expanded sheet to ${requiredRowCount + 10} rows`);
+ try {
+ if (!activeSheet) return false;
+
+ const currentRowCount = activeSheet.getRowCount();
+ if (requiredRowCount > currentRowCount) {
+ const newRowCount = requiredRowCount + 10;
+ activeSheet.setRowCount(newRowCount);
+ }
+ return true;
+ } catch (error) {
+ console.error('❌ Error in ensureRowCapacity:', error);
+ return false;
}
}, []);
- // 🆕 GRD_LIST용 동적 테이블 생성 함수
- const createGrdListTable = React.useCallback((activeSheet: any, template: TemplateItem) => {
- console.log('🏗️ Creating GRD_LIST table');
-
- // columnsJSON의 visible 컬럼들을 seq 순서로 정렬하여 사용
- const visibleColumns = columnsJSON
- .filter(col => col.hidden !== true) // hidden이 true가 아닌 것들만
- .sort((a, b) => {
- // seq가 없는 경우 999999로 처리하여 맨 뒤로 보냄
- const seqA = a.seq !== undefined ? a.seq : 999999;
- const seqB = b.seq !== undefined ? b.seq : 999999;
- return seqA - seqB;
- });
+ const ensureColumnCapacity = React.useCallback((activeSheet: any, requiredColumnCount: number) => {
+ try {
+ if (!activeSheet) return false;
+
+ const currentColumnCount = activeSheet.getColumnCount();
+ if (requiredColumnCount > currentColumnCount) {
+ const newColumnCount = requiredColumnCount + 10;
+ activeSheet.setColumnCount(newColumnCount);
+ }
+ return true;
+ } catch (error) {
+ console.error('❌ Error in ensureColumnCapacity:', error);
+ return false;
+ }
+ }, []);
- console.log('📊 Using columns:', visibleColumns.map(c => `${c.key}(seq:${c.seq})`));
+ const setOptimalColumnWidths = React.useCallback((activeSheet: any, columns: any[], startCol: number, tableData: any[]) => {
+ columns.forEach((column, colIndex) => {
+ const targetCol = startCol + colIndex;
+ const optimalWidth = column.type === 'NUMBER' ? 100 : column.type === 'STRING' ? 150 : 120;
+ activeSheet.setColumnWidth(targetCol, optimalWidth);
+ });
+ }, []);
- if (visibleColumns.length === 0) {
- console.warn('❌ No visible columns found in columnsJSON');
- return [];
- }
+ // 🚀 최적화된 GRD_LIST 생성
+ const createGrdListTableOptimized = React.useCallback((activeSheet: any, template: TemplateItem) => {
+ console.log('🚀 Creating optimized GRD_LIST table');
- // 테이블 생성 시작
+ const visibleColumns = columnsJSON
+ .filter(col => col.hidden !== true)
+ .sort((a, b) => (a.seq ?? 999999) - (b.seq ?? 999999));
+
+ if (visibleColumns.length === 0) return [];
+
+ const startCol = 1;
+ const dataStartRow = 1;
const mappings: CellMapping[] = [];
- const startCol = 1; // A열 제외하고 B열부터 시작
-
- // 🔍 그룹 헤더 분석
- const groupInfo = analyzeColumnGroups(visibleColumns);
- const hasGroups = groupInfo.groups.length > 0;
-
- // 헤더 행 계산: 그룹이 있으면 2행, 없으면 1행
- const groupHeaderRow = 0;
- const columnHeaderRow = hasGroups ? 1 : 0;
- const dataStartRow = hasGroups ? 2 : 1;
-
- // 🎨 헤더 스타일 생성
- const groupHeaderStyle = new GC.Spread.Sheets.Style();
- groupHeaderStyle.backColor = "#1e40af"; // 더 진한 파란색
- groupHeaderStyle.foreColor = "#ffffff";
- groupHeaderStyle.font = "bold 13px Arial";
- groupHeaderStyle.hAlign = GC.Spread.Sheets.HorizontalAlign.center;
- groupHeaderStyle.vAlign = GC.Spread.Sheets.VerticalAlign.center;
-
- const columnHeaderStyle = new GC.Spread.Sheets.Style();
- columnHeaderStyle.backColor = "#3b82f6"; // 기본 파란색
- columnHeaderStyle.foreColor = "#ffffff";
- columnHeaderStyle.font = "bold 12px Arial";
- columnHeaderStyle.hAlign = GC.Spread.Sheets.HorizontalAlign.center;
- columnHeaderStyle.vAlign = GC.Spread.Sheets.VerticalAlign.center;
-
- let currentCol = startCol;
-
- // 🏗️ 그룹 헤더 및 컬럼 헤더 생성
- if (hasGroups) {
- // 그룹 헤더가 있는 경우
- groupInfo.groups.forEach(group => {
- if (group.isGroup) {
- // 그룹 헤더 생성 및 병합
- const groupStartCol = currentCol;
- const groupEndCol = currentCol + group.columns.length - 1;
-
- // 그룹 헤더 셀 설정
- const groupHeaderCell = activeSheet.getCell(groupHeaderRow, groupStartCol);
- groupHeaderCell.value(group.head);
-
- // 그룹 헤더 병합
- if (group.columns.length > 1) {
- activeSheet.addSpan(groupHeaderRow, groupStartCol, 1, group.columns.length);
- }
-
- // 그룹 헤더 스타일 적용
- for (let col = groupStartCol; col <= groupEndCol; col++) {
- activeSheet.setStyle(groupHeaderRow, col, groupHeaderStyle);
- activeSheet.getCell(groupHeaderRow, col).locked(true);
- }
-
- console.log(`📝 Group Header [${groupHeaderRow}, ${groupStartCol}-${groupEndCol}]: ${group.head}`);
-
- // 그룹 내 개별 컬럼 헤더 생성
- group.columns.forEach((column, index) => {
- const colIndex = groupStartCol + index;
- const columnHeaderCell = activeSheet.getCell(columnHeaderRow, colIndex);
- columnHeaderCell.value(column.label);
- activeSheet.setStyle(columnHeaderRow, colIndex, columnHeaderStyle);
- columnHeaderCell.locked(true);
-
- console.log(`📝 Column Header [${columnHeaderRow}, ${colIndex}]: ${column.label}`);
- });
-
- currentCol += group.columns.length;
- } else {
- // 그룹이 아닌 단일 컬럼
- const column = group.columns[0];
-
- // 그룹 헤더 행에는 빈 셀 (개별 컬럼이므로)
- const groupHeaderCell = activeSheet.getCell(groupHeaderRow, currentCol);
- groupHeaderCell.value("");
- activeSheet.setStyle(groupHeaderRow, currentCol, groupHeaderStyle);
- groupHeaderCell.locked(true);
-
- // 컬럼 헤더 생성
- const columnHeaderCell = activeSheet.getCell(columnHeaderRow, currentCol);
- columnHeaderCell.value(column.label);
- activeSheet.setStyle(columnHeaderRow, currentCol, columnHeaderStyle);
- columnHeaderCell.locked(true);
-
- console.log(`📝 Single Column [${columnHeaderRow}, ${currentCol}]: ${column.label}`);
- currentCol++;
- }
- });
- } else {
- // 그룹이 없는 경우 - 기존 로직
- visibleColumns.forEach((column, colIndex) => {
- const headerCol = startCol + colIndex;
- const headerCell = activeSheet.getCell(columnHeaderRow, headerCol);
- headerCell.value(column.label);
- activeSheet.setStyle(columnHeaderRow, headerCol, columnHeaderStyle);
- headerCell.locked(true);
-
- console.log(`📝 Header [${columnHeaderRow}, ${headerCol}]: ${column.label}`);
- });
- }
- // 데이터 행 생성
- const dataRowCount = tableData.length;
- ensureRowCapacity(activeSheet, dataStartRow + dataRowCount);
+ ensureColumnCapacity(activeSheet, startCol + visibleColumns.length);
+ ensureRowCapacity(activeSheet, dataStartRow + tableData.length);
+
+ // 헤더 생성
+ const headerStyle = new GC.Spread.Sheets.Style();
+ headerStyle.backColor = "#3b82f6";
+ headerStyle.foreColor = "#ffffff";
+ headerStyle.font = "bold 12px Arial";
+ headerStyle.hAlign = GC.Spread.Sheets.HorizontalAlign.center;
+
+ visibleColumns.forEach((column, colIndex) => {
+ const targetCol = startCol + colIndex;
+ const cell = activeSheet.getCell(0, targetCol);
+ cell.value(column.label);
+ cell.locked(true);
+ activeSheet.setStyle(0, targetCol, headerStyle);
+ });
+
+ // 🚀 데이터 배치 처리 준비
+ const allValues: Array<{row: number, col: number, value: any}> = [];
+ const allStyles: Array<{row: number, col: number, isEditable: boolean}> = [];
+
+ // 🔧 편집 가능한 셀 정보 수집 (드롭다운용)
+ const dropdownConfigs: Array<{
+ startRow: number;
+ col: number;
+ rowCount: number;
+ options: string[];
+ editableRows: number[]; // 편집 가능한 행만 추적
+ }> = [];
- tableData.forEach((rowData, rowIndex) => {
- const targetRow = dataStartRow + rowIndex;
+ visibleColumns.forEach((column, colIndex) => {
+ const targetCol = startCol + colIndex;
- visibleColumns.forEach((column, colIndex) => {
- const targetCol = startCol + colIndex;
- const cellAddress = getCellAddress(targetRow, targetCol);
- const isEditable = isFieldEditable(column.key, rowData);
+ // 드롭다운 설정을 위한 편집 가능한 행 찾기
+ if (column.type === "LIST" && column.options) {
+ const editableRows: number[] = [];
+ tableData.forEach((rowData, rowIndex) => {
+ if (isFieldEditable(column.key, rowData)) {
+ editableRows.push(dataStartRow + rowIndex);
+ }
+ });
- // 매핑 정보 추가
+ if (editableRows.length > 0) {
+ dropdownConfigs.push({
+ startRow: dataStartRow,
+ col: targetCol,
+ rowCount: tableData.length,
+ options: column.options,
+ editableRows: editableRows
+ });
+ }
+ }
+
+ tableData.forEach((rowData, rowIndex) => {
+ const targetRow = dataStartRow + rowIndex;
+ const cellEditable = isFieldEditable(column.key, rowData);
+ const value = rowData[column.key];
+
mappings.push({
attId: column.key,
- cellAddress: cellAddress,
- isEditable: isEditable,
+ cellAddress: getCellAddress(targetRow, targetCol),
+ isEditable: cellEditable,
dataRowIndex: rowIndex
});
- // 셀 값 설정
- const cell = activeSheet.getCell(targetRow, targetCol);
- const value = rowData[column.key];
- cell.value(value ?? null);
-
- // 스타일 적용
- const style = createCellStyle(isEditable);
- activeSheet.setStyle(targetRow, targetCol, style);
- cell.locked(!isEditable);
-
- console.log(`📝 Data [${targetRow}, ${targetCol}]: ${column.key} = "${value}" (${isEditable ? 'Editable' : 'ReadOnly'})`);
+ allValues.push({
+ row: targetRow,
+ col: targetCol,
+ value: value ?? null
+ });
- // LIST 타입 드롭다운 설정
- if (column.type === "LIST" && column.options && isEditable) {
- setupOptimizedListValidation(activeSheet, { row: targetRow, col: targetCol }, column.options, 1);
- }
+ allStyles.push({
+ row: targetRow,
+ col: targetCol,
+ isEditable: cellEditable
+ });
});
});
- console.log(`🏗️ GRD_LIST table created with ${mappings.length} mappings, hasGroups: ${hasGroups}`);
- return mappings;
- }, [tableData, columnsJSON, isFieldEditable, createCellStyle, ensureRowCapacity, setupOptimizedListValidation]);
-
- // 🔍 컬럼 그룹 분석 함수
- const analyzeColumnGroups = React.useCallback((columns: DataTableColumnJSON[]) => {
- const groups: Array<{
- head: string;
- isGroup: boolean;
- columns: DataTableColumnJSON[];
- }> = [];
+ // 🚀 배치로 값과 스타일 설정
+ setBatchValues(activeSheet, allValues);
+ setBatchStyles(activeSheet, allStyles);
- let i = 0;
- while (i < columns.length) {
- const currentCol = columns[i];
-
- // head가 없거나 빈 문자열인 경우 단일 컬럼으로 처리
- if (!currentCol.head || !currentCol.head.trim()) {
- groups.push({
- head: '',
- isGroup: false,
- columns: [currentCol]
+ // 🎯 개선된 드롭다운 설정 (편집 가능한 셀에만)
+ dropdownConfigs.forEach(({ col, options, editableRows }) => {
+ try {
+ console.log(`🎯 Setting dropdown for column ${col}, editable rows: ${editableRows.length}`);
+
+ const safeOptions = options
+ .filter(opt => opt !== null && opt !== undefined && opt !== '')
+ .map(opt => String(opt).trim())
+ .filter(opt => opt.length > 0)
+ .slice(0, 20);
+
+ if (safeOptions.length === 0) return;
+
+ // 편집 가능한 행에만 드롭다운 적용
+ editableRows.forEach(targetRow => {
+ try {
+ const comboBoxCellType = new GC.Spread.Sheets.CellTypes.ComboBox();
+ comboBoxCellType.items(safeOptions);
+ comboBoxCellType.editorValueType(GC.Spread.Sheets.CellTypes.EditorValueType.text);
+
+ const cellValidator = GC.Spread.Sheets.DataValidation.createListValidator(safeOptions.join(','));
+ cellValidator.showInputMessage(false);
+ cellValidator.showErrorMessage(false);
+
+ activeSheet.setCellType(targetRow, col, comboBoxCellType);
+ activeSheet.setDataValidator(targetRow, col, cellValidator);
+
+ // 🚀 편집 권한 명시적 설정
+ const cell = activeSheet.getCell(targetRow, col);
+ cell.locked(false);
+
+ console.log(`✅ Dropdown applied to editable cell [${targetRow}, ${col}]`);
+ } catch (cellError) {
+ console.warn(`⚠️ Failed to apply dropdown to [${targetRow}, ${col}]:`, cellError);
+ }
});
- i++;
- continue;
+ } catch (error) {
+ console.error(`❌ Dropdown config failed for column ${col}:`, error);
}
+ });
- // 같은 head를 가진 연속된 컬럼들을 찾기
- const groupHead = currentCol.head.trim();
- const groupColumns: DataTableColumnJSON[] = [currentCol];
- let j = i + 1;
-
- while (j < columns.length && columns[j].head && columns[j].head.trim() === groupHead) {
- groupColumns.push(columns[j]);
- j++;
- }
+ setOptimalColumnWidths(activeSheet, visibleColumns, startCol, tableData);
+
+ console.log(`✅ Optimized GRD_LIST created:`);
+ console.log(` - Total mappings: ${mappings.length}`);
+ console.log(` - Editable cells: ${mappings.filter(m => m.isEditable).length}`);
+ console.log(` - Dropdown configs: ${dropdownConfigs.length}`);
+
+ return mappings;
+ }, [tableData, columnsJSON, isFieldEditable, setBatchValues, setBatchStyles, ensureColumnCapacity, ensureRowCapacity, getCellAddress, setOptimalColumnWidths]);
- // 그룹 추가
- groups.push({
- head: groupHead,
- isGroup: groupColumns.length > 1,
- columns: groupColumns
- });
+ const setupSheetProtectionAndEvents = React.useCallback((activeSheet: any, mappings: CellMapping[]) => {
+ console.log(`🛡️ Setting up protection and events for ${mappings.length} mappings`);
- i = j; // 다음 그룹으로 이동
- }
+ // 🔧 시트 보호 완전 해제 후 편집 권한 설정
+ activeSheet.options.isProtected = false;
- return { groups };
- }, []);
+ // 🔧 편집 가능한 셀들을 위한 강화된 설정
+ mappings.forEach((mapping) => {
+ const cellPos = parseCellAddress(mapping.cellAddress);
+ if (!cellPos) return;
+
+ try {
+ const cell = activeSheet.getCell(cellPos.row, cellPos.col);
+ const columnConfig = columnsJSON.find(col => col.key === mapping.attId);
+
+ if (mapping.isEditable) {
+ // 🚀 편집 가능한 셀 설정 강화
+ cell.locked(false);
+
+ if (columnConfig?.type === "LIST" && columnConfig.options) {
+ // LIST 타입: 새 ComboBox 인스턴스 생성
+ const comboBox = new GC.Spread.Sheets.CellTypes.ComboBox();
+ comboBox.items(columnConfig.options);
+ comboBox.editorValueType(GC.Spread.Sheets.CellTypes.EditorValueType.text);
+ activeSheet.setCellType(cellPos.row, cellPos.col, comboBox);
+
+ // DataValidation도 추가
+ const validator = GC.Spread.Sheets.DataValidation.createListValidator(columnConfig.options.join(','));
+ activeSheet.setDataValidator(cellPos.row, cellPos.col, validator);
+ } else if (columnConfig?.type === "NUMBER") {
+ // NUMBER 타입: 숫자 입력 허용
+ const textCellType = new GC.Spread.Sheets.CellTypes.Text();
+ activeSheet.setCellType(cellPos.row, cellPos.col, textCellType);
+
+ // 숫자 validation 추가 (에러 메시지 없이)
+ const numberValidator = GC.Spread.Sheets.DataValidation.createNumberValidator(
+ GC.Spread.Sheets.ConditionalFormatting.ComparisonOperators.between,
+ -999999999, 999999999, true
+ );
+ numberValidator.showInputMessage(false);
+ numberValidator.showErrorMessage(false);
+ activeSheet.setDataValidator(cellPos.row, cellPos.col, numberValidator);
+ } else {
+ // 기본 TEXT 타입: 자유 텍스트 입력
+ const textCellType = new GC.Spread.Sheets.CellTypes.Text();
+ activeSheet.setCellType(cellPos.row, cellPos.col, textCellType);
+ }
+
+ // 편집 가능 스타일 재적용
+ const editableStyle = createCellStyle(true);
+ activeSheet.setStyle(cellPos.row, cellPos.col, editableStyle);
+
+ console.log(`🔓 Cell [${cellPos.row}, ${cellPos.col}] ${mapping.attId} set as EDITABLE`);
+ } else {
+ // 읽기 전용 셀
+ cell.locked(true);
+ const readonlyStyle = createCellStyle(false);
+ activeSheet.setStyle(cellPos.row, cellPos.col, readonlyStyle);
+ }
+ } catch (error) {
+ console.error(`❌ Error processing cell ${mapping.cellAddress}:`, error);
+ }
+ });
- // 🛡️ 시트 보호 및 이벤트 설정
- const setupSheetProtectionAndEvents = React.useCallback((activeSheet: any, mappings: CellMapping[]) => {
- console.log(`🛡️ Setting up protection and events for ${mappings.length} mappings`);
-
- // 시트 보호 설정
+ // 🛡️ 시트 보호 재설정 (편집 허용 모드로)
activeSheet.options.isProtected = true;
activeSheet.options.protectionOptions = {
allowSelectLockedCells: true,
allowSelectUnlockedCells: true,
allowSort: false,
allowFilter: false,
- allowEditObjects: false,
+ allowEditObjects: true, // ✅ 편집 객체 허용
allowResizeRows: false,
- allowResizeColumns: false
+ allowResizeColumns: false,
+ allowFormatCells: false,
+ allowInsertRows: false,
+ allowInsertColumns: false,
+ allowDeleteRows: false,
+ allowDeleteColumns: false
};
// 🎯 변경 감지 이벤트
@@ -699,11 +810,10 @@ export function TemplateViewDialog({
});
});
- // 🚫 편집 시작 권한 확인 (수정됨)
+ // 🚫 편집 시작 권한 확인
activeSheet.bind(GC.Spread.Sheets.Events.EditStarting, (event: any, info: any) => {
console.log(`🎯 EditStarting: Row ${info.row}, Col ${info.col}`);
-
- // ✅ 정확한 매핑 찾기 (행/열 정확히 일치)
+
const exactMapping = mappings.find(m => {
const cellPos = parseCellAddress(m.cellAddress);
return cellPos && cellPos.row === info.row && cellPos.col === info.col;
@@ -711,12 +821,11 @@ export function TemplateViewDialog({
if (!exactMapping) {
console.log(`ℹ️ No mapping found for [${info.row}, ${info.col}] - allowing edit`);
- return; // 매핑이 없으면 허용 (템플릿 영역 밖)
+ return; // 매핑이 없으면 허용
}
- console.log(`📋 Found mapping: ${exactMapping.attId} at ${exactMapping.cellAddress}`);
+ console.log(`📋 Found mapping: ${exactMapping.attId}, isEditable: ${exactMapping.isEditable}`);
- // 기본 편집 권한 확인
if (!exactMapping.isEditable) {
console.log(`🚫 Field ${exactMapping.attId} is not editable`);
toast.warning(`${exactMapping.attId} field is read-only`);
@@ -724,12 +833,9 @@ export function TemplateViewDialog({
return;
}
- // SPREAD_LIST 또는 GRD_LIST 개별 행 SHI 확인
+ // SPREAD_LIST/GRD_LIST 개별 행 SHI 확인
if ((templateType === 'SPREAD_LIST' || templateType === 'GRD_LIST') && exactMapping.dataRowIndex !== undefined) {
const dataRowIndex = exactMapping.dataRowIndex;
-
- console.log(`🔍 Checking SHI for data row ${dataRowIndex}`);
-
if (dataRowIndex >= 0 && dataRowIndex < tableData.length) {
const rowData = tableData[dataRowIndex];
if (rowData?.shi === true) {
@@ -738,41 +844,31 @@ export function TemplateViewDialog({
info.cancel = true;
return;
}
- } else {
- console.warn(`⚠️ Invalid dataRowIndex: ${dataRowIndex} (tableData.length: ${tableData.length})`);
}
}
console.log(`✅ Edit allowed for ${exactMapping.attId}`);
});
- // ✅ 편집 완료 검증 (수정됨)
+ // ✅ 편집 완료 검증
activeSheet.bind(GC.Spread.Sheets.Events.EditEnded, (event: any, info: any) => {
- console.log(`🏁 EditEnded: Row ${info.row}, Col ${info.col}`);
-
- // ✅ 정확한 매핑 찾기
+ console.log(`🏁 EditEnded: Row ${info.row}, Col ${info.col}, New value: ${activeSheet.getValue(info.row, info.col)}`);
+
const exactMapping = mappings.find(m => {
const cellPos = parseCellAddress(m.cellAddress);
return cellPos && cellPos.row === info.row && cellPos.col === info.col;
});
- if (!exactMapping) {
- console.log(`ℹ️ No mapping found for [${info.row}, ${info.col}] - skipping validation`);
- return;
- }
+ if (!exactMapping) return;
const columnConfig = columnsJSON.find(col => col.key === exactMapping.attId);
if (columnConfig) {
const cellValue = activeSheet.getValue(info.row, info.col);
- console.log(`🔍 Validating ${exactMapping.attId}: "${cellValue}"`);
-
const errorMessage = validateCellValue(cellValue, columnConfig.type, columnConfig.options);
const cell = activeSheet.getCell(info.row, info.col);
if (errorMessage) {
- console.log(`❌ Validation failed: ${errorMessage}`);
-
- // 🚨 에러 스타일 적용 (편집 가능 상태 유지)
+ // 🚨 에러 스타일 적용
const errorStyle = new GC.Spread.Sheets.Style();
errorStyle.backColor = "#fef2f2";
errorStyle.foreColor = "#dc2626";
@@ -780,222 +876,221 @@ export function TemplateViewDialog({
errorStyle.borderRight = new GC.Spread.Sheets.LineBorder("#dc2626", GC.Spread.Sheets.LineStyle.thick);
errorStyle.borderTop = new GC.Spread.Sheets.LineBorder("#dc2626", GC.Spread.Sheets.LineStyle.thick);
errorStyle.borderBottom = new GC.Spread.Sheets.LineBorder("#dc2626", GC.Spread.Sheets.LineStyle.thick);
-
+
activeSheet.setStyle(info.row, info.col, errorStyle);
- cell.locked(!exactMapping.isEditable);
-
- toast.warning(`Invalid value in ${exactMapping.attId}: ${errorMessage}. Please correct the value.`, { duration: 5000 });
+ cell.locked(!exactMapping.isEditable); // 편집 가능 상태 유지
+ toast.warning(`Invalid value in ${exactMapping.attId}: ${errorMessage}`, { duration: 5000 });
} else {
- console.log(`✅ Validation passed`);
-
// ✅ 정상 스타일 복원
const normalStyle = createCellStyle(exactMapping.isEditable);
activeSheet.setStyle(info.row, info.col, normalStyle);
cell.locked(!exactMapping.isEditable);
}
}
+
+ setHasChanges(true);
});
- console.log(`🛡️ Protection and events configured for ${mappings.length} mappings`);
+ console.log(`🛡️ Protection configured. Editable cells: ${mappings.filter(m => m.isEditable).length}`);
}, [templateType, tableData, createCellStyle, validateCellValue, columnsJSON]);
- // ═══════════════════════════════════════════════════════════════════════════════
- // 🏗️ 메인 SpreadSheets 초기화 함수
- // ═══════════════════════════════════════════════════════════════════════════════
-
- const initSpread = React.useCallback((spread: any, template?: TemplateItem) => {
+ // 🚀 최적화된 initSpread
+ const initSpread = React.useCallback(async (spread: any, template?: TemplateItem) => {
const workingTemplate = template || selectedTemplate;
- if (!spread || !workingTemplate) return;
+ if (!spread || !workingTemplate) {
+ console.error('❌ Invalid spread or template');
+ return;
+ }
try {
- // 🔄 초기 설정
+ console.log('🚀 Starting optimized spread initialization...');
+ setIsInitializing(true);
+ updateProgress('Initializing...', 0, 100);
+
setCurrentSpread(spread);
setHasChanges(false);
setValidationErrors([]);
- // 성능을 위한 렌더링 일시 중단
+ // 🚀 핵심 최적화: 모든 렌더링과 이벤트 중단
spread.suspendPaint();
+ spread.suspendEvent();
+ spread.suspendCalcService();
+
+ updateProgress('Setting up workspace...', 10, 100);
try {
- const activeSheet = spread.getActiveSheet();
-
- // 시트 보호 해제 (편집을 위해)
- activeSheet.options.isProtected = false;
+ let activeSheet = getSafeActiveSheet(spread, 'initSpread');
+ if (!activeSheet) {
+ throw new Error('Failed to get initial activeSheet');
+ }
+ activeSheet.options.isProtected = false;
let mappings: CellMapping[] = [];
- // 🆕 GRD_LIST 처리
- if (templateType === 'GRD_LIST' && workingTemplate.GRD_LST_SETUP) {
- console.log('🏗️ Processing GRD_LIST template');
+ if (templateType === 'GRD_LIST') {
+ updateProgress('Creating dynamic table...', 20, 100);
- // 기본 워크북 설정
spread.clearSheets();
spread.addSheet(0);
const sheet = spread.getSheet(0);
sheet.name('Data');
spread.setActiveSheet('Data');
- // 동적 테이블 생성
- mappings = createGrdListTable(sheet, workingTemplate);
+ updateProgress('Processing table data...', 50, 100);
+ mappings = createGrdListTableOptimized(sheet, workingTemplate);
} else {
- // 기존 SPREAD_LIST 및 SPREAD_ITEM 처리
- let contentJson = null;
- let dataSheets = null;
-
- // SPR_LST_SETUP.CONTENT 우선 사용
- 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);
- }
- // SPR_ITM_LST_SETUP.CONTENT 대안 사용
- 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);
- }
+ updateProgress('Loading template structure...', 20, 100);
+
+ let contentJson = workingTemplate.SPR_LST_SETUP?.CONTENT || workingTemplate.SPR_ITM_LST_SETUP?.CONTENT;
+ let dataSheets = workingTemplate.SPR_LST_SETUP?.DATA_SHEETS || workingTemplate.SPR_ITM_LST_SETUP?.DATA_SHEETS;
- if (!contentJson) {
- console.warn('❌ No CONTENT found in template:', workingTemplate.NAME);
- return;
+ if (!contentJson || !dataSheets) {
+ throw new Error(`No template content found for ${workingTemplate.NAME}`);
}
- // 🏗️ SpreadSheets 초기화
const jsonData = typeof contentJson === 'string' ? JSON.parse(contentJson) : contentJson;
- // 템플릿 구조 로드
+ updateProgress('Loading template layout...', 40, 100);
spread.fromJSON(jsonData);
+
+ activeSheet = getSafeActiveSheet(spread, 'after-fromJSON');
+ if (!activeSheet) {
+ throw new Error('ActiveSheet became null after loading template');
+ }
+
+ activeSheet.options.isProtected = false;
- // 📊 셀 매핑 및 데이터 처리
- if (dataSheets && dataSheets.length > 0) {
+ if (templateType === 'SPREAD_LIST' && tableData.length > 0) {
+ updateProgress('Processing data rows...', 60, 100);
- // 🔄 각 데이터 시트의 매핑 정보 처리
dataSheets.forEach(dataSheet => {
- if (dataSheet.MAP_CELL_ATT) {
- dataSheet.MAP_CELL_ATT.forEach(mapping => {
+ if (dataSheet.MAP_CELL_ATT && dataSheet.MAP_CELL_ATT.length > 0) {
+ dataSheet.MAP_CELL_ATT.forEach((mapping: any) => {
const { ATT_ID, IN } = mapping;
-
- if (IN && IN.trim() !== "") {
- const cellPos = parseCellAddress(IN);
- if (cellPos) {
- const columnConfig = columnsJSON.find(col => col.key === ATT_ID);
-
- // 🎯 템플릿 타입별 데이터 처리
- if (templateType === 'SPREAD_ITEM' && selectedRow) {
- // 📝 단일 행 처리 (SPREAD_ITEM)
- const isEditable = isFieldEditable(ATT_ID);
-
- // 매핑 정보 저장
- mappings.push({
- attId: ATT_ID,
- cellAddress: IN,
- isEditable: isEditable,
- dataRowIndex: 0
- });
-
- const cell = activeSheet.getCell(cellPos.row, cellPos.col);
- const value = selectedRow[ATT_ID];
-
- // 값 설정
- cell.value(value ?? null);
-
- // 🎨 스타일 및 편집 권한 설정
- cell.locked(!isEditable);
- const style = createCellStyle(isEditable);
- activeSheet.setStyle(cellPos.row, cellPos.col, style);
-
- // 📋 LIST 타입 드롭다운 설정
- if (columnConfig?.type === "LIST" && columnConfig.options && isEditable) {
- setupOptimizedListValidation(activeSheet, cellPos, columnConfig.options, 1);
- }
-
- } else if (templateType === 'SPREAD_LIST' && tableData.length > 0) {
- // 📊 복수 행 처리 (SPREAD_LIST) - 성능 최적화됨
- console.log(`🔄 Processing SPREAD_LIST for ${ATT_ID} with ${tableData.length} rows`);
-
- // 🚀 행 확장 (필요시)
- ensureRowCapacity(activeSheet, cellPos.row + tableData.length);
-
- // 📋 각 행마다 개별 매핑 생성
- tableData.forEach((rowData, index) => {
- const targetRow = cellPos.row + index;
- const targetCellAddress = `${String.fromCharCode(65 + cellPos.col)}${targetRow + 1}`;
- const cellEditable = isFieldEditable(ATT_ID, rowData);
-
- // 개별 매핑 추가
- mappings.push({
- attId: ATT_ID,
- cellAddress: targetCellAddress, // 각 행마다 다른 주소
- isEditable: cellEditable,
- dataRowIndex: index // 원본 데이터 인덱스
- });
-
- console.log(`📝 Mapping ${ATT_ID} Row ${index}: ${targetCellAddress} (${cellEditable ? 'Editable' : 'ReadOnly'})`);
- });
-
- // 📋 LIST 타입 드롭다운 설정 (조건부)
- if (columnConfig?.type === "LIST" && columnConfig.options) {
- // 편집 가능한 행이 하나라도 있으면 드롭다운 설정
- const hasEditableRows = tableData.some((rowData) => isFieldEditable(ATT_ID, rowData));
- if (hasEditableRows) {
- setupOptimizedListValidation(activeSheet, cellPos, columnConfig.options, tableData.length);
- }
- }
-
- // 🎨 개별 셀 데이터 및 스타일 설정
- tableData.forEach((rowData, index) => {
- const targetRow = cellPos.row + index;
- const cell = activeSheet.getCell(targetRow, cellPos.col);
- const value = rowData[ATT_ID];
-
- // 값 설정
- cell.value(value ?? null);
- // console.log(`📝 Row ${targetRow}: ${ATT_ID} = "${value}"`);
-
- // 편집 권한 및 스타일 설정
- const cellEditable = isFieldEditable(ATT_ID, rowData);
- cell.locked(!cellEditable);
- const style = createCellStyle(cellEditable);
- activeSheet.setStyle(targetRow, cellPos.col, style);
- });
- }
-
- // console.log(`📌 Mapped ${ATT_ID} → ${IN} (${templateType})`);
+ if (!ATT_ID || !IN || IN.trim() === "") return;
+
+ const cellPos = parseCellAddress(IN);
+ if (!cellPos) return;
+
+ const requiredRows = cellPos.row + tableData.length;
+ if (!ensureRowCapacity(activeSheet, requiredRows)) return;
+
+ // 🚀 배치 데이터 준비
+ const valuesToSet: Array<{row: number, col: number, value: any}> = [];
+ const stylesToSet: Array<{row: number, col: number, isEditable: boolean}> = [];
+
+ tableData.forEach((rowData, index) => {
+ const targetRow = cellPos.row + index;
+ const cellEditable = isFieldEditable(ATT_ID, rowData);
+ const value = rowData[ATT_ID];
+
+ mappings.push({
+ attId: ATT_ID,
+ cellAddress: getCellAddress(targetRow, cellPos.col),
+ isEditable: cellEditable,
+ dataRowIndex: index
+ });
+
+ valuesToSet.push({
+ row: targetRow,
+ col: cellPos.col,
+ value: value ?? null
+ });
+
+ stylesToSet.push({
+ row: targetRow,
+ col: cellPos.col,
+ isEditable: cellEditable
+ });
+ });
+
+ // 🚀 배치 처리
+ setBatchValues(activeSheet, valuesToSet);
+ setBatchStyles(activeSheet, stylesToSet);
+
+ // 드롭다운 설정
+ const columnConfig = columnsJSON.find(col => col.key === ATT_ID);
+ if (columnConfig?.type === "LIST" && columnConfig.options) {
+ const hasEditableRows = tableData.some((rowData) => isFieldEditable(ATT_ID, rowData));
+ if (hasEditableRows) {
+ setupOptimizedListValidation(activeSheet, cellPos, columnConfig.options, tableData.length);
}
}
});
}
});
+
+ } else if (templateType === 'SPREAD_ITEM' && selectedRow) {
+ updateProgress('Setting up form fields...', 60, 100);
+
+ dataSheets.forEach(dataSheet => {
+ dataSheet.MAP_CELL_ATT?.forEach((mapping: any) => {
+ const { ATT_ID, IN } = mapping;
+ const cellPos = parseCellAddress(IN);
+ if (cellPos) {
+ const isEditable = isFieldEditable(ATT_ID);
+ const value = selectedRow[ATT_ID];
+
+ mappings.push({
+ attId: ATT_ID,
+ cellAddress: IN,
+ isEditable: isEditable,
+ dataRowIndex: 0
+ });
+
+ const cell = activeSheet.getCell(cellPos.row, cellPos.col);
+ cell.value(value ?? null);
+
+ const style = createCellStyle(isEditable);
+ activeSheet.setStyle(cellPos.row, cellPos.col, style);
+
+ const columnConfig = columnsJSON.find(col => col.key === ATT_ID);
+ if (columnConfig?.type === "LIST" && columnConfig.options && isEditable) {
+ setupOptimizedListValidation(activeSheet, cellPos, columnConfig.options, 1);
+ }
+ }
+ });
+ });
}
}
- // 💾 매핑 정보 저장 및 이벤트 설정
+ updateProgress('Configuring interactions...', 90, 100);
setCellMappings(mappings);
- setupSheetProtectionAndEvents(activeSheet, mappings);
+
+ const finalActiveSheet = getSafeActiveSheet(spread, 'setupEvents');
+ if (finalActiveSheet) {
+ setupSheetProtectionAndEvents(finalActiveSheet, mappings);
+ }
+
+ updateProgress('Finalizing...', 100, 100);
+ console.log(`✅ Optimized initialization completed with ${mappings.length} mappings`);
} finally {
- // 렌더링 재개
+ // 🚀 올바른 순서로 재개
+ spread.resumeCalcService();
+ spread.resumeEvent();
spread.resumePaint();
}
} catch (error) {
- console.error('❌ Error initializing spread:', error);
- toast.error('Failed to load template');
- if (spread?.resumePaint) {
- spread.resumePaint();
- }
+ console.error('❌ Error in optimized spread initialization:', error);
+ if (spread?.resumeCalcService) spread.resumeCalcService();
+ if (spread?.resumeEvent) spread.resumeEvent();
+ if (spread?.resumePaint) spread.resumePaint();
+ toast.error(`Template loading failed: ${error.message}`);
+ } finally {
+ setIsInitializing(false);
+ setLoadingProgress(null);
}
- }, [selectedTemplate, templateType, selectedRow, tableData, isFieldEditable, columnsJSON, createCellStyle, setupOptimizedListValidation, ensureRowCapacity, setupSheetProtectionAndEvents, createGrdListTable]);
+ }, [selectedTemplate, templateType, selectedRow, tableData, updateProgress, getSafeActiveSheet, createGrdListTableOptimized, setBatchValues, setBatchStyles, setupSheetProtectionAndEvents, setCellMappings]);
- // 변경사항 저장 함수
const handleSaveChanges = React.useCallback(async () => {
if (!currentSpread || !hasChanges) {
toast.info("No changes to save");
return;
}
- // 저장 전 데이터 검증
const errors = validateAllData();
if (errors.length > 0) {
toast.error(`Cannot save: ${errors.length} validation errors found. Please fix them first.`);
@@ -1004,11 +1099,9 @@ export function TemplateViewDialog({
try {
setIsPending(true);
-
const activeSheet = currentSpread.getActiveSheet();
if (templateType === 'SPREAD_ITEM' && selectedRow) {
- // 단일 행 저장
const dataToSave = { ...selectedRow };
cellMappings.forEach(mapping => {
@@ -1038,7 +1131,6 @@ export function TemplateViewDialog({
onUpdateSuccess?.(dataToSave);
} else if ((templateType === 'SPREAD_LIST' || templateType === 'GRD_LIST') && tableData.length > 0) {
- // 복수 행 저장 (SPREAD_LIST와 GRD_LIST 동일 처리)
const updatedRows: GenericData[] = [];
let saveCount = 0;
@@ -1047,7 +1139,6 @@ export function TemplateViewDialog({
const dataToSave = { ...originalRow };
let hasRowChanges = false;
- // 각 매핑에 대해 해당 행의 값 확인
cellMappings.forEach(mapping => {
if (mapping.dataRowIndex === i && mapping.isEditable) {
const columnConfig = columnsJSON.find(col => col.key === mapping.attId);
@@ -1058,8 +1149,6 @@ export function TemplateViewDialog({
const cellPos = parseCellAddress(mapping.cellAddress);
if (cellPos) {
const cellValue = activeSheet.getValue(cellPos.row, cellPos.col);
-
- // 값이 변경되었는지 확인
if (cellValue !== originalRow[mapping.attId]) {
dataToSave[mapping.attId] = cellValue;
hasRowChanges = true;
@@ -1069,10 +1158,8 @@ export function TemplateViewDialog({
}
});
- // 변경사항이 있는 행만 저장
if (hasRowChanges) {
- dataToSave.TAG_NO = originalRow.TAG_NO; // TAG_NO는 절대 변경되지 않도록
-
+ dataToSave.TAG_NO = originalRow.TAG_NO;
const { success } = await updateFormDataInDB(
formCode,
contractItemId,
@@ -1084,7 +1171,7 @@ export function TemplateViewDialog({
saveCount++;
}
} else {
- updatedRows.push(originalRow); // 변경사항이 없으면 원본 유지
+ updatedRows.push(originalRow);
}
}
@@ -1109,21 +1196,18 @@ export function TemplateViewDialog({
if (!isOpen) return null;
- // 데이터 유효성 검사
const isDataValid = templateType === 'SPREAD_ITEM' ? !!selectedRow : tableData.length > 0;
const dataCount = templateType === 'SPREAD_ITEM' ? 1 : tableData.length;
-
+
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent
- className="w-[80%] max-w-none h-[80vh] flex flex-col"
- style={{ maxWidth: "80vw" }}
+ className="w-[90vw] max-w-[1400px] h-[85vh] flex flex-col fixed top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%] z-50"
>
<DialogHeader className="flex-shrink-0">
<DialogTitle>SEDP Template - {formCode}</DialogTitle>
<DialogDescription>
<div className="space-y-3">
- {/* 템플릿 선택 */}
{availableTemplates.length > 1 && (
<div className="flex items-center gap-4">
<span className="text-sm font-medium">Template:</span>
@@ -1134,11 +1218,7 @@ export function TemplateViewDialog({
<SelectContent>
{availableTemplates.map(template => (
<SelectItem key={template.TMPL_ID} value={template.TMPL_ID}>
- {template.NAME} ({
- template.GRD_LST_SETUP && columnsJSON.length > 0
- ? 'GRD_LIST'
- : template.TMPL_TYPE
- })
+ {template.NAME} ({template.TMPL_TYPE})
</SelectItem>
))}
</SelectContent>
@@ -1146,14 +1226,13 @@ export function TemplateViewDialog({
</div>
)}
- {/* 템플릿 정보 */}
{selectedTemplate && (
<div className="flex items-center gap-4 text-sm">
<span className="font-medium text-blue-600">
Template Type: {
- templateType === 'SPREAD_LIST' ? 'List View (SPREAD_LIST)' :
- templateType === 'SPREAD_ITEM' ? 'Item View (SPREAD_ITEM)' :
- 'Grid List View (GRD_LIST)'
+ templateType === 'SPREAD_LIST' ? 'List View (SPREAD_LIST)' :
+ templateType === 'SPREAD_ITEM' ? 'Item View (SPREAD_ITEM)' :
+ 'Grid List View (GRD_LIST)'
}
</span>
{templateType === 'SPREAD_ITEM' && selectedRow && (
@@ -1176,7 +1255,6 @@ export function TemplateViewDialog({
</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>
@@ -1200,8 +1278,15 @@ export function TemplateViewDialog({
</DialogDescription>
</DialogHeader>
- {/* SpreadSheets 컴포넌트 영역 */}
- <div className="flex-1 overflow-hidden">
+ <div className="flex-1 overflow-hidden relative">
+ {/* 🆕 로딩 프로그레스 오버레이 */}
+ <LoadingProgress
+ phase={loadingProgress?.phase || ''}
+ progress={loadingProgress?.progress || 0}
+ total={loadingProgress?.total || 100}
+ isVisible={isInitializing && !!loadingProgress}
+ />
+
{selectedTemplate && isClient && isDataValid ? (
<SpreadSheets
key={`${templateType}-${selectedTemplate.TMPL_ID}-${selectedTemplateId}`}
diff --git a/components/form-data/update-form-sheet.tsx b/components/form-data/update-form-sheet.tsx
index 91cb7a07..6de68a1a 100644
--- a/components/form-data/update-form-sheet.tsx
+++ b/components/form-data/update-form-sheet.tsx
@@ -104,8 +104,10 @@ export function UpdateTagSheet({
const isFieldEditable = React.useCallback((column: DataTableColumnJSON) => {
if (column.shi === true) return false; // SHI‑only
- if (column.key === "TAG_NO" || column.key === "TAG_DESC") return true;
- return editableFields.includes(column.key);
+ if (column.key === "TAG_NO" || column.key === "TAG_DESC") return false;
+ if (column.key === "status") return false;
+ // return editableFields.includes(column.key);
+ return true
}, [editableFields]);
const isFieldReadOnly = React.useCallback((column: DataTableColumnJSON) => !isFieldEditable(column), [isFieldEditable]);