From 0b7c967f563a06e8b7d88d2ca11c5930008f0a2e Mon Sep 17 00:00:00 2001 From: kjs Date: Tue, 28 Apr 2026 18:15:22 +0900 Subject: [PATCH] Refactor analytics report data retrieval and improve logistics info page loading logic - Updated the analytics report controller to correctly join work_order_process and work_order_process_result for accurate production data retrieval. - Enhanced the logistics info page to load all tabs on mount for immediate count accuracy, while ensuring only the active tab is reloaded on subsequent changes to prevent race conditions. This refactor improves data accuracy and user experience in the logistics module. --- .../controllers/analyticsReportController.ts | 28 ++++++++++--------- .../(main)/COMPANY_10/logistics/info/page.tsx | 17 +++++------ .../(main)/COMPANY_16/logistics/info/page.tsx | 17 +++++------ .../quality/inspection-result/page.tsx | 6 ++++ .../(main)/COMPANY_29/logistics/info/page.tsx | 17 +++++------ .../(main)/COMPANY_30/logistics/info/page.tsx | 17 +++++------ .../(main)/COMPANY_7/logistics/info/page.tsx | 17 +++++------ .../(main)/COMPANY_8/logistics/info/page.tsx | 17 +++++------ .../(main)/COMPANY_9/logistics/info/page.tsx | 17 +++++------ 9 files changed, 84 insertions(+), 69 deletions(-) diff --git a/backend-node/src/controllers/analyticsReportController.ts b/backend-node/src/controllers/analyticsReportController.ts index e2f73653..d5d843b1 100644 --- a/backend-node/src/controllers/analyticsReportController.ts +++ b/backend-node/src/controllers/analyticsReportController.ts @@ -59,37 +59,39 @@ export async function getProductionReportData(req: any, res: Response): Promise< const cf = buildCompanyFilter(companyCode, "wop", idx); if (cf.condition) { conditions.push(cf.condition); params.push(...cf.params); idx = cf.nextIdx; } - const dateExpr = "COALESCE(NULLIF(wop.started_at, ''), wop.created_date::date::text)"; + const dateExpr = "COALESCE(NULLIF(wopr.started_at, ''), wop.created_date::date::text)"; const df = buildDateFilter(startDate, endDate, dateExpr, idx); conditions.push(...df.conditions); params.push(...df.params); idx = df.nextIdx; const whereClause = buildWhereClause(conditions); - // 실제 공정별 생산 데이터는 work_order_process에 있음 - // (work_instruction.routing은 routing_version_id UUID일 뿐이라 공정명이 아님) + // 공정 메타(process_code/name/plan_qty)는 work_order_process, + // 실적(started_at/completed_at/good_qty/defect_qty/equipment_code 등)은 work_order_process_result에 있음 const dataQuery = ` SELECT - COALESCE(NULLIF(wop.started_at, ''), wop.created_date::date::text) as date, + COALESCE(NULLIF(wopr.started_at, ''), wop.created_date::date::text) as date, COALESCE(NULLIF(wop.process_name, ''), NULLIF(wop.process_code, ''), '미지정') as process, COALESCE(NULLIF(em.equipment_name, ''), NULLIF(em.equipment_code, ''), '미지정') as equipment, COALESCE(NULLIF(ii.item_name, ''), NULLIF(ii.item_number, ''), '미지정') as item, COALESCE(NULLIF(wi.worker, ''), '미지정') as worker, CAST(COALESCE(NULLIF(wop.plan_qty, ''), '0') AS numeric) as "planQty", - CAST(COALESCE(NULLIF(wop.good_qty, ''), '0') AS numeric) as "prodQty", - CAST(COALESCE(NULLIF(wop.defect_qty, ''), '0') AS numeric) as "defectQty", + CAST(COALESCE(NULLIF(wopr.good_qty, ''), '0') AS numeric) as "prodQty", + CAST(COALESCE(NULLIF(wopr.defect_qty, ''), '0') AS numeric) as "defectQty", CASE - WHEN NULLIF(wop.started_at, '') IS NOT NULL - AND NULLIF(wop.completed_at, '') IS NOT NULL + WHEN NULLIF(wopr.started_at, '') IS NOT NULL + AND NULLIF(wopr.completed_at, '') IS NOT NULL THEN GREATEST( - EXTRACT(EPOCH FROM (wop.completed_at::timestamp - wop.started_at::timestamp)) / 3600.0, + EXTRACT(EPOCH FROM (wopr.completed_at::timestamp - wopr.started_at::timestamp)) / 3600.0, 0 ) ELSE 0 END as "runTime", - CAST(COALESCE(NULLIF(wop.total_paused_time, ''), '0') AS numeric) / 3600.0 as "downTime", - wop.status, + CAST(COALESCE(NULLIF(wopr.total_paused_time, ''), '0') AS numeric) / 3600.0 as "downTime", + wopr.status, wop.company_code FROM work_order_process wop + LEFT JOIN work_order_process_result wopr + ON wopr.wop_id = wop.id AND wopr.company_code = wop.company_code LEFT JOIN work_instruction wi ON wop.wo_id = wi.id AND wop.company_code = wi.company_code LEFT JOIN LATERAL ( @@ -97,8 +99,8 @@ export async function getProductionReportData(req: any, res: Response): Promise< FROM equipment_mng WHERE company_code = wi.company_code AND (id = wi.equipment_id OR equipment_code = wi.equipment_id - OR id = wop.equipment_code OR equipment_code = wop.equipment_code) - ORDER BY (id = wi.equipment_id OR id = wop.equipment_code) DESC, created_date DESC + OR id = wopr.equipment_code OR equipment_code = wopr.equipment_code) + ORDER BY (id = wi.equipment_id OR id = wopr.equipment_code) DESC, created_date DESC LIMIT 1 ) em ON true LEFT JOIN LATERAL ( diff --git a/frontend/app/(main)/COMPANY_10/logistics/info/page.tsx b/frontend/app/(main)/COMPANY_10/logistics/info/page.tsx index f799c3d2..ebcbf314 100644 --- a/frontend/app/(main)/COMPANY_10/logistics/info/page.tsx +++ b/frontend/app/(main)/COMPANY_10/logistics/info/page.tsx @@ -419,17 +419,18 @@ export default function LogisticsInfoPage() { } }, []); - // 초기 데이터 로드 (탭 전환 시) + // 마운트 시 5개 탭 전체 로드(카운트 즉시 정확), 이후 탭 전환 시 활성 탭만 재로드 + // 두 useEffect를 분리하면 mount 시 동시 실행으로 race condition 발생 → 단일 useEffect + ref 가드 + const isInitialLoadRef = useRef(true); useEffect(() => { - fetchTabData(activeTab); + if (isInitialLoadRef.current) { + isInitialLoadRef.current = false; + TAB_CONFIGS.forEach((c) => fetchTabData(c.key)); + } else { + fetchTabData(activeTab); + } }, [activeTab, fetchTabData]); - // 마운트 시 5개 탭 전체 카운트 로드 — 탭 배지 숫자가 진입 즉시 정확히 표시되도록 - useEffect(() => { - TAB_CONFIGS.forEach((c) => fetchTabData(c.key)); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - // 탭 변경 const handleTabChange = useCallback((tab: string) => { setActiveTab(tab as TabKey); diff --git a/frontend/app/(main)/COMPANY_16/logistics/info/page.tsx b/frontend/app/(main)/COMPANY_16/logistics/info/page.tsx index 7f50e5e2..03f2ff30 100644 --- a/frontend/app/(main)/COMPANY_16/logistics/info/page.tsx +++ b/frontend/app/(main)/COMPANY_16/logistics/info/page.tsx @@ -419,17 +419,18 @@ export default function LogisticsInfoPage() { } }, []); - // 초기 데이터 로드 (탭 전환 시) + // 마운트 시 5개 탭 전체 로드(카운트 즉시 정확), 이후 탭 전환 시 활성 탭만 재로드 + // 두 useEffect를 분리하면 mount 시 동시 실행으로 race condition 발생 → 단일 useEffect + ref 가드 + const isInitialLoadRef = useRef(true); useEffect(() => { - fetchTabData(activeTab); + if (isInitialLoadRef.current) { + isInitialLoadRef.current = false; + TAB_CONFIGS.forEach((c) => fetchTabData(c.key)); + } else { + fetchTabData(activeTab); + } }, [activeTab, fetchTabData]); - // 마운트 시 5개 탭 전체 카운트 로드 — 탭 배지 숫자가 진입 즉시 정확히 표시되도록 - useEffect(() => { - TAB_CONFIGS.forEach((c) => fetchTabData(c.key)); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - // 탭 변경 const handleTabChange = useCallback((tab: string) => { setActiveTab(tab as TabKey); diff --git a/frontend/app/(main)/COMPANY_16/quality/inspection-result/page.tsx b/frontend/app/(main)/COMPANY_16/quality/inspection-result/page.tsx index d4093cab..2c51c171 100644 --- a/frontend/app/(main)/COMPANY_16/quality/inspection-result/page.tsx +++ b/frontend/app/(main)/COMPANY_16/quality/inspection-result/page.tsx @@ -310,6 +310,7 @@ export default function InspectionResultPage() { # + 검사유형 검사항목 검사기준 합격기준 @@ -323,6 +324,11 @@ export default function InspectionResultPage() { {detailData.map((d, idx) => ( {idx + 1} + + {d.inspection_type ? ( + {d.inspection_type} + ) : "-"} + {d.inspection_item_name || "-"} {d.inspection_standard || "-"} {d.pass_criteria || "-"} diff --git a/frontend/app/(main)/COMPANY_29/logistics/info/page.tsx b/frontend/app/(main)/COMPANY_29/logistics/info/page.tsx index e6d7858b..1ab90f3b 100644 --- a/frontend/app/(main)/COMPANY_29/logistics/info/page.tsx +++ b/frontend/app/(main)/COMPANY_29/logistics/info/page.tsx @@ -419,17 +419,18 @@ export default function LogisticsInfoPage() { } }, []); - // 초기 데이터 로드 (탭 전환 시) + // 마운트 시 5개 탭 전체 로드(카운트 즉시 정확), 이후 탭 전환 시 활성 탭만 재로드 + // 두 useEffect를 분리하면 mount 시 동시 실행으로 race condition 발생 → 단일 useEffect + ref 가드 + const isInitialLoadRef = useRef(true); useEffect(() => { - fetchTabData(activeTab); + if (isInitialLoadRef.current) { + isInitialLoadRef.current = false; + TAB_CONFIGS.forEach((c) => fetchTabData(c.key)); + } else { + fetchTabData(activeTab); + } }, [activeTab, fetchTabData]); - // 마운트 시 5개 탭 전체 카운트 로드 — 탭 배지 숫자가 진입 즉시 정확히 표시되도록 - useEffect(() => { - TAB_CONFIGS.forEach((c) => fetchTabData(c.key)); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - // 탭 변경 const handleTabChange = useCallback((tab: string) => { setActiveTab(tab as TabKey); diff --git a/frontend/app/(main)/COMPANY_30/logistics/info/page.tsx b/frontend/app/(main)/COMPANY_30/logistics/info/page.tsx index a0fa4141..9e92f81f 100644 --- a/frontend/app/(main)/COMPANY_30/logistics/info/page.tsx +++ b/frontend/app/(main)/COMPANY_30/logistics/info/page.tsx @@ -419,17 +419,18 @@ export default function LogisticsInfoPage() { } }, []); - // 초기 데이터 로드 (탭 전환 시) + // 마운트 시 5개 탭 전체 로드(카운트 즉시 정확), 이후 탭 전환 시 활성 탭만 재로드 + // 두 useEffect를 분리하면 mount 시 동시 실행으로 race condition 발생 → 단일 useEffect + ref 가드 + const isInitialLoadRef = useRef(true); useEffect(() => { - fetchTabData(activeTab); + if (isInitialLoadRef.current) { + isInitialLoadRef.current = false; + TAB_CONFIGS.forEach((c) => fetchTabData(c.key)); + } else { + fetchTabData(activeTab); + } }, [activeTab, fetchTabData]); - // 마운트 시 5개 탭 전체 카운트 로드 — 탭 배지 숫자가 진입 즉시 정확히 표시되도록 - useEffect(() => { - TAB_CONFIGS.forEach((c) => fetchTabData(c.key)); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - // 탭 변경 const handleTabChange = useCallback((tab: string) => { setActiveTab(tab as TabKey); diff --git a/frontend/app/(main)/COMPANY_7/logistics/info/page.tsx b/frontend/app/(main)/COMPANY_7/logistics/info/page.tsx index 2ca5c82e..0530029a 100644 --- a/frontend/app/(main)/COMPANY_7/logistics/info/page.tsx +++ b/frontend/app/(main)/COMPANY_7/logistics/info/page.tsx @@ -420,17 +420,18 @@ export default function LogisticsInfoPage() { } }, []); - // 초기 데이터 로드 (탭 전환 시) + // 마운트 시 5개 탭 전체 로드(카운트 즉시 정확), 이후 탭 전환 시 활성 탭만 재로드 + // 두 useEffect를 분리하면 mount 시 동시 실행으로 race condition 발생 → 단일 useEffect + ref 가드 + const isInitialLoadRef = useRef(true); useEffect(() => { - fetchTabData(activeTab); + if (isInitialLoadRef.current) { + isInitialLoadRef.current = false; + TAB_CONFIGS.forEach((c) => fetchTabData(c.key)); + } else { + fetchTabData(activeTab); + } }, [activeTab, fetchTabData]); - // 마운트 시 5개 탭 전체 카운트 로드 — 탭 배지 숫자가 진입 즉시 정확히 표시되도록 - useEffect(() => { - TAB_CONFIGS.forEach((c) => fetchTabData(c.key)); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - // 탭 변경 const handleTabChange = useCallback((tab: string) => { setActiveTab(tab as TabKey); diff --git a/frontend/app/(main)/COMPANY_8/logistics/info/page.tsx b/frontend/app/(main)/COMPANY_8/logistics/info/page.tsx index 351e1ca8..d668e239 100644 --- a/frontend/app/(main)/COMPANY_8/logistics/info/page.tsx +++ b/frontend/app/(main)/COMPANY_8/logistics/info/page.tsx @@ -419,17 +419,18 @@ export default function LogisticsInfoPage() { } }, []); - // 초기 데이터 로드 (탭 전환 시) + // 마운트 시 5개 탭 전체 로드(카운트 즉시 정확), 이후 탭 전환 시 활성 탭만 재로드 + // 두 useEffect를 분리하면 mount 시 동시 실행으로 race condition 발생 → 단일 useEffect + ref 가드 + const isInitialLoadRef = useRef(true); useEffect(() => { - fetchTabData(activeTab); + if (isInitialLoadRef.current) { + isInitialLoadRef.current = false; + TAB_CONFIGS.forEach((c) => fetchTabData(c.key)); + } else { + fetchTabData(activeTab); + } }, [activeTab, fetchTabData]); - // 마운트 시 5개 탭 전체 카운트 로드 — 탭 배지 숫자가 진입 즉시 정확히 표시되도록 - useEffect(() => { - TAB_CONFIGS.forEach((c) => fetchTabData(c.key)); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - // 탭 변경 const handleTabChange = useCallback((tab: string) => { setActiveTab(tab as TabKey); diff --git a/frontend/app/(main)/COMPANY_9/logistics/info/page.tsx b/frontend/app/(main)/COMPANY_9/logistics/info/page.tsx index 4b5e45d3..7b8c23ff 100644 --- a/frontend/app/(main)/COMPANY_9/logistics/info/page.tsx +++ b/frontend/app/(main)/COMPANY_9/logistics/info/page.tsx @@ -419,17 +419,18 @@ export default function LogisticsInfoPage() { } }, []); - // 초기 데이터 로드 (탭 전환 시) + // 마운트 시 5개 탭 전체 로드(카운트 즉시 정확), 이후 탭 전환 시 활성 탭만 재로드 + // 두 useEffect를 분리하면 mount 시 동시 실행으로 race condition 발생 → 단일 useEffect + ref 가드 + const isInitialLoadRef = useRef(true); useEffect(() => { - fetchTabData(activeTab); + if (isInitialLoadRef.current) { + isInitialLoadRef.current = false; + TAB_CONFIGS.forEach((c) => fetchTabData(c.key)); + } else { + fetchTabData(activeTab); + } }, [activeTab, fetchTabData]); - // 마운트 시 5개 탭 전체 카운트 로드 — 탭 배지 숫자가 진입 즉시 정확히 표시되도록 - useEffect(() => { - TAB_CONFIGS.forEach((c) => fetchTabData(c.key)); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - // 탭 변경 const handleTabChange = useCallback((tab: string) => { setActiveTab(tab as TabKey);