feat: enhance component configuration and rendering

- Updated the RealtimePreviewDynamic component to display selected component information more clearly.
- Added dynamic field type labels in the RealtimePreviewDynamic component for better user understanding.
- Introduced a table refresh counter in the ScreenDesigner component to handle table column updates effectively.
- Improved the V2PropertiesPanel and V2SelectConfigPanel to support additional properties and enhance usability.
- Refactored the DynamicComponentRenderer to better handle field types and improve component configuration merging.

Made-with: Cursor
This commit is contained in:
DDD1542
2026-03-12 15:01:05 +09:00
parent 83aa8f3250
commit 80be7c5a76
11 changed files with 1163 additions and 70 deletions

View File

@@ -302,9 +302,16 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
return type;
};
const componentType = mapToV2ComponentType(rawComponentType);
const mappedComponentType = mapToV2ComponentType(rawComponentType);
// 컴포넌트 타입 변환 완료
// fieldType 기반 동적 컴포넌트 전환 (통합 필드 설정 패널에서 설정된 값)
const componentType = (() => {
const ft = (component as any).componentConfig?.fieldType;
if (!ft) return mappedComponentType;
if (["text", "number", "password", "textarea", "slider", "color", "numbering"].includes(ft)) return "v2-input";
if (["select", "category", "entity"].includes(ft)) return "v2-select";
return mappedComponentType;
})();
// 🆕 조건부 렌더링 체크 (conditionalConfig)
// componentConfig 또는 overrides에서 conditionalConfig를 가져와서 formData와 비교
@@ -738,7 +745,21 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
// 컬럼 메타데이터 기반 componentConfig 병합 (DB 최신 설정 우선)
const isEntityJoinColumn = fieldName?.includes(".");
const baseColumnName = isEntityJoinColumn ? undefined : fieldName;
const mergedComponentConfig = mergeColumnMeta(screenTableName, baseColumnName, component.componentConfig || {});
const rawMergedConfig = mergeColumnMeta(screenTableName, baseColumnName, component.componentConfig || {});
// fieldType이 설정된 경우, source/inputType 보조 속성 자동 보완
const mergedComponentConfig = (() => {
const ft = rawMergedConfig?.fieldType;
if (!ft) return rawMergedConfig;
const patch: Record<string, any> = {};
if (["select", "category", "entity"].includes(ft) && !rawMergedConfig.source) {
patch.source = ft === "category" ? "category" : ft === "entity" ? "entity" : "static";
}
if (["text", "number", "password", "textarea", "slider", "color", "numbering"].includes(ft) && !rawMergedConfig.inputType) {
patch.inputType = ft;
}
return Object.keys(patch).length > 0 ? { ...rawMergedConfig, ...patch } : rawMergedConfig;
})();
// NOT NULL 기반 필수 여부를 component.required에 반영
const notNullRequired = isColumnRequiredByMeta(screenTableName, baseColumnName);
@@ -755,17 +776,16 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
onClick,
onDragStart,
onDragEnd,
size: needsExternalHorizLabel
? { ...(component.size || newComponent.defaultSize), width: undefined, height: undefined }
: component.size || newComponent.defaultSize,
position: component.position,
config: mergedComponentConfig,
componentConfig: mergedComponentConfig,
// componentConfig의 모든 속성을 props로 spread (tableName, displayField 등)
// componentConfig spread를 먼저 → 이후 명시적 속성이 override
...(mergedComponentConfig || {}),
// 🔧 style은 맨 마지막에! (componentConfig.style이 있어도 mergedStyle이 우선)
// size/position/style/label은 componentConfig spread 이후에 설정 (덮어쓰기 방지)
size: needsExternalHorizLabel
? { ...(component.size || newComponent.defaultSize), width: undefined }
: component.size || newComponent.defaultSize,
position: component.position,
style: mergedStyle,
// 수평 라벨 → 외부에서 처리하므로 label 전달 안 함
label: needsExternalHorizLabel ? undefined : effectiveLabel,
// NOT NULL 메타데이터 포함된 필수 여부 (V2Hierarchy 등 직접 props.required 참조하는 컴포넌트용)
required: effectiveRequired,

