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);