ERP-node/frontend/lib/registry/DynamicComponentRenderer.tsx

868 lines
32 KiB
TypeScript

"use client";
import React from "react";
import { ComponentData } from "@/types/screen";
import { DynamicLayoutRenderer } from "./DynamicLayoutRenderer";
import { ComponentRegistry } from "./ComponentRegistry";
import { filterDOMProps } from "@/lib/utils/domPropsFilter";
// Unified 컴포넌트 import
import {
UnifiedInput,
UnifiedSelect,
UnifiedDate,
UnifiedList,
UnifiedLayout,
UnifiedGroup,
UnifiedMedia,
UnifiedBiz,
UnifiedHierarchy,
} from "@/components/unified";
import { UnifiedRepeater } from "@/components/unified/UnifiedRepeater";
// 컴포넌트 렌더러 인터페이스
export interface ComponentRenderer {
(props: {
component: ComponentData;
isSelected?: boolean;
isInteractive?: boolean;
formData?: Record<string, any>;
originalData?: Record<string, any>; // 부분 업데이트용 원본 데이터
onFormDataChange?: (fieldName: string, value: any) => void;
onClick?: (e?: React.MouseEvent) => void;
onDragStart?: (e: React.DragEvent) => void;
onDragEnd?: () => void;
children?: React.ReactNode;
onZoneComponentDrop?: (e: React.DragEvent, zoneId: string, layoutId: string) => void;
onZoneClick?: (zoneId: string) => void;
// 버튼 액션을 위한 추가 props
screenId?: number;
tableName?: string;
onRefresh?: () => void;
onClose?: () => void;
// 테이블 선택된 행 정보 (다중 선택 액션용)
selectedRows?: any[];
selectedRowsData?: any[];
onSelectedRowsChange?: (
selectedRows: any[],
selectedRowsData: any[],
sortBy?: string,
sortOrder?: "asc" | "desc",
columnOrder?: string[],
tableDisplayData?: any[],
) => void;
// 테이블 정렬 정보 (엑셀 다운로드용)
sortBy?: string;
sortOrder?: "asc" | "desc";
tableDisplayData?: any[]; // 🆕 화면 표시 데이터
// 플로우 선택된 데이터 정보 (플로우 위젯 선택 액션용)
flowSelectedData?: any[];
flowSelectedStepId?: number | null;
onFlowSelectedDataChange?: (selectedData: any[], stepId: number | null) => void;
// 테이블 새로고침 키
refreshKey?: number;
// 편집 모드
mode?: "view" | "edit";
// 설정 변경 핸들러 (상세설정과 연동)
onConfigChange?: (config: any) => void;
[key: string]: any;
}): React.ReactElement;
}
// 레거시 렌더러 레지스트리 (기존 컴포넌트들용)
class LegacyComponentRegistry {
private renderers: Map<string, ComponentRenderer> = new Map();
// 컴포넌트 렌더러 등록
register(componentType: string, renderer: ComponentRenderer) {
this.renderers.set(componentType, renderer);
}
// 컴포넌트 렌더러 조회
get(componentType: string): ComponentRenderer | undefined {
return this.renderers.get(componentType);
}
// 등록된 모든 컴포넌트 타입 조회
getRegisteredTypes(): string[] {
return Array.from(this.renderers.keys());
}
// 컴포넌트 타입이 등록되어 있는지 확인
has(componentType: string): boolean {
const result = this.renderers.has(componentType);
return result;
}
}
// 전역 레거시 레지스트리 인스턴스
export const legacyComponentRegistry = new LegacyComponentRegistry();
// 하위 호환성을 위한 기존 이름 유지
export const componentRegistry = legacyComponentRegistry;
// 동적 컴포넌트 렌더러 컴포넌트
export interface DynamicComponentRendererProps {
component: ComponentData;
isSelected?: boolean;
isPreview?: boolean; // 반응형 모드 플래그
isDesignMode?: boolean; // 디자인 모드 여부 (false일 때 데이터 로드)
onClick?: (e?: React.MouseEvent) => void;
onDragStart?: (e: React.DragEvent) => void;
onDragEnd?: () => void;
children?: React.ReactNode;
// 폼 데이터 관련
formData?: Record<string, any>;
originalData?: Record<string, any>; // 부분 업데이트용 원본 데이터
onFormDataChange?: (fieldName: string, value: any) => void;
// 버튼 액션을 위한 추가 props
screenId?: number;
tableName?: string;
menuId?: number; // 🆕 메뉴 ID (카테고리 관리 등에 필요)
menuObjid?: number; // 🆕 메뉴 OBJID (메뉴 스코프 - 카테고리/채번)
selectedScreen?: any; // 🆕 화면 정보 전체 (menuId 등 추출용)
userId?: string; // 🆕 현재 사용자 ID
userName?: string; // 🆕 현재 사용자 이름
companyCode?: string; // 🆕 현재 사용자의 회사 코드
onRefresh?: () => void;
onClose?: () => void;
onSave?: () => Promise<void>; // 🆕 EditModal의 handleSave 콜백
// 테이블 선택된 행 정보 (다중 선택 액션용)
selectedRows?: any[];
// 🆕 그룹 데이터 (EditModal → ModalRepeaterTable)
groupedData?: Record<string, any>[];
// 🆕 비활성화할 필드 목록 (EditModal → 각 컴포넌트)
disabledFields?: string[];
selectedRowsData?: any[];
onSelectedRowsChange?: (
selectedRows: any[],
selectedRowsData: any[],
sortBy?: string,
sortOrder?: "asc" | "desc",
columnOrder?: string[],
tableDisplayData?: any[],
) => void;
// 테이블 정렬 정보 (엑셀 다운로드용)
sortBy?: string;
sortOrder?: "asc" | "desc";
columnOrder?: string[];
tableDisplayData?: any[]; // 🆕 화면 표시 데이터
// 플로우 선택된 데이터 정보 (플로우 위젯 선택 액션용)
flowSelectedData?: any[];
flowSelectedStepId?: number | null;
onFlowSelectedDataChange?: (selectedData: any[], stepId: number | null) => void;
// 테이블 새로고침 키
refreshKey?: number;
// 플로우 새로고침 키
flowRefreshKey?: number;
onFlowRefresh?: () => void;
// 편집 모드
mode?: "view" | "edit";
// 모달 내에서 렌더링 여부
isInModal?: boolean;
// 탭 관련 정보 (탭 내부의 컴포넌트에서 사용)
parentTabId?: string; // 부모 탭 ID
parentTabsComponentId?: string; // 부모 탭 컴포넌트 ID
// 🆕 조건부 비활성화 상태
conditionalDisabled?: boolean;
[key: string]: any;
}
export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> = ({
component,
isSelected = false,
isPreview = false,
onClick,
onDragStart,
onDragEnd,
children,
...props
}) => {
// 컴포넌트 타입 추출 - 새 시스템에서는 componentType 속성 사용, 레거시는 type 사용
const componentType = (component as any).componentType || component.type;
// 🆕 Unified 컴포넌트 처리
if (componentType?.startsWith("unified-")) {
const unifiedType = componentType as string;
const config = (component as any).componentConfig || {};
const fieldName = (component as any).columnName || component.id;
const currentValue = props.formData?.[fieldName];
const handleChange = (value: any) => {
if (props.onFormDataChange) {
props.onFormDataChange(fieldName, value);
}
};
// 공통 props
const commonProps = {
id: component.id,
label: (component as any).label,
required: (component as any).required,
readonly: (component as any).readonly,
// conditionalDisabled가 true이면 비활성화
disabled: (component as any).disabled || props.disabledFields?.includes(fieldName) || props.conditionalDisabled,
value: currentValue,
onChange: handleChange,
tableName: (component as any).tableName || props.tableName,
columnName: fieldName,
style: component.style,
size: component.size,
position: component.position,
};
switch (unifiedType) {
case "unified-input":
return (
<UnifiedInput
unifiedType="UnifiedInput"
{...commonProps}
config={{
type: config.inputType || config.type || "text",
format: config.format,
placeholder: config.placeholder,
mask: config.mask,
min: config.min,
max: config.max,
step: config.step,
buttonText: config.buttonText,
buttonVariant: config.buttonVariant,
}}
/>
);
case "unified-select":
return (
<UnifiedSelect
unifiedType="UnifiedSelect"
{...commonProps}
config={{
mode: config.mode || "dropdown",
source: config.source || "static",
options: config.options || [],
multiple: config.multiple,
searchable: config.searchable,
codeGroup: config.codeGroup,
codeCategory: config.codeCategory,
table: config.table,
valueColumn: config.valueColumn,
labelColumn: config.labelColumn,
// 엔티티(참조 테이블) 관련 속성
entityTable: config.entityTable,
entityValueColumn: config.entityValueColumn,
entityLabelColumn: config.entityLabelColumn,
entityValueField: config.entityValueField,
entityLabelField: config.entityLabelField,
}}
/>
);
case "unified-date":
return (
<UnifiedDate
unifiedType="UnifiedDate"
{...commonProps}
config={{
type: config.dateType || config.type || "date",
format: config.format,
range: config.range,
minDate: config.minDate,
maxDate: config.maxDate,
showToday: config.showToday,
}}
/>
);
case "unified-list":
// 데이터 소스: config.data > props.tableDisplayData > []
const listData = config.data?.length > 0 ? config.data : props.tableDisplayData || [];
return (
<UnifiedList
unifiedType="UnifiedList"
{...commonProps}
config={{
viewMode: config.viewMode || "table",
columns: config.columns || [],
source: config.source || "static",
sortable: config.sortable,
pagination: config.pagination,
searchable: config.searchable,
editable: config.editable,
pageable: config.pageable,
pageSize: config.pageSize,
cardConfig: config.cardConfig,
dataSource: {
table: config.dataSource?.table || props.tableName,
},
}}
data={listData}
selectedRows={props.selectedRowsData || []}
onRowSelect={(rows) => {
// 항상 선택된 데이터를 전달 (modalDataStore에 자동 저장됨)
if (props.onSelectedRowsChange) {
props.onSelectedRowsChange(
rows.map((r: any) => r.id || r.objid),
rows,
props.sortBy,
props.sortOrder,
undefined,
props.tableDisplayData,
);
}
}}
/>
);
case "unified-layout":
return (
<UnifiedLayout
unifiedType="UnifiedLayout"
{...commonProps}
config={{
type: config.layoutType || config.type || "grid",
columns: config.columns,
gap: config.gap,
direction: config.direction,
use12Column: config.use12Column,
}}
>
{children}
</UnifiedLayout>
);
case "unified-group":
return (
<UnifiedGroup
unifiedType="UnifiedGroup"
{...commonProps}
config={{
type: config.groupType || config.type || "section",
collapsible: config.collapsible,
defaultOpen: config.defaultOpen,
tabs: config.tabs || [],
showHeader: config.showHeader,
}}
title={config.title}
>
{children}
</UnifiedGroup>
);
case "unified-media":
return (
<UnifiedMedia
unifiedType="UnifiedMedia"
{...commonProps}
config={{
type: config.mediaType || config.type || "image",
accept: config.accept,
maxSize: config.maxSize,
multiple: config.multiple,
preview: config.preview,
}}
/>
);
case "unified-biz":
return (
<UnifiedBiz
unifiedType="UnifiedBiz"
{...commonProps}
config={{
type: config.bizType || config.type || "flow",
flowConfig: config.flowConfig,
rackConfig: config.rackConfig,
numberingConfig: config.numberingConfig,
}}
/>
);
case "unified-hierarchy":
return (
<UnifiedHierarchy
unifiedType="UnifiedHierarchy"
{...commonProps}
config={{
type: config.hierarchyType || config.type || "tree",
viewMode: config.viewMode || "tree",
dataSource: config.dataSource || "static",
maxLevel: config.maxLevel,
draggable: config.draggable,
}}
/>
);
case "unified-repeater":
return (
<UnifiedRepeater
config={{
renderMode: config.renderMode || "inline",
dataSource: {
tableName: config.dataSource?.tableName || props.tableName || "",
foreignKey: config.dataSource?.foreignKey || "",
referenceKey: config.dataSource?.referenceKey || "",
sourceTable: config.dataSource?.sourceTable,
displayColumn: config.dataSource?.displayColumn,
},
columns: config.columns || [],
modal: config.modal,
button: config.button,
features: config.features || {
showAddButton: true,
showDeleteButton: true,
inlineEdit: false,
dragSort: false,
showRowNumber: false,
selectable: false,
multiSelect: false,
},
}}
parentId={props.formData?.[config.dataSource?.referenceKey] || props.formData?.id}
onDataChange={(data) => {
console.log("UnifiedRepeater data changed:", data);
}}
onRowClick={(row) => {
console.log("UnifiedRepeater row clicked:", row);
}}
onButtonClick={(action, row, buttonConfig) => {
console.log("UnifiedRepeater button clicked:", action, row, buttonConfig);
}}
/>
);
default:
return (
<div className="flex h-full w-full items-center justify-center rounded border-2 border-dashed border-amber-300 bg-amber-50 p-4">
<div className="text-center">
<div className="mb-2 text-sm font-medium text-amber-600">Unified </div>
<div className="text-xs text-amber-500"> : {unifiedType}</div>
</div>
</div>
);
}
}
// 🎯 카테고리 타입 우선 처리 (inputType 또는 webType 확인)
const inputType = (component as any).componentConfig?.inputType || (component as any).inputType;
const webType = (component as any).componentConfig?.webType;
const tableName = (component as any).tableName;
const columnName = (component as any).columnName;
// 카테고리 셀렉트: webType이 "category"이고 tableName과 columnName이 있는 경우만
// ⚠️ 단, componentType이 "select-basic"인 경우는 ComponentRegistry로 처리 (다중선택 등 고급 기능 지원)
if (
(inputType === "category" || webType === "category") &&
tableName &&
columnName &&
componentType === "select-basic"
) {
// select-basic은 ComponentRegistry에서 처리하도록 아래로 통과
} else if ((inputType === "category" || webType === "category") && tableName && columnName) {
try {
const { CategorySelectComponent } = require("@/lib/registry/components/category-select/CategorySelectComponent");
const fieldName = columnName || component.id;
const currentValue = props.formData?.[fieldName] || "";
const handleChange = (value: any) => {
if (props.onFormDataChange) {
props.onFormDataChange(fieldName, value);
}
};
// 🆕 disabledFields 체크 + readonly 체크
const isFieldDisabled = props.disabledFields?.includes(columnName) || (component as any).disabled;
const isFieldReadonly = (component as any).readonly || (component as any).componentConfig?.readonly;
return (
<CategorySelectComponent
tableName={tableName}
columnName={columnName}
value={currentValue}
onChange={handleChange}
placeholder={component.componentConfig?.placeholder || "선택하세요"}
required={(component as any).required}
disabled={isFieldDisabled}
readonly={isFieldReadonly}
className="w-full"
/>
);
} catch (error) {
console.error("❌ CategorySelectComponent 로드 실패:", error);
}
}
// 레이아웃 컴포넌트 처리
if (componentType === "layout") {
// DOM 안전한 props만 전달
const safeLayoutProps = filterDOMProps(props);
return (
<DynamicLayoutRenderer
layout={component as any}
allComponents={props.allComponents || []}
isSelected={isSelected}
onClick={onClick}
onDragStart={onDragStart}
onDragEnd={onDragEnd}
onUpdateLayout={props.onUpdateLayout}
// onComponentDrop 제거 - 일반 캔버스 드롭만 사용
onZoneClick={props.onZoneClick}
isInteractive={props.isInteractive}
formData={props.formData}
onFormDataChange={props.onFormDataChange}
screenId={props.screenId}
tableName={props.tableName}
onRefresh={props.onRefresh}
onClose={props.onClose}
mode={props.mode}
isInModal={props.isInModal}
originalData={props.originalData}
{...safeLayoutProps}
/>
);
}
// 1. 새 컴포넌트 시스템에서 먼저 조회
const newComponent = ComponentRegistry.getComponent(componentType);
if (newComponent) {
// 새 컴포넌트 시스템으로 렌더링
try {
const NewComponentRenderer = newComponent.component;
if (NewComponentRenderer) {
// React 전용 props들을 명시적으로 분리하고 DOM 안전한 props만 전달
const {
isInteractive,
formData,
onFormDataChange,
tableName,
menuId, // 🆕 메뉴 ID
menuObjid, // 🆕 메뉴 OBJID (메뉴 스코프)
selectedScreen, // 🆕 화면 정보
onRefresh,
onClose,
onSave, // 🆕 EditModal의 handleSave 콜백
screenId,
userId, // 🆕 사용자 ID
userName, // 🆕 사용자 이름
companyCode, // 🆕 회사 코드
mode,
isInModal,
originalData,
allComponents,
onUpdateLayout,
onZoneClick,
selectedRows,
selectedRowsData,
onSelectedRowsChange,
sortBy, // 🆕 정렬 컬럼
sortOrder, // 🆕 정렬 방향
tableDisplayData, // 🆕 화면 표시 데이터
flowSelectedData,
flowSelectedStepId,
onFlowSelectedDataChange,
refreshKey,
flowRefreshKey, // Added this
onFlowRefresh, // Added this
onConfigChange,
isPreview,
autoGeneration,
disabledFields, // 🆕 비활성화 필드 목록
...restProps
} = props;
// DOM 안전한 props만 필터링
const safeProps = filterDOMProps(restProps);
// 컴포넌트의 columnName에 해당하는 formData 값 추출
const fieldName = (component as any).columnName || component.id;
// modal-repeater-table은 배열 데이터를 다루므로 빈 배열로 초기화
let currentValue;
if (componentType === "modal-repeater-table" || componentType === "repeat-screen-modal") {
// EditModal에서 전달된 groupedData가 있으면 우선 사용
currentValue = props.groupedData || formData?.[fieldName] || [];
} else {
currentValue = formData?.[fieldName] || "";
}
// onChange 핸들러 - 컴포넌트 타입에 따라 다르게 처리
const handleChange = (value: any) => {
// autocomplete-search-input, entity-search-input은 자체적으로 onFormDataChange를 호출하므로 중복 저장 방지
if (componentType === "autocomplete-search-input" || componentType === "entity-search-input") {
return;
}
// React 이벤트 객체인 경우 값 추출
let actualValue = value;
if (value && typeof value === "object" && value.nativeEvent && value.target) {
// SyntheticEvent인 경우 target.value 추출
actualValue = value.target.value;
}
if (onFormDataChange) {
// modal-repeater-table은 배열 데이터를 다룸
if (componentType === "modal-repeater-table") {
onFormDataChange(fieldName, actualValue);
}
// RepeaterInput 같은 복합 컴포넌트는 전체 데이터를 전달
else if (componentType === "repeater-field-group" || componentType === "repeater") {
onFormDataChange(fieldName, actualValue);
} else {
onFormDataChange(fieldName, actualValue);
}
}
};
// 렌더러 props 구성
// 숨김 값 추출
const hiddenValue = component.hidden || component.componentConfig?.hidden;
// 숨김 처리: 인터랙티브 모드(실제 뷰)에서만 숨김, 디자인 모드에서는 표시
if (hiddenValue && isInteractive) {
return null;
}
// size.width와 size.height를 style.width와 style.height로 변환
const finalStyle: React.CSSProperties = {
...component.style,
width: component.size?.width ? `${component.size.width}px` : component.style?.width,
height: component.size?.height ? `${component.size.height}px` : component.style?.height,
};
// 🆕 엔티티 검색 컴포넌트는 componentConfig.tableName을 사용해야 함 (화면 테이블이 아닌 검색 대상 테이블)
const useConfigTableName =
componentType === "entity-search-input" ||
componentType === "autocomplete-search-input" ||
componentType === "modal-repeater-table";
const rendererProps = {
component,
isSelected,
onClick,
onDragStart,
onDragEnd,
size: component.size || newComponent.defaultSize,
position: component.position,
style: finalStyle, // size를 포함한 최종 style
config: component.componentConfig,
componentConfig: component.componentConfig,
// componentConfig의 모든 속성을 props로 spread (tableName, displayField 등)
...(component.componentConfig || {}),
value: currentValue, // formData에서 추출한 현재 값 전달
// 새로운 기능들 전달
autoGeneration: component.autoGeneration || component.componentConfig?.autoGeneration,
hidden: hiddenValue,
// React 전용 props들은 직접 전달 (DOM에 전달되지 않음)
isInteractive,
formData,
onFormDataChange,
onChange: handleChange, // 개선된 onChange 핸들러 전달
// 🆕 엔티티 검색 컴포넌트는 componentConfig.tableName 유지, 그 외는 화면 테이블명 사용
tableName: useConfigTableName ? component.componentConfig?.tableName || tableName : tableName,
menuId, // 🆕 메뉴 ID
menuObjid, // 🆕 메뉴 OBJID (메뉴 스코프)
selectedScreen, // 🆕 화면 정보
onRefresh,
onClose,
onSave, // 🆕 EditModal의 handleSave 콜백
screenId,
userId, // 🆕 사용자 ID
userName, // 🆕 사용자 이름
companyCode, // 🆕 회사 코드
// 🆕 화면 모드 (edit/view)와 컴포넌트 UI 모드 구분
screenMode: mode,
// componentConfig.mode가 있으면 유지 (entity-search-input의 UI 모드)
mode: component.componentConfig?.mode || mode,
isInModal,
readonly: component.readonly,
// 🆕 disabledFields 체크 또는 기존 readonly
disabled: disabledFields?.includes(fieldName) || component.readonly,
originalData,
allComponents,
onUpdateLayout,
onZoneClick,
// 테이블 선택된 행 정보 전달
selectedRows,
selectedRowsData,
onSelectedRowsChange,
// 테이블 정렬 정보 전달
sortBy,
sortOrder,
tableDisplayData, // 🆕 화면 표시 데이터
// 플로우 선택된 데이터 정보 전달
flowSelectedData,
flowSelectedStepId,
onFlowSelectedDataChange,
// 설정 변경 핸들러 전달
onConfigChange,
refreshKey,
// 플로우 새로고침 키
flowRefreshKey,
onFlowRefresh,
// 반응형 모드 플래그 전달
isPreview,
// 디자인 모드 플래그 전달 - isPreview와 명확히 구분
isDesignMode: props.isDesignMode !== undefined ? props.isDesignMode : false,
// 🆕 그룹 데이터 전달 (EditModal → ConditionalContainer → ModalRepeaterTable)
// Note: 이 props들은 DOM 요소에 전달되면 안 됨
// 각 컴포넌트에서 명시적으로 destructure하여 사용해야 함
groupedData: props.groupedData, // ✅ 언더스코어 제거하여 직접 전달
_groupedData: props.groupedData, // 하위 호환성 유지
// 🆕 UniversalFormModal용 initialData 전달
// originalData가 비어있지 않으면 originalData 사용, 아니면 formData 사용
// 생성 모드에서는 originalData가 빈 객체이므로 formData를 사용해야 함
_initialData: (originalData && Object.keys(originalData).length > 0) ? originalData : formData,
_originalData: originalData,
// 🆕 탭 관련 정보 전달 (탭 내부의 테이블 컴포넌트에서 사용)
parentTabId: props.parentTabId,
parentTabsComponentId: props.parentTabsComponentId,
};
// 렌더러가 클래스인지 함수인지 확인
const isClass =
typeof NewComponentRenderer === "function" &&
NewComponentRenderer.prototype &&
NewComponentRenderer.prototype.render;
if (componentType === "table-search-widget") {
console.log("🔍 [DynamicComponentRenderer] table-search-widget 렌더링 분기:", {
isClass,
hasPrototype: !!NewComponentRenderer.prototype,
hasRender: !!NewComponentRenderer.prototype?.render,
componentName: NewComponentRenderer.name,
componentProp: rendererProps.component,
screenId: rendererProps.screenId,
});
}
if (isClass) {
// 클래스 기반 렌더러 (AutoRegisteringComponentRenderer 상속)
const rendererInstance = new NewComponentRenderer(rendererProps);
return rendererInstance.render();
} else {
// 함수형 컴포넌트
// refreshKey를 React key로 전달하여 컴포넌트 리마운트 강제
// 🔧 디버깅: table-search-widget인 경우 직접 호출 후 반환
if (componentType === "table-search-widget") {
console.log("🔧🔧🔧 [DynamicComponentRenderer] TableSearchWidget 직접 호출 반환");
console.log("🔧 [DynamicComponentRenderer] NewComponentRenderer 함수 확인:", {
name: NewComponentRenderer.name,
toString: NewComponentRenderer.toString().substring(0, 200),
});
try {
const result = NewComponentRenderer(rendererProps);
console.log("🔧 [DynamicComponentRenderer] TableSearchWidget 결과 상세:", {
resultType: typeof result,
type: result?.type?.name || result?.type || "unknown",
propsKeys: result?.props ? Object.keys(result.props) : [],
propsStyle: result?.props?.style,
propsChildren: typeof result?.props?.children,
});
// 직접 호출 결과를 반환
return result;
} catch (directCallError) {
console.error("❌ [DynamicComponentRenderer] TableSearchWidget 직접 호출 실패:", directCallError);
}
}
return <NewComponentRenderer key={refreshKey} {...rendererProps} />;
}
}
} catch (error) {
console.error(`❌ 새 컴포넌트 렌더링 실패 (${componentType}):`, error);
}
}
// 2. 레거시 시스템에서 조회
const renderer = legacyComponentRegistry.get(componentType);
if (!renderer) {
console.error(`⚠️ 등록되지 않은 컴포넌트 타입: ${componentType}`, {
component: component,
componentId: component.id,
componentLabel: component.label,
componentType: componentType,
originalType: component.type,
originalComponentType: (component as any).componentType,
componentConfig: component.componentConfig,
webTypeConfig: (component as any).webTypeConfig,
autoGeneration: (component as any).autoGeneration,
availableNewComponents: ComponentRegistry.getAllComponents().map((c) => c.id),
availableLegacyComponents: legacyComponentRegistry.getRegisteredTypes(),
});
// 폴백 렌더링 - 기본 플레이스홀더
return (
<div className="border-border bg-muted flex h-full w-full items-center justify-center rounded border-2 border-dashed p-4">
<div className="text-center">
<div className="text-muted-foreground mb-2 text-sm font-medium">{component.label || component.id}</div>
<div className="text-muted-foreground/70 text-xs"> : {componentType}</div>
</div>
</div>
);
}
// 동적 렌더링 실행
try {
// 레거시 시스템에서도 DOM 안전한 props만 전달
const safeLegacyProps = filterDOMProps(props);
return renderer({
component,
isSelected,
onClick,
onDragStart,
onDragEnd,
children,
// React 전용 props들은 명시적으로 전달 (레거시 컴포넌트가 필요한 경우)
isInteractive: props.isInteractive,
formData: props.formData,
onFormDataChange: props.onFormDataChange,
screenId: props.screenId,
tableName: props.tableName,
userId: props.userId, // 🆕 사용자 ID
userName: props.userName, // 🆕 사용자 이름
companyCode: props.companyCode, // 🆕 회사 코드
onRefresh: props.onRefresh,
onClose: props.onClose,
mode: props.mode,
isInModal: props.isInModal,
originalData: props.originalData,
onUpdateLayout: props.onUpdateLayout,
onZoneClick: props.onZoneClick,
onZoneComponentDrop: props.onZoneComponentDrop,
allComponents: props.allComponents,
// 테이블 선택된 행 정보 전달
selectedRows: props.selectedRows,
selectedRowsData: props.selectedRowsData,
onSelectedRowsChange: props.onSelectedRowsChange,
// 플로우 선택된 데이터 정보 전달
flowSelectedData: props.flowSelectedData,
flowSelectedStepId: props.flowSelectedStepId,
onFlowSelectedDataChange: props.onFlowSelectedDataChange,
refreshKey: props.refreshKey,
// DOM 안전한 props들
...safeLegacyProps,
});
} catch (error) {
console.error(`❌ 컴포넌트 렌더링 실패 (${componentType}):`, error);
// 오류 발생 시 폴백 렌더링
return (
<div className="flex h-full w-full items-center justify-center rounded border-2 border-red-300 bg-red-50 p-4">
<div className="text-center">
<div className="mb-2 text-sm font-medium text-red-600"> </div>
<div className="text-xs text-red-400">
{componentType}: {error instanceof Error ? error.message : "알 수 없는 오류"}
</div>
</div>
</div>
);
}
};
export default DynamicComponentRenderer;