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;