From 5f4838cb1931acd58fd853c48ffe896a272380e0 Mon Sep 17 00:00:00 2001 From: kjs Date: Fri, 24 Apr 2026 18:48:43 +0900 Subject: [PATCH] feat(pop): implement process list and result retrieval endpoints - Added `getProcessList` to retrieve the list of work order processes along with their results, grouped by process ID. - Implemented `getProcessResult` to fetch details of a specific work order process result by its ID. - Updated `popProductionRoutes` to enable the new endpoints for accessing process data. These additions enhance the functionality of the POP production module, allowing for better tracking and management of work order processes and their results. --- .../controllers/popProductionController.ts | 165 ++++++++++++++++++ .../src/routes/popProductionRoutes.ts | 8 +- 2 files changed, 169 insertions(+), 4 deletions(-) diff --git a/backend-node/src/controllers/popProductionController.ts b/backend-node/src/controllers/popProductionController.ts index 5420ed69..c5def3e1 100644 --- a/backend-node/src/controllers/popProductionController.ts +++ b/backend-node/src/controllers/popProductionController.ts @@ -3572,3 +3572,168 @@ export const getChecklistItems = async ( return res.status(500).json({ success: false, message: error.message }); } }; + +/** + * 작업공정 목록 조회 (POP 공정실행 화면) + * GET /api/pop/production/processes + * + * 응답: WorkOrderProcessRaw[] + * - work_order_process (기준정보) + work_order_process_result (실적 배열) + * - plan_qty는 work_instruction_detail.qty에서 가져옴 + */ +export const getProcessList = async ( + req: AuthenticatedRequest, + res: Response, +) => { + const pool = getPool(); + try { + const companyCode = req.user!.companyCode; + + // 1) 작업공정 기준 목록 + const wopRes = await pool.query( + `SELECT + p.id, p.wo_id, p.seq_no, p.process_code, p.process_name, + p.parent_process_id, p.routing_detail_id, p.batch_id, + p.target_warehouse_id, p.target_location_code, p.use_default_warehouse, + p.standard_time, p.is_required, p.is_fixed_order, p.remark, + p.created_date, + COALESCE(NULLIF(p.plan_qty,''), NULL)::numeric AS plan_qty_base, + wid.qty AS wi_detail_qty + FROM work_order_process p + LEFT JOIN work_instruction_detail wid ON wid.id = p.wo_id + WHERE p.company_code = $1 + ORDER BY p.created_date DESC, p.seq_no ASC`, + [companyCode], + ); + const wops = wopRes.rows; + + if (wops.length === 0) { + return res.json({ success: true, data: [] }); + } + + // 2) 실적(wop_result) 조회 — accepted_results 배열로 그룹핑 + const wopIds = wops.map((w: any) => w.id); + const resRes = await pool.query( + `SELECT + id, wop_id, seq, status, result_status, + input_qty, good_qty, defect_qty, concession_qty, total_production_qty, + is_rework, rework_source_id, accepted_by, accepted_at, + started_at, completed_at, equipment_code, batch_id + FROM work_order_process_result + WHERE wop_id = ANY($1::text[]) + ORDER BY wop_id, seq ASC`, + [wopIds], + ); + + const resultsByWop = new Map(); + for (const r of resRes.rows) { + const arr = resultsByWop.get(r.wop_id) || []; + arr.push(r); + resultsByWop.set(r.wop_id, arr); + } + + // 3) 대표 상태/수량은 첫 실적 기준으로 매핑 (프론트는 accepted_results도 별도 순회) + const data = wops.map((w: any) => { + const results = resultsByWop.get(w.id) || []; + const first = results[0] || {}; + return { + id: w.id, + wo_id: w.wo_id, + seq_no: w.seq_no, + process_code: w.process_code, + process_name: w.process_name, + status: first.status || "waiting", + result_status: first.result_status || null, + plan_qty: w.plan_qty_base ?? w.wi_detail_qty ?? null, + input_qty: first.input_qty ?? null, + good_qty: first.good_qty ?? null, + defect_qty: first.defect_qty ?? null, + concession_qty: first.concession_qty ?? null, + total_production_qty: first.total_production_qty ?? null, + parent_process_id: w.parent_process_id ?? null, + is_rework: first.is_rework ?? null, + rework_source_id: first.rework_source_id ?? null, + started_at: first.started_at ?? null, + completed_at: first.completed_at ?? null, + accepted_by: first.accepted_by ?? null, + accepted_at: first.accepted_at ?? null, + created_date: w.created_date ?? null, + batch_id: first.batch_id ?? w.batch_id ?? null, + equipment_code: first.equipment_code ?? null, + // Phase C 계산 필드는 별도 엔드포인트(available-qty 등)에서 제공 + available_qty: null, + prev_good_qty: null, + my_input_qty: null, + rework_available_qty: null, + split_no: null, + split_total: null, + batch_count: results.length, + batch_list: null, + batch_index: null, + accepted_results: results.map((r: any) => ({ + id: r.id, + seq: r.seq, + status: r.status, + result_status: r.result_status, + input_qty: r.input_qty, + good_qty: r.good_qty, + defect_qty: r.defect_qty, + concession_qty: r.concession_qty, + total_production_qty: r.total_production_qty, + is_rework: r.is_rework, + rework_source_id: r.rework_source_id, + accepted_by: r.accepted_by, + accepted_at: r.accepted_at, + started_at: r.started_at, + completed_at: r.completed_at, + equipment_code: r.equipment_code, + batch_id: r.batch_id, + })), + }; + }); + + return res.json({ success: true, data }); + } catch (error: any) { + logger.error("[pop/production] processes 조회 오류:", error); + return res.status(500).json({ success: false, message: error.message }); + } +}; + +/** + * 단일 작업공정 결과 조회 + * GET /api/pop/production/result/:id + * :id = work_order_process_result.id (wop_result row 식별자) + */ +export const getProcessResult = async ( + req: AuthenticatedRequest, + res: Response, +) => { + const pool = getPool(); + try { + const companyCode = req.user!.companyCode; + const { id } = req.params; + if (!id) { + return res.status(400).json({ success: false, message: "id는 필수입니다." }); + } + + const r = await pool.query( + `SELECT r.*, p.process_code, p.process_name, p.wo_id, p.seq_no + FROM work_order_process_result r + JOIN work_order_process p ON p.id = r.wop_id + WHERE r.id = $1 AND r.company_code = $2 + LIMIT 1`, + [id, companyCode], + ); + + if (r.rowCount === 0) { + return res + .status(404) + .json({ success: false, message: "결과를 찾을 수 없습니다." }); + } + + return res.json({ success: true, data: r.rows[0] }); + } catch (error: any) { + logger.error("[pop/production] result 조회 오류:", error); + return res.status(500).json({ success: false, message: error.message }); + } +}; diff --git a/backend-node/src/routes/popProductionRoutes.ts b/backend-node/src/routes/popProductionRoutes.ts index ab43a5fc..eea9ebad 100644 --- a/backend-node/src/routes/popProductionRoutes.ts +++ b/backend-node/src/routes/popProductionRoutes.ts @@ -23,8 +23,8 @@ import { saveMaterialInput, getMaterialInputs, getChecklistItems, - // TODO(mhkim-node merge): getProcessList / getProcessResult 함수가 popProductionController에 정의되어 있지 않음. - // 브랜치 머지 시 함수 구현 누락으로 서버 기동 실패. 복구되면 import 및 router.get 복원 필요. + getProcessList, + getProcessResult, } from "../controllers/popProductionController"; const router = Router(); @@ -53,7 +53,7 @@ router.get("/bom-materials/:processId", getBomMaterials); router.post("/material-input", saveMaterialInput); router.get("/material-inputs/:processId", getMaterialInputs); router.get("/checklist-items/:processId", getChecklistItems); -// router.get("/processes", getProcessList); // TODO: 함수 미구현으로 임시 비활성화 -// router.get("/result/:id", getProcessResult); // TODO: 함수 미구현으로 임시 비활성화 +router.get("/processes", getProcessList); +router.get("/result/:id", getProcessResult); export default router;