Merge remote-tracking branch 'origin/main' into ksh

This commit is contained in:
SeongHyun Kim 2026-01-07 09:06:29 +09:00
commit c365f06ed7
6 changed files with 166 additions and 20 deletions

View File

@ -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
// 부모 역할이면 연쇄관계 로직 적용 안함 (자식만 부모 값에 따라 필터링됨)

View File

@ -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<string>();
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<string, string> = {};
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,

View File

@ -605,7 +605,7 @@ export const RackStructureComponent: React.FC<RackStructureComponentProps> = ({
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<RackStructureComponentProps> = ({
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]);

View File

@ -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; // 구역

View File

@ -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<TableListComponentProps> = ({
}
// 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<TableListComponentProps> = ({
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<TableListComponentProps> = ({
// 🆕 편집 모드 진입 placeholder (실제 구현은 visibleColumns 정의 후)
const startEditingRef = useRef<() => void>(() => {});
// 🆕 카테고리 라벨 매핑 (API에서 가져온 것)
const [categoryLabelCache, setCategoryLabelCache] = useState<Record<string, string>>({});
// 🆕 각 컬럼의 고유값 목록 계산 (라벨 포함)
const columnUniqueValues = useMemo(() => {
const result: Record<string, Array<{ value: string; label: string }>> = {};
if (data.length === 0) return result;
// 🆕 전체 데이터에서 개별 값 -> 라벨 매핑 테이블 구축 (다중 값 처리용)
const globalLabelMap: Record<string, Map<string, string>> = {};
(tableConfig.columns || []).forEach((column: { columnName: string }) => {
if (column.columnName === "__checkbox__") return;
@ -2265,22 +2282,69 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
`${column.columnName}_value_label`, // 예: division_value_label
];
const valuesMap = new Map<string, string>(); // value -> label
const singleValueLabelMap = new Map<string, string>(); // 개별 값 -> 라벨 (다중값 처리용)
// 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())
@ -2289,7 +2353,44 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
});
return result;
}, [data, tableConfig.columns, joinColumnMapping]);
}, [data, tableConfig.columns, joinColumnMapping, categoryLabelCache]);
// 🆕 라벨을 못 찾은 CATEGORY_ 코드들을 API로 조회
useEffect(() => {
const unlabeledCodes = new Set<string>();
// 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) => {

View File

@ -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,