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>
This commit is contained in:
DDD1542
2026-04-23 14:03:45 +09:00
parent e6496c56f9
commit 28c1c8c029
9 changed files with 4647 additions and 0 deletions

View File

@@ -0,0 +1,97 @@
/**
* 절단계획 컨트롤러
*/
import { Response } from "express";
import { AuthenticatedRequest } from "../types/auth";
import * as svc from "../services/cuttingPlanService";
import { logger } from "../utils/logger";
function wrap(res: Response, fn: () => Promise<any>, errMsg: string) {
return fn().then(
(data) => res.json({ success: true, data }),
(e: any) => {
logger.error(errMsg, { error: e?.message });
return res.status(500).json({ success: false, message: e?.message || errMsg });
}
);
}
export function getMaterials(req: AuthenticatedRequest, res: Response) {
const companyCode = req.user!.companyCode;
const cutType = (req.query.cutType as string) || "area";
return wrap(res, () => svc.getMaterials(companyCode, cutType), "원자재 조회 실패");
}
export function searchItems(req: AuthenticatedRequest, res: Response) {
const companyCode = req.user!.companyCode;
const keyword = req.query.keyword as string | undefined;
return wrap(res, () => svc.searchItems(companyCode, keyword), "품목 검색 실패");
}
export function getOrders(req: AuthenticatedRequest, res: Response) {
const companyCode = req.user!.companyCode;
const from = req.query.from as string | undefined;
const to = req.query.to as string | undefined;
const keyword = req.query.keyword as string | undefined;
const page = req.query.page ? parseInt(req.query.page as string, 10) : undefined;
const limit = req.query.limit ? parseInt(req.query.limit as string, 10) : undefined;
const excludeInPlan = req.query.excludeInPlan === "true";
return wrap(
res,
() => svc.getOrders(companyCode, { from, to, keyword, page, limit, excludeInPlan }),
"수주 조회 실패"
);
}
export function getPlans(req: AuthenticatedRequest, res: Response) {
const companyCode = req.user!.companyCode;
const { from, to, planNo, status } = req.query;
return wrap(res, () => svc.getPlans(companyCode, {
from: from as string, to: to as string,
planNo: planNo as string, status: status as string,
}), "계획 목록 조회 실패");
}
export async function getPlanDetail(req: AuthenticatedRequest, res: Response) {
try {
const companyCode = req.user!.companyCode;
const id = parseInt(req.params.id, 10);
const data = await svc.getPlanDetail(companyCode, id);
if (!data) return res.status(404).json({ success: false, message: "계획을 찾을 수 없습니다" });
return res.json({ success: true, data });
} catch (e: any) {
logger.error("계획 상세 조회 실패", { error: e?.message });
return res.status(500).json({ success: false, message: e?.message });
}
}
export function nextPlanNo(req: AuthenticatedRequest, res: Response) {
const companyCode = req.user!.companyCode;
return wrap(res, () => svc.nextPlanNo(companyCode), "계획번호 생성 실패");
}
export async function savePlan(req: AuthenticatedRequest, res: Response) {
try {
const companyCode = req.user!.companyCode;
const userId = req.user!.userId || "system";
const data = await svc.savePlan(companyCode, userId, req.body);
return res.json({ success: true, data });
} catch (e: any) {
logger.error("계획 저장 실패", { error: e?.message });
return res.status(500).json({ success: false, message: e?.message });
}
}
export async function deletePlan(req: AuthenticatedRequest, res: Response) {
try {
const companyCode = req.user!.companyCode;
const id = parseInt(req.params.id, 10);
const ok = await svc.deletePlan(companyCode, id);
if (!ok) return res.status(404).json({ success: false, message: "삭제 대상을 찾을 수 없습니다" });
return res.json({ success: true });
} catch (e: any) {
logger.error("계획 삭제 실패", { error: e?.message });
return res.status(500).json({ success: false, message: e?.message });
}
}