diff --git a/frontend/lib/registry/components/entity-search-input/EntitySearchInputComponent.tsx b/frontend/lib/registry/components/entity-search-input/EntitySearchInputComponent.tsx index 8bdd5758..70785171 100644 --- a/frontend/lib/registry/components/entity-search-input/EntitySearchInputComponent.tsx +++ b/frontend/lib/registry/components/entity-search-input/EntitySearchInputComponent.tsx @@ -52,7 +52,8 @@ export function EntitySearchInputComponent({ // 연쇄관계 설정 추출 (webTypeConfig 또는 component.componentConfig에서) const config = component?.componentConfig || {}; const effectiveCascadingRelationCode = cascadingRelationCode || config.cascadingRelationCode; - const effectiveParentFieldId = parentFieldId || config.parentFieldId; + // cascadingParentField: ConfigPanel에서 저장되는 필드명 + const effectiveParentFieldId = parentFieldId || config.cascadingParentField || config.parentFieldId; const effectiveCascadingRole = config.cascadingRole; // "parent" | "child" | undefined // 부모 역할이면 연쇄관계 로직 적용 안함 (자식만 부모 값에 따라 필터링됨) diff --git a/frontend/lib/registry/components/modal-repeater-table/ItemSelectionModal.tsx b/frontend/lib/registry/components/modal-repeater-table/ItemSelectionModal.tsx index 1eca9fab..7bf7a81d 100644 --- a/frontend/lib/registry/components/modal-repeater-table/ItemSelectionModal.tsx +++ b/frontend/lib/registry/components/modal-repeater-table/ItemSelectionModal.tsx @@ -17,6 +17,7 @@ import { Search, Loader2 } from "lucide-react"; import { useEntitySearch } from "../entity-search-input/useEntitySearch"; import { ItemSelectionModalProps, ModalFilterConfig } from "./types"; import { apiClient } from "@/lib/api/client"; +import { getCategoryLabelsByCodes } from "@/lib/api/tableCategoryValue"; export function ItemSelectionModal({ open, @@ -99,13 +100,44 @@ export function ItemSelectionModal({ } } - // 정렬 후 옵션으로 변환 + // 🆕 CATEGORY_ 코드가 있는지 확인하고 라벨 조회 + const allCodes = new Set(); + for (const val of uniqueValues) { + // 콤마로 구분된 다중 값도 처리 + const codes = val.split(",").map(c => c.trim()); + codes.forEach(code => { + if (code.startsWith("CATEGORY_")) { + allCodes.add(code); + } + }); + } + + // CATEGORY_ 코드가 있으면 라벨 조회 + let labelMap: Record = {}; + if (allCodes.size > 0) { + try { + const labelResponse = await getCategoryLabelsByCodes(Array.from(allCodes)); + if (labelResponse.success && labelResponse.data) { + labelMap = labelResponse.data; + } + } catch (labelError) { + console.error("카테고리 라벨 조회 실패:", labelError); + } + } + + // 정렬 후 옵션으로 변환 (라벨 적용) const options = Array.from(uniqueValues) .sort() - .map((val) => ({ - value: val, - label: val, - })); + .map((val) => { + // 콤마로 구분된 다중 값 처리 + if (val.includes(",")) { + const codes = val.split(",").map(c => c.trim()); + const labels = codes.map(code => labelMap[code] || code); + return { value: val, label: labels.join(", ") }; + } + // 단일 값 + return { value: val, label: labelMap[val] || val }; + }); setCategoryOptions((prev) => ({ ...prev, diff --git a/frontend/lib/registry/components/rack-structure/RackStructureComponent.tsx b/frontend/lib/registry/components/rack-structure/RackStructureComponent.tsx index d80fd2c7..77eadca0 100644 --- a/frontend/lib/registry/components/rack-structure/RackStructureComponent.tsx +++ b/frontend/lib/registry/components/rack-structure/RackStructureComponent.tsx @@ -605,7 +605,7 @@ export const RackStructureComponent: React.FC = ({ location_type: context?.locationType || "선반", status: context?.status || "사용", // 추가 필드 (테이블 컬럼명과 동일) - warehouse_id: context?.warehouseCode, + warehouse_code: context?.warehouseCode, warehouse_name: context?.warehouseName, floor: context?.floor, zone: context?.zone, @@ -623,6 +623,18 @@ export const RackStructureComponent: React.FC = ({ setPreviewData(locations); setIsPreviewGenerated(true); + + console.log("🏗️ [RackStructure] 생성된 위치 데이터:", { + locationsCount: locations.length, + firstLocation: locations[0], + context: { + warehouseCode: context?.warehouseCode, + warehouseName: context?.warehouseName, + floor: context?.floor, + zone: context?.zone, + }, + }); + onChange?.(locations); }, [conditions, context, generateLocationCode, onChange, missingFields, hasRowOverlap, duplicateErrors, existingLocations, rowOverlapErrors]); diff --git a/frontend/lib/registry/components/rack-structure/types.ts b/frontend/lib/registry/components/rack-structure/types.ts index 5ab7bd7e..8670d4a0 100644 --- a/frontend/lib/registry/components/rack-structure/types.ts +++ b/frontend/lib/registry/components/rack-structure/types.ts @@ -27,7 +27,7 @@ export interface GeneratedLocation { location_type?: string; // 위치 유형 status?: string; // 사용 여부 // 추가 필드 (상위 폼에서 매핑된 값) - warehouse_id?: string; // 창고 ID/코드 + warehouse_code?: string; // 창고 코드 (DB 컬럼명과 동일) warehouse_name?: string; // 창고명 floor?: string; // 층 zone?: string; // 구역 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) => { diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index 9b847ef3..6daf17e9 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -1375,17 +1375,17 @@ export class ButtonActionExecutor { // 저장 전 중복 체크 const firstLocation = locations[0]; - const warehouseId = firstLocation.warehouse_id || firstLocation.warehouseCode; + const warehouseCode = firstLocation.warehouse_code || firstLocation.warehouse_id || firstLocation.warehouseCode; const floor = firstLocation.floor; const zone = firstLocation.zone; - if (warehouseId && floor && zone) { - console.log("🔍 [handleRackStructureBatchSave] 기존 데이터 중복 체크:", { warehouseId, floor, zone }); + if (warehouseCode && floor && zone) { + console.log("🔍 [handleRackStructureBatchSave] 기존 데이터 중복 체크:", { warehouseCode, floor, zone }); try { const existingResponse = await DynamicFormApi.getTableData(tableName, { filters: { - warehouse_id: warehouseId, + warehouse_code: warehouseCode, floor: floor, zone: zone, }, @@ -1435,8 +1435,8 @@ export class ButtonActionExecutor { location_name: loc.location_name || loc.locationName, row_num: loc.row_num || String(loc.rowNum), level_num: loc.level_num || String(loc.levelNum), - // 창고 정보 (렉 구조 컴포넌트에서 전달) - warehouse_id: loc.warehouse_id || loc.warehouseCode, + // 창고 정보 (렉 구조 컴포넌트에서 전달) - DB 컬럼명은 warehouse_code + warehouse_code: loc.warehouse_code || loc.warehouse_id || loc.warehouseCode, warehouse_name: loc.warehouse_name || loc.warehouseName, // 위치 정보 (렉 구조 컴포넌트에서 전달) floor: loc.floor,