refactor: finalize canonical data-view cleanup
Build & Deploy to K8s / build-and-deploy (push) Successful in 6m29s

This commit is contained in:
DDD1542
2026-05-20 11:30:26 +09:00
parent 318cac4f68
commit bd4286f7ac
14 changed files with 737 additions and 48 deletions
@@ -43,8 +43,13 @@ export function ComponentsPanel({
const allComponents = useMemo(() => {
const components = ComponentRegistry.getAllComponents();
// ★ 새 생성 경로는 canonical 'table' (displayMode='table').
// v2-table-list 는 옛 저장 화면 호환 hard blocker 로 자동 등록되지만
// 팔레트에는 hidden 처리한다 (아래 hiddenComponents 참고).
// v2-table-list / table-list registration shell 은 2026-05-20 cleanup 으로 삭제되어
// ComponentRegistry 에는 더 이상 등록되지 않는다. 옛 저장 layout 의 v2-table-list /
// table-list 는 BlockRenderer / DynamicComponentRenderer / templateMigrate 의 alias
// 라우팅으로 canonical 'table' 정의를 통해 들어온 뒤, TableComponent 의 early
// delegation 으로 _shared/{TableListComponent,V2TableListContainerWrapper} 본체에서
// 기능 손실 없이 렌더된다. 아래 hiddenComponents 의 옛 ID 들은 만약 외부 코드가
// register 를 추가하더라도 팔레트에는 노출되지 않도록 한 안전망이다.
return components;
}, []);
+1 -1
View File
@@ -366,7 +366,7 @@ export interface LayoutItem {
y: number;
width: number;
height: number;
componentKind: string; // 정확한 컴포넌트 종류 (table-list, button-primary)
componentKind: string; // 정확한 컴포넌트 종류 (canonical 예: table / button — legacy 예: table-list / v2-table-list / button-primary)
widgetType: string; // 일반적인 위젯 타입 (button, text 등)
label?: string;
bindField?: string; // 바인딩된 필드명 (컬럼명)
+5 -4
View File
@@ -16,11 +16,12 @@
import type { FieldConfig } from "@/types/invyone-component";
/**
* FieldConfig[] → v2-table-list 의 ColumnConfig[] 호환 배열.
* FieldConfig[] → table-like 컴포넌트 (canonical 'table' / legacy 'table-list' /
* hidden 'v2-table-list') 의 ColumnConfig[] 호환 배열.
*
* 상세 매핑 규칙은 v2-table-list 내부 포맷 확정 후 보강한다. 현재는 공통 필드
* (column_name / column_label / visible / display_order / width / align / sortable)
* 만 매핑.
* 현재 공통 필드 (column_name / column_label / visible / display_order /
* width / align / sortable) 만 매핑한다. shared `_shared/V2TableListContainerWrapper`
* 가 본 어댑터를 호출해서 v2 본체에 columns 를 전달한다.
*/
export function fieldsToColumns(
fields: FieldConfig[],
@@ -11,6 +11,7 @@ import { useV2FormOptional } from "@/components/v2/V2FormContext";
import { apiClient } from "@/lib/api/client";
import { getAdaptiveLabelColor } from "@/lib/utils/darkModeColor";
import { isTableLikeComponentType } from "@/lib/utils/componentTypeUtils";
// 컬럼 메타데이터 캐시 (테이블명 → 컬럼 설정 맵)
export const columnMetaCache: Record<string, Record<string, any>> = {};
@@ -932,10 +933,9 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
const rendererInstance = new RendererClass(rendererProps);
renderedElement = rendererInstance.render();
} else {
const needsKeyRefresh =
componentType === "v2-table-list" ||
componentType === "table-list" ||
componentType === "v2-repeater";
// canonical 'table' / legacy 'table-list' / hidden 'v2-table-list' / v2-repeater
// 는 refreshKey 변동 시 강제 remount 가 필요 (data refetch 트리거).
const needsKeyRefresh = isTableLikeComponentType(componentType) || componentType === "v2-repeater";
renderedElement = <NewComponentRenderer key={needsKeyRefresh ? refreshKey : component.id} {...rendererProps} />;
}
@@ -5,19 +5,25 @@ import { ComponentConfig } from "@/types/component";
/**
* Container 컴포넌트 통합 설정 타입
*
* 11개의 레이아웃/컨테이너 계열 컴포넌트를 통합.
* containerType 으로 탭/섹션/아코디언/반복/조건부 분기.
* canonical 새 생성 경로. containerType 으로 tabs / section / accordion / repeater /
* conditional 분기. SectionVariant 로 card / paper / plain 분기.
*
* 흡수 대상 (11):
* - v2-tabs-widget (탭)
* - v2-section-card / v2-section-paper (섹션)
* - v2-repeat-container / v2-repeater (반복)
* - accordion-basic (아코디언)
* - section-card / section-paper (legacy)
* - tabs (legacy)
* - conditional-container (조건부)
* - repeat-container / repeat-screen-modal / repeater-field-group (legacy)
* - screen-split-panel (legacy)
* 흡수 완료 (alias 라우팅 — DynamicComponentRenderer.LEGACY_TO_UNIFIED + getComponentConfigPanel.CONFIG_PANEL_ALIAS):
* - tabs-widget / v2-tabs-widget / tabs / v2-tabs → container(tabs)
* - section-card / v2-section-card → container(section, sectionVariant=card)
* - section-paper / v2-section-paper → container(section, sectionVariant=paper)
*
* 보존 (canonical skeleton 부족 또는 도메인 특화) — concrete blocker, 본 cleanup 범위 외:
* - accordion-basic → canonical container.containerType=accordion skeleton 부족
* - conditional-container → canonical container.containerType=conditional skeleton 부족
* - repeat-container → canonical container.containerType=repeater skeleton 부족
* - v2-repeat-container → 동일. basicV2Components palette item
* - v2-repeater → 별도 데이터 조회/선택 도메인
* - repeat-screen-modal / repeater-field-group → 도메인 특화
* - screen-split-panel → 화면 임베딩 + 데이터 전달 (별도 도메인)
*
* split-panel-layout / v2-split-panel-layout / split-panel-layout2 는 table 의 split
* displayMode 와 짝이고 SplitPanelContext provider 다수 사용처 때문에 별도 보존.
*/
export type ContainerType =
+10 -2
View File
@@ -103,8 +103,16 @@ import "./grouped-table/GroupedTableRenderer"; // Phase G.3.1 — canonical 그
// form 컴포넌트는 롤백됨 (2026-04-11): "폼" 은 별도 컴포넌트가 아닌
// 화면 디자이너의 3뷰 탭(목록/등록 팝업/수정 팝업) 구조로 처리할 예정.
// 관련: notes/gbpark/2026-04-11-component-unification-plan.md §3.2
import "./table/TableRenderer"; // v2-table-list + v2-table-grouped + v2-pivot-grid + v2-split-panel-layout + legacy 9종 흡수
import "./container/ContainerRenderer"; // v2-tabs-widget + v2-section-card/paper + v2-repeat-container + accordion + conditional + legacy 11종 흡수
// canonical 'table' — 새 생성 경로. table-list / v2-table-list 등 옛 ID 는
// LEGACY_TO_UNIFIED + getComponentConfigPanel.CONFIG_PANEL_ALIAS 로 alias 라우팅 후
// TableComponent early delegation 에서 _shared/{TableListComponent,V2TableListContainerWrapper}
// 로 위임된다. shell 폴더 (table-list/, v2-table-list/) 는 2026-05-20 cleanup 으로 삭제됨.
import "./table/TableRenderer";
// canonical 'container' — 새 생성 경로. alias 흡수: tabs-widget / v2-tabs-widget /
// section-card / v2-section-card / section-paper / v2-section-paper / tabs / v2-tabs.
// 보존 (skeleton 부족 또는 도메인 특화): accordion-basic / conditional-container /
// repeat-container / v2-repeat-container / screen-split-panel / split-panel-layout 계열.
import "./container/ContainerRenderer";
import "./v2-repeat-container/RepeatContainerRenderer"; // canonical container.containerType=repeater skeleton 부족 → 보존
// v2-section-card / v2-section-paper → canonical container alias (containerType=section + sectionVariant) 로 라우팅 (auto-register 제거)
import "./domain/v2-rack-structure/RackStructureRenderer";
@@ -14,6 +14,74 @@ import { GroupedView } from "./views/GroupedView";
import { CardView } from "./views/CardView";
import { PivotView } from "./views/PivotView";
/**
* ─────────────────────────────────────────────────────────────────────────
* Legacy / v2 table-list delegation (2026-05-20 canonical cleanup follow-up)
*
* canonical 새 생성 경로는 그대로 'table' (이 파일의 본체 `TableComponent`).
* 단, old 저장 layout 중 componentType === 'table-list' / 'v2-table-list' 인
* 컴포넌트가 BlockRenderer / DynamicComponentRenderer 의 alias 라우팅으로
* 본 컴포넌트에 도달하면 기능 손실 없이 shared legacy/v2 runtime 으로 위임한다.
* ─────────────────────────────────────────────────────────────────────────
*/
import { TableListWrapper as LegacyTableListWrapper } from "./_shared/TableListComponent";
import { TableListContainerWrapper as V2TableListContainerWrapper } from "./_shared/V2TableListContainerWrapper";
/**
* raw original componentType 탐색 — meaningful table type 만 즉시 채택.
*
* BlockRenderer / DynamicComponentRenderer 의 alias 라우팅을 통해 본 컴포넌트에 도달하면
* `component.type` 이 layout JSON 의 일반 값 ("component", "widget", "container", "row" 등)
* 으로 박혀 들어오는 경우가 있다. 그런 generic 값은 delegation 판단에 도움이 안 되므로
* skip 하고 더 구체적인 후보를 계속 본다.
*
* 우선순위:
* 1) MEANINGFUL_TABLE_TYPES 에 즉시 일치 → 채택
* 2) GENERIC_TYPES 에 일치 → skip
* 3) 그 외 첫 non-empty string → fallback 후보로 기억
* 4) 모든 후보 + url last segment 검사 후 fallback 반환
*/
const _MEANINGFUL_TABLE_TYPES = new Set<string>([
"table",
"table-list",
"v2-table-list",
"data-table",
"datatable",
]);
const _GENERIC_COMPONENT_TYPES = new Set<string>([
"component", "widget", "container", "group", "row", "column", "area", "flow", "tabs",
]);
function _resolveRawComponentType(component: any, props: any): string | undefined {
const candidates: unknown[] = [
component?.componentType,
component?.component_type,
component?.componentConfig?.type,
component?.component_config?.type,
props?.componentType,
component?.type,
props?.type,
];
let fallback: string | undefined;
const consider = (v: unknown): string | undefined => {
if (typeof v !== "string" || v.length === 0) return undefined;
if (_MEANINGFUL_TABLE_TYPES.has(v)) return v;
if (_GENERIC_COMPONENT_TYPES.has(v)) return undefined;
if (!fallback) fallback = v;
return undefined;
};
for (const v of candidates) {
const hit = consider(v);
if (hit) return hit;
}
if (typeof component?.url === "string") {
const last = component.url.split("/").pop();
const hit = consider(last);
if (hit) return hit;
}
return fallback;
}
const VALID_MODES: TableDisplayMode[] = ["table", "split", "grouped", "pivot", "card"];
const ROW_HEIGHT_PRESETS: Record<TableRowHeight, string> = {
compact: "28px",
@@ -38,7 +106,50 @@ export const TableComponent: React.FC<TableComponentProps> = ({
style,
...props
}) => {
// ─── 4경로 머지 ───
// ─── Legacy / v2 delegation (early return) ───
// canonical 새 생성 경로는 'table'. 그 외 라우팅된 old type 은 shared runtime 으로 위임.
// props 손실 방지: 모든 props 를 ...rest 로 전달 + component/config/isDesignMode 등 명시.
const _rawType = _resolveRawComponentType(component, props);
if (_rawType === "table-list") {
// old table-list layout — 기능 완전 보존: 필터/정렬/선택/체크박스/카드모드/
// inline edit / toolbar / export / search / linked filter / exclude filter /
// GroupSum / DataProvider / DataReceiver / FieldConfig adapter / tableName /
// selectedTable / dbTable 모두 shared LegacyTableListWrapper 에서 그대로 처리.
return (
<LegacyTableListWrapper
{...(props as any)}
component={component}
config={config as any}
isDesignMode={isDesignMode}
isSelected={isSelected}
onClick={onClick}
onDragStart={onDragStart}
onDragEnd={onDragEnd}
className={className}
style={style}
/>
);
}
if (_rawType === "v2-table-list") {
// old v2-table-list layout — V2TableListContainerWrapper 가
// FieldConfig → columns 어댑터 + ResizeObserver + DataProvider/DataReceiver 포함.
return (
<V2TableListContainerWrapper
{...(props as any)}
component={component}
config={config as any}
isDesignMode={isDesignMode}
isSelected={isSelected}
onClick={onClick}
onDragStart={onDragStart}
onDragEnd={onDragEnd}
className={className}
style={style}
/>
);
}
// ─── 4경로 머지 (canonical 'table' 본체, 기존 코드 그대로) ───
const fromProps: Partial<TableConfig> = {};
const p = props as any;
if (typeof p.displayMode === "string" && (VALID_MODES as string[]).includes(p.displayMode))
@@ -594,11 +594,11 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
const [displayColumns, setDisplayColumns] = useState<ColumnConfig[]>([]);
const [joinColumnMapping, setJoinColumnMapping] = useState<Record<string, string>>({});
const [columnMeta, setColumnMeta] = useState<
Record<string, { webType?: string; codeInfo?: string; inputType?: string }>
Record<string, { web_type?: string; webType?: string; codeInfo?: string; inputType?: string }>
>({});
// 🆕 엔티티 조인 테이블의 컬럼 메타데이터 (테이블명.컬럼명 → inputType)
const [joinedColumnMeta, setJoinedColumnMeta] = useState<
Record<string, { webType?: string; codeInfo?: string; inputType?: string }>
Record<string, { web_type?: string; webType?: string; codeInfo?: string; inputType?: string }>
>({});
const [categoryMappings, setCategoryMappings] = useState<
Record<string, Record<string, { label: string; color?: string }>>
@@ -1170,7 +1170,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
const cached = tableColumnCache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < TABLE_CACHE_TTL) {
const labels: Record<string, string> = {};
const meta: Record<string, { webType?: string; codeInfo?: string; inputType?: string }> = {};
const meta: Record<string, { web_type?: string; webType?: string; codeInfo?: string; inputType?: string }> = {};
// 캐시된 inputTypes 맵 생성
const inputTypeMap: Record<string, string> = {};
@@ -1210,7 +1210,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
});
const labels: Record<string, string> = {};
const meta: Record<string, { webType?: string; codeInfo?: string; inputType?: string }> = {};
const meta: Record<string, { web_type?: string; webType?: string; codeInfo?: string; inputType?: string }> = {};
columns.forEach((col: any) => {
labels[col.column_name] = col.display_name || col.comment || col.column_name;
@@ -1385,7 +1385,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
}
// 조인된 테이블별로 inputType 정보 가져오기
const newJoinedColumnMeta: Record<string, { webType?: string; codeInfo?: string; inputType?: string }> = {};
const newJoinedColumnMeta: Record<string, { web_type?: string; webType?: string; codeInfo?: string; inputType?: string }> = {};
for (const [joinedTable, columns] of Object.entries(joinedTableColumns)) {
try {
@@ -396,7 +396,7 @@ import { TableFilter, ColumnVisibility, GroupSumConfig } from "@/types/table-opt
import { useAuth } from "@/hooks/useAuth";
import { useScreenContextOptional } from "@/contexts/ScreenContext";
import { useSplitPanelContext, SplitPanelPosition } from "@/contexts/SplitPanelContext";
import type { DataProvidable, DataReceivable, DataReceiverConfig, DataReceivableComponentType } from "@/types/data-transfer";
import type { DataProvidable, DataReceivable, DataReceiverConfig, DataReceivableComponentType, EntityJoinColumnMeta } from "@/types/data-transfer";
// ========================================
// 인터페이스
@@ -1165,6 +1165,8 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
setIsAllSelected(false);
},
// ★ snake_case 페이로드는 EntityJoinColumnMeta 와 동치 (런타임 컨트랙트 그대로),
// 타입 inference 한계만 cast 로 풀어준다 (2026-05-20 canonical cleanup).
getEntityJoinColumns: () => {
return (tableConfig.columns || [])
.filter((col) => col.additionalJoinInfo)
@@ -1173,7 +1175,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
source_column: col.additionalJoinInfo!.sourceColumn,
join_alias: col.additionalJoinInfo!.joinAlias,
reference_table: col.additionalJoinInfo!.referenceTable,
}));
})) as unknown as EntityJoinColumnMeta[];
},
};
@@ -1317,7 +1319,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
// 프론트엔드 카테고리 매핑으로 추가 라벨 변환
const mapping = categoryMappings[columnName];
if (mapping && Object.keys(mapping).length > 0) {
options = options.map((opt) => ({
options = options.map((opt: any) => ({
value: opt.value,
label: mapping[opt.value]?.label || opt.label,
}));
@@ -1478,7 +1480,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
tableDisplayStore.setTableData(
tableConfig.selectedTable,
initialData,
parsedOrder.filter((col) => col !== "__checkbox__"),
parsedOrder.filter((col: string) => col !== "__checkbox__"),
sortColumn ?? null,
sortDirection,
{
@@ -1602,7 +1604,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
const tables = cached.tables || [];
const tableInfo = tables.find((t: any) => t.table_name === tableConfig.selectedTable);
const label =
tableInfo?.display_name || (tableInfo as any)?.comment || tableInfo?.description || tableConfig.selectedTable;
(tableInfo as any)?.display_name || (tableInfo as any)?.comment || tableInfo?.description || tableConfig.selectedTable;
setTableLabel(label);
return;
}
@@ -1616,7 +1618,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
const tableInfo = tables.find((t: any) => t.table_name === tableConfig.selectedTable);
const label =
tableInfo?.display_name || (tableInfo as any)?.comment || tableInfo?.description || tableConfig.selectedTable;
(tableInfo as any)?.display_name || (tableInfo as any)?.comment || tableInfo?.description || tableConfig.selectedTable;
setTableLabel(label);
} catch (error) {
console.error("테이블 라벨 가져오기 실패:", error);
@@ -2297,6 +2299,8 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
cleanColumnOrder,
newSortColumn,
newSortDirection,
// ★ store 의 setTableData snake_case 시그니처와 호출자 camelCase 가 혼재 (legacy).
// 기능 그대로 유지하려면 키 이름 변경 X — 좁은 cast 로 TS 만 우회.
{
filterConditions: Object.keys(searchValues).length > 0 ? searchValues : undefined,
searchTerm: searchTerm || undefined,
@@ -2305,7 +2309,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
currentPage: currentPage,
pageSize: localPageSize,
totalItems: totalItems,
},
} as any,
);
}
} else {
@@ -2498,7 +2502,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
// currentSplitPosition을 사용하여 정확한 위치 확인 (splitPanelPosition이 없을 수 있음)
const effectiveSplitPosition = splitPanelPosition || currentSplitPosition;
if (splitPanelContext && effectiveSplitPosition === "left" && !splitPanelContext.disableAutoDataTransfer) {
if (splitPanelContext && effectiveSplitPosition === "left" && !(splitPanelContext as any).disableAutoDataTransfer) {
if (!isCurrentlySelected) {
// 선택된 경우: 데이터 저장
splitPanelContext.setSelectedLeftData(row);
@@ -2535,7 +2539,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
const effectiveSplitPosition = splitPanelPosition || currentSplitPosition;
if (splitPanelContext && effectiveSplitPosition === "left" && !splitPanelContext.disableAutoDataTransfer) {
if (splitPanelContext && effectiveSplitPosition === "left" && !(splitPanelContext as any).disableAutoDataTransfer) {
// 분할 패널 좌측: 단일 행 선택 모드
if (!isCurrentlySelected) {
setSelectedRows(new Set([rowKey]));
@@ -4178,7 +4182,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
onSelectedRowsChange(
Array.from(selectedRows),
selectedRowsData,
sortColumn,
sortColumn ?? undefined,
sortDirection,
currentColumnOrder,
reorderedData,
@@ -4977,13 +4981,15 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
}, [data, groupByColumns, columnLabels, columnMeta, tableConfig.columns]);
// 🆕 그룹별 합산된 데이터 계산 (FilterPanel에서 설정한 경우)
// ★ groupByColumn 은 GroupSumConfig 가 snake_case (group_by_column) 만 정의하지만,
// 런타임은 camelCase 로 받아서 매핑한다 (FilterPanel 측 호환). cast 로만 우회.
const summedData = useMemo(() => {
// 그룹핑이 비활성화되었거나 그룹 기준 컬럼이 없으면 원본 데이터 반환
if (!groupSumConfig?.enabled || !groupSumConfig?.groupByColumn) {
if (!groupSumConfig?.enabled || !(groupSumConfig as any)?.groupByColumn) {
return filteredData;
}
const groupByColumn = groupSumConfig.groupByColumn;
const groupByColumn = (groupSumConfig as any).groupByColumn;
const groupMap = new Map<string, any>();
// 조인 컬럼인지 확인하고 실제 키 추론
@@ -6111,7 +6117,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
return (
<th
key={column.columnName}
ref={(el) => (columnRefs.current[column.columnName] = el)}
ref={(el) => { columnRefs.current[column.columnName] = el; }}
className={cn(
"group text-muted-foreground relative h-8 overflow-hidden text-[10px] font-bold uppercase tracking-[0.04em] text-ellipsis whitespace-nowrap select-none sm:h-10 sm:text-xs",
column.columnName === "__checkbox__" ? "px-0 py-1" : "px-3 py-2",
@@ -6681,7 +6687,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
ref={editInputRef as any}
value={editingValue}
onChange={(e) => setEditingValue(e.target.value)}
onKeyDown={handleEditKeyDown}
onKeyDown={handleEditKeyDown as any}
onBlur={saveEditing}
className="border-primary bg-background h-8 w-full shrink-0 border-2 px-2 py-1 text-xs focus:outline-none sm:px-4 sm:py-1.5 sm:text-sm"
autoFocus
@@ -77,6 +77,11 @@ export interface ColumnConfig {
autoGeneration?: AutoGenerationConfig; // 자동생성 설정
editable?: boolean; // 🆕 편집 가능 여부 (기본값: true, false면 인라인 편집 불가)
// 🆕 inputType — 컬럼 메타로부터 추론된 webType 캐시 (image / file / date 등)
// 런타임에서 columnMeta[col].inputType 또는 column.inputType 으로 읽혀서 cell rendering 분기에 쓰임.
// table shared 이동 시 노출된 타입 누락 보강 (2026-05-20 canonical cleanup).
inputType?: string;
// 🎯 추가 조인 컬럼 정보 (조인 탭에서 추가한 컬럼들)
additionalJoinInfo?: {
sourceTable: string; // 원본 테이블
+3 -1
View File
@@ -34,8 +34,10 @@ export const isFileComponent = (component: ComponentData): boolean => {
export const isButtonComponent = (component: ComponentData): boolean => {
if (!component || !component.type) return false;
// ComponentData.type union 에는 "button" 이 명시되지 않지만 legacy 저장 layout 에서
// type === "button" 으로 직접 박혀온 경우가 있어 보존 — cast 로만 union 우회.
return (
component.type === "button" ||
(component as any).type === "button" ||
(component.type === "widget" && (component as any).widgetType === "button") ||
(component.type === "component" &&
((component as any).webType === "button" || (component as any).componentType === "button"))
+2 -1
View File
@@ -825,7 +825,8 @@ export interface TemplateComponent {
/**
* 컴포넌트 종류 — ComponentRegistry 의 ID 참조.
* 예: 'v2-table-list', 'v2-button-primary', 'v2-bom-tree'
* canonical 예: 'table', 'container', 'stats', 'button', 'input', 'search'
* legacy 예 (alias 라우팅으로 호환): 'v2-table-list', 'v2-button-primary', 'v2-bom-tree'
*/
componentId: string;
+1 -1
View File
@@ -220,7 +220,7 @@ export interface ComponentComponent extends BaseComponent {
*/
export interface TabInlineComponent {
id: string;
component_type: string; // 컴포넌트 타입 (예: "v2-text-display", "v2-table-list")
component_type: string; // 컴포넌트 타입 (canonical 예: "table" / "container" / "stats" / "button" — legacy v2-text-display / v2-table-list 도 alias 라우팅으로 호환)
label?: string;
position: Position; // 탭 내부에서의 위치
size: Size; // 컴포넌트 크기
@@ -459,3 +459,547 @@ M notes/gbpark/2026-05-19-canonical-data-view-cleanup-followup.md (본 §10)
컴포넌트군 정착 후 `TABLE_LIKE_COMPONENT_TYPES` 본체로 흡수 검토
- `repeat-container``dataSourceType = "table-list"` enum naming — 도메인 분리 위해
`tableList``legacyTableList` 등으로 rename 검토 (별도 트랙)
---
## 12. 2026-05-20 — table-list / v2-table-list cleanup 최종 수렴
§10 / §11 에서 도입한 canonical-aware helper 및 §10.3 의 hard blocker 보존 정책을
유지한 채로, table-list / v2-table-list registration shell 자체를 삭제하고
런타임 본체를 `_shared/` 로 이동, `TableComponent` 의 early delegation 으로 옛 layout
호환을 완성했다. 이번 단계는 잔여 매칭 105건 (43 파일) 전수 분류 + stale 4건 정리 +
TableComponent resolver 견고화 + DynamicComponentRenderer active 분기 helper 화 까지
포함한다.
### 12.1 shell 삭제 완료 (이전 단계 누적)
```
D frontend/lib/registry/components/table-list/ (폴더 전체)
D frontend/lib/registry/components/v2-table-list/ (폴더 전체)
├─ index.ts (ComponentDefinition 등록 shell)
├─ TableListRenderer.tsx
├─ TableListComponent.tsx → _shared/TableListComponent.tsx (cp)
├─ TableListContainerWrapper.tsx → _shared/V2TableListContainerWrapper.tsx
└─ README.md
```
- ComponentRegistry 에 `table-list` / `v2-table-list` 직접 등록 0건
- `registry/components/index.ts` 의 side-effect import 0건
- BlockRenderer / DynamicComponentRenderer / templateMigrate / getComponentConfigPanel
의 alias 라우팅만 남음 (모두 canonical `"table"` 로 라우팅)
### 12.2 shared runtime 경로 (현재 상태)
```
frontend/lib/registry/components/table/_shared/
├── TableListComponent.tsx # legacy table-list 본체 (6838줄)
│ ├─ TableListComponent
│ ├─ TableListWrapper
│ └─ TableListComponentProps
├── V2TableListComponent.tsx # v2-table-list 본체 (7216줄)
│ ├─ TableListComponent
│ ├─ TableListWrapper
│ └─ TableListComponentProps
├── V2TableListContainerWrapper.tsx # v2-table-list 반응형 wrapper (FieldConfig adapter + ResizeObserver)
│ └─ TableListContainerWrapper
├── SingleTableWithSticky.tsx # 공유 sticky 렌더 (variant="v2" 분기 포함)
├── CardModeRenderer.tsx # 공유 카드 모드 렌더 (variant="v2" 분기 포함)
├── TableListConfigPanel.tsx
└── tableListConfigTypes.ts # ColumnConfig / TableListConfig / EntityJoinInfo / ...
```
### 12.3 early delegation 경로 (TableComponent.tsx)
```
[BlockRenderer]
isTableLikeComponentType(componentId) → canonical "table" alias
└─ ComponentRegistry.getComponent("table") → canonical TableComponent
[DynamicComponentRenderer]
LEGACY_TO_UNIFIED: { "v2-table-list": "table", "table-list": "table" }
+ needsKeyRefresh = isTableLikeComponentType(componentType) || componentType === "v2-repeater"
[TableComponent.tsx]
_resolveRawComponentType(component, props):
candidates = [componentType, component_type, componentConfig.type,
component_config.type, props.componentType, component.type,
props.type, url last segment]
- MEANINGFUL Set (table / table-list / v2-table-list / data-table / datatable) → 즉시 채택
- GENERIC Set (component / widget / container / group / row / column / area / flow / tabs) → skip
- 그 외 첫 non-empty string → fallback 후보
├─ rawType === "table-list" → <LegacyTableListWrapper>
├─ rawType === "v2-table-list" → <V2TableListContainerWrapper>
└─ default → 본체 4경로 머지 + Grouped/Card/Pivot/SplitView
```
### 12.4 잔여 105건 (43 파일) 분류
| 분류 | 위치 (대표) | 보존 결정 |
|---|---|---|
| **canonical alias / runtime compat** | DynamicComponentRenderer / templateMigrate / getComponentConfigPanel / TableComponent / componentTypeUtils | ✅ 보존 (concrete blocker) |
| **schema/default old layout compat** | componentConfig.ts v2-table-list overridesSchema / defaultConfig | ✅ 보존 (concrete blocker — 옛 layout JSON 검증) |
| **dataSourceType enum / domain value** | repeat-container/types.ts / v2-repeat-container/types.ts + ConfigPanel select | ✅ 보존 (컴포넌트 type 이 아닌 data source mode enum) |
| **InvDataConfigPanel old config** | InvDataConfigPanel.tsx (5건) | ✅ 보존 (사용자 hard blocker 명시) |
| **shared self-id** | _shared/{Legacy,V2}TableListComponent 의 `component_type: "table-list"` / `componentType="v2-table-list"` (7건) | ✅ 보존 (DataProvider/Receiver 컨트랙트) |
| **table_id naming** | _shared/*.tsx 의 `tableId = \`table-list-${id}\`` / table-options.ts 예시 | ✅ 보존 (DOM/store key) |
| **V2List wrapper** | V2List.tsx line 53 `type: "table-list"` | ✅ 보존 (shared props 시그니처 호환) |
| **component-events publisher/subscriber** | types/component-events.ts (3건) | ✅ 보존 (이벤트 토픽 메타) |
| **helper / 분류 매핑** | componentTypeUtils / MultilangSettingsModal / responsiveDefaults / ScreenNode / RealtimePreviewDynamic / TabsWidget / InvyoneStudio | ✅ 보존 (canonical-aware entry) |
| **stale 주석 (이번 정리)** | screen-management.ts / invyone-component.ts / screenGroup.ts / fieldConfig/adapters.ts | ✏ 갱신 (canonical wording 추가) |
| **docs / README 예시** | selected-items-detail-input/README.md / v2-timeline-scheduler/README.md / 각 컴포넌트 주석 | ✅ 보존 (코드 영향 없음, 옛 layout 흔적) |
| **active branch (이번 정리)** | DynamicComponentRenderer line 935-938 OR 체인 | ✏ `isTableLikeComponentType` 호출로 단일화 |
| **stale 자동등록 주석 (이전 정리, §11.x)** | ComponentsPanel.tsx allComponents 위 주석 | ✏ "shell 삭제 → alias 라우팅 → early delegation" 명시 |
### 12.5 이번 단계 (2026-05-20) 변경 파일
```
M frontend/lib/registry/components/table/TableComponent.tsx
└ _resolveRawComponentType 견고화 (MEANINGFUL 즉시 채택 / GENERIC skip / fallback)
- candidates 우선순위: componentType → component_type → componentConfig.type →
component_config.type → props.componentType →
component.type → props.type → url last segment
M frontend/lib/registry/DynamicComponentRenderer.tsx
└ needsKeyRefresh OR 체인 → isTableLikeComponentType(componentType) || === "v2-repeater"
└ isTableLikeComponentType import 추가
M frontend/components/screen/panels/ComponentsPanel.tsx
└ stale 주석 갱신: "v2-table-list 자동 등록" 진술 제거,
"shell 삭제 → alias 라우팅 → early delegation" 정확한 흐름 명시
M frontend/types/screen-management.ts
└ TabInlineComponent.component_type 예시 주석: legacy v2-text-display / v2-table-list 만
있던 것을 canonical (table / container / stats / button) + legacy alias 명시
M frontend/types/invyone-component.ts
└ componentId 예시 주석: canonical (table / container / stats / button / input / search) +
legacy alias 라우팅 호환 명시
M frontend/lib/api/screenGroup.ts
└ ScreenComponent.componentKind 주석: canonical / legacy 예시 모두 명시
M frontend/lib/fieldConfig/adapters.ts
└ fieldsToColumns docstring: v2-table-list 전용 → table-like (canonical 'table' /
legacy 'table-list' / hidden 'v2-table-list') 로 일반화.
호출자 V2TableListContainerWrapper 명시.
M notes/gbpark/2026-05-19-canonical-data-view-cleanup-followup.md
└ 본 §12 추가
```
### 12.6 다음 삭제 조건 (concrete blocker 별)
| Hard Blocker | 폐기 조건 |
|---|---|
| `componentConfig.ts` v2-table-list overridesSchema / defaultConfig | 모든 저장된 v2-table-list layout 이 canonical TableConfig 로 마이그레이션 완료 + DB 통계 v2-table-list 카운트 0 |
| `_shared/V2TableListComponent.tsx` 본체 (7216줄) | canonical TableComponent 가 다음 parity 100% 달성: v2 inline edit / category·code select / date picker fallback / image url·fallback / ResizeObserver wrapper / DataProvider·DataReceiver / FieldConfig adapter / sortable·searchable·filterable per-column / linked filter / exclude filter / GroupSum / context menu / export Excel |
| `_shared/TableListComponent.tsx` 본체 (6838줄) | canonical TableComponent 가 legacy parity 100% (FlowWidget SingleTableWithSticky / linked filter / exclude filter / 카드모드 / inline edit / DataProvider·DataReceiver) + FlowWidget 가 canonical table 으로 마이그레이션 |
| `_shared/V2TableListContainerWrapper.tsx` (FieldConfig adapter + ResizeObserver) | canonical TableComponent 가 자체적으로 ResizeObserver + FieldConfig adapter 내장 후 |
| `TableComponent.tsx` early delegation 분기 | 위 두 본체 폐기 시 함께 제거 |
| `DynamicComponentRenderer.LEGACY_TO_UNIFIED` v2-table-list/table-list alias | DB / 저장 layout 에서 옛 ID 사용 0 + 6개월 운영 안정 후 |
| `templateMigrate.LEGACY_TO_UNIFIED` v2-table-list/table-list alias | 위와 동일 |
| `getComponentConfigPanel.CONFIG_PANEL_ALIAS` v2-table-list/table-list | 위와 동일 |
| `componentTypeUtils.TABLE_LIKE_COMPONENT_TYPES` | 위 전체 폐기 후 |
| `InvDataConfigPanel.tsx` v2-table-list | old layout config 마이그레이션 완료 후 |
| `repeat-container` / `v2-repeat-container` `dataSourceType = "table-list"` enum | 별도 도메인 트랙 — `legacyTableList` 같은 명시적 enum 으로 rename |
| `V2List.tsx` 의 `type: "table-list"` 컴포넌트 객체 구성 | V2List 자체 폐기 후 |
### 12.7 기능 보존 검증 (delegation 경로 + adapter)
- **TableComponent early delegation 우선순위 검증**: `_rawType === "table-list"` / `"v2-table-list"` 일 때 early return → canonical 4경로 머지 / GroupedView / CardView / PivotView 코드에는 절대 진입하지 않음. 옛 layout 이 weak canonical TableComponent 로 떨어지는 경로 0건 확인.
- **FieldConfig adapter (fieldsToColumns)**: `V2TableListContainerWrapper` 가 import. 경로 끊김 없음.
- **DataProvider / DataReceiver**: `_shared/TableListComponent.tsx` 의 `dataProvider.component_type = "table-list"` / `_shared/V2TableListComponent.tsx` 의 `component_type = "table"` 으로 자체 등록. ScreenContext 호환.
- **selectedTable / tableName / dbTable 호환**: `getTableNameFromTableLikeComponent` helper 가 8 후보 (componentConfig.selectedTable / tableName / table_name / component_config.selectedTable / tableName / table_name / root tableName / table_name) 모두 검사. 끊김 없음.
- **sourceProvider / dataReceiver 호환**: ButtonPrimaryComponent (v2 / non-v2) 의 자동 탐색이 `isTableLikeComponentType(provider.component_type)` 로 통일 (§11). canonical / legacy / hidden 모두 자동 발견.
### 12.8 Acceptance 결과 (2026-05-20)
| 검증 항목 | 결과 |
|---|---|
| `git diff --check` | ✅ pass (출력 없음) |
| 옛 폴더 직접 import (`table-list/TableListRenderer` / `v2-table-list/TableListRenderer` / `components/table-list` / `components/v2-table-list` / `./table-list*` / `./v2-table-list*`) — `_shared/**` 제외 | ✅ `componentConfig.ts` historical 주석 **1건**만 |
| 입력 canonical 금지 토큰 (`v2-input` / `v2-select` / `V2InputRenderer` / `V2SelectRenderer`) | ✅ 0건 |
| EntityPicker / entity-picker | ✅ 0건 |
| `cd backend-spring && ./gradlew compileJava` | ✅ BUILD SUCCESSFUL |
| `npx tsc --noEmit --pretty false \| rg "lib/registry/components/table/TableComponent\|lib/registry/components/table/_shared\|lib/utils/componentTypeUtils\|components/dash/BlockRenderer\|lib/registry/DynamicComponentRenderer\|components/screen/panels/ComponentsPanel"` 신규 오류 | ✅ 0건 (이전 단계 좁은 cast 유효, resolver 견고화 / helper 화 / 주석 갱신 모두 클린) |
---
## 13. 2026-05-20 — Container 계열 cleanup 분류 + stale 정리
§12 의 table cleanup 과 동일 원칙 (canonical 새 생성 경로 보존 / shell 삭제 → alias /
shared runtime / FieldConfig·DataPort 호환 유지) 으로 container 계열 잔여를 전수 분류.
container 계열은 table 과 달리 **canonical container skeleton 이 일부 모드에서 부족**
하여 hard blocker 보존이 다수다. 본 단계는 분류 + stale 주석 2건 정리 + 보고서.
### 13.1 잔존 폴더 현황 (registry/components/)
| 폴더 | 상태 | 비고 |
|---|---|---|
| `container/` | ✅ canonical | 새 생성 경로 (containerType: tabs / section / accordion / repeater / conditional) |
| `accordion-basic/` | 🔒 보존 (hard blocker) | canonical container.containerType=accordion skeleton 부족 |
| `conditional-container/` | 🔒 보존 (hard blocker) | canonical container.containerType=conditional skeleton 부족 |
| `repeat-container/` | 🔒 보존 (hard blocker) | canonical container.containerType=repeater skeleton 부족. ComponentDefinition `hidden: true` |
| `v2-repeat-container/` | 🔒 보존 (hard blocker) | 동일. `basicV2Components` palette item |
| `v2-repeater/` | 🔒 보존 (hard blocker) | 별도 데이터 조회/선택 도메인. palette item |
| `repeat-screen-modal/` | 🔒 보존 (domain) | 도메인 특화 — 화면 모달 반복 |
| `repeater-field-group/` | 🔒 보존 (domain) | 도메인 특화 — 필드 그룹 반복 |
| `modal-repeater-table/` | 🔒 보존 (domain) | 도메인 특화 |
| `simple-repeater-table/` | 🔒 보존 (domain) | 도메인 특화 |
| `screen-split-panel/` | 🔒 보존 (domain) | 화면 임베딩 + 데이터 전달. SplitPanelContext provider. backend API (`/screen-split-panel/...`) 도 별도 |
| `split-panel-layout/` | 🔒 보존 (hard blocker) | SplitPanelContext provider 다수 사용처 (`(main)/screens/[screenId]/page.tsx` / `(pop)/pop/screens/[screenId]/page.tsx` / `InteractiveScreenViewer.tsx` / `RealtimePreview.tsx` / `RealtimePreviewDynamic.tsx` / `SplitPanelAwareWrapper.tsx` 등) |
| `split-panel-layout2/` | 🔒 보존 (hard blocker) | 새 분할 — table.displayMode='split' 와 짝 |
| `v2-split-panel-layout/` | 🔒 보존 (hard blocker) | 7000+줄 master-detail UX 구현체. alias 없음 (canonical split-mode parity 부족) |
### 13.2 canonical container 흡수 매핑 (이미 완료된 alias 라우팅)
```
DynamicComponentRenderer.LEGACY_TO_UNIFIED (line 387~394)
templateMigrate.LEGACY_TO_UNIFIED (line 46~56)
getComponentConfigPanel.CONFIG_PANEL_ALIAS (line 147~151)
┌── ✅ alias 흡수 (canonical 'container' 라우팅) ──┐
│ tabs-widget / v2-tabs-widget / tabs / v2-tabs → container (containerType=tabs)
│ section-card / v2-section-card → container (section, sectionVariant=card)
│ section-paper / v2-section-paper → container (section, sectionVariant=paper)
└──────────────────────────────────────────────────┘
┌── 🔒 보존 (concrete blocker — alias 없음 또는 별도 import 보존) ──┐
│ accordion-basic → AccordionBasicConfigPanel 직접 import
│ conditional-container → ConditionalContainerConfigPanel 직접 import
│ repeat-container → RepeatContainerConfigPanel 직접 import
│ v2-repeat-container → CONFIG_PANEL_ALIAS["v2-repeat-container"]="container" + 자체 import (skeleton 부족)
│ split-panel-layout → SplitPanelLayoutConfigPanel 직접 import
│ v2-split-panel-layout → 동일
│ split-panel-layout2 → 동일
│ screen-split-panel → 동일
└──────────────────────────────────────────────────┘
```
### 13.3 잔여 매칭 분류 요약
| 분류 | 위치 (대표) | 결정 |
|---|---|---|
| **canonical alias / runtime compat** | DynamicComponentRenderer / templateMigrate / getComponentConfigPanel alias map | ✅ 보존 (hard blocker) |
| **schema/default old layout compat** | componentConfig.ts v2-split-panel-layout / v2-repeat-container overridesSchema·defaultConfig | ✅ 보존 (hard blocker) |
| **canonical container 흡수 라우팅** | DynamicComponentRenderer line 390~394 (tabs/section alias) | ✅ 보존 (런타임 동작) |
| **canonical skeleton 부족 보존** | accordion-basic / conditional-container / repeat-container / v2-repeat-container | ✅ 보존 (hard blocker — skeleton 완성 후 폐기) |
| **SplitPanelContext + master-detail UX** | split-panel-layout / v2-split-panel-layout / split-panel-layout2 / screen-split-panel + 다수 사용처 | ✅ 보존 (hard blocker — canonical split UX 미완) |
| **domain-specific** | modal-repeater-table / simple-repeater-table / repeat-screen-modal / repeater-field-group / screen-split-panel API | ✅ 보존 (도메인 트랙 별도) |
| **InvyoneStudio drag/drop 분기** | `compType === "split-panel-layout" \|\| === "v2-split-panel-layout"` × 다수 / `=== "tabs-widget" \|\| === "v2-tabs-widget" \|\| isCanonicalTabs` × 다수 | ✅ 보존 (도메인 특화 drag/drop, 흡수 helper 시 시그니처 더 복잡 — canonical container.containerType=tabs 까지 인식하는 helper 는 component 객체 + componentConfig 둘 다 필요) |
| **ContainerComponent OR 체인** | `ContainerComponent.tsx` line 85-86 `v2-tabs-widget \|\| tabs-widget` | ✅ 보존 (canonical container 본체의 옛 ID 흡수 처리 — 본체 self-id) |
| **buttonActions split-panel/screen-split-panel 분기** | line 3294, 3478, 3845-3848 등 | ✅ 보존 (split-panel-layout 의 leftPanel.tableName 등 고유 path — table cleanup 의 hard blocker 와 동일) |
| **InteractiveScreenViewer / RealtimePreview / RealtimePreviewDynamic tabs/split 분기** | 다수 | ✅ 보존 (canonical container.containerType=tabs 와 옛 ID 의 동시 인식, 도메인 특화) |
| **screen-split-panel API client** | `lib/api/screenEmbedding.ts` line 195~249 | ✅ 보존 (backend `/screen-split-panel` endpoint — frontend 폐기 불가) |
| **stale 주석 — 이번 정리** | `registry/components/index.ts` line 106-107 / `container/types.ts` 헤더 docstring | ✏ 갱신 (2건) |
### 13.4 이번 단계 (2026-05-20) 변경 파일
```
M frontend/lib/registry/components/index.ts
└ line 106-107 의 "v2-table-list + ... 9종 흡수" / "container ... 11종 흡수"
주석을 정확한 현재 상태 (alias 라우팅 + early delegation + 보존 사유) 로 갱신.
auto-register import 자체는 그대로 (canonical TableRenderer / ContainerRenderer +
accordion-basic / split-panel-layout / split-panel-layout2 / conditional-container /
screen-split-panel / repeat-container / v2-repeat-container / v2-split-panel-layout
auto-register 보존).
M frontend/lib/registry/components/container/types.ts
└ 헤더 docstring 갱신: "흡수 대상 (11)" 단일 목록 → "흡수 완료 (alias 라우팅)" +
"보존 (skeleton 부족 또는 도메인 특화) — concrete blocker" 두 그룹으로 분리.
canonical 새 생성 경로 + alias 흡수 매핑 + hard blocker 사유 명시.
M notes/gbpark/2026-05-19-canonical-data-view-cleanup-followup.md
└ 본 §13 추가
```
### 13.5 Hard Blocker (다음 삭제 조건)
| Hard Blocker | 폐기 조건 |
|---|---|
| `accordion-basic/` 폴더 + ComponentDefinition | canonical `container.containerType=accordion` skeleton 완성 (헤더 / 패널 / 토글 / 다중 선택 / 키보드 네비게이션 / 애니메이션) + 옛 layout 마이그레이션 |
| `conditional-container/` 폴더 + ComponentDefinition | canonical `container.containerType=conditional` skeleton 완성 (조건식 평가 / sections / 동적 표시 / 자식 컴포넌트 마운트 처리) + 옛 layout 마이그레이션 |
| `repeat-container/` + `v2-repeat-container/` 폴더 | canonical `container.containerType=repeater` skeleton 완성 (데이터 lookup / 선택 / append / 인라인 add / 슬롯 컴포넌트 / `data-repeat-container` DOM 마커 / `_repeatContainerTables` save group 처리 / RepeatContainerConfig 마이그레이션) |
| `v2-repeater/` | canonical container repeater 완성 + canonical table multi-select 가 동일 UX 구현 |
| `split-panel-layout/` 폴더 + SplitPanelContext | canonical `table.displayMode=split` 의 master-detail UX 완성 (leftPanel/rightPanel / drag-drop / resize / selection sync / nested child / disableAutoDataTransfer) + 모든 사용처 마이그레이션 (`(main)/screens/[screenId]/page.tsx` / `(pop)/pop/screens/[screenId]/page.tsx` / `InteractiveScreenViewer.tsx` / `RealtimePreview.tsx` / `RealtimePreviewDynamic.tsx` / `SplitPanelAwareWrapper.tsx`) |
| `v2-split-panel-layout/` 폴더 (7000+줄) | 위와 동일 |
| `split-panel-layout2/` 폴더 | canonical split master-detail UX 완성 후 |
| `screen-split-panel/` 폴더 + backend `/screen-split-panel` API | 화면 임베딩 도메인 별도 트랙. canonical 흡수 시 backend endpoint 도 함께 정리 |
| `componentConfig.ts` v2-split-panel-layout / v2-repeat-container overridesSchema·defaultConfig | 옛 layout JSON 의 해당 type 사용 0 + 6개월 운영 안정 |
| `DynamicComponentRenderer.LEGACY_TO_UNIFIED` tabs/section alias | 옛 ID 사용 0 + 안정 후 |
| `templateMigrate.LEGACY_TO_UNIFIED` tabs/section alias | 위와 동일 |
| `getComponentConfigPanel.CONFIG_PANEL_ALIAS` tabs/section alias + accordion-basic / split-panel-layout / conditional-container / repeat-container 직접 import | 위 hard blocker 와 짝 |
| `modal-repeater-table / simple-repeater-table / repeat-screen-modal / repeater-field-group` | 별도 도메인 마이그레이션 트랙 |
### 13.6 기능 보존 검증
- **canonical container 흡수 라우팅 끊김 없음**: BlockRenderer / DynamicComponentRenderer / templateMigrate / getComponentConfigPanel 4 곳 모두에서 옛 ID → "container" 로 alias 처리됨. ContainerComponent 본체에서 옛 ID raw type 도 인식 (line 85-86).
- **canonical skeleton 부족 컴포넌트들 보존**: ComponentRegistry 에 자체 ComponentDefinition 등록 (auto-register import 유지). palette 에서는 `hiddenComponents` 로 숨김 처리되지만, 옛 저장 layout JSON 에서 직접 type 지목 시 렌더 가능.
- **SplitPanelContext 끊김 없음**: provider import 다수 사용처 (`(main)/screens/[screenId]/page.tsx` / `(pop)/pop/screens/[screenId]/page.tsx` / `InteractiveScreenViewer.tsx` / `RealtimePreview.tsx` / `RealtimePreviewDynamic.tsx` / `SplitPanelAwareWrapper.tsx`) 모두 그대로. master-detail data transfer 경로 보존.
- **schema/default 호환**: `componentConfig.ts` 의 v2-split-panel-layout / v2-repeat-container schema + defaultConfig 보존. 옛 layout JSON 검증 통과.
- **table cleanup 결과 보존**: `table-list/` / `v2-table-list/` 폴더 0건 + 직접 import 0건 + `_shared/{TableListComponent,V2TableListComponent,V2TableListContainerWrapper}` 본체 그대로.
- **FieldConfig / DataPort / sourceProvider / dataReceiver 호환 모두 유지**: `componentTypeUtils.isTableLikeComponent*` helpers + `fieldsToColumns` adapter + DataProvidable/DataReceivable 인터페이스 모두 변경 없음.
### 13.7 Acceptance 결과 (2026-05-20 container)
| 검증 항목 | 결과 |
|---|---|
| `git diff --check` | ✅ pass |
| 옛 table 폴더 직접 import (`table-list/TableListRenderer` 등) — `_shared/**` 제외 | ✅ `componentConfig.ts` historical 주석 1건만 |
| 입력 canonical 금지 토큰 | ✅ 0건 |
| EntityPicker / entity-picker | ✅ 0건 |
| `cd backend-spring && ./gradlew compileJava` | ✅ BUILD SUCCESSFUL |
| `npx tsc --noEmit \| rg "container\|tabs\|section\|accordion\|conditional\|repeat-container\|split-panel\|screen-split-panel\|DynamicComponentRenderer\|templateMigrate\|ComponentsPanel"` 신규 오류 | ✅ 0건 (주석 갱신 2건 모두 클린) |
---
## 14. 2026-05-20 — Final Audit / Close-out
input → stats → table → container → chart → card-list → grouped-table 까지 모든 canonical
data-view cleanup 단계가 완료되었다. 본 §14 는 전체 상태 검증 + 누적 잔여 분류 +
다음 삭제 조건의 최종 보정. 코드 수정은 **0건** (검증/문서 중심 단계).
### 14.1 Canonical 6종 — 새 생성 경로 상태
| Canonical ID | 폴더 | 본체 | 상태 |
|---|---|---|---|
| `stats` | `lib/registry/components/stats/` | StatsComponent | ✅ canonical 새 생성 경로 |
| `table` | `lib/registry/components/table/` | TableComponent + `_shared/{TableListComponent,V2TableListComponent,V2TableListContainerWrapper}` | ✅ canonical + early delegation |
| `container` | `lib/registry/components/container/` | ContainerComponent | ✅ canonical (tabs / section / accordion / repeater / conditional. 단 accordion/repeater/conditional skeleton 부족) |
| `chart` | `lib/registry/components/chart/` | ChartRenderer (recharts: bar / horizontalBar / line / donut) | ✅ canonical |
| `card-list` | `lib/registry/components/card-list/` | CardListRenderer | ✅ canonical |
| `grouped-table` | `lib/registry/components/grouped-table/` | GroupedTableRenderer | ✅ canonical |
### 14.2 삭제 완료 폴더 (11개)
```
D lib/registry/components/aggregation-widget/ (stats 흡수)
D lib/registry/components/v2-aggregation-widget/ (stats 흡수)
D lib/registry/components/v2-status-count/ (stats 흡수)
D lib/registry/components/tabs/ (container 흡수, alias 라우팅)
D lib/registry/components/v2-tabs-widget/ (container 흡수, alias 라우팅)
D lib/registry/components/section-card/ (container 흡수, alias 라우팅)
D lib/registry/components/v2-section-card/ (container 흡수, alias 라우팅)
D lib/registry/components/section-paper/ (container 흡수, alias 라우팅)
D lib/registry/components/v2-section-paper/ (container 흡수, alias 라우팅)
D lib/registry/components/table-list/ (table 흡수 + _shared/TableListComponent 로 이동)
D lib/registry/components/v2-table-list/ (table 흡수 + _shared/V2TableListContainerWrapper 로 이동)
```
부가 삭제:
```
D V2AggregationWidgetConfigPanel.tsx (1085줄)
D V2StatusCountConfigPanel.tsx (679줄)
```
### 14.3 Alias / Delegation 라우팅 (모두 작동 중)
```
┌─── BlockRenderer ───┐ ┌─── DynamicComponentRenderer ───┐
│ isTableLikeComponent│ │ LEGACY_TO_UNIFIED │
│ → canonical "table" │ v2-table-list → table │
│ ★ alias 안전장치 유지 │ table-list → table │
└─────────────────────┘ │ v2-aggregation-widget → stats │
│ aggregation-widget → stats │
┌─── templateMigrate.LEGACY_TO_UNIFIED ───┐│ v2-status-count → stats │
│ v2-table-list / table-list → table ││ v2-tabs-widget → container │
│ v2-aggregation-widget → stats ││ tabs-widget / tabs → container│
│ v2-tabs-widget / tabs-widget → container││ v2-section-card → container │
│ v2-section-card/paper → container ││ v2-section-paper → container │
│ section-card / section-paper → container││ section-card / paper → cont │
│ v2-repeat-container → container ││ v2-repeat-container → cont │
│ accordion-basic → container ││ accordion-basic → container │
└──────────────────────────────────────────┘└────────────────────────────────┘
┌─── getComponentConfigPanel ───────────────────────────────────────────┐
│ CONFIG_PANEL_ALIAS │
│ v2-table-list / table-list → table │
│ v2-tabs-widget / tabs-widget → container │
│ v2-section-card / v2-section-paper / section-card / paper → container│
│ v2-repeat-container → container │
│ v2-aggregation-widget / v2-status-count / aggregation-widget → stats│
│ │
│ CONFIG_PANEL_MAP (직접 import — alias 없음 hard blocker): │
│ accordion-basic / split-panel-layout / v2-split-panel-layout / │
│ split-panel-layout2 / screen-split-panel / conditional-container / │
│ repeat-container / v2-repeat-container │
└───────────────────────────────────────────────────────────────────────┘
┌─── TableComponent.tsx (canonical table 본체) ───────────────────────┐
│ _resolveRawComponentType(component, props): │
│ - MEANINGFUL (table / table-list / v2-table-list / data-table / │
│ datatable) → 즉시 채택 │
│ - GENERIC (component / widget / container / group / row / column / │
│ area / flow / tabs) → skip │
│ - 그 외 → fallback │
│ │
│ rawType === "table-list" → <LegacyTableListWrapper> │
│ rawType === "v2-table-list" → <V2TableListContainerWrapper> │
│ default → 본체 4경로 머지 + Grouped/Card/Pivot │
└─────────────────────────────────────────────────────────────────────┘
```
### 14.4 누적 잔여 분류 (전체 735건 매칭)
| 분류 | 결정 |
|---|---|
| canonical alias / runtime compat (DynamicComponentRenderer / templateMigrate / getComponentConfigPanel) | ✅ 보존 (hard blocker — 옛 ID 사용 0 + 안정 후 폐기) |
| schema/default old layout compat (componentConfig.ts: v2-table-list / v2-split-panel-layout / v2-repeat-container schema·default) | ✅ 보존 (hard blocker) |
| canonical container 흡수 alias (tabs / section) | ✅ 작동 중 |
| canonical skeleton 부족 (accordion / conditional / repeat 계열) | 🔒 hard blocker (skeleton 완성 + 마이그레이션 후 폐기) |
| SplitPanelContext + master-detail UX (split-panel / screen-split-panel) | 🔒 hard blocker (canonical split UX 미완) |
| domain-specific (modal-repeater / simple-repeater / repeat-screen-modal / repeater-field-group) | 🔒 hard blocker (도메인 트랙 별도) |
| InvyoneStudio drag/drop / buttonActions split-panel / InteractiveScreenViewer / RealtimePreview/Dynamic tabs/split 분기 | ✅ 보존 (도메인 특화 — canonical 흡수 시 함께 정리) |
| ContainerComponent self-id 인식 (v2-tabs-widget / tabs-widget) | ✅ 보존 (본체 옛 ID 흡수 처리) |
| `_shared/` 본체 self-id (`component_type: "table-list"` / `componentType="v2-table-list"`) | ✅ 보존 (DataProvider/Receiver 컨트랙트) |
| repeat-container `dataSourceType="table-list"` enum | ✅ 보존 (도메인 다름 — data source mode 값) |
| component-events publisher/subscriber 주석 (3건) | ✅ 보존 (메타) |
| helper / 분류 매핑 (componentTypeUtils / MultilangSettingsModal / responsiveDefaults / ScreenNode / RealtimePreviewDynamic / TabsWidget / InvyoneStudio summary count) | ✅ 보존 (canonical-aware entry) |
| docs / README 예시 | ✅ 보존 (코드 영향 0) |
| stale 주석 — 누적 정리 완료 | ✏ 갱신 완료 (§10 / §11 / §12 / §13 누적 8건) |
### 14.5 남은 Hard Blocker (concrete blockers)
#### 14.5.1 Schema / Default (componentConfig.ts)
- `v2-table-list` overridesSchema + defaultConfig
- `v2-split-panel-layout` overridesSchema + defaultConfig
- `v2-repeat-container` overridesSchema + defaultConfig
#### 14.5.2 Canonical Skeleton 부족
- `accordion-basic/` — canonical `container.containerType=accordion` skeleton 부족
- `conditional-container/` — canonical `container.containerType=conditional` skeleton 부족
- `repeat-container/` + `v2-repeat-container/` + `v2-repeater/` — canonical `container.containerType=repeater` 데이터 lookup / 슬롯 / save group 부족
#### 14.5.3 SplitPanel + Master-detail UX
- `split-panel-layout/` (SplitPanelContext provider 다수 사용처)
- `v2-split-panel-layout/` (7000+줄 master-detail UX 본체)
- `split-panel-layout2/` (새 분할)
- `screen-split-panel/` (화면 임베딩 + backend `/screen-split-panel` API)
#### 14.5.4 Table _shared 본체 (canonical TableComponent parity 부족)
- `_shared/TableListComponent.tsx` (6838줄) — legacy FlowWidget SingleTableWithSticky / linked filter / exclude filter
- `_shared/V2TableListComponent.tsx` (7216줄) — v2 inline edit / category·code select / image url·fallback / GroupSum / DataProvider/Receiver
- `_shared/V2TableListContainerWrapper.tsx` — ResizeObserver + FieldConfig adapter
- `table/TableComponent.tsx` early delegation 분기
#### 14.5.5 Alias 매핑
- `DynamicComponentRenderer.LEGACY_TO_UNIFIED` (table / stats / container 그룹)
- `templateMigrate.LEGACY_TO_UNIFIED`
- `getComponentConfigPanel.CONFIG_PANEL_ALIAS`
- `componentTypeUtils.TABLE_LIKE_COMPONENT_TYPES` Set
#### 14.5.6 Domain / Wrapper
- `InvDataConfigPanel.tsx` v2-table-list (old config hard blocker)
- `V2List.tsx` `type: "table-list"` (shared TableListComponent props 호환)
- `repeat-container` / `v2-repeat-container` `dataSourceType = "table-list"` enum (도메인 다름)
- `modal-repeater-table` / `simple-repeater-table` / `repeat-screen-modal` / `repeater-field-group` (도메인 특화)
- `lib/api/screenEmbedding.ts` `/screen-split-panel` API client (backend endpoint 짝)
### 14.6 다음 삭제 조건 (요약)
| Hard Blocker 그룹 | 폐기 조건 |
|---|---|
| `_shared/TableListComponent.tsx` (legacy) | canonical TableComponent legacy parity 100% (FlowWidget SingleTableWithSticky / linked filter / exclude filter / 카드모드 / inline edit / DataProvider·Receiver) + FlowWidget 마이그레이션 |
| `_shared/V2TableListComponent.tsx` (v2) | canonical TableComponent v2 parity 100% (v2 inline edit / category·code select / date picker fallback / image url·fallback / ResizeObserver / DataProvider·Receiver / FieldConfig adapter / sortable·searchable·filterable per-column / linked filter / exclude filter / GroupSum / context menu / export Excel) |
| `V2TableListContainerWrapper.tsx` | canonical TableComponent 가 자체적으로 ResizeObserver + FieldConfig adapter 내장 후 |
| `TableComponent.tsx` early delegation | 위 두 본체 폐기 시 함께 |
| `componentConfig.ts` v2-table-list / v2-split-panel-layout / v2-repeat-container schema | 옛 layout JSON 의 해당 type 사용 0 + 6개월 운영 안정 |
| `accordion-basic` | canonical container accordion skeleton 완성 + 마이그레이션 |
| `conditional-container` | canonical container conditional skeleton 완성 + 마이그레이션 |
| `repeat-container` / `v2-repeat-container` / `v2-repeater` | canonical container repeater 완성 (데이터 lookup / 슬롯 / append / 인라인 add / DOM marker / save group) |
| `split-panel-layout` 계열 + SplitPanelContext | canonical `table.displayMode=split` master-detail UX 완성 + 다수 사용처 마이그레이션 |
| `screen-split-panel` + backend API | 화면 임베딩 도메인 별도 트랙. canonical 흡수 시 endpoint 도 정리 |
| `LEGACY_TO_UNIFIED` / `CONFIG_PANEL_ALIAS` / `TABLE_LIKE_COMPONENT_TYPES` 매핑 | 위 hard blocker 전체 폐기 후 단순화 |
| `InvDataConfigPanel.tsx` v2-table-list | old layout config 마이그레이션 완료 후 |
| `V2List.tsx` | V2List 자체 폐기 후 (canonical 흡수 시점) |
| `repeat-container` `dataSourceType="table-list"` enum | 도메인 분리 트랙 — `legacyTableList` rename |
| domain-specific (modal-repeater 등) | 각 도메인 트랙 |
### 14.7 Final Acceptance 결과 (2026-05-20)
| 검증 항목 | 결과 |
|---|---|
| `git diff --check` | ✅ pass (출력 없음) |
| 입력 canonical 금지 토큰 (`v2-input` / `v2-select` / `V2InputRenderer` / `V2SelectRenderer`) | ✅ 0건 |
| EntityPicker / entity-picker | ✅ 0건 |
| table direct import (`table-list/TableListRenderer` / `v2-table-list/TableListRenderer` / `components/table-list` / `components/v2-table-list` / `./table-list*` / `./v2-table-list*`) — `_shared/**` 제외 | ✅ `componentConfig.ts` historical 주석 1건만 |
| stats deleted import (`AggregationWidgetRenderer` / `StatusCountRenderer` / `V2AggregationWidgetConfigPanel` / `V2StatusCountConfigPanel` / `components/aggregation-widget` / `components/v2-aggregation-widget` / `components/v2-status-count`) | ✅ 0건 |
| tabs/section deleted import (`TabsRenderer` / `tabs-component` / `SectionCardRenderer` / `SectionPaperRenderer` / `components/tabs` / `components/v2-tabs-widget` / `components/section-card` / `components/section-paper` / `components/v2-section-card` / `components/v2-section-paper`) | ✅ 0건 |
| 11개 삭제 폴더 존재 X (aggregation-widget / v2-aggregation-widget / v2-status-count / tabs / v2-tabs-widget / section-card / v2-section-card / section-paper / v2-section-paper / table-list / v2-table-list) | ✅ ALL DELETED |
| `cd backend-spring && ./gradlew compileJava` | ✅ BUILD SUCCESSFUL |
| `npx tsc --noEmit --pretty false \| rg <변경 파일 패턴>` — **변경 파일 기준 신규 오류** | ✅ 0건 |
### 14.8 Known Residual Risks
1. **기존 전체 `tsc` 오류는 여전히 존재할 수 있음.**
본 cleanup 은 canonical data-view 라우팅 / shared runtime 이동 / helper 도입 / 좁은
타입 cast 만 수행. 본 cleanup 범위 외의 snake_case ↔ camelCase 타입 불일치,
union 타입 누락, ComponentData 시그니처 불일치 등 기존 오류는 정리하지 않음.
별도 typecheck cleanup 트랙에서 처리해야 함.
2. **넓은 grep 패턴은 기존 split-panel / InvyoneStudio 오류를 잡을 수 있음.**
예시: `rg "split-panel\|InvyoneStudio"` 또는 `rg "container"` 같은 광역 패턴은
본 cleanup 무관한 InvyoneStudio drag/drop 분기의 기존 union 오류, SplitPanel
master-detail 의 기존 snake_case 불일치 등을 함께 매칭. 본 §14.7 acceptance 의
`tsc` 검증 패턴은 **변경 파일 list 기준** 으로 좁혀 사용한다.
3. **acceptance 의 신규 오류 0건 기준 = 변경 파일 기준.**
본 cleanup 14단계 누적 변경 파일 list:
```
lib/registry/components/table/TableComponent.tsx
lib/registry/components/table/_shared/{TableListComponent,V2TableListComponent,V2TableListContainerWrapper,tableListConfigTypes}.tsx
lib/utils/componentTypeUtils.ts
lib/utils/buttonActions.ts
lib/utils/templateMigrate.ts
lib/utils/getComponentConfigPanel.tsx
lib/utils/responsiveDefaults.ts
lib/utils/layoutV2Converter.ts (touched in earlier phases)
lib/registry/DynamicComponentRenderer.tsx
lib/registry/components/index.ts
lib/registry/components/container/types.ts
lib/registry/components/v2-button-primary/ButtonPrimaryComponent.tsx
lib/registry/components/button-primary/ButtonPrimaryComponent.tsx
lib/registry/components/universal-form-modal/UniversalFormModalConfigPanel.tsx
lib/schemas/componentConfig.ts
lib/api/screenGroup.ts
lib/fieldConfig/adapters.ts
app/(main)/screens/[screenId]/page.tsx
app/(pop)/pop/screens/[screenId]/page.tsx (touched in earlier phases)
components/dash/BlockRenderer.tsx
components/screen/RealtimePreviewDynamic.tsx
components/screen/ScreenNode.tsx
components/screen/widgets/TabsWidget.tsx
components/screen/modals/MultilangSettingsModal.tsx
components/screen/panels/ComponentsPanel.tsx
components/screen/config-panels/button/DataTab.tsx
components/screen/config-panels/button-config/ActionTab.tsx
components/v2/config-panels/InvLegacyButtonConfigPanel.tsx
components/v2/V2List.tsx
types/screen-management.ts
types/invyone-component.ts
backend-spring/src/main/java/com/erp/service/ScreenGroupService.java
backend-spring/src/main/resources/mapper/screenGroup.xml
```
이 list 의 변경 line 들에서 신규 오류 0건. 같은 파일의 다른 line 에 있는 기존
오류 (예: V2TableListComponent 의 cp 이전부터 있던 union 불일치, ActionTab 의
snake_case 불일치) 는 본 cleanup 범위 외.
4. **Backend 컴파일 + 런타임 영향 0건.**
본 cleanup 의 backend 변경은 `ScreenGroupService.countTableLikeWidgets` helper
추가 + `screenGroup.xml` 의 `componentType IN ('table', 'table-list', 'v2-table-list')` 확장만.
`./gradlew compileJava BUILD SUCCESSFUL`. 기존 backend 컴파일 정책에 영향 없음.
5. **Hard Blocker 폐기는 운영 안정성 보장 후.**
§14.6 의 다음 삭제 조건은 모두 "옛 layout JSON 사용 0 + N개월 운영 안정"
또는 "canonical skeleton parity 100%" 같은 boolean 조건. 운영 환경 DB 통계 +
회귀 테스트가 선결.
### 14.9 본 §14 의 코드 수정
**0건.** 본 단계는 검증 + 분류 + 보고서 보정 only. 기능 파일 / 주석 / 문서 / 코드
모두 미변경.
```
M notes/gbpark/2026-05-19-canonical-data-view-cleanup-followup.md (본 §14 추가)
```