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:
@@ -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 (
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user