View File

@@ -6,7 +6,7 @@
import { ComponentCategory } from "@/types/component";
import { createComponentDefinition } from "../../utils/createComponentDefinition";
import { V2InputConfigPanel } from "@/components/v2/config-panels/V2InputConfigPanel";
import { V2FieldConfigPanel } from "@/components/v2/config-panels/V2FieldConfigPanel";
import { V2Input } from "@/components/v2/V2Input";
export const V2InputDefinition = createComponentDefinition({
@@ -72,7 +72,7 @@ export const V2InputDefinition = createComponentDefinition({
tags: ["input", "text", "number", "v2"],
// 설정 패널
configPanel: V2InputConfigPanel,
configPanel: V2FieldConfigPanel,
});
export default V2InputDefinition;

View File

@@ -6,7 +6,7 @@
import { ComponentCategory } from "@/types/component";
import { createComponentDefinition } from "../../utils/createComponentDefinition";
import { V2SelectConfigPanel } from "@/components/v2/config-panels/V2SelectConfigPanel";
import { V2FieldConfigPanel } from "@/components/v2/config-panels/V2FieldConfigPanel";
import { V2Select } from "@/components/v2/V2Select";
export const V2SelectDefinition = createComponentDefinition({
@@ -82,7 +82,7 @@ export const V2SelectDefinition = createComponentDefinition({
tags: ["select", "dropdown", "combobox", "v2"],
// 설정 패널
configPanel: V2SelectConfigPanel,
configPanel: V2FieldConfigPanel,
});
export default V2SelectDefinition;

View File

@@ -8,8 +8,8 @@ import type { ConfigPanelContext } from "@/lib/registry/components/common/Config
// 컴포넌트별 ConfigPanel 동적 import 맵
const CONFIG_PANEL_MAP: Record<string, () => Promise<any>> = {
// ========== V2 컴포넌트 ==========
"v2-input": () => import("@/components/v2/config-panels/V2InputConfigPanel"),
"v2-select": () => import("@/components/v2/config-panels/V2SelectConfigPanel"),
"v2-input": () => import("@/components/v2/config-panels/V2FieldConfigPanel"),
"v2-select": () => import("@/components/v2/config-panels/V2FieldConfigPanel"),
"v2-date": () => import("@/components/v2/config-panels/V2DateConfigPanel"),
"v2-list": () => import("@/components/v2/config-panels/V2ListConfigPanel"),
"v2-media": () => import("@/components/v2/config-panels/V2MediaConfigPanel"),
@@ -205,6 +205,7 @@ export interface ComponentConfigPanelProps {
menuObjid?: number;
allComponents?: any[];
currentComponent?: any;
componentType?: string;
}
export const DynamicComponentConfigPanel: React.FC<ComponentConfigPanelProps> = ({
@@ -217,6 +218,7 @@ export const DynamicComponentConfigPanel: React.FC<ComponentConfigPanelProps> =
menuObjid,
allComponents,
currentComponent,
componentType,
}) => {
const [ConfigPanelComponent, setConfigPanelComponent] = React.useState<React.ComponentType<any> | null>(null);
const [loading, setLoading] = React.useState(true);
@@ -484,6 +486,8 @@ export const DynamicComponentConfigPanel: React.FC<ComponentConfigPanelProps> =
onConfigChange={onChange}
context={context}
screenTableName={screenTableName}
tableName={screenTableName}
columnName={currentComponent?.columnName || config?.columnName || config?.fieldName}
tableColumns={selectedTableColumns}
tables={tables}
allTables={allTablesList.length > 0 ? allTablesList : tables}
@@ -493,6 +497,7 @@ export const DynamicComponentConfigPanel: React.FC<ComponentConfigPanelProps> =
currentComponent={currentComponent}
screenComponents={screenComponents}
inputType={currentComponent?.inputType || config?.inputType}
componentType={componentType || componentId}
sourceTableColumns={sourceTableColumns}
targetTableColumns={targetTableColumns}
onSourceTableChange={handleSourceTableChange}