Functions.jsx 컴포넌트 분할: 1,583→460줄 (3훅+8컴포넌트+유틸)

- lottoUtils.jsx: 공통 유틸·상수 추출 (Ball, NumberRow, 통계 헬퍼 등)
- hooks/useLottoData.js: 핵심 데이터 로드 (최신회차, 통계, 시뮬레이션, 리포트)
- hooks/usePurchases.js: 구매 기록 CRUD
- hooks/useManualRecommend.js: 수동 추천 + 히스토리
- components/: MetricBlock, FrequencyChart, PerformanceBanner,
  ConfidenceRing, CombinedRecommendPanel, ReportPanel,
  PersonalAnalysisPanel, PurchasePanel 분리
- getReport import 누락 버그 수정

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-03 07:53:25 +09:00
parent 2b463682d5
commit 299ce636ff
13 changed files with 1342 additions and 1256 deletions

View File

@@ -0,0 +1,157 @@
import React, { useState } from 'react';
import { ballClass, NumberRow, METHOD_META, METHOD_ORDER, SCORE_META, fmtKST } from '../lottoUtils';
const CombinedRecommendPanel = ({ combined, history, loading, histLoading, onRun, onCopy }) => {
const [histExpand, setHistExpand] = useState(false);
return (
<section className="lotto-panel lotto-panel--wide lotto-combined">
<div className="lotto-panel__head">
<div>
<p className="lotto-panel__eyebrow">AI · 종합 추론</p>
<h3>종합 추론 번호 추천</h3>
<p className="lotto-panel__sub">
5가지 통계 기법(빈도·지문··공동출현·다양성) 가중 투표로 합산해
최적 6 번호를 도출합니다.
</p>
</div>
<div className="lotto-panel__actions">
{loading && <span className="lotto-chip">분석 </span>}
<button className="button primary small" onClick={onRun} disabled={loading}>
{loading ? '추론 중…' : '🔮 종합 추론 실행'}
</button>
{history.length > 0 && (
<button className="button ghost small" onClick={() => setHistExpand(p => !p)}>
이력 {history.length} {histExpand ? '▲' : '▼'}
</button>
)}
</div>
</div>
{!combined && !loading && (
<p className="lotto-empty">버튼을 눌러 종합 추론을 실행하세요.</p>
)}
{combined && (
<>
{/* 기법별 추천 번호 */}
<div className="lotto-combined__methods">
{METHOD_ORDER.map((key) => {
const meta = METHOD_META[key];
const m = combined.methods?.[key];
if (!m) return null;
return (
<div key={key} className="lotto-combined__method">
<div className="lotto-combined__method-head">
<span className="lotto-combined__method-icon">{meta.icon}</span>
<div>
<p className="lotto-combined__method-name" style={{ color: meta.color }}>
{meta.label}
<span className="lotto-combined__method-weight"> ({m.weight_pct}%)</span>
</p>
<p className="lotto-combined__method-desc">{meta.desc}</p>
</div>
</div>
<div className="lotto-combined__method-nums">
{m.numbers.map((n) => {
const inFinal = combined.final_numbers.includes(n);
return (
<span
key={n}
className={`lotto-ball ${ballClass(n).replace('lotto-ball ', '')} ${inFinal ? 'is-final' : 'is-dim'}`}
>
{n}
</span>
);
})}
</div>
</div>
);
})}
</div>
{/* 최종 추론 결과 */}
<div className="lotto-combined__final">
<div className="lotto-combined__final-head">
<span className="lotto-combined__final-badge">종합 추론 결과</span>
{combined.deduped && (
<span className="lotto-chip lotto-chip--muted">중복 (이미 저장됨)</span>
)}
<button className="button ghost small" onClick={() => onCopy(combined.final_numbers)}>
복사
</button>
</div>
<div className="lotto-combined__final-balls">
{combined.final_numbers.map((n) => {
const votes = combined.vote_counts?.[String(n)] ?? 0;
return (
<div key={n} className="lotto-combined__final-ball-wrap">
<span className={ballClass(n)}>{n}</span>
<span className="lotto-combined__vote-dots">
{Array.from({ length: 5 }).map((_, i) => (
<span key={i} className={`lotto-combined__vote-dot ${i < votes ? 'is-on' : ''}`} />
))}
</span>
</div>
);
})}
</div>
<p className="lotto-combined__final-sub">
점은 해당 번호가 채택된 기법 (최대 5)
</p>
</div>
{/* 점수 바 */}
<div className="lotto-combined__scores">
<p className="lotto-combined__scores-title">조합 품질 점수</p>
{SCORE_META.map(({ key, label, color, weight }) => {
const val = combined.scores?.[key] ?? 0;
const pct = Math.round(val * 100);
return (
<div key={key} className="lotto-combined__score-row">
<span className="lotto-combined__score-label">{label}</span>
<span className="lotto-combined__score-weight">{weight}%</span>
<div className="lotto-combined__score-bar-wrap">
<div
className="lotto-combined__score-bar"
style={{ width: `${pct}%`, background: color }}
/>
</div>
<span className="lotto-combined__score-val">{pct}</span>
</div>
);
})}
<div className="lotto-combined__score-total">
종합 점수 <strong>{Math.round((combined.scores?.score_total ?? 0) * 100)}</strong> / 100
</div>
</div>
<p className="lotto-combined__disclaimer">
추천은 역대 통계 패턴 기반 참고 자료이며, 당첨을 보장하지 않습니다.
</p>
</>
)}
{/* 추천 이력 */}
{histExpand && (
<div className="lotto-combined__history">
<p className="lotto-combined__history-title">종합 추론 이력</p>
{histLoading && <p className="lotto-empty">로딩 </p>}
{history.map((item) => (
<div key={item.id} className="lotto-combined__history-item">
<div className="lotto-combined__history-meta">
<span>#{item.id}</span>
<span>{fmtKST(item.created_at)}</span>
<span>기준 {item.based_on_draw ?? '-'}</span>
</div>
<NumberRow nums={item.numbers} />
<button className="button ghost small" onClick={() => onCopy(item.numbers)}>복사</button>
</div>
))}
</div>
)}
</section>
);
};
export default CombinedRecommendPanel;

