This commit is contained in:
dohyeons 2025-11-21 03:33:49 +09:00
parent 0450390b2a
commit 205d062f4a
4 changed files with 289 additions and 248 deletions

View File

@ -782,7 +782,10 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
try { try {
setLoadingMaterials(true); setLoadingMaterials(true);
setShowMaterialPanel(true); setShowMaterialPanel(true);
const response = await getMaterials(selectedDbConnection, hierarchyConfig.material, locaKey); const response = await getMaterials(selectedDbConnection, {
...hierarchyConfig.material,
locaKey: locaKey,
});
if (response.success && response.data) { if (response.success && response.data) {
// layerColumn이 있으면 정렬 // layerColumn이 있으면 정렬
const sortedMaterials = hierarchyConfig.material.layerColumn const sortedMaterials = hierarchyConfig.material.layerColumn

View File

@ -34,7 +34,8 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps)
const [showInfoPanel, setShowInfoPanel] = useState(false); const [showInfoPanel, setShowInfoPanel] = useState(false);
const [externalDbConnectionId, setExternalDbConnectionId] = useState<number | null>(null); const [externalDbConnectionId, setExternalDbConnectionId] = useState<number | null>(null);
const [layoutName, setLayoutName] = useState<string>(""); const [layoutName, setLayoutName] = useState<string>("");
const [hierarchyConfig, setHierarchyConfig] = useState<any>(null);
// 검색 및 필터 // 검색 및 필터
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
const [filterType, setFilterType] = useState<string>("all"); const [filterType, setFilterType] = useState<string>("all");
@ -49,39 +50,51 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps)
if (response.success && response.data) { if (response.success && response.data) {
const { layout, objects } = response.data; const { layout, objects } = response.data;
// 레이아웃 정보 저장 // 레이아웃 정보 저장 (snake_case와 camelCase 모두 지원)
setLayoutName(layout.layoutName); setLayoutName(layout.layout_name || layout.layoutName);
setExternalDbConnectionId(layout.externalDbConnectionId); setExternalDbConnectionId(layout.external_db_connection_id || layout.externalDbConnectionId);
// hierarchy_config 저장
if (layout.hierarchy_config) {
const config =
typeof layout.hierarchy_config === "string"
? JSON.parse(layout.hierarchy_config)
: layout.hierarchy_config;
setHierarchyConfig(config);
}
// 객체 데이터 변환 // 객체 데이터 변환
const loadedObjects: PlacedObject[] = objects.map((obj: any) => ({ const loadedObjects: PlacedObject[] = objects.map((obj: any) => {
id: obj.id, const objectType = obj.object_type;
type: obj.object_type, return {
name: obj.object_name, id: obj.id,
position: { type: objectType,
x: parseFloat(obj.position_x), name: obj.object_name,
y: parseFloat(obj.position_y), position: {
z: parseFloat(obj.position_z), x: parseFloat(obj.position_x),
}, y: parseFloat(obj.position_y),
size: { z: parseFloat(obj.position_z),
x: parseFloat(obj.size_x), },
y: parseFloat(obj.size_y), size: {
z: parseFloat(obj.size_z), x: parseFloat(obj.size_x),
}, y: parseFloat(obj.size_y),
rotation: obj.rotation ? parseFloat(obj.rotation) : 0, z: parseFloat(obj.size_z),
color: obj.color, },
areaKey: obj.area_key, rotation: obj.rotation ? parseFloat(obj.rotation) : 0,
locaKey: obj.loca_key, color: getObjectColor(objectType), // 타입별 기본 색상 사용
locType: obj.loc_type, areaKey: obj.area_key,
materialCount: obj.material_count, locaKey: obj.loca_key,
materialPreview: obj.material_preview_height locType: obj.loc_type,
? { height: parseFloat(obj.material_preview_height) } materialCount: obj.material_count,
: undefined, materialPreview: obj.material_preview_height
parentId: obj.parent_id, ? { height: parseFloat(obj.material_preview_height) }
displayOrder: obj.display_order, : undefined,
locked: obj.locked, parentId: obj.parent_id,
visible: obj.visible !== false, displayOrder: obj.display_order,
})); locked: obj.locked,
visible: obj.visible !== false,
};
});
setPlacedObjects(loadedObjects); setPlacedObjects(loadedObjects);
} else { } else {
@ -101,16 +114,30 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps)
}; };
loadLayout(); loadLayout();
}, [layoutId, toast]); // eslint-disable-next-line react-hooks/exhaustive-deps
}, [layoutId]); // toast 제거 - 무한 루프 방지
// Location의 자재 목록 로드 // Location의 자재 목록 로드
const loadMaterialsForLocation = async (locaKey: string, externalDbConnectionId: number) => { const loadMaterialsForLocation = async (locaKey: string, externalDbConnectionId: number) => {
if (!hierarchyConfig?.material) {
console.warn("hierarchyConfig.material이 없습니다. 자재 로드를 건너뜁니다.");
return;
}
try { try {
setLoadingMaterials(true); setLoadingMaterials(true);
setShowInfoPanel(true); setShowInfoPanel(true);
const response = await getMaterials(externalDbConnectionId, locaKey);
const response = await getMaterials(externalDbConnectionId, {
tableName: hierarchyConfig.material.tableName,
keyColumn: hierarchyConfig.material.keyColumn,
locationKeyColumn: hierarchyConfig.material.locationKeyColumn,
layerColumn: hierarchyConfig.material.layerColumn,
locaKey: locaKey,
});
if (response.success && response.data) { if (response.success && response.data) {
const sortedMaterials = response.data.sort((a, b) => a.LOLAYER - b.LOLAYER); const layerColumn = hierarchyConfig.material.layerColumn || "LOLAYER";
const sortedMaterials = response.data.sort((a: any, b: any) => (a[layerColumn] || 0) - (b[layerColumn] || 0));
setMaterials(sortedMaterials); setMaterials(sortedMaterials);
} else { } else {
setMaterials([]); setMaterials([]);
@ -196,6 +223,49 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps)
}); });
}, [placedObjects, filterType, searchQuery]); }, [placedObjects, filterType, searchQuery]);
// 객체 타입별 기본 색상 (useMemo로 최적화)
const getObjectColor = useMemo(() => {
return (type: string): string => {
const colorMap: Record<string, string> = {
area: "#3b82f6", // 파란색
"location-bed": "#2563eb", // 진한 파란색
"location-stp": "#6b7280", // 회색
"location-temp": "#f59e0b", // 주황색
"location-dest": "#10b981", // 초록색
"crane-mobile": "#8b5cf6", // 보라색
rack: "#ef4444", // 빨간색
};
return colorMap[type] || "#3b82f6";
};
}, []);
// 3D 캔버스용 placements 변환 (useMemo로 최적화)
const canvasPlacements = useMemo(() => {
return placedObjects.map((obj) => ({
id: obj.id,
name: obj.name,
position_x: obj.position.x,
position_y: obj.position.y,
position_z: obj.position.z,
size_x: obj.size.x,
size_y: obj.size.y,
size_z: obj.size.z,
color: obj.color,
data_source_type: obj.type,
material_count: obj.materialCount,
material_preview_height: obj.materialPreview?.height,
yard_layout_id: undefined,
material_code: null,
material_name: null,
quantity: null,
unit: null,
data_source_config: undefined,
data_binding: undefined,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
}));
}, [placedObjects]);
if (isLoading) { if (isLoading) {
return ( return (
<div className="flex h-full items-center justify-center"> <div className="flex h-full items-center justify-center">
@ -217,13 +287,13 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps)
{/* 메인 영역 */} {/* 메인 영역 */}
<div className="flex flex-1 overflow-hidden"> <div className="flex flex-1 overflow-hidden">
{/* 좌측: 검색/필터 */} {/* 좌측: 검색/필터 */}
<div className="flex h-full w-[20%] flex-col border-r"> <div className="flex h-full w-64 flex-shrink-0 flex-col border-r">
<div className="space-y-4 p-4"> <div className="space-y-4 p-4">
{/* 검색 */} {/* 검색 */}
<div> <div>
<Label className="mb-2 block text-sm font-semibold"></Label> <Label className="mb-2 block text-sm font-semibold"></Label>
<div className="relative"> <div className="relative">
<Search className="text-muted-foreground absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2" /> <Search className="text-muted-foreground absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2" />
<Input <Input
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
@ -234,7 +304,7 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps)
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
className="absolute right-1 top-1/2 h-7 w-7 -translate-y-1/2 p-0" className="absolute top-1/2 right-1 h-7 w-7 -translate-y-1/2 p-0"
onClick={() => setSearchQuery("")} onClick={() => setSearchQuery("")}
> >
<X className="h-3 w-3" /> <X className="h-3 w-3" />
@ -281,9 +351,7 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps)
{/* 객체 목록 */} {/* 객체 목록 */}
<div className="flex-1 overflow-y-auto border-t p-4"> <div className="flex-1 overflow-y-auto border-t p-4">
<Label className="mb-2 block text-sm font-semibold"> <Label className="mb-2 block text-sm font-semibold"> ({filteredObjects.length})</Label>
({filteredObjects.length})
</Label>
{filteredObjects.length === 0 ? ( {filteredObjects.length === 0 ? (
<div className="text-muted-foreground flex h-32 items-center justify-center text-center text-sm"> <div className="text-muted-foreground flex h-32 items-center justify-center text-center text-sm">
{searchQuery ? "검색 결과가 없습니다" : "객체가 없습니다"} {searchQuery ? "검색 결과가 없습니다" : "객체가 없습니다"}
@ -306,9 +374,7 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps)
key={obj.id} key={obj.id}
onClick={() => handleObjectClick(obj.id)} onClick={() => handleObjectClick(obj.id)}
className={`bg-background hover:bg-accent cursor-pointer rounded-lg border p-3 transition-all ${ className={`bg-background hover:bg-accent cursor-pointer rounded-lg border p-3 transition-all ${
selectedObject?.id === obj.id selectedObject?.id === obj.id ? "ring-primary bg-primary/5 ring-2" : "hover:shadow-sm"
? "ring-primary bg-primary/5 ring-2"
: "hover:shadow-sm"
}`} }`}
> >
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
@ -317,13 +383,13 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps)
<div className="text-muted-foreground mt-1 flex items-center gap-2 text-xs"> <div className="text-muted-foreground mt-1 flex items-center gap-2 text-xs">
<span <span
className="inline-block h-2 w-2 rounded-full" className="inline-block h-2 w-2 rounded-full"
style={{ backgroundColor: obj.color }} style={{ backgroundColor: getObjectColor(obj.type) }}
/> />
<span>{typeLabel}</span> <span>{typeLabel}</span>
</div> </div>
</div> </div>
</div> </div>
{/* 추가 정보 */} {/* 추가 정보 */}
<div className="mt-2 space-y-1"> <div className="mt-2 space-y-1">
{obj.areaKey && ( {obj.areaKey && (
@ -354,33 +420,7 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps)
<div className="relative flex-1"> <div className="relative flex-1">
{!isLoading && ( {!isLoading && (
<Yard3DCanvas <Yard3DCanvas
placements={useMemo( placements={canvasPlacements}
() =>
placedObjects.map((obj) => ({
id: obj.id,
name: obj.name,
position_x: obj.position.x,
position_y: obj.position.y,
position_z: obj.position.z,
size_x: obj.size.x,
size_y: obj.size.y,
size_z: obj.size.z,
color: obj.color,
data_source_type: obj.type,
material_count: obj.materialCount,
material_preview_height: obj.materialPreview?.height,
yard_layout_id: undefined,
material_code: null,
material_name: null,
quantity: null,
unit: null,
data_source_config: undefined,
data_binding: undefined,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
})),
[placedObjects],
)}
selectedPlacementId={selectedObject?.id || null} selectedPlacementId={selectedObject?.id || null}
onPlacementClick={(placement) => handleObjectClick(placement?.id || null)} onPlacementClick={(placement) => handleObjectClick(placement?.id || null)}
focusOnPlacementId={null} focusOnPlacementId={null}
@ -390,17 +430,12 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps)
</div> </div>
{/* 우측: 정보 패널 */} {/* 우측: 정보 패널 */}
{showInfoPanel && selectedObject && ( <div className="h-full w-[480px] flex-shrink-0 overflow-y-auto border-l">
<div className="h-full w-[25%] overflow-y-auto border-l"> {selectedObject ? (
<div className="p-4"> <div className="p-4">
<div className="mb-4 flex items-center justify-between"> <div className="mb-4">
<div> <h3 className="text-lg font-semibold"> </h3>
<h3 className="text-lg font-semibold"> </h3> <p className="text-muted-foreground text-xs">{selectedObject.name}</p>
<p className="text-muted-foreground text-xs">{selectedObject.name}</p>
</div>
<Button variant="ghost" size="sm" onClick={() => setShowInfoPanel(false)}>
<X className="h-4 w-4" />
</Button>
</div> </div>
{/* 기본 정보 */} {/* 기본 정보 */}
@ -429,72 +464,74 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps)
)} )}
</div> </div>
{/* 자재 목록 (Location인 경우) */} {/* 자재 목록 (Location인 경우) - 아코디언 */}
{(selectedObject.type === "location-bed" || {(selectedObject.type === "location-bed" ||
selectedObject.type === "location-stp" || selectedObject.type === "location-stp" ||
selectedObject.type === "location-temp" || selectedObject.type === "location-temp" ||
selectedObject.type === "location-dest") && ( selectedObject.type === "location-dest") && (
<div className="mt-4"> <div className="mt-4">
<Label className="mb-2 block text-sm font-semibold"> </Label>
{loadingMaterials ? ( {loadingMaterials ? (
<div className="flex h-32 items-center justify-center"> <div className="flex h-32 items-center justify-center">
<Loader2 className="text-muted-foreground h-6 w-6 animate-spin" /> <Loader2 className="text-muted-foreground h-6 w-6 animate-spin" />
</div> </div>
) : materials.length === 0 ? ( ) : materials.length === 0 ? (
<div className="text-muted-foreground flex h-32 items-center justify-center text-center text-sm"> <div className="text-muted-foreground flex h-32 items-center justify-center text-center text-sm">
{externalDbConnectionId {externalDbConnectionId ? "자재가 없습니다" : "외부 DB 연결이 설정되지 않았습니다"}
? "자재가 없습니다"
: "외부 DB 연결이 설정되지 않았습니다"}
</div> </div>
) : ( ) : (
<div className="space-y-2"> <div className="space-y-2">
{materials.map((material, index) => ( <Label className="mb-2 block text-sm font-semibold"> ({materials.length})</Label>
<div {materials.map((material, index) => {
key={`${material.STKKEY}-${index}`} const displayColumns = hierarchyConfig?.material?.displayColumns || [];
className="bg-muted hover:bg-accent rounded-lg border p-3 transition-colors" return (
> <details
<div className="mb-2 flex items-start justify-between"> key={`${material.STKKEY}-${index}`}
<div className="flex-1"> className="bg-muted group hover:bg-accent rounded-lg border transition-colors"
<p className="text-sm font-medium">{material.STKKEY}</p> >
<p className="text-muted-foreground mt-0.5 text-xs"> <summary className="flex cursor-pointer items-center justify-between p-3 text-sm font-medium">
: {material.LOLAYER} | Area: {material.AREAKEY} <div className="flex-1">
</p> <div className="flex items-center gap-2">
<span className="font-semibold">
{material[hierarchyConfig?.material?.layerColumn || "LOLAYER"]}
</span>
{displayColumns[0] && (
<span className="text-muted-foreground text-xs">
{material[displayColumns[0].column]}
</span>
)}
</div>
</div>
<svg
className="text-muted-foreground h-4 w-4 transition-transform group-open:rotate-180"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</summary>
<div className="space-y-2 border-t p-3 pt-3">
{displayColumns.map((colConfig: any) => (
<div key={colConfig.column} className="flex justify-between text-xs">
<span className="text-muted-foreground">{colConfig.label}:</span>
<span className="font-medium">{material[colConfig.column] || "-"}</span>
</div>
))}
</div> </div>
</div> </details>
<div className="text-muted-foreground grid grid-cols-2 gap-2 text-xs"> );
{material.STKWIDT && ( })}
<div>
: <span className="font-medium">{material.STKWIDT}</span>
</div>
)}
{material.STKLENG && (
<div>
: <span className="font-medium">{material.STKLENG}</span>
</div>
)}
{material.STKHEIG && (
<div>
: <span className="font-medium">{material.STKHEIG}</span>
</div>
)}
{material.STKWEIG && (
<div>
: <span className="font-medium">{material.STKWEIG}</span>
</div>
)}
</div>
{material.STKRMKS && (
<p className="text-muted-foreground mt-2 text-xs italic">{material.STKRMKS}</p>
)}
</div>
))}
</div> </div>
)} )}
</div> </div>
)} )}
</div> </div>
</div> ) : (
)} <div className="flex h-full items-center justify-center p-4">
<p className="text-muted-foreground text-sm"> </p>
</div>
)}
</div>
</div> </div>
</div> </div>
); );

View File

@ -297,42 +297,42 @@ export default function HierarchyConfigPanel({
{level.tableName && columnsCache[level.tableName] && ( {level.tableName && columnsCache[level.tableName] && (
<> <>
<div> <div>
<Label className="text-[10px]">ID </Label> <Label className="text-[10px]">ID </Label>
<Select <Select
value={level.keyColumn} value={level.keyColumn}
onValueChange={(val) => handleLevelChange(level.level, "keyColumn", val)} onValueChange={(val) => handleLevelChange(level.level, "keyColumn", val)}
> >
<SelectTrigger className="h-7 text-[10px]"> <SelectTrigger className="h-7 text-[10px]">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{columnsCache[level.tableName].map((col) => ( {columnsCache[level.tableName].map((col) => (
<SelectItem key={col} value={col} className="text-xs"> <SelectItem key={col} value={col} className="text-xs">
{col} {col}
</SelectItem> </SelectItem>
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
<div> <div>
<Label className="text-[10px]"> </Label> <Label className="text-[10px]"> </Label>
<Select <Select
value={level.nameColumn} value={level.nameColumn}
onValueChange={(val) => handleLevelChange(level.level, "nameColumn", val)} onValueChange={(val) => handleLevelChange(level.level, "nameColumn", val)}
> >
<SelectTrigger className="h-7 text-[10px]"> <SelectTrigger className="h-7 text-[10px]">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{columnsCache[level.tableName].map((col) => ( {columnsCache[level.tableName].map((col) => (
<SelectItem key={col} value={col} className="text-xs"> <SelectItem key={col} value={col} className="text-xs">
{col} {col}
</SelectItem> </SelectItem>
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
<div> <div>
@ -419,82 +419,82 @@ export default function HierarchyConfigPanel({
{localConfig.material?.tableName && columnsCache[localConfig.material.tableName] && ( {localConfig.material?.tableName && columnsCache[localConfig.material.tableName] && (
<> <>
<div> <div>
<Label className="text-[10px]">ID </Label> <Label className="text-[10px]">ID </Label>
<Select <Select
value={localConfig.material.keyColumn} value={localConfig.material.keyColumn}
onValueChange={(val) => handleMaterialChange("keyColumn", val)} onValueChange={(val) => handleMaterialChange("keyColumn", val)}
> >
<SelectTrigger className="h-7 text-[10px]"> <SelectTrigger className="h-7 text-[10px]">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{columnsCache[localConfig.material.tableName].map((col) => ( {columnsCache[localConfig.material.tableName].map((col) => (
<SelectItem key={col} value={col} className="text-xs"> <SelectItem key={col} value={col} className="text-xs">
{col} {col}
</SelectItem> </SelectItem>
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
</div>
<div>
<Label className="text-[10px]"> </Label>
<Select
value={localConfig.material.locationKeyColumn}
onValueChange={(val) => handleMaterialChange("locationKeyColumn", val)}
>
<SelectTrigger className="h-7 text-[10px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
{columnsCache[localConfig.material.tableName].map((col) => (
<SelectItem key={col} value={col} className="text-xs">
{col}
</SelectItem>
))}
</SelectContent>
</Select>
</div> </div>
<div> <div>
<Label className="text-[10px]"> </Label> <Label className="text-[10px]"> ()</Label>
<Select <Select
value={localConfig.material.locationKeyColumn}
onValueChange={(val) => handleMaterialChange("locationKeyColumn", val)}
>
<SelectTrigger className="h-7 text-[10px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
{columnsCache[localConfig.material.tableName].map((col) => (
<SelectItem key={col} value={col} className="text-xs">
{col}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label className="text-[10px]"> ()</Label>
<Select
value={localConfig.material.layerColumn || "__none__"} value={localConfig.material.layerColumn || "__none__"}
onValueChange={(val) => handleMaterialChange("layerColumn", val === "__none__" ? undefined : val)} onValueChange={(val) => handleMaterialChange("layerColumn", val === "__none__" ? undefined : val)}
> >
<SelectTrigger className="h-7 text-[10px]"> <SelectTrigger className="h-7 text-[10px]">
<SelectValue placeholder="레이어 컬럼" /> <SelectValue placeholder="레이어 컬럼" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="__none__"></SelectItem> <SelectItem value="__none__"></SelectItem>
{columnsCache[localConfig.material.tableName].map((col) => ( {columnsCache[localConfig.material.tableName].map((col) => (
<SelectItem key={col} value={col} className="text-xs"> <SelectItem key={col} value={col} className="text-xs">
{col} {col}
</SelectItem> </SelectItem>
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
<div> <div>
<Label className="text-[10px]"> ()</Label> <Label className="text-[10px]"> ()</Label>
<Select <Select
value={localConfig.material.quantityColumn || "__none__"} value={localConfig.material.quantityColumn || "__none__"}
onValueChange={(val) => handleMaterialChange("quantityColumn", val === "__none__" ? undefined : val)} onValueChange={(val) => handleMaterialChange("quantityColumn", val === "__none__" ? undefined : val)}
> >
<SelectTrigger className="h-7 text-[10px]"> <SelectTrigger className="h-7 text-[10px]">
<SelectValue placeholder="수량 컬럼" /> <SelectValue placeholder="수량 컬럼" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="__none__"></SelectItem> <SelectItem value="__none__"></SelectItem>
{columnsCache[localConfig.material.tableName].map((col) => ( {columnsCache[localConfig.material.tableName].map((col) => (
<SelectItem key={col} value={col} className="text-xs"> <SelectItem key={col} value={col} className="text-xs">
{col} {col}
</SelectItem> </SelectItem>
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
<Separator className="my-3" /> <Separator className="my-3" />

View File

@ -91,9 +91,7 @@ export const deleteLayout = async (id: number): Promise<ApiResponse<void>> => {
// ========== 외부 DB 테이블 조회 API ========== // ========== 외부 DB 테이블 조회 API ==========
export const getTables = async ( export const getTables = async (connectionId: number): Promise<ApiResponse<Array<{ table_name: string }>>> => {
connectionId: number
): Promise<ApiResponse<Array<{ table_name: string }>>> => {
try { try {
const response = await apiClient.get(`/digital-twin/data/tables/${connectionId}`); const response = await apiClient.get(`/digital-twin/data/tables/${connectionId}`);
return response.data; return response.data;
@ -105,10 +103,7 @@ export const getTables = async (
} }
}; };
export const getTablePreview = async ( export const getTablePreview = async (connectionId: number, tableName: string): Promise<ApiResponse<any[]>> => {
connectionId: number,
tableName: string
): Promise<ApiResponse<any[]>> => {
try { try {
const response = await apiClient.get(`/digital-twin/data/table-preview/${connectionId}/${tableName}`); const response = await apiClient.get(`/digital-twin/data/table-preview/${connectionId}/${tableName}`);
return response.data; return response.data;
@ -123,7 +118,10 @@ export const getTablePreview = async (
// ========== 외부 DB 데이터 조회 API ========== // ========== 외부 DB 데이터 조회 API ==========
// 창고 목록 조회 // 창고 목록 조회
export const getWarehouses = async (externalDbConnectionId: number, tableName: string): Promise<ApiResponse<Warehouse[]>> => { export const getWarehouses = async (
externalDbConnectionId: number,
tableName: string,
): Promise<ApiResponse<Warehouse[]>> => {
try { try {
const response = await apiClient.get("/digital-twin/data/warehouses", { const response = await apiClient.get("/digital-twin/data/warehouses", {
params: { externalDbConnectionId, tableName }, params: { externalDbConnectionId, tableName },
@ -138,7 +136,11 @@ export const getWarehouses = async (externalDbConnectionId: number, tableName: s
}; };
// Area 목록 조회 // Area 목록 조회
export const getAreas = async (externalDbConnectionId: number, tableName: string, warehouseKey: string): Promise<ApiResponse<Area[]>> => { export const getAreas = async (
externalDbConnectionId: number,
tableName: string,
warehouseKey: string,
): Promise<ApiResponse<Area[]>> => {
try { try {
const response = await apiClient.get("/digital-twin/data/areas", { const response = await apiClient.get("/digital-twin/data/areas", {
params: { externalDbConnectionId, tableName, warehouseKey }, params: { externalDbConnectionId, tableName, warehouseKey },
@ -179,18 +181,18 @@ export const getMaterials = async (
keyColumn: string; keyColumn: string;
locationKeyColumn: string; locationKeyColumn: string;
layerColumn?: string; layerColumn?: string;
locaKey: string;
}, },
locaKey: string,
): Promise<ApiResponse<MaterialData[]>> => { ): Promise<ApiResponse<MaterialData[]>> => {
try { try {
const response = await apiClient.get("/digital-twin/data/materials", { const response = await apiClient.get("/digital-twin/data/materials", {
params: { params: {
externalDbConnectionId, externalDbConnectionId,
tableName: materialConfig.tableName, tableName: materialConfig.tableName,
keyColumn: materialConfig.keyColumn, keyColumn: materialConfig.keyColumn,
locationKeyColumn: materialConfig.locationKeyColumn, locationKeyColumn: materialConfig.locationKeyColumn,
layerColumn: materialConfig.layerColumn, layerColumn: materialConfig.layerColumn,
locaKey locaKey: materialConfig.locaKey,
}, },
}); });
return response.data; return response.data;
@ -241,7 +243,7 @@ export interface HierarchyData {
// 전체 계층 데이터 조회 // 전체 계층 데이터 조회
export const getHierarchyData = async ( export const getHierarchyData = async (
externalDbConnectionId: number, externalDbConnectionId: number,
hierarchyConfig: any hierarchyConfig: any,
): Promise<ApiResponse<HierarchyData>> => { ): Promise<ApiResponse<HierarchyData>> => {
try { try {
const response = await apiClient.post("/digital-twin/data/hierarchy", { const response = await apiClient.post("/digital-twin/data/hierarchy", {
@ -262,7 +264,7 @@ export const getChildrenData = async (
externalDbConnectionId: number, externalDbConnectionId: number,
hierarchyConfig: any, hierarchyConfig: any,
parentLevel: number, parentLevel: number,
parentKey: string parentKey: string,
): Promise<ApiResponse<any[]>> => { ): Promise<ApiResponse<any[]>> => {
try { try {
const response = await apiClient.post("/digital-twin/data/children", { const response = await apiClient.post("/digital-twin/data/children", {
@ -279,4 +281,3 @@ export const getChildrenData = async (
}; };
} }
}; };