[agent-pipeline] pipe-20260311052455-y968 round-2

This commit is contained in:
DDD1542 2026-03-11 14:41:14 +09:00
parent b329b52036
commit d358de60d6
2 changed files with 106 additions and 232 deletions

View File

@ -54,3 +54,27 @@ export interface ConfigPanelBuilderProps<T = any> {
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<string, any>;
onChange: (config: Record<string, any>) => void;
context?: ConfigPanelContext;
}

View File

@ -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<string, () => Promise<any>> = {
// ========== V2 컴포넌트 ==========
"v2-input": () => import("@/components/v2/config-panels/V2InputConfigPanel"),
@ -123,22 +123,18 @@ const CONFIG_PANEL_MAP: Record<string, () => Promise<any>> = {
"badge-status": () => import("@/components/screen/config-panels/BadgeConfigPanel"),
};
// 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;
}
@ -151,11 +147,9 @@ export async function getComponentConfigPanel(componentId: string): Promise<Reac
// 3차: *ConfigPanel로 끝나는 첫 번째 named export
// 4차: default export
const pascalCaseName = `${toPascalCase(componentId)}ConfigPanel`;
// v2- 접두사가 있는 경우 접두사를 제거한 이름도 시도
const baseComponentId = componentId.startsWith("v2-") ? componentId.slice(3) : componentId;
const basePascalCaseName = `${toPascalCase(baseComponentId)}ConfigPanel`;
// 모듈에서 ConfigPanel로 끝나는 첫 번째 named export를 찾는 fallback
const findConfigPanelExport = () => {
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<Reac
return null;
}
// 캐시에 저장
configPanelCache.set(componentId, ConfigPanelComponent);
return ConfigPanelComponent;
} catch (error) {
console.error(`컴포넌트 "${componentId}"의 ConfigPanel 로드 실패:`, error);
@ -186,24 +178,14 @@ export async function getComponentConfigPanel(componentId: string): Promise<Reac
}
}
/**
* 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
.split("-")
@ -218,12 +200,12 @@ export interface ComponentConfigPanelProps {
componentId: string;
config: Record<string, any>;
onChange: (config: Record<string, any>) => 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<ComponentConfigPanelProps> = ({
@ -237,53 +219,41 @@ export const DynamicComponentConfigPanel: React.FC<ComponentConfigPanelProps> =
allComponents,
currentComponent,
}) => {
// 모든 useState를 최상단에 선언 (Hooks 규칙)
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[]>([]);
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<ComponentConfigPanelProps> =
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<React.SetStateAction<any[]>>, 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 (
<div className="rounded-md border border-dashed border-input bg-muted p-4 w-full">
<div className="flex items-center gap-2 text-muted-foreground">
<span className="text-sm font-medium"> ...</span>
<span className="text-sm font-medium"> ...</span>
</div>
<p className="mt-1 text-xs text-muted-foreground"> .</p>
</div>
@ -395,7 +322,7 @@ export const DynamicComponentConfigPanel: React.FC<ComponentConfigPanelProps> =
return (
<div className="rounded-md border border-dashed border-destructive/30 bg-destructive/10 p-4 w-full">
<div className="flex items-center gap-2 text-destructive">
<span className="text-sm font-medium"> </span>
<span className="text-sm font-medium"> </span>
</div>
<p className="mt-1 text-xs text-destructive"> : {error}</p>
</div>
@ -403,31 +330,26 @@ export const DynamicComponentConfigPanel: React.FC<ComponentConfigPanelProps> =
}
if (!ConfigPanelComponent) {
console.warn(`⚠️ DynamicComponentConfigPanel: ${componentId} ConfigPanelComponent가 null`);
return (
<div className="rounded-md border border-dashed border-amber-300 bg-amber-50 p-4 w-full">
<div className="flex items-center gap-2 text-amber-600">
<span className="text-sm font-medium"> </span>
<span className="text-sm font-medium"> </span>
</div>
<p className="mt-1 text-xs text-amber-500"> "{componentId}" .</p>
<p className="mt-1 text-xs text-amber-500"> &quot;{componentId}&quot; .</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) {
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<ComponentConfigPanelProps> =
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<ComponentConfigPanelProps> =
);
}
// 🆕 수주 등록 관련 컴포넌트들은 간단한 인터페이스 사용
const isSimpleConfigPanel = [
"autocomplete-search-input",
"modal-repeater-table",
"conditional-container",
].includes(componentId);
if (isSimpleConfigPanel) {
return <ConfigPanelComponent config={config} onConfigChange={onChange} />;
}
// 🆕 V2 컴포넌트들은 전용 props 사용
if (componentId.startsWith("v2-")) {
return (
<ConfigPanelComponent
config={config}
onChange={onChange}
menuObjid={menuObjid}
inputType={currentComponent?.inputType || config?.inputType}
screenTableName={screenTableName}
tableColumns={selectedTableColumns}
/>
);
}
// 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} // 🆕 대상 테이블 변경 핸들러
/>
);
}
// 🆕 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 (
<ConfigPanelComponent
component={componentForButton}
onUpdateProperty={(path: string, value: any) => {
// 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<ComponentConfigPanelProps> =
);
}
// --- 통일된 props: 모든 일반 패널에 동일한 props 전달 ---
const context: ConfigPanelContext = {
tables,
tableColumns: selectedTableColumns,
screenTableName,
menuObjid,
allComponents,
currentComponent,
allTables: allTablesList.length > 0 ? allTablesList : tables,
screenComponents,
};
return (
<ConfigPanelComponent
config={config}
onChange={onChange}
onConfigChange={onChange} // TableListConfigPanel을 위한 추가 prop
onConfigChange={onChange}
context={context}
screenTableName={screenTableName}
tableColumns={selectedTableColumns} // 동적으로 변경되는 컬럼 전달
tables={tables} // 기본 테이블 목록 (현재 화면의 테이블만)
allTables={componentId === "repeater-field-group" ? allTablesList : tables} // RepeaterConfigPanel만 전체 테이블
onTableChange={handleTableChange} // 테이블 변경 핸들러 전달
menuObjid={menuObjid} // 🆕 메뉴 OBJID 전달
allComponents={allComponents} // 🆕 현재 화면의 모든 컴포넌트 (연쇄 드롭다운 부모 감지용)
currentComponent={currentComponent} // 🆕 현재 컴포넌트 정보
screenComponents={screenComponents} // 🆕 집계 위젯 등에서 사용하는 컴포넌트 목록
tableColumns={selectedTableColumns}
tables={tables}
allTables={allTablesList.length > 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}
/>
);
};