feat: V2Repeater 컴포넌트 추가 및 DynamicComponentRenderer 통합 처리 개선

- V2Repeater 컴포넌트를 추가하여 인라인 테이블, 모달, 버튼 등 다양한 반복 데이터 관리를 지원합니다.
- V2RepeaterConfigPanel을 통해 반복 컴포넌트의 설정 패널을 통합하였습니다.
- DynamicComponentRenderer에서 모든 v2- 컴포넌트를 ComponentRegistry에서 통합 처리하도록 개선하여 코드의 일관성을 높였습니다.
- 레거시 타입을 v2 컴포넌트로 매핑하는 로직을 정리하여 가독성을 향상시켰습니다.
This commit is contained in:
kjs 2026-01-28 17:58:18 +09:00
parent 95bef976a5
commit 3ab8c9b5a0
2 changed files with 35 additions and 318 deletions

View File

@ -20,6 +20,7 @@ import { V2Group } from "./V2Group";
import { V2Media } from "./V2Media"; import { V2Media } from "./V2Media";
import { V2Biz } from "./V2Biz"; import { V2Biz } from "./V2Biz";
import { V2Hierarchy } from "./V2Hierarchy"; import { V2Hierarchy } from "./V2Hierarchy";
import { V2Repeater } from "./V2Repeater";
// 설정 패널 import // 설정 패널 import
import { V2InputConfigPanel } from "./config-panels/V2InputConfigPanel"; import { V2InputConfigPanel } from "./config-panels/V2InputConfigPanel";
@ -31,6 +32,7 @@ import { V2GroupConfigPanel } from "./config-panels/V2GroupConfigPanel";
import { V2MediaConfigPanel } from "./config-panels/V2MediaConfigPanel"; import { V2MediaConfigPanel } from "./config-panels/V2MediaConfigPanel";
import { V2BizConfigPanel } from "./config-panels/V2BizConfigPanel"; import { V2BizConfigPanel } from "./config-panels/V2BizConfigPanel";
import { V2HierarchyConfigPanel } from "./config-panels/V2HierarchyConfigPanel"; import { V2HierarchyConfigPanel } from "./config-panels/V2HierarchyConfigPanel";
import { V2RepeaterConfigPanel } from "./config-panels/V2RepeaterConfigPanel";
// V2 컴포넌트 정의 // V2 컴포넌트 정의
const v2ComponentDefinitions: ComponentDefinition[] = [ const v2ComponentDefinitions: ComponentDefinition[] = [
@ -179,6 +181,31 @@ const v2ComponentDefinitions: ComponentDefinition[] = [
dataSource: "static", dataSource: "static",
}, },
}, },
{
id: "v2-repeater",
name: "통합 반복",
description: "인라인 테이블, 모달, 버튼 등 다양한 반복 데이터 관리를 지원하는 통합 컴포넌트",
category: ComponentCategory.V2,
webType: "entity" as WebType,
component: V2Repeater as any,
tags: ["repeater", "table", "modal", "button", "data", "v2"],
defaultSize: { width: 600, height: 300 },
configPanel: V2RepeaterConfigPanel as any,
defaultConfig: {
renderMode: "inline",
dataSource: {
tableName: "",
foreignKey: "",
referenceKey: "",
},
columns: [],
features: {
showAddButton: true,
showDeleteButton: true,
inlineEdit: false,
},
},
},
]; ];
/** /**

View File

@ -1,25 +1,11 @@
"use client"; "use client";
import React, { useCallback } from "react"; import React from "react";
import { ComponentData } from "@/types/screen"; import { ComponentData } from "@/types/screen";
import { DynamicLayoutRenderer } from "./DynamicLayoutRenderer"; import { DynamicLayoutRenderer } from "./DynamicLayoutRenderer";
import { ComponentRegistry } from "./ComponentRegistry"; import { ComponentRegistry } from "./ComponentRegistry";
import { filterDOMProps } from "@/lib/utils/domPropsFilter"; import { filterDOMProps } from "@/lib/utils/domPropsFilter";
// V2 컴포넌트 import
import {
V2Input,
V2Select,
V2Date,
V2List,
V2Layout,
V2Group,
V2Media,
V2Biz,
V2Hierarchy,
} from "@/components/v2";
import { V2Repeater } from "@/components/v2/V2Repeater";
// 통합 폼 시스템 import // 통합 폼 시스템 import
import { useV2FormOptional } from "@/components/v2/V2FormContext"; import { useV2FormOptional } from "@/components/v2/V2FormContext";
@ -189,11 +175,13 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
// 컴포넌트 타입 추출 - 새 시스템에서는 componentType 속성 사용, 레거시는 type 사용 // 컴포넌트 타입 추출 - 새 시스템에서는 componentType 속성 사용, 레거시는 type 사용
const rawComponentType = (component as any).componentType || component.type; const rawComponentType = (component as any).componentType || component.type;
// 🆕 레거시 타입을 v2 컴포넌트로 매핑 (v2 컴포넌트가 없으면 원본 유지) // 레거시 타입을 v2 컴포넌트로 매핑 (v2 컴포넌트가 없으면 원본 유지)
const mapToV2ComponentType = (type: string | undefined): string | undefined => { const mapToV2ComponentType = (type: string | undefined): string | undefined => {
if (!type) return type; if (!type) return type;
// 이미 v2- 또는 v2- 접두사가 있으면 그대로 반환
if (type.startsWith("v2-") || type.startsWith("v2-")) return type; // 이미 v2- 접두사가 있으면 그대로 반환
if (type.startsWith("v2-")) return type;
// 레거시 타입을 v2로 매핑 시도 // 레거시 타입을 v2로 매핑 시도
const v2Type = `v2-${type}`; const v2Type = `v2-${type}`;
// v2 버전이 등록되어 있는지 확인 // v2 버전이 등록되어 있는지 확인
@ -208,306 +196,8 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
// 컴포넌트 타입 변환 완료 // 컴포넌트 타입 변환 완료
// 🆕 V2 폼 시스템 연동 (최상위에서 한 번만 호출) // 🆕 모든 v2- 컴포넌트는 ComponentRegistry에서 통합 처리
// eslint-disable-next-line react-hooks/rules-of-hooks // (v2-input, v2-select, v2-repeat-container 등 모두 동일하게 처리)
const v2FormContextLocal = useV2FormOptional();
// 🆕 V2 컴포넌트 처리
if (componentType?.startsWith("v2-")) {
const v2Type = componentType as string;
const config = (component as any).componentConfig || {};
const fieldName = (component as any).columnName || component.id;
// V2 시스템이 있으면 거기서 값 가져오기, 없으면 props.formData 사용
const currentValue = v2FormContextLocal
? v2FormContextLocal.getValue(fieldName)
: props.formData?.[fieldName];
// 🆕 통합 onChange 핸들러 - 양쪽 시스템에 전파
const handleChange = (value: any) => {
// 1. V2 시스템에 전파
if (v2FormContextLocal) {
v2FormContextLocal.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 (v2Type) {
// V2 입력 컴포넌트
case "v2-input":
return (
<V2Input
v2Type="V2Input"
{...commonProps}
config={{
type: config.inputType || config.type || "text",
inputType: 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,
autoGeneration: config.autoGeneration,
tableName: (component as any).tableName || props.tableName,
}}
autoGeneration={config.autoGeneration}
formData={props.formData}
originalData={props.originalData}
/>
);
// V2 선택 컴포넌트
case "v2-select":
// v2-select는 항상 테이블 컬럼에서 distinct 값을 자동 로드
return (
<V2Select
v2Type="V2Select"
{...commonProps}
config={{
mode: config.mode || "dropdown",
source: config.source || "distinct", // 기본값: 테이블 컬럼에서 distinct 조회
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,
}}
/>
);
// V2 날짜 컴포넌트
case "v2-date":
return (
<V2Date
v2Type="V2Date"
{...commonProps}
config={{
type: config.dateType || config.type || "date",
format: config.format,
range: config.range,
minDate: config.minDate,
maxDate: config.maxDate,
showToday: config.showToday,
}}
/>
);
case "v2-list":
// 데이터 소스: config.data > props.tableDisplayData > []
const listData = config.data?.length > 0 ? config.data : props.tableDisplayData || [];
return (
<V2List
v2Type="V2List"
{...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 "v2-layout":
return (
<V2Layout
v2Type="V2Layout"
{...commonProps}
config={{
type: config.layoutType || config.type || "grid",
columns: config.columns,
gap: config.gap,
direction: config.direction,
use12Column: config.use12Column,
}}
>
{children}
</V2Layout>
);
case "v2-group":
return (
<V2Group
v2Type="V2Group"
{...commonProps}
config={{
type: config.groupType || config.type || "section",
collapsible: config.collapsible,
defaultOpen: config.defaultOpen,
tabs: config.tabs || [],
showHeader: config.showHeader,
}}
title={config.title}
>
{children}
</V2Group>
);
case "v2-media":
return (
<V2Media
v2Type="V2Media"
{...commonProps}
config={{
type: config.mediaType || config.type || "image",
accept: config.accept,
maxSize: config.maxSize,
multiple: config.multiple,
preview: config.preview,
}}
/>
);
case "v2-biz":
return (
<V2Biz
v2Type="V2Biz"
{...commonProps}
config={{
type: config.bizType || config.type || "flow",
flowConfig: config.flowConfig,
rackConfig: config.rackConfig,
numberingConfig: config.numberingConfig,
}}
/>
);
case "v2-hierarchy":
return (
<V2Hierarchy
v2Type="V2Hierarchy"
{...commonProps}
config={{
type: config.hierarchyType || config.type || "tree",
viewMode: config.viewMode || "tree",
dataSource: config.dataSource || "static",
maxLevel: config.maxLevel,
draggable: config.draggable,
}}
/>
);
case "v2-repeater":
// 🆕 저장 설정 추출 (useCustomTable, mainTableName, foreignKeyColumn)
const repeaterTargetTable = config.useCustomTable && config.mainTableName
? config.mainTableName
: config.dataSource?.tableName;
return (
<V2Repeater
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">V2 </div>
<div className="text-xs text-amber-500"> : {v2Type}</div>
</div>
</div>
);
}
}
// 🎯 카테고리 타입 우선 처리 (inputType 또는 webType 확인) // 🎯 카테고리 타입 우선 처리 (inputType 또는 webType 확인)
const inputType = (component as any).componentConfig?.inputType || (component as any).inputType; const inputType = (component as any).componentConfig?.inputType || (component as any).inputType;