From d358de60d6353199c69effebbf8325527b6e3a8c Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Wed, 11 Mar 2026 14:41:14 +0900 Subject: [PATCH] [agent-pipeline] pipe-20260311052455-y968 round-2 --- .../components/common/ConfigPanelTypes.ts | 24 ++ .../lib/utils/getComponentConfigPanel.tsx | 314 +++++------------- 2 files changed, 106 insertions(+), 232 deletions(-) diff --git a/frontend/lib/registry/components/common/ConfigPanelTypes.ts b/frontend/lib/registry/components/common/ConfigPanelTypes.ts index a60aa987..cbd31ab9 100644 --- a/frontend/lib/registry/components/common/ConfigPanelTypes.ts +++ b/frontend/lib/registry/components/common/ConfigPanelTypes.ts @@ -54,3 +54,27 @@ export interface ConfigPanelBuilderProps { tableColumns?: ConfigOption[]; children?: React.ReactNode; } + +/** + * 설정 패널에 전달되는 화면/컴포넌트 컨텍스트 정보 + */ +export interface ConfigPanelContext { + tables?: any[]; + tableColumns?: any[]; + screenTableName?: string; + menuObjid?: number; + allComponents?: any[]; + currentComponent?: any; + allTables?: any[]; + screenComponents?: any[]; + currentScreenCompanyCode?: string; +} + +/** + * 모든 ConfigPanel이 공통으로 받는 표준 Props + */ +export interface StandardConfigPanelProps { + config: Record; + onChange: (config: Record) => void; + context?: ConfigPanelContext; +} diff --git a/frontend/lib/utils/getComponentConfigPanel.tsx b/frontend/lib/utils/getComponentConfigPanel.tsx index 3f201bd4..ef770a2e 100644 --- a/frontend/lib/utils/getComponentConfigPanel.tsx +++ b/frontend/lib/utils/getComponentConfigPanel.tsx @@ -3,9 +3,9 @@ */ import React from "react"; +import type { ConfigPanelContext } from "@/lib/registry/components/common/ConfigPanelTypes"; // 컴포넌트별 ConfigPanel 동적 import 맵 -// 모든 ConfigPanel이 있는 컴포넌트를 여기에 등록해야 슬롯/중첩 컴포넌트에서 전용 설정 패널이 표시됨 const CONFIG_PANEL_MAP: Record Promise> = { // ========== V2 컴포넌트 ========== "v2-input": () => import("@/components/v2/config-panels/V2InputConfigPanel"), @@ -123,22 +123,18 @@ const CONFIG_PANEL_MAP: Record Promise> = { "badge-status": () => import("@/components/screen/config-panels/BadgeConfigPanel"), }; -// ConfigPanel 컴포넌트 캐시 const configPanelCache = new Map>(); /** * 컴포넌트 ID로 ConfigPanel 컴포넌트를 동적으로 로드 */ export async function getComponentConfigPanel(componentId: string): Promise | null> { - // 캐시에서 먼저 확인 if (configPanelCache.has(componentId)) { return configPanelCache.get(componentId)!; } - // 매핑에서 import 함수 찾기 const importFn = CONFIG_PANEL_MAP[componentId]; if (!importFn) { - console.warn(`컴포넌트 "${componentId}"에 대한 ConfigPanel을 찾을 수 없습니다.`); return null; } @@ -151,11 +147,9 @@ export async function getComponentConfigPanel(componentId: string): Promise { for (const key of Object.keys(module)) { if (key.endsWith("ConfigPanel") && typeof module[key] === "function") { @@ -176,9 +170,7 @@ export async function getComponentConfigPanel(componentId: string): Promise; onChange: (config: Record) => void; - screenTableName?: string; // 화면에서 지정한 테이블명 - tableColumns?: any[]; // 테이블 컬럼 정보 - tables?: any[]; // 전체 테이블 목록 - menuObjid?: number; // 🆕 메뉴 OBJID (코드/카테고리/채번규칙 스코프용) - allComponents?: any[]; // 🆕 현재 화면의 모든 컴포넌트 (연쇄 드롭다운 부모 감지용) - currentComponent?: any; // 🆕 현재 컴포넌트 정보 + screenTableName?: string; + tableColumns?: any[]; + tables?: any[]; + menuObjid?: number; + allComponents?: any[]; + currentComponent?: any; } export const DynamicComponentConfigPanel: React.FC = ({ @@ -237,53 +219,41 @@ export const DynamicComponentConfigPanel: React.FC = allComponents, currentComponent, }) => { - // 모든 useState를 최상단에 선언 (Hooks 규칙) const [ConfigPanelComponent, setConfigPanelComponent] = React.useState | null>(null); const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(null); const [selectedTableColumns, setSelectedTableColumns] = React.useState(tableColumns); const [allTablesList, setAllTablesList] = React.useState([]); - - // 🆕 selected-items-detail-input 전용 상태 const [sourceTableColumns, setSourceTableColumns] = React.useState([]); const [targetTableColumns, setTargetTableColumns] = React.useState([]); React.useEffect(() => { let mounted = true; - async function loadConfigPanel() { try { setLoading(true); setError(null); - const component = await getComponentConfigPanel(componentId); - if (mounted) { setConfigPanelComponent(() => component); setLoading(false); } } catch (err) { - console.error(`❌ DynamicComponentConfigPanel: ${componentId} 로드 실패:`, err); if (mounted) { setError(err instanceof Error ? err.message : String(err)); setLoading(false); } } } - loadConfigPanel(); - - return () => { - mounted = false; - }; + return () => { mounted = false; }; }, [componentId]); - // tableColumns가 변경되면 selectedTableColumns도 업데이트 React.useEffect(() => { setSelectedTableColumns(tableColumns); }, [tableColumns]); - // RepeaterConfigPanel과 selected-items-detail-input에서 전체 테이블 목록 로드 + // repeater-field-group / selected-items-detail-input에서 전체 테이블 목록 로드 React.useEffect(() => { if (componentId === "repeater-field-group" || componentId === "selected-items-detail-input") { const loadAllTables = async () => { @@ -291,100 +261,57 @@ export const DynamicComponentConfigPanel: React.FC = const { tableManagementApi } = await import("@/lib/api/tableManagement"); const response = await tableManagementApi.getTableList(); if (response.success && response.data) { - console.log(`✅ 전체 테이블 목록 로드 완료 (${componentId}):`, response.data.length); setAllTablesList(response.data); } - } catch (error) { - console.error("전체 테이블 목록 로드 실패:", error); + } catch (_) { + // 전체 테이블 목록 로드 실패 시 무시 } }; loadAllTables(); } }, [componentId]); - // 🆕 selected-items-detail-input: 초기 sourceTable/targetTable 컬럼 로드 + // selected-items-detail-input: 초기 sourceTable/targetTable 컬럼 로드 React.useEffect(() => { - if (componentId === "selected-items-detail-input") { - console.log("🔍 selected-items-detail-input 초기 설정:", config); - - // 원본 테이블 컬럼 로드 - if (config.sourceTable) { - const loadSourceColumns = async () => { - try { - const { tableTypeApi } = await import("@/lib/api/screen"); - const columnsResponse = await tableTypeApi.getColumns(config.sourceTable); + if (componentId !== "selected-items-detail-input") return; - const columns = (columnsResponse || []).map((col: any) => ({ - columnName: col.columnName || col.column_name, - columnLabel: col.displayName || col.columnLabel || col.column_label || col.columnName || col.column_name, - dataType: col.dataType || col.data_type || col.dbType, - inputType: col.inputType || col.input_type, // 🆕 inputType 추가 - })); - - console.log("✅ 원본 테이블 컬럼 초기 로드 완료:", columns.length); - setSourceTableColumns(columns); - } catch (error) { - console.error("❌ 원본 테이블 컬럼 초기 로드 실패:", error); - } - }; - loadSourceColumns(); + const loadColumns = async (tableName: string, setter: React.Dispatch>, includeCodeCategory?: boolean) => { + try { + const { tableTypeApi } = await import("@/lib/api/screen"); + const columnsResponse = await tableTypeApi.getColumns(tableName); + const columns = (columnsResponse || []).map((col: any) => ({ + columnName: col.columnName || col.column_name, + columnLabel: col.displayName || col.columnLabel || col.column_label || col.columnName || col.column_name, + dataType: col.dataType || col.data_type || col.dbType, + inputType: col.inputType || col.input_type, + ...(includeCodeCategory ? { codeCategory: col.codeCategory || col.code_category } : {}), + })); + setter(columns); + } catch (_) { + setter([]); } - - // 대상 테이블 컬럼 로드 - if (config.targetTable) { - const loadTargetColumns = async () => { - try { - const { tableTypeApi } = await import("@/lib/api/screen"); - const columnsResponse = await tableTypeApi.getColumns(config.targetTable); + }; - const columns = (columnsResponse || []).map((col: any) => ({ - columnName: col.columnName || col.column_name, - columnLabel: col.displayName || col.columnLabel || col.column_label || col.columnName || col.column_name, - dataType: col.dataType || col.data_type || col.dbType, - inputType: col.inputType || col.input_type, // 🆕 inputType 추가 - codeCategory: col.codeCategory || col.code_category, // 🆕 codeCategory 추가 - })); - - console.log("✅ 대상 테이블 컬럼 초기 로드 완료:", columns.length); - setTargetTableColumns(columns); - } catch (error) { - console.error("❌ 대상 테이블 컬럼 초기 로드 실패:", error); - } - }; - loadTargetColumns(); - } - } + if (config.sourceTable) loadColumns(config.sourceTable, setSourceTableColumns); + if (config.targetTable) loadColumns(config.targetTable, setTargetTableColumns, true); }, [componentId, config.sourceTable, config.targetTable]); - // 🆕 allComponents를 screenComponents 형태로 변환 (집계 위젯 등에서 사용) - // Hooks 규칙: 조건부 return 전에 선언해야 함 const screenComponents = React.useMemo(() => { - if (!allComponents) { - console.log("[getComponentConfigPanel] allComponents is undefined or null"); - return []; - } - console.log("[getComponentConfigPanel] allComponents 변환 시작:", allComponents.length, "개"); - const result = allComponents.map((comp: any) => { - const columnName = comp.columnName || comp.componentConfig?.columnName || comp.componentConfig?.fieldName; - console.log(`[getComponentConfigPanel] comp: ${comp.id}, type: ${comp.componentType || comp.type}, columnName: ${columnName}`); - return { - id: comp.id, - componentType: comp.componentType || comp.type, - label: comp.label || comp.name || comp.id, - tableName: comp.componentConfig?.tableName || comp.tableName, - // 🆕 폼 필드 인식용 columnName 추가 - columnName, - }; - }); - console.log("[getComponentConfigPanel] screenComponents 변환 완료:", result); - return result; + if (!allComponents) return []; + return allComponents.map((comp: any) => ({ + id: comp.id, + componentType: comp.componentType || comp.type, + label: comp.label || comp.name || comp.id, + tableName: comp.componentConfig?.tableName || comp.tableName, + columnName: comp.columnName || comp.componentConfig?.columnName || comp.componentConfig?.fieldName, + })); }, [allComponents]); if (loading) { return (
- ⏳ 로딩 중... + 로딩 중...

