From 702b506665b7d1a5f41076bbf13f5b1ec9f2290a Mon Sep 17 00:00:00 2001 From: kjs Date: Thu, 13 Nov 2025 17:52:33 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=B6=84=ED=95=A0=20=ED=8C=A8=EB=84=90?= =?UTF-8?q?=20=ED=85=8C=EC=9D=B4=EB=B8=94=EC=97=90=EC=84=9C=20=EC=B9=B4?= =?UTF-8?q?=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EA=B0=92=EC=9D=84=20=EB=9D=BC?= =?UTF-8?q?=EB=B2=A8=EB=A1=9C=20=ED=91=9C=EC=8B=9C=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 좌측/우측 패널 테이블의 카테고리 타입 컬럼 매핑 로드 - formatCellValue 함수 추가하여 카테고리 코드를 라벨+배지로 변환 - 좌측 패널 테이블 렌더링 부분(그룹화/일반)에 formatCellValue 적용 - 우측 패널 테이블 렌더링 부분에 formatCellValue 적용 - Badge 컴포넌트 import 및 카테고리별 색상 표시 변경 사항: - 카테고리 매핑 상태 추가 (leftCategoryMappings, rightCategoryMappings) - API 클라이언트 import 추가 - 카테고리 값 조회 API 호출 useEffect 2개 추가 - 셀 값 포맷팅 함수 formatCellValue 추가 - 모든 테이블 td 렌더링에서 String(value) → formatCellValue() 적용 --- .../SplitPanelLayoutComponent.tsx | 155 +++++++++++++++++- 1 file changed, 146 insertions(+), 9 deletions(-) diff --git a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx index 955bae86..a7e7e874 100644 --- a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx @@ -6,11 +6,13 @@ import { SplitPanelLayoutConfig } from "./types"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; +import { Badge } from "@/components/ui/badge"; import { Plus, Search, GripVertical, Loader2, ChevronDown, ChevronUp, Save, ChevronRight, Pencil, Trash2 } from "lucide-react"; import { dataApi } from "@/lib/api/data"; import { entityJoinApi } from "@/lib/api/entityJoin"; import { useToast } from "@/hooks/use-toast"; import { tableTypeApi } from "@/lib/api/screen"; +import { apiClient } from "@/lib/api/client"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from "@/components/ui/dialog"; import { Label } from "@/components/ui/label"; import { useTableOptions } from "@/contexts/TableOptionsContext"; @@ -64,6 +66,8 @@ export const SplitPanelLayoutComponent: React.FC const [expandedItems, setExpandedItems] = useState>(new Set()); // 펼쳐진 항목들 const [leftColumnLabels, setLeftColumnLabels] = useState>({}); // 좌측 컬럼 라벨 const [rightColumnLabels, setRightColumnLabels] = useState>({}); // 우측 컬럼 라벨 + const [leftCategoryMappings, setLeftCategoryMappings] = useState>>({}); // 좌측 카테고리 매핑 + const [rightCategoryMappings, setRightCategoryMappings] = useState>>({}); // 우측 카테고리 매핑 const { toast } = useToast(); // 추가 모달 상태 @@ -241,6 +245,39 @@ export const SplitPanelLayoutComponent: React.FC })); }, [leftData, leftGrouping]); + // 셀 값 포맷팅 함수 (카테고리 타입 처리) + const formatCellValue = useCallback(( + columnName: string, + value: any, + categoryMappings: Record> + ) => { + if (value === null || value === undefined) return "-"; + + // 카테고리 매핑이 있는지 확인 + const mapping = categoryMappings[columnName]; + if (mapping && mapping[String(value)]) { + const categoryData = mapping[String(value)]; + const displayLabel = categoryData.label || String(value); + const displayColor = categoryData.color || "#64748b"; + + // 배지로 표시 + return ( + + {displayLabel} + + ); + } + + // 일반 값 + return String(value); + }, []); + // 좌측 데이터 로드 const loadLeftData = useCallback(async () => { const leftTableName = componentConfig.leftPanel?.tableName; @@ -523,6 +560,112 @@ export const SplitPanelLayoutComponent: React.FC loadRightTableColumns(); }, [componentConfig.rightPanel?.tableName, 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> = {}; + + for (const col of categoryColumns) { + const columnName = col.columnName || col.column_name; + try { + const response = await apiClient.get( + `/table-type/category-values/${leftTableName}/${columnName}` + ); + + 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); + } + } catch (error) { + console.error(`좌측 카테고리 값 조회 실패 [${columnName}]:`, error); + } + } + + setLeftCategoryMappings(mappings); + } catch (error) { + console.error("좌측 카테고리 매핑 로드 실패:", error); + } + }; + + loadLeftCategoryMappings(); + }, [componentConfig.leftPanel?.tableName, isDesignMode]); + + // 우측 테이블 카테고리 매핑 로드 + useEffect(() => { + const loadRightCategoryMappings = async () => { + const rightTableName = componentConfig.rightPanel?.tableName; + if (!rightTableName || isDesignMode) return; + + try { + // 1. 컬럼 메타 정보 조회 + const columnsResponse = await tableTypeApi.getColumns(rightTableName); + const categoryColumns = columnsResponse.filter( + (col: any) => col.inputType === "category" + ); + + if (categoryColumns.length === 0) { + setRightCategoryMappings({}); + return; + } + + // 2. 각 카테고리 컬럼에 대한 값 조회 + const mappings: Record> = {}; + + for (const col of categoryColumns) { + const columnName = col.columnName || col.column_name; + try { + const response = await apiClient.get( + `/table-type/category-values/${rightTableName}/${columnName}` + ); + + 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); + } + } catch (error) { + console.error(`우측 카테고리 값 조회 실패 [${columnName}]:`, error); + } + } + + setRightCategoryMappings(mappings); + } catch (error) { + console.error("우측 카테고리 매핑 로드 실패:", error); + } + }; + + loadRightCategoryMappings(); + }, [componentConfig.rightPanel?.tableName, isDesignMode]); + // 항목 펼치기/접기 토글 const toggleExpand = useCallback((itemId: any) => { setExpandedItems(prev => { @@ -1193,9 +1336,7 @@ export const SplitPanelLayoutComponent: React.FC className="whitespace-nowrap px-3 py-2 text-sm text-gray-900" style={{ textAlign: col.align || "left" }} > - {item[col.name] !== null && item[col.name] !== undefined - ? String(item[col.name]) - : "-"} + {formatCellValue(col.name, item[col.name], leftCategoryMappings)} ))} @@ -1246,9 +1387,7 @@ export const SplitPanelLayoutComponent: React.FC className="whitespace-nowrap px-3 py-2 text-sm text-gray-900" style={{ textAlign: col.align || "left" }} > - {item[col.name] !== null && item[col.name] !== undefined - ? String(item[col.name]) - : "-"} + {formatCellValue(col.name, item[col.name], leftCategoryMappings)} ))} @@ -1619,9 +1758,7 @@ export const SplitPanelLayoutComponent: React.FC className="whitespace-nowrap px-3 py-2 text-sm text-gray-900" style={{ textAlign: col.align || "left" }} > - {item[col.name] !== null && item[col.name] !== undefined - ? String(item[col.name]) - : "-"} + {formatCellValue(col.name, item[col.name], rightCategoryMappings)} ))} {!isDesignMode && (