From e19a28fa52511448d7095638038c03688f8589e0 Mon Sep 17 00:00:00 2001 From: leeheejin Date: Thu, 22 Jan 2026 09:59:20 +0900 Subject: [PATCH 1/7] =?UTF-8?q?=EC=97=91=EC=85=80=20=EC=97=85=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=EC=97=90=EC=84=9C=20=EA=B3=B5=EA=B8=89=EC=97=85?= =?UTF-8?q?=EC=B2=B4=20=EB=AA=85=EC=9D=B4=20=EC=95=88=EB=93=A4=EC=96=B4?= =?UTF-8?q?=EA=B0=80=EB=8D=98=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/common/ExcelUploadModal.tsx | 39 ++++++++++++++++--- 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/frontend/components/common/ExcelUploadModal.tsx b/frontend/components/common/ExcelUploadModal.tsx index 64fe38b8..cddbb73f 100644 --- a/frontend/components/common/ExcelUploadModal.tsx +++ b/frontend/components/common/ExcelUploadModal.tsx @@ -928,12 +928,39 @@ export const ExcelUploadModal: React.FC = ({ {field.inputType === "entity" ? ( + {/* 중복 체크 체크박스 */} +
+ {mapping.systemColumn ? ( + + handleDuplicateCheckChange(mapping.excelColumn, checked as boolean) + } + className="h-4 w-4" + /> + ) : ( + - + )} +
))} + {/* 중복 체크 안내 */} + {duplicateCheckCount > 0 ? ( +
+
+
+ +
+

+ 중복 키: {columnMappings + .filter((m) => m.checkDuplicate && m.systemColumn) + .map((m) => { + const col = systemColumns.find((c) => c.name === m.systemColumn); + return col?.label || m.systemColumn; + }) + .join(" + ")} +

+

+ 위 컬럼 값이 모두 일치하는 기존 데이터가 있으면 중복으로 처리합니다. +

+
+
+
+ 중복 시: + +
+
+
+ ) : ( +
+
+ +
+

중복 체크 (선택사항)

+

+ "중복 키" 체크박스를 선택하면 해당 컬럼 값으로 기존 데이터와 비교합니다. + 여러 컬럼을 선택하면 복합 키로 중복을 판단합니다. +

+
+
+
+ )} + {/* 매핑 자동 저장 안내 */} {isAutoMappingLoaded ? (
@@ -1298,6 +1497,11 @@ export const ExcelUploadModal: React.FC = ({

{mapping.excelColumn} →{" "} {col?.label || mapping.systemColumn} + {mapping.checkDuplicate && ( + + (중복 체크: {mapping.duplicateAction === "overwrite" ? "덮어쓰기" : "건너뛰기"}) + + )}

); })} @@ -1307,6 +1511,29 @@ export const ExcelUploadModal: React.FC = ({
+ {/* 중복 체크 요약 */} + {duplicateCheckCount > 0 && ( +
+

중복 체크 설정

+
+

+ 중복 키:{" "} + {columnMappings + .filter((m) => m.checkDuplicate && m.systemColumn) + .map((m) => { + const col = systemColumns.find((c) => c.name === m.systemColumn); + return col?.label || m.systemColumn; + }) + .join(" + ")} +

+

+ 중복 시 처리:{" "} + {duplicateAction === "overwrite" ? "덮어쓰기 (기존 데이터 업데이트)" : "건너뛰기 (해당 행 무시)"} +

+
+
+ )} +
From fad174859176ccdf6b3c542ab0abe00f6cb43024 Mon Sep 17 00:00:00 2001 From: leeheejin Date: Thu, 22 Jan 2026 14:17:11 +0900 Subject: [PATCH 4/7] =?UTF-8?q?=ED=87=B4=EC=82=AC=EC=9D=BC=20=EC=A0=9C?= =?UTF-8?q?=EB=8C=80=EB=A1=9C=20=EC=9E=91=EB=8F=99=EB=90=98=EA=B2=8C=20?= =?UTF-8?q?=ED=96=88=EA=B3=A0=20=ED=87=B4=EC=82=AC=EC=9D=BC=20=EC=A7=80?= =?UTF-8?q?=EC=9A=B0=EB=A9=B4=20=EC=98=A4=EB=A5=98=EB=82=AC=EB=8D=98?= =?UTF-8?q?=EA=B1=B0=20=EC=88=98=EC=A0=95=ED=96=88=EC=9D=8C=EC=9A=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/services/dynamicFormService.ts | 8 +- .../SplitPanelLayoutComponent.tsx | 121 +++++++++++++++--- 2 files changed, 109 insertions(+), 20 deletions(-) diff --git a/backend-node/src/services/dynamicFormService.ts b/backend-node/src/services/dynamicFormService.ts index 89d96859..9e0915ee 100644 --- a/backend-node/src/services/dynamicFormService.ts +++ b/backend-node/src/services/dynamicFormService.ts @@ -937,11 +937,17 @@ export class DynamicFormService { }) .join(", "); - // 🆕 JSONB 타입 값은 JSON 문자열로 변환 + // 🆕 JSONB 타입 값은 JSON 문자열로 변환, 빈 문자열은 null로 변환 const values: any[] = Object.keys(changedFields).map((key) => { const value = changedFields[key]; const dataType = columnTypes[key]; + // 🔧 빈 문자열은 null로 변환 (날짜 필드 등에서 값을 지울 때 필요) + if (value === "" || value === undefined) { + console.log(`🔄 빈 값 → null 변환: ${key}`); + return null; + } + // JSONB/JSON 타입이고 배열/객체인 경우 JSON 문자열로 변환 if ( (dataType === "jsonb" || dataType === "json") && diff --git a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx index 9b8e7cf0..dd03df3e 100644 --- a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx @@ -951,23 +951,43 @@ export const SplitPanelLayoutComponent: React.FC // 추가 dataFilter 적용 let filteredData = result.data || []; const dataFilter = componentConfig.rightPanel?.dataFilter; - if (dataFilter?.enabled && dataFilter.conditions?.length > 0) { + // 🔧 filters 또는 conditions 배열 모두 지원 + const filterConditions = dataFilter?.filters || dataFilter?.conditions || []; + if (dataFilter?.enabled && filterConditions.length > 0) { + console.log(`🔍 [기본탭] dataFilter 설정:`, JSON.stringify(dataFilter, null, 2)); + console.log(`🔍 [기본탭] 필터 전 데이터 수:`, filteredData.length); filteredData = filteredData.filter((item: any) => { - return dataFilter.conditions.every((cond: any) => { - const value = item[cond.column]; + return filterConditions.every((cond: any) => { + // 🔧 columnName 또는 column 필드 모두 지원 + const columnName = cond.columnName || cond.column; + const value = item[columnName]; const condValue = cond.value; + let result = true; switch (cond.operator) { case "equals": - return value === condValue; + result = value === condValue; + break; case "notEquals": - return value !== condValue; + result = value !== condValue; + break; case "contains": - return String(value).includes(String(condValue)); + result = String(value).includes(String(condValue)); + break; + case "is_null": + case "NULL": + result = value === null || value === undefined || value === ""; + break; + case "is_not_null": + case "NOT NULL": + result = value !== null && value !== undefined && value !== ""; + break; default: - return true; + result = true; } + return result; }); }); + console.log(`🔍 [기본탭] 필터 후 데이터 수:`, filteredData.length); } setRightData(filteredData); @@ -1080,23 +1100,48 @@ export const SplitPanelLayoutComponent: React.FC // 데이터 필터 적용 const dataFilter = tabConfig.dataFilter; - if (dataFilter?.enabled && dataFilter.conditions?.length > 0) { + console.log(`🔍 [추가탭 ${tabIndex}] dataFilter 설정:`, JSON.stringify(dataFilter, null, 2)); + // 🔧 filters 또는 conditions 배열 모두 지원 (DataFilterConfigPanel은 filters 사용) + const filterConditions = dataFilter?.filters || dataFilter?.conditions || []; + console.log(`🔍 [추가탭 ${tabIndex}] filterConditions:`, filterConditions); + console.log(`🔍 [추가탭 ${tabIndex}] 필터 전 데이터 수:`, resultData.length); + if (dataFilter?.enabled && filterConditions.length > 0) { + const beforeCount = resultData.length; resultData = resultData.filter((item: any) => { - return dataFilter.conditions.every((cond: any) => { - const value = item[cond.column]; + return filterConditions.every((cond: any) => { + // 🔧 columnName 또는 column 필드 모두 지원 + const columnName = cond.columnName || cond.column; + const value = item[columnName]; const condValue = cond.value; + let result = true; switch (cond.operator) { case "equals": - return value === condValue; + result = value === condValue; + break; case "notEquals": - return value !== condValue; + result = value !== condValue; + break; case "contains": - return String(value).includes(String(condValue)); + result = String(value).includes(String(condValue)); + break; + case "is_null": + case "NULL": + result = value === null || value === undefined || value === ""; + break; + case "is_not_null": + case "NOT NULL": + result = value !== null && value !== undefined && value !== ""; + break; default: - return true; + result = true; } + console.log(`🔍 [필터 체크] ${columnName}=${JSON.stringify(value)}, operator=${cond.operator}, result=${result}`); + return result; }); }); + console.log(`🔍 [추가탭 ${tabIndex}] 필터 후 데이터 수: ${beforeCount} → ${resultData.length}`); + } else { + console.log(`🔍 [추가탭 ${tabIndex}] 필터 비활성화 또는 조건 없음 (enabled=${dataFilter?.enabled}, conditions=${filterConditions.length})`); } // 중복 제거 적용 @@ -1557,6 +1602,7 @@ export const SplitPanelLayoutComponent: React.FC // 추가 버튼 핸들러 const handleAddClick = useCallback( (panel: "left" | "right") => { + console.log("🆕 [추가모달] handleAddClick 호출:", { panel, activeTabIndex }); setAddModalPanel(panel); // 우측 패널 추가 시, 좌측에서 선택된 항목의 조인 컬럼 값을 자동으로 채움 @@ -1567,16 +1613,19 @@ export const SplitPanelLayoutComponent: React.FC componentConfig.rightPanel?.rightColumn ) { const leftColumnValue = selectedLeftItem[componentConfig.leftPanel.leftColumn]; - setAddModalFormData({ + const initialData = { [componentConfig.rightPanel.rightColumn]: leftColumnValue, - }); + }; + console.log("🆕 [추가모달] 초기 데이터 설정:", initialData); + setAddModalFormData(initialData); } else { + console.log("🆕 [추가모달] 빈 데이터로 초기화"); setAddModalFormData({}); } setShowAddModal(true); }, - [selectedLeftItem, componentConfig], + [selectedLeftItem, componentConfig, activeTabIndex], ); // 수정 버튼 핸들러 @@ -1681,10 +1730,44 @@ export const SplitPanelLayoutComponent: React.FC // 기존 자동 편집 모드 (인라인 편집 모달) setEditModalPanel(panel); setEditModalItem(item); - setEditModalFormData({ ...item }); + + // 🔧 우측 패널(추가탭 포함) 수정 시 selectedLeftItem의 FK 값 병합 + let mergedItem = { ...item }; + if (panel === "right" && selectedLeftItem) { + // 현재 활성 탭의 relation 설정 가져오기 + const currentTabConfig = + activeTabIndex === 0 + ? componentConfig.rightPanel + : componentConfig.rightPanel?.additionalTabs?.[activeTabIndex - 1]; + + const relationKeys = currentTabConfig?.relation?.keys; + const leftColumn = currentTabConfig?.relation?.leftColumn; + const rightColumn = currentTabConfig?.relation?.foreignKey || currentTabConfig?.relation?.rightColumn; + + if (relationKeys && relationKeys.length > 0) { + // 복합키인 경우 + relationKeys.forEach((key: any) => { + if (key.leftColumn && key.rightColumn && selectedLeftItem[key.leftColumn] !== undefined) { + // item에 해당 FK 값이 없거나 빈 값이면 selectedLeftItem에서 가져옴 + if (mergedItem[key.rightColumn] === undefined || mergedItem[key.rightColumn] === null || mergedItem[key.rightColumn] === "") { + mergedItem[key.rightColumn] = selectedLeftItem[key.leftColumn]; + } + } + }); + } else if (leftColumn && rightColumn) { + // 단일키인 경우 + if (selectedLeftItem[leftColumn] !== undefined) { + if (mergedItem[rightColumn] === undefined || mergedItem[rightColumn] === null || mergedItem[rightColumn] === "") { + mergedItem[rightColumn] = selectedLeftItem[leftColumn]; + } + } + } + } + + setEditModalFormData(mergedItem); setShowEditModal(true); }, - [componentConfig], + [componentConfig, selectedLeftItem, activeTabIndex], ); // 수정 모달 저장 From 1720b6c8265f7ea27afb68d041585f63a1cba70e Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Thu, 22 Jan 2026 14:44:49 +0900 Subject: [PATCH 5/7] =?UTF-8?q?fix(frontend):=20ModernDatePicker=20?= =?UTF-8?q?=EB=82=A0=EC=A7=9C=20=EB=B2=94=EC=9C=84=20=EC=9E=90=EB=8F=99=20?= =?UTF-8?q?=EC=A0=95=EB=A0=AC=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EB=B6=88=EA=B0=80=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/screen/filters/ModernDatePicker.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/frontend/components/screen/filters/ModernDatePicker.tsx b/frontend/components/screen/filters/ModernDatePicker.tsx index 0a134927..54fdcfed 100644 --- a/frontend/components/screen/filters/ModernDatePicker.tsx +++ b/frontend/components/screen/filters/ModernDatePicker.tsx @@ -84,8 +84,20 @@ export const ModernDatePicker: React.FC = ({ label, value }; const handleConfirm = () => { + // 날짜 순서 자동 정렬 + let finalValue = { ...tempValue }; + + if (finalValue.from && finalValue.to) { + // from이 to보다 나중이면 swap + if (finalValue.from > finalValue.to) { + const temp = finalValue.from; + finalValue.from = finalValue.to; + finalValue.to = temp; + } + } + // 확인 버튼을 눌렀을 때만 onChange 호출 - onChange(tempValue); + onChange(finalValue); setIsOpen(false); setSelectingType("from"); }; From ef32de3087eff783844e40db01e82475b2f1ca29 Mon Sep 17 00:00:00 2001 From: leeheejin Date: Thu, 22 Jan 2026 16:05:18 +0900 Subject: [PATCH 6/7] =?UTF-8?q?=EB=B2=94=EC=9A=A9=ED=8F=BC=EB=AA=A8?= =?UTF-8?q?=EB=8B=AC=20=EB=8D=B0=EC=9D=B4=ED=84=B0=EB=B6=80=EB=B6=84=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=82=AD=EC=A0=9C=ED=95=B4?= =?UTF-8?q?=EB=8F=84=20=EC=95=88=EC=A7=80=EC=9B=8C=EC=A7=80=EB=8D=98?= =?UTF-8?q?=EA=B1=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/components/screen/EditModal.tsx | 9 ++- .../UniversalFormModalComponent.tsx | 18 ++++-- frontend/lib/utils/buttonActions.ts | 60 +++++++++++++------ 3 files changed, 64 insertions(+), 23 deletions(-) diff --git a/frontend/components/screen/EditModal.tsx b/frontend/components/screen/EditModal.tsx index 48bf898f..106787cf 100644 --- a/frontend/components/screen/EditModal.tsx +++ b/frontend/components/screen/EditModal.tsx @@ -1039,8 +1039,15 @@ export const EditModal: React.FC = ({ className }) => { } ); + // 🆕 _tableSection_ 데이터가 있는지 확인 (TableSectionRenderer 사용 시) + // _tableSection_ 데이터가 있으면 buttonActions.ts의 handleUniversalFormModalTableSectionSave가 처리 + const hasTableSectionData = Object.keys(formData).some(k => + k.startsWith("_tableSection_") || k.startsWith("__tableSection_") + ); + // 🆕 그룹 데이터가 있으면 EditModal.handleSave 사용 (일괄 저장) - const shouldUseEditModalSave = groupData.length > 0 || !hasUniversalFormModal; + // 단, _tableSection_ 데이터가 있으면 EditModal.handleSave 사용하지 않음 (buttonActions.ts가 처리) + const shouldUseEditModalSave = !hasTableSectionData && (groupData.length > 0 || !hasUniversalFormModal); // 🔑 첨부파일 컴포넌트가 행(레코드) 단위로 파일을 저장할 수 있도록 tableName 추가 const enrichedFormData = { diff --git a/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx b/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx index 939bb5d5..5fc24920 100644 --- a/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx +++ b/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx @@ -439,6 +439,12 @@ export function UniversalFormModalComponent({ event.detail.formData[normalizedKey] = value; console.log(`[UniversalFormModal] 테이블 섹션 병합: ${key} → ${normalizedKey}, ${value.length}개 항목`); } + + // 🆕 원본 테이블 섹션 데이터도 병합 (삭제 추적용) + if (key.startsWith("_originalTableSectionData_") && Array.isArray(value)) { + event.detail.formData[key] = value; + console.log(`[UniversalFormModal] 원본 테이블 섹션 데이터 병합: ${key}, ${value.length}개 항목`); + } } // 🆕 수정 모드: 원본 그룹 데이터 전달 (UPDATE/DELETE 추적용) @@ -928,17 +934,19 @@ export function UniversalFormModalComponent({ newFormData[tableSectionKey] = items; console.log(`[initializeForm] 테이블 섹션 ${section.id}: formData[${tableSectionKey}]에 저장됨`); - // 🆕 원본 그룹 데이터 저장 (삭제 추적용) - // groupedDataInitializedRef가 false일 때만 설정 (true면 _groupedData useEffect에서 이미 처리됨) - // DB에서 로드한 데이터를 originalGroupedData에 저장해야 삭제 시 비교 가능 + // 🆕 테이블 섹션 원본 데이터 저장 (삭제 추적용) + // 각 테이블 섹션별로 별도의 키에 원본 데이터 저장 (groupedDataInitializedRef와 무관하게 항상 저장) + const originalTableSectionKey = `_originalTableSectionData_${section.id}`; + newFormData[originalTableSectionKey] = JSON.parse(JSON.stringify(items)); + console.log(`[initializeForm] 테이블 섹션 ${section.id}: formData[${originalTableSectionKey}]에 원본 ${items.length}건 저장`); + + // 기존 originalGroupedData에도 추가 (하위 호환성) if (!groupedDataInitializedRef.current) { setOriginalGroupedData((prev) => { const newOriginal = [...prev, ...JSON.parse(JSON.stringify(items))]; console.log(`[initializeForm] 테이블 섹션 ${section.id}: originalGroupedData에 ${items.length}건 추가 (총 ${newOriginal.length}건)`); return newOriginal; }); - } else { - console.log(`[initializeForm] 테이블 섹션 ${section.id}: _groupedData로 이미 초기화됨, originalGroupedData 설정 스킵`); } } } catch (error) { diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index a4b6074c..b8d37c19 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -522,21 +522,7 @@ export class ButtonActionExecutor { } console.log("✅ [handleSave] 필수 항목 검증 통과"); - // 🆕 EditModal 등에서 전달된 onSave 콜백이 있으면 우선 사용 - if (onSave) { - console.log("✅ [handleSave] onSave 콜백 발견 - 콜백 실행"); - try { - await onSave(); - return true; - } catch (error) { - console.error("❌ [handleSave] onSave 콜백 실행 오류:", error); - throw error; - } - } - - console.log("⚠️ [handleSave] onSave 콜백 없음 - 기본 저장 로직 실행"); - - // 🆕 저장 전 이벤트 발생 (SelectedItemsDetailInput 등에서 최신 데이터 수집) + // 🆕 저장 전 이벤트 먼저 발생 (UniversalFormModal의 __tableSection_ 데이터 병합을 위해) // context.formData를 이벤트 detail에 포함하여 직접 수정 가능하게 함 // skipDefaultSave 플래그를 통해 기본 저장 로직을 건너뛸 수 있음 const beforeSaveEventDetail = { @@ -552,13 +538,38 @@ export class ButtonActionExecutor { // 약간의 대기 시간을 주어 이벤트 핸들러가 formData를 업데이트할 수 있도록 함 await new Promise((resolve) => setTimeout(resolve, 100)); + console.log("📦 [handleSave] beforeFormSave 이벤트 후 formData keys:", Object.keys(context.formData || {})); + // 🔧 skipDefaultSave 플래그 확인 - SelectedItemsDetailInput 등에서 자체 UPSERT 처리 시 기본 저장 건너뛰기 if (beforeSaveEventDetail.skipDefaultSave) { console.log("🚫 [handleSave] skipDefaultSave=true - 기본 저장 로직 건너뛰기 (컴포넌트에서 자체 처리)"); return true; } - console.log("📦 [handleSave] beforeFormSave 이벤트 후 formData:", context.formData); + // 🆕 _tableSection_ 데이터가 있는지 확인 (TableSectionRenderer 사용 시) + // beforeFormSave 이벤트 후에 체크해야 UniversalFormModal에서 병합된 데이터를 확인할 수 있음 + const hasTableSectionData = Object.keys(context.formData || {}).some(k => + k.startsWith("_tableSection_") || k.startsWith("__tableSection_") + ); + + if (hasTableSectionData) { + console.log("📋 [handleSave] _tableSection_ 데이터 감지 - onSave 콜백 건너뛰고 테이블 섹션 저장 로직 사용"); + } + + // 🆕 EditModal 등에서 전달된 onSave 콜백이 있으면 우선 사용 + // 단, _tableSection_ 데이터가 있으면 건너뛰기 (handleUniversalFormModalTableSectionSave가 처리) + if (onSave && !hasTableSectionData) { + console.log("✅ [handleSave] onSave 콜백 발견 - 콜백 실행 (테이블 섹션 데이터 없음)"); + try { + await onSave(); + return true; + } catch (error) { + console.error("❌ [handleSave] onSave 콜백 실행 오류:", error); + throw error; + } + } + + console.log("⚠️ [handleSave] 기본 저장 로직 실행 (onSave 콜백 없음 또는 _tableSection_ 데이터 있음)"); // 🆕 렉 구조 컴포넌트 일괄 저장 감지 let rackStructureLocations: any[] | undefined; @@ -2238,9 +2249,24 @@ export class ButtonActionExecutor { } // 3️⃣ 삭제된 품목 DELETE (원본에는 있지만 현재에는 없는 항목) + // 🆕 테이블 섹션별 원본 데이터 사용 (우선), 없으면 전역 originalGroupedData 사용 + const sectionOriginalKey = `_originalTableSectionData_${sectionId}`; + const sectionOriginalData: any[] = modalData[sectionOriginalKey] || formData[sectionOriginalKey] || []; + + // 섹션별 원본 데이터가 있으면 사용, 없으면 전역 originalGroupedData 사용 + const originalDataForDelete = sectionOriginalData.length > 0 ? sectionOriginalData : originalGroupedData; + + console.log(`🔍 [DELETE 비교] 섹션 ${sectionId}:`, { + sectionOriginalKey, + sectionOriginalCount: sectionOriginalData.length, + globalOriginalCount: originalGroupedData.length, + usingData: sectionOriginalData.length > 0 ? "섹션별 원본" : "전역 원본", + currentCount: currentItems.length + }); + // ⚠️ id 타입 통일: 문자열로 변환하여 비교 (숫자 vs 문자열 불일치 방지) const currentIds = new Set(currentItems.map((item) => String(item.id)).filter(Boolean)); - const deletedItems = originalGroupedData.filter((orig) => orig.id && !currentIds.has(String(orig.id))); + const deletedItems = originalDataForDelete.filter((orig) => orig.id && !currentIds.has(String(orig.id))); for (const deletedItem of deletedItems) { console.log(`🗑️ [DELETE] 품목 삭제: id=${deletedItem.id}, tableName=${saveTableName}`); From 294e0e4e182c18886a4fa88029003c71a1e72bb0 Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Thu, 22 Jan 2026 19:59:28 +0900 Subject: [PATCH 7/7] =?UTF-8?q?fix:=20=EB=B6=84=ED=95=A0=20=ED=8C=A8?= =?UTF-8?q?=EB=84=90=20=EC=B6=94=EA=B0=80=20=ED=83=AD(additionalTabs)=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EB=B2=84=ED=8A=BC=EC=9D=B4=20=ED=95=B4?= =?UTF-8?q?=EB=8B=B9=20=ED=83=AD=EC=9D=98=20editButton=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=EC=9D=84=20=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SplitPanelLayoutComponent.tsx | 180 ++++++++++-------- 1 file changed, 101 insertions(+), 79 deletions(-) diff --git a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx index 9b8e7cf0..8560db4e 100644 --- a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx @@ -1583,98 +1583,120 @@ export const SplitPanelLayoutComponent: React.FC const handleEditClick = useCallback( (panel: "left" | "right", item: any) => { // 🆕 우측 패널 수정 버튼 설정 확인 - if (panel === "right" && componentConfig.rightPanel?.editButton?.mode === "modal") { - const modalScreenId = componentConfig.rightPanel?.editButton?.modalScreenId; + // 🔧 현재 활성 탭에 따라 해당 탭의 editButton 설정 사용 + if (panel === "right") { + // 기본 탭(0)이면 rightPanel.editButton, 추가 탭이면 additionalTabs의 editButton 사용 + const editButtonConfig = + activeTabIndex === 0 + ? componentConfig.rightPanel?.editButton + : componentConfig.rightPanel?.additionalTabs?.[activeTabIndex - 1]?.editButton; - if (modalScreenId) { - // 커스텀 모달 화면 열기 - const rightTableName = componentConfig.rightPanel?.tableName || ""; + // 해당 탭의 테이블명 가져오기 + const currentTableName = + activeTabIndex === 0 + ? componentConfig.rightPanel?.tableName || "" + : componentConfig.rightPanel?.additionalTabs?.[activeTabIndex - 1]?.tableName || ""; - // Primary Key 찾기 (우선순위: 설정값 > id > ID > non-null 필드) - // 🔧 설정에서 primaryKeyColumn 지정 가능 - const configuredPrimaryKey = componentConfig.rightPanel?.editButton?.primaryKeyColumn; + console.log("🔧 [SplitPanel] 수정 버튼 클릭 - 현재 탭 설정 확인:", { + activeTabIndex, + editButtonConfig, + currentTableName, + isModalMode: editButtonConfig?.mode === "modal", + }); - let primaryKeyName = "id"; - let primaryKeyValue: any; + if (editButtonConfig?.mode === "modal") { + const modalScreenId = editButtonConfig.modalScreenId; - if (configuredPrimaryKey && item[configuredPrimaryKey] !== undefined && item[configuredPrimaryKey] !== null) { - // 설정된 Primary Key 사용 - primaryKeyName = configuredPrimaryKey; - primaryKeyValue = item[configuredPrimaryKey]; - } else if (item.id !== undefined && item.id !== null) { - primaryKeyName = "id"; - primaryKeyValue = item.id; - } else if (item.ID !== undefined && item.ID !== null) { - primaryKeyName = "ID"; - primaryKeyValue = item.ID; - } else { - // 🔧 첫 번째 non-null 필드를 Primary Key로 간주 - const keys = Object.keys(item); - let found = false; - for (const key of keys) { - if (item[key] !== undefined && item[key] !== null) { - primaryKeyName = key; - primaryKeyValue = item[key]; - found = true; - break; + if (modalScreenId) { + // 커스텀 모달 화면 열기 + + // Primary Key 찾기 (우선순위: 설정값 > id > ID > non-null 필드) + // 🔧 설정에서 primaryKeyColumn 지정 가능 + const configuredPrimaryKey = editButtonConfig.primaryKeyColumn; + + let primaryKeyName = "id"; + let primaryKeyValue: any; + + if (configuredPrimaryKey && item[configuredPrimaryKey] !== undefined && item[configuredPrimaryKey] !== null) { + // 설정된 Primary Key 사용 + primaryKeyName = configuredPrimaryKey; + primaryKeyValue = item[configuredPrimaryKey]; + } else if (item.id !== undefined && item.id !== null) { + primaryKeyName = "id"; + primaryKeyValue = item.id; + } else if (item.ID !== undefined && item.ID !== null) { + primaryKeyName = "ID"; + primaryKeyValue = item.ID; + } else { + // 🔧 첫 번째 non-null 필드를 Primary Key로 간주 + const keys = Object.keys(item); + let found = false; + for (const key of keys) { + if (item[key] !== undefined && item[key] !== null) { + primaryKeyName = key; + primaryKeyValue = item[key]; + found = true; + break; + } + } + // 모든 필드가 null이면 첫 번째 필드 사용 + if (!found && keys.length > 0) { + primaryKeyName = keys[0]; + primaryKeyValue = item[keys[0]]; } } - // 모든 필드가 null이면 첫 번째 필드 사용 - if (!found && keys.length > 0) { - primaryKeyName = keys[0]; - primaryKeyValue = item[keys[0]]; - } - } - console.log("✅ 수정 모달 열기:", { - tableName: rightTableName, - primaryKeyName, - primaryKeyValue, - screenId: modalScreenId, - fullItem: item, - }); + console.log("✅ 수정 모달 열기:", { + activeTabIndex, + tableName: currentTableName, + primaryKeyName, + primaryKeyValue, + screenId: modalScreenId, + fullItem: item, + }); - // modalDataStore에도 저장 (호환성 유지) - import("@/stores/modalDataStore").then(({ useModalDataStore }) => { - useModalDataStore.getState().setData(rightTableName, [item]); - }); + // modalDataStore에도 저장 (호환성 유지) + import("@/stores/modalDataStore").then(({ useModalDataStore }) => { + useModalDataStore.getState().setData(currentTableName, [item]); + }); - // 🆕 groupByColumns 추출 - const groupByColumns = componentConfig.rightPanel?.editButton?.groupByColumns || []; + // 🆕 groupByColumns 추출 + const groupByColumns = editButtonConfig.groupByColumns || []; - console.log("🔧 [SplitPanel] 수정 버튼 클릭 - groupByColumns 확인:", { - groupByColumns, - editButtonConfig: componentConfig.rightPanel?.editButton, - hasGroupByColumns: groupByColumns.length > 0, - }); + console.log("🔧 [SplitPanel] 수정 버튼 클릭 - groupByColumns 확인:", { + groupByColumns, + editButtonConfig, + hasGroupByColumns: groupByColumns.length > 0, + }); - // ScreenModal 열기 이벤트 발생 (URL 파라미터로 ID + groupByColumns + primaryKeyColumn 전달) - window.dispatchEvent( - new CustomEvent("openScreenModal", { - detail: { - screenId: modalScreenId, - urlParams: { - mode: "edit", - editId: primaryKeyValue, - tableName: rightTableName, - primaryKeyColumn: primaryKeyName, // 🆕 Primary Key 컬럼명 전달 - ...(groupByColumns.length > 0 && { - groupByColumns: JSON.stringify(groupByColumns), - }), + // ScreenModal 열기 이벤트 발생 (URL 파라미터로 ID + groupByColumns + primaryKeyColumn 전달) + window.dispatchEvent( + new CustomEvent("openScreenModal", { + detail: { + screenId: modalScreenId, + urlParams: { + mode: "edit", + editId: primaryKeyValue, + tableName: currentTableName, + primaryKeyColumn: primaryKeyName, // 🆕 Primary Key 컬럼명 전달 + ...(groupByColumns.length > 0 && { + groupByColumns: JSON.stringify(groupByColumns), + }), + }, }, - }, - }), - ); + }), + ); - console.log("✅ [SplitPanel] openScreenModal 이벤트 발생:", { - screenId: modalScreenId, - editId: primaryKeyValue, - tableName: rightTableName, - primaryKeyColumn: primaryKeyName, - groupByColumns: groupByColumns.length > 0 ? JSON.stringify(groupByColumns) : "없음", - }); + console.log("✅ [SplitPanel] openScreenModal 이벤트 발생:", { + screenId: modalScreenId, + editId: primaryKeyValue, + tableName: currentTableName, + primaryKeyColumn: primaryKeyName, + groupByColumns: groupByColumns.length > 0 ? JSON.stringify(groupByColumns) : "없음", + }); - return; + return; + } } } @@ -1684,7 +1706,7 @@ export const SplitPanelLayoutComponent: React.FC setEditModalFormData({ ...item }); setShowEditModal(true); }, - [componentConfig], + [componentConfig, activeTabIndex], ); // 수정 모달 저장