diff --git a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx index 46b08753..f3adf081 100644 --- a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx +++ b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx @@ -542,10 +542,17 @@ export const InteractiveScreenViewerDynamic: React.FC = ({ + {/* 핵심 액션 */} 저장 삭제 편집 - 복사 (품목코드 초기화) 페이지 이동 - 데이터 전달 모달 열기 - 연관 데이터 버튼 모달 열기 - - (deprecated) 데이터 전달 + 모달 열기 - - 즉시 저장 - 제어 흐름 - 테이블 이력 보기 + 데이터 전달 + + {/* 엑셀 관련 */} 엑셀 다운로드 엑셀 업로드 + + {/* 고급 기능 */} + 즉시 저장 + 제어 흐름 + + {/* 특수 기능 (필요 시 사용) */} 바코드 스캔 - 코드 병합 - {/* 공차등록 */} 운행알림 및 종료 + + {/* 🔒 숨김 처리 - 기존 시스템 호환성 유지, UI에서만 숨김 + 복사 (품목코드 초기화) + 연관 데이터 버튼 모달 열기 + (deprecated) 데이터 전달 + 모달 열기 + 테이블 이력 보기 + 코드 병합 + 공차등록 + */} diff --git a/frontend/components/unified/UnifiedRepeater.tsx b/frontend/components/unified/UnifiedRepeater.tsx index 3ffe7545..1ed649b1 100644 --- a/frontend/components/unified/UnifiedRepeater.tsx +++ b/frontend/components/unified/UnifiedRepeater.tsx @@ -104,14 +104,30 @@ export const UnifiedRepeater: React.FC = ({ // 저장 이벤트 리스너 useEffect(() => { const handleSaveEvent = async (event: CustomEvent) => { - const tableName = config.dataSource?.tableName; + // 🆕 mainTableName이 설정된 경우 우선 사용, 없으면 dataSource.tableName 사용 + const tableName = config.useCustomTable && config.mainTableName + ? config.mainTableName + : config.dataSource?.tableName; const eventParentId = event.detail?.parentId; const mainFormData = event.detail?.mainFormData; + + // 🆕 마스터 테이블에서 생성된 ID (FK 연결용) + const masterRecordId = event.detail?.masterRecordId || mainFormData?.id; if (!tableName || data.length === 0) { + console.log("📋 UnifiedRepeater 저장 스킵:", { tableName, dataLength: data.length }); return; } + console.log("📋 UnifiedRepeater 저장 시작:", { + tableName, + useCustomTable: config.useCustomTable, + mainTableName: config.mainTableName, + foreignKeyColumn: config.foreignKeyColumn, + masterRecordId, + dataLength: data.length + }); + try { // 테이블 유효 컬럼 조회 let validColumns: Set = new Set(); @@ -130,12 +146,25 @@ export const UnifiedRepeater: React.FC = ({ // 내부 필드 제거 const cleanRow = Object.fromEntries(Object.entries(row).filter(([key]) => !key.startsWith("_"))); - // 메인 폼 데이터 병합 - const { id: _mainId, ...mainFormDataWithoutId } = mainFormData || {}; - const mergedData = { - ...mainFormDataWithoutId, - ...cleanRow, - }; + // 메인 폼 데이터 병합 (커스텀 테이블 사용 시에는 메인 폼 데이터 병합 안함) + let mergedData: Record; + if (config.useCustomTable && config.mainTableName) { + // 커스텀 테이블: 리피터 데이터만 저장 + mergedData = { ...cleanRow }; + + // 🆕 FK 자동 연결 + if (config.foreignKeyColumn && masterRecordId) { + mergedData[config.foreignKeyColumn] = masterRecordId; + console.log(`📎 FK 자동 연결: ${config.foreignKeyColumn} = ${masterRecordId}`); + } + } else { + // 기존 방식: 메인 폼 데이터 병합 + const { id: _mainId, ...mainFormDataWithoutId } = mainFormData || {}; + mergedData = { + ...mainFormDataWithoutId, + ...cleanRow, + }; + } // 유효하지 않은 컬럼 제거 const filteredData: Record = {}; @@ -148,9 +177,9 @@ export const UnifiedRepeater: React.FC = ({ await apiClient.post(`/table-management/tables/${tableName}/add`, filteredData); } - console.log("UnifiedRepeater 저장 완료:", data.length, "건"); + console.log("✅ UnifiedRepeater 저장 완료:", data.length, "건 →", tableName); } catch (error) { - console.error("UnifiedRepeater 저장 실패:", error); + console.error("❌ UnifiedRepeater 저장 실패:", error); throw error; } }; @@ -159,7 +188,7 @@ export const UnifiedRepeater: React.FC = ({ return () => { window.removeEventListener("repeaterSave" as any, handleSaveEvent); }; - }, [data, config.dataSource?.tableName, parentId]); + }, [data, config.dataSource?.tableName, config.useCustomTable, config.mainTableName, config.foreignKeyColumn, parentId]); // 현재 테이블 컬럼 정보 로드 useEffect(() => { @@ -631,6 +660,107 @@ export const UnifiedRepeater: React.FC = ({ }; }, [config.fieldName]); + // 🆕 데이터 전달 이벤트 리스너 (transferData 버튼 액션용) + useEffect(() => { + // componentDataTransfer: 특정 컴포넌트 ID로 데이터 전달 + const handleComponentDataTransfer = async (event: Event) => { + const customEvent = event as CustomEvent; + const { targetComponentId, data: transferData, mappingRules, mode } = customEvent.detail || {}; + + // 이 컴포넌트가 대상인지 확인 + if (targetComponentId !== parentId && targetComponentId !== config.fieldName) { + return; + } + + console.log("📥 [UnifiedRepeater] componentDataTransfer 수신:", { + targetComponentId, + dataCount: transferData?.length, + mode, + myId: parentId, + }); + + if (!transferData || transferData.length === 0) { + return; + } + + // 데이터 매핑 처리 + const mappedData = transferData.map((item: any, index: number) => { + const newRow: any = { _id: `transfer_${Date.now()}_${index}` }; + + if (mappingRules && mappingRules.length > 0) { + // 매핑 규칙이 있으면 적용 + mappingRules.forEach((rule: any) => { + newRow[rule.targetField] = item[rule.sourceField]; + }); + } else { + // 매핑 규칙 없으면 그대로 복사 + Object.assign(newRow, item); + } + + return newRow; + }); + + // mode에 따라 데이터 처리 + if (mode === "replace") { + handleDataChange(mappedData); + } else if (mode === "merge") { + // 중복 제거 후 병합 (id 기준) + const existingIds = new Set(data.map((row) => row.id || row._id)); + const newItems = mappedData.filter((row: any) => !existingIds.has(row.id || row._id)); + handleDataChange([...data, ...newItems]); + } else { + // 기본: append + handleDataChange([...data, ...mappedData]); + } + }; + + // splitPanelDataTransfer: 분할 패널에서 전역 이벤트로 전달 + const handleSplitPanelDataTransfer = async (event: Event) => { + const customEvent = event as CustomEvent; + const { data: transferData, mappingRules, mode, sourcePosition } = customEvent.detail || {}; + + console.log("📥 [UnifiedRepeater] splitPanelDataTransfer 수신:", { + dataCount: transferData?.length, + mode, + sourcePosition, + }); + + if (!transferData || transferData.length === 0) { + return; + } + + // 데이터 매핑 처리 + const mappedData = transferData.map((item: any, index: number) => { + const newRow: any = { _id: `transfer_${Date.now()}_${index}` }; + + if (mappingRules && mappingRules.length > 0) { + mappingRules.forEach((rule: any) => { + newRow[rule.targetField] = item[rule.sourceField]; + }); + } else { + Object.assign(newRow, item); + } + + return newRow; + }); + + // mode에 따라 데이터 처리 + if (mode === "replace") { + handleDataChange(mappedData); + } else { + handleDataChange([...data, ...mappedData]); + } + }; + + window.addEventListener("componentDataTransfer", handleComponentDataTransfer as EventListener); + window.addEventListener("splitPanelDataTransfer", handleSplitPanelDataTransfer as EventListener); + + return () => { + window.removeEventListener("componentDataTransfer", handleComponentDataTransfer as EventListener); + window.removeEventListener("splitPanelDataTransfer", handleSplitPanelDataTransfer as EventListener); + }; + }, [parentId, config.fieldName, data, handleDataChange]); + return (
{/* 헤더 영역 */} diff --git a/frontend/components/unified/config-panels/UnifiedRepeaterConfigPanel.tsx b/frontend/components/unified/config-panels/UnifiedRepeaterConfigPanel.tsx index 044fa037..6bfdc27c 100644 --- a/frontend/components/unified/config-panels/UnifiedRepeaterConfigPanel.tsx +++ b/frontend/components/unified/config-panels/UnifiedRepeaterConfigPanel.tsx @@ -529,6 +529,103 @@ export const UnifiedRepeaterConfigPanel: React.FC + {/* 저장 대상 테이블 설정 */} +
+ +

+ 화면 메인 테이블과 다른 테이블에 저장할 경우 설정 +

+ +
+ { + if (!checked) { + updateConfig({ + useCustomTable: false, + mainTableName: undefined, + foreignKeyColumn: undefined, + foreignKeySourceColumn: undefined, + }); + } else { + updateConfig({ useCustomTable: true }); + } + }} + /> + +
+ + {config.useCustomTable && ( +
+ {/* 저장 테이블 선택 */} +
+ + + updateConfig({ mainTableName: e.target.value })} + placeholder="테이블명 직접 입력 (예: receiving_detail)" + className="h-7 text-xs" + /> +
+ + {/* FK 컬럼 설정 */} +
+
+ + updateConfig({ foreignKeyColumn: e.target.value })} + placeholder="예: receiving_id" + className="h-7 text-xs" + /> +
+
+ + updateConfig({ foreignKeySourceColumn: e.target.value })} + placeholder="예: id" + className="h-7 text-xs" + /> +
+
+ + {config.mainTableName && config.foreignKeyColumn && ( +
+ 저장 흐름: {config.mainTableName}.{config.foreignKeyColumn} → + {currentTableName}.{config.foreignKeySourceColumn || "id"} (자동 연결) +
+ )} +
+ )} + + {!config.useCustomTable && ( +
+ 현재: 화면 메인 테이블 ({currentTableName || "미설정"})에 저장 +
+ )} +
+ + + {/* 현재 화면 정보 */}
diff --git a/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx b/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx index fbdaf6da..3c9aab47 100644 --- a/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx +++ b/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx @@ -743,6 +743,111 @@ export const TableListConfigPanel: React.FC = ({
테이블 리스트 설정
+ {/* 커스텀 테이블 설정 */} +
+
+

데이터 소스 테이블

+

화면 메인 테이블과 다른 테이블을 사용할 수 있습니다

+
+
+ + {/* 커스텀 테이블 사용 여부 */} +
+ { + handleChange("useCustomTable", checked); + if (!checked) { + // 커스텀 테이블 해제 시 화면 메인 테이블로 복원 + handleChange("customTableName", undefined); + handleChange("selectedTable", screenTableName); + } + }} + /> + +
+ + {config.useCustomTable && ( +
+ {/* 테이블 선택 */} +
+ + + + + + + + + + 테이블을 찾을 수 없습니다 + + {availableTables.map((table) => ( + { + handleChange("customTableName", table.tableName); + handleChange("selectedTable", table.tableName); + // 테이블 변경 시 컬럼 초기화 + handleChange("columns", []); + }} + className="text-xs" + > + + {table.displayName || table.tableName} + + ))} + + + + + +
+ + {/* 읽기전용 설정 */} +
+ handleChange("isReadOnly", checked)} + /> + +
+ + {config.customTableName && ( +
+ 현재 설정: {config.customTableName} 테이블 사용 + {config.isReadOnly && " (읽기전용)"} +
+ )} +
+ )} + + {!config.useCustomTable && screenTableName && ( +
+ 현재: 화면 메인 테이블 ({screenTableName}) 사용 +
+ )} +
+ {/* 테이블 제목 설정 */}
diff --git a/frontend/lib/registry/components/table-list/types.ts b/frontend/lib/registry/components/table-list/types.ts index 7adb87d1..a43ccdfa 100644 --- a/frontend/lib/registry/components/table-list/types.ts +++ b/frontend/lib/registry/components/table-list/types.ts @@ -235,6 +235,11 @@ export interface TableListConfig extends ComponentConfig { showHeader: boolean; showFooter: boolean; + // 🆕 커스텀 테이블 설정 (화면 메인 테이블과 다른 테이블 사용 시) + customTableName?: string; // 컴포넌트가 사용할 커스텀 테이블명 + useCustomTable?: boolean; // true면 customTableName 사용, false면 화면 메인 테이블 사용 + isReadOnly?: boolean; // 읽기전용 여부 (조회용 테이블인 경우 true) + // 체크박스 설정 checkbox: CheckboxConfig; diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index 34b32132..d0607be1 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -1533,6 +1533,7 @@ export class ButtonActionExecutor { tableName: context.tableName, mainFormDataKeys: Object.keys(mainFormData), saveResultData: saveResult?.data, + masterRecordId: savedId, // 🆕 마스터 레코드 ID (FK 연결용) }); window.dispatchEvent( new CustomEvent("repeaterSave", { @@ -1540,6 +1541,7 @@ export class ButtonActionExecutor { parentId: savedId, tableName: context.tableName, mainFormData, // 🆕 메인 폼 데이터 전달 + masterRecordId: savedId, // 🆕 마스터 레코드 ID (FK 자동 연결용) }, }), ); diff --git a/frontend/types/unified-repeater.ts b/frontend/types/unified-repeater.ts index 2ef5560c..c4274612 100644 --- a/frontend/types/unified-repeater.ts +++ b/frontend/types/unified-repeater.ts @@ -139,6 +139,12 @@ export interface UnifiedRepeaterConfig { // 렌더링 모드 renderMode: RepeaterRenderMode; + // 🆕 저장 대상 테이블 설정 (화면 메인 테이블과 다른 테이블에 저장할 경우) + mainTableName?: string; // 리피터 데이터가 저장될 테이블명 (미설정 시 화면 메인 테이블) + useCustomTable?: boolean; // true면 mainTableName 사용 + foreignKeyColumn?: string; // 마스터 테이블과 연결할 FK 컬럼명 (예: receiving_id) + foreignKeySourceColumn?: string; // 마스터 테이블의 PK 컬럼명 (예: id) - 자동 연결용 + // 데이터 소스 설정 dataSource: RepeaterDataSource;