diff --git a/frontend/components/admin/dashboard/widgets/yard-3d/DigitalTwinEditor.tsx b/frontend/components/admin/dashboard/widgets/yard-3d/DigitalTwinEditor.tsx index 7c5ad29f..88e844d3 100644 --- a/frontend/components/admin/dashboard/widgets/yard-3d/DigitalTwinEditor.tsx +++ b/frontend/components/admin/dashboard/widgets/yard-3d/DigitalTwinEditor.tsx @@ -782,7 +782,10 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi try { setLoadingMaterials(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) { // layerColumn이 있으면 정렬 const sortedMaterials = hierarchyConfig.material.layerColumn diff --git a/frontend/components/admin/dashboard/widgets/yard-3d/DigitalTwinViewer.tsx b/frontend/components/admin/dashboard/widgets/yard-3d/DigitalTwinViewer.tsx index d8162e31..3945a692 100644 --- a/frontend/components/admin/dashboard/widgets/yard-3d/DigitalTwinViewer.tsx +++ b/frontend/components/admin/dashboard/widgets/yard-3d/DigitalTwinViewer.tsx @@ -34,7 +34,8 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps) const [showInfoPanel, setShowInfoPanel] = useState(false); const [externalDbConnectionId, setExternalDbConnectionId] = useState(null); const [layoutName, setLayoutName] = useState(""); - + const [hierarchyConfig, setHierarchyConfig] = useState(null); + // 검색 및 필터 const [searchQuery, setSearchQuery] = useState(""); const [filterType, setFilterType] = useState("all"); @@ -49,39 +50,51 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps) if (response.success && response.data) { const { layout, objects } = response.data; - // 레이아웃 정보 저장 - setLayoutName(layout.layoutName); - setExternalDbConnectionId(layout.externalDbConnectionId); + // 레이아웃 정보 저장 (snake_case와 camelCase 모두 지원) + setLayoutName(layout.layout_name || layout.layoutName); + 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) => ({ - id: obj.id, - type: obj.object_type, - name: obj.object_name, - position: { - x: parseFloat(obj.position_x), - y: parseFloat(obj.position_y), - z: parseFloat(obj.position_z), - }, - size: { - x: parseFloat(obj.size_x), - y: parseFloat(obj.size_y), - z: parseFloat(obj.size_z), - }, - rotation: obj.rotation ? parseFloat(obj.rotation) : 0, - color: obj.color, - areaKey: obj.area_key, - locaKey: obj.loca_key, - locType: obj.loc_type, - materialCount: obj.material_count, - materialPreview: obj.material_preview_height - ? { height: parseFloat(obj.material_preview_height) } - : undefined, - parentId: obj.parent_id, - displayOrder: obj.display_order, - locked: obj.locked, - visible: obj.visible !== false, - })); + const loadedObjects: PlacedObject[] = objects.map((obj: any) => { + const objectType = obj.object_type; + return { + id: obj.id, + type: objectType, + name: obj.object_name, + position: { + x: parseFloat(obj.position_x), + y: parseFloat(obj.position_y), + z: parseFloat(obj.position_z), + }, + size: { + x: parseFloat(obj.size_x), + y: parseFloat(obj.size_y), + z: parseFloat(obj.size_z), + }, + rotation: obj.rotation ? parseFloat(obj.rotation) : 0, + color: getObjectColor(objectType), // 타입별 기본 색상 사용 + areaKey: obj.area_key, + locaKey: obj.loca_key, + locType: obj.loc_type, + materialCount: obj.material_count, + materialPreview: obj.material_preview_height + ? { height: parseFloat(obj.material_preview_height) } + : undefined, + parentId: obj.parent_id, + displayOrder: obj.display_order, + locked: obj.locked, + visible: obj.visible !== false, + }; + }); setPlacedObjects(loadedObjects); } else { @@ -101,16 +114,30 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps) }; loadLayout(); - }, [layoutId, toast]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [layoutId]); // toast 제거 - 무한 루프 방지 // Location의 자재 목록 로드 const loadMaterialsForLocation = async (locaKey: string, externalDbConnectionId: number) => { + if (!hierarchyConfig?.material) { + console.warn("hierarchyConfig.material이 없습니다. 자재 로드를 건너뜁니다."); + return; + } + try { setLoadingMaterials(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) { - 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); } else { setMaterials([]); @@ -196,6 +223,49 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps) }); }, [placedObjects, filterType, searchQuery]); + // 객체 타입별 기본 색상 (useMemo로 최적화) + const getObjectColor = useMemo(() => { + return (type: string): string => { + const colorMap: Record = { + 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) { return (
@@ -217,13 +287,13 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps) {/* 메인 영역 */}
{/* 좌측: 검색/필터 */} -
+
{/* 검색 */}
- + setSearchQuery(e.target.value)} @@ -234,7 +304,7 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps) +
+

