/** * 컴포넌트 ID로 해당 컴포넌트의 ConfigPanel을 동적으로 로드하는 유틸리티 */ import React from "react"; import type { ConfigPanelContext } from "@/lib/registry/components/common/ConfigPanelTypes"; // 컴포넌트별 ConfigPanel 동적 import 맵 const CONFIG_PANEL_MAP: Record Promise> = { // ========== V2 컴포넌트 ========== "v2-input": () => import("@/components/v2/config-panels/V2FieldConfigPanel"), "v2-select": () => import("@/components/v2/config-panels/V2FieldConfigPanel"), "v2-date": () => import("@/components/v2/config-panels/V2DateConfigPanel"), "v2-list": () => import("@/components/v2/config-panels/V2ListConfigPanel"), "v2-media": () => import("@/components/v2/config-panels/V2MediaConfigPanel"), "v2-biz": () => import("@/components/v2/config-panels/V2BizConfigPanel"), "v2-group": () => import("@/components/v2/config-panels/V2GroupConfigPanel"), "v2-hierarchy": () => import("@/components/v2/config-panels/V2HierarchyConfigPanel"), "v2-layout": () => import("@/components/v2/config-panels/V2LayoutConfigPanel"), "v2-repeater": () => import("@/components/v2/config-panels/V2RepeaterConfigPanel"), // ========== 기본 입력 컴포넌트 ========== "text-input": () => import("@/lib/registry/components/text-input/TextInputConfigPanel"), "number-input": () => import("@/lib/registry/components/number-input/NumberInputConfigPanel"), "date-input": () => import("@/lib/registry/components/date-input/DateInputConfigPanel"), "textarea-basic": () => import("@/lib/registry/components/textarea-basic/TextareaBasicConfigPanel"), "select-basic": () => import("@/lib/registry/components/select-basic/SelectBasicConfigPanel"), "checkbox-basic": () => import("@/lib/registry/components/checkbox-basic/CheckboxBasicConfigPanel"), "radio-basic": () => import("@/lib/registry/components/radio-basic/RadioBasicConfigPanel"), "toggle-switch": () => import("@/lib/registry/components/toggle-switch/ToggleSwitchConfigPanel"), "file-upload": () => import("@/lib/registry/components/file-upload/FileUploadConfigPanel"), "slider-basic": () => import("@/lib/registry/components/slider-basic/SliderBasicConfigPanel"), "test-input": () => import("@/lib/registry/components/test-input/TestInputConfigPanel"), // ========== 버튼 ========== "button-primary": () => import("@/components/screen/config-panels/ButtonConfigPanel"), "v2-button-primary": () => import("@/components/screen/config-panels/ButtonConfigPanel"), // ========== 표시 컴포넌트 ========== "text-display": () => import("@/lib/registry/components/text-display/TextDisplayConfigPanel"), "v2-text-display": () => import("@/lib/registry/components/v2-text-display/TextDisplayConfigPanel"), "image-display": () => import("@/lib/registry/components/image-display/ImageDisplayConfigPanel"), "divider-line": () => import("@/lib/registry/components/divider-line/DividerLineConfigPanel"), "v2-divider-line": () => import("@/lib/registry/components/v2-divider-line/DividerLineConfigPanel"), "image-widget": () => import("@/lib/registry/components/image-widget/ImageWidgetConfigPanel"), // ========== 레이아웃/컨테이너 ========== "accordion-basic": () => import("@/lib/registry/components/accordion-basic/AccordionBasicConfigPanel"), "card-display": () => import("@/lib/registry/components/card-display/CardDisplayConfigPanel"), "v2-card-display": () => import("@/lib/registry/components/v2-card-display/CardDisplayConfigPanel"), "section-card": () => import("@/lib/registry/components/section-card/SectionCardConfigPanel"), "v2-section-card": () => import("@/lib/registry/components/v2-section-card/SectionCardConfigPanel"), "section-paper": () => import("@/lib/registry/components/section-paper/SectionPaperConfigPanel"), "v2-section-paper": () => import("@/lib/registry/components/v2-section-paper/SectionPaperConfigPanel"), "split-panel-layout": () => import("@/lib/registry/components/split-panel-layout/SplitPanelLayoutConfigPanel"), "v2-split-panel-layout": () => import("@/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutConfigPanel"), "split-panel-layout2": () => import("@/lib/registry/components/split-panel-layout2/SplitPanelLayout2ConfigPanel"), "screen-split-panel": () => import("@/lib/registry/components/screen-split-panel/ScreenSplitPanelConfigPanel"), "conditional-container": () => import("@/lib/registry/components/conditional-container/ConditionalContainerConfigPanel"), "v2-split-line": () => import("@/lib/registry/components/v2-split-line/SplitLineConfigPanel"), // ========== 테이블/리스트 ========== "table-list": () => import("@/lib/registry/components/table-list/TableListConfigPanel"), "v2-table-list": () => import("@/lib/registry/components/v2-table-list/TableListConfigPanel"), "pivot-grid": () => import("@/lib/registry/components/pivot-grid/PivotGridConfigPanel"), "v2-pivot-grid": () => import("@/lib/registry/components/v2-pivot-grid/PivotGridConfigPanel"), "table-search-widget": () => import("@/lib/registry/components/table-search-widget/TableSearchWidgetConfigPanel"), "v2-table-search-widget": () => import("@/lib/registry/components/v2-table-search-widget/TableSearchWidgetConfigPanel"), "tax-invoice-list": () => import("@/lib/registry/components/tax-invoice-list/TaxInvoiceListConfigPanel"), // ========== 리피터/반복 ========== "repeat-container": () => import("@/lib/registry/components/repeat-container/RepeatContainerConfigPanel"), "v2-repeat-container": () => import("@/lib/registry/components/v2-repeat-container/RepeatContainerConfigPanel"), "repeater-field-group": () => import("@/components/webtypes/config/RepeaterConfigPanel"), "simple-repeater-table": () => import("@/lib/registry/components/simple-repeater-table/SimpleRepeaterTableConfigPanel"), "modal-repeater-table": () => import("@/lib/registry/components/modal-repeater-table/ModalRepeaterTableConfigPanel"), "repeat-screen-modal": () => import("@/lib/registry/components/repeat-screen-modal/RepeatScreenModalConfigPanel"), "related-data-buttons": () => import("@/lib/registry/components/related-data-buttons/RelatedDataButtonsConfigPanel"), // ========== 검색/선택 ========== "autocomplete-search-input": () => import("@/lib/registry/components/autocomplete-search-input/AutocompleteSearchInputConfigPanel"), "entity-search-input": () => import("@/lib/registry/components/entity-search-input/EntitySearchInputConfigPanel"), "selected-items-detail-input": () => import("@/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputConfigPanel"), "customer-item-mapping": () => import("@/lib/registry/components/customer-item-mapping/CustomerItemMappingConfigPanel"), "mail-recipient-selector": () => import("@/lib/registry/components/mail-recipient-selector/MailRecipientSelectorConfigPanel"), "location-swap-selector": () => import("@/lib/registry/components/location-swap-selector/LocationSwapSelectorConfigPanel"), "v2-location-swap-selector": () => import("@/lib/registry/components/v2-location-swap-selector/LocationSwapSelectorConfigPanel"), // ========== 특수 컴포넌트 ========== "flow-widget": () => import("@/components/screen/config-panels/FlowWidgetConfigPanel"), "tabs-widget": () => import("@/components/screen/config-panels/TabsConfigPanel"), "v2-tabs-widget": () => import("@/components/screen/config-panels/TabsConfigPanel"), "tabs": () => import("@/components/screen/config-panels/TabsConfigPanel"), "v2-tabs": () => import("@/components/screen/config-panels/TabsConfigPanel"), "map": () => import("@/lib/registry/components/map/MapConfigPanel"), "rack-structure": () => import("@/lib/registry/components/rack-structure/RackStructureConfigPanel"), "v2-rack-structure": () => import("@/lib/registry/components/v2-rack-structure/RackStructureConfigPanel"), "aggregation-widget": () => import("@/lib/registry/components/aggregation-widget/AggregationWidgetConfigPanel"), "v2-aggregation-widget": () => import("@/lib/registry/components/v2-aggregation-widget/AggregationWidgetConfigPanel"), "numbering-rule": () => import("@/lib/registry/components/numbering-rule/NumberingRuleConfigPanel"), "v2-numbering-rule": () => import("@/lib/registry/components/v2-numbering-rule/NumberingRuleConfigPanel"), "category-manager": () => import("@/lib/registry/components/category-manager/CategoryManagerConfigPanel"), "universal-form-modal": () => import("@/lib/registry/components/universal-form-modal/UniversalFormModalConfigPanel"), "v2-process-work-standard": () => import("@/lib/registry/components/v2-process-work-standard/ProcessWorkStandardConfigPanel"), // ========== V2 BOM 컴포넌트 ========== "v2-bom-item-editor": () => import("@/components/v2/config-panels/V2BomItemEditorConfigPanel"), "v2-bom-tree": () => import("@/components/v2/config-panels/V2BomTreeConfigPanel"), // ========== 레거시 위젯 (component/onUpdateProperty props 사용) ========== "card": () => import("@/components/screen/config-panels/CardConfigPanel"), "dashboard": () => import("@/components/screen/config-panels/DashboardConfigPanel"), "stats": () => import("@/components/screen/config-panels/StatsCardConfigPanel"), "stats-card": () => import("@/components/screen/config-panels/StatsCardConfigPanel"), "progress": () => import("@/components/screen/config-panels/ProgressBarConfigPanel"), "progress-bar": () => import("@/components/screen/config-panels/ProgressBarConfigPanel"), "chart": () => import("@/components/screen/config-panels/ChartConfigPanel"), "chart-basic": () => import("@/components/screen/config-panels/ChartConfigPanel"), "alert": () => import("@/components/screen/config-panels/AlertConfigPanel"), "alert-info": () => import("@/components/screen/config-panels/AlertConfigPanel"), "badge": () => import("@/components/screen/config-panels/BadgeConfigPanel"), "badge-status": () => import("@/components/screen/config-panels/BadgeConfigPanel"), }; const configPanelCache = new Map>(); /** * 컴포넌트 ID로 ConfigPanel 컴포넌트를 동적으로 로드 */ export async function getComponentConfigPanel(componentId: string): Promise | null> { if (configPanelCache.has(componentId)) { return configPanelCache.get(componentId)!; } const importFn = CONFIG_PANEL_MAP[componentId]; if (!importFn) { return null; } try { const module = await importFn(); // 모듈에서 ConfigPanel 컴포넌트 추출 (우선순위): // 1차: PascalCase 변환된 이름 (예: text-input -> TextInputConfigPanel) // 2차: v2- 접두사 제거 후 PascalCase (예: v2-table-list -> TableListConfigPanel) // 3차: *ConfigPanel로 끝나는 첫 번째 named export // 4차: default export const pascalCaseName = `${toPascalCase(componentId)}ConfigPanel`; const baseComponentId = componentId.startsWith("v2-") ? componentId.slice(3) : componentId; const basePascalCaseName = `${toPascalCase(baseComponentId)}ConfigPanel`; const findConfigPanelExport = () => { for (const key of Object.keys(module)) { if (key.endsWith("ConfigPanel") && typeof module[key] === "function") { return module[key]; } } return null; }; const ConfigPanelComponent = module[pascalCaseName] || module[basePascalCaseName] || findConfigPanelExport() || module.default; if (!ConfigPanelComponent) { console.error(`컴포넌트 "${componentId}"의 ConfigPanel을 모듈에서 찾을 수 없습니다.`); return null; } configPanelCache.set(componentId, ConfigPanelComponent); return ConfigPanelComponent; } catch (error) { console.error(`컴포넌트 "${componentId}"의 ConfigPanel 로드 실패:`, error); return null; } } export function hasComponentConfigPanel(componentId: string): boolean { return componentId in CONFIG_PANEL_MAP; } export function getSupportedConfigPanelComponents(): string[] { return Object.keys(CONFIG_PANEL_MAP); } function toPascalCase(str: string): string { return str .split("-") .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) .join(""); } /** * 컴포넌트 설정 패널을 렌더링하는 React 컴포넌트 */ export interface ComponentConfigPanelProps { componentId: string; config: Record; onChange: (config: Record) => void; screenTableName?: string; tableColumns?: any[]; tables?: any[]; menuObjid?: number; allComponents?: any[]; currentComponent?: any; componentType?: string; } export const DynamicComponentConfigPanel: React.FC = ({ componentId, config, onChange, screenTableName, tableColumns, tables, menuObjid, allComponents, currentComponent, componentType, }) => { 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([]); 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) { if (mounted) { setError(err instanceof Error ? err.message : String(err)); setLoading(false); } } } loadConfigPanel(); return () => { mounted = false; }; }, [componentId]); React.useEffect(() => { setSelectedTableColumns(tableColumns); }, [tableColumns]); // repeater-field-group / selected-items-detail-input에서 전체 테이블 목록 로드 React.useEffect(() => { if (componentId === "repeater-field-group" || componentId === "selected-items-detail-input") { const loadAllTables = async () => { try { const { tableManagementApi } = await import("@/lib/api/tableManagement"); const response = await tableManagementApi.getTableList(); if (response.success && response.data) { setAllTablesList(response.data); } } catch (_) { // 전체 테이블 목록 로드 실패 시 무시 } }; loadAllTables(); } }, [componentId]); // selected-items-detail-input: 초기 sourceTable/targetTable 컬럼 로드 React.useEffect(() => { if (componentId !== "selected-items-detail-input") return; 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.sourceTable) loadColumns(config.sourceTable, setSourceTableColumns); if (config.targetTable) loadColumns(config.targetTable, setTargetTableColumns, true); }, [componentId, config.sourceTable, config.targetTable]); const screenComponents = React.useMemo(() => { 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 (
로딩 중...

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

); } if (error) { return (
로드 실패

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

); } if (!ConfigPanelComponent) { return (
설정 패널 없음

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

); } // 테이블 변경 핸들러 const handleTableChange = async (tableName: string) => { try { const existingTable = tables?.find((t) => t.tableName === tableName); if (existingTable?.columns?.length > 0) { setSelectedTableColumns(existingTable.columns); return; } 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, columnLabel: col.displayName || col.columnLabel || col.column_label || col.columnName || col.column_name, dataType: col.dataType || col.data_type || col.dbType, webType: col.webType || col.web_type, input_type: col.inputType || col.input_type, widgetType: col.widgetType || col.widget_type || col.webType || col.web_type, isNullable: col.isNullable || col.is_nullable, required: col.required !== undefined ? col.required : col.isNullable === "NO" || col.is_nullable === "NO", columnDefault: col.columnDefault || col.column_default, characterMaximumLength: col.characterMaximumLength || col.character_maximum_length, codeCategory: col.codeCategory || col.code_category, codeValue: col.codeValue || col.code_value, })); setSelectedTableColumns(columns); } catch (_) { setSelectedTableColumns([]); } }; const handleSourceTableChange = async (tableName: string) => { 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, })); setSourceTableColumns(columns); } catch (_) { setSourceTableColumns([]); } }; const handleTargetTableChange = async (tableName: string) => { 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, codeCategory: col.codeCategory || col.code_category, })); setTargetTableColumns(columns); } catch (_) { setTargetTableColumns([]); } }; // --- 특수 래퍼: 레거시 위젯 (component/onUpdateProperty props) --- const LEGACY_PANELS = new Set([ "card", "dashboard", "stats", "stats-card", "progress", "progress-bar", "chart", "chart-basic", "alert", "alert-info", "badge", "badge-status", ]); if (LEGACY_PANELS.has(componentId)) { const pseudoComponent = { id: currentComponent?.id || "temp", type: "component", componentConfig: config, ...currentComponent, }; return ( { if (path.startsWith("componentConfig.")) { const key = path.replace("componentConfig.", ""); onChange({ ...config, [key]: value }); } }} /> ); } // --- 특수 래퍼: ButtonConfigPanel (component/onUpdateProperty props) --- if (componentId === "button-primary" || componentId === "v2-button-primary") { const componentForButton = currentComponent || { id: "temp", type: "component", componentType: componentId, componentConfig: config, }; return ( { if (path.startsWith("componentConfig.")) { const configPath = path.replace("componentConfig.", ""); const pathParts = configPath.split("."); const currentConfig = componentForButton.componentConfig || {}; 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]] = {}; current = current[pathParts[i]]; } current[pathParts[pathParts.length - 1]] = value; onChange(newConfig); } else { const currentConfig = componentForButton.componentConfig || {}; onChange({ ...currentConfig, [path]: value }); } }} allComponents={allComponents} currentTableName={screenTableName} /> ); } // --- 통일된 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} componentType={componentType || componentId} sourceTableColumns={sourceTableColumns} targetTableColumns={targetTableColumns} onSourceTableChange={handleSourceTableChange} onTargetTableChange={handleTargetTableChange} /> ); };