- EditModal, InteractiveScreenViewer, SaveModal 컴포넌트에서 리피터 데이터(배열)를 마스터 저장에서 제외하고, 별도로 저장하는 로직을 추가하였습니다. - 리피터 데이터 저장 이벤트를 발생시켜 UnifiedRepeater 컴포넌트가 이를 리스닝하도록 개선하였습니다. - 각 컴포넌트에서 최종 저장 데이터 로그를 업데이트하여, 저장 과정에서의 데이터 흐름을 명확히 하였습니다. 이로 인해 데이터 저장의 효율성과 리피터 관리의 일관성이 향상되었습니다.
917 lines
35 KiB
TypeScript
917 lines
35 KiB
TypeScript
"use client";
|
|
|
|
import React, { useCallback } 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";
|
|
|
|
// 통합 폼 시스템 import
|
|
import { useUnifiedFormOptional } from "@/components/unified/UnifiedFormContext";
|
|
|
|
// 컴포넌트 렌더러 인터페이스
|
|
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[];
|
|
// 🆕 컴포넌트 업데이트 콜백 (탭 내부 컴포넌트 위치 조정 등)
|
|
onUpdateComponent?: (updatedComponent: any) => void;
|
|
// 🆕 탭 내부 컴포넌트 선택 콜백
|
|
onSelectTabComponent?: (tabId: string, compId: string, comp: any) => void;
|
|
selectedTabComponentId?: string;
|
|
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 rawComponentType = (component as any).componentType || component.type;
|
|
|
|
// 🆕 레거시 타입을 v2 컴포넌트로 매핑 (v2 컴포넌트가 없으면 원본 유지)
|
|
const mapToV2ComponentType = (type: string | undefined): string | undefined => {
|
|
if (!type) return type;
|
|
// 이미 v2- 또는 unified- 접두사가 있으면 그대로 반환
|
|
if (type.startsWith("v2-") || type.startsWith("unified-")) return type;
|
|
// 레거시 타입을 v2로 매핑 시도
|
|
const v2Type = `v2-${type}`;
|
|
// v2 버전이 등록되어 있는지 확인
|
|
if (ComponentRegistry.hasComponent(v2Type)) {
|
|
return v2Type;
|
|
}
|
|
// v2 버전이 없으면 원본 유지
|
|
return type;
|
|
};
|
|
|
|
const componentType = mapToV2ComponentType(rawComponentType);
|
|
|
|
// 컴포넌트 타입 변환 완료
|
|
|
|
// 🆕 Unified 폼 시스템 연동 (최상위에서 한 번만 호출)
|
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
const unifiedFormContextForUnified = useUnifiedFormOptional();
|
|
|
|
// 🆕 Unified 컴포넌트 처리
|
|
if (componentType?.startsWith("unified-")) {
|
|
const unifiedType = componentType as string;
|
|
const config = (component as any).componentConfig || {};
|
|
const fieldName = (component as any).columnName || component.id;
|
|
|
|
// Unified 시스템이 있으면 거기서 값 가져오기, 없으면 props.formData 사용
|
|
const currentValue = unifiedFormContextForUnified
|
|
? unifiedFormContextForUnified.getValue(fieldName)
|
|
: props.formData?.[fieldName];
|
|
|
|
// 🆕 통합 onChange 핸들러 - 양쪽 시스템에 전파
|
|
const handleChange = (value: any) => {
|
|
// 1. Unified 시스템에 전파
|
|
if (unifiedFormContextForUnified) {
|
|
unifiedFormContextForUnified.setValue(fieldName, value);
|
|
}
|
|
// 2. 레거시 콜백도 호출 (호환성)
|
|
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",
|
|
inputType: config.inputType || config.type || "text", // 🆕 inputType 명시적 전달
|
|
format: config.format,
|
|
placeholder: config.placeholder,
|
|
mask: config.mask,
|
|
min: config.min,
|
|
max: config.max,
|
|
step: config.step,
|
|
buttonText: config.buttonText,
|
|
buttonVariant: config.buttonVariant,
|
|
autoGeneration: config.autoGeneration,
|
|
tableName: (component as any).tableName || props.tableName, // 🆕 채번용 테이블명
|
|
}}
|
|
autoGeneration={config.autoGeneration}
|
|
formData={props.formData}
|
|
originalData={props.originalData}
|
|
/>
|
|
);
|
|
|
|
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":
|
|
// 🆕 저장 설정 추출 (useCustomTable, mainTableName, foreignKeyColumn)
|
|
const repeaterTargetTable = config.useCustomTable && config.mainTableName
|
|
? config.mainTableName
|
|
: config.dataSource?.tableName;
|
|
|
|
return (
|
|
<UnifiedRepeater
|
|
config={{
|
|
renderMode: config.renderMode || "inline",
|
|
// 🆕 저장 설정 추가
|
|
useCustomTable: config.useCustomTable,
|
|
mainTableName: config.mainTableName,
|
|
foreignKeyColumn: config.foreignKeyColumn,
|
|
foreignKeySourceColumn: config.foreignKeySourceColumn, // 🆕 FK 소스 컬럼 추가
|
|
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) => {
|
|
// 🆕 formData 업데이트 (부모로 데이터 전달)
|
|
if (props.onFormDataChange) {
|
|
// _targetTable 메타데이터 추가
|
|
const dataWithTargetTable = data.map((item: any) => ({
|
|
...item,
|
|
_targetTable: repeaterTargetTable,
|
|
}));
|
|
props.onFormDataChange(component.id || "repeaterData", dataWithTargetTable);
|
|
}
|
|
}}
|
|
onRowClick={(row) => {
|
|
}}
|
|
onButtonClick={(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;
|
|
|
|
// 다중 레코드를 다루는 컴포넌트는 배열 데이터로 초기화
|
|
let currentValue;
|
|
if (componentType === "modal-repeater-table" ||
|
|
componentType === "repeat-screen-modal" ||
|
|
componentType === "selected-items-detail-input") {
|
|
// EditModal/ScreenModal에서 전달된 groupedData가 있으면 우선 사용
|
|
currentValue = props.groupedData || formData?.[fieldName] || [];
|
|
} else {
|
|
currentValue = formData?.[fieldName] || "";
|
|
}
|
|
|
|
// 🆕 Unified 폼 시스템 연동 (Context가 있으면 사용, 없으면 null)
|
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
const unifiedFormContext = useUnifiedFormOptional();
|
|
|
|
// onChange 핸들러 - 컴포넌트 타입에 따라 다르게 처리
|
|
// 🆕 Unified 시스템과 레거시 시스템 모두에 전파
|
|
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;
|
|
}
|
|
|
|
// 1. Unified 폼 시스템에 전파 (있으면)
|
|
if (unifiedFormContext) {
|
|
unifiedFormContext.setValue(fieldName, actualValue);
|
|
}
|
|
|
|
// 2. 레거시 onFormDataChange 콜백도 호출 (호환성 유지)
|
|
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 전달
|
|
// 우선순위: props.initialData > originalData > formData
|
|
// 조건부 컨테이너에서 전달된 initialData가 있으면 그것을 사용
|
|
_initialData: props.initialData || ((originalData && Object.keys(originalData).length > 0) ? originalData : formData),
|
|
_originalData: originalData,
|
|
// 🆕 initialData도 직접 전달 (조건부 컨테이너 → 내부 컴포넌트)
|
|
initialData: props.initialData,
|
|
// 🆕 탭 관련 정보 전달 (탭 내부의 테이블 컴포넌트에서 사용)
|
|
parentTabId: props.parentTabId,
|
|
parentTabsComponentId: props.parentTabsComponentId,
|
|
// 🆕 컴포넌트 업데이트 콜백 (탭 내부 컴포넌트 위치 조정 등)
|
|
onUpdateComponent: props.onUpdateComponent,
|
|
// 🆕 탭 내부 컴포넌트 선택 콜백
|
|
onSelectTabComponent: props.onSelectTabComponent,
|
|
selectedTabComponentId: props.selectedTabComponentId,
|
|
};
|
|
|
|
// 렌더러가 클래스인지 함수인지 확인
|
|
const isClass =
|
|
typeof NewComponentRenderer === "function" &&
|
|
NewComponentRenderer.prototype &&
|
|
NewComponentRenderer.prototype.render;
|
|
|
|
if (isClass) {
|
|
// 클래스 기반 렌더러 (AutoRegisteringComponentRenderer 상속)
|
|
const rendererInstance = new NewComponentRenderer(rendererProps);
|
|
return rendererInstance.render();
|
|
} else {
|
|
// 함수형 컴포넌트
|
|
// refreshKey를 React key로 전달하여 컴포넌트 리마운트 강제
|
|
|
|
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;
|