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>
|
||||
|
||||
@@ -244,6 +244,169 @@
|
||||
border: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.lotto-compare {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
}
|
||||
|
||||
.lotto-metrics {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 16px;
|
||||
padding: 14px;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.lotto-metrics__head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.lotto-metrics__title {
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.lotto-metrics__sum {
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.lotto-metric-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.lotto-metric-card {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
padding: 10px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.lotto-metric-card__label {
|
||||
margin: 0;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.lotto-metric-card__value {
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.lotto-odd-even {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.lotto-odd-even__labels {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.lotto-odd-even__bar {
|
||||
height: 10px;
|
||||
border-radius: 999px;
|
||||
overflow: hidden;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
border: 1px solid var(--line);
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.lotto-odd-even__odd {
|
||||
background: rgba(247, 168, 165, 0.6);
|
||||
}
|
||||
|
||||
.lotto-odd-even__even {
|
||||
background: rgba(151, 201, 170, 0.6);
|
||||
}
|
||||
|
||||
.lotto-buckets {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.lotto-bucket {
|
||||
display: grid;
|
||||
grid-template-columns: 54px minmax(0, 1fr) 28px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.lotto-bucket__label {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.lotto-bucket__bar {
|
||||
height: 8px;
|
||||
border-radius: 999px;
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.lotto-bucket__bar span {
|
||||
display: block;
|
||||
height: 100%;
|
||||
background: rgba(133, 165, 216, 0.7);
|
||||
}
|
||||
|
||||
.lotto-bucket__value {
|
||||
text-align: right;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.lotto-batch {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.lotto-batch__item {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 16px;
|
||||
padding: 14px;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.lotto-batch__meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.lotto-batch__title {
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.lotto-batch__sub {
|
||||
margin: 4px 0 0;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.lotto-empty {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
|
||||
Reference in New Issue
Block a user