Merge remote-tracking branch 'origin/main' into ksh
This commit is contained in:
commit
c365f06ed7
|
|
@ -52,7 +52,8 @@ export function EntitySearchInputComponent({
|
||||||
// 연쇄관계 설정 추출 (webTypeConfig 또는 component.componentConfig에서)
|
// 연쇄관계 설정 추출 (webTypeConfig 또는 component.componentConfig에서)
|
||||||
const config = component?.componentConfig || {};
|
const config = component?.componentConfig || {};
|
||||||
const effectiveCascadingRelationCode = cascadingRelationCode || config.cascadingRelationCode;
|
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
|
const effectiveCascadingRole = config.cascadingRole; // "parent" | "child" | undefined
|
||||||
|
|
||||||
// 부모 역할이면 연쇄관계 로직 적용 안함 (자식만 부모 값에 따라 필터링됨)
|
// 부모 역할이면 연쇄관계 로직 적용 안함 (자식만 부모 값에 따라 필터링됨)
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import { Search, Loader2 } from "lucide-react";
|
||||||
import { useEntitySearch } from "../entity-search-input/useEntitySearch";
|
import { useEntitySearch } from "../entity-search-input/useEntitySearch";
|
||||||
import { ItemSelectionModalProps, ModalFilterConfig } from "./types";
|
import { ItemSelectionModalProps, ModalFilterConfig } from "./types";
|
||||||
import { apiClient } from "@/lib/api/client";
|
import { apiClient } from "@/lib/api/client";
|
||||||
|
import { getCategoryLabelsByCodes } from "@/lib/api/tableCategoryValue";
|
||||||
|
|
||||||
export function ItemSelectionModal({
|
export function ItemSelectionModal({
|
||||||
open,
|
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)
|
const options = Array.from(uniqueValues)
|
||||||
.sort()
|
.sort()
|
||||||
.map((val) => ({
|
.map((val) => {
|
||||||
value: val,
|
// 콤마로 구분된 다중 값 처리
|
||||||
label: 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) => ({
|
setCategoryOptions((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
|
|
|
||||||
|
|
@ -605,7 +605,7 @@ export const RackStructureComponent: React.FC<RackStructureComponentProps> = ({
|
||||||
location_type: context?.locationType || "선반",
|
location_type: context?.locationType || "선반",
|
||||||
status: context?.status || "사용",
|
status: context?.status || "사용",
|
||||||
// 추가 필드 (테이블 컬럼명과 동일)
|
// 추가 필드 (테이블 컬럼명과 동일)
|
||||||
warehouse_id: context?.warehouseCode,
|
warehouse_code: context?.warehouseCode,
|
||||||
warehouse_name: context?.warehouseName,
|
warehouse_name: context?.warehouseName,
|
||||||
floor: context?.floor,
|
floor: context?.floor,
|
||||||
zone: context?.zone,
|
zone: context?.zone,
|
||||||
|
|
@ -623,6 +623,18 @@ export const RackStructureComponent: React.FC<RackStructureComponentProps> = ({
|
||||||
|
|
||||||
setPreviewData(locations);
|
setPreviewData(locations);
|
||||||
setIsPreviewGenerated(true);
|
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);
|
onChange?.(locations);
|
||||||
}, [conditions, context, generateLocationCode, onChange, missingFields, hasRowOverlap, duplicateErrors, existingLocations, rowOverlapErrors]);
|
}, [conditions, context, generateLocationCode, onChange, missingFields, hasRowOverlap, duplicateErrors, existingLocations, rowOverlapErrors]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ export interface GeneratedLocation {
|
||||||
location_type?: string; // 위치 유형
|
location_type?: string; // 위치 유형
|
||||||
status?: string; // 사용 여부
|
status?: string; // 사용 여부
|
||||||
// 추가 필드 (상위 폼에서 매핑된 값)
|
// 추가 필드 (상위 폼에서 매핑된 값)
|
||||||
warehouse_id?: string; // 창고 ID/코드
|
warehouse_code?: string; // 창고 코드 (DB 컬럼명과 동일)
|
||||||
warehouse_name?: string; // 창고명
|
warehouse_name?: string; // 창고명
|
||||||
floor?: string; // 층
|
floor?: string; // 층
|
||||||
zone?: string; // 구역
|
zone?: string; // 구역
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { WebType } from "@/types/common";
|
||||||
import { tableTypeApi } from "@/lib/api/screen";
|
import { tableTypeApi } from "@/lib/api/screen";
|
||||||
import { entityJoinApi } from "@/lib/api/entityJoin";
|
import { entityJoinApi } from "@/lib/api/entityJoin";
|
||||||
import { codeCache } from "@/lib/caching/codeCache";
|
import { codeCache } from "@/lib/caching/codeCache";
|
||||||
|
import { getCategoryLabelsByCodes } from "@/lib/api/tableCategoryValue";
|
||||||
import { useEntityJoinOptimization } from "@/lib/hooks/useEntityJoinOptimization";
|
import { useEntityJoinOptimization } from "@/lib/hooks/useEntityJoinOptimization";
|
||||||
import { getFullImageUrl } from "@/lib/api/client";
|
import { getFullImageUrl } from "@/lib/api/client";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
@ -471,6 +472,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 헤더 필터 적용 (joinColumnMapping 사용 안 함 - 직접 컬럼명 사용)
|
// 2. 헤더 필터 적용 (joinColumnMapping 사용 안 함 - 직접 컬럼명 사용)
|
||||||
|
// 🆕 다중 값 지원: 셀 값이 "A,B,C" 형태일 때, 필터에서 "A"를 선택하면 해당 행도 표시
|
||||||
if (Object.keys(headerFilters).length > 0) {
|
if (Object.keys(headerFilters).length > 0) {
|
||||||
result = result.filter((row) => {
|
result = result.filter((row) => {
|
||||||
return Object.entries(headerFilters).every(([columnName, values]) => {
|
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 cellValue = row[columnName] ?? row[columnName.toLowerCase()] ?? row[columnName.toUpperCase()];
|
||||||
const cellStr = cellValue !== null && cellValue !== undefined ? String(cellValue) : "";
|
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 정의 후)
|
// 🆕 편집 모드 진입 placeholder (실제 구현은 visibleColumns 정의 후)
|
||||||
const startEditingRef = useRef<() => void>(() => {});
|
const startEditingRef = useRef<() => void>(() => {});
|
||||||
|
|
||||||
|
// 🆕 카테고리 라벨 매핑 (API에서 가져온 것)
|
||||||
|
const [categoryLabelCache, setCategoryLabelCache] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
// 🆕 각 컬럼의 고유값 목록 계산 (라벨 포함)
|
// 🆕 각 컬럼의 고유값 목록 계산 (라벨 포함)
|
||||||
const columnUniqueValues = useMemo(() => {
|
const columnUniqueValues = useMemo(() => {
|
||||||
const result: Record<string, Array<{ value: string; label: string }>> = {};
|
const result: Record<string, Array<{ value: string; label: string }>> = {};
|
||||||
|
|
||||||
if (data.length === 0) return result;
|
if (data.length === 0) return result;
|
||||||
|
|
||||||
|
// 🆕 전체 데이터에서 개별 값 -> 라벨 매핑 테이블 구축 (다중 값 처리용)
|
||||||
|
const globalLabelMap: Record<string, Map<string, string>> = {};
|
||||||
|
|
||||||
(tableConfig.columns || []).forEach((column: { columnName: string }) => {
|
(tableConfig.columns || []).forEach((column: { columnName: string }) => {
|
||||||
if (column.columnName === "__checkbox__") return;
|
if (column.columnName === "__checkbox__") return;
|
||||||
|
|
||||||
|
|
@ -2265,23 +2282,70 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
`${column.columnName}_value_label`, // 예: division_value_label
|
`${column.columnName}_value_label`, // 예: division_value_label
|
||||||
];
|
];
|
||||||
const valuesMap = new Map<string, string>(); // value -> label
|
const valuesMap = new Map<string, string>(); // value -> label
|
||||||
|
const singleValueLabelMap = new Map<string, string>(); // 개별 값 -> 라벨 (다중값 처리용)
|
||||||
|
|
||||||
|
// 1차: 모든 데이터에서 개별 값 -> 라벨 매핑 수집 (단일값 + 다중값 모두)
|
||||||
data.forEach((row) => {
|
data.forEach((row) => {
|
||||||
const val = row[mappedColumnName];
|
const val = row[mappedColumnName];
|
||||||
if (val !== null && val !== undefined && val !== "") {
|
if (val !== null && val !== undefined && val !== "") {
|
||||||
const valueStr = String(val);
|
const valueStr = String(val);
|
||||||
// 라벨 컬럼 후보들 중 값이 있는 것 사용, 없으면 원본 값 사용
|
|
||||||
let label = valueStr;
|
// 라벨 컬럼에서 라벨 찾기
|
||||||
|
let labelStr = "";
|
||||||
for (const labelCol of labelColumnCandidates) {
|
for (const labelCol of labelColumnCandidates) {
|
||||||
if (row[labelCol] && row[labelCol] !== "") {
|
if (row[labelCol] && row[labelCol] !== "") {
|
||||||
label = String(row[labelCol]);
|
labelStr = String(row[labelCol]);
|
||||||
break;
|
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 쌍으로 저장하고 라벨 기준 정렬
|
// value-label 쌍으로 저장하고 라벨 기준 정렬
|
||||||
result[column.columnName] = Array.from(valuesMap.entries())
|
result[column.columnName] = Array.from(valuesMap.entries())
|
||||||
.map(([value, label]) => ({ value, label }))
|
.map(([value, label]) => ({ value, label }))
|
||||||
|
|
@ -2289,7 +2353,44 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
});
|
});
|
||||||
|
|
||||||
return result;
|
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) => {
|
const toggleHeaderFilter = useCallback((columnName: string, value: string) => {
|
||||||
|
|
|
||||||
|
|
@ -1375,17 +1375,17 @@ export class ButtonActionExecutor {
|
||||||
|
|
||||||
// 저장 전 중복 체크
|
// 저장 전 중복 체크
|
||||||
const firstLocation = locations[0];
|
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 floor = firstLocation.floor;
|
||||||
const zone = firstLocation.zone;
|
const zone = firstLocation.zone;
|
||||||
|
|
||||||
if (warehouseId && floor && zone) {
|
if (warehouseCode && floor && zone) {
|
||||||
console.log("🔍 [handleRackStructureBatchSave] 기존 데이터 중복 체크:", { warehouseId, floor, zone });
|
console.log("🔍 [handleRackStructureBatchSave] 기존 데이터 중복 체크:", { warehouseCode, floor, zone });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const existingResponse = await DynamicFormApi.getTableData(tableName, {
|
const existingResponse = await DynamicFormApi.getTableData(tableName, {
|
||||||
filters: {
|
filters: {
|
||||||
warehouse_id: warehouseId,
|
warehouse_code: warehouseCode,
|
||||||
floor: floor,
|
floor: floor,
|
||||||
zone: zone,
|
zone: zone,
|
||||||
},
|
},
|
||||||
|
|
@ -1435,8 +1435,8 @@ export class ButtonActionExecutor {
|
||||||
location_name: loc.location_name || loc.locationName,
|
location_name: loc.location_name || loc.locationName,
|
||||||
row_num: loc.row_num || String(loc.rowNum),
|
row_num: loc.row_num || String(loc.rowNum),
|
||||||
level_num: loc.level_num || String(loc.levelNum),
|
level_num: loc.level_num || String(loc.levelNum),
|
||||||
// 창고 정보 (렉 구조 컴포넌트에서 전달)
|
// 창고 정보 (렉 구조 컴포넌트에서 전달) - DB 컬럼명은 warehouse_code
|
||||||
warehouse_id: loc.warehouse_id || loc.warehouseCode,
|
warehouse_code: loc.warehouse_code || loc.warehouse_id || loc.warehouseCode,
|
||||||
warehouse_name: loc.warehouse_name || loc.warehouseName,
|
warehouse_name: loc.warehouse_name || loc.warehouseName,
|
||||||
// 위치 정보 (렉 구조 컴포넌트에서 전달)
|
// 위치 정보 (렉 구조 컴포넌트에서 전달)
|
||||||
floor: loc.floor,
|
floor: loc.floor,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue