415 lines
15 KiB
TypeScript
415 lines
15 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";
|
|
|
|
// 컴포넌트 렌더러 인터페이스
|
|
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;
|
|
userId?: string; // 🆕 현재 사용자 ID
|
|
userName?: string; // 🆕 현재 사용자 이름
|
|
companyCode?: 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";
|
|
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;
|
|
[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;
|
|
|
|
// 레이아웃 컴포넌트 처리
|
|
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,
|
|
onRefresh,
|
|
onClose,
|
|
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,
|
|
...restProps
|
|
} = props;
|
|
|
|
// 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) => {
|
|
// 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과 함께 전달
|
|
onFormDataChange(fieldName, actualValue);
|
|
} else {
|
|
// 이미 fieldName이 포함된 경우는 그대로 전달
|
|
onFormDataChange(fieldName, actualValue);
|
|
}
|
|
}
|
|
};
|
|
|
|
// 렌더러 props 구성
|
|
// component.style에서 height 제거 (RealtimePreviewDynamic에서 size.height로 처리)
|
|
const { height: _height, ...styleWithoutHeight } = component.style || {};
|
|
|
|
// 숨김 값 추출
|
|
const hiddenValue = component.hidden || component.componentConfig?.hidden;
|
|
|
|
const rendererProps = {
|
|
component,
|
|
isSelected,
|
|
onClick,
|
|
onDragStart,
|
|
onDragEnd,
|
|
size: component.size || newComponent.defaultSize,
|
|
position: component.position,
|
|
style: styleWithoutHeight,
|
|
config: component.componentConfig,
|
|
componentConfig: component.componentConfig,
|
|
value: currentValue, // formData에서 추출한 현재 값 전달
|
|
// 새로운 기능들 전달
|
|
autoGeneration: component.autoGeneration || component.componentConfig?.autoGeneration,
|
|
hidden: hiddenValue,
|
|
// React 전용 props들은 직접 전달 (DOM에 전달되지 않음)
|
|
isInteractive,
|
|
formData,
|
|
onFormDataChange,
|
|
onChange: handleChange, // 개선된 onChange 핸들러 전달
|
|
tableName,
|
|
onRefresh,
|
|
onClose,
|
|
screenId,
|
|
userId, // 🆕 사용자 ID
|
|
userName, // 🆕 사용자 이름
|
|
companyCode, // 🆕 회사 코드
|
|
mode,
|
|
isInModal,
|
|
readonly: component.readonly,
|
|
disabled: 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,
|
|
};
|
|
|
|
// 렌더러가 클래스인지 함수인지 확인
|
|
if (
|
|
typeof NewComponentRenderer === "function" &&
|
|
NewComponentRenderer.prototype &&
|
|
NewComponentRenderer.prototype.render
|
|
) {
|
|
// 클래스 기반 렌더러 (AutoRegisteringComponentRenderer 상속)
|
|
const rendererInstance = new NewComponentRenderer(rendererProps);
|
|
return rendererInstance.render();
|
|
} else {
|
|
// 함수형 컴포넌트
|
|
return <NewComponentRenderer {...rendererProps} />;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error(`❌ 새 컴포넌트 렌더링 실패 (${componentType}):`, error);
|
|
}
|
|
}
|
|
|
|
// 2. 레거시 시스템에서 조회
|
|
const renderer = legacyComponentRegistry.get(componentType);
|
|
|
|
if (!renderer) {
|
|
console.error(`⚠️ 등록되지 않은 컴포넌트 타입: ${componentType}`, {
|
|
component: component,
|
|
componentType: componentType,
|
|
componentConfig: component.componentConfig,
|
|
availableNewComponents: ComponentRegistry.getAllComponents().map((c) => c.id),
|
|
availableLegacyComponents: legacyComponentRegistry.getRegisteredTypes(),
|
|
});
|
|
|
|
// 폴백 렌더링 - 기본 플레이스홀더
|
|
return (
|
|
<div className="flex h-full w-full items-center justify-center rounded border-2 border-dashed border-border bg-muted p-4">
|
|
<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>
|
|
</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;
|