View File

@@ -0,0 +1,25 @@
import React from 'react';
const ConfidenceRing = ({ score }) => {
const r = 28, c = 2 * Math.PI * r;
const fill = (score / 100) * c;
const color = score >= 80 ? '#97c9aa' : score >= 60 ? '#fdd4b1' : '#f7a8a5';
return (
<svg width="72" height="72" viewBox="0 0 72 72" className="lotto-confidence-ring" aria-hidden>
<circle cx="36" cy="36" r={r} stroke="rgba(255,255,255,0.08)" strokeWidth="6" fill="none" />
<circle
cx="36" cy="36" r={r}
stroke={color} strokeWidth="6" fill="none"
strokeDasharray={`${fill} ${c - fill}`}
strokeLinecap="round"
transform="rotate(-90 36 36)"
/>
<text x="36" y="41" textAnchor="middle" fill={color} fontSize="16" fontWeight="600"
style={{ fontFamily: 'inherit' }}>
{score}
</text>
</svg>
);
};
export default ConfidenceRing;

View File

@@ -0,0 +1,39 @@
import React, { useMemo } from 'react';
import { buildFrequencySeries } from '../lottoUtils';
const FrequencyChart = ({ stats }) => {
const { series, max } = useMemo(() => buildFrequencySeries(stats?.frequency), [stats]);
const ticks = useMemo(() => [max, Math.round(max * 0.5), 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) => {
const showLabel = item.number === 1 || item.number % 5 === 0;
return (
<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" aria-hidden={!showLabel}>
{showLabel ? item.number : ''}
</span>
</div>
);
})}
</div>
</div>
);
};
export default FrequencyChart;

