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.
This commit is contained in:
kjs
2026-04-28 18:15:22 +09:00
parent bf8d99ccf5
commit 0b7c967f56
9 changed files with 84 additions and 69 deletions

View File

@@ -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 (

View File

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

View File

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

View File

@@ -310,6 +310,7 @@ export default function InspectionResultPage() {
<TableHeader className="sticky top-0 z-10 bg-muted shadow-[0_1px_0_0_hsl(var(--border))]">
<TableRow className="hover:bg-muted">
<TableHead className="w-10 text-center">#</TableHead>
<TableHead className="min-w-[90px]"></TableHead>
<TableHead className="min-w-[140px]"></TableHead>
<TableHead className="min-w-[120px]"></TableHead>
<TableHead className="min-w-[100px]"></TableHead>
@@ -323,6 +324,11 @@ export default function InspectionResultPage() {
{detailData.map((d, idx) => (
<TableRow key={d.id}>
<TableCell className="text-center text-muted-foreground text-xs">{idx + 1}</TableCell>
<TableCell className="text-sm">
{d.inspection_type ? (
<Badge variant="outline" className="text-xs">{d.inspection_type}</Badge>
) : "-"}
</TableCell>
<TableCell className="text-sm font-medium">{d.inspection_item_name || "-"}</TableCell>
<TableCell className="text-sm">{d.inspection_standard || "-"}</TableCell>
<TableCell className="text-sm">{d.pass_criteria || "-"}</TableCell>

View File

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

View File

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

View File

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

View File

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

View File

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