# 디지털 트윈 동적 계층 구조 마이그레이션 가이드

## 개요

**기존 구조**: 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";

<HierarchyConfigPanel
  externalDbConnectionId={selectedDbConnection}
  hierarchyConfig={hierarchyConfig}
  onHierarchyConfigChange={setHierarchyConfig}
  availableTables={availableTables}
  onLoadTables={loadTablesFromDb}
  onLoadColumns={loadColumnsFromTable}
/>
```

### 계층 구조 설정 예시

```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<HierarchyConfig | null>(null);

// 좌측 사이드바에 추가
<HierarchyConfigPanel
  externalDbConnectionId={selectedDbConnection}
  hierarchyConfig={hierarchyConfig}
  onHierarchyConfigChange={setHierarchyConfig}
  availableTables={availableTables}
  onLoadTables={loadTables}
  onLoadColumns={loadColumns}
/>
```

### 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


