diff --git a/frontend/lib/registry/components/split-panel-layout2/SplitPanelLayout2Component.tsx b/frontend/lib/registry/components/split-panel-layout2/SplitPanelLayout2Component.tsx index f3ed2145..f5d5666b 100644 --- a/frontend/lib/registry/components/split-panel-layout2/SplitPanelLayout2Component.tsx +++ b/frontend/lib/registry/components/split-panel-layout2/SplitPanelLayout2Component.tsx @@ -36,7 +36,7 @@ import { Card, CardContent } from "@/components/ui/card"; import { toast } from "sonner"; import { useScreenContextOptional } from "@/contexts/ScreenContext"; import { apiClient } from "@/lib/api/client"; -import { getCategoryValues } from "@/lib/api/tableCategoryValue"; +import { getCategoryValues, getCategoryLabelsByCodes } from "@/lib/api/tableCategoryValue"; export interface SplitPanelLayout2ComponentProps extends ComponentRendererProps { // 추가 props @@ -1354,6 +1354,40 @@ export const SplitPanelLayout2Component: React.FC { + if (isDesignMode) return; + const allData = [...leftData, ...rightData]; + if (allData.length === 0) return; + + const unresolvedCodes = new Set(); + const checkValue = (v: unknown) => { + if (typeof v === "string" && (v.startsWith("CAT_") || v.startsWith("CATEGORY_"))) { + if (!categoryLabelMap[v]) unresolvedCodes.add(v); + } + }; + for (const item of allData) { + for (const val of Object.values(item)) { + if (Array.isArray(val)) { + val.forEach(checkValue); + } else { + checkValue(val); + } + } + } + + if (unresolvedCodes.size === 0) return; + + const resolveMissingLabels = async () => { + const result = await getCategoryLabelsByCodes(Array.from(unresolvedCodes)); + if (result.success && result.data && Object.keys(result.data).length > 0) { + setCategoryLabelMap((prev) => ({ ...prev, ...result.data })); + } + }; + + resolveMissingLabels(); + }, [isDesignMode, leftData, rightData, categoryLabelMap]); + // 컴포넌트 언마운트 시 DataProvider 해제 useEffect(() => { return () => { diff --git a/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx b/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx index 92b4cab9..0d6c2c3f 100644 --- a/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx @@ -1001,23 +1001,24 @@ export const SplitPanelLayoutComponent: React.FC return formatNumberValue(value, format); } - // 🆕 카테고리 매핑 찾기 (여러 키 형태 시도) + // 카테고리 매핑 찾기 (여러 키 형태 시도) // 1. 전체 컬럼명 (예: "item_info.material") // 2. 컬럼명만 (예: "material") + // 3. 전역 폴백: 모든 매핑에서 value 검색 let mapping = categoryMappings[columnName]; if (!mapping && columnName.includes(".")) { - // 조인된 컬럼의 경우 컬럼명만으로 다시 시도 const simpleColumnName = columnName.split(".").pop() || columnName; mapping = categoryMappings[simpleColumnName]; } - if (mapping && mapping[String(value)]) { - const categoryData = mapping[String(value)]; - const displayLabel = categoryData.label || String(value); + const strValue = String(value); + + if (mapping && mapping[strValue]) { + const categoryData = mapping[strValue]; + const displayLabel = categoryData.label || strValue; const displayColor = categoryData.color || "#64748b"; - // 배지로 표시 return ( ); } + // 전역 폴백: 컬럼명으로 매핑을 못 찾았을 때, 전체 매핑에서 값 검색 + if (!mapping && (strValue.startsWith("CAT_") || strValue.startsWith("CATEGORY_"))) { + for (const key of Object.keys(categoryMappings)) { + const m = categoryMappings[key]; + if (m && m[strValue]) { + const categoryData = m[strValue]; + const displayLabel = categoryData.label || strValue; + const displayColor = categoryData.color || "#64748b"; + return ( + + {displayLabel} + + ); + } + } + } + // 🆕 자동 날짜 감지 (ISO 8601 형식 또는 Date 객체) if (typeof value === "string" && value.match(/^\d{4}-\d{2}-\d{2}(T|\s)/)) { return formatDateValue(value, "YYYY-MM-DD"); @@ -1981,43 +2005,59 @@ export const SplitPanelLayoutComponent: React.FC loadRightTableColumns(); }, [componentConfig.rightPanel?.tableName, componentConfig.rightPanel?.additionalTabs, isDesignMode]); - // 좌측 테이블 카테고리 매핑 로드 + // 좌측 테이블 카테고리 매핑 로드 (조인된 테이블 포함) useEffect(() => { const loadLeftCategoryMappings = async () => { const leftTableName = componentConfig.leftPanel?.tableName; if (!leftTableName || isDesignMode) return; try { - // 1. 컬럼 메타 정보 조회 - const columnsResponse = await tableTypeApi.getColumns(leftTableName); - const categoryColumns = columnsResponse.filter((col: any) => col.inputType === "category"); - - if (categoryColumns.length === 0) { - setLeftCategoryMappings({}); - return; - } - - // 2. 각 카테고리 컬럼에 대한 값 조회 const mappings: Record> = {}; + const tablesToLoad = new Set([leftTableName]); - for (const col of categoryColumns) { - const columnName = col.columnName || col.column_name; + // 좌측 패널 컬럼 설정에서 조인된 테이블 추출 + const leftColumns = componentConfig.leftPanel?.columns || []; + leftColumns.forEach((col: any) => { + const colName = col.name || col.columnName; + if (colName && colName.includes(".")) { + const joinTableName = colName.split(".")[0]; + tablesToLoad.add(joinTableName); + } + }); + + // 각 테이블에 대해 카테고리 매핑 로드 + for (const tableName of tablesToLoad) { try { - const response = await apiClient.get(`/table-categories/${leftTableName}/${columnName}/values?includeInactive=true`); + const columnsResponse = await tableTypeApi.getColumns(tableName); + const categoryColumns = columnsResponse.filter((col: any) => col.inputType === "category"); - if (response.data.success && response.data.data) { - const valueMap: Record = {}; - response.data.data.forEach((item: any) => { - valueMap[item.value_code || item.valueCode] = { - label: item.value_label || item.valueLabel, - color: item.color, - }; - }); - mappings[columnName] = valueMap; - console.log(`✅ 좌측 카테고리 매핑 로드 [${columnName}]:`, valueMap); + for (const col of categoryColumns) { + const columnName = col.columnName || col.column_name; + try { + const response = await apiClient.get(`/table-categories/${tableName}/${columnName}/values?includeInactive=true`); + + if (response.data.success && response.data.data) { + const valueMap: Record = {}; + response.data.data.forEach((item: any) => { + valueMap[item.value_code || item.valueCode] = { + label: item.value_label || item.valueLabel, + color: item.color, + }; + }); + + // 조인된 테이블은 "테이블명.컬럼명" 형태로도 저장 + const mappingKey = tableName === leftTableName ? columnName : `${tableName}.${columnName}`; + mappings[mappingKey] = valueMap; + + // 컬럼명만으로도 접근 가능하도록 추가 저장 + mappings[columnName] = { ...(mappings[columnName] || {}), ...valueMap }; + } + } catch (error) { + console.error(`좌측 카테고리 값 조회 실패 [${tableName}.${columnName}]:`, error); + } } } catch (error) { - console.error(`좌측 카테고리 값 조회 실패 [${columnName}]:`, error); + console.error(`좌측 카테고리 테이블 컬럼 조회 실패 [${tableName}]:`, error); } } @@ -2028,7 +2068,7 @@ export const SplitPanelLayoutComponent: React.FC }; loadLeftCategoryMappings(); - }, [componentConfig.leftPanel?.tableName, isDesignMode]); + }, [componentConfig.leftPanel?.tableName, componentConfig.leftPanel?.columns, isDesignMode]); // 우측 테이블 카테고리 매핑 로드 (조인된 테이블 포함) useEffect(() => { @@ -3720,9 +3760,22 @@ export const SplitPanelLayoutComponent: React.FC displayFields = configuredColumns.slice(0, 2).map((col: any) => { const colName = typeof col === "string" ? col : col.name || col.columnName; const colLabel = typeof col === "object" ? col.label : leftColumnLabels[colName] || colName; + const rawValue = getEntityJoinValue(item, colName); + // 카테고리 매핑이 있으면 라벨로 변환 + let displayValue = rawValue; + if (rawValue != null && rawValue !== "") { + const strVal = String(rawValue); + let mapping = leftCategoryMappings[colName]; + if (!mapping && colName.includes(".")) { + mapping = leftCategoryMappings[colName.split(".").pop() || colName]; + } + if (mapping && mapping[strVal]) { + displayValue = mapping[strVal].label; + } + } return { label: colLabel, - value: item[colName], + value: displayValue, }; }); @@ -3734,10 +3787,21 @@ export const SplitPanelLayoutComponent: React.FC const keys = Object.keys(item).filter( (k) => k !== "id" && k !== "ID" && k !== "children" && k !== "level" && shouldShowField(k), ); - displayFields = keys.slice(0, 2).map((key) => ({ - label: leftColumnLabels[key] || key, - value: item[key], - })); + displayFields = keys.slice(0, 2).map((key) => { + const rawValue = item[key]; + let displayValue = rawValue; + if (rawValue != null && rawValue !== "") { + const strVal = String(rawValue); + const mapping = leftCategoryMappings[key]; + if (mapping && mapping[strVal]) { + displayValue = mapping[strVal].label; + } + } + return { + label: leftColumnLabels[key] || key, + value: displayValue, + }; + }); if (index === 0) { console.log(" ⚠️ 설정된 컬럼 없음, 자동 선택:", displayFields);