/** * 품질 모니터링 데이터 조회 (서버 페이징 + 통계 합산) * work_order_process(공정 메타) + work_order_process_result(실적) JOIN * - 페이지: 화면 표 표시용 * - summary: KPI 카드용 (전체 합산) */ import { Response } from "express"; import { AuthenticatedRequest } from "../types/auth"; import { getPool } from "../database/db"; import { logger } from "../utils/logger"; export async function getQualityMonitoringData(req: AuthenticatedRequest, res: Response) { try { const companyCode = req.user!.companyCode; const from = (req.query.from as string | undefined)?.trim() || ""; const to = (req.query.to as string | undefined)?.trim() || ""; const page = Math.max(1, parseInt(String(req.query.page ?? "1"), 10) || 1); const size = Math.max(1, Math.min(500, parseInt(String(req.query.size ?? "50"), 10) || 50)); const offset = (page - 1) * size; const params: any[] = [companyCode]; const conds: string[] = ["wopr.company_code = $1"]; if (from) { params.push(`${from} 00:00:00`); conds.push(`wopr.created_date >= $${params.length}::timestamp`); } if (to) { params.push(`${to} 23:59:59`); conds.push(`wopr.created_date <= $${params.length}::timestamp`); } const whereClause = `WHERE ${conds.join(" AND ")}`; const pool = getPool(); // 1) total + summary (KPI 카드) const summaryQuery = ` SELECT COUNT(*)::int AS total, SUM(CASE WHEN wopr.status = 'completed' AND COALESCE(CAST(NULLIF(wopr.defect_qty, '') AS numeric), 0) = 0 THEN 1 ELSE 0 END)::int AS passed, SUM(CASE WHEN wopr.status = 'completed' AND COALESCE(CAST(NULLIF(wopr.defect_qty, '') AS numeric), 0) > 0 THEN 1 ELSE 0 END)::int AS failed, SUM(CASE WHEN wopr.status <> 'completed' OR wopr.status IS NULL THEN 1 ELSE 0 END)::int AS pending FROM work_order_process_result wopr ${whereClause} `; const summaryRes = await pool.query(summaryQuery, params); const summaryRow = summaryRes.rows[0] || { total: 0, passed: 0, failed: 0, pending: 0 }; const total = summaryRow.total || 0; const passRate = total > 0 ? Math.round((summaryRow.passed / total) * 1000) / 10 : 0; // 2) 페이지 데이터 const pageParams = [...params, size, offset]; const dataQuery = ` SELECT wopr.id, wopr.wop_id, wopr.status, wopr.input_qty, wopr.good_qty, wopr.defect_qty, wopr.started_at, wopr.completed_at, wopr.completed_by, wopr.accepted_by, wop.wo_id, wop.process_code, wop.process_name, wop.plan_qty FROM work_order_process_result wopr LEFT JOIN work_order_process wop ON wop.id = wopr.wop_id AND wop.company_code = wopr.company_code ${whereClause} ORDER BY wopr.created_date DESC LIMIT $${params.length + 1} OFFSET $${params.length + 2} `; const dataRes = await pool.query(dataQuery, pageParams); const rows = dataRes.rows.map((r: any) => ({ id: r.id, wo_id: r.wo_id, process_code: r.process_code || "", process_name: r.process_name || "", status: r.status || "", plan_qty: Number(r.plan_qty) || 0, input_qty: Number(r.input_qty) || 0, good_qty: Number(r.good_qty) || 0, defect_qty: Number(r.defect_qty) || 0, started_at: r.started_at || null, completed_at: r.completed_at || null, worker_name: r.completed_by || r.accepted_by || "", })); logger.info("품질 모니터링 조회", { companyCode, from, to, page, size, total, }); return res.json({ success: true, rows, total, page, size, totalPages: Math.max(1, Math.ceil(total / size)), summary: { total, passed: summaryRow.passed || 0, failed: summaryRow.failed || 0, pending: summaryRow.pending || 0, passRate, }, }); } catch (error: any) { logger.error("품질 모니터링 조회 실패", { error: error.message }); return res.status(500).json({ success: false, message: error.message }); } }