lotto lab 전 차수 그래프화 추가

This commit is contained in:
2026-01-25 23:51:50 +09:00
parent b76e0ef779
commit 80a61e74ee
3 changed files with 220 additions and 1 deletions

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useMemo, useState } from 'react';
import { deleteHistory, getHistory, getLatest, recommend } from '../../api';
import { deleteHistory, getHistory, getLatest, getStats, recommend } from '../../api';
const fmtKST = (value) => value?.replace('T', ' ') ?? '';
@@ -22,6 +22,46 @@ const NumberRow = ({ nums }) => (
);
const bucketOrder = ['1-10', '11-20', '21-30', '31-40', '41-45'];
const STATS_CACHE_KEY = 'lotto_stats_v1';
const readStatsCache = () => {
if (typeof window === 'undefined') return null;
try {
const raw = localStorage.getItem(STATS_CACHE_KEY);
if (!raw) return null;
const parsed = JSON.parse(raw);
if (!parsed || !Array.isArray(parsed.frequency)) return null;
return parsed;
} catch {
return null;
}
};
const writeStatsCache = (data) => {
if (typeof window === 'undefined') return;
try {
localStorage.setItem(STATS_CACHE_KEY, JSON.stringify(data));
} catch {
// ignore storage failures
}
};
const buildFrequencySeries = (frequency) => {
const map = new Map();
(frequency ?? []).forEach((item) => {
const number = Number(item?.number);
const count = Number(item?.count) || 0;
if (Number.isFinite(number) && number >= 1 && number <= 45) {
map.set(number, count);
}
});
const series = Array.from({ length: 45 }, (_, idx) => ({
number: idx + 1,
count: map.get(idx + 1) ?? 0,
}));
const max = Math.max(1, ...series.map((item) => item.count));
return { series, max };
};
const toBucketEntries = (metrics) => {
if (!metrics?.buckets) return [];
@@ -103,6 +143,49 @@ const MetricBlock = ({ title, metrics }) => {
);
};
const FrequencyChart = ({ stats }) => {
const { series, max } = useMemo(
() => buildFrequencySeries(stats?.frequency),
[stats]
);
const ticks = useMemo(() => {
const mid = Math.round(max * 0.5);
return [max, mid, 0];
}, [max]);
if (!stats) return null;
return (
<div className="lotto-chart">
<div className="lotto-chart__y">
<span>횟수</span>
<div className="lotto-chart__ticks">
{ticks.map((value) => (
<span key={value}>{value}</span>
))}
</div>
</div>
<div className="lotto-chart__plot" role="list">
{series.map((item) => (
<div
key={item.number}
className="lotto-chart__col"
role="listitem"
>
<span
className="lotto-chart__bar"
style={{ height: `${(item.count / max) * 100}%` }}
title={`${item.number}번: ${item.count}`}
aria-label={`${item.number}${item.count}`}
/>
<span className="lotto-chart__x">{item.number}</span>
</div>
))}
</div>
</div>
);
};
export default function Functions() {
const [latest, setLatest] = useState(null);
const [params, setParams] = useState({
@@ -122,6 +205,9 @@ export default function Functions() {
const [result, setResult] = useState(null);
const [history, setHistory] = useState([]);
const [stats, setStats] = useState(() => readStatsCache());
const [statsLoading, setStatsLoading] = useState(false);
const [statsError, setStatsError] = useState('');
const [loading, setLoading] = useState({
latest: false,
recommend: false,
@@ -155,6 +241,28 @@ export default function Functions() {
}
};
const refreshStats = async () => {
setStatsLoading(true);
setStatsError('');
try {
const cached = readStatsCache();
if (cached && !stats) {
setStats(cached);
}
const data = await getStats();
const shouldUpdate =
!cached || cached.total_draws !== data?.total_draws;
if (shouldUpdate) {
setStats(data);
writeStatsCache(data);
}
} catch (e) {
setStatsError(e?.message ?? String(e));
} finally {
setStatsLoading(false);
}
};
const onRecommend = async () => {
setLoading((s) => ({ ...s, recommend: true }));
setError('');
@@ -195,6 +303,7 @@ export default function Functions() {
useEffect(() => {
refreshLatest();
refreshHistory();
refreshStats();
}, []);
return (
@@ -432,6 +541,46 @@ export default function Functions() {
<p className="lotto-empty">아직 추천 결과가 없습니다.</p>
)}
</section>
<section className="lotto-panel">
<div className="lotto-panel__head">
<div>
<p className="lotto-panel__eyebrow">Distribution</p>
<h3>전체 회차 번호 분포</h3>
<p className="lotto-panel__sub">
1~45 번호가 등장한 횟수를 기준으로 분포를 표시합니다.
</p>
</div>
<div className="lotto-panel__actions">
{statsLoading ? (
<span className="lotto-chip">로딩 </span>
) : null}
{stats?.total_draws ? (
<span className="lotto-chip">
{stats.total_draws}회차
</span>
) : null}
<button
className="button ghost small"
onClick={refreshStats}
disabled={statsLoading}
>
새로고침
</button>
</div>
</div>
{statsError ? (
<p className="lotto-empty">{statsError}</p>
) : null}
{stats ? (
<FrequencyChart stats={stats} />
) : (
<p className="lotto-empty">
통계 데이터를 불러오지 못했습니다.
</p>
)}
</section>
</div>
<section className="lotto-panel">