lotto 기능에 추천 결과 통계 시각화 (분포, 합계, 홀짝)를 추가
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user