--- description: 화면 컴포넌트 개발 시 필수 가이드 - V2 컴포넌트, 엔티티 조인, 폼 데이터, 다국어 지원 alwaysApply: false --- # 화면 컴포넌트 개발 가이드 새로운 화면 컴포넌트를 개발할 때 반드시 따라야 하는 핵심 원칙과 패턴을 설명합니다. 이 가이드는 컴포넌트가 시스템의 핵심 기능(엔티티 조인, 다국어, 폼 데이터 관리 등)과 올바르게 통합되도록 하는 방법을 설명합니다. --- ## 목차 0. [V2 컴포넌트 규칙 (최우선)](#0-v2-컴포넌트-규칙-최우선) 1. [컴포넌트별 테이블 설정 (핵심 원칙)](#1-컴포넌트별-테이블-설정-핵심-원칙) 2. [엔티티 조인 컬럼 활용 (필수)](#2-엔티티-조인-컬럼-활용-필수) 3. [폼 데이터 관리](#3-폼-데이터-관리) 4. [다국어 지원](#4-다국어-지원) 5. [컬럼 설정 패널 구현](#5-컬럼-설정-패널-구현) 6. [체크리스트](#6-체크리스트) --- ## 0. V2 컴포넌트 규칙 (최우선) ### 핵심 원칙 **화면관리 시스템에서는 반드시 V2 컴포넌트만 사용하고 수정합니다.** 원본 컴포넌트(v2 접두사 없는 것)는 더 이상 사용하지 않으며, 모든 수정/개발은 V2 폴더에서 진행합니다. ### V2 컴포넌트 목록 (18개) | 컴포넌트 ID | 이름 | 경로 | |------------|------|------| | `v2-button-primary` | 기본 버튼 | `v2-button-primary/` | | `v2-text-display` | 텍스트 표시 | `v2-text-display/` | | `v2-divider-line` | 구분선 | `v2-divider-line/` | | `v2-table-list` | 테이블 리스트 | `v2-table-list/` | | `v2-card-display` | 카드 디스플레이 | `v2-card-display/` | | `v2-split-panel-layout` | 분할 패널 | `v2-split-panel-layout/` | | `v2-numbering-rule` | 채번 규칙 | `v2-numbering-rule/` | | `v2-table-search-widget` | 검색 필터 | `v2-table-search-widget/` | | `v2-repeat-screen-modal` | 반복 화면 모달 | `v2-repeat-screen-modal/` | | `v2-section-paper` | 섹션 페이퍼 | `v2-section-paper/` | | `v2-section-card` | 섹션 카드 | `v2-section-card/` | | `v2-tabs-widget` | 탭 위젯 | `v2-tabs-widget/` | | `v2-location-swap-selector` | 출발지/도착지 선택 | `v2-location-swap-selector/` | | `v2-rack-structure` | 렉 구조 | `v2-rack-structure/` | | `v2-unified-repeater` | 통합 리피터 | `v2-unified-repeater/` | | `v2-pivot-grid` | 피벗 그리드 | `v2-pivot-grid/` | | `v2-aggregation-widget` | 집계 위젯 | `v2-aggregation-widget/` | | `v2-repeat-container` | 리피터 컨테이너 | `v2-repeat-container/` | ### 파일 경로 ``` frontend/lib/registry/components/ ├── v2-button-primary/ ← V2 컴포넌트 (수정 대상) ├── v2-table-list/ ← V2 컴포넌트 (수정 대상) ├── v2-split-panel-layout/ ← V2 컴포넌트 (수정 대상) ├── ... ├── button-primary/ ← 원본 (수정 금지) ├── table-list/ ← 원본 (수정 금지) ├── split-panel-layout/ ← 원본 (수정 금지) └── ... ``` ### 수정/개발 시 규칙 1. **버그 수정**: V2 폴더의 파일만 수정 2. **기능 추가**: V2 폴더에만 추가 3. **새 컴포넌트 생성**: `v2-` 접두사로 폴더 생성, ID도 `v2-` 접두사 사용 4. **원본 폴더는 절대 수정하지 않음** ### 컴포넌트 등록 V2 컴포넌트는 `frontend/lib/registry/components/index.ts`에서 등록됩니다: ```typescript // V2 컴포넌트들 (화면관리 전용) import "./v2-unified-repeater/UnifiedRepeaterRenderer"; import "./v2-button-primary/ButtonPrimaryRenderer"; import "./v2-split-panel-layout/SplitPanelLayoutRenderer"; // ... 기타 v2 컴포넌트들 ``` ### Definition 네이밍 규칙 V2 컴포넌트의 Definition은 `V2` 접두사를 사용합니다: ```typescript // index.ts export const V2TableListDefinition = createComponentDefinition({ id: "v2-table-list", name: "테이블 리스트", // ... }); // Renderer.tsx import { V2TableListDefinition } from "./index"; export class TableListRenderer extends AutoRegisteringComponentRenderer { static componentDefinition = V2TableListDefinition; // ... } ``` --- ## 1. 컴포넌트별 테이블 설정 (핵심 원칙) ### 핵심 원칙 **하나의 화면에서 여러 테이블을 다룰 수 있습니다.** 화면 생성 시 "메인 테이블"을 필수로 지정하지 않으며, 컴포넌트별로 사용할 테이블을 지정할 수 있습니다. ### 왜 필요한가? 일반적인 ERP 화면에서는 여러 테이블이 동시에 필요합니다: | 예시: 입고 화면 | 테이블 | 용도 | | --------------- | ----------------------- | ------------------------------- | | 메인 폼 | `receiving_mng` | 입고 마스터 정보 입력/저장 | | 조회 리스트 | `purchase_order_detail` | 발주 상세 목록 조회 (읽기 전용) | | 입력 리피터 | `receiving_detail` | 입고 상세 항목 입력/저장 | ### 컴포넌트 설정 패턴 #### 1. 테이블 리스트 (조회용) ```typescript interface TableListConfig { // 조회용 테이블 (화면 메인 테이블과 다를 수 있음) customTableName?: string; // 사용할 테이블명 useCustomTable?: boolean; // true: customTableName 사용 isReadOnly?: boolean; // true: 조회만, 저장 안 함 } ``` #### 2. 리피터 (입력/저장용) ```typescript interface UnifiedRepeaterConfig { // 저장 대상 테이블 (화면 메인 테이블과 다를 수 있음) mainTableName?: string; // 저장할 테이블명 useCustomTable?: boolean; // true: mainTableName 사용 // FK 자동 연결 (마스터-디테일 관계) foreignKeyColumn?: string; // 이 테이블의 FK 컬럼 (예: receiving_id) foreignKeySourceColumn?: string; // 마스터 테이블의 PK 컬럼 (예: id) } ``` ### 조회 테이블 설정 UI 표준 (테이블 리스트) 테이블 리스트 등 조회용 컴포넌트의 ConfigPanel에서: ```tsx // 현재 선택된 테이블 카드 형태로 표시
{config.customTableName || screenTableName || "테이블 미선택"}
{config.useCustomTable ? "커스텀 테이블" : "화면 기본 테이블"}
// 테이블 선택 Combobox (기본/전체 그룹) {/* 그룹 1: 화면 기본 테이블 */} {screenTableName && ( { handleChange("useCustomTable", false); handleChange("customTableName", undefined); handleChange("selectedTable", screenTableName); handleChange("columns", []); // 테이블 변경 시 컬럼 초기화 }} > {screenTableName} )} {/* 그룹 2: 전체 테이블 */} {availableTables .filter((table) => table.tableName !== screenTableName) .map((table) => ( { handleChange("useCustomTable", true); handleChange("customTableName", table.tableName); handleChange("selectedTable", table.tableName); handleChange("columns", []); // 테이블 변경 시 컬럼 초기화 }} > {table.displayName || table.tableName} ))} // 읽기전용 설정
handleChange("isReadOnly", checked)} />
``` ### 저장 테이블 설정 UI 표준 (리피터) 리피터 등 저장 기능이 있는 컴포넌트의 ConfigPanel에서: ```tsx // 1. 테이블 선택 Combobox {/* 그룹 1: 현재 화면 테이블 (기본) */} {currentTableName} {/* 그룹 2: 연관 테이블 (FK 자동 설정) */} {relatedTables.length > 0 && ( {relatedTables.map((table) => ( {table.tableName} FK: {table.foreignKeyColumn} ))} )} {/* 그룹 3: 전체 테이블 */} {allTables.map((table) => ( {table.displayName || table.tableName} ))} ; // 2. 연관 테이블 선택 시 FK/PK 자동 설정 const handleSaveTableSelect = (tableName: string) => { const relation = relatedTables.find((r) => r.tableName === tableName); if (relation) { // 엔티티 관계에서 자동으로 FK/PK 가져옴 updateConfig({ useCustomTable: true, mainTableName: tableName, foreignKeyColumn: relation.foreignKeyColumn, foreignKeySourceColumn: relation.referenceColumn, }); } else { // 연관 테이블이 아니면 수동 입력 필요 updateConfig({ useCustomTable: true, mainTableName: tableName, foreignKeyColumn: undefined, foreignKeySourceColumn: undefined, }); } }; ``` ### 연관 테이블 조회 API 엔티티 관계에서 현재 테이블을 참조하는 테이블 목록을 조회합니다: ```typescript // API 호출 const response = await apiClient.get( `/api/table-management/columns/${currentTableName}/referenced-by` ); // 응답 { success: true, data: [ { tableName: "receiving_detail", // 참조하는 테이블 columnName: "receiving_id", // FK 컬럼 referenceColumn: "id", // 참조되는 컬럼 (PK) }, // ... ] } ``` ### FK 자동 연결 동작 마스터 저장 후 디테일 저장 시 FK가 자동으로 설정됩니다: ```typescript // 1. 마스터 저장 이벤트 발생 (ButtonConfigPanel에서) window.dispatchEvent( new CustomEvent("repeaterSave", { detail: { masterRecordId: savedId, // 마스터 테이블에 저장된 ID tableName: "receiving_mng", mainFormData: formData, }, }) ); // 2. 리피터에서 이벤트 수신 및 FK 설정 useEffect(() => { const handleSaveEvent = (event: CustomEvent) => { const { masterRecordId } = event.detail; if (config.foreignKeyColumn && masterRecordId) { // 모든 행에 FK 값 자동 설정 const updatedRows = rows.map((row) => ({ ...row, [config.foreignKeyColumn]: masterRecordId, })); // 저장 실행 saveRows(updatedRows); } }; window.addEventListener("repeaterSave", handleSaveEvent); return () => window.removeEventListener("repeaterSave", handleSaveEvent); }, [config.foreignKeyColumn, rows]); ``` ### 저장 테이블 변경 시 컬럼 자동 로드 저장 테이블이 변경되면 해당 테이블의 컬럼이 자동으로 로드됩니다: ```typescript // 저장 테이블 또는 화면 테이블 기준으로 컬럼 로드 const targetTableForColumns = config.useCustomTable && config.mainTableName ? config.mainTableName : currentTableName; useEffect(() => { const loadColumns = async () => { if (!targetTableForColumns) return; const columnData = await tableTypeApi.getColumns(targetTableForColumns); setCurrentTableColumns(columnData); }; loadColumns(); }, [targetTableForColumns]); ``` ### 요약 | 상황 | 처리 방법 | | ------------------------------------- | ----------------------------------- | | 화면과 같은 테이블에 저장 | `useCustomTable: false` (기본값) | | 다른 테이블에 저장 + 엔티티 관계 있음 | 연관 테이블 선택 → FK/PK 자동 설정 | | 다른 테이블에 저장 + 엔티티 관계 없음 | 전체 테이블에서 선택 → FK 수동 입력 | | 조회만 (저장 안 함) | `isReadOnly: true` 설정 | --- ## 2. 엔티티 조인 컬럼 활용 (필수) ### 핵심 원칙 **화면을 새로 만들어서 화면 안에 넣는 방식을 사용하지 않습니다.** 대신, 현재 화면의 메인 테이블을 기준으로 테이블 타입관리의 엔티티 관계를 불러와서 조인되어 있는 컬럼들을 모두 사용 가능하게 해야 합니다. ### API 사용법 ```typescript import { entityJoinApi } from "@/lib/api/entityJoin"; // 테이블의 엔티티 조인 컬럼 정보 가져오기 const result = await entityJoinApi.getEntityJoinColumns(tableName); // 응답 구조 { tableName: string; joinTables: Array<{ tableName: string; // 조인 테이블명 (예: item_info) currentDisplayColumn: string; // 현재 표시 컬럼 availableColumns: Array<{ // 사용 가능한 컬럼들 columnName: string; columnLabel: string; dataType: string; description?: string; }>; }>; availableColumns: Array<{ // 플랫한 구조의 전체 사용 가능 컬럼 tableName: string; columnName: string; columnLabel: string; dataType: string; joinAlias: string; // 예: item_code_item_name suggestedLabel: string; // 예: 품목명 }>; summary: { totalJoinTables: number; totalAvailableColumns: number; } } ``` ### 컬럼 선택 UI 구현 ConfigPanel에서 엔티티 조인 컬럼을 표시하는 표준 패턴입니다. ```typescript // 상태 정의 const [entityJoinColumns, setEntityJoinColumns] = useState<{ availableColumns: Array<{ tableName: string; columnName: string; columnLabel: string; dataType: string; joinAlias: string; suggestedLabel: string; }>; joinTables: Array<{ tableName: string; currentDisplayColumn: string; availableColumns: Array<{ columnName: string; columnLabel: string; dataType: string; description?: string; }>; }>; }>({ availableColumns: [], joinTables: [] }); const [loadingEntityJoins, setLoadingEntityJoins] = useState(false); // 엔티티 조인 컬럼 로드 useEffect(() => { const fetchEntityJoinColumns = async () => { const tableName = config.selectedTable || screenTableName; if (!tableName) { setEntityJoinColumns({ availableColumns: [], joinTables: [] }); return; } setLoadingEntityJoins(true); try { const result = await entityJoinApi.getEntityJoinColumns(tableName); setEntityJoinColumns({ availableColumns: result.availableColumns || [], joinTables: result.joinTables || [], }); } catch (error) { console.error("엔티티 조인 컬럼 조회 오류:", error); setEntityJoinColumns({ availableColumns: [], joinTables: [] }); } finally { setLoadingEntityJoins(false); } }; fetchEntityJoinColumns(); }, [config.selectedTable, screenTableName]); ``` ### 컬럼 선택 UI 렌더링 ```tsx { /* 엔티티 조인 컬럼 섹션 */ } { entityJoinColumns.joinTables.length > 0 && (
{entityJoinColumns.joinTables.map((joinTable) => (
{joinTable.tableName} ({joinTable.availableColumns.length})
{joinTable.availableColumns.map((col) => { // "테이블명.컬럼명" 형식으로 컬럼 이름 생성 const fullColumnName = `${joinTable.tableName}.${col.columnName}`; const isSelected = config.columns?.some( (c) => c.columnName === fullColumnName ); return (
{ if (isSelected) { removeColumn(fullColumnName); } else { addEntityJoinColumn(joinTable.tableName, col); } }} >
{col.columnLabel}
{col.columnName}
); })}
))}
); } ``` ### 엔티티 조인 컬럼 추가 함수 ```typescript const addEntityJoinColumn = (tableName: string, column: any) => { const fullColumnName = `${tableName}.${column.columnName}`; const newColumn: ColumnConfig = { columnName: fullColumnName, displayName: column.columnLabel || column.columnName, visible: true, sortable: true, searchable: true, align: "left", format: "text", order: config.columns?.length || 0, isEntityJoin: true, // 엔티티 조인 컬럼 표시 entityJoinTable: tableName, entityJoinColumn: column.columnName, }; onChange({ ...config, columns: [...(config.columns || []), newColumn], }); }; ``` ### 데이터 조회 시 엔티티 조인 활용 ```typescript // 엔티티 조인이 포함된 데이터 조회 const response = await entityJoinApi.getTableDataWithJoins(tableName, { page: 1, size: 10, enableEntityJoin: true, // 추가 조인 컬럼 지정 (화면 설정에서 선택한 컬럼들) additionalJoinColumns: config.columns ?.filter((col) => col.isEntityJoin) ?.map((col) => ({ sourceTable: col.entityJoinTable!, sourceColumn: col.entityJoinColumn!, joinAlias: col.columnName, })), }); ``` ### 셀 값 추출 헬퍼 엔티티 조인 컬럼의 값을 데이터에서 추출하는 헬퍼 함수입니다. ```typescript const getEntityJoinValue = (item: any, columnName: string): any => { // 직접 매칭 시도 if (item[columnName] !== undefined) { return item[columnName]; } // "테이블명.컬럼명" 형식인 경우 if (columnName.includes(".")) { const [tableName, fieldName] = columnName.split("."); // 1. 소스 컬럼 추론 (item_info → item_code) const inferredSourceColumn = tableName .replace("_info", "_code") .replace("_mng", "_id"); // 2. 정확한 키 매핑: 소스컬럼_필드명 const exactKey = `${inferredSourceColumn}_${fieldName}`; if (item[exactKey] !== undefined) { return item[exactKey]; } // 3. item_id 패턴 시도 const idPatternKey = `${tableName.replace("_info", "_id")}_${fieldName}`; if (item[idPatternKey] !== undefined) { return item[idPatternKey]; } // 4. 단순 필드명으로 시도 if (item[fieldName] !== undefined) { return item[fieldName]; } } return undefined; }; ``` --- ## 3. 폼 데이터 관리 ### 통합 폼 시스템 (UnifiedFormContext) 새 컴포넌트는 통합 폼 시스템을 사용해야 합니다. ```typescript import { useFormCompatibility } from "@/hooks/useFormCompatibility"; const MyComponent = ({ onFormDataChange, formData, ...props }) => { // 호환성 브릿지 사용 const { getValue, setValue, submit } = useFormCompatibility({ legacyOnFormDataChange: onFormDataChange, }); // 값 읽기 const currentValue = getValue("fieldName"); // 값 설정 (모든 시스템에 전파됨) const handleChange = (value: any) => { setValue("fieldName", value); }; // 저장 const handleSave = async () => { const result = await submit({ tableName: "my_table", mode: "insert", }); }; }; ``` ### 레거시 컴포넌트와의 호환성 기존 `beforeFormSave` 이벤트를 사용하는 컴포넌트(리피터 등)와 호환됩니다. ```typescript import { useBeforeFormSave } from "@/hooks/useFormCompatibility"; const MyRepeaterComponent = ({ value, columnName }) => { // beforeFormSave 이벤트에서 데이터 수집 useEffect(() => { const handleSaveRequest = (event: CustomEvent) => { if (event.detail && columnName) { event.detail.formData[columnName] = value; } }; window.addEventListener("beforeFormSave", handleSaveRequest); return () => window.removeEventListener("beforeFormSave", handleSaveRequest); }, [value, columnName]); }; ``` ### onChange 핸들러 패턴 컴포넌트에서 값이 변경될 때 사용하는 표준 패턴입니다. ```typescript // 기본 패턴 (권장) const handleChange = useCallback( (value: any) => { // 1. UnifiedFormContext가 있으면 사용 if (unifiedContext) { unifiedContext.setValue(fieldName, value); } // 2. ScreenContext가 있으면 사용 if (screenContext?.updateFormData) { screenContext.updateFormData(fieldName, value); } // 3. 레거시 콜백이 있으면 호출 if (onFormDataChange) { onFormDataChange(fieldName, value); } }, [fieldName, unifiedContext, screenContext, onFormDataChange] ); ``` --- ## 4. 다국어 지원 ### 타입 정의 시 다국어 필드 추가 텍스트가 표시되는 **모든 속성**에 `langKeyId`와 `langKey` 필드를 추가합니다. ```typescript interface MyComponentConfig { // 기본 텍스트 title?: string; titleLangKeyId?: number; titleLangKey?: string; // 컬럼 배열 columns?: Array<{ name: string; label: string; langKeyId?: number; langKey?: string; }>; } ``` ### 라벨 추출 로직 등록 파일: `frontend/lib/utils/multilangLabelExtractor.ts` ```typescript // extractMultilangLabels 함수에 추가 if (comp.componentType === "my-new-component") { const config = comp.componentConfig as MyComponentConfig; // 제목 추출 if (config?.title) { addLabel({ id: `${comp.id}_title`, componentId: `${comp.id}_title`, label: config.title, type: "title", parentType: "my-new-component", parentLabel: config.title, langKeyId: config.titleLangKeyId, langKey: config.titleLangKey, }); } // 컬럼 추출 if (config?.columns && Array.isArray(config.columns)) { config.columns.forEach((col, index) => { addLabel({ id: `${comp.id}_col_${index}`, componentId: `${comp.id}_col_${index}`, label: col.label || col.name, type: "column", parentType: "my-new-component", parentLabel: config.title || "컴포넌트", langKeyId: col.langKeyId, langKey: col.langKey, }); }); } } ``` ### 매핑 적용 로직 등록 ```typescript // applyMultilangMappings 함수에 추가 if (comp.componentType === "my-new-component") { const config = comp.componentConfig as MyComponentConfig; // 제목 매핑 const titleMapping = mappingMap.get(`${comp.id}_title`); if (titleMapping) { updated.componentConfig = { ...updated.componentConfig, titleLangKeyId: titleMapping.keyId, titleLangKey: titleMapping.langKey, }; } // 컬럼 매핑 if (config?.columns && Array.isArray(config.columns)) { const updatedColumns = config.columns.map((col, index) => { const colMapping = mappingMap.get(`${comp.id}_col_${index}`); if (colMapping) { return { ...col, langKeyId: colMapping.keyId, langKey: colMapping.langKey, }; } return col; }); updated.componentConfig = { ...updated.componentConfig, columns: updatedColumns, }; } } ``` ### 번역 표시 로직 ```typescript import { useScreenMultiLang } from "@/contexts/ScreenMultiLangContext"; const MyComponent = ({ component }) => { const { getTranslatedText } = useScreenMultiLang(); const config = component.componentConfig; // 제목 번역 const displayTitle = config?.titleLangKey ? getTranslatedText(config.titleLangKey, config.title || "") : config?.title || ""; // 컬럼 헤더 번역 const translatedColumns = config?.columns?.map((col) => ({ ...col, displayLabel: col.langKey ? getTranslatedText(col.langKey, col.label) : col.label, })); return (

{displayTitle}

{translatedColumns?.map((col, idx) => ( ))}
{col.displayLabel}
); }; ``` ### ScreenMultiLangContext에 키 수집 로직 추가 파일: `frontend/contexts/ScreenMultiLangContext.tsx` ```typescript // collectLangKeys 함수에 추가 if (comp.componentType === "my-new-component") { const config = comp.componentConfig; if (config?.titleLangKey) { keys.add(config.titleLangKey); } if (config?.columns && Array.isArray(config.columns)) { config.columns.forEach((col: any) => { if (col.langKey) { keys.add(col.langKey); } }); } } ``` --- ## 5. 컬럼 설정 패널 구현 ### 필수 구조 모든 테이블/목록 기반 컴포넌트의 설정 패널은 다음 구조를 따릅니다: ```typescript interface ConfigPanelProps { config: MyComponentConfig; onChange: (config: Partial) => void; screenTableName?: string; // 화면에 연결된 테이블명 tableColumns?: any[]; // 테이블 컬럼 정보 } export const MyComponentConfigPanel: React.FC = ({ config, onChange, screenTableName, tableColumns, }) => { // 1. 기본 테이블 컬럼 상태 const [availableColumns, setAvailableColumns] = useState>([]); // 2. 엔티티 조인 컬럼 상태 (필수!) const [entityJoinColumns, setEntityJoinColumns] = useState<{ availableColumns: Array<{...}>; joinTables: Array<{...}>; }>({ availableColumns: [], joinTables: [] }); // 3. 로딩 상태 const [loadingEntityJoins, setLoadingEntityJoins] = useState(false); // 4. 화면 테이블명이 있으면 자동 설정 useEffect(() => { if (screenTableName && !config.selectedTable) { onChange({ ...config, selectedTable: screenTableName, columns: config.columns || [], }); } }, [screenTableName]); // 5. 기본 컬럼 로드 useEffect(() => { // tableColumns prop 또는 API에서 로드 }, [config.selectedTable, screenTableName, tableColumns]); // 6. 엔티티 조인 컬럼 로드 (필수!) useEffect(() => { const fetchEntityJoinColumns = async () => { const tableName = config.selectedTable || screenTableName; if (!tableName) { setEntityJoinColumns({ availableColumns: [], joinTables: [] }); return; } setLoadingEntityJoins(true); try { const result = await entityJoinApi.getEntityJoinColumns(tableName); setEntityJoinColumns({ availableColumns: result.availableColumns || [], joinTables: result.joinTables || [], }); } catch (error) { setEntityJoinColumns({ availableColumns: [], joinTables: [] }); } finally { setLoadingEntityJoins(false); } }; fetchEntityJoinColumns(); }, [config.selectedTable, screenTableName]); // 7. UI 렌더링 return (
{/* 기본 테이블 컬럼 */}
{/* 기본 컬럼 체크박스들 */}
{/* 엔티티 조인 컬럼 (필수!) */} {entityJoinColumns.joinTables.length > 0 && (
{/* 조인 테이블별 컬럼 선택 UI */}
)}
); }; ``` --- ## 6. 체크리스트 새 컴포넌트 개발 시 다음 항목을 확인하세요: ### V2 컴포넌트 규칙 (최우선) - [ ] V2 폴더(`v2-*/`)에서 작업 중인지 확인 - [ ] 원본 폴더는 수정하지 않음 - [ ] 컴포넌트 ID에 `v2-` 접두사 사용 - [ ] Definition 이름에 `V2` 접두사 사용 (예: `V2TableListDefinition`) - [ ] Renderer에서 올바른 V2 Definition 참조 확인 ### 컴포넌트별 테이블 설정 (핵심) - [ ] 화면 메인 테이블과 다른 테이블을 사용할 수 있는지 확인 - [ ] `useCustomTable`, `mainTableName` (또는 `customTableName`) 설정 지원 - [ ] 연관 테이블 선택 시 FK/PK 자동 설정 (`/api/table-management/columns/:tableName/referenced-by` API 활용) - [ ] 저장 테이블 변경 시 해당 테이블의 컬럼 자동 로드 - [ ] 테이블 선택 UI는 Combobox 형태로 그룹별 표시 (기본/연관/전체) - [ ] FK 자동 연결: `repeaterSave` 이벤트에서 `masterRecordId` 수신 및 적용 ### 엔티티 조인 (필수) - [ ] `entityJoinApi.getEntityJoinColumns()` 호출하여 조인 컬럼 로드 - [ ] 설정 패널에 "엔티티 조인 컬럼" 섹션 추가 - [ ] 조인 컬럼 선택 시 `tableName.columnName` 형식으로 저장 - [ ] 데이터 조회 시 `getTableDataWithJoins()` 사용 - [ ] 셀 값 추출 시 `getEntityJoinValue()` 헬퍼 사용 ### 폼 데이터 관리 - [ ] `useFormCompatibility` 훅 사용 - [ ] 값 변경 시 `setValue()` 호출 - [ ] 리피터 컴포넌트는 `beforeFormSave` 이벤트 처리 ### 다국어 지원 - [ ] 타입 정의에 `langKeyId`, `langKey` 필드 추가 - [ ] `extractMultilangLabels` 함수에 라벨 추출 로직 추가 - [ ] `applyMultilangMappings` 함수에 매핑 적용 로직 추가 - [ ] `collectLangKeys` 함수에 키 수집 로직 추가 - [ ] 컴포넌트에서 `useScreenMultiLang` 훅으로 번역 표시 ### 설정 패널 - [ ] `screenTableName` prop 처리 - [ ] `tableColumns` prop 처리 - [ ] 엔티티 조인 컬럼 로드 및 표시 - [ ] 컬럼 추가/제거/순서변경 기능 --- ## 관련 파일 목록 | 파일 | 역할 | | ---------------------------------------------------- | --------------------- | | `frontend/lib/api/entityJoin.ts` | 엔티티 조인 API | | `frontend/hooks/useFormCompatibility.ts` | 폼 호환성 브릿지 | | `frontend/components/unified/UnifiedFormContext.tsx` | 통합 폼 Context | | `frontend/lib/utils/multilangLabelExtractor.ts` | 다국어 라벨 추출/매핑 | | `frontend/contexts/ScreenMultiLangContext.tsx` | 다국어 번역 Context | --- ## 참고: TableListConfigPanel 예시 `frontend/lib/registry/components/table-list/TableListConfigPanel.tsx` 파일에서 엔티티 조인 컬럼을 어떻게 표시하는지 참고하세요. 주요 패턴: 1. `entityJoinApi.getEntityJoinColumns(tableName)` 호출 2. `joinTables` 배열을 순회하며 각 조인 테이블의 컬럼 표시 3. `tableName.columnName` 형식으로 컬럼명 생성 4. `isEntityJoin: true` 플래그로 일반 컬럼과 구분