feat(lotto): DecisionCard 하위 컴포넌트(Pick/Tier/Toggle/Retro) + 스타일
This commit is contained in:
19
src/pages/lotto/components/decision/PickCard.jsx
Normal file
19
src/pages/lotto/components/decision/PickCard.jsx
Normal file
@@ -0,0 +1,19 @@
|
||||
const ROLE_COLOR = { '안정': 'stable', '균형': 'balance', '공격': 'aggro' };
|
||||
|
||||
export default function PickCard({ pick, index, total }) {
|
||||
const role = pick.risk_tag;
|
||||
return (
|
||||
<div className="lc-set">
|
||||
<div className="lc-set__head">
|
||||
<span className={`lc-set__role lc-set__role--${ROLE_COLOR[role]}`}>● {role}</span>
|
||||
<span className="lc-set__idx">Set {index + 1} / {total}</span>
|
||||
</div>
|
||||
<div className="lc-balls">
|
||||
{pick.numbers.map(n => (
|
||||
<span key={n} className={`ball ball--${Math.ceil(n / 10)}`}>{n}</span>
|
||||
))}
|
||||
</div>
|
||||
<p className="lc-set__reason">{pick.reason}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
11
src/pages/lotto/components/decision/RetrospectiveBox.jsx
Normal file
11
src/pages/lotto/components/decision/RetrospectiveBox.jsx
Normal file
@@ -0,0 +1,11 @@
|
||||
export default function RetrospectiveBox({ briefing, review }) {
|
||||
const retro = briefing?.narrative?.retrospective;
|
||||
if (!retro) return null;
|
||||
const drawNo = review?.draw_no ?? (briefing?.draw_no ? briefing.draw_no - 1 : null);
|
||||
return (
|
||||
<aside className="lc-retro">
|
||||
<p className="lc-retro__time">▸ 지난 주 {drawNo ? `${drawNo}회` : ''} 회고</p>
|
||||
<p className="lc-retro__body">{retro}</p>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
28
src/pages/lotto/components/decision/TierModeToggle.jsx
Normal file
28
src/pages/lotto/components/decision/TierModeToggle.jsx
Normal file
@@ -0,0 +1,28 @@
|
||||
const MODES = [
|
||||
{ key: 'core', label: '코어', sets: 5, amount: 5000 },
|
||||
{ key: 'core_bonus', label: '+ 보너스', sets: 10, amount: 10000 },
|
||||
{ key: 'core_bonus_extended', label: '+ 확장', sets: 15, amount: 15000 },
|
||||
{ key: 'full', label: '+ 풀', sets: 20, amount: 20000 },
|
||||
];
|
||||
|
||||
export default function TierModeToggle({ value, onChange }) {
|
||||
return (
|
||||
<div className="lc-toggle" role="tablist">
|
||||
{MODES.map((m, i) => (
|
||||
<button
|
||||
key={m.key}
|
||||
role="tab"
|
||||
aria-selected={value === m.key}
|
||||
className={`lc-toggle__chip ${value === m.key ? 'is-active' : ''}`}
|
||||
onClick={() => onChange(m.key)}
|
||||
>
|
||||
<span className="lc-toggle__dots">{'●'.repeat(i + 1) + '○'.repeat(3 - i)}</span>
|
||||
<span className="lc-toggle__lbl">{m.label}</span>
|
||||
<span className="lc-toggle__sub">{m.sets}세트 · {m.amount.toLocaleString()}원</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { MODES };
|
||||
25
src/pages/lotto/components/decision/TierSection.jsx
Normal file
25
src/pages/lotto/components/decision/TierSection.jsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import PickCard from './PickCard';
|
||||
|
||||
const TIER_TITLE = {
|
||||
core: '코어 (필수, 5세트)',
|
||||
bonus: '보너스 (+5)',
|
||||
extended: '확장 (+5)',
|
||||
pool: '풀 (+5)',
|
||||
};
|
||||
|
||||
export default function TierSection({ tier, picks, rationale, indexBase = 0, totalSets }) {
|
||||
if (!picks?.length) return null;
|
||||
return (
|
||||
<section className={`lc-tier lc-tier--${tier}`}>
|
||||
<header className="lc-tier__head">
|
||||
<h4>{TIER_TITLE[tier]}</h4>
|
||||
{rationale && tier !== 'core' && (
|
||||
<p className="lc-tier__rationale">{rationale}</p>
|
||||
)}
|
||||
</header>
|
||||
{picks.map((p, i) => (
|
||||
<PickCard key={i} pick={p} index={indexBase + i} total={totalSets} />
|
||||
))}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
47
src/pages/lotto/components/decision/decision.css
Normal file
47
src/pages/lotto/components/decision/decision.css
Normal file
@@ -0,0 +1,47 @@
|
||||
.lc-card { max-width: 720px; margin: 0 auto; background: linear-gradient(180deg, #161220 0%, #1a1426 100%);
|
||||
border: 1px solid rgba(255,255,255,0.08); border-radius: 16px; padding: 24px; color: #ece6f7; }
|
||||
.lc-head { display: flex; justify-content: space-between; align-items: flex-end; margin-bottom: 14px; }
|
||||
.lc-eyebrow { font-size: 10px; letter-spacing: 2px; opacity: 0.5; text-transform: uppercase; margin: 0 0 4px; }
|
||||
.lc-title { font-size: 22px; font-weight: 700; margin: 0; letter-spacing: -0.02em; }
|
||||
.lc-conf { display: flex; flex-direction: column; align-items: flex-end; }
|
||||
.lc-conf__num { font-family: 'Courier New', monospace; font-size: 28px; font-weight: 700; color: #b8a8ff; letter-spacing: -0.04em; }
|
||||
.lc-conf__lbl { font-size: 9px; letter-spacing: 1.5px; opacity: 0.55; }
|
||||
.lc-retro { background: rgba(184, 168, 255, 0.06); border-left: 2px solid rgba(184, 168, 255, 0.4);
|
||||
padding: 10px 14px; margin: 14px 0; border-radius: 4px; }
|
||||
.lc-retro__time { font-size: 9px; letter-spacing: 1.5px; color: #b8a8ff; opacity: 0.7; margin: 0 0 4px; }
|
||||
.lc-retro__body { font-size: 13px; line-height: 1.55; opacity: 0.85; margin: 0; }
|
||||
.lc-headline { font-size: 16px; font-weight: 600; line-height: 1.5; margin: 18px 0 4px; }
|
||||
.lc-headline-3 { font-size: 12px; opacity: 0.65; line-height: 1.55; margin: 0 0 18px; }
|
||||
.lc-balance { display: flex; justify-content: space-between; align-items: center; padding: 10px 14px;
|
||||
background: rgba(255,255,255,0.03); border-radius: 8px; margin-bottom: 16px; font-size: 11px; }
|
||||
.lc-balance__chips { display: flex; gap: 8px; }
|
||||
.lc-chip { padding: 3px 8px; border-radius: 100px; font-weight: 600; font-size: 11px; }
|
||||
.lc-chip--stable { background: rgba(80, 200, 120, 0.15); color: #76e09a; }
|
||||
.lc-chip--balance { background: rgba(255, 200, 80, 0.15); color: #ffce6e; }
|
||||
.lc-chip--aggro { background: rgba(255, 100, 130, 0.15); color: #ff8aa0; }
|
||||
.lc-toggle { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin: 16px 0; }
|
||||
.lc-toggle__chip { padding: 10px 8px; background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.08);
|
||||
border-radius: 10px; color: #ece6f7; cursor: pointer; display: flex; flex-direction: column; gap: 4px; align-items: center; }
|
||||
.lc-toggle__chip.is-active { background: rgba(184, 168, 255, 0.15); border-color: rgba(184, 168, 255, 0.5); }
|
||||
.lc-toggle__dots { letter-spacing: 2px; font-size: 10px; opacity: 0.7; }
|
||||
.lc-toggle__lbl { font-size: 12px; font-weight: 600; }
|
||||
.lc-toggle__sub { font-size: 10px; opacity: 0.55; }
|
||||
.lc-tier { margin-bottom: 14px; }
|
||||
.lc-tier__head { padding: 8px 0; border-top: 1px dashed rgba(255,255,255,0.1); margin-bottom: 8px; }
|
||||
.lc-tier:first-of-type .lc-tier__head { border-top: none; }
|
||||
.lc-tier__head h4 { font-size: 12px; font-weight: 600; margin: 0 0 4px; opacity: 0.75; letter-spacing: 0.5px; }
|
||||
.lc-tier__rationale { font-size: 11px; opacity: 0.55; margin: 0; }
|
||||
.lc-set { background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.06); border-radius: 12px;
|
||||
padding: 14px; margin-bottom: 10px; }
|
||||
.lc-set__head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
|
||||
.lc-set__role { font-size: 11px; font-weight: 600; letter-spacing: 0.5px; }
|
||||
.lc-set__role--stable { color: #76e09a; }
|
||||
.lc-set__role--balance { color: #ffce6e; }
|
||||
.lc-set__role--aggro { color: #ff8aa0; }
|
||||
.lc-set__idx { font-size: 10px; opacity: 0.4; }
|
||||
.lc-balls { display: flex; gap: 6px; margin-bottom: 8px; flex-wrap: wrap; }
|
||||
.lc-set__reason { font-size: 12px; opacity: 0.7; line-height: 1.45; margin: 0; }
|
||||
.lc-actions { display: flex; gap: 10px; margin-top: 18px; }
|
||||
@media (max-width: 480px) {
|
||||
.lc-toggle { grid-template-columns: repeat(2, 1fr); }
|
||||
}
|
||||
Reference in New Issue
Block a user