ERP-node/frontend/lib/utils/getComponentConfigPanel.tsx

453 lines
19 KiB
TypeScript
Raw Permalink Normal View History

2025-09-12 14:24:25 +09:00
/**
* ID로 ConfigPanel을
*/
import React from "react";
// 컴포넌트별 ConfigPanel 동적 import 맵
const CONFIG_PANEL_MAP: Record<string, () => Promise<any>> = {
"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"),
"button-primary": () => import("@/components/screen/config-panels/ButtonConfigPanel"),
2025-09-12 14:24:25 +09:00
"text-display": () => import("@/lib/registry/components/text-display/TextDisplayConfigPanel"),
"slider-basic": () => import("@/lib/registry/components/slider-basic/SliderBasicConfigPanel"),
"image-display": () => import("@/lib/registry/components/image-display/ImageDisplayConfigPanel"),
"divider-line": () => import("@/lib/registry/components/divider-line/DividerLineConfigPanel"),
2025-09-12 16:47:02 +09:00
"accordion-basic": () => import("@/lib/registry/components/accordion-basic/AccordionBasicConfigPanel"),
2025-09-15 11:43:59 +09:00
"table-list": () => import("@/lib/registry/components/table-list/TableListConfigPanel"),
2025-09-15 17:10:46 +09:00
"card-display": () => import("@/lib/registry/components/card-display/CardDisplayConfigPanel"),
2025-10-15 17:25:38 +09:00
"split-panel-layout": () => import("@/lib/registry/components/split-panel-layout/SplitPanelLayoutConfigPanel"),
"split-panel-layout2": () => import("@/lib/registry/components/split-panel-layout2/SplitPanelLayout2ConfigPanel"),
"repeater-field-group": () => import("@/components/webtypes/config/RepeaterConfigPanel"),
2025-10-20 10:55:33 +09:00
"flow-widget": () => import("@/components/screen/config-panels/FlowWidgetConfigPanel"),
// 🆕 수주 등록 관련 컴포넌트들
"autocomplete-search-input": () =>
import("@/lib/registry/components/autocomplete-search-input/AutocompleteSearchInputConfigPanel"),
"entity-search-input": () => import("@/lib/registry/components/entity-search-input/EntitySearchInputConfigPanel"),
"modal-repeater-table": () => import("@/lib/registry/components/modal-repeater-table/ModalRepeaterTableConfigPanel"),
// 🆕 조건부 컨테이너
"conditional-container": () =>
import("@/lib/registry/components/conditional-container/ConditionalContainerConfigPanel"),
// 🆕 선택 항목 상세입력
"selected-items-detail-input": () =>
import("@/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputConfigPanel"),
// 🆕 섹션 그룹화 레이아웃
"section-card": () => import("@/lib/registry/components/section-card/SectionCardConfigPanel"),
"section-paper": () => import("@/lib/registry/components/section-paper/SectionPaperConfigPanel"),
2025-11-24 17:24:47 +09:00
// 🆕 탭 컴포넌트
"tabs-widget": () => import("@/components/screen/config-panels/TabsConfigPanel"),
2025-09-12 14:24:25 +09:00
};
// ConfigPanel 컴포넌트 캐시
const configPanelCache = new Map<string, React.ComponentType<any>>();
/**
* ID로 ConfigPanel
*/
export async function getComponentConfigPanel(componentId: string): Promise<React.ComponentType<any> | null> {
// 캐시에서 먼저 확인
if (configPanelCache.has(componentId)) {
return configPanelCache.get(componentId)!;
}
// 매핑에서 import 함수 찾기
const importFn = CONFIG_PANEL_MAP[componentId];
if (!importFn) {
console.warn(`컴포넌트 "${componentId}"에 대한 ConfigPanel을 찾을 수 없습니다.`);
return null;
}
try {
const module = await importFn();
2025-09-12 16:47:02 +09:00
2025-09-12 14:24:25 +09:00
// 모듈에서 ConfigPanel 컴포넌트 추출
const ConfigPanelComponent =
module[`${toPascalCase(componentId)}ConfigPanel`] ||
module.RepeaterConfigPanel || // repeater-field-group의 export명
2025-10-20 10:55:33 +09:00
module.FlowWidgetConfigPanel || // flow-widget의 export명
module.CustomerItemMappingConfigPanel || // customer-item-mapping의 export명
module.SelectedItemsDetailInputConfigPanel || // selected-items-detail-input의 export명
module.ButtonConfigPanel || // button-primary의 export명
module.SectionCardConfigPanel || // section-card의 export명
module.SectionPaperConfigPanel || // section-paper의 export명
2025-11-24 17:24:47 +09:00
module.TabsConfigPanel || // tabs-widget의 export명
module.default;
2025-09-12 16:47:02 +09:00
2025-09-12 14:24:25 +09:00
if (!ConfigPanelComponent) {
console.error(`컴포넌트 "${componentId}"의 ConfigPanel을 모듈에서 찾을 수 없습니다.`);
return null;
}
// 캐시에 저장
configPanelCache.set(componentId, ConfigPanelComponent);
2025-09-12 16:47:02 +09:00
2025-09-12 14:24:25 +09:00
return ConfigPanelComponent;
} catch (error) {
console.error(`컴포넌트 "${componentId}"의 ConfigPanel 로드 실패:`, error);
return null;
}
}
/**
* ID가 ConfigPanel을
*/
export function hasComponentConfigPanel(componentId: string): boolean {
return componentId in CONFIG_PANEL_MAP;
}
/**
* ID
*/
export function getSupportedConfigPanelComponents(): string[] {
return Object.keys(CONFIG_PANEL_MAP);
}
/**
* kebab-case를 PascalCase로
* text-input TextInput
*/
function toPascalCase(str: string): string {
return str
2025-09-12 16:47:02 +09:00
.split("-")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join("");
2025-09-12 14:24:25 +09:00
}
/**
* React
*/
export interface ComponentConfigPanelProps {
componentId: string;
config: Record<string, any>;
onChange: (config: Record<string, any>) => void;
2025-09-12 16:47:02 +09:00
screenTableName?: string; // 화면에서 지정한 테이블명
tableColumns?: any[]; // 테이블 컬럼 정보
2025-10-15 17:25:38 +09:00
tables?: any[]; // 전체 테이블 목록
menuObjid?: number; // 🆕 메뉴 OBJID (코드/카테고리/채번규칙 스코프용)
2025-12-10 13:53:44 +09:00
allComponents?: any[]; // 🆕 현재 화면의 모든 컴포넌트 (연쇄 드롭다운 부모 감지용)
currentComponent?: any; // 🆕 현재 컴포넌트 정보
2025-09-12 14:24:25 +09:00
}
export const DynamicComponentConfigPanel: React.FC<ComponentConfigPanelProps> = ({
componentId,
config,
onChange,
2025-09-12 16:47:02 +09:00
screenTableName,
tableColumns,
2025-10-15 17:25:38 +09:00
tables,
menuObjid,
2025-12-10 13:53:44 +09:00
allComponents,
currentComponent,
2025-09-12 14:24:25 +09:00
}) => {
// 모든 useState를 최상단에 선언 (Hooks 규칙)
2025-09-12 14:24:25 +09:00
const [ConfigPanelComponent, setConfigPanelComponent] = React.useState<React.ComponentType<any> | null>(null);
const [loading, setLoading] = React.useState(true);
const [error, setError] = React.useState<string | null>(null);
const [selectedTableColumns, setSelectedTableColumns] = React.useState(tableColumns);
const [allTablesList, setAllTablesList] = React.useState<any[]>([]);
// 🆕 selected-items-detail-input 전용 상태
const [sourceTableColumns, setSourceTableColumns] = React.useState<any[]>([]);
const [targetTableColumns, setTargetTableColumns] = React.useState<any[]>([]);
2025-09-12 14:24:25 +09:00
React.useEffect(() => {
let mounted = true;
async function loadConfigPanel() {
try {
setLoading(true);
setError(null);
2025-09-12 16:47:02 +09:00
2025-09-12 14:24:25 +09:00
const component = await getComponentConfigPanel(componentId);
2025-09-12 16:47:02 +09:00
2025-09-12 14:24:25 +09:00
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;
};
}, [componentId]);
// tableColumns가 변경되면 selectedTableColumns도 업데이트
React.useEffect(() => {
setSelectedTableColumns(tableColumns);
}, [tableColumns]);
// RepeaterConfigPanel과 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) {
console.log(`✅ 전체 테이블 목록 로드 완료 (${componentId}):`, response.data.length);
setAllTablesList(response.data);
}
} catch (error) {
console.error("전체 테이블 목록 로드 실패:", error);
}
};
loadAllTables();
}
}, [componentId]);
// 🆕 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);
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();
}
// 대상 테이블 컬럼 로드
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();
}
}
}, [componentId, config.sourceTable, config.targetTable]);
2025-09-12 14:24:25 +09:00
if (loading) {
return (
<div className="rounded-md border border-dashed border-gray-300 bg-gray-50 p-4 w-full">
2025-09-12 14:24:25 +09:00
<div className="flex items-center gap-2 text-gray-600">
<span className="text-sm font-medium"> ...</span>
</div>
<p className="mt-1 text-xs text-gray-500"> .</p>
</div>
);
}
if (error) {
return (
<div className="rounded-md border border-dashed border-red-300 bg-red-50 p-4 w-full">
2025-09-12 14:24:25 +09:00
<div className="flex items-center gap-2 text-red-600">
<span className="text-sm font-medium"> </span>
</div>
<p className="mt-1 text-xs text-red-500"> : {error}</p>
</div>
);
}
if (!ConfigPanelComponent) {
console.warn(`⚠️ DynamicComponentConfigPanel: ${componentId} ConfigPanelComponent가 null`);
return (
<div className="rounded-md border border-dashed border-yellow-300 bg-yellow-50 p-4 w-full">
2025-09-12 14:24:25 +09:00
<div className="flex items-center gap-2 text-yellow-600">
<span className="text-sm font-medium"> </span>
</div>
<p className="mt-1 text-xs text-yellow-500"> "{componentId}" .</p>
</div>
);
}
// 테이블 변경 핸들러 - 선택된 테이블의 컬럼을 동적으로 로드
const handleTableChange = async (tableName: string) => {
try {
// 먼저 tables에서 찾아보기 (이미 컬럼이 있는 경우)
const existingTable = tables?.find((t) => t.tableName === tableName);
if (existingTable && existingTable.columns && 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,
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 (error) {
console.error("❌ 테이블 변경 오류:", error);
// 오류 발생 시 빈 배열
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 추가
}));
console.log("✅ 원본 테이블 컬럼 로드 완료:", columns.length);
setSourceTableColumns(columns);
} catch (error) {
console.error("❌ 원본 테이블 컬럼 로드 실패:", error);
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 추가
}));
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);
setTargetTableColumns([]);
}
};
// 🆕 수주 등록 관련 컴포넌트들은 간단한 인터페이스 사용
const isSimpleConfigPanel = [
"autocomplete-search-input",
"modal-repeater-table",
"conditional-container",
].includes(componentId);
if (isSimpleConfigPanel) {
return <ConfigPanelComponent config={config} onConfigChange={onChange} />;
}
// entity-search-input은 currentComponent 정보 필요 (참조 테이블 자동 로드용)
// 그리고 allComponents 필요 (연쇄관계 부모 필드 선택용)
if (componentId === "entity-search-input") {
return (
<ConfigPanelComponent
config={config}
onConfigChange={onChange}
currentComponent={currentComponent}
allComponents={allComponents}
/>
);
}
// 🆕 selected-items-detail-input은 특별한 props 사용
if (componentId === "selected-items-detail-input") {
return (
<ConfigPanelComponent
config={config}
onChange={onChange}
sourceTableColumns={sourceTableColumns} // 🆕 원본 테이블 컬럼
targetTableColumns={targetTableColumns} // 🆕 대상 테이블 컬럼
allTables={allTablesList.length > 0 ? allTablesList : tables} // 전체 테이블 목록 (동적 로드 or 전달된 목록)
screenTableName={screenTableName} // 🆕 현재 화면의 테이블명 (자동 설정용)
onSourceTableChange={handleSourceTableChange} // 🆕 원본 테이블 변경 핸들러
onTargetTableChange={handleTargetTableChange} // 🆕 대상 테이블 변경 핸들러
/>
);
}
2025-09-12 16:47:02 +09:00
return (
<ConfigPanelComponent
config={config}
onChange={onChange}
2025-10-15 17:25:38 +09:00
onConfigChange={onChange} // TableListConfigPanel을 위한 추가 prop
2025-09-12 16:47:02 +09:00
screenTableName={screenTableName}
tableColumns={selectedTableColumns} // 동적으로 변경되는 컬럼 전달
tables={tables} // 기본 테이블 목록 (현재 화면의 테이블만)
allTables={componentId === "repeater-field-group" ? allTablesList : tables} // RepeaterConfigPanel만 전체 테이블
onTableChange={handleTableChange} // 테이블 변경 핸들러 전달
menuObjid={menuObjid} // 🆕 메뉴 OBJID 전달
2025-12-10 13:53:44 +09:00
allComponents={allComponents} // 🆕 현재 화면의 모든 컴포넌트 (연쇄 드롭다운 부모 감지용)
currentComponent={currentComponent} // 🆕 현재 컴포넌트 정보
2025-09-12 16:47:02 +09:00
/>
);
2025-09-12 14:24:25 +09:00
};