상세 정보

+

{selectedObject.name}

{/* 기본 정보 */} @@ -429,72 +464,74 @@ export default function DigitalTwinViewer({ layoutId }: DigitalTwinViewerProps) )}
- {/* 자재 목록 (Location인 경우) */} + {/* 자재 목록 (Location인 경우) - 아코디언 */} {(selectedObject.type === "location-bed" || selectedObject.type === "location-stp" || selectedObject.type === "location-temp" || selectedObject.type === "location-dest") && (
- {loadingMaterials ? (
) : materials.length === 0 ? (
- {externalDbConnectionId - ? "자재가 없습니다" - : "외부 DB 연결이 설정되지 않았습니다"} + {externalDbConnectionId ? "자재가 없습니다" : "외부 DB 연결이 설정되지 않았습니다"}
) : (
- {materials.map((material, index) => ( -
-
-
-

{material.STKKEY}

-

- 층: {material.LOLAYER} | Area: {material.AREAKEY} -

+ + {materials.map((material, index) => { + const displayColumns = hierarchyConfig?.material?.displayColumns || []; + return ( +
+ +
+
+ + 층 {material[hierarchyConfig?.material?.layerColumn || "LOLAYER"]} + + {displayColumns[0] && ( + + {material[displayColumns[0].column]} + + )} +
+
+ + + +
+
+ {displayColumns.map((colConfig: any) => ( +
+ {colConfig.label}: + {material[colConfig.column] || "-"} +
+ ))}
-
-
- {material.STKWIDT && ( -
- 폭: {material.STKWIDT} -
- )} - {material.STKLENG && ( -
- 길이: {material.STKLENG} -
- )} - {material.STKHEIG && ( -
- 높이: {material.STKHEIG} -
- )} - {material.STKWEIG && ( -
- 무게: {material.STKWEIG} -
- )} -
- {material.STKRMKS && ( -

{material.STKRMKS}

- )} -
- ))} + + ); + })}
)}
)}
-
- )} + ) : ( +
+

객체를 선택하세요

+
+ )} +
); diff --git a/frontend/components/admin/dashboard/widgets/yard-3d/HierarchyConfigPanel.tsx b/frontend/components/admin/dashboard/widgets/yard-3d/HierarchyConfigPanel.tsx index 30654fb8..8a6f4bfd 100644 --- a/frontend/components/admin/dashboard/widgets/yard-3d/HierarchyConfigPanel.tsx +++ b/frontend/components/admin/dashboard/widgets/yard-3d/HierarchyConfigPanel.tsx @@ -297,42 +297,42 @@ export default function HierarchyConfigPanel({ {level.tableName && columnsCache[level.tableName] && ( <> -
- - -
+
+ + +
-
- - +
+ +
@@ -419,82 +419,82 @@ export default function HierarchyConfigPanel({ {localConfig.material?.tableName && columnsCache[localConfig.material.tableName] && ( <> -
- - +
+ + +
+ +
+ +
-
- - -
- -
- - handleMaterialChange("layerColumn", val === "__none__" ? undefined : val)} - > - - - - + > + + + + 없음 - {columnsCache[localConfig.material.tableName].map((col) => ( - - {col} - - ))} - - -
+ {columnsCache[localConfig.material.tableName].map((col) => ( + + {col} + + ))} + + +
-
- - handleMaterialChange("quantityColumn", val === "__none__" ? undefined : val)} - > - - - - + > + + + + 없음 - {columnsCache[localConfig.material.tableName].map((col) => ( - - {col} - - ))} - - + {columnsCache[localConfig.material.tableName].map((col) => ( + + {col} + + ))} + +
diff --git a/frontend/lib/api/digitalTwin.ts b/frontend/lib/api/digitalTwin.ts index 968fc9c4..c20525b4 100644 --- a/frontend/lib/api/digitalTwin.ts +++ b/frontend/lib/api/digitalTwin.ts @@ -91,9 +91,7 @@ export const deleteLayout = async (id: number): Promise> => { // ========== 외부 DB 테이블 조회 API ========== -export const getTables = async ( - connectionId: number -): Promise>> => { +export const getTables = async (connectionId: number): Promise>> => { try { const response = await apiClient.get(`/digital-twin/data/tables/${connectionId}`); return response.data; @@ -105,10 +103,7 @@ export const getTables = async ( } }; -export const getTablePreview = async ( - connectionId: number, - tableName: string -): Promise> => { +export const getTablePreview = async (connectionId: number, tableName: string): Promise> => { try { const response = await apiClient.get(`/digital-twin/data/table-preview/${connectionId}/${tableName}`); return response.data; @@ -123,7 +118,10 @@ export const getTablePreview = async ( // ========== 외부 DB 데이터 조회 API ========== // 창고 목록 조회 -export const getWarehouses = async (externalDbConnectionId: number, tableName: string): Promise> => { +export const getWarehouses = async ( + externalDbConnectionId: number, + tableName: string, +): Promise> => { try { const response = await apiClient.get("/digital-twin/data/warehouses", { params: { externalDbConnectionId, tableName }, @@ -138,7 +136,11 @@ export const getWarehouses = async (externalDbConnectionId: number, tableName: s }; // Area 목록 조회 -export const getAreas = async (externalDbConnectionId: number, tableName: string, warehouseKey: string): Promise> => { +export const getAreas = async ( + externalDbConnectionId: number, + tableName: string, + warehouseKey: string, +): Promise> => { try { const response = await apiClient.get("/digital-twin/data/areas", { params: { externalDbConnectionId, tableName, warehouseKey }, @@ -179,18 +181,18 @@ export const getMaterials = async ( keyColumn: string; locationKeyColumn: string; layerColumn?: string; + locaKey: string; }, - locaKey: string, ): Promise> => { try { const response = await apiClient.get("/digital-twin/data/materials", { - params: { - externalDbConnectionId, + params: { + externalDbConnectionId, tableName: materialConfig.tableName, keyColumn: materialConfig.keyColumn, locationKeyColumn: materialConfig.locationKeyColumn, layerColumn: materialConfig.layerColumn, - locaKey + locaKey: materialConfig.locaKey, }, }); return response.data; @@ -241,7 +243,7 @@ export interface HierarchyData { // 전체 계층 데이터 조회 export const getHierarchyData = async ( externalDbConnectionId: number, - hierarchyConfig: any + hierarchyConfig: any, ): Promise> => { try { const response = await apiClient.post("/digital-twin/data/hierarchy", { @@ -262,7 +264,7 @@ export const getChildrenData = async ( externalDbConnectionId: number, hierarchyConfig: any, parentLevel: number, - parentKey: string + parentKey: string, ): Promise> => { try { const response = await apiClient.post("/digital-twin/data/children", { @@ -279,4 +281,3 @@ export const getChildrenData = async ( }; } }; -