Files
invyone/frontend/components/dash/BlockRenderer.tsx
T
gbpark a5bbd1eb7c refactor(numbering-rule): NumberingRule → Input canonical 흡수 + 채번 관리 페이지 분리
- 옛 registry/numbering-rule, registry/v2-numbering-rule, V2NumberingRuleConfigPanel,
  NumberingRuleTemplate 폐기 — InvFieldConfigPanel + InputComponent 로 통합
- input 에 numbering-picker / select-pickers 추가, autonum 타입 흡수
- 채번 관리 전용 admin 페이지(systemMng/numberingRuleList) + CreateDialog +
  SequenceManagementPanel 신설
- backend NumberingRule controller/service/mapper 갱신 (시퀀스 관리 엔드포인트)
- input canonical 진행 노트 + 채번 관리 mockup 추가

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 21:42:13 +09:00

164 lines
5.2 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';
// 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 def = ComponentRegistry.getComponent(block.componentId);
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}
/>
);
}