- 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>
6.1 KiB
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
getOrdersSQL 수정:cpm.id AS batch_id, cpm.plan_no AS batch_nosavePlan: src_orders 배열로 처리, 첫 행만 qty 보유- excludeInPlan 수정: production_plan + shipment_plan만 제외 (cutting_plan은 중복 허용)
- 배치번호 클릭 → 전체 계획 로드 (
loadPlan)
미해결 이슈 (다음 세션 시작점)
자투리 "보관" 상태 영속화 (2026-04-21 1차 처리 완료, UI 검증 대기)
수정 내용:
- ✅
cutting_plan_sheet에remnants JSONB컬럼 추가 (ALTER 실행 완료, vexplor_dev) - ✅
backend-node/src/services/cuttingPlanService.ts—savePlanINSERT에remnants파라미터 14번째 추가 (JSON.stringify) - ✅
getPlanDetail은SELECT *이므로 자동 포함 (pg가 JSONB → JS 배열 자동 파싱) - ✅
frontend/app/(main)/COMPANY_30/production/cutting-plan/page.tsxsavePlanpayload area sheets에remnants: sh.remnants ?? null추가loadPlanarea 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_cwdENOENT로 죽을 수 있음. 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"로 집계.- 수정:
savePlanpayload 빌드 시sheetGroups.representative.remnants를 같은 그룹 indices에 복제. 개별 sheet가 자체 remnants를 갖고 있으면 그대로 유지.
- 수정:
남은 잠재 이슈 (확대편집 모달 관련):
ZoomEditorModal의statusOverrides는 lazy init이라 sheet.remnants 변경 시 stale- piece drag 핸들러에서
remnants: undefined설정 → 수동 결정 손실 - 비대표 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);