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

466 lines
17 KiB
TypeScript
Raw Normal View History

2025-09-10 14:09:32 +09:00
"use client";
import React from "react";
import { ComponentData } from "@/types/screen";
2025-09-10 18:36:28 +09:00
import { DynamicLayoutRenderer } from "./DynamicLayoutRenderer";
2025-09-11 18:38:28 +09:00
import { ComponentRegistry } from "./ComponentRegistry";
import { filterDOMProps } from "@/lib/utils/domPropsFilter";
2025-09-10 14:09:32 +09:00
// 컴포넌트 렌더러 인터페이스
export interface ComponentRenderer {
(props: {
component: ComponentData;
isSelected?: boolean;
isInteractive?: boolean;
formData?: Record<string, any>;
2025-09-18 18:49:30 +09:00
originalData?: Record<string, any>; // 부분 업데이트용 원본 데이터
2025-09-10 14:09:32 +09:00
onFormDataChange?: (fieldName: string, value: any) => void;
onClick?: (e?: React.MouseEvent) => void;
onDragStart?: (e: React.DragEvent) => void;
onDragEnd?: () => void;
children?: React.ReactNode;
2025-09-11 16:21:00 +09:00
onZoneComponentDrop?: (e: React.DragEvent, zoneId: string, layoutId: string) => void;
onZoneClick?: (zoneId: string) => void;
2025-09-12 14:24:25 +09:00
// 버튼 액션을 위한 추가 props
screenId?: number;
tableName?: string;
onRefresh?: () => void;
onClose?: () => void;
2025-09-18 18:49:30 +09:00
// 테이블 선택된 행 정보 (다중 선택 액션용)
selectedRows?: any[];
selectedRowsData?: any[];
2025-11-05 10:23:00 +09:00
onSelectedRowsChange?: (selectedRows: any[], selectedRowsData: any[], sortBy?: string, sortOrder?: "asc" | "desc", columnOrder?: string[], tableDisplayData?: any[]) => void;
// 테이블 정렬 정보 (엑셀 다운로드용)
sortBy?: string;
sortOrder?: "asc" | "desc";
2025-11-05 10:23:00 +09:00
tableDisplayData?: any[]; // 🆕 화면 표시 데이터
// 플로우 선택된 데이터 정보 (플로우 위젯 선택 액션용)
flowSelectedData?: any[];
flowSelectedStepId?: number | null;
onFlowSelectedDataChange?: (selectedData: any[], stepId: number | null) => void;
2025-09-18 18:49:30 +09:00
// 테이블 새로고침 키
refreshKey?: number;
// 편집 모드
mode?: "view" | "edit";
// 설정 변경 핸들러 (상세설정과 연동)
onConfigChange?: (config: any) => void;
2025-09-10 14:09:32 +09:00
[key: string]: any;
}): React.ReactElement;
}
2025-09-11 18:38:28 +09:00
// 레거시 렌더러 레지스트리 (기존 컴포넌트들용)
class LegacyComponentRegistry {
2025-09-10 14:09:32 +09:00
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;
}
}
2025-09-11 18:38:28 +09:00
// 전역 레거시 레지스트리 인스턴스
export const legacyComponentRegistry = new LegacyComponentRegistry();
// 하위 호환성을 위한 기존 이름 유지
export const componentRegistry = legacyComponentRegistry;
2025-09-10 14:09:32 +09:00
// 동적 컴포넌트 렌더러 컴포넌트
export interface DynamicComponentRendererProps {
component: ComponentData;
isSelected?: boolean;
2025-10-16 18:16:57 +09:00
isPreview?: boolean; // 반응형 모드 플래그
isDesignMode?: boolean; // 디자인 모드 여부 (false일 때 데이터 로드)
2025-09-10 14:09:32 +09:00
onClick?: (e?: React.MouseEvent) => void;
onDragStart?: (e: React.DragEvent) => void;
onDragEnd?: () => void;
children?: React.ReactNode;
2025-09-18 18:49:30 +09:00
// 폼 데이터 관련
formData?: Record<string, any>;
originalData?: Record<string, any>; // 부분 업데이트용 원본 데이터
onFormDataChange?: (fieldName: string, value: any) => void;
2025-09-12 14:24:25 +09:00
// 버튼 액션을 위한 추가 props
screenId?: number;
tableName?: string;
2025-11-05 15:23:57 +09:00
menuId?: number; // 🆕 메뉴 ID (카테고리 관리 등에 필요)
menuObjid?: number; // 🆕 메뉴 OBJID (메뉴 스코프 - 카테고리/채번)
2025-11-05 15:23:57 +09:00
selectedScreen?: any; // 🆕 화면 정보 전체 (menuId 등 추출용)
2025-10-29 11:26:00 +09:00
userId?: string; // 🆕 현재 사용자 ID
userName?: string; // 🆕 현재 사용자 이름
companyCode?: string; // 🆕 현재 사용자의 회사 코드
2025-09-12 14:24:25 +09:00
onRefresh?: () => void;
onClose?: () => void;
// 테이블 선택된 행 정보 (다중 선택 액션용)
selectedRows?: any[];
selectedRowsData?: any[];
2025-11-05 10:23:00 +09:00
onSelectedRowsChange?: (selectedRows: any[], selectedRowsData: any[], sortBy?: string, sortOrder?: "asc" | "desc", columnOrder?: string[], tableDisplayData?: any[]) => void;
// 테이블 정렬 정보 (엑셀 다운로드용)
sortBy?: string;
sortOrder?: "asc" | "desc";
columnOrder?: string[];
2025-11-05 10:23:00 +09:00
tableDisplayData?: any[]; // 🆕 화면 표시 데이터
// 플로우 선택된 데이터 정보 (플로우 위젯 선택 액션용)
flowSelectedData?: any[];
flowSelectedStepId?: number | null;
onFlowSelectedDataChange?: (selectedData: any[], stepId: number | null) => void;
// 테이블 새로고침 키
refreshKey?: number;
2025-10-23 17:55:04 +09:00
// 플로우 새로고침 키
flowRefreshKey?: number;
onFlowRefresh?: () => void;
2025-09-18 18:49:30 +09:00
// 편집 모드
mode?: "view" | "edit";
// 모달 내에서 렌더링 여부
isInModal?: boolean;
2025-09-10 14:09:32 +09:00
[key: string]: any;
}
export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> = ({
component,
isSelected = false,
2025-10-16 18:16:57 +09:00
isPreview = false,
2025-09-10 14:09:32 +09:00
onClick,
onDragStart,
onDragEnd,
children,
...props
}) => {
2025-09-12 16:47:02 +09:00
// 컴포넌트 타입 추출 - 새 시스템에서는 componentType 속성 사용, 레거시는 type 사용
const componentType = (component as any).componentType || component.type;
2025-09-10 14:09:32 +09:00
2025-11-05 18:08:51 +09:00
// 🎯 카테고리 타입 우선 처리 (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이 있는 경우만
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);
}
};
return (
<CategorySelectComponent
tableName={tableName}
columnName={columnName}
value={currentValue}
onChange={handleChange}
placeholder={component.componentConfig?.placeholder || "선택하세요"}
required={(component as any).required}
disabled={(component as any).readonly}
className="w-full"
/>
);
} catch (error) {
console.error("❌ CategorySelectComponent 로드 실패:", error);
}
}
2025-09-10 18:36:28 +09:00
// 레이아웃 컴포넌트 처리
if (componentType === "layout") {
// DOM 안전한 props만 전달
const safeLayoutProps = filterDOMProps(props);
2025-09-10 18:36:28 +09:00
return (
<DynamicLayoutRenderer
layout={component as any}
allComponents={props.allComponents || []}
isSelected={isSelected}
onClick={onClick}
onDragStart={onDragStart}
onDragEnd={onDragEnd}
2025-09-11 12:22:39 +09:00
onUpdateLayout={props.onUpdateLayout}
2025-09-11 16:21:00 +09:00
// 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}
2025-09-10 18:36:28 +09:00
/>
);
}
2025-09-11 18:38:28 +09:00
// 1. 새 컴포넌트 시스템에서 먼저 조회
const newComponent = ComponentRegistry.getComponent(componentType);
2025-09-12 14:24:25 +09:00
2025-09-11 18:38:28 +09:00
if (newComponent) {
// 새 컴포넌트 시스템으로 렌더링
try {
const NewComponentRenderer = newComponent.component;
if (NewComponentRenderer) {
// React 전용 props들을 명시적으로 분리하고 DOM 안전한 props만 전달
const {
isInteractive,
formData,
onFormDataChange,
tableName,
2025-11-05 15:23:57 +09:00
menuId, // 🆕 메뉴 ID
menuObjid, // 🆕 메뉴 OBJID (메뉴 스코프)
2025-11-05 15:23:57 +09:00
selectedScreen, // 🆕 화면 정보
onRefresh,
onClose,
screenId,
2025-10-29 11:26:00 +09:00
userId, // 🆕 사용자 ID
userName, // 🆕 사용자 이름
companyCode, // 🆕 회사 코드
mode,
isInModal,
originalData,
allComponents,
onUpdateLayout,
onZoneClick,
selectedRows,
selectedRowsData,
onSelectedRowsChange,
sortBy, // 🆕 정렬 컬럼
sortOrder, // 🆕 정렬 방향
2025-11-05 10:23:00 +09:00
tableDisplayData, // 🆕 화면 표시 데이터
flowSelectedData,
flowSelectedStepId,
onFlowSelectedDataChange,
refreshKey,
2025-10-23 17:55:04 +09:00
flowRefreshKey, // Added this
onFlowRefresh, // Added this
onConfigChange,
2025-10-22 17:19:47 +09:00
isPreview,
autoGeneration,
...restProps
} = props;
2025-10-29 11:26:00 +09:00
2025-10-22 17:19:47 +09:00
// DOM 안전한 props만 필터링
const safeProps = filterDOMProps(restProps);
// 컴포넌트의 columnName에 해당하는 formData 값 추출
const fieldName = (component as any).columnName || component.id;
const currentValue = formData?.[fieldName] || "";
// onChange 핸들러 - 컴포넌트 타입에 따라 다르게 처리
const handleChange = (value: any) => {
2025-10-21 14:41:56 +09:00
// React 이벤트 객체인 경우 값 추출
let actualValue = value;
if (value && typeof value === "object" && value.nativeEvent && value.target) {
// SyntheticEvent인 경우 target.value 추출
actualValue = value.target.value;
}
if (onFormDataChange) {
// RepeaterInput 같은 복합 컴포넌트는 전체 데이터를 전달
// 단순 input 컴포넌트는 (fieldName, value) 형태로 전달받음
if (componentType === "repeater-field-group" || componentType === "repeater") {
// fieldName과 함께 전달
2025-10-21 14:41:56 +09:00
onFormDataChange(fieldName, actualValue);
} else {
// 이미 fieldName이 포함된 경우는 그대로 전달
2025-10-21 14:41:56 +09:00
onFormDataChange(fieldName, actualValue);
}
}
};
// 렌더러 props 구성
2025-10-22 17:19:47 +09:00
// component.style에서 height 제거 (RealtimePreviewDynamic에서 size.height로 처리)
2025-11-05 16:18:00 +09:00
// 단, layout 타입 컴포넌트(split-panel-layout 등)는 height 유지
const isLayoutComponent =
component.type === "layout" ||
componentType === "split-panel-layout" ||
componentType?.includes("layout");
2025-10-22 17:19:47 +09:00
const { height: _height, ...styleWithoutHeight } = component.style || {};
2025-10-29 11:26:00 +09:00
2025-10-24 14:11:12 +09:00
// 숨김 값 추출
2025-10-23 15:06:00 +09:00
const hiddenValue = component.hidden || component.componentConfig?.hidden;
2025-10-29 11:26:00 +09:00
const rendererProps = {
component,
isSelected,
onClick,
onDragStart,
onDragEnd,
size: component.size || newComponent.defaultSize,
position: component.position,
2025-11-05 16:18:00 +09:00
style: isLayoutComponent ? component.style : styleWithoutHeight, // 레이아웃은 height 유지
config: component.componentConfig,
componentConfig: component.componentConfig,
value: currentValue, // formData에서 추출한 현재 값 전달
// 새로운 기능들 전달
2025-10-23 15:06:00 +09:00
autoGeneration: component.autoGeneration || component.componentConfig?.autoGeneration,
hidden: hiddenValue,
// React 전용 props들은 직접 전달 (DOM에 전달되지 않음)
isInteractive,
formData,
onFormDataChange,
onChange: handleChange, // 개선된 onChange 핸들러 전달
tableName,
2025-11-05 15:23:57 +09:00
menuId, // 🆕 메뉴 ID
menuObjid, // 🆕 메뉴 OBJID (메뉴 스코프)
2025-11-05 15:23:57 +09:00
selectedScreen, // 🆕 화면 정보
onRefresh,
onClose,
screenId,
2025-10-29 11:26:00 +09:00
userId, // 🆕 사용자 ID
userName, // 🆕 사용자 이름
companyCode, // 🆕 회사 코드
mode,
isInModal,
readonly: component.readonly,
disabled: component.readonly,
originalData,
allComponents,
onUpdateLayout,
onZoneClick,
// 테이블 선택된 행 정보 전달
selectedRows,
selectedRowsData,
onSelectedRowsChange,
// 테이블 정렬 정보 전달
sortBy,
sortOrder,
2025-11-05 10:23:00 +09:00
tableDisplayData, // 🆕 화면 표시 데이터
// 플로우 선택된 데이터 정보 전달
flowSelectedData,
flowSelectedStepId,
onFlowSelectedDataChange,
// 설정 변경 핸들러 전달
onConfigChange,
refreshKey,
2025-10-23 17:55:04 +09:00
// 플로우 새로고침 키
flowRefreshKey,
onFlowRefresh,
2025-10-16 18:16:57 +09:00
// 반응형 모드 플래그 전달
isPreview,
// 디자인 모드 플래그 전달 - isPreview와 명확히 구분
isDesignMode: props.isDesignMode !== undefined ? props.isDesignMode : false,
};
// 렌더러가 클래스인지 함수인지 확인
if (
typeof NewComponentRenderer === "function" &&
NewComponentRenderer.prototype &&
NewComponentRenderer.prototype.render
) {
// 클래스 기반 렌더러 (AutoRegisteringComponentRenderer 상속)
const rendererInstance = new NewComponentRenderer(rendererProps);
return rendererInstance.render();
} else {
// 함수형 컴포넌트
return <NewComponentRenderer {...rendererProps} />;
}
2025-09-11 18:38:28 +09:00
}
} catch (error) {
console.error(`❌ 새 컴포넌트 렌더링 실패 (${componentType}):`, error);
}
}
// 2. 레거시 시스템에서 조회
const renderer = legacyComponentRegistry.get(componentType);
2025-09-10 14:09:32 +09:00
if (!renderer) {
2025-09-12 14:24:25 +09:00
console.error(`⚠️ 등록되지 않은 컴포넌트 타입: ${componentType}`, {
component: component,
componentType: componentType,
componentConfig: component.componentConfig,
availableNewComponents: ComponentRegistry.getAllComponents().map((c) => c.id),
availableLegacyComponents: legacyComponentRegistry.getRegisteredTypes(),
});
2025-09-10 14:09:32 +09:00
// 폴백 렌더링 - 기본 플레이스홀더
return (
<div className="flex h-full w-full items-center justify-center rounded border-2 border-dashed border-border bg-muted p-4">
2025-09-10 14:09:32 +09:00
<div className="text-center">
<div className="mb-2 text-sm font-medium text-muted-foreground">{component.label || component.id}</div>
<div className="text-xs text-muted-foreground/70"> : {componentType}</div>
2025-09-10 14:09:32 +09:00
</div>
</div>
);
}
// 동적 렌더링 실행
try {
// 레거시 시스템에서도 DOM 안전한 props만 전달
const safeLegacyProps = filterDOMProps(props);
2025-09-10 14:09:32 +09:00
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,
2025-10-29 11:26:00 +09:00
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,
2025-09-10 14:09:32 +09:00
});
} 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;