Files
vexplor_dev/backend-node/src/controllers/qualityMonitoringController.ts

117 lines
4.1 KiB
TypeScript
Raw Normal View History

/**
* ( + )
* 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 });
}
}