import { useCallback, useEffect, useMemo, useState } from 'react' import { fetchWithRefresh } from '../../lib/api' import { crmUrl } from '../../lib/endpoints' import EfficiencyScatter from './EfficiencyScatter' import DistanceStrip from './DistanceStrip' import DriverHistoryLine from './DriverHistoryLine' import './DriverDailyStat.css' const POLL_MS = 5 * 60 * 1000 // 5분 const localToday = () => { const d = new Date() return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}` } const asList = (json) => (Array.isArray(json) ? json : (json?.data ?? [])) const asObject = (json) => (json?.data ?? json ?? null) export default function DriverDailyStatPage() { const [date, setDate] = useState(localToday()) const [driverId, setDriverId] = useState('') // '' = 선택 안 함 const [rows, setRows] = useState([]) const [history, setHistory] = useState(null) const [ovLoading, setOvLoading] = useState(true) const [histLoading, setHistLoading] = useState(false) const [error, setError] = useState(null) const [updatedAt, setUpdatedAt] = useState(null) // 개요(전체) — 날짜에만 의존. 드라이버 선택은 여기 안 건드림. const loadOverview = useCallback(async (signal) => { try { const res = await fetchWithRefresh(crmUrl(`/driver-daily-stat?date=${date}`)) if (!res.ok) throw new Error(`개요 요청 실패 (${res.status})`) const list = asList(await res.json()) if (signal?.aborted) return setRows(list) setUpdatedAt(new Date()) setError(null) } catch (e) { if (!signal?.aborted) setError(e.message) } finally { if (!signal?.aborted) setOvLoading(false) } }, [date]) useEffect(() => { const ctrl = new AbortController() setOvLoading(true) loadOverview(ctrl.signal) const id = setInterval(() => loadOverview(ctrl.signal), POLL_MS) return () => { ctrl.abort(); clearInterval(id) } }, [loadOverview]) // 드릴다운(단일 드라이버) — 선택됐을 때만, 아래에 따로. const loadHistory = useCallback(async (signal) => { if (!driverId) { setHistory(null); return } try { setHistLoading(true) const res = await fetchWithRefresh(crmUrl(`/driver-daily-stat/history?driverId=${driverId}&date=${date}`)) if (!res.ok) throw new Error(`이력 요청 실패 (${res.status})`) const h = asObject(await res.json()) if (signal?.aborted) return setHistory(h) } catch (e) { if (!signal?.aborted) setError(e.message) } finally { if (!signal?.aborted) setHistLoading(false) } }, [date, driverId]) useEffect(() => { const ctrl = new AbortController() loadHistory(ctrl.signal) const id = setInterval(() => loadHistory(ctrl.signal), POLL_MS) return () => { ctrl.abort(); clearInterval(id) } }, [loadHistory]) const drivers = useMemo(() => { const seen = new Map() for (const r of rows) { if (r.ddsDriverId != null && !seen.has(r.ddsDriverId)) { seen.set(r.ddsDriverId, r.ddsMainDriver ?? `드라이버 ${r.ddsDriverId}`) } } return [...seen.entries()].map(([id, name]) => ({ id, name })) }, [rows]) const selectedName = drivers.find((d) => String(d.id) === String(driverId))?.name // 픽업 있음(산점도) / 픽업 0(칩) 분리 const activeRows = useMemo(() => rows.filter((r) => Number(r.ddsTotalQty ?? 0) > 0), [rows]) const zeroRows = useMemo(() => rows.filter((r) => Number(r.ddsTotalQty ?? 0) <= 0), [rows]) // 전체 요약 (선택과 무관, 날짜 기준 전체 합) const summary = useMemo(() => { let plannedQty = 0, totalQty = 0, completed = 0, target = 0 for (const r of rows) { plannedQty += Number(r.ddsPlannedQty ?? 0) totalQty += Number(r.ddsTotalQty ?? 0) completed += Number(r.ddsCompletedCount ?? 0) target += Number(r.ddsPlannedCount ?? 0) } const rate = plannedQty > 0 ? Math.round((totalQty / plannedQty) * 100) : null return { plannedQty: Math.round(plannedQty), totalQty: Math.round(totalQty), completed, target, rate, } }, [rows]) return (

드라이버 운행 현황

5분마다 자동 갱신 {updatedAt && ` · 업데이트 ${updatedAt.toLocaleTimeString('ko-KR', { hour: '2-digit', minute: '2-digit' })}`} {ovLoading && rows.length > 0 && ' · 갱신 중…'}
setDate(e.target.value)} />
{error &&
불러오기 오류: {error}
} {ovLoading && rows.length === 0 &&
불러오는 중…
} {/* 전체 요약 */} {rows.length > 0 && (
전체 예상량
{summary.plannedQty.toLocaleString()} L
전체 픽업량
{summary.totalQty.toLocaleString()} L
{summary.rate != null &&
예상 대비 {summary.rate}%
}
전체 방문 수
{summary.completed.toLocaleString()} / {summary.target.toLocaleString()}
)} {/* 전체 개요 — 선택과 무관하게 항상 유지 */} {(!ovLoading || rows.length > 0) && (
전체 드라이버 — 거리 대비 픽업량
setDriverId(String(id))} />
운행 중 운행 종료 채움 = 예상 대비 픽업량 · 마커 클릭 → 아래에 누적 차트
)} {/* 기타 운행 (오일 픽업 없음) — 거리 스트립 */} {zeroRows.length > 0 && (
기타 운행 (오일 픽업 없음) — 이동거리
)} {/* 단일 드라이버 드릴다운 — 아래에 추가로 등장 */} {driverId && (
{selectedName} · 시간대별 누적{histLoading && ' · 불러오는 중…'}
)}
) }