From adcb7a7764edda7d6d6dac9ab776238a5cd49418 Mon Sep 17 00:00:00 2001 From: chpark Date: Fri, 15 May 2026 11:22:48 +0900 Subject: [PATCH] =?UTF-8?q?=ED=92=88=EC=A7=88=EA=B4=80=EB=A6=AC=204?= =?UTF-8?q?=EB=A9=94=EB=89=B4=20=E2=80=94=20wace=5Fplm=20JSP=20+=20quality?= =?UTF-8?q?.xml=20MyBatis=201:1=20=EC=9E=AC=EC=A0=95=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 수입검사 요청 (incoming-request): - 필터 12종 (품의서/발주서/프로젝트/품번/품명/공급업체/입고결과/제품구분/ 검사여부/요청현황/요청자/요청일범위) - 그리드 12컬럼 (proposal_no, purchase_order_no, project_no, product_name, part_no, part_name, partner_name, delivery_status, request_date, request_user_name, inspection_yn, request_status) - 백엔드: purchase_order_master + incoming_inspection_detail LEFT JOIN + sales_request_master + contract_mgmt + part_mng + user_info 수입검사 진행 (incoming-mgmt): - 필터 11종 (수입요청과 유사 + 검사일범위 + 검사현황) - 그리드 19컬럼 (검사일/검사자/품의서/발주서/프로젝트/제품구분/품명모델/ 부품품번/부품명/공급업체/입고일/입고수량/입고결과/검사수량/불량수량/ 불량률/검사현황/검사성적서) - SUM(defect_qty) 서브쿼리로 불량률 자동 계산 - 하단 요약: 총 입고수량/검사수량/불량수량 공정검사 관리 (process-inspection): - 필터 10종 (프로젝트/제품구분/품번/품명/작업환경/측정기/검사일범위/ 검사자/검사결과/진행공정) - 그리드 9컬럼 (검사일/검사자/프로젝트/제품구분/품번/품명/검사수량합계/ 검사결과/첨부파일) - master/detail 집계 + EXISTS 필터로 wace 1:1 반제품검사 관리 (semi-product-inspection): - 필터 8종 (모델명/작업지시번호/부품품번/부품명/검사일범위/검사자/ 불량유형/귀책부서) - 그리드 14컬럼 (검사일/검사자/제품구분/모델명/작업지시번호/부품품번/ 부품명/입고수량/양품수량/불량수량/불량률/재생수량/최종양품수량) - data_type='GOOD' 마스터 + 동일 inspection_group_id 의 'DEFECT' SUM - 재생수량(disposition_type='수정완료') + 최종양품수량 자동 산정 - 하단 요약: 5개 합계 카드 frontend/lib/api/quality.ts 타입 1:1 정합, 모든 필터 파라미터 직렬화. --- backend-node/src/routes/qualityRoutes.ts | 297 +++++++++--------- .../COMPANY_16/quality/incoming-mgmt/page.tsx | 136 +++++--- .../quality/incoming-request/page.tsx | 123 +++++--- .../quality/process-inspection/page.tsx | 105 ++++--- .../quality/semi-product-inspection/page.tsx | 112 ++++--- frontend/lib/api/quality.ts | 55 ++-- 6 files changed, 500 insertions(+), 328 deletions(-) diff --git a/backend-node/src/routes/qualityRoutes.ts b/backend-node/src/routes/qualityRoutes.ts index c664edf0..25c74e05 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,143 @@ 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.proposal_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_cd::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(iid.request_date, pom.regdate::date) >= ?", q.request_start_date); + if (q.request_end_date) add("COALESCE(iid.request_date, pom.regdate::date) <= ?", 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, + pom.objid::text AS objid, + COALESCE(srm.proposal_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.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, + 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_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 part_mng pm ON pm.objid = pom.part_objid 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"]; + const where: string[] = ["iid.inspection_yn = '검사'"]; 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.proposal_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_cd::text = ?", q.search_product_cd); + if (q.inspector_id) add("iid.inspector_id = ?", q.inspector_id); + if (q.inspection_start_date) add("iid.inspection_date >= ?", q.inspection_start_date); + if (q.inspection_end_date) add("iid.inspection_date <= ?", 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, + TO_CHAR(iid.inspection_date, 'YYYY-MM-DD') AS inspection_date, + COALESCE(ui_ins.user_name, '') AS inspector_name, + COALESCE(srm.proposal_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(pm.product_name, '') AS model_name, + COALESCE(pm.part_no, '') AS part_no, + COALESCE(pm.part_name, '') 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, + 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_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 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 iid.inspection_date 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 +171,59 @@ 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 = 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 = pid2.project_objid WHERE pid2.master_objid = pim.objid AND pj.product = ?)", 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); 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(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 = pid.project_objid + LEFT JOIN code_info ci ON ci.objid::text = pj.product + LEFT JOIN user_info ui ON ui.user_id = pim.inspector_id 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 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 +231,34 @@ 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}%`); + if (q.inspection_start_date) add("COALESCE(g.inspection_date, g.reg_date::date) >= ?", q.inspection_start_date); + if (q.inspection_end_date) add("COALESCE(g.inspection_date, g.reg_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, 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 +267,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 +281,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/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, };