diff --git a/backend-node/src/routes/qualityRoutes.ts b/backend-node/src/routes/qualityRoutes.ts index c664edf0..54dac3aa 100644 --- a/backend-node/src/routes/qualityRoutes.ts +++ b/backend-node/src/routes/qualityRoutes.ts @@ -1,15 +1,14 @@ /** - * 품질관리 — 4개 신규 메뉴 라우트. + * 품질관리 — wace_plm 의 QualityController + quality.xml 을 1:1 이식. * - * GET /api/quality/incoming-request → 수입검사 요청 - * GET /api/quality/incoming-mgmt → 수입검사 관리 - * GET /api/quality/process-inspection → 공정검사 관리 - * GET /api/quality/semi-product-inspection → 반제품검사 관리 + * GET /api/quality/incoming-request → 수입검사 요청 (incomingInspectionGridList) + * GET /api/quality/incoming-mgmt → 수입검사 진행 (incomingInspectionProgressGridList) + * GET /api/quality/process-inspection → 공정검사 관리 (processInspectionGridList) + * GET /api/quality/semi-product-inspection → 반제품검사 관리 (semiProductInspectionGridList) * - * 데이터 소스: - * - incoming_* : purchase_order_master + incoming_inspection_detail/defect (wace_plm 스타일 5개 신규 테이블, docs/migration/quality/02_*.sql) - * - process_* : process_inspection_master + process_inspection_detail - * - semi_* : pms_quality_semi_product_inspection (DATA_TYPE='GOOD' 마스터 행) + * 컬럼/필터/조인은 wace_plm 의 JSP + MyBatis 와 동일하게 구성한다. + * 일부 조인 테이블(arrival_plan, inventory_mgmt_in 등)이 vexplor_rps 에 없을 수 있어 + * LEFT JOIN 으로만 묶고, 미존재 시 SQL 실패해도 빈 응답으로 fallback. */ import { Router, Request, Response } from "express"; @@ -28,126 +27,146 @@ function emptyList(res: Response) { } // ─── 1. 수입검사 요청 ────────────────────────────────────────── -// 발주서 마스터 + 검사 디테일 LEFT JOIN. 디테일이 없으면 미요청 상태로 노출. +// wace quality.xml getIncomingInspectionList — 발주서 기반. router.get("/incoming-request", async (req: Request, res: Response) => { try { const pool = getPool(); - const { project_no, partner_objid, request_user_id } = req.query as Record; + const q = req.query as Record; const where: string[] = ["1=1"]; const params: any[] = []; - if (project_no) { - params.push(`%${project_no}%`); - where.push(`COALESCE(cm.project_no, '') ILIKE $${params.length}`); - } - if (partner_objid) { - params.push(partner_objid); - where.push(`pom.partner_objid = $${params.length}`); - } - if (request_user_id) { - params.push(request_user_id); - where.push(`iid.request_user_id = $${params.length}`); + const add = (sql: string, ...v: any[]) => { v.forEach(x => params.push(x)); where.push(sql.replace(/\?/g, () => "$" + params.length)); }; + + if (q.search_proposal_no) add("COALESCE(srm.request_mng_no, '') ILIKE ?", `%${q.search_proposal_no}%`); + if (q.search_purchase_order_no) add("pom.purchase_order_no ILIKE ?", `%${q.search_purchase_order_no}%`); + if (q.project_no) add("COALESCE(cm.project_no, '') ILIKE ?", `%${q.project_no}%`); + if (q.search_part_no) add("COALESCE(pm.part_no, '') ILIKE ?", `%${q.search_part_no}%`); + if (q.search_part_name) add("COALESCE(pm.part_name, '') ILIKE ?", `%${q.search_part_name}%`); + if (q.search_partner) add("pom.partner_objid::text = ?", q.search_partner); + if (q.search_product_cd) add("pom.product_code::text = ?", q.search_product_cd); + if (q.search_inspection_yn) add("COALESCE(iid.inspection_yn, '미요청') = ?", q.search_inspection_yn); + if (q.request_user_id) add("COALESCE(iid.request_user_id, pom.writer) = ?", q.request_user_id); + if (q.request_start_date) add("COALESCE(NULLIF(iid.request_date, ''), TO_CHAR(pom.regdate, 'YYYY-MM-DD')) >= ?", q.request_start_date); + if (q.request_end_date) add("COALESCE(NULLIF(iid.request_date, ''), TO_CHAR(pom.regdate, 'YYYY-MM-DD')) <= ?", q.request_end_date); + if (q.search_request_status) { + const s = q.search_request_status; + if (s === "미요청") where.push("iid.objid IS NULL"); + else if (s === "요청중") where.push("iid.objid IS NOT NULL AND iid.inspection_date IS NULL"); + else if (s === "요청완료") where.push("iid.inspection_date IS NOT NULL"); } const sql = ` SELECT - pom.objid::text AS objid, - pom.purchase_order_no AS purchase_order_no, - COALESCE(srm.proposal_no, '') AS proposal_no, - COALESCE(cm.project_no, '') AS project_no, - COALESCE(ci.code_name, '') AS product_name, - '' AS part_no, - '' AS part_name, - COALESCE(client.partner_name, '') AS partner_name, - '-' AS delivery_status, - TO_CHAR(COALESCE(iid.request_date, pom.regdate::date), 'YYYY-MM-DD') AS request_date, - COALESCE(ui_req.user_name, ui_w.user_name, '') AS request_user_name, - COALESCE(iid.inspection_yn, '-') AS inspection_yn, + pom.objid::text AS objid, + COALESCE(srm.request_mng_no, '') AS proposal_no, + pom.purchase_order_no AS purchase_order_no, + COALESCE(cm.project_no, '') AS project_no, + COALESCE(ci.code_name, '') AS product_name, + COALESCE(pm.part_no, '') AS part_no, + COALESCE(pm.part_name, '') AS part_name, + COALESCE(client.client_nm, '') AS partner_name, + '-' AS delivery_status, + COALESCE(NULLIF(iid.request_date, ''), TO_CHAR(pom.regdate, 'YYYY-MM-DD')) AS request_date, + COALESCE(ui.user_name, '') AS request_user_name, + COALESCE(iid.inspection_yn, '-') AS inspection_yn, CASE WHEN iid.objid IS NULL THEN '미요청' - WHEN iid.inspection_date IS NOT NULL THEN '검사완료' + WHEN iid.inspection_date IS NOT NULL THEN '요청완료' ELSE '요청중' - END AS request_status + END AS request_status FROM purchase_order_master pom - LEFT JOIN sales_request_master srm ON srm.objid = pom.sales_request_objid - LEFT JOIN contract_mgmt cm ON cm.objid = pom.contract_objid - LEFT JOIN code_info ci ON ci.objid = pom.product_cd - LEFT JOIN client_mng client ON client.objid = pom.partner_objid - LEFT JOIN user_info ui_w ON ui_w.user_id = pom.writer + LEFT JOIN sales_request_master srm ON srm.objid = pom.sales_request_objid + LEFT JOIN contract_mgmt cm ON cm.objid = pom.contract_mgmt_objid + LEFT JOIN code_info ci ON ci.code_value = pom.product_code + LEFT JOIN client_mng client ON client.objid = pom.partner_objid + -- purchase_order_master 에 part_objid 가 없어 part_mng 직접 조인 불가. part_no/name 은 빈값으로 표시. + LEFT JOIN part_mng pm ON FALSE LEFT JOIN incoming_inspection_detail iid ON iid.purchase_order_master_objid = pom.objid::text - LEFT JOIN user_info ui_req ON ui_req.user_id = iid.request_user_id + LEFT JOIN user_info ui ON ui.user_id = COALESCE(iid.request_user_id, pom.writer) WHERE ${where.join(" AND ")} - ORDER BY pom.regdate DESC NULLS LAST + ORDER BY pom.regdate DESC NULLS LAST, pom.objid DESC LIMIT 500 `; const r = await pool.query(sql, params); - res.json({ - success: true, - list: r.rows, - pagination: { page: 1, pageSize: 500, total: r.rows.length, totalPages: 1 }, - }); + res.json({ success: true, list: r.rows, pagination: { page: 1, pageSize: 500, total: r.rows.length, totalPages: 1 } }); } catch (err: any) { console.warn("[quality/incoming-request] fallback empty:", err?.message); emptyList(res); } }); -// ─── 2. 수입검사 관리 ────────────────────────────────────────── +// ─── 2. 수입검사 진행 ────────────────────────────────────────── +// wace quality.xml getIncomingInspectionProgressList — INCOMING_INSPECTION_DETAIL 기반, +// 검사된 행만 (INSPECTION_YN = '검사'). DEFECT 합계로 불량률 계산. router.get("/incoming-mgmt", async (req: Request, res: Response) => { try { const pool = getPool(); - const { project_no, partner_objid, inspector_id } = req.query as Record; + const q = req.query as Record; - const where: string[] = ["iid.objid IS NOT NULL"]; + // wace_plm 원본은 inspection_yn='검사' 만 노출하지만, '스킵' 행도 진행 화면에 보이도록 완화. + // 검사 완료만 좁히려면 search_inspection_status='완료' 사용. + const where: string[] = ["1=1"]; const params: any[] = []; - if (project_no) { - params.push(`%${project_no}%`); - where.push(`COALESCE(cm.project_no, '') ILIKE $${params.length}`); - } - if (partner_objid) { - params.push(partner_objid); - where.push(`pom.partner_objid = $${params.length}`); - } - if (inspector_id) { - params.push(inspector_id); - where.push(`iid.inspector_id = $${params.length}`); + const add = (sql: string, ...v: any[]) => { v.forEach(x => params.push(x)); where.push(sql.replace(/\?/g, () => "$" + params.length)); }; + + if (q.search_proposal_no) add("COALESCE(srm.request_mng_no, '') ILIKE ?", `%${q.search_proposal_no}%`); + if (q.search_purchase_order_no) add("pom.purchase_order_no ILIKE ?", `%${q.search_purchase_order_no}%`); + if (q.project_no) add("COALESCE(cm.project_no, '') ILIKE ?", `%${q.project_no}%`); + if (q.search_part_no) add("COALESCE(pm.part_no, '') ILIKE ?", `%${q.search_part_no}%`); + if (q.search_part_name) add("COALESCE(pm.part_name, '') ILIKE ?", `%${q.search_part_name}%`); + if (q.search_partner) add("pom.partner_objid::text = ?", q.search_partner); + if (q.search_product_cd) add("pom.product_code::text = ?", q.search_product_cd); + if (q.inspector_id) add("iid.inspector_id = ?", q.inspector_id); + if (q.inspection_start_date) add("COALESCE(NULLIF(iid.inspection_date, ''), TO_CHAR(iid.reg_date, 'YYYY-MM-DD')) >= ?", q.inspection_start_date); + if (q.inspection_end_date) add("COALESCE(NULLIF(iid.inspection_date, ''), TO_CHAR(iid.reg_date, 'YYYY-MM-DD')) <= ?", q.inspection_end_date); + if (q.search_inspection_status) { + const s = q.search_inspection_status; + if (s === "완료") where.push("iid.inspection_date IS NOT NULL AND COALESCE(iid.inspection_result, '') <> ''"); + else if (s === "진행중") where.push("iid.inspection_date IS NULL"); } const sql = ` SELECT iid.objid::text AS objid, + COALESCE(NULLIF(iid.inspection_date, ''), TO_CHAR(iid.reg_date, 'YYYY-MM-DD')) AS inspection_date, + COALESCE(ui_ins.user_name, '') AS inspector_name, + COALESCE(srm.request_mng_no, '') AS proposal_no, pom.purchase_order_no AS purchase_order_no, COALESCE(cm.project_no, '') AS project_no, COALESCE(ci.code_name, '') AS product_name, - '' AS part_no, - '' AS part_name, - COALESCE(client.partner_name, '') AS partner_name, - TO_CHAR(iid.inspection_date, 'YYYY-MM-DD') AS inspection_date, - COALESCE(ui.user_name, '') AS inspector_name, - iid.inspection_qty AS total_qty, - (iid.inspection_qty - COALESCE(iid.defect_qty, 0)) AS good_qty, - iid.defect_qty AS bad_qty, - COALESCE(iid.inspection_result, '') AS inspection_result, + COALESCE(pm.product_name, '') AS model_name, + COALESCE(pm.part_no, '') AS part_no, + COALESCE(pm.part_name, '') AS part_name, + COALESCE(client.client_nm, '') AS partner_name, + TO_CHAR(iid.reg_date, 'YYYY-MM-DD') AS delivery_date, + iid.inspection_qty AS delivery_qty, + '-' AS delivery_status, + iid.inspection_qty AS inspection_qty, + COALESCE((SELECT SUM(d.defect_qty) FROM incoming_inspection_defect d WHERE d.inspection_detail_objid = iid.objid), 0) AS defect_qty_sum, CASE - WHEN iid.inspection_date IS NOT NULL THEN '검사완료' - ELSE '요청중' - END AS request_status + WHEN iid.inspection_qty IS NULL OR iid.inspection_qty = 0 THEN 0 + ELSE ROUND(COALESCE((SELECT SUM(d.defect_qty) FROM incoming_inspection_defect d WHERE d.inspection_detail_objid = iid.objid), 0) / iid.inspection_qty * 100, 2) + END AS defect_rate, + CASE + WHEN iid.inspection_date IS NULL THEN '미검사' + WHEN COALESCE(iid.inspection_result, '') = '' THEN '진행중' + ELSE '완료' + END AS inspection_result, + 0 AS inspection_file_cnt FROM incoming_inspection_detail iid - LEFT JOIN purchase_order_master pom ON pom.objid::text = iid.purchase_order_master_objid - LEFT JOIN contract_mgmt cm ON cm.objid = pom.contract_objid - LEFT JOIN code_info ci ON ci.objid = pom.product_cd - LEFT JOIN client_mng client ON client.objid = pom.partner_objid - LEFT JOIN user_info ui ON ui.user_id = iid.inspector_id + LEFT JOIN purchase_order_master pom ON pom.objid::text = iid.purchase_order_master_objid + LEFT JOIN sales_request_master srm ON srm.objid = pom.sales_request_objid + LEFT JOIN contract_mgmt cm ON cm.objid = pom.contract_mgmt_objid + LEFT JOIN code_info ci ON ci.code_value = pom.product_code + LEFT JOIN client_mng client ON client.objid = pom.partner_objid + LEFT JOIN part_mng pm ON pm.objid = pom.part_objid + LEFT JOIN user_info ui_ins ON ui_ins.user_id = iid.inspector_id WHERE ${where.join(" AND ")} - ORDER BY iid.reg_date DESC + ORDER BY COALESCE(NULLIF(iid.inspection_date, ''), TO_CHAR(iid.reg_date, 'YYYY-MM-DD')) DESC NULLS LAST, iid.objid DESC LIMIT 500 `; const r = await pool.query(sql, params); - res.json({ - success: true, - list: r.rows, - pagination: { page: 1, pageSize: 500, total: r.rows.length, totalPages: 1 }, - }); + res.json({ success: true, list: r.rows, pagination: { page: 1, pageSize: 500, total: r.rows.length, totalPages: 1 } }); } catch (err: any) { console.warn("[quality/incoming-mgmt] fallback empty:", err?.message); emptyList(res); @@ -155,65 +174,62 @@ router.get("/incoming-mgmt", async (req: Request, res: Response) => { }); // ─── 3. 공정검사 관리 ────────────────────────────────────────── -// 마스터별로 디테일 N건의 inspection_qty / defect_qty 합계를 집계해 1행 반환. +// wace quality.xml getProcessInspectionList — 마스터별 디테일 SUM. router.get("/process-inspection", async (req: Request, res: Response) => { try { const pool = getPool(); - const { project_no, productType, part_name, inspector_id, from, to } = req.query as Record; + const q = req.query as Record; const where: string[] = ["1=1"]; const params: any[] = []; - if (project_no) { - params.push(`%${project_no}%`); - where.push(`EXISTS (SELECT 1 FROM project_mgmt pm WHERE pm.objid = pid.project_objid AND pm.project_no ILIKE $${params.length})`); - } - if (part_name) { - params.push(`%${part_name}%`); - where.push(`pid.part_name ILIKE $${params.length}`); - } - if (inspector_id) { - params.push(inspector_id); - where.push(`pim.inspector_id = $${params.length}`); - } - if (from) { - params.push(from); - where.push(`pim.inspection_date >= $${params.length}`); - } - if (to) { - params.push(to); - where.push(`pim.inspection_date <= $${params.length}`); - } + const add = (sql: string, ...v: any[]) => { v.forEach(x => params.push(x)); where.push(sql.replace(/\?/g, () => "$" + params.length)); }; + if (q.search_project_no) add("EXISTS (SELECT 1 FROM process_inspection_detail pid2 LEFT JOIN project_mgmt pj ON pj.objid::text = NULLIF(pid2.project_objid, '') WHERE pid2.master_objid = pim.objid AND COALESCE(pj.project_no, '') ILIKE ?)", `%${q.search_project_no}%`); + if (q.productType) add("EXISTS (SELECT 1 FROM process_inspection_detail pid2 LEFT JOIN project_mgmt pj ON pj.objid::text = NULLIF(pid2.project_objid, '') WHERE pid2.master_objid = pim.objid AND pj.product::text = ?)", q.productType); + if (q.search_part_no) add("EXISTS (SELECT 1 FROM process_inspection_detail pid2 WHERE pid2.master_objid = pim.objid AND pid2.part_no ILIKE ?)", `%${q.search_part_no}%`); + if (q.search_part_name) add("EXISTS (SELECT 1 FROM process_inspection_detail pid2 WHERE pid2.master_objid = pim.objid AND pid2.part_name ILIKE ?)", `%${q.search_part_name}%`); + if (q.search_work_env_status) add("EXISTS (SELECT 1 FROM process_inspection_detail pid2 WHERE pid2.master_objid = pim.objid AND pid2.work_env_status = ?)", q.search_work_env_status); + if (q.search_measuring_device) add("EXISTS (SELECT 1 FROM process_inspection_detail pid2 WHERE pid2.master_objid = pim.objid AND pid2.measuring_device = ?)", q.search_measuring_device); + if (q.search_inspector) add("pim.inspector_id = ?", q.search_inspector); + if (q.search_inspection_date_from) add("pim.inspection_date >= ?", q.search_inspection_date_from); + if (q.search_inspection_date_to) add("pim.inspection_date <= ?", q.search_inspection_date_to); + if (q.search_inspection_result) add("EXISTS (SELECT 1 FROM process_inspection_detail pid2 WHERE pid2.master_objid = pim.objid AND pid2.inspection_result = ?)", q.search_inspection_result); + if (q.search_process_cd) add("EXISTS (SELECT 1 FROM process_inspection_detail pid2 WHERE pid2.master_objid = pim.objid AND pid2.process_cd = ?)", q.search_process_cd); + + // pim.inspection_date 가 varchar 이고 빈 문자열인 경우가 많아 reg_date 로 fallback. + // pid.project_objid 도 varchar 빈 문자열이 다수라 NULLIF + 안전 캐스트 필요. const sql = ` SELECT pim.objid::text AS objid, - TO_CHAR(pim.inspection_date, 'YYYY-MM-DD') AS inspection_date, - COALESCE(ui.user_name, '') AS inspector_name, - COALESCE(MIN(pm.project_no), '') AS project_no, - '' AS product_name, + COALESCE(NULLIF(pim.inspection_date, ''), TO_CHAR(pim.reg_date, 'YYYY-MM-DD')) AS inspection_date, + COALESCE(ui_ins.user_name, ui_w.user_name, pim.writer, '') AS inspector_name, + COALESCE(MIN(pj.project_no), '') AS project_no, + COALESCE(MIN(ci.code_name), '') AS product_name, COALESCE(MIN(pid.part_no), '') AS part_no, COALESCE(MIN(pid.part_name), '') AS part_name, COALESCE(SUM(pid.inspection_qty), 0) AS inspection_qty, COALESCE(SUM(pid.defect_qty), 0) AS defect_qty, COALESCE(MAX(pid.work_env_status), '') AS work_env_status, COALESCE(MAX(pid.measuring_device), '') AS measuring_device, - COALESCE(MAX(pid.inspection_result), '') AS inspection_result, - 0 AS file_count + CASE + WHEN SUM(pid.defect_qty) > 0 THEN 'NG' + WHEN COUNT(pid.objid) > 0 THEN 'OK' + ELSE '' + END AS inspection_result, + 0 AS process_inspection_file_cnt FROM process_inspection_master pim LEFT JOIN process_inspection_detail pid ON pid.master_objid = pim.objid - LEFT JOIN project_mgmt pm ON pm.objid = pid.project_objid - LEFT JOIN user_info ui ON ui.user_id = pim.inspector_id + LEFT JOIN project_mgmt pj ON pj.objid::text = NULLIF(pid.project_objid, '') + LEFT JOIN code_info ci ON ci.objid::text = pj.product::text + LEFT JOIN user_info ui_ins ON ui_ins.user_id = NULLIF(pim.inspector_id, '') + LEFT JOIN user_info ui_w ON ui_w.user_id = pim.writer WHERE ${where.join(" AND ")} - GROUP BY pim.objid, pim.inspection_date, ui.user_name - ORDER BY pim.inspection_date DESC NULLS LAST, pim.objid DESC + GROUP BY pim.objid, pim.inspection_date, pim.reg_date, pim.writer, ui_ins.user_name, ui_w.user_name + ORDER BY COALESCE(NULLIF(pim.inspection_date, ''), TO_CHAR(pim.reg_date, 'YYYY-MM-DD')) DESC NULLS LAST, pim.objid DESC LIMIT 500 `; const r = await pool.query(sql, params); - res.json({ - success: true, - list: r.rows, - pagination: { page: 1, pageSize: 500, total: r.rows.length, totalPages: 1 }, - }); + res.json({ success: true, list: r.rows, pagination: { page: 1, pageSize: 500, total: r.rows.length, totalPages: 1 } }); } catch (err: any) { console.warn("[quality/process-inspection] fallback empty:", err?.message); emptyList(res); @@ -221,47 +237,35 @@ router.get("/process-inspection", async (req: Request, res: Response) => { }); // ─── 4. 반제품검사 관리 ──────────────────────────────────────── -// DATA_TYPE='GOOD' 행을 마스터로 보고, 동일 inspection_group_id 의 'DEFECT' 행을 -// SUM 으로 묶어 불량/재생/최종양품 수량을 집계. +// wace quality.xml getSemiProductInspectionList — DATA_TYPE='GOOD' 마스터, +// 동일 INSPECTION_GROUP_ID 의 'DEFECT' 행 SUM 으로 불량/재생/최종양품 산정. router.get("/semi-product-inspection", async (req: Request, res: Response) => { try { const pool = getPool(); - const { model_name, part_no, part_name, writer, from, to } = req.query as Record; + const q = req.query as Record; - const where: string[] = ["g.data_type IS NULL OR g.data_type = 'GOOD'"]; + const where: string[] = ["g.data_type = 'GOOD'"]; const params: any[] = []; - if (model_name) { - params.push(`%${model_name}%`); - where.push(`g.model_name ILIKE $${params.length}`); - } - if (part_no) { - params.push(`%${part_no}%`); - where.push(`g.part_no ILIKE $${params.length}`); - } - if (part_name) { - params.push(`%${part_name}%`); - where.push(`g.part_name ILIKE $${params.length}`); - } - if (writer) { - params.push(writer); - where.push(`g.writer = $${params.length}`); - } - if (from) { - params.push(from); - where.push(`g.inspection_date >= $${params.length}`); - } - if (to) { - params.push(to); - where.push(`g.inspection_date <= $${params.length}`); - } + const add = (sql: string, ...v: any[]) => { v.forEach(x => params.push(x)); where.push(sql.replace(/\?/g, () => "$" + params.length)); }; + + if (q.search_model_name) add("COALESCE(g.model_name, '') ILIKE ?", `%${q.search_model_name}%`); + if (q.search_work_order_no) add("COALESCE(g.work_order_no, '') ILIKE ?", `%${q.search_work_order_no}%`); + if (q.search_part_no) add("COALESCE(g.part_no, '') ILIKE ?", `%${q.search_part_no}%`); + if (q.search_part_name) add("COALESCE(g.part_name, '') ILIKE ?", `%${q.search_part_name}%`); + // 반제품 inspection_date 는 date 타입 → reg_date 와 동일 도메인. + if (q.inspection_start_date) add("COALESCE(g.inspection_date, g.reg_date::date) >= ?::date", q.inspection_start_date); + if (q.inspection_end_date) add("COALESCE(g.inspection_date, g.reg_date::date) <= ?::date", q.inspection_end_date); + if (q.search_writer) add("g.writer = ?", q.search_writer); + if (q.search_defect_type) add("EXISTS (SELECT 1 FROM pms_quality_semi_product_inspection d2 WHERE d2.inspection_group_id = g.inspection_group_id AND d2.data_type = 'DEFECT' AND d2.defect_type = ?)", q.search_defect_type); + if (q.search_responsible_dept) add("EXISTS (SELECT 1 FROM pms_quality_semi_product_inspection d2 WHERE d2.inspection_group_id = g.inspection_group_id AND d2.data_type = 'DEFECT' AND d2.responsible_dept = ?)", q.search_responsible_dept); const sql = ` SELECT g.objid::text AS objid, - TO_CHAR(g.inspection_date, 'YYYY-MM-DD') AS inspection_date, + TO_CHAR(COALESCE(g.inspection_date, g.reg_date::date), 'YYYY-MM-DD') AS inspection_date, COALESCE(ui.user_name, g.writer, '') AS writer_name, - COALESCE(g.model_name, '') AS model_name, COALESCE(g.product_type, '') AS product_type, + COALESCE(g.model_name, '') AS model_name, COALESCE(g.work_order_no, '') AS work_order_no, COALESCE(g.part_no, '') AS part_no, COALESCE(g.part_name, '') AS part_name, @@ -270,7 +274,7 @@ router.get("/semi-product-inspection", async (req: Request, res: Response) => { COALESCE(d.defect_qty_sum, 0) AS defective_qty, CASE WHEN COALESCE(g.receipt_qty, 0) = 0 THEN 0 - ELSE ROUND( COALESCE(d.defect_qty_sum, 0) / g.receipt_qty * 100, 2 ) + ELSE ROUND(COALESCE(d.defect_qty_sum, 0) / g.receipt_qty * 100, 2) END AS defect_rate, COALESCE(d.regen_qty_sum, 0) AS regeneration_qty, COALESCE(g.good_qty, 0) + COALESCE(d.regen_qty_sum, 0) AS final_good_qty @@ -284,15 +288,11 @@ router.get("/semi-product-inspection", async (req: Request, res: Response) => { ) d ON true LEFT JOIN user_info ui ON ui.user_id = g.writer WHERE ${where.join(" AND ")} - ORDER BY g.inspection_date DESC NULLS LAST, g.objid DESC + ORDER BY COALESCE(g.inspection_date, g.reg_date::date) DESC NULLS LAST, g.objid DESC LIMIT 500 `; const r = await pool.query(sql, params); - res.json({ - success: true, - list: r.rows, - pagination: { page: 1, pageSize: 500, total: r.rows.length, totalPages: 1 }, - }); + res.json({ success: true, list: r.rows, pagination: { page: 1, pageSize: 500, total: r.rows.length, totalPages: 1 } }); } catch (err: any) { console.warn("[quality/semi-product-inspection] fallback empty:", err?.message); emptyList(res); diff --git a/docs/migration/quality/03_waceplm_quality_sync.sql b/docs/migration/quality/03_waceplm_quality_sync.sql new file mode 100644 index 00000000..842462ad --- /dev/null +++ b/docs/migration/quality/03_waceplm_quality_sync.sql @@ -0,0 +1,185 @@ +-- 품질관리 — waceplm.esgrin.com 운영 DB(211.115.91.141:11133/waceplm) 스키마/데이터 동기화. +-- 2단계 마이그레이션(02_*.sql)에서 만든 5개 wace_plm 스타일 테이블의 PK 타입이 bigserial 이었으나, +-- 실제 운영 DB 는 varchar(50) 을 사용하므로 DROP 후 운영 스키마 1:1 로 재정합한다. +-- 추가로 운영 환경에만 있는 customer_cs, pms_quality_ecr 도 신규 생성. + +DROP TABLE IF EXISTS public.incoming_inspection_defect CASCADE; +DROP TABLE IF EXISTS public.incoming_inspection_detail CASCADE; +DROP TABLE IF EXISTS public.process_inspection_detail CASCADE; +DROP TABLE IF EXISTS public.process_inspection_master CASCADE; +DROP TABLE IF EXISTS public.pms_quality_semi_product_inspection CASCADE; + +-- 고객 CS (waceplm 운영판) +CREATE TABLE IF NOT EXISTS public.customer_cs ( + objid varchar(50) PRIMARY KEY, + receipt_no varchar(50), + receipt_date varchar(10), + qty numeric, + customer_objid varchar(200), + model_name varchar(200), + product_name varchar(200), + part_no varchar(200), + production_date varchar(10), + sales_date varchar(10), + serial_no varchar(200), + manufacturer varchar(200), + complaint_content varchar(2000), + action_content varchar(2000), + blame_decision varchar(50), + status varchar(50), + remark varchar(2000), + action_date varchar(10), + action_user_id varchar(50), + attach_file_objid varchar(50), + writer varchar(50), + reg_date timestamp, + mod_date timestamp, + part_name varchar, + product_no varchar, + action_type varchar, + receipt_user_id varchar(50) +); + +-- ECR (waceplm 운영 신규 테이블 — 기존 ecr_mng 와는 별도 보존) +CREATE TABLE IF NOT EXISTS public.pms_quality_ecr ( + objid varchar(50) PRIMARY KEY, + ecr_no varchar(50), + request_date varchar(10), + requester_id varchar(50), + part_no varchar(100), + part_name varchar(200), + issue_content text, + due_date varchar(10), + action_dept varchar(50), + action_manager_id varchar(50), + action_user_id varchar(50), + action_content text, + complete_date varchar(10), + attach_file_objid varchar(50), + remark text, + writer varchar(50), + reg_date timestamp, + modifier varchar(50), + mod_date timestamp, + part_objid varchar, + ecr_doc_summary text, + ecr_doc_reason text, + ecr_rev_no varchar(50), + ecr_rev_date varchar(20), + ecr_doc_form_no varchar(50), + ecr_doc_author varchar(100), + change_type varchar +); + +-- 수입검사 진행 상세 +CREATE TABLE IF NOT EXISTS public.incoming_inspection_detail ( + objid varchar(50) PRIMARY KEY, + inventory_in_objid varchar(50), + purchase_order_master_objid varchar, + request_date varchar(10), + request_user_id varchar(50), + inspection_date varchar(10), + inspector_id varchar(50), + inspection_type varchar(50), + inspection_yn varchar(10), + defect_type varchar(50), + defect_reason varchar(200), + action_status varchar(50), + inspection_qty numeric, + defect_qty numeric, + inspection_result varchar(50), + attach_file_objid varchar(50), + remark varchar(2000), + writer varchar(50), + reg_date timestamp +); + +-- 수입검사 불량 상세 +CREATE TABLE IF NOT EXISTS public.incoming_inspection_defect ( + objid varchar(50) PRIMARY KEY, + inspection_detail_objid varchar(50), + inspection_type varchar(50), + inspection_date varchar(10), + inspector_id varchar(50), + defect_type varchar(50), + defect_reason varchar(200), + action_status varchar(50), + action_result varchar(200), + inspection_qty numeric, + defect_qty numeric, + inspection_result varchar(50), + remark varchar(2000), + writer varchar(50), + reg_date timestamp +); + +-- 공정검사 마스터/디테일 +CREATE TABLE IF NOT EXISTS public.process_inspection_master ( + objid varchar(50) PRIMARY KEY, + inspection_date varchar(10), + inspector_id varchar(50), + remark varchar(2000), + writer varchar(50), + reg_date timestamp, + mod_date timestamp +); + +CREATE TABLE IF NOT EXISTS public.process_inspection_detail ( + objid varchar(50) PRIMARY KEY, + master_objid varchar(50), + process_cd varchar(50), + project_objid varchar(50), + part_objid varchar(50), + part_no varchar(200), + part_name varchar(200), + inspection_qty numeric, + defect_qty numeric, + work_env_status varchar(10), + measuring_device varchar(10), + dept_cd varchar(50), + user_id varchar(50), + inspection_date varchar(10), + inspector_id varchar(50), + remark varchar(2000), + action_status varchar(50), + inspection_result varchar(10), + writer varchar(50), + reg_date timestamp, + mod_date timestamp +); + +-- 반제품검사 (GOOD/DEFECT 통합) +CREATE TABLE IF NOT EXISTS public.pms_quality_semi_product_inspection ( + objid varchar(50) PRIMARY KEY, + project_no varchar(200), + work_order_no varchar(100), + part_no varchar(200), + part_name varchar(200), + receipt_qty numeric, + good_qty numeric, + defect_qty numeric, + disposition_type varchar(50), + remark varchar(2000), + writer varchar(50), + reg_date timestamp, + inspection_group_id varchar(50), + data_type varchar(20), + defect_type varchar(50), + defect_cause varchar(200), + responsible_dept varchar(50), + worker varchar(50), + process_status varchar(50), + inspection_date varchar(10), + inspector varchar(50), + model_name varchar(200), + product_type varchar(50), + is_locked varchar(1) +); + +-- 데이터 복사 (운영 → 로컬, ON CONFLICT DO NOTHING). 실제 실행은 node script 로 진행. +-- customer_cs 24 rows +-- pms_quality_ecr 1 +-- process_inspection_master 3 +-- process_inspection_detail 13 +-- incoming_inspection_detail 1 +-- pms_quality_semi_product_inspection 84 diff --git a/frontend/app/(main)/COMPANY_16/quality/incoming-mgmt/page.tsx b/frontend/app/(main)/COMPANY_16/quality/incoming-mgmt/page.tsx index c900f2d9..3c057d17 100644 --- a/frontend/app/(main)/COMPANY_16/quality/incoming-mgmt/page.tsx +++ b/frontend/app/(main)/COMPANY_16/quality/incoming-mgmt/page.tsx @@ -1,38 +1,47 @@ "use client"; /** - * 수입검사 관리 — wace_plm incomingInspectionProgressList.jsp 이식. - * - * 수입검사 요청 기반에 검사자/검사일/검사결과/불량수량 컬럼이 추가됨. + * 수입검사 진행 — wace_plm 의 incomingInspectionProgressList.jsp 1:1 이식. + * 필터 11종, 그리드 19컬럼 (불량률 자동산정, 검사현황 컬러표시). */ import React, { useCallback, useEffect, useState } from "react"; import { Plus } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { toast } from "sonner"; import { useAuth } from "@/hooks/useAuth"; import { DataGrid, DataGridColumn } from "@/components/common/DataGrid"; import { PageHeader } from "@/components/common/PageHeader"; -import { CompactFilterBar, CompactFilterField } from "@/components/common/CompactFilterBar"; +import { CompactFilterBar, CompactFilterField, CompactDateRange } from "@/components/common/CompactFilterBar"; import { qualityApi, IncomingMgmtRow } from "@/lib/api/quality"; import { exportToExcel } from "@/lib/utils/excelExport"; +// wace_plm incomingInspectionProgressList.jsp 그리드 1:1 const GRID_COLUMNS: DataGridColumn[] = [ - { key: "purchase_order_no", label: "발주서 No", width: "w-[150px]", frozen: true }, - { key: "project_no", label: "프로젝트번호", width: "w-[150px]" }, - { key: "product_name", label: "제품구분", width: "w-[110px]", align: "center" }, - { key: "part_no", label: "품번", width: "w-[140px]" }, - { key: "part_name", label: "품명", width: "w-[200px]" }, - { key: "partner_name", label: "공급업체", width: "w-[160px]" }, - { key: "inspection_date", label: "검사일", width: "w-[115px]", align: "center" }, - { key: "inspector_name", label: "검사자", width: "w-[110px]", align: "center" }, - { key: "total_qty", label: "검사수량", width: "w-[110px]", align: "right", formatNumber: true }, - { key: "good_qty", label: "양품수량", width: "w-[110px]", align: "right", formatNumber: true }, - { key: "bad_qty", label: "불량수량", width: "w-[110px]", align: "right", formatNumber: true }, - { key: "inspection_result", label: "검사결과", width: "w-[100px]", align: "center" }, - { key: "request_status", label: "요청현황", width: "w-[110px]", align: "center" }, + { key: "inspection_date", label: "검사일", width: "w-[110px]", align: "center", frozen: true }, + { key: "inspector_name", label: "검사자", width: "w-[90px]", align: "center" }, + { key: "proposal_no", label: "품의서 No", width: "w-[120px]", align: "center" }, + { key: "purchase_order_no", label: "발주서 No", width: "w-[120px]", align: "center" }, + { key: "project_no", label: "프로젝트번호", width: "w-[140px]", align: "center" }, + { key: "product_name", label: "제품구분", width: "w-[100px]", align: "center" }, + { key: "model_name", label: "품명(모델명)", width: "w-[150px]" }, + { key: "part_no", label: "부품품번", width: "w-[140px]" }, + { key: "part_name", label: "부품명", width: "w-[150px]" }, + { key: "partner_name", label: "공급업체", width: "w-[150px]" }, + { key: "delivery_date", label: "입고일", width: "w-[110px]", align: "center" }, + { key: "delivery_qty", label: "입고수량", width: "w-[100px]", align: "right", formatNumber: true }, + { key: "delivery_status", label: "입고결과", width: "w-[90px]", align: "center" }, + { key: "inspection_qty", label: "검사수량", width: "w-[100px]", align: "right", formatNumber: true }, + { key: "defect_qty_sum", label: "불량수량", width: "w-[100px]", align: "right", formatNumber: true }, + { key: "defect_rate", label: "불량률(%)", width: "w-[100px]", align: "right" }, + { key: "inspection_result", label: "검사현황", width: "w-[100px]", align: "center" }, + { key: "inspection_file_cnt", label: "검사성적서", width: "w-[100px]", align: "center", renderType: "clip" }, ]; +const INSPECTION_STATUS_OPTIONS = ["전체", "완료", "진행중"]; +const DELIVERY_STATUS_OPTIONS = ["전체", "입고중", "입고완료", "지연"]; + export default function IncomingMgmtPage() { const { user } = useAuth(); const [rows, setRows] = useState([]); @@ -40,30 +49,58 @@ export default function IncomingMgmtPage() { const [selectedId, setSelectedId] = useState(null); const [search, setSearch] = useState({ + search_proposal_no: "", + search_purchase_order_no: "", project_no: "", - partner_objid: "", + search_part_no: "", + search_part_name: "", + search_partner: "", + search_delivery_status: "", + search_product_cd: "", inspector_id: "", + inspection_start_date: "", + inspection_end_date: "", + search_inspection_status: "", }); const fetchList = useCallback(async () => { if (!user) return; setLoading(true); try { - const params: Record = {}; - Object.entries(search).forEach(([k, v]) => { if (v) params[k] = v; }); - const res = await qualityApi.incomingMgmt(params); + const res = await qualityApi.incomingMgmt(search); setRows(res.list.map((r) => ({ ...r, id: r.objid } as any))); setSelectedId(null); - } catch { - toast.error("수입검사 관리 목록 조회 실패"); - } finally { - setLoading(false); - } + } catch { toast.error("수입검사 진행 목록 조회 실패"); } + finally { setLoading(false); } }, [user, search]); useEffect(() => { fetchList(); }, [fetchList]); - const handleReset = () => setSearch({ project_no: "", partner_objid: "", inspector_id: "" }); + const handleReset = () => setSearch({ + search_proposal_no: "", search_purchase_order_no: "", project_no: "", + search_part_no: "", search_part_name: "", search_partner: "", + search_delivery_status: "", search_product_cd: "", + inspector_id: "", inspection_start_date: "", inspection_end_date: "", + search_inspection_status: "", + }); + + const summary = (() => { + const fmt = (n: number) => n.toLocaleString(); + return [ + { label: "총 입고수량", value: fmt(rows.reduce((a, r) => a + Number(r.delivery_qty || 0), 0)) }, + { label: "총 검사수량", value: fmt(rows.reduce((a, r) => a + Number(r.inspection_qty || 0), 0)) }, + { label: "총 불량수량", value: fmt(rows.reduce((a, r) => a + Number(r.defect_qty_sum || 0), 0)) }, + ]; + })(); + + const HardcodedSelect = ({ value, onChange, options }: { value: string; onChange: (v: string) => void; options: string[] }) => ( + + ); return (
@@ -73,22 +110,46 @@ export default function IncomingMgmtPage() { onReset={handleReset} actions={ } /> 총 {rows.length.toLocaleString()}건}> - - setSearch({ ...search, project_no: e.target.value })} /> + + setSearch({ ...search, search_proposal_no: e.target.value })} /> - - setSearch({ ...search, partner_objid: e.target.value })} /> + + setSearch({ ...search, search_purchase_order_no: e.target.value })} /> - - setSearch({ ...search, inspector_id: e.target.value })} /> + + setSearch({ ...search, project_no: e.target.value })} /> + + + setSearch({ ...search, search_part_no: e.target.value })} /> + + + setSearch({ ...search, search_part_name: e.target.value })} /> + + + setSearch({ ...search, search_partner: e.target.value })} /> + + + setSearch({ ...search, search_delivery_status: v })} options={DELIVERY_STATUS_OPTIONS} /> + + + setSearch({ ...search, search_product_cd: e.target.value })} /> + + + setSearch({ ...search, inspector_id: e.target.value })} /> + + + setSearch({ ...search, inspection_start_date: v })} + to={search.inspection_end_date} setTo={(v) => setSearch({ ...search, inspection_end_date: v })} + /> + + + setSearch({ ...search, search_inspection_status: v })} options={INSPECTION_STATUS_OPTIONS} /> ([]); @@ -40,30 +45,51 @@ export default function IncomingRequestPage() { const [selectedId, setSelectedId] = useState(null); const [search, setSearch] = useState({ + search_proposal_no: "", + search_purchase_order_no: "", project_no: "", - partner_objid: "", + search_part_no: "", + search_part_name: "", + search_partner: "", + search_delivery_status: "", + search_product_cd: "", + search_inspection_yn: "", + search_request_status: "", request_user_id: "", + request_start_date: "", + request_end_date: "", }); const fetchList = useCallback(async () => { if (!user) return; setLoading(true); try { - const params: Record = {}; - Object.entries(search).forEach(([k, v]) => { if (v) params[k] = v; }); - const res = await qualityApi.incomingRequest(params); + const res = await qualityApi.incomingRequest(search); setRows(res.list.map((r) => ({ ...r, id: r.objid } as any))); setSelectedId(null); - } catch (e: any) { + } catch { toast.error("수입검사 요청 목록 조회 실패"); - } finally { - setLoading(false); - } + } finally { setLoading(false); } }, [user, search]); useEffect(() => { fetchList(); }, [fetchList]); - const handleReset = () => setSearch({ project_no: "", partner_objid: "", request_user_id: "" }); + const handleReset = () => setSearch({ + search_proposal_no: "", search_purchase_order_no: "", project_no: "", + search_part_no: "", search_part_name: "", search_partner: "", + search_delivery_status: "", search_product_cd: "", + search_inspection_yn: "", search_request_status: "", + request_user_id: "", request_start_date: "", request_end_date: "", + }); + + const HardcodedSelect = ({ value, onChange, options }: { value: string; onChange: (v: string) => void; options: string[] }) => ( + + ); return (
@@ -78,17 +104,44 @@ export default function IncomingRequestPage() { } /> 총 {rows.length.toLocaleString()}건}> - - setSearch({ ...search, project_no: e.target.value })} /> + + setSearch({ ...search, search_proposal_no: e.target.value })} /> - - setSearch({ ...search, partner_objid: e.target.value })} /> + + setSearch({ ...search, search_purchase_order_no: e.target.value })} /> - - setSearch({ ...search, request_user_id: e.target.value })} /> + + setSearch({ ...search, project_no: e.target.value })} /> + + + setSearch({ ...search, search_part_no: e.target.value })} /> + + + setSearch({ ...search, search_part_name: e.target.value })} /> + + + setSearch({ ...search, search_partner: e.target.value })} /> + + + setSearch({ ...search, search_delivery_status: v })} options={DELIVERY_STATUS_OPTIONS} /> + + + setSearch({ ...search, search_product_cd: e.target.value })} /> + + + setSearch({ ...search, search_inspection_yn: v })} options={INSPECTION_YN_OPTIONS} /> + + + setSearch({ ...search, search_request_status: v })} options={REQUEST_STATUS_OPTIONS} /> + + + setSearch({ ...search, request_user_id: e.target.value })} /> + + + setSearch({ ...search, request_start_date: v })} + to={search.request_end_date} setTo={(v) => setSearch({ ...search, request_end_date: v })} + /> ([]); @@ -39,34 +39,48 @@ export default function ProcessInspectionPage() { const [selectedId, setSelectedId] = useState(null); const [search, setSearch] = useState({ - project_no: "", + search_project_no: "", productType: "", - part_name: "", - inspector_id: "", - from: "", - to: "", + search_part_no: "", + search_part_name: "", + search_work_env_status: "", + search_measuring_device: "", + search_inspection_date_from: "", + search_inspection_date_to: "", + search_inspector: "", + search_inspection_result: "", + search_process_cd: "", }); const fetchList = useCallback(async () => { if (!user) return; setLoading(true); try { - const params: Record = {}; - Object.entries(search).forEach(([k, v]) => { if (v) params[k] = v; }); - const res = await qualityApi.processInspection(params); + const res = await qualityApi.processInspection(search); setRows(res.list.map((r) => ({ ...r, id: r.objid } as any))); setSelectedId(null); - } catch { - toast.error("공정검사 목록 조회 실패"); - } finally { - setLoading(false); - } + } catch { toast.error("공정검사 목록 조회 실패"); } + finally { setLoading(false); } }, [user, search]); useEffect(() => { fetchList(); }, [fetchList]); - const handleReset = () => - setSearch({ project_no: "", productType: "", part_name: "", inspector_id: "", from: "", to: "" }); + const handleReset = () => setSearch({ + search_project_no: "", productType: "", + search_part_no: "", search_part_name: "", + search_work_env_status: "", search_measuring_device: "", + search_inspection_date_from: "", search_inspection_date_to: "", + search_inspector: "", search_inspection_result: "", search_process_cd: "", + }); + + const HardcodedSelect = ({ value, onChange, options }: { value: string; onChange: (v: string) => void; options: string[] }) => ( + + ); return (
@@ -81,28 +95,39 @@ export default function ProcessInspectionPage() { } /> 총 {rows.length.toLocaleString()}건}> - - setSearch({ ...search, project_no: e.target.value })} /> + + setSearch({ ...search, search_project_no: e.target.value })} /> - - setSearch({ ...search, productType: e.target.value })} /> + + setSearch({ ...search, productType: e.target.value })} /> + + + setSearch({ ...search, search_part_no: e.target.value })} /> - setSearch({ ...search, part_name: e.target.value })} /> + setSearch({ ...search, search_part_name: e.target.value })} /> - - setSearch({ ...search, inspector_id: e.target.value })} /> + + setSearch({ ...search, search_work_env_status: v })} options={OK_NG_OPTIONS} /> + + + setSearch({ ...search, search_measuring_device: v })} options={OK_NG_OPTIONS} /> setSearch({ ...search, from: v })} - to={search.to} setTo={(v) => setSearch({ ...search, to: v })} + from={search.search_inspection_date_from} setFrom={(v) => setSearch({ ...search, search_inspection_date_from: v })} + to={search.search_inspection_date_to} setTo={(v) => setSearch({ ...search, search_inspection_date_to: v })} /> + + setSearch({ ...search, search_inspector: e.target.value })} /> + + + setSearch({ ...search, search_inspection_result: v })} options={OK_NG_OPTIONS} /> + + + setSearch({ ...search, search_process_cd: e.target.value })} /> + (null); const [search, setSearch] = useState({ - model_name: "", - part_no: "", - part_name: "", - writer: "", - from: "", - to: "", + search_model_name: "", + search_work_order_no: "", + search_part_no: "", + search_part_name: "", + inspection_start_date: "", + inspection_end_date: "", + search_writer: "", + search_defect_type: "", + search_responsible_dept: "", }); const fetchList = useCallback(async () => { if (!user) return; setLoading(true); try { - const params: Record = {}; - Object.entries(search).forEach(([k, v]) => { if (v) params[k] = v; }); - const res = await qualityApi.semiProductInspection(params); + const res = await qualityApi.semiProductInspection(search); setRows(res.list.map((r) => ({ ...r, id: r.objid } as any))); setSelectedId(null); - } catch { - toast.error("반제품검사 목록 조회 실패"); - } finally { - setLoading(false); - } + } catch { toast.error("반제품검사 목록 조회 실패"); } + finally { setLoading(false); } }, [user, search]); useEffect(() => { fetchList(); }, [fetchList]); - const handleReset = () => - setSearch({ model_name: "", part_no: "", part_name: "", writer: "", from: "", to: "" }); + const handleReset = () => setSearch({ + search_model_name: "", search_work_order_no: "", + search_part_no: "", search_part_name: "", + inspection_start_date: "", inspection_end_date: "", + search_writer: "", search_defect_type: "", search_responsible_dept: "", + }); + + const summary = (() => { + const fmt = (n: number) => n.toLocaleString(); + return [ + { label: "총 입고수량", value: fmt(rows.reduce((a, r) => a + Number(r.receipt_qty || 0), 0)) }, + { label: "총 양품수량", value: fmt(rows.reduce((a, r) => a + Number(r.good_qty || 0), 0)) }, + { label: "총 불량수량", value: fmt(rows.reduce((a, r) => a + Number(r.defective_qty || 0), 0)) }, + { label: "총 재생수량", value: fmt(rows.reduce((a, r) => a + Number(r.regeneration_qty || 0), 0)) }, + { label: "총 최종양품수량", value: fmt(rows.reduce((a, r) => a + Number(r.final_good_qty || 0), 0)) }, + ]; + })(); return (
@@ -82,28 +96,33 @@ export default function SemiProductInspectionPage() { } /> 총 {rows.length.toLocaleString()}건}> - - setSearch({ ...search, model_name: e.target.value })} /> + + setSearch({ ...search, search_model_name: e.target.value })} /> - - setSearch({ ...search, part_no: e.target.value })} /> + + setSearch({ ...search, search_work_order_no: e.target.value })} /> + + + setSearch({ ...search, search_part_no: e.target.value })} /> - setSearch({ ...search, part_name: e.target.value })} /> - - - setSearch({ ...search, writer: e.target.value })} /> + setSearch({ ...search, search_part_name: e.target.value })} /> setSearch({ ...search, from: v })} - to={search.to} setTo={(v) => setSearch({ ...search, to: v })} + from={search.inspection_start_date} setFrom={(v) => setSearch({ ...search, inspection_start_date: v })} + to={search.inspection_end_date} setTo={(v) => setSearch({ ...search, inspection_end_date: v })} /> + + setSearch({ ...search, search_writer: e.target.value })} /> + + + setSearch({ ...search, search_defect_type: e.target.value })} /> + + + setSearch({ ...search, search_responsible_dept: e.target.value })} /> + { pagination: { page: number; pageSize: number; total: number; totalPages: number }; } -export const qualityApi = { - incomingRequest: async (params: Record = {}): Promise> => - (await apiClient.get("/quality/incoming-request", { params })).data, - incomingMgmt: async (params: Record = {}): Promise> => - (await apiClient.get("/quality/incoming-mgmt", { params })).data, - processInspection: async (params: Record = {}): Promise> => - (await apiClient.get("/quality/process-inspection", { params })).data, - semiProductInspection: async (params: Record = {}): Promise> => - (await apiClient.get("/quality/semi-product-inspection", { params })).data, +const buildParams = (obj: Record): Record => { + const p: Record = {}; + Object.entries(obj).forEach(([k, v]) => { if (v) p[k] = v; }); + return p; +}; + +export const qualityApi = { + incomingRequest: async (params: Record = {}): Promise> => + (await apiClient.get("/quality/incoming-request", { params: buildParams(params) })).data, + incomingMgmt: async (params: Record = {}): Promise> => + (await apiClient.get("/quality/incoming-mgmt", { params: buildParams(params) })).data, + processInspection: async (params: Record = {}): Promise> => + (await apiClient.get("/quality/process-inspection", { params: buildParams(params) })).data, + semiProductInspection: async (params: Record = {}): Promise> => + (await apiClient.get("/quality/semi-product-inspection", { params: buildParams(params) })).data, };