Files
invyone/frontend/components/dash/BlockRenderer.tsx
T
DDD1542 2f398ae0b3 chore: 제어모드 IDE 작업 + v2/legacy 레지스트리 컴포넌트 폐기
- 제어모드 IDE: ControlCardPanel, control/ide/* (Canvas/LeftRail/RightRail/PanZoomStage/V3RuleNode 등), schemas, lib/api/control
- 레지스트리 정리: aggregation-widget, status-count, section-card/paper, table-list(legacy/v2), tabs-widget 폐기 → table/_shared/ 로 통합
- InvLegacyButtonConfigPanel cp 마이그레이션
- canonical data view cleanup 후속 노트
2026-05-19 21:31:03 +09:00

168 lines
5.3 KiB
TypeScript

'use client';
/**
* BlockRenderer — 하나의 BlockV2 를 실제 React 컴포넌트로 렌더한다.
* ComponentRegistry 에서 componentId 로 정의를 찾아 위임.
*
* TemplateRenderer (line grid) 와 PopupTemplateRenderer (absolute) 양쪽에서
* 공유되는 최소 단위. 별도 파일로 분리한 이유는 팝업 번들 경량화 —
* PopupTemplateRenderer 가 TemplateRenderer 전체(line layout 알고리즘 800+줄)
* 를 딸려 로드하지 않도록.
*/
import React from 'react';
import type { BlockV2, CanvasV2 } from '@/types/invyone-component';
import { ComponentRegistry } from '@/lib/registry/ComponentRegistry';
import { isTableLikeComponentType } from '@/lib/utils/componentTypeUtils';
// side-effect: 컴포넌트 레지스트리 등록
import '@/lib/registry/components';
import type { TemplateRenderContext, ViewKey } from './TemplateRenderer';
export function BlockRenderer({
block,
context,
view,
canvas,
runtimeSize,
}: {
block: BlockV2;
context: TemplateRenderContext;
view: ViewKey;
/**
* 선택: 제공되면 block 의 %좌표를 px 로 환산해 컴포넌트에 전달한다.
* line grid 의 실제 runtime cell 크기를 component.size 로 전달한다.
*/
canvas?: CanvasV2;
runtimeSize?: {
width: number;
height: number;
};
}) {
const resolvedTableName =
block.config?.selectedTable ||
block.config?.tableName ||
context.primaryTable;
const resolvedColumnName =
block.config?.columnName ||
block.config?.column_name ||
block.config?.fieldKey ||
block.config?.bindField ||
block.config?.column;
const resolvedValue =
resolvedColumnName != null
? context.formRow?.[resolvedColumnName]
: undefined;
// 사용자가 ConfigPanel 에 설정한 defaultValue 를 form data 값이 없을 때 보존.
// formRow[col] 가 의미있는 값일 때만 defaultValue 로 hijack — undefined/null 은 무시.
const runtimeConfig =
resolvedColumnName != null && resolvedValue !== undefined && resolvedValue !== null
? { ...block.config, defaultValue: resolvedValue }
: block.config;
const handleFormValueChange = (
fieldNameOrPatch: string | Record<string, any>,
value?: any,
) => {
if (typeof fieldNameOrPatch === 'string') {
context.onFormRowChange?.({ [fieldNameOrPatch]: value });
return;
}
context.onFormRowChange?.(fieldNameOrPatch);
};
const registryComponentId = isTableLikeComponentType(block.componentId)
? 'table'
: block.componentId;
const def = ComponentRegistry.getComponent(registryComponentId);
if (!def?.component) {
return (
<div className="flex h-full w-full items-center justify-center rounded border border-dashed border-slate-300 bg-slate-50 p-2 text-center text-[10px] text-slate-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-400">
<div>
<div className="font-mono text-[11px] font-bold">
{block.componentId || '(empty)'}
</div>
<div className="opacity-70"></div>
</div>
</div>
);
}
const Cmp = def.component as React.ComponentType<any>;
const bw = canvas?.baseWidth ?? 0;
const bh = canvas?.baseHeight ?? 0;
const position = {
x: bw > 0 ? block.xPct * bw : 0,
y: bh > 0 ? block.yPct * bh : 0,
z: 1,
};
const size = {
width: bw > 0 ? block.wPct * bw : 0,
height: bh > 0 ? block.hPct * bh : 0,
};
const isButtonLike =
block.componentId === 'button' ||
block.componentId === 'button-bar' ||
block.componentId === 'pagination';
const effectiveSize =
isButtonLike && runtimeSize
? {
width:
runtimeSize.width > 0
? Math.min(size.width, Math.max(0, runtimeSize.width - 2))
: size.width,
height:
runtimeSize.height > 0
? Math.min(size.height, Math.max(0, runtimeSize.height - 2))
: size.height,
}
: size;
return (
<Cmp
component={{
id: block.id,
componentType: block.componentId,
tableName: resolvedTableName,
columnName: resolvedColumnName,
column_name: resolvedColumnName,
value: resolvedValue,
position,
size: effectiveSize,
componentConfig: runtimeConfig,
component_config: runtimeConfig,
style: {},
}}
componentConfig={runtimeConfig}
config={runtimeConfig}
tableName={resolvedTableName}
columnName={resolvedColumnName}
column_name={resolvedColumnName}
value={resolvedValue}
isDesignMode={false}
isPreview={true}
isInteractive={true}
formData={context.formRow}
form_data={context.formRow}
onFormDataChange={(fieldName: string, value: any) =>
handleFormValueChange(fieldName, value)
}
onChange={(value: any) =>
resolvedColumnName
? handleFormValueChange(resolvedColumnName, value)
: undefined
}
originalData={context.formRow}
_originalData={context.formRow}
onSearch={context.onSearch}
searchParams={context.searchParams}
onRowSelect={context.onRowSelect}
onAdd={context.onAdd}
onEdit={context.onEdit}
onDelete={context.onDelete}
onFormSubmit={context.onFormSubmit}
onFormCancel={context.onFormCancel}
selectedRow={context.selectedRow}
view={view}
/>
);
}