Files
vexplor/CUTTING_PLAN_PROGRESS.md
DDD1542 28c1c8c029 feat: Add cutting plan management for COMPANY_30
- Cutting optimization (Guillotine FFDH) with mixed/homogeneous modes
- Remnant management with persistence (cutting_plan_sheet.remnants JSONB)
- Work instruction creation linked via batch_no/cutting_plan_id

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 14:03:45 +09:00

6.1 KiB

절단계획 관리 작업 진행상황

마지막 업데이트: 2026-04-21 작업자: gbpark (COMPANY_30 / 중앙안전유)

작업 개요

유리 절단 최적화를 위한 절단계획 관리 페이지 구축. Guillotine 절단 + 자투리 관리 + 배치번호 추적.

핵심 파일

  • 알고리즘: frontend/lib/cutting/packing.ts
  • 메인 페이지: frontend/app/(main)/COMPANY_30/production/cutting-plan/page.tsx
  • 백엔드 서비스: backend-node/src/services/cuttingPlanService.ts
  • 컨트롤러: backend-node/src/controllers/cuttingPlanController.ts
  • 라우트: backend-node/src/routes/cuttingPlanRoutes.ts

완료된 작업

1. 알고리즘

  • 혼합 최적 / 동일 품목 우선 알고리즘 분리 (구조 자체가 다름)
  • MaxRects → 2-stage Guillotine FFDH 전환 (직선 절단만 가능하도록)
  • packAreaHomogeneous: 실제 maxCap 측정 후 전용 sheet + 잔여물 혼합 방식

2. 자투리(Remnant) 관리

  • Plane sweep 기반 빈 영역 검출 (computeRemnants)
  • kerf 너비 false positive 필터링 (w > minDim && h > minDim)
  • Union-Find으로 인접 자투리 그룹화
  • 그룹 폴리곤 외곽선 계산 (computeGroupOutline)
  • 그룹 분할: 가로/세로/큰 사각형 우선 (decomposeUnion)
  • 자투리 개별 상태 토글 (보관/폐기), shift+click 지원
  • 수동 편집 화면 + 확대(Zoom) 화면 모두에서 토글 가능

3. 드래그 동작

  • kerf 고려한 nearest-limit 스냅 (수동편집 + 확대편집)

4. 배치번호(Batch No) 추적

  • DB: cutting_plan_item.src_no → 수주 LEFT JOIN
  • getOrders SQL 수정: cpm.id AS batch_id, cpm.plan_no AS batch_no
  • savePlan: src_orders 배열로 처리, 첫 행만 qty 보유
  • excludeInPlan 수정: production_plan + shipment_plan만 제외 (cutting_plan은 중복 허용)
  • 배치번호 클릭 → 전체 계획 로드 (loadPlan)

미해결 이슈 (다음 세션 시작점)

자투리 "보관" 상태 영속화 (2026-04-21 1차 처리 완료, UI 검증 대기)

수정 내용:

  • cutting_plan_sheetremnants JSONB 컬럼 추가 (ALTER 실행 완료, vexplor_dev)
  • backend-node/src/services/cuttingPlanService.tssavePlan INSERT에 remnants 파라미터 14번째 추가 (JSON.stringify)
  • getPlanDetailSELECT *이므로 자동 포함 (pg가 JSONB → JS 배열 자동 파싱)
  • frontend/app/(main)/COMPANY_30/production/cutting-plan/page.tsx
    • savePlan payload area sheets에 remnants: sh.remnants ?? null 추가
    • loadPlan area Sheet 복원 시 remnants: Array.isArray(sh.remnants) ? ... : undefined 추가

UI 검증 (사용자 확인 완료):

  • DB 저장/복원 동작. 단, 배치번호 클릭 한 번으로는 안 보이고 두 번째 클릭에서야 복원되는 증상.