설정 패널을 불러오는 중입니다.

@@ -395,7 +322,7 @@ export const DynamicComponentConfigPanel: React.FC = return (
- ⚠️ 로드 실패 + 로드 실패

설정 패널을 불러올 수 없습니다: {error}

@@ -403,31 +330,26 @@ export const DynamicComponentConfigPanel: React.FC = } if (!ConfigPanelComponent) { - console.warn(`⚠️ DynamicComponentConfigPanel: ${componentId} ConfigPanelComponent가 null`); return (
- ⚠️ 설정 패널 없음 + 설정 패널 없음
-

컴포넌트 "{componentId}"에 대한 설정 패널이 없습니다.

+

컴포넌트 "{componentId}"에 대한 설정 패널이 없습니다.

); } - // 테이블 변경 핸들러 - 선택된 테이블의 컬럼을 동적으로 로드 + // 테이블 변경 핸들러 const handleTableChange = async (tableName: string) => { try { - // 먼저 tables에서 찾아보기 (이미 컬럼이 있는 경우) const existingTable = tables?.find((t) => t.tableName === tableName); - if (existingTable && existingTable.columns && existingTable.columns.length > 0) { + if (existingTable?.columns?.length > 0) { setSelectedTableColumns(existingTable.columns); return; } - - // 컬럼이 없으면 tableTypeApi로 조회 (ScreenDesigner와 동일한 방식) const { tableTypeApi } = await import("@/lib/api/screen"); const columnsResponse = await tableTypeApi.getColumns(tableName); - const columns = (columnsResponse || []).map((col: any) => ({ tableName: col.tableName || tableName, columnName: col.columnName || col.column_name, @@ -443,73 +365,46 @@ export const DynamicComponentConfigPanel: React.FC = codeCategory: col.codeCategory || col.code_category, codeValue: col.codeValue || col.code_value, })); - setSelectedTableColumns(columns); - } catch (error) { - console.error("❌ 테이블 변경 오류:", error); - // 오류 발생 시 빈 배열 + } catch (_) { setSelectedTableColumns([]); } }; - // 🆕 원본 테이블 컬럼 로드 핸들러 (selected-items-detail-input용) const handleSourceTableChange = async (tableName: string) => { - console.log("🔄 원본 테이블 변경:", tableName); try { const { tableTypeApi } = await import("@/lib/api/screen"); const columnsResponse = await tableTypeApi.getColumns(tableName); - const columns = (columnsResponse || []).map((col: any) => ({ columnName: col.columnName || col.column_name, columnLabel: col.displayName || col.columnLabel || col.column_label || col.columnName || col.column_name, dataType: col.dataType || col.data_type || col.dbType, - inputType: col.inputType || col.input_type, // 🆕 inputType 추가 + inputType: col.inputType || col.input_type, })); - - console.log("✅ 원본 테이블 컬럼 로드 완료:", columns.length); setSourceTableColumns(columns); - } catch (error) { - console.error("❌ 원본 테이블 컬럼 로드 실패:", error); + } catch (_) { setSourceTableColumns([]); } }; - // 🆕 대상 테이블 컬럼 로드 핸들러 (selected-items-detail-input용) const handleTargetTableChange = async (tableName: string) => { - console.log("🔄 대상 테이블 변경:", tableName); try { const { tableTypeApi } = await import("@/lib/api/screen"); const columnsResponse = await tableTypeApi.getColumns(tableName); - - console.log("📡 [handleTargetTableChange] API 응답 (원본):", { - totalColumns: columnsResponse.length, - sampleColumns: columnsResponse.slice(0, 3), - currency_code_raw: columnsResponse.find((c: any) => (c.columnName || c.column_name) === 'currency_code') - }); - const columns = (columnsResponse || []).map((col: any) => ({ columnName: col.columnName || col.column_name, columnLabel: col.displayName || col.columnLabel || col.column_label || col.columnName || col.column_name, dataType: col.dataType || col.data_type || col.dbType, - inputType: col.inputType || col.input_type, // 🆕 inputType 추가 - codeCategory: col.codeCategory || col.code_category, // 🆕 codeCategory 추가 + inputType: col.inputType || col.input_type, + codeCategory: col.codeCategory || col.code_category, })); - - console.log("✅ 대상 테이블 컬럼 변환 완료:", { - tableName, - totalColumns: columns.length, - currency_code: columns.find((c: any) => c.columnName === "currency_code"), - discount_rate: columns.find((c: any) => c.columnName === "discount_rate") - }); - setTargetTableColumns(columns); - } catch (error) { - console.error("❌ 대상 테이블 컬럼 로드 실패:", error); + } catch (_) { setTargetTableColumns([]); } }; - // 레거시 위젯 패널 (component/onUpdateProperty props 사용) + // --- 특수 래퍼: 레거시 위젯 (component/onUpdateProperty props) --- const LEGACY_PANELS = new Set([ "card", "dashboard", "stats", "stats-card", "progress", "progress-bar", "chart", "chart-basic", @@ -536,94 +431,31 @@ export const DynamicComponentConfigPanel: React.FC = ); } - // 🆕 수주 등록 관련 컴포넌트들은 간단한 인터페이스 사용 - const isSimpleConfigPanel = [ - "autocomplete-search-input", - "modal-repeater-table", - "conditional-container", - ].includes(componentId); - - if (isSimpleConfigPanel) { - return ; - } - - // 🆕 V2 컴포넌트들은 전용 props 사용 - if (componentId.startsWith("v2-")) { - return ( - - ); - } - - // entity-search-input은 currentComponent 정보 필요 (참조 테이블 자동 로드용) - // 그리고 allComponents 필요 (연쇄관계 부모 필드 선택용) - if (componentId === "entity-search-input") { - return ( - - ); - } - - // 🆕 selected-items-detail-input은 특별한 props 사용 - if (componentId === "selected-items-detail-input") { - return ( - 0 ? allTablesList : tables} // 전체 테이블 목록 (동적 로드 or 전달된 목록) - screenTableName={screenTableName} // 🆕 현재 화면의 테이블명 (자동 설정용) - onSourceTableChange={handleSourceTableChange} // 🆕 원본 테이블 변경 핸들러 - onTargetTableChange={handleTargetTableChange} // 🆕 대상 테이블 변경 핸들러 - /> - ); - } - - // 🆕 ButtonConfigPanel은 component와 onUpdateProperty를 사용 + // --- 특수 래퍼: ButtonConfigPanel (component/onUpdateProperty props) --- if (componentId === "button-primary" || componentId === "v2-button-primary") { - // currentComponent가 있으면 그것을 사용, 없으면 config에서 component 구조 생성 const componentForButton = currentComponent || { id: "temp", type: "component", componentType: componentId, componentConfig: config, }; - return ( { - // path가 componentConfig로 시작하면 내부 경로 추출 if (path.startsWith("componentConfig.")) { const configPath = path.replace("componentConfig.", ""); const pathParts = configPath.split("."); - - // 중첩된 경로 처리 - 현재 config를 기반으로 새 config 생성 const currentConfig = componentForButton.componentConfig || {}; - const newConfig = JSON.parse(JSON.stringify(currentConfig)); // deep clone + const newConfig = JSON.parse(JSON.stringify(currentConfig)); let current: any = newConfig; for (let i = 0; i < pathParts.length - 1; i++) { - if (!current[pathParts[i]]) { - current[pathParts[i]] = {}; - } + if (!current[pathParts[i]]) current[pathParts[i]] = {}; current = current[pathParts[i]]; } current[pathParts[pathParts.length - 1]] = value; - onChange(newConfig); } else { - // 직접 config 속성 변경 const currentConfig = componentForButton.componentConfig || {}; onChange({ ...currentConfig, [path]: value }); } @@ -634,20 +466,38 @@ export const DynamicComponentConfigPanel: React.FC = ); } + // --- 통일된 props: 모든 일반 패널에 동일한 props 전달 --- + const context: ConfigPanelContext = { + tables, + tableColumns: selectedTableColumns, + screenTableName, + menuObjid, + allComponents, + currentComponent, + allTables: allTablesList.length > 0 ? allTablesList : tables, + screenComponents, + }; + return ( 0 ? allTablesList : tables} + onTableChange={handleTableChange} + menuObjid={menuObjid} + allComponents={allComponents} + currentComponent={currentComponent} + screenComponents={screenComponents} + inputType={currentComponent?.inputType || config?.inputType} + sourceTableColumns={sourceTableColumns} + targetTableColumns={targetTableColumns} + onSourceTableChange={handleSourceTableChange} + onTargetTableChange={handleTargetTableChange} /> ); };