View File

@@ -0,0 +1,59 @@
import React from 'react';
import { toBucketEntries } from '../lottoUtils';
const MetricBlock = ({ title, metrics }) => {
if (!metrics) return null;
const buckets = toBucketEntries(metrics);
const maxBucket = buckets.length ? Math.max(...buckets.map(([, v]) => Number(v) || 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 MetricBlock;

View File

@@ -0,0 +1,48 @@
import React from 'react';
const PerformanceBanner = ({ perf }) => {
if (!perf || perf.total_checked === 0) return null;
const imp = perf.vs_random?.improvement_pct ?? 0;
const prizeHits = (perf.by_rank?.rank_3 ?? 0) + (perf.by_rank?.rank_4 ?? 0) + (perf.by_rank?.rank_5 ?? 0);
return (
<div className="lotto-perf-banner">
<span className="lotto-perf-banner__label">신뢰도 지표</span>
<div className="lotto-perf-banner__items">
<div className="lotto-perf-banner__item">
<span className="lotto-perf-banner__val">{perf.total_checked}</span>
<span className="lotto-perf-banner__lbl">검증 회차</span>
</div>
<div className="lotto-perf-banner__divider" />
<div className="lotto-perf-banner__item">
<span className="lotto-perf-banner__val">{(perf.avg_correct ?? 0).toFixed(1)}</span>
<span className="lotto-perf-banner__lbl">평균 일치수</span>
</div>
<div className="lotto-perf-banner__divider" />
<div className="lotto-perf-banner__item">
<span className={`lotto-perf-banner__val ${imp > 0 ? 'is-pos' : ''}`}>
{imp > 0 ? '+' : ''}{imp.toFixed(1)}%
</span>
<span className="lotto-perf-banner__lbl">무작위 대비</span>
</div>
<div className="lotto-perf-banner__divider" />
<div className="lotto-perf-banner__item">
<span className="lotto-perf-banner__val">
{((perf.rate_3plus ?? 0) * 100).toFixed(1)}%
</span>
<span className="lotto-perf-banner__lbl">3 일치율</span>
</div>
{prizeHits > 0 && (
<>
<div className="lotto-perf-banner__divider" />
<div className="lotto-perf-banner__item">
<span className="lotto-perf-banner__val is-prize">{prizeHits}</span>
<span className="lotto-perf-banner__lbl">3~5 당첨</span>
</div>
</>
)}
</div>
</div>
);
};
export default PerformanceBanner;

View File

@@ -0,0 +1,83 @@
import React from 'react';
import { NumberRow } from '../lottoUtils';
const PersonalAnalysisPanel = ({ data, loading }) => {
const zones = Object.entries(data?.pattern?.zone_avg ?? {});
const maxZone = zones.length ? Math.max(...zones.map(([, v]) => Number(v) || 0), 1) : 1;
return (
<section className="lotto-panel lotto-panel--wide">
<div className="lotto-panel__head">
<div>
<p className="lotto-panel__eyebrow">My Pattern</p>
<h3> 번호 패턴</h3>
{data && data.total_analyzed > 0 && (
<p className="lotto-panel__sub"> {data.total_analyzed} 추천 기반 분석</p>
)}
</div>
</div>
{(loading || !data || data.total_analyzed === 0) ? (
<p className="lotto-empty">
{loading ? '불러오는 중...' : '추천 이력이 없습니다.'}
</p>
) : (
<div className="lotto-analysis">
<div className="lotto-analysis__row">
<div className="lotto-analysis__group">
<p className="lotto-analysis__label">
내가 자주 선택한 번호 <span>TOP 10</span>
</p>
<NumberRow nums={data.top_picks ?? []} />
</div>
<div className="lotto-analysis__group">
<p className="lotto-analysis__label">선택 성향</p>
<div className="lotto-personal-tendency">
{data.vs_draw_avg?.odd_tendency && (
<span className="lotto-personal-tendency__badge">
{data.vs_draw_avg.odd_tendency}
</span>
)}
{data.vs_draw_avg?.sum_tendency && (
<span className="lotto-personal-tendency__badge">
{data.vs_draw_avg.sum_tendency}
</span>
)}
</div>
<div className="lotto-analysis__stats">
<span>홀수 평균 <strong>{data.pattern?.avg_odd_count?.toFixed(1)}</strong></span>
<span>합계 평균 <strong>{data.pattern?.avg_sum?.toFixed(1)}</strong></span>
<span>
연속번호 포함률{' '}
<strong>
{((data.pattern?.consecutive_rate ?? 0) * 100).toFixed(0)}%
</strong>
</span>
</div>
</div>
{zones.length > 0 && (
<div className="lotto-analysis__group">
<p className="lotto-analysis__label">구간별 선택 비율</p>
<div className="lotto-buckets">
{zones.map(([zone, avg]) => (
<div key={zone} className="lotto-bucket">
<span className="lotto-bucket__label">{zone}</span>
<div className="lotto-bucket__bar" aria-hidden>
<span style={{ width: `${((Number(avg) || 0) / maxZone) * 100}%` }} />
</div>
<span className="lotto-bucket__value">{Number(avg).toFixed(1)}</span>
</div>
))}
</div>
</div>
)}
</div>
</div>
)}
</section>
);
};
export default PersonalAnalysisPanel;

View File

@@ -0,0 +1,173 @@
import React from 'react';
import { fmtWon } from '../lottoUtils';
const PurchasePanel = ({
records, stats, loading,
formOpen, form, formSaving, formError, editId,
onFormOpen, onFormClose, onFormChange, onFormSubmit,
onEditStart, onDelete,
}) => {
const winRate = stats?.total_records > 0
? ((stats.prize_count / stats.total_records) * 100).toFixed(1)
: '0.0';
const netColor = (stats?.net ?? 0) >= 0 ? 'is-pos' : 'is-neg';
return (
<section className="lotto-panel lotto-panel--wide">
<div className="lotto-panel__head">
<div>
<p className="lotto-panel__eyebrow">Purchase Tracker</p>
<h3>구매 기록</h3>
<p className="lotto-panel__sub">구매 내역 기록 수익률 추적</p>
</div>
<div className="lotto-panel__actions">
{loading && <span className="lotto-chip">로딩 </span>}
<button className="button small" onClick={onFormOpen} disabled={formOpen}>
+ 추가
</button>
</div>
</div>
{/* 통계 바 */}
{stats && stats.total_records > 0 && (
<div className="lotto-purchase-stats">
<div className="lotto-purchase-stat">
<span className="lotto-purchase-stat__val">{fmtWon(stats.total_invested)}</span>
<span className="lotto-purchase-stat__lbl"> 투자</span>
</div>
<div className="lotto-purchase-stat">
<span className="lotto-purchase-stat__val">{fmtWon(stats.total_prize)}</span>
<span className="lotto-purchase-stat__lbl"> 당첨금</span>
</div>
<div className="lotto-purchase-stat">
<span className={`lotto-purchase-stat__val ${netColor}`}>
{(stats.net ?? 0) >= 0 ? '+' : ''}{fmtWon(stats.net)}
</span>
<span className="lotto-purchase-stat__lbl">순손익</span>
</div>
<div className="lotto-purchase-stat">
<span className="lotto-purchase-stat__val">{stats.return_rate?.toFixed(1)}%</span>
<span className="lotto-purchase-stat__lbl">회수율</span>
</div>
<div className="lotto-purchase-stat">
<span className="lotto-purchase-stat__val">{winRate}%</span>
<span className="lotto-purchase-stat__lbl">당첨률</span>
</div>
{stats.max_prize > 0 && (
<div className="lotto-purchase-stat">
<span className="lotto-purchase-stat__val is-prize">{fmtWon(stats.max_prize)}</span>
<span className="lotto-purchase-stat__lbl">최대 당첨금</span>
</div>
)}
</div>
)}
{/* 입력 폼 */}
{formOpen && (
<form className="lotto-purchase-form" onSubmit={onFormSubmit}>
<p className="lotto-purchase-form__title">
{editId != null ? '기록 수정' : '구매 기록 추가'}
</p>
<div className="lotto-purchase-form__grid">
<label className="lotto-field">
회차
<input
type="number" min={1}
value={form.draw_no}
onChange={(e) => onFormChange('draw_no', e.target.value)}
placeholder="예: 1181"
required
/>
</label>
<label className="lotto-field">
구매금액
<input
type="number" step={1000} min={1000}
value={form.amount}
onChange={(e) => onFormChange('amount', Number(e.target.value))}
/>
</label>
<label className="lotto-field">
세트
<input
type="number" min={1} max={20}
value={form.sets}
onChange={(e) => onFormChange('sets', Number(e.target.value))}
/>
</label>
<label className="lotto-field">
당첨금
<input
type="number" min={0}
value={form.prize}
onChange={(e) => onFormChange('prize', Number(e.target.value))}
/>
</label>
<label className="lotto-field lotto-purchase-form__note">
메모
<input
type="text"
value={form.note}
onChange={(e) => onFormChange('note', e.target.value)}
placeholder="예: 5등 1개"
/>
</label>
</div>
{formError && (
<p className="lotto-empty" style={{ color: '#f9b6b1' }}>{formError}</p>
)}
<div className="lotto-purchase-form__actions">
<button type="button" className="button ghost small" onClick={onFormClose}>
취소
</button>
<button type="submit" className="button primary small" disabled={formSaving}>
{formSaving ? '저장 중...' : editId != null ? '수정 완료' : '추가'}
</button>
</div>
</form>
)}
{/* 기록 목록 */}
{records.length === 0 ? (
<p className="lotto-empty">구매 기록이 없습니다.</p>
) : (
<div className="lotto-purchase-list">
<div className="lotto-purchase-list__head">
<span>회차</span>
<span>투자금</span>
<span>당첨금</span>
<span>손익</span>
<span>메모</span>
<span />
</div>
{records.map((rec) => {
const net = (rec.prize ?? 0) - (rec.amount ?? 0);
return (
<div key={rec.id} className="lotto-purchase-row">
<span className="lotto-purchase-row__drw">{rec.draw_no}</span>
<span>{fmtWon(rec.amount)}</span>
<span className={(rec.prize ?? 0) > 0 ? 'is-prize' : ''}>
{fmtWon(rec.prize)}
</span>
<span className={net >= 0 ? 'is-pos' : 'is-neg'}>
{net >= 0 ? '+' : ''}{fmtWon(net)}
</span>
<span className="lotto-purchase-row__note">{rec.note || '-'}</span>
<div className="lotto-purchase-row__actions">
<button className="button ghost small" onClick={() => onEditStart(rec)}>
수정
</button>
<button className="button danger small" onClick={() => onDelete(rec.id)}>
삭제
</button>
</div>
</div>
);
})}
</div>
)}
</section>
);
};
export default PurchasePanel;

View File

@@ -0,0 +1,142 @@
import React, { useState } from 'react';
import { NumberRow } from '../lottoUtils';
import ConfidenceRing from './ConfidenceRing';
const ReportPanel = ({ report, history, loading, onRefresh, onSelectDrw }) => {
const [histExpand, setHistExpand] = useState(false);
return (
<section className="lotto-panel lotto-panel--wide">
<div className="lotto-panel__head">
<div>
<p className="lotto-panel__eyebrow">Weekly Report</p>
<h3>이번 공략 리포트</h3>
{report && (
<p className="lotto-panel__sub">
{report.target_drw_no} 대상 · {report.based_on_draw} 기준
</p>
)}
</div>
<div className="lotto-panel__actions">
{loading && <span className="lotto-chip">로딩 </span>}
<button className="button ghost small" onClick={onRefresh} disabled={loading}>
새로고침
</button>
{history?.length > 0 && (
<button className="button ghost small" onClick={() => setHistExpand((p) => !p)}>
지난 리포트 {histExpand ? '▲' : '▼'}
</button>
)}
</div>
</div>
{/* 지난 리포트 목록 */}
{histExpand && history?.length > 0 && (
<div className="lotto-report-history">
{history.map((h) => (
<button
key={h.drw_no}
className="button ghost small"
onClick={() => { onSelectDrw(h.drw_no); setHistExpand(false); }}
>
{h.drw_no}
</button>
))}
</div>
)}
{!report && !loading && (
<p className="lotto-empty">리포트 데이터가 없습니다.</p>
)}
{loading && !report && (
<p className="lotto-empty">불러오는 ...</p>
)}
{report && (
<>
{/* 신뢰도 + 패턴 요약 */}
<div className="lotto-report-top">
<div className="lotto-report-confidence">
<ConfidenceRing score={report.confidence_score ?? 0} />
<div>
<p className="lotto-report-confidence__title">신뢰도 점수</p>
<div className="lotto-report-confidence__factors">
{Object.entries(report.confidence_factors ?? {}).map(([k, v]) => (
<div key={k} className="lotto-report-confidence__factor">
<span className="lotto-report-confidence__factor-lbl">
{k === 'data_volume' ? '데이터 충분도'
: k === 'pattern_consistency' ? '패턴 안정성'
: k === 'recent_trend' ? '최근 트렌드' : k}
</span>
<div className="lotto-pick__bar">
<span style={{ width: `${v}%` }} />
</div>
<span className="lotto-report-confidence__factor-val">{v}</span>
</div>
))}
</div>
</div>
</div>
<div className="lotto-report-pattern">
<p className="lotto-report-pattern__title">최근 패턴</p>
<div className="lotto-report-pattern__stats">
<div className="lotto-report-pattern__stat">
<span>합계 평균</span>
<strong>{report.recent_pattern?.recent_sum_avg?.toFixed(1) ?? '-'}</strong>
</div>
<div className="lotto-report-pattern__stat">
<span>홀수 평균</span>
<strong>{report.recent_pattern?.recent_odd_avg?.toFixed(1) ?? '-'}</strong>
</div>
{(report.recent_pattern?.triple_appear ?? []).length > 0 && (
<div className="lotto-report-pattern__stat">
<span>3 연속 출현</span>
<NumberRow nums={report.recent_pattern.triple_appear} />
</div>
)}
</div>
</div>
</div>
{/* 핫 / 콜드 / 오버듀 */}
<div className="lotto-analysis__row">
<div className="lotto-analysis__group">
<p className="lotto-analysis__label">
🔥 번호 <span>최근 10 과출현</span>
</p>
<NumberRow nums={report.hot_numbers ?? []} />
</div>
<div className="lotto-analysis__group">
<p className="lotto-analysis__label">
🧊 콜드 번호 <span>역대 저빈도 10</span>
</p>
<NumberRow nums={report.cold_numbers ?? []} />
</div>
<div className="lotto-analysis__group">
<p className="lotto-analysis__label">
오버듀 <span>가장 오래 미출현</span>
</p>
<NumberRow nums={report.overdue_numbers ?? []} />
</div>
</div>
{/* 전략 추천 세트 */}
{(report.recommended_sets ?? []).length > 0 && (
<div className="lotto-strategy-cards">
{report.recommended_sets.map((set, i) => (
<div key={i} className="lotto-strategy-card">
<p className="lotto-strategy-card__name">{set.strategy}</p>
<NumberRow nums={set.numbers} />
<p className="lotto-strategy-card__desc">{set.description}</p>
</div>
))}
</div>
)}
</>
)}
</section>
);
};
export default ReportPanel;