수정..
This commit is contained in:
parent
0450390b2a
commit
205d062f4a
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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" />
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue