diff --git a/backend-node/package-lock.json b/backend-node/package-lock.json index f482dc7b..8b97b9ed 100644 --- a/backend-node/package-lock.json +++ b/backend-node/package-lock.json @@ -1046,6 +1046,7 @@ "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -2373,6 +2374,7 @@ "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz", "integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==", "license": "MIT", + "peer": true, "dependencies": { "cluster-key-slot": "1.1.2", "generic-pool": "3.9.0", @@ -3485,6 +3487,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.17.tgz", "integrity": "sha512-gfehUI8N1z92kygssiuWvLiwcbOB3IRktR6hTDgJlXMYh5OvkPSRmgfoBUmfZt+vhwJtX7v1Yw4KvvAf7c5QKQ==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -3721,6 +3724,7 @@ "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "6.21.0", "@typescript-eslint/types": "6.21.0", @@ -3938,6 +3942,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4463,6 +4468,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001741", @@ -5673,6 +5679,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -5951,6 +5958,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -7486,6 +7494,7 @@ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -8455,7 +8464,6 @@ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "license": "MIT", - "peer": true, "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -9343,6 +9351,7 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", "license": "MIT", + "peer": true, "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", @@ -10198,7 +10207,6 @@ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" } @@ -11006,6 +11014,7 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -11111,6 +11120,7 @@ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/backend-node/src/controllers/entitySearchController.ts b/backend-node/src/controllers/entitySearchController.ts index 62fc8bbe..c0c4c36d 100644 --- a/backend-node/src/controllers/entitySearchController.ts +++ b/backend-node/src/controllers/entitySearchController.ts @@ -181,20 +181,92 @@ export async function getDistinctColumnValues(req: AuthenticatedRequest, res: Re ? `WHERE ${whereConditions.join(" AND ")}` : ""; - // DISTINCT 쿼리 실행 - const query = ` + // 1단계: DISTINCT 값 조회 + const distinctQuery = ` SELECT DISTINCT "${columnName}" as value, "${effectiveLabelColumn}" as label FROM "${tableName}" ${whereClause} ORDER BY "${effectiveLabelColumn}" ASC LIMIT 500 `; + const result = await pool.query(distinctQuery, params); - const result = await pool.query(query, params); + // 2단계: 카테고리/코드 라벨 변환 (값이 있을 때만) + if (result.rows.length > 0) { + const rawValues = result.rows.map((r: any) => r.value); + const labelMap: Record = {}; + + // category_values에서 라벨 조회 + try { + const cvCompanyCondition = companyCode !== "*" + ? `AND (company_code = $4 OR company_code = '*')` + : ""; + const cvParams = companyCode !== "*" + ? [tableName, columnName, rawValues, companyCode] + : [tableName, columnName, rawValues]; + + const cvResult = await pool.query( + `SELECT value_code, value_label FROM category_values + WHERE table_name = $1 AND column_name = $2 + AND value_code = ANY($3) AND is_active = true + ${cvCompanyCondition}`, + cvParams + ); + cvResult.rows.forEach((r: any) => { + labelMap[r.value_code] = r.value_label; + }); + } catch (e) { + // category_values 조회 실패 시 무시 + } + + // code_info에서 라벨 조회 (code_category 기반) + try { + const ttcResult = await pool.query( + `SELECT code_category FROM table_type_columns + WHERE table_name = $1 AND column_name = $2 AND code_category IS NOT NULL + LIMIT 1`, + [tableName, columnName] + ); + const codeCategory = ttcResult.rows[0]?.code_category; + + if (codeCategory) { + const ciCompanyCondition = companyCode !== "*" + ? `AND (company_code = $3 OR company_code = '*')` + : ""; + const ciParams = companyCode !== "*" + ? [codeCategory, rawValues, companyCode] + : [codeCategory, rawValues]; + + const ciResult = await pool.query( + `SELECT code_value, code_name FROM code_info + WHERE code_category = $1 AND code_value = ANY($2) AND is_active = 'Y' + ${ciCompanyCondition}`, + ciParams + ); + ciResult.rows.forEach((r: any) => { + if (!labelMap[r.code_value]) { + labelMap[r.code_value] = r.code_name; + } + }); + } + } catch (e) { + // code_info 조회 실패 시 무시 + } + + // 라벨 매핑 적용 + if (Object.keys(labelMap).length > 0) { + result.rows.forEach((row: any) => { + if (labelMap[row.value]) { + row.label = labelMap[row.value]; + } + }); + } + } logger.info("컬럼 DISTINCT 값 조회 성공", { tableName, columnName, + columnInputType: columnInputType || "none", labelColumn: effectiveLabelColumn, companyCode, hasFilters: !!filtersParam, diff --git a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx index a12e4bce..f5ff5a39 100644 --- a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx @@ -1341,56 +1341,79 @@ export const SplitPanelLayoutComponent: React.FC const getLeftColumnUniqueValues = useCallback( async (columnName: string) => { const leftTableName = componentConfig.leftPanel?.tableName; - if (!leftTableName || leftData.length === 0) return []; + if (!leftTableName) return []; - // 현재 로드된 데이터에서 고유값 추출 - const uniqueValues = new Set(); + // 1단계: 카테고리 API 시도 (DB에서 라벨 조회) + try { + const { apiClient } = await import("@/lib/api/client"); + const response = await apiClient.get(`/table-categories/${leftTableName}/${columnName}/values`); + if (response.data.success && response.data.data && response.data.data.length > 0) { + return response.data.data.map((item: any) => ({ + value: item.valueCode, + label: item.valueLabel, + })); + } + } catch { + // 카테고리 API 실패 시 다음 단계로 + } + + // 2단계: DISTINCT API (백엔드 라벨 변환 포함) + try { + const { apiClient } = await import("@/lib/api/client"); + const response = await apiClient.get(`/entity/${leftTableName}/distinct/${columnName}`); + if (response.data.success && response.data.data && response.data.data.length > 0) { + return response.data.data.map((item: any) => ({ + value: String(item.value), + label: String(item.label), + })); + } + } catch { + // DISTINCT API 실패 시 다음 단계로 + } + + // 3단계: 로컬 데이터에서 고유값 추출 (최종 fallback) + if (leftData.length === 0) return []; + + const uniqueValuesMap = new Map(); leftData.forEach((item) => { - // 🆕 조인 컬럼 처리 (item_info.standard → item_code_standard 또는 item_id_standard) let value: any; if (columnName.includes(".")) { - // 조인 컬럼: getEntityJoinValue와 동일한 로직 적용 const [refTable, fieldName] = columnName.split("."); const inferredSourceColumn = refTable.replace("_info", "_code").replace("_mng", "_id"); - // 정확한 키로 먼저 시도 const exactKey = `${inferredSourceColumn}_${fieldName}`; value = item[exactKey]; - // 🆕 item_id 패턴 시도 if (value === undefined) { const idPatternKey = `${refTable.replace("_info", "_id").replace("_mng", "_id")}_${fieldName}`; value = item[idPatternKey]; } - // 기본 별칭 패턴 시도 (item_code_name 또는 item_id_name) if (value === undefined && (fieldName === "item_name" || fieldName === "name")) { const aliasKey = `${inferredSourceColumn}_name`; value = item[aliasKey]; - // item_id_name 패턴도 시도 if (value === undefined) { const idAliasKey = `${refTable.replace("_info", "_id").replace("_mng", "_id")}_name`; value = item[idAliasKey]; } } } else { - // 일반 컬럼 value = item[columnName]; } if (value !== null && value !== undefined && value !== "") { - // _name 필드 우선 사용 (category/entity type) - const displayValue = item[`${columnName}_name`] || value; - uniqueValues.add(String(displayValue)); + const strValue = String(value); + const nameField = item[`${columnName}_name`]; + const label = nameField || strValue; + uniqueValuesMap.set(strValue, label); } }); - return Array.from(uniqueValues).map((value) => ({ - value: value, - label: value, - })); + return Array.from(uniqueValuesMap.entries()) + .map(([value, label]) => ({ value, label })) + .sort((a, b) => a.label.localeCompare(b.label)); }, [componentConfig.leftPanel?.tableName, leftData], ); diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index a01e88ed..1d8fa196 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -919,66 +919,63 @@ export const TableListComponent: React.FC = ({ // 컬럼의 고유 값 조회 함수 const getColumnUniqueValues = async (columnName: string) => { - const meta = columnMeta[columnName]; - const inputType = meta?.inputType || "text"; + const { apiClient } = await import("@/lib/api/client"); - // 카테고리 타입인 경우 전체 정의된 값 조회 (백엔드 API) - if (inputType === "category") { - try { - // API 클라이언트 사용 (쿠키 인증 자동 처리) - const { apiClient } = await import("@/lib/api/client"); - const response = await apiClient.get(`/table-categories/${tableConfig.selectedTable}/${columnName}/values`); - - if (response.data.success && response.data.data) { - const categoryOptions = response.data.data.map((item: any) => ({ - value: item.valueCode, // 카멜케이스 - label: item.valueLabel, // 카멜케이스 - })); - - return categoryOptions; - } - } catch { - // 에러 시 현재 데이터 기반으로 fallback - } - } - - // 백엔드 DISTINCT API로 전체 고유값 조회 (페이징과 무관하게 모든 값 반환) + // 1단계: 카테고리 API 시도 (columnMeta 무관하게 항상 시도) try { - const { apiClient } = await import("@/lib/api/client"); - const response = await apiClient.get(`/entity/${tableConfig.selectedTable}/distinct/${columnName}`); - + const response = await apiClient.get(`/table-categories/${tableConfig.selectedTable}/${columnName}/values`); if (response.data.success && response.data.data && response.data.data.length > 0) { return response.data.data.map((item: any) => ({ - value: String(item.value), - label: String(item.label), + value: item.valueCode, + label: item.valueLabel, })); } } catch { - // DISTINCT API 실패 시 현재 데이터 기반으로 fallback + // 카테고리 API 실패 시 다음 단계로 } - // fallback: 현재 로드된 데이터에서 고유 값 추출 - const isLabelType = ["category", "entity", "code"].includes(inputType); - const labelField = isLabelType ? `${columnName}_name` : columnName; + // 2단계: DISTINCT API (백엔드에서 category_values/code_info 라벨 변환 포함) + try { + const response = await apiClient.get(`/entity/${tableConfig.selectedTable}/distinct/${columnName}`); + if (response.data.success && response.data.data && response.data.data.length > 0) { + let options = response.data.data.map((item: any) => ({ + value: String(item.value), + label: String(item.label), + })); + // 프론트엔드 카테고리 매핑으로 추가 라벨 변환 + const mapping = categoryMappings[columnName]; + if (mapping && Object.keys(mapping).length > 0) { + options = options.map((opt) => ({ + value: opt.value, + label: mapping[opt.value]?.label || opt.label, + })); + } + + return options; + } + } catch { + // DISTINCT API 실패 시 다음 단계로 + } + + // 3단계: 현재 로드된 데이터에서 고유 값 추출 (최종 fallback) const uniqueValuesMap = new Map(); + const mapping = categoryMappings[columnName]; data.forEach((row) => { const value = row[columnName]; if (value !== null && value !== undefined && value !== "") { - const label = isLabelType && row[labelField] ? row[labelField] : String(value); - uniqueValuesMap.set(String(value), label); + const strValue = String(value); + const nameField = row[`${columnName}_name`]; + const mappedLabel = mapping?.[strValue]?.label; + const label = mappedLabel || nameField || strValue; + uniqueValuesMap.set(strValue, label); } }); - const result = Array.from(uniqueValuesMap.entries()) - .map(([value, label]) => ({ - value: value, - label: label, - })) + return Array.from(uniqueValuesMap.entries()) + .map(([value, label]) => ({ value, label })) .sort((a, b) => a.label.localeCompare(b.label)); - - return result; }; const registration = { @@ -1031,6 +1028,7 @@ export const TableListComponent: React.FC = ({ tableConfig.columns, columnLabels, columnMeta, // columnMeta가 변경되면 재등록 (inputType 정보 필요) + categoryMappings, // 카테고리 매핑 변경 시 재등록 (필터 라벨 변환용) columnWidths, tableLabel, data, // 데이터 자체가 변경되면 재등록 (고유 값 조회용) diff --git a/frontend/lib/registry/components/table-search-widget/TableSearchWidget.tsx b/frontend/lib/registry/components/table-search-widget/TableSearchWidget.tsx index 2422c89e..17705ff6 100644 --- a/frontend/lib/registry/components/table-search-widget/TableSearchWidget.tsx +++ b/frontend/lib/registry/components/table-search-widget/TableSearchWidget.tsx @@ -393,12 +393,14 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentTable?.tableName, filterMode, screenId, currentTableTabId, JSON.stringify(presetFilters)]); - // select 옵션 초기 로드 (한 번만 실행, 이후 유지) + // select 옵션 로드 (getColumnUniqueValues 변경 시 재로드 - columnMeta 갱신 반영) useEffect(() => { if (!currentTable?.getColumnUniqueValues || activeFilters.length === 0) { return; } + let cancelled = false; + const loadSelectOptions = async () => { const selectFilters = activeFilters.filter((f) => f.filterType === "select"); @@ -406,26 +408,28 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table return; } - const newOptions: Record> = { ...selectOptions }; + const newOptions: Record> = {}; for (const filter of selectFilters) { - // 이미 로드된 옵션이 있으면 스킵 (초기값 유지) - if (newOptions[filter.columnName] && newOptions[filter.columnName].length > 0) { - continue; - } - try { const options = await currentTable.getColumnUniqueValues(filter.columnName); - newOptions[filter.columnName] = options; + if (options && options.length > 0) { + newOptions[filter.columnName] = options; + } } catch (error) { console.error("❌ [TableSearchWidget] select 옵션 로드 실패:", filter.columnName, error); } } - setSelectOptions(newOptions); + + if (!cancelled && Object.keys(newOptions).length > 0) { + setSelectOptions((prev) => ({ ...prev, ...newOptions })); + } }; loadSelectOptions(); - }, [activeFilters, currentTable?.tableName, currentTable?.getColumnUniqueValues]); // dataCount 제거, tableName으로 변경 + + return () => { cancelled = true; }; + }, [activeFilters, currentTable?.tableName, currentTable?.getColumnUniqueValues]); // 높이 변화 감지 및 알림 (실제 화면에서만) useEffect(() => { 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 3439c220..19107fda 100644 --- a/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx @@ -1683,56 +1683,79 @@ export const SplitPanelLayoutComponent: React.FC const getLeftColumnUniqueValues = useCallback( async (columnName: string) => { const leftTableName = componentConfig.leftPanel?.tableName; - if (!leftTableName || leftData.length === 0) return []; + if (!leftTableName) return []; - // 현재 로드된 데이터에서 고유값 추출 - const uniqueValues = new Set(); + // 1단계: 카테고리 API 시도 (DB에서 라벨 조회) + try { + const { apiClient } = await import("@/lib/api/client"); + const response = await apiClient.get(`/table-categories/${leftTableName}/${columnName}/values`); + if (response.data.success && response.data.data && response.data.data.length > 0) { + return response.data.data.map((item: any) => ({ + value: item.valueCode, + label: item.valueLabel, + })); + } + } catch { + // 카테고리 API 실패 시 다음 단계로 + } + + // 2단계: DISTINCT API (백엔드 라벨 변환 포함) + try { + const { apiClient } = await import("@/lib/api/client"); + const response = await apiClient.get(`/entity/${leftTableName}/distinct/${columnName}`); + if (response.data.success && response.data.data && response.data.data.length > 0) { + return response.data.data.map((item: any) => ({ + value: String(item.value), + label: String(item.label), + })); + } + } catch { + // DISTINCT API 실패 시 다음 단계로 + } + + // 3단계: 로컬 데이터에서 고유값 추출 (최종 fallback) + if (leftData.length === 0) return []; + + const uniqueValuesMap = new Map(); leftData.forEach((item) => { - // 🆕 조인 컬럼 처리 (item_info.standard → item_code_standard 또는 item_id_standard) let value: any; if (columnName.includes(".")) { - // 조인 컬럼: getEntityJoinValue와 동일한 로직 적용 const [refTable, fieldName] = columnName.split("."); const inferredSourceColumn = refTable.replace("_info", "_code").replace("_mng", "_id"); - // 정확한 키로 먼저 시도 const exactKey = `${inferredSourceColumn}_${fieldName}`; value = item[exactKey]; - // 🆕 item_id 패턴 시도 if (value === undefined) { const idPatternKey = `${refTable.replace("_info", "_id").replace("_mng", "_id")}_${fieldName}`; value = item[idPatternKey]; } - // 기본 별칭 패턴 시도 (item_code_name 또는 item_id_name) if (value === undefined && (fieldName === "item_name" || fieldName === "name")) { const aliasKey = `${inferredSourceColumn}_name`; value = item[aliasKey]; - // item_id_name 패턴도 시도 if (value === undefined) { const idAliasKey = `${refTable.replace("_info", "_id").replace("_mng", "_id")}_name`; value = item[idAliasKey]; } } } else { - // 일반 컬럼 value = item[columnName]; } if (value !== null && value !== undefined && value !== "") { - // _name 필드 우선 사용 (category/entity type) - const displayValue = item[`${columnName}_name`] || value; - uniqueValues.add(String(displayValue)); + const strValue = String(value); + const nameField = item[`${columnName}_name`]; + const label = nameField || strValue; + uniqueValuesMap.set(strValue, label); } }); - return Array.from(uniqueValues).map((value) => ({ - value: value, - label: value, - })); + return Array.from(uniqueValuesMap.entries()) + .map(([value, label]) => ({ value, label })) + .sort((a, b) => a.label.localeCompare(b.label)); }, [componentConfig.leftPanel?.tableName, leftData], ); diff --git a/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx b/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx index 4087be04..cc36afd6 100644 --- a/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx @@ -1038,66 +1038,64 @@ export const TableListComponent: React.FC = ({ // 컬럼의 고유 값 조회 함수 const getColumnUniqueValues = async (columnName: string) => { - const meta = columnMeta[columnName]; - const inputType = meta?.inputType || "text"; + const { apiClient } = await import("@/lib/api/client"); - // 카테고리 타입인 경우 전체 정의된 값 조회 (백엔드 API) - if (inputType === "category") { - try { - // API 클라이언트 사용 (쿠키 인증 자동 처리) - const { apiClient } = await import("@/lib/api/client"); - const response = await apiClient.get(`/table-categories/${tableConfig.selectedTable}/${columnName}/values`); - - if (response.data.success && response.data.data) { - const categoryOptions = response.data.data.map((item: any) => ({ - value: item.valueCode, // 카멜케이스 - label: item.valueLabel, // 카멜케이스 - })); - - return categoryOptions; - } - } catch (error: any) { - // 에러 시 현재 데이터 기반으로 fallback - } - } - - // 백엔드 DISTINCT API로 전체 고유값 조회 (페이징과 무관하게 모든 값 반환) + // 1단계: 카테고리 API 시도 (columnMeta 무관하게 항상 시도) try { - const { apiClient } = await import("@/lib/api/client"); - const response = await apiClient.get(`/entity/${tableConfig.selectedTable}/distinct/${columnName}`); - + const response = await apiClient.get(`/table-categories/${tableConfig.selectedTable}/${columnName}/values`); if (response.data.success && response.data.data && response.data.data.length > 0) { return response.data.data.map((item: any) => ({ + value: item.valueCode, + label: item.valueLabel, + })); + } + } catch { + // 카테고리 API 실패 시 다음 단계로 + } + + // 2단계: DISTINCT API (백엔드에서 category_values/code_info 라벨 변환 포함) + try { + const response = await apiClient.get(`/entity/${tableConfig.selectedTable}/distinct/${columnName}`); + if (response.data.success && response.data.data && response.data.data.length > 0) { + let options = response.data.data.map((item: any) => ({ value: String(item.value), label: String(item.label), })); + + // 프론트엔드 카테고리 매핑으로 추가 라벨 변환 + const mapping = categoryMappings[columnName]; + if (mapping && Object.keys(mapping).length > 0) { + options = options.map((opt) => ({ + value: opt.value, + label: mapping[opt.value]?.label || opt.label, + })); + } + + return options; } - } catch (error: any) { - // DISTINCT API 실패 시 현재 데이터 기반으로 fallback + } catch { + // DISTINCT API 실패 시 다음 단계로 } - // fallback: 현재 로드된 데이터에서 고유 값 추출 - const isLabelType = ["category", "entity", "code"].includes(inputType); - const labelField = isLabelType ? `${columnName}_name` : columnName; - + // 3단계: 현재 로드된 데이터에서 고유 값 추출 (최종 fallback) const uniqueValuesMap = new Map(); + const mapping = categoryMappings[columnName]; data.forEach((row) => { const value = row[columnName]; if (value !== null && value !== undefined && value !== "") { - const label = isLabelType && row[labelField] ? row[labelField] : String(value); - uniqueValuesMap.set(String(value), label); + const strValue = String(value); + // _name 필드 또는 카테고리 매핑에서 라벨 가져오기 + const nameField = row[`${columnName}_name`]; + const mappedLabel = mapping?.[strValue]?.label; + const label = mappedLabel || nameField || strValue; + uniqueValuesMap.set(strValue, label); } }); - const result = Array.from(uniqueValuesMap.entries()) - .map(([value, label]) => ({ - value: value, - label: label, - })) + return Array.from(uniqueValuesMap.entries()) + .map(([value, label]) => ({ value, label })) .sort((a, b) => a.label.localeCompare(b.label)); - - return result; }; const registration = { @@ -1150,6 +1148,7 @@ export const TableListComponent: React.FC = ({ tableConfig.columns, columnLabels, columnMeta, // columnMeta가 변경되면 재등록 (inputType 정보 필요) + categoryMappings, // 카테고리 매핑 변경 시 재등록 (필터 라벨 변환용) columnWidths, tableLabel, data, // 데이터 자체가 변경되면 재등록 (고유 값 조회용) diff --git a/frontend/lib/registry/components/v2-table-search-widget/TableSearchWidget.tsx b/frontend/lib/registry/components/v2-table-search-widget/TableSearchWidget.tsx index 39e888ab..63b01dd6 100644 --- a/frontend/lib/registry/components/v2-table-search-widget/TableSearchWidget.tsx +++ b/frontend/lib/registry/components/v2-table-search-widget/TableSearchWidget.tsx @@ -437,12 +437,14 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentTable?.tableName, filterMode, screenId, currentTableTabId, JSON.stringify(presetFilters)]); - // select 옵션 로드 (데이터 변경 시 빈 옵션 재조회) + // select 옵션 로드 (getColumnUniqueValues 변경 시 재로드 - columnMeta 갱신 반영) useEffect(() => { if (!currentTable?.getColumnUniqueValues || activeFilters.length === 0) { return; } + let cancelled = false; + const loadSelectOptions = async () => { const selectFilters = activeFilters.filter((f) => f.filterType === "select"); @@ -465,21 +467,14 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table } } - if (hasNewOptions) { - setSelectOptions((prev) => { - // 이미 로드된 옵션은 유지, 새로 로드된 옵션만 병합 - const merged = { ...prev }; - for (const [key, value] of Object.entries(loadedOptions)) { - if (!merged[key] || merged[key].length === 0) { - merged[key] = value; - } - } - return merged; - }); + if (!cancelled && hasNewOptions) { + setSelectOptions((prev) => ({ ...prev, ...loadedOptions })); } }; loadSelectOptions(); + + return () => { cancelled = true; }; }, [activeFilters, currentTable?.tableName, currentTable?.getColumnUniqueValues, currentTable?.dataCount]); // 높이 변화 감지 및 알림 (실제 화면에서만)