추가 원인 & 조치 (2026-04-21 2차):

  • page.tsx line ~364 useEffect([packMode, cutType, kerf, margin, mat1.code, mat2.code])가 loadPlan의 setCutType/setKerf/...에 반응해 calculate()를 호출 → 방금 복원한 remnants를 덮어씀.
  • 2번째 클릭엔 state가 같아 useEffect 안 돌고 그대로 유지.
  • 수정: skipAutoRecalcRef (useRef) 도입. loadPlan 시작 시 true 세팅, 직후 useEffect에서 1회 소비 후 false.
  • 관련 위치: page.tsx (const skipAutoRecalcRef = useRef(false) 선언부, 재계산 useEffect 상단 가드, loadPlan 초입 세팅).

인프라 이슈 (참고):

  • 사무실 우분투 pms-backend-mac 컨테이너는 Syncthing이 파일 교체 순간 cwd가 사라지며 nodemon이 uv_cwd ENOENT로 죽을 수 있음. Hot reload 이상 시 docker restart pms-backend-mac.

자투리 관리 탭 집계 수정 (2026-04-21 3차):

  • 증상: 배치 계획 탭에서 "혼합 1/2"로 표시된 그룹이 자투리 관리 탭에서는 "보관 0개"로 잘못 집계.
  • 원인 1 (표시): RemnantView가 그룹 단위 row 생성 + allKeep = every("keep") 기준 → 혼합 그룹이 전부 discard로 분류.
    • 수정: groups.forEach 내부에서 rects를 status 기준으로 split하여 keep/discard 각각 row 생성.
  • 원인 2 (저장): sheetGroups의 대표 sheet에만 remnants가 있고 비대표 5장은 null → 로드 시 비대표는 기본 "discard"로 집계.
    • 수정: savePlan payload 빌드 시 sheetGroups.representative.remnants를 같은 그룹 indices에 복제. 개별 sheet가 자체 remnants를 갖고 있으면 그대로 유지.

남은 잠재 이슈 (확대편집 모달 관련):

  1. ZoomEditorModalstatusOverrides는 lazy init이라 sheet.remnants 변경 시 stale
  2. piece drag 핸들러에서 remnants: undefined 설정 → 수동 결정 손실
  3. 비대표 sheet 개별 편집 시 그룹 전파 기본값이 그 sheet를 덮지는 않지만, 대표 편집이 이후에 오면 비대표 개별 편집이 덮어질 여지 (현재는 대표에서만 편집 UI 제공이라 문제 없음)

보류 작업 (사용자가 나중에 진행)

  • 중앙 품목 페이지 헤더의 필터/정렬 기능을 절단계획 수주 탭에 적용
  • Step 3: 마우스로 절단선 직접 그리기 (interactive cutting)
  • Step 4: 드래그로 새 자투리 생성 (manual remnant drawing)

DB 검증 정보 (vexplor_dev)

  • 서버: 211.115.91.141:11134/vexplor_dev
  • 검증된 데이터: CP-2026-0001 (5 수주), CP-2026-0002 (4 수주)
  • 구조: cutting_plan_mng → cutting_plan_item(src_no=수주FK) → cutting_plan_sheet → cutting_plan_placement
  • cutting_plan_sheet.remnants JSONB (2026-04-21 추가): [{id,x,y,w,h,status:"keep"|"discard"}]

핵심 코드 스니펫

ZoomEditorModal statusOverrides 초기화 (page.tsx):

const [statusOverrides, setStatusOverrides] = useState<Record<string, "keep" | "discard">>(() => {
  const init: Record<string, "keep" | "discard"> = {};
  if (sheet.remnants) {
    sheet.remnants.forEach((rm) => {
      init[`${rm.x}|${rm.y}|${rm.w}|${rm.h}`] = rm.status;
    });
  }
  return init;
});

자투리 필터 (packing.ts):

const minDim = Math.max(1, kerf);
return finalRects.filter((r) => r.w > minDim && r.h > minDim);