lotto 기능에 추천 결과 통계 시각화 (분포, 합계, 홀짝)를 추가

This commit is contained in:
2026-01-25 22:10:24 +09:00
parent 2495feef3e
commit dcd2910cea
2 changed files with 305 additions and 5 deletions

View File

@@ -21,6 +21,88 @@ const NumberRow = ({ nums }) => (
</div>
);
const bucketOrder = ['1-10', '11-20', '21-30', '31-40', '41-45'];
const toBucketEntries = (metrics) => {
if (!metrics?.buckets) return [];
const entries = Object.entries(metrics.buckets);
const ordered = bucketOrder
.filter((key) => Object.prototype.hasOwnProperty.call(metrics.buckets, key))
.map((key) => [key, metrics.buckets[key]]);
const rest = entries
.filter(([key]) => !bucketOrder.includes(key))
.sort((a, b) => {
const aStart = Number(a[0].split('-')[0]);
const bStart = Number(b[0].split('-')[0]);
if (Number.isNaN(aStart) || Number.isNaN(bStart)) return 0;
return aStart - bStart;
});
return [...ordered, ...rest];
};
const MetricBlock = ({ title, metrics }) => {
if (!metrics) return null;
const buckets = toBucketEntries(metrics);
const maxBucket = buckets.length
? Math.max(...buckets.map(([, value]) => Number(value) || 0), 1)
: 1;
const odd = Number(metrics.odd) || 0;
const even = Number(metrics.even) || 0;
const totalOE = odd + even || 1;
const oddPct = (odd / totalOE) * 100;
return (
<div className="lotto-metrics">
<div className="lotto-metrics__head">
<p className="lotto-metrics__title">{title}</p>
<span className="lotto-metrics__sum">합계 {metrics.sum ?? '-'}</span>
</div>
<div className="lotto-metric-cards">
<div className="lotto-metric-card">
<p className="lotto-metric-card__label">최솟값</p>
<p className="lotto-metric-card__value">{metrics.min ?? '-'}</p>
</div>
<div className="lotto-metric-card">
<p className="lotto-metric-card__label">최댓값</p>
<p className="lotto-metric-card__value">{metrics.max ?? '-'}</p>
</div>
<div className="lotto-metric-card">
<p className="lotto-metric-card__label">범위</p>
<p className="lotto-metric-card__value">{metrics.range ?? '-'}</p>
</div>
</div>
<div className="lotto-odd-even">
<div className="lotto-odd-even__labels">
<span> {odd}</span>
<span> {even}</span>
</div>
<div className="lotto-odd-even__bar" aria-hidden>
<span className="lotto-odd-even__odd" style={{ width: `${oddPct}%` }} />
<span
className="lotto-odd-even__even"
style={{ width: `${100 - oddPct}%` }}
/>
</div>
</div>
{buckets.length ? (
<div className="lotto-buckets">
{buckets.map(([label, value]) => (
<div key={label} className="lotto-bucket">
<span className="lotto-bucket__label">{label}</span>
<div className="lotto-bucket__bar" aria-hidden>
<span
style={{ width: `${((Number(value) || 0) / maxBucket) * 100}%` }}
/>
</div>
<span className="lotto-bucket__value">{value}</span>
</div>
))}
</div>
) : null}
</div>
);
};
export default function Functions() {
const [latest, setLatest] = useState(null);
const [params, setParams] = useState({
@@ -169,6 +251,9 @@ export default function Functions() {
<p className="lotto-bonus">
보너스 <strong>{latest.bonus}</strong>
</p>
{latest.metrics ? (
<MetricBlock title="당첨 통계" metrics={latest.metrics} />
) : null}
</>
) : (
<p className="lotto-empty">최신 회차 데이터가 없습니다.</p>
@@ -285,11 +370,63 @@ export default function Functions() {
번호 복사
</button>
</div>
<NumberRow nums={result.numbers} />
<details className="lotto-details">
<summary>설명 보기</summary>
<pre>{JSON.stringify(result.explain, null, 2)}</pre>
</details>
{result.numbers ? <NumberRow nums={result.numbers} /> : null}
{result.metrics || latest?.metrics ? (
<div className="lotto-compare">
{result.metrics ? (
<MetricBlock title="추천 통계" metrics={result.metrics} />
) : null}
{latest?.metrics ? (
<MetricBlock
title="당첨 통계 (최신 회차)"
metrics={latest.metrics}
/>
) : null}
</div>
) : null}
{Array.isArray(result.items) && result.items.length ? (
<details className="lotto-details">
<summary>추천 후보 보기</summary>
<div className="lotto-batch">
{result.items.map((item, idx) => (
<div
key={item.id ?? `candidate-${idx}`}
className="lotto-batch__item"
>
<div className="lotto-batch__meta">
<div>
<p className="lotto-batch__title">
후보 #{item.id ?? idx + 1}
</p>
<p className="lotto-batch__sub">
기준 회차 {item.based_on_draw ?? '-'}
</p>
</div>
<button
className="button ghost small"
onClick={() => copyNumbers(item.numbers)}
>
복사
</button>
</div>
<NumberRow nums={item.numbers} />
{item.metrics ? (
<MetricBlock
title="후보 통계"
metrics={item.metrics}
/>
) : null}
</div>
))}
</div>
</details>
) : null}
{result.explain ? (
<details className="lotto-details">
<summary>설명 보기</summary>
<pre>{JSON.stringify(result.explain, null, 2)}</pre>
</details>
) : null}
</div>
) : (
<p className="lotto-empty">아직 추천 결과가 없습니다.</p>