ERP-node/frontend/components/admin/dashboard/widgets/yard-3d/HIERARCHY_MIGRATION_GUIDE.md

11 KiB

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

개요

기존 구조: Area(구역) → Location(위치) 고정 2단계
신규 구조: 동적 N-Level 계층 (영역 → 하위 영역 → ... → 자재)


1. 데이터베이스 마이그레이션

실행 방법

# 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

사용 방법:

import HierarchyConfigPanel from "./HierarchyConfigPanel";

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

계층 구조 설정 예시

{
  "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. 포함 여부 확인

import { isContainedIn } from "./spatialContainment";

const isValid = isContainedIn(childObject, parentObject);
// 자식 객체가 부모 객체 내부에 있는지 AABB로 검증

2. 유효한 부모 찾기

import { findValidParent } from "./spatialContainment";

const parent = findValidParent(draggedChild, allObjects, hierarchyLevels);
// 드래그 중인 자식 객체를 포함하는 부모 객체 자동 감지

3. 검증

import { validateSpatialContainment } from "./spatialContainment";

const result = validateSpatialContainment(child, allObjects);
if (!result.valid) {
  alert("하위 영역은 반드시 상위 영역 내부에 배치되어야 합니다!");
}

4. 그룹 이동 (부모 이동 시 자식도 함께)

import { updateChildrenPositions, getAllDescendants } from "./spatialContainment";

// 부모 객체 이동 시
const updatedChildren = updateChildrenPositions(
  parentObject,
  oldPosition,
  newPosition,
  allObjects
);

// 모든 하위 자손(재귀) 가져오기
const descendants = getAllDescendants(parentId, allObjects);

5. DigitalTwinEditor 통합 방법

Step 1: HierarchyConfigPanel 추가

// 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: 계층 데이터 로드

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에서 검증

// 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: 그룹 이동 구현

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