# 디지털 트윈 동적 계층 구조 마이그레이션 가이드 ## 개요 **기존 구조**: Area(구역) → Location(위치) 고정 2단계 **신규 구조**: 동적 N-Level 계층 (영역 → 하위 영역 → ... → 자재) --- ## 1. 데이터베이스 마이그레이션 ### 실행 방법 ```bash # PostgreSQL 컨테이너 접속 docker exec -it pms-db psql -U postgres -d erp # 마이그레이션 실행 \i db/migrations/042_refactor_digital_twin_hierarchy.sql ``` ### 변경 사항 - `digital_twin_layout` 테이블에 `hierarchy_config` JSONB 컬럼 추가 - 기존 테이블 매핑 컬럼들 제거 (warehouse_table_name, area_table_name 등) - `digital_twin_objects` 테이블에 계층 관련 컬럼 추가: - `hierarchy_level`: 계층 레벨 (1, 2, 3, ...) - `parent_key`: 부모 객체의 외부 DB 키 - `external_key`: 자신의 외부 DB 키 --- ## 2. 백엔드 API 변경 사항 ### 신규 API 엔드포인트 #### 전체 계층 데이터 조회 ``` POST /api/digital-twin/data/hierarchy Request Body: { "externalDbConnectionId": 15, "hierarchyConfig": "{...}" // JSON 문자열 } Response: { "success": true, "data": { "warehouse": [...], "levels": [ { "level": 1, "name": "구역", "data": [...] }, { "level": 2, "name": "위치", "data": [...] } ], "materials": [ { "location_key": "LOC001", "count": 150 } ] } } ``` #### 특정 부모의 하위 데이터 조회 ``` POST /api/digital-twin/data/children Request Body: { "externalDbConnectionId": 15, "hierarchyConfig": "{...}", "parentLevel": 1, "parentKey": "AREA001" } Response: { "success": true, "data": [...] // 다음 레벨 데이터 } ``` ### 레거시 API (호환성 유지) - `/api/digital-twin/data/warehouses` (GET) - `/api/digital-twin/data/areas` (GET) - `/api/digital-twin/data/locations` (GET) - `/api/digital-twin/data/materials` (GET) - `/api/digital-twin/data/material-counts` (POST로 변경) --- ## 3. 프론트엔드 변경 사항 ### 새로운 컴포넌트 #### `HierarchyConfigPanel.tsx` 동적 계층 구조 설정 UI **사용 방법:** ```tsx import HierarchyConfigPanel from "./HierarchyConfigPanel"; ``` ### 계층 구조 설정 예시 ```json { "warehouse": { "tableName": "MWARMA", "keyColumn": "WAREKEY", "nameColumn": "WARENAME" }, "levels": [ { "level": 1, "name": "구역", "tableName": "MAREMA", "keyColumn": "AREAKEY", "nameColumn": "AREANAME", "parentKeyColumn": "WAREKEY", "objectTypes": ["area"] }, { "level": 2, "name": "위치", "tableName": "MLOCMA", "keyColumn": "LOCAKEY", "nameColumn": "LOCANAME", "parentKeyColumn": "AREAKEY", "typeColumn": "LOCTYPE", "objectTypes": ["location-bed", "location-stp"] } ], "material": { "tableName": "WSTKKY", "keyColumn": "STKKEY", "locationKeyColumn": "LOCAKEY", "layerColumn": "LOLAYER", "quantityColumn": "STKQUAN" } } ``` --- ## 4. 공간적 종속성 (Spatial Containment) ### 새로운 유틸리티: `spatialContainment.ts` #### 주요 함수 **1. 포함 여부 확인** ```typescript import { isContainedIn } from "./spatialContainment"; const isValid = isContainedIn(childObject, parentObject); // 자식 객체가 부모 객체 내부에 있는지 AABB로 검증 ``` **2. 유효한 부모 찾기** ```typescript import { findValidParent } from "./spatialContainment"; const parent = findValidParent(draggedChild, allObjects, hierarchyLevels); // 드래그 중인 자식 객체를 포함하는 부모 객체 자동 감지 ``` **3. 검증** ```typescript import { validateSpatialContainment } from "./spatialContainment"; const result = validateSpatialContainment(child, allObjects); if (!result.valid) { alert("하위 영역은 반드시 상위 영역 내부에 배치되어야 합니다!"); } ``` **4. 그룹 이동 (부모 이동 시 자식도 함께)** ```typescript import { updateChildrenPositions, getAllDescendants } from "./spatialContainment"; // 부모 객체 이동 시 const updatedChildren = updateChildrenPositions( parentObject, oldPosition, newPosition, allObjects ); // 모든 하위 자손(재귀) 가져오기 const descendants = getAllDescendants(parentId, allObjects); ``` --- ## 5. DigitalTwinEditor 통합 방법 ### Step 1: HierarchyConfigPanel 추가 ```tsx // DigitalTwinEditor.tsx import HierarchyConfigPanel, { HierarchyConfig } from "./HierarchyConfigPanel"; const [hierarchyConfig, setHierarchyConfig] = useState(null); // 좌측 사이드바에 추가 ``` ### Step 2: 계층 데이터 로드 ```tsx import { getHierarchyData, getChildrenData } from "@/lib/api/digitalTwin"; const loadHierarchyData = async () => { if (!selectedDbConnection || !hierarchyConfig) return; const response = await getHierarchyData(selectedDbConnection, hierarchyConfig); if (response.success && response.data) { // 창고 데이터 setWarehouses(response.data.warehouse); // 각 레벨 데이터 response.data.levels.forEach((level) => { if (level.level === 1) { setAvailableAreas(level.data); } else if (level.level === 2) { setAvailableLocations(level.data); } // ... 추가 레벨 }); // 자재 개수 setMaterialCounts(response.data.materials); } }; ``` ### Step 3: Yard3DCanvas에서 검증 ```tsx // Yard3DCanvas.tsx 또는 DigitalTwinEditor.tsx import { validateSpatialContainment } from "./spatialContainment"; const handleObjectDrop = (droppedObject: PlacedObject) => { const result = validateSpatialContainment( { id: droppedObject.id, position: droppedObject.position, size: droppedObject.size, hierarchyLevel: droppedObject.hierarchyLevel || 1, parentId: droppedObject.parentId, }, placedObjects.map((obj) => ({ id: obj.id, position: obj.position, size: obj.size, hierarchyLevel: obj.hierarchyLevel || 1, parentId: obj.parentId, })) ); if (!result.valid) { toast({ variant: "destructive", title: "배치 오류", description: "하위 영역은 반드시 상위 영역 내부에 배치되어야 합니다.", }); return; // 배치 취소 } // 유효하면 부모 ID 업데이트 droppedObject.parentId = result.parent?.id; // 상태 업데이트 setPlacedObjects([...placedObjects, droppedObject]); }; ``` ### Step 4: 그룹 이동 구현 ```tsx import { updateChildrenPositions, getAllDescendants } from "./spatialContainment"; const handleObjectMove = ( movedObject: PlacedObject, oldPosition: { x: number; y: number; z: number }, newPosition: { x: number; y: number; z: number } ) => { // 이동한 객체 업데이트 const updatedObjects = placedObjects.map((obj) => obj.id === movedObject.id ? { ...obj, position: newPosition } : obj ); // 모든 하위 자손 가져오기 const descendants = getAllDescendants( movedObject.id, placedObjects.map((obj) => ({ id: obj.id, position: obj.position, size: obj.size, hierarchyLevel: obj.hierarchyLevel || 1, parentId: obj.parentId, })) ); // 하위 자손들도 같이 이동 const delta = { x: newPosition.x - oldPosition.x, y: newPosition.y - oldPosition.y, z: newPosition.z - oldPosition.z, }; descendants.forEach((descendant) => { const index = updatedObjects.findIndex((obj) => obj.id === descendant.id); if (index !== -1) { updatedObjects[index].position = { x: updatedObjects[index].position.x + delta.x, y: updatedObjects[index].position.y + delta.y, z: updatedObjects[index].position.z + delta.z, }; } }); setPlacedObjects(updatedObjects); }; ``` --- ## 6. 테스트 시나리오 ### 테스트 1: 계층 구조 설정 1. 외부 DB 선택 2. 창고 테이블 선택 및 컬럼 매핑 3. 레벨 추가 (레벨 1: 구역, 레벨 2: 위치) 4. 각 레벨의 테이블 및 컬럼 매핑 5. 자재 테이블 설정 6. "저장" 클릭하여 `hierarchy_config` 저장 ### 테스트 2: 데이터 로드 1. 계층 구조 설정 완료 후 2. 창고 선택 3. 각 레벨 데이터가 좌측 패널에 표시되는지 확인 4. 자재 개수가 올바르게 표시되는지 확인 ### 테스트 3: 3D 배치 및 공간적 종속성 1. 레벨 1 (구역) 객체를 3D 캔버스에 드래그앤드롭 2. 레벨 2 (위치) 객체를 레벨 1 객체 **내부**에 드래그앤드롭 → 성공 3. 레벨 2 객체를 레벨 1 객체 **외부**에 드롭 → 오류 메시지 표시 ### 테스트 4: 그룹 이동 1. 레벨 1 객체를 이동 2. 해당 레벨 1 객체의 모든 하위 객체(레벨 2, 3, ...)도 같이 이동하는지 확인 3. 부모-자식 관계가 유지되는지 확인 ### 테스트 5: 레이아웃 저장/로드 1. 위 단계를 완료한 후 "저장" 클릭 2. 페이지 새로고침 3. 레이아웃을 다시 로드하여 계층 구조 및 객체 위치가 복원되는지 확인 --- ## 7. 마이그레이션 체크리스트 - [ ] DB 마이그레이션 실행 (042_refactor_digital_twin_hierarchy.sql) - [ ] 백엔드 API 테스트 (Postman/cURL) - [ ] `HierarchyConfigPanel` 컴포넌트 통합 - [ ] `spatialContainment.ts` 유틸리티 통합 - [ ] `DigitalTwinEditor`에서 계층 데이터 로드 구현 - [ ] `Yard3DCanvas`에서 공간적 종속성 검증 구현 - [ ] 그룹 이동 기능 구현 - [ ] 모든 테스트 시나리오 통과 - [ ] 레거시 API와의 호환성 확인 --- ## 8. 주의사항 1. **기존 레이아웃 데이터**: 마이그레이션 전 기존 레이아웃이 있다면 백업 필요 2. **컬럼 매핑 검증**: 외부 DB 테이블의 컬럼명이 변경될 수 있으므로 auto-mapping 로직 필수 3. **성능**: N-Level이 3단계 이상 깊어지면 재귀 쿼리 성능 모니터링 필요 4. **권한**: 외부 DB에 대한 읽기 권한 확인 --- ## 9. 향후 개선 사항 1. **드래그 중 실시간 검증**: 드래그하는 동안 부모 영역 하이라이트 2. **시각적 피드백**: 유효한 배치 위치를 그리드에 색상으로 표시 3. **계층 구조 시각화**: 좌측 패널에 트리 구조로 표시 4. **Undo/Redo**: 객체 배치 실행 취소 기능 5. **스냅 가이드**: 부모 영역 테두리에 스냅 가이드라인 표시 --- **작성일**: 2025-11-20 **작성자**: AI Assistant