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";
|
2025-09-19 02:15:21 +09:00
|
|
|
import { filterDOMProps } from "@/lib/utils/domPropsFilter";
|
2025-09-10 14:09:32 +09:00
|
|
|
|
2025-12-19 15:44:38 +09:00
|
|
|
// Unified 컴포넌트 import
|
|
|
|
|
import {
|
|
|
|
|
UnifiedInput,
|
|
|
|
|
UnifiedSelect,
|
|
|
|
|
UnifiedDate,
|
|
|
|
|
UnifiedList,
|
|
|
|
|
UnifiedLayout,
|
|
|
|
|
UnifiedGroup,
|
|
|
|
|
UnifiedMedia,
|
|
|
|
|
UnifiedBiz,
|
|
|
|
|
UnifiedHierarchy,
|
|
|
|
|
} from "@/components/unified";
|
2025-12-23 16:44:53 +09:00
|
|
|
import { UnifiedRepeater } from "@/components/unified/UnifiedRepeater";
|
2025-12-19 15:44:38 +09:00
|
|
|
|
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-12-23 13:53:22 +09:00
|
|
|
onSelectedRowsChange?: (
|
|
|
|
|
selectedRows: any[],
|
|
|
|
|
selectedRowsData: any[],
|
|
|
|
|
sortBy?: string,
|
|
|
|
|
sortOrder?: "asc" | "desc",
|
|
|
|
|
columnOrder?: string[],
|
|
|
|
|
tableDisplayData?: any[],
|
|
|
|
|
) => void;
|
2025-11-04 18:31:26 +09:00
|
|
|
// 테이블 정렬 정보 (엑셀 다운로드용)
|
|
|
|
|
sortBy?: string;
|
|
|
|
|
sortOrder?: "asc" | "desc";
|
2025-11-05 10:23:00 +09:00
|
|
|
tableDisplayData?: any[]; // 🆕 화면 표시 데이터
|
2025-10-23 17:26:14 +09:00
|
|
|
// 플로우 선택된 데이터 정보 (플로우 위젯 선택 액션용)
|
|
|
|
|
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";
|
2025-09-24 18:07:36 +09:00
|
|
|
// 설정 변경 핸들러 (상세설정과 연동)
|
|
|
|
|
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; // 반응형 모드 플래그
|
2025-10-17 15:31:23 +09:00
|
|
|
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 (카테고리 관리 등에 필요)
|
2025-11-11 14:32:00 +09:00
|
|
|
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;
|
2025-11-25 12:07:14 +09:00
|
|
|
onSave?: () => Promise<void>; // 🆕 EditModal의 handleSave 콜백
|
2025-09-19 02:15:21 +09:00
|
|
|
// 테이블 선택된 행 정보 (다중 선택 액션용)
|
|
|
|
|
selectedRows?: any[];
|
2025-11-24 15:24:31 +09:00
|
|
|
// 🆕 그룹 데이터 (EditModal → ModalRepeaterTable)
|
|
|
|
|
groupedData?: Record<string, any>[];
|
2025-11-25 14:23:54 +09:00
|
|
|
// 🆕 비활성화할 필드 목록 (EditModal → 각 컴포넌트)
|
|
|
|
|
disabledFields?: string[];
|
2025-09-19 02:15:21 +09:00
|
|
|
selectedRowsData?: any[];
|
2025-12-23 13:53:22 +09:00
|
|
|
onSelectedRowsChange?: (
|
|
|
|
|
selectedRows: any[],
|
|
|
|
|
selectedRowsData: any[],
|
|
|
|
|
sortBy?: string,
|
|
|
|
|
sortOrder?: "asc" | "desc",
|
|
|
|
|
columnOrder?: string[],
|
|
|
|
|
tableDisplayData?: any[],
|
|
|
|
|
) => void;
|
2025-11-04 18:31:26 +09:00
|
|
|
// 테이블 정렬 정보 (엑셀 다운로드용)
|
|
|
|
|
sortBy?: string;
|
|
|
|
|
sortOrder?: "asc" | "desc";
|
|
|
|
|
columnOrder?: string[];
|
2025-11-05 10:23:00 +09:00
|
|
|
tableDisplayData?: any[]; // 🆕 화면 표시 데이터
|
2025-10-23 17:26:14 +09:00
|
|
|
// 플로우 선택된 데이터 정보 (플로우 위젯 선택 액션용)
|
|
|
|
|
flowSelectedData?: any[];
|
|
|
|
|
flowSelectedStepId?: number | null;
|
|
|
|
|
onFlowSelectedDataChange?: (selectedData: any[], stepId: number | null) => void;
|
2025-09-19 02:15:21 +09:00
|
|
|
// 테이블 새로고침 키
|
|
|
|
|
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-12-17 15:00:15 +09:00
|
|
|
// 탭 관련 정보 (탭 내부의 컴포넌트에서 사용)
|
2025-12-23 13:53:22 +09:00
|
|
|
parentTabId?: string; // 부모 탭 ID
|
2025-12-17 15:00:15 +09:00
|
|
|
parentTabsComponentId?: string; // 부모 탭 컴포넌트 ID
|
2025-12-22 10:44:22 +09:00
|
|
|
// 🆕 조건부 비활성화 상태
|
|
|
|
|
conditionalDisabled?: 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-12-19 15:44:38 +09:00
|
|
|
// 🆕 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];
|
2025-12-23 13:53:22 +09:00
|
|
|
|
2025-12-19 15:44:38 +09:00
|
|
|
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,
|
2025-12-22 10:44:22 +09:00
|
|
|
// conditionalDisabled가 true이면 비활성화
|
|
|
|
|
disabled: (component as any).disabled || props.disabledFields?.includes(fieldName) || props.conditionalDisabled,
|
2025-12-19 15:44:38 +09:00
|
|
|
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":
|
2025-12-23 13:53:22 +09:00
|
|
|
// 데이터 소스: config.data > props.tableDisplayData > []
|
|
|
|
|
const listData = config.data?.length > 0 ? config.data : props.tableDisplayData || [];
|
|
|
|
|
|
2025-12-19 15:44:38 +09:00
|
|
|
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,
|
2025-12-23 13:53:22 +09:00
|
|
|
pageable: config.pageable,
|
|
|
|
|
pageSize: config.pageSize,
|
|
|
|
|
cardConfig: config.cardConfig,
|
|
|
|
|
dataSource: {
|
|
|
|
|
table: config.dataSource?.table || props.tableName,
|
|
|
|
|
},
|
2025-12-19 15:44:38 +09:00
|
|
|
}}
|
2025-12-23 13:53:22 +09:00
|
|
|
data={listData}
|
|
|
|
|
selectedRows={props.selectedRowsData || []}
|
|
|
|
|
onRowSelect={
|
|
|
|
|
props.onSelectedRowsChange
|
|
|
|
|
? (rows) =>
|
|
|
|
|
props.onSelectedRowsChange?.(
|
|
|
|
|
rows.map((r: any) => r.id || r.objid),
|
|
|
|
|
rows,
|
|
|
|
|
props.sortBy,
|
|
|
|
|
props.sortOrder,
|
|
|
|
|
undefined,
|
|
|
|
|
props.tableDisplayData,
|
|
|
|
|
)
|
|
|
|
|
: undefined
|
|
|
|
|
}
|
2025-12-19 15:44:38 +09:00
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
|
2025-12-23 16:44:53 +09:00
|
|
|
case "unified-repeater":
|
|
|
|
|
return (
|
|
|
|
|
<UnifiedRepeater
|
|
|
|
|
config={{
|
|
|
|
|
renderMode: config.renderMode || "inline",
|
2025-12-24 09:58:22 +09:00
|
|
|
dataSource: {
|
|
|
|
|
tableName: config.dataSource?.tableName || props.tableName || "",
|
|
|
|
|
foreignKey: config.dataSource?.foreignKey || "",
|
|
|
|
|
referenceKey: config.dataSource?.referenceKey || "",
|
|
|
|
|
sourceTable: config.dataSource?.sourceTable,
|
|
|
|
|
displayColumn: config.dataSource?.displayColumn,
|
2025-12-23 16:44:53 +09:00
|
|
|
},
|
|
|
|
|
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);
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
|
2025-12-19 15:44:38 +09:00
|
|
|
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>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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;
|
2025-12-23 13:53:22 +09:00
|
|
|
|
2025-11-05 18:08:51 +09:00
|
|
|
// 카테고리 셀렉트: webType이 "category"이고 tableName과 columnName이 있는 경우만
|
2025-11-20 18:31:50 +09:00
|
|
|
// ⚠️ 단, componentType이 "select-basic"인 경우는 ComponentRegistry로 처리 (다중선택 등 고급 기능 지원)
|
2025-12-23 13:53:22 +09:00
|
|
|
if (
|
|
|
|
|
(inputType === "category" || webType === "category") &&
|
|
|
|
|
tableName &&
|
|
|
|
|
columnName &&
|
|
|
|
|
componentType === "select-basic"
|
|
|
|
|
) {
|
2025-11-20 18:31:50 +09:00
|
|
|
// select-basic은 ComponentRegistry에서 처리하도록 아래로 통과
|
|
|
|
|
} else if ((inputType === "category" || webType === "category") && tableName && columnName) {
|
2025-11-05 18:08:51 +09:00
|
|
|
try {
|
|
|
|
|
const { CategorySelectComponent } = require("@/lib/registry/components/category-select/CategorySelectComponent");
|
|
|
|
|
const fieldName = columnName || component.id;
|
|
|
|
|
const currentValue = props.formData?.[fieldName] || "";
|
2025-12-23 13:53:22 +09:00
|
|
|
|
2025-11-05 18:08:51 +09:00
|
|
|
const handleChange = (value: any) => {
|
|
|
|
|
if (props.onFormDataChange) {
|
|
|
|
|
props.onFormDataChange(fieldName, value);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-04 10:39:07 +09:00
|
|
|
// 🆕 disabledFields 체크 + readonly 체크
|
|
|
|
|
const isFieldDisabled = props.disabledFields?.includes(columnName) || (component as any).disabled;
|
|
|
|
|
const isFieldReadonly = (component as any).readonly || (component as any).componentConfig?.readonly;
|
2025-11-25 14:23:54 +09:00
|
|
|
|
2025-11-05 18:08:51 +09:00
|
|
|
return (
|
|
|
|
|
<CategorySelectComponent
|
|
|
|
|
tableName={tableName}
|
|
|
|
|
columnName={columnName}
|
|
|
|
|
value={currentValue}
|
|
|
|
|
onChange={handleChange}
|
|
|
|
|
placeholder={component.componentConfig?.placeholder || "선택하세요"}
|
|
|
|
|
required={(component as any).required}
|
2025-11-25 14:23:54 +09:00
|
|
|
disabled={isFieldDisabled}
|
2025-12-04 10:39:07 +09:00
|
|
|
readonly={isFieldReadonly}
|
2025-11-05 18:08:51 +09:00
|
|
|
className="w-full"
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("❌ CategorySelectComponent 로드 실패:", error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-10 18:36:28 +09:00
|
|
|
// 레이아웃 컴포넌트 처리
|
|
|
|
|
if (componentType === "layout") {
|
2025-09-19 02:15:21 +09:00
|
|
|
// 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}
|
2025-09-19 02:15:21 +09:00
|
|
|
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) {
|
2025-09-19 02:15:21 +09:00
|
|
|
// React 전용 props들을 명시적으로 분리하고 DOM 안전한 props만 전달
|
|
|
|
|
const {
|
|
|
|
|
isInteractive,
|
|
|
|
|
formData,
|
|
|
|
|
onFormDataChange,
|
|
|
|
|
tableName,
|
2025-11-05 15:23:57 +09:00
|
|
|
menuId, // 🆕 메뉴 ID
|
2025-11-11 14:32:00 +09:00
|
|
|
menuObjid, // 🆕 메뉴 OBJID (메뉴 스코프)
|
2025-11-05 15:23:57 +09:00
|
|
|
selectedScreen, // 🆕 화면 정보
|
2025-09-19 02:15:21 +09:00
|
|
|
onRefresh,
|
|
|
|
|
onClose,
|
2025-11-25 12:07:14 +09:00
|
|
|
onSave, // 🆕 EditModal의 handleSave 콜백
|
2025-09-19 02:15:21 +09:00
|
|
|
screenId,
|
2025-10-29 11:26:00 +09:00
|
|
|
userId, // 🆕 사용자 ID
|
|
|
|
|
userName, // 🆕 사용자 이름
|
|
|
|
|
companyCode, // 🆕 회사 코드
|
2025-09-19 02:15:21 +09:00
|
|
|
mode,
|
|
|
|
|
isInModal,
|
|
|
|
|
originalData,
|
|
|
|
|
allComponents,
|
|
|
|
|
onUpdateLayout,
|
|
|
|
|
onZoneClick,
|
|
|
|
|
selectedRows,
|
|
|
|
|
selectedRowsData,
|
|
|
|
|
onSelectedRowsChange,
|
2025-11-04 18:31:26 +09:00
|
|
|
sortBy, // 🆕 정렬 컬럼
|
|
|
|
|
sortOrder, // 🆕 정렬 방향
|
2025-11-05 10:23:00 +09:00
|
|
|
tableDisplayData, // 🆕 화면 표시 데이터
|
2025-10-23 17:26:14 +09:00
|
|
|
flowSelectedData,
|
|
|
|
|
flowSelectedStepId,
|
|
|
|
|
onFlowSelectedDataChange,
|
2025-09-19 02:15:21 +09:00
|
|
|
refreshKey,
|
2025-10-23 17:55:04 +09:00
|
|
|
flowRefreshKey, // Added this
|
|
|
|
|
onFlowRefresh, // Added this
|
2025-09-24 18:07:36 +09:00
|
|
|
onConfigChange,
|
2025-10-22 17:19:47 +09:00
|
|
|
isPreview,
|
|
|
|
|
autoGeneration,
|
2025-11-25 14:23:54 +09:00
|
|
|
disabledFields, // 🆕 비활성화 필드 목록
|
2025-10-22 17:19:47 +09:00
|
|
|
...restProps
|
2025-09-19 02:15:21 +09:00
|
|
|
} = props;
|
2025-10-29 11:26:00 +09:00
|
|
|
|
2025-10-22 17:19:47 +09:00
|
|
|
// DOM 안전한 props만 필터링
|
|
|
|
|
const safeProps = filterDOMProps(restProps);
|
2025-09-19 02:15:21 +09:00
|
|
|
|
|
|
|
|
// 컴포넌트의 columnName에 해당하는 formData 값 추출
|
|
|
|
|
const fieldName = (component as any).columnName || component.id;
|
2025-12-23 13:53:22 +09:00
|
|
|
|
2025-11-19 11:48:00 +09:00
|
|
|
// modal-repeater-table은 배열 데이터를 다루므로 빈 배열로 초기화
|
|
|
|
|
let currentValue;
|
2025-12-11 13:25:13 +09:00
|
|
|
if (componentType === "modal-repeater-table" || componentType === "repeat-screen-modal") {
|
2025-11-25 16:56:50 +09:00
|
|
|
// EditModal에서 전달된 groupedData가 있으면 우선 사용
|
2025-11-24 15:24:31 +09:00
|
|
|
currentValue = props.groupedData || formData?.[fieldName] || [];
|
2025-11-19 11:48:00 +09:00
|
|
|
} else {
|
|
|
|
|
currentValue = formData?.[fieldName] || "";
|
|
|
|
|
}
|
2025-12-23 13:53:22 +09:00
|
|
|
|
2025-10-17 15:31:23 +09:00
|
|
|
// onChange 핸들러 - 컴포넌트 타입에 따라 다르게 처리
|
|
|
|
|
const handleChange = (value: any) => {
|
2025-12-01 15:21:03 +09:00
|
|
|
// autocomplete-search-input, entity-search-input은 자체적으로 onFormDataChange를 호출하므로 중복 저장 방지
|
|
|
|
|
if (componentType === "autocomplete-search-input" || componentType === "entity-search-input") {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-17 15:31:23 +09:00
|
|
|
if (onFormDataChange) {
|
2025-11-19 11:48:00 +09:00
|
|
|
// modal-repeater-table은 배열 데이터를 다룸
|
|
|
|
|
if (componentType === "modal-repeater-table") {
|
|
|
|
|
onFormDataChange(fieldName, actualValue);
|
|
|
|
|
}
|
2025-10-17 15:31:23 +09:00
|
|
|
// RepeaterInput 같은 복합 컴포넌트는 전체 데이터를 전달
|
2025-11-19 11:48:00 +09:00
|
|
|
else if (componentType === "repeater-field-group" || componentType === "repeater") {
|
2025-10-21 14:41:56 +09:00
|
|
|
onFormDataChange(fieldName, actualValue);
|
2025-10-17 15:31:23 +09:00
|
|
|
} else {
|
2025-10-21 14:41:56 +09:00
|
|
|
onFormDataChange(fieldName, actualValue);
|
2025-10-17 15:31:23 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-10-16 15:05:24 +09:00
|
|
|
// 렌더러 props 구성
|
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
|
|
|
|
2025-12-16 18:02:08 +09:00
|
|
|
// 숨김 처리: 인터랙티브 모드(실제 뷰)에서만 숨김, 디자인 모드에서는 표시
|
|
|
|
|
if (hiddenValue && isInteractive) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-17 14:22:50 +09:00
|
|
|
// 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,
|
|
|
|
|
};
|
2025-11-17 12:23:45 +09:00
|
|
|
|
2025-12-02 18:03:52 +09:00
|
|
|
// 🆕 엔티티 검색 컴포넌트는 componentConfig.tableName을 사용해야 함 (화면 테이블이 아닌 검색 대상 테이블)
|
2025-12-23 13:53:22 +09:00
|
|
|
const useConfigTableName =
|
|
|
|
|
componentType === "entity-search-input" ||
|
|
|
|
|
componentType === "autocomplete-search-input" ||
|
|
|
|
|
componentType === "modal-repeater-table";
|
|
|
|
|
|
2025-10-16 15:05:24 +09:00
|
|
|
const rendererProps = {
|
|
|
|
|
component,
|
|
|
|
|
isSelected,
|
|
|
|
|
onClick,
|
|
|
|
|
onDragStart,
|
|
|
|
|
onDragEnd,
|
|
|
|
|
size: component.size || newComponent.defaultSize,
|
|
|
|
|
position: component.position,
|
2025-11-17 14:22:50 +09:00
|
|
|
style: finalStyle, // size를 포함한 최종 style
|
2025-10-16 15:05:24 +09:00
|
|
|
config: component.componentConfig,
|
|
|
|
|
componentConfig: component.componentConfig,
|
2025-11-17 16:48:42 +09:00
|
|
|
// componentConfig의 모든 속성을 props로 spread (tableName, displayField 등)
|
|
|
|
|
...(component.componentConfig || {}),
|
2025-10-16 15:05:24 +09:00
|
|
|
value: currentValue, // formData에서 추출한 현재 값 전달
|
|
|
|
|
// 새로운 기능들 전달
|
2025-10-23 15:06:00 +09:00
|
|
|
autoGeneration: component.autoGeneration || component.componentConfig?.autoGeneration,
|
|
|
|
|
hidden: hiddenValue,
|
2025-10-16 15:05:24 +09:00
|
|
|
// React 전용 props들은 직접 전달 (DOM에 전달되지 않음)
|
|
|
|
|
isInteractive,
|
|
|
|
|
formData,
|
|
|
|
|
onFormDataChange,
|
2025-10-17 15:31:23 +09:00
|
|
|
onChange: handleChange, // 개선된 onChange 핸들러 전달
|
2025-12-02 18:03:52 +09:00
|
|
|
// 🆕 엔티티 검색 컴포넌트는 componentConfig.tableName 유지, 그 외는 화면 테이블명 사용
|
2025-12-23 13:53:22 +09:00
|
|
|
tableName: useConfigTableName ? component.componentConfig?.tableName || tableName : tableName,
|
2025-11-05 15:23:57 +09:00
|
|
|
menuId, // 🆕 메뉴 ID
|
2025-11-11 14:32:00 +09:00
|
|
|
menuObjid, // 🆕 메뉴 OBJID (메뉴 스코프)
|
2025-11-05 15:23:57 +09:00
|
|
|
selectedScreen, // 🆕 화면 정보
|
2025-10-16 15:05:24 +09:00
|
|
|
onRefresh,
|
|
|
|
|
onClose,
|
2025-11-25 12:07:14 +09:00
|
|
|
onSave, // 🆕 EditModal의 handleSave 콜백
|
2025-10-16 15:05:24 +09:00
|
|
|
screenId,
|
2025-10-29 11:26:00 +09:00
|
|
|
userId, // 🆕 사용자 ID
|
|
|
|
|
userName, // 🆕 사용자 이름
|
|
|
|
|
companyCode, // 🆕 회사 코드
|
2025-12-16 14:38:03 +09:00
|
|
|
// 🆕 화면 모드 (edit/view)와 컴포넌트 UI 모드 구분
|
|
|
|
|
screenMode: mode,
|
|
|
|
|
// componentConfig.mode가 있으면 유지 (entity-search-input의 UI 모드)
|
|
|
|
|
mode: component.componentConfig?.mode || mode,
|
2025-10-16 15:05:24 +09:00
|
|
|
isInModal,
|
|
|
|
|
readonly: component.readonly,
|
2025-11-25 14:23:54 +09:00
|
|
|
// 🆕 disabledFields 체크 또는 기존 readonly
|
|
|
|
|
disabled: disabledFields?.includes(fieldName) || component.readonly,
|
2025-10-16 15:05:24 +09:00
|
|
|
originalData,
|
|
|
|
|
allComponents,
|
|
|
|
|
onUpdateLayout,
|
|
|
|
|
onZoneClick,
|
|
|
|
|
// 테이블 선택된 행 정보 전달
|
|
|
|
|
selectedRows,
|
|
|
|
|
selectedRowsData,
|
|
|
|
|
onSelectedRowsChange,
|
2025-11-04 18:31:26 +09:00
|
|
|
// 테이블 정렬 정보 전달
|
|
|
|
|
sortBy,
|
|
|
|
|
sortOrder,
|
2025-11-05 10:23:00 +09:00
|
|
|
tableDisplayData, // 🆕 화면 표시 데이터
|
2025-10-23 17:26:14 +09:00
|
|
|
// 플로우 선택된 데이터 정보 전달
|
|
|
|
|
flowSelectedData,
|
|
|
|
|
flowSelectedStepId,
|
|
|
|
|
onFlowSelectedDataChange,
|
2025-10-16 15:05:24 +09:00
|
|
|
// 설정 변경 핸들러 전달
|
|
|
|
|
onConfigChange,
|
|
|
|
|
refreshKey,
|
2025-10-23 17:55:04 +09:00
|
|
|
// 플로우 새로고침 키
|
|
|
|
|
flowRefreshKey,
|
|
|
|
|
onFlowRefresh,
|
2025-10-16 18:16:57 +09:00
|
|
|
// 반응형 모드 플래그 전달
|
|
|
|
|
isPreview,
|
2025-10-17 15:31:23 +09:00
|
|
|
// 디자인 모드 플래그 전달 - isPreview와 명확히 구분
|
|
|
|
|
isDesignMode: props.isDesignMode !== undefined ? props.isDesignMode : false,
|
2025-11-24 15:24:31 +09:00
|
|
|
// 🆕 그룹 데이터 전달 (EditModal → ConditionalContainer → ModalRepeaterTable)
|
2025-12-10 16:47:48 +09:00
|
|
|
// Note: 이 props들은 DOM 요소에 전달되면 안 됨
|
|
|
|
|
// 각 컴포넌트에서 명시적으로 destructure하여 사용해야 함
|
2025-12-11 13:25:13 +09:00
|
|
|
groupedData: props.groupedData, // ✅ 언더스코어 제거하여 직접 전달
|
|
|
|
|
_groupedData: props.groupedData, // 하위 호환성 유지
|
2025-12-08 17:54:11 +09:00
|
|
|
// 🆕 UniversalFormModal용 initialData 전달
|
2025-12-24 13:11:52 +09:00
|
|
|
// originalData가 비어있지 않으면 originalData 사용, 아니면 formData 사용
|
|
|
|
|
// 생성 모드에서는 originalData가 빈 객체이므로 formData를 사용해야 함
|
|
|
|
|
_initialData: (originalData && Object.keys(originalData).length > 0) ? originalData : formData,
|
2025-12-10 16:47:48 +09:00
|
|
|
_originalData: originalData,
|
2025-12-18 09:53:26 +09:00
|
|
|
// 🆕 탭 관련 정보 전달 (탭 내부의 테이블 컴포넌트에서 사용)
|
|
|
|
|
parentTabId: props.parentTabId,
|
|
|
|
|
parentTabsComponentId: props.parentTabsComponentId,
|
2025-10-16 15:05:24 +09:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 렌더러가 클래스인지 함수인지 확인
|
2025-12-24 14:46:51 +09:00
|
|
|
const isClass =
|
2025-10-16 15:05:24 +09:00
|
|
|
typeof NewComponentRenderer === "function" &&
|
|
|
|
|
NewComponentRenderer.prototype &&
|
2025-12-24 14:46:51 +09:00
|
|
|
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) {
|
2025-10-16 15:05:24 +09:00
|
|
|
// 클래스 기반 렌더러 (AutoRegisteringComponentRenderer 상속)
|
|
|
|
|
const rendererInstance = new NewComponentRenderer(rendererProps);
|
|
|
|
|
return rendererInstance.render();
|
|
|
|
|
} else {
|
|
|
|
|
// 함수형 컴포넌트
|
2025-12-12 10:55:09 +09:00
|
|
|
// refreshKey를 React key로 전달하여 컴포넌트 리마운트 강제
|
2025-12-24 14:46:51 +09:00
|
|
|
|
|
|
|
|
// 🔧 디버깅: 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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-12 10:55:09 +09:00
|
|
|
return <NewComponentRenderer key={refreshKey} {...rendererProps} />;
|
2025-10-16 15:05:24 +09:00
|
|
|
}
|
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,
|
2025-11-28 14:56:11 +09:00
|
|
|
componentId: component.id,
|
|
|
|
|
componentLabel: component.label,
|
2025-09-12 14:24:25 +09:00
|
|
|
componentType: componentType,
|
2025-11-28 14:56:11 +09:00
|
|
|
originalType: component.type,
|
|
|
|
|
originalComponentType: (component as any).componentType,
|
2025-09-12 14:24:25 +09:00
|
|
|
componentConfig: component.componentConfig,
|
2025-11-28 14:56:11 +09:00
|
|
|
webTypeConfig: (component as any).webTypeConfig,
|
|
|
|
|
autoGeneration: (component as any).autoGeneration,
|
2025-09-12 14:24:25 +09:00
|
|
|
availableNewComponents: ComponentRegistry.getAllComponents().map((c) => c.id),
|
|
|
|
|
availableLegacyComponents: legacyComponentRegistry.getRegisteredTypes(),
|
|
|
|
|
});
|
2025-09-10 14:09:32 +09:00
|
|
|
|
|
|
|
|
// 폴백 렌더링 - 기본 플레이스홀더
|
|
|
|
|
return (
|
2025-12-23 13:53:22 +09:00
|
|
|
<div className="border-border bg-muted flex h-full w-full items-center justify-center rounded border-2 border-dashed p-4">
|
2025-09-10 14:09:32 +09:00
|
|
|
<div className="text-center">
|
2025-12-23 13:53:22 +09:00
|
|
|
<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>
|
2025-09-10 14:09:32 +09:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 동적 렌더링 실행
|
|
|
|
|
try {
|
2025-09-19 02:15:21 +09:00
|
|
|
// 레거시 시스템에서도 DOM 안전한 props만 전달
|
|
|
|
|
const safeLegacyProps = filterDOMProps(props);
|
|
|
|
|
|
2025-09-10 14:09:32 +09:00
|
|
|
return renderer({
|
|
|
|
|
component,
|
|
|
|
|
isSelected,
|
|
|
|
|
onClick,
|
|
|
|
|
onDragStart,
|
|
|
|
|
onDragEnd,
|
|
|
|
|
children,
|
2025-09-19 02:15:21 +09:00
|
|
|
// 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, // 🆕 회사 코드
|
2025-09-19 02:15:21 +09:00
|
|
|
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,
|
2025-10-23 17:26:14 +09:00
|
|
|
// 플로우 선택된 데이터 정보 전달
|
|
|
|
|
flowSelectedData: props.flowSelectedData,
|
|
|
|
|
flowSelectedStepId: props.flowSelectedStepId,
|
|
|
|
|
onFlowSelectedDataChange: props.onFlowSelectedDataChange,
|
2025-09-19 02:15:21 +09:00
|
|
|
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;
|