diff --git a/frontend/components/screen/panels/ComponentsPanel.tsx b/frontend/components/screen/panels/ComponentsPanel.tsx index 9d5cdf95..d239f577 100644 --- a/frontend/components/screen/panels/ComponentsPanel.tsx +++ b/frontend/components/screen/panels/ComponentsPanel.tsx @@ -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; }, []); diff --git a/frontend/lib/api/screenGroup.ts b/frontend/lib/api/screenGroup.ts index 59402fd2..666a42d9 100644 --- a/frontend/lib/api/screenGroup.ts +++ b/frontend/lib/api/screenGroup.ts @@ -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; // 바인딩된 필드명 (컬럼명) diff --git a/frontend/lib/fieldConfig/adapters.ts b/frontend/lib/fieldConfig/adapters.ts index 1a308e8f..a586eac0 100644 --- a/frontend/lib/fieldConfig/adapters.ts +++ b/frontend/lib/fieldConfig/adapters.ts @@ -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[], diff --git a/frontend/lib/registry/DynamicComponentRenderer.tsx b/frontend/lib/registry/DynamicComponentRenderer.tsx index 0a900e10..7ac916c5 100644 --- a/frontend/lib/registry/DynamicComponentRenderer.tsx +++ b/frontend/lib/registry/DynamicComponentRenderer.tsx @@ -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> = {}; @@ -932,10 +933,9 @@ export const DynamicComponentRenderer: React.FC = 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 = ; } diff --git a/frontend/lib/registry/components/container/types.ts b/frontend/lib/registry/components/container/types.ts index f6ef2570..7bfb8e7f 100644 --- a/frontend/lib/registry/components/container/types.ts +++ b/frontend/lib/registry/components/container/types.ts @@ -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 = diff --git a/frontend/lib/registry/components/index.ts b/frontend/lib/registry/components/index.ts index 046e4088..9d6482ed 100644 --- a/frontend/lib/registry/components/index.ts +++ b/frontend/lib/registry/components/index.ts @@ -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"; diff --git a/frontend/lib/registry/components/table/TableComponent.tsx b/frontend/lib/registry/components/table/TableComponent.tsx index 60a721a0..b2d69801 100644 --- a/frontend/lib/registry/components/table/TableComponent.tsx +++ b/frontend/lib/registry/components/table/TableComponent.tsx @@ -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([ + "table", + "table-list", + "v2-table-list", + "data-table", + "datatable", +]); +const _GENERIC_COMPONENT_TYPES = new Set([ + "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 = { compact: "28px", @@ -38,7 +106,50 @@ export const TableComponent: React.FC = ({ 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 ( + + ); + } + if (_rawType === "v2-table-list") { + // old v2-table-list layout — V2TableListContainerWrapper 가 + // FieldConfig → columns 어댑터 + ResizeObserver + DataProvider/DataReceiver 포함. + return ( + + ); + } + + // ─── 4경로 머지 (canonical 'table' 본체, 기존 코드 그대로) ─── const fromProps: Partial = {}; const p = props as any; if (typeof p.displayMode === "string" && (VALID_MODES as string[]).includes(p.displayMode)) diff --git a/frontend/lib/registry/components/table/_shared/TableListComponent.tsx b/frontend/lib/registry/components/table/_shared/TableListComponent.tsx index e987bf39..cd662e27 100644 --- a/frontend/lib/registry/components/table/_shared/TableListComponent.tsx +++ b/frontend/lib/registry/components/table/_shared/TableListComponent.tsx @@ -594,11 +594,11 @@ export const TableListComponent: React.FC = ({ const [displayColumns, setDisplayColumns] = useState([]); const [joinColumnMapping, setJoinColumnMapping] = useState>({}); const [columnMeta, setColumnMeta] = useState< - Record + Record >({}); // 🆕 엔티티 조인 테이블의 컬럼 메타데이터 (테이블명.컬럼명 → inputType) const [joinedColumnMeta, setJoinedColumnMeta] = useState< - Record + Record >({}); const [categoryMappings, setCategoryMappings] = useState< Record> @@ -1170,7 +1170,7 @@ export const TableListComponent: React.FC = ({ const cached = tableColumnCache.get(cacheKey); if (cached && Date.now() - cached.timestamp < TABLE_CACHE_TTL) { const labels: Record = {}; - const meta: Record = {}; + const meta: Record = {}; // 캐시된 inputTypes 맵 생성 const inputTypeMap: Record = {}; @@ -1210,7 +1210,7 @@ export const TableListComponent: React.FC = ({ }); const labels: Record = {}; - const meta: Record = {}; + const meta: Record = {}; 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 = ({ } // 조인된 테이블별로 inputType 정보 가져오기 - const newJoinedColumnMeta: Record = {}; + const newJoinedColumnMeta: Record = {}; for (const [joinedTable, columns] of Object.entries(joinedTableColumns)) { try { diff --git a/frontend/lib/registry/components/table/_shared/V2TableListComponent.tsx b/frontend/lib/registry/components/table/_shared/V2TableListComponent.tsx index 82289330..841aec5b 100644 --- a/frontend/lib/registry/components/table/_shared/V2TableListComponent.tsx +++ b/frontend/lib/registry/components/table/_shared/V2TableListComponent.tsx @@ -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 = ({ 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 = ({ 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 = ({ // 프론트엔드 카테고리 매핑으로 추가 라벨 변환 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 = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ currentPage: currentPage, pageSize: localPageSize, totalItems: totalItems, - }, + } as any, ); } } else { @@ -2498,7 +2502,7 @@ export const TableListComponent: React.FC = ({ // 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 = ({ 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 = ({ onSelectedRowsChange( Array.from(selectedRows), selectedRowsData, - sortColumn, + sortColumn ?? undefined, sortDirection, currentColumnOrder, reorderedData, @@ -4977,13 +4981,15 @@ export const TableListComponent: React.FC = ({ }, [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(); // 조인 컬럼인지 확인하고 실제 키 추론 @@ -6111,7 +6117,7 @@ export const TableListComponent: React.FC = ({ return ( (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 = ({ 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 diff --git a/frontend/lib/registry/components/table/_shared/tableListConfigTypes.ts b/frontend/lib/registry/components/table/_shared/tableListConfigTypes.ts index 9aadc25b..50cf5ea9 100644 --- a/frontend/lib/registry/components/table/_shared/tableListConfigTypes.ts +++ b/frontend/lib/registry/components/table/_shared/tableListConfigTypes.ts @@ -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; // 원본 테이블 diff --git a/frontend/lib/utils/componentTypeUtils.ts b/frontend/lib/utils/componentTypeUtils.ts index 79826355..117686bc 100644 --- a/frontend/lib/utils/componentTypeUtils.ts +++ b/frontend/lib/utils/componentTypeUtils.ts @@ -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")) diff --git a/frontend/types/invyone-component.ts b/frontend/types/invyone-component.ts index 54272641..f8b605fe 100644 --- a/frontend/types/invyone-component.ts +++ b/frontend/types/invyone-component.ts @@ -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; diff --git a/frontend/types/screen-management.ts b/frontend/types/screen-management.ts index 495618e2..ac373462 100644 --- a/frontend/types/screen-management.ts +++ b/frontend/types/screen-management.ts @@ -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; // 컴포넌트 크기 diff --git a/notes/gbpark/2026-05-19-canonical-data-view-cleanup-followup.md b/notes/gbpark/2026-05-19-canonical-data-view-cleanup-followup.md index f4fe24c5..d646a318 100644 --- a/notes/gbpark/2026-05-19-canonical-data-view-cleanup-followup.md +++ b/notes/gbpark/2026-05-19-canonical-data-view-cleanup-followup.md @@ -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" → + ├─ rawType === "v2-table-list" → + └─ 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" → │ +│ rawType === "v2-table-list" → │ +│ 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 추가) +```