From 12d3419b7ff99546f2c37755e2ccb29dbfcb60a4 Mon Sep 17 00:00:00 2001 From: leeheejin Date: Tue, 6 Jan 2026 17:39:36 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B5=AC=EB=B6=84=20=ED=95=84=ED=84=B0?= =?UTF-8?q?=EB=A7=81=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../table-list/TableListComponent.tsx | 113 +++++++++++++++++- 1 file changed, 107 insertions(+), 6 deletions(-) diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index 24a93af8..389dd92d 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -6,6 +6,7 @@ import { WebType } from "@/types/common"; import { tableTypeApi } from "@/lib/api/screen"; import { entityJoinApi } from "@/lib/api/entityJoin"; import { codeCache } from "@/lib/caching/codeCache"; +import { getCategoryLabelsByCodes } from "@/lib/api/tableCategoryValue"; import { useEntityJoinOptimization } from "@/lib/hooks/useEntityJoinOptimization"; import { getFullImageUrl } from "@/lib/api/client"; import { Button } from "@/components/ui/button"; @@ -471,6 +472,7 @@ export const TableListComponent: React.FC = ({ } // 2. 헤더 필터 적용 (joinColumnMapping 사용 안 함 - 직접 컬럼명 사용) + // 🆕 다중 값 지원: 셀 값이 "A,B,C" 형태일 때, 필터에서 "A"를 선택하면 해당 행도 표시 if (Object.keys(headerFilters).length > 0) { result = result.filter((row) => { return Object.entries(headerFilters).every(([columnName, values]) => { @@ -480,7 +482,16 @@ export const TableListComponent: React.FC = ({ const cellValue = row[columnName] ?? row[columnName.toLowerCase()] ?? row[columnName.toUpperCase()]; const cellStr = cellValue !== null && cellValue !== undefined ? String(cellValue) : ""; - return values.has(cellStr); + // 정확히 일치하는 경우 + if (values.has(cellStr)) return true; + + // 다중 값인 경우: 콤마로 분리해서 하나라도 포함되면 true + if (cellStr.includes(",")) { + const cellValues = cellStr.split(",").map(v => v.trim()); + return cellValues.some(v => values.has(v)); + } + + return false; }); }); } @@ -2248,12 +2259,18 @@ export const TableListComponent: React.FC = ({ // 🆕 편집 모드 진입 placeholder (실제 구현은 visibleColumns 정의 후) const startEditingRef = useRef<() => void>(() => {}); + // 🆕 카테고리 라벨 매핑 (API에서 가져온 것) + const [categoryLabelCache, setCategoryLabelCache] = useState>({}); + // 🆕 각 컬럼의 고유값 목록 계산 (라벨 포함) const columnUniqueValues = useMemo(() => { const result: Record> = {}; if (data.length === 0) return result; + // 🆕 전체 데이터에서 개별 값 -> 라벨 매핑 테이블 구축 (다중 값 처리용) + const globalLabelMap: Record> = {}; + (tableConfig.columns || []).forEach((column: { columnName: string }) => { if (column.columnName === "__checkbox__") return; @@ -2265,23 +2282,70 @@ export const TableListComponent: React.FC = ({ `${column.columnName}_value_label`, // 예: division_value_label ]; const valuesMap = new Map(); // value -> label + const singleValueLabelMap = new Map(); // 개별 값 -> 라벨 (다중값 처리용) + // 1차: 모든 데이터에서 개별 값 -> 라벨 매핑 수집 (단일값 + 다중값 모두) data.forEach((row) => { const val = row[mappedColumnName]; if (val !== null && val !== undefined && val !== "") { const valueStr = String(val); - // 라벨 컬럼 후보들 중 값이 있는 것 사용, 없으면 원본 값 사용 - let label = valueStr; + + // 라벨 컬럼에서 라벨 찾기 + let labelStr = ""; for (const labelCol of labelColumnCandidates) { if (row[labelCol] && row[labelCol] !== "") { - label = String(row[labelCol]); + labelStr = String(row[labelCol]); break; } } - valuesMap.set(valueStr, label); + + // 단일 값인 경우 + if (!valueStr.includes(",")) { + if (labelStr) { + singleValueLabelMap.set(valueStr, labelStr); + } + } else { + // 다중 값인 경우: 값과 라벨을 각각 분리해서 매핑 + const individualValues = valueStr.split(",").map(v => v.trim()); + const individualLabels = labelStr ? labelStr.split(",").map(l => l.trim()) : []; + + // 값과 라벨 개수가 같으면 1:1 매핑 + if (individualValues.length === individualLabels.length) { + individualValues.forEach((v, idx) => { + if (individualLabels[idx] && !singleValueLabelMap.has(v)) { + singleValueLabelMap.set(v, individualLabels[idx]); + } + }); + } + } } }); + // 2차: 모든 값 처리 (다중 값 포함) - 필터 목록용 + data.forEach((row) => { + const val = row[mappedColumnName]; + if (val !== null && val !== undefined && val !== "") { + const valueStr = String(val); + + // 콤마로 구분된 다중 값인지 확인 + if (valueStr.includes(",")) { + // 다중 값: 각각 분리해서 개별 라벨 찾기 + const individualValues = valueStr.split(",").map(v => v.trim()); + // 🆕 singleValueLabelMap → categoryLabelCache 순으로 라벨 찾기 + const individualLabels = individualValues.map(v => + singleValueLabelMap.get(v) || categoryLabelCache[v] || v + ); + valuesMap.set(valueStr, individualLabels.join(", ")); + } else { + // 단일 값: 매핑에서 찾거나 캐시에서 찾거나 원본 사용 + const label = singleValueLabelMap.get(valueStr) || categoryLabelCache[valueStr] || valueStr; + valuesMap.set(valueStr, label); + } + } + }); + + globalLabelMap[column.columnName] = singleValueLabelMap; + // value-label 쌍으로 저장하고 라벨 기준 정렬 result[column.columnName] = Array.from(valuesMap.entries()) .map(([value, label]) => ({ value, label })) @@ -2289,7 +2353,44 @@ export const TableListComponent: React.FC = ({ }); return result; - }, [data, tableConfig.columns, joinColumnMapping]); + }, [data, tableConfig.columns, joinColumnMapping, categoryLabelCache]); + + // 🆕 라벨을 못 찾은 CATEGORY_ 코드들을 API로 조회 + useEffect(() => { + const unlabeledCodes = new Set(); + + // columnUniqueValues에서 라벨이 코드 그대로인 항목 찾기 + Object.values(columnUniqueValues).forEach(items => { + items.forEach(item => { + // 라벨에 CATEGORY_가 포함되어 있으면 라벨을 못 찾은 것 + if (item.label.includes("CATEGORY_")) { + // 콤마로 분리해서 개별 코드 추출 + const codes = item.label.split(",").map(c => c.trim()); + codes.forEach(code => { + if (code.startsWith("CATEGORY_") && !categoryLabelCache[code]) { + unlabeledCodes.add(code); + } + }); + } + }); + }); + + if (unlabeledCodes.size === 0) return; + + // API로 라벨 조회 + const fetchLabels = async () => { + try { + const response = await getCategoryLabelsByCodes(Array.from(unlabeledCodes)); + if (response.success && response.data) { + setCategoryLabelCache(prev => ({ ...prev, ...response.data })); + } + } catch (error) { + console.error("카테고리 라벨 조회 실패:", error); + } + }; + + fetchLabels(); + }, [columnUniqueValues, categoryLabelCache]); // 🆕 헤더 필터 토글 const toggleHeaderFilter = useCallback((columnName: string, value: string) => { -- 2.43.0