From bf1c23e66a367239238038fd1461c9d6a434a690 Mon Sep 17 00:00:00 2001 From: gahusb Date: Wed, 15 Apr 2026 08:31:35 +0900 Subject: [PATCH] =?UTF-8?q?feat(lotto):=20=EB=B8=8C=EB=A6=AC=ED=95=91=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20+=20CSS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/lotto/Lotto.css | 34 +++++++++++++++++++ .../components/briefing/BriefingEmpty.jsx | 12 +++++++ .../components/briefing/BriefingHeader.jsx | 28 +++++++++++++++ .../components/briefing/BriefingSummary.jsx | 16 +++++++++ .../briefing/CuratorUsageFooter.jsx | 17 ++++++++++ .../lotto/components/briefing/PickSetCard.jsx | 18 ++++++++++ .../lotto/components/briefing/pricing.js | 23 +++++++++++++ 7 files changed, 148 insertions(+) create mode 100644 src/pages/lotto/components/briefing/BriefingEmpty.jsx create mode 100644 src/pages/lotto/components/briefing/BriefingHeader.jsx create mode 100644 src/pages/lotto/components/briefing/BriefingSummary.jsx create mode 100644 src/pages/lotto/components/briefing/CuratorUsageFooter.jsx create mode 100644 src/pages/lotto/components/briefing/PickSetCard.jsx create mode 100644 src/pages/lotto/components/briefing/pricing.js diff --git a/src/pages/lotto/Lotto.css b/src/pages/lotto/Lotto.css index 0b70d6a..4ac59dc 100644 --- a/src/pages/lotto/Lotto.css +++ b/src/pages/lotto/Lotto.css @@ -1475,3 +1475,37 @@ gap: 10px; } } + +/* ── Briefing UI ──────────────────────────────────────────────────────────── */ +.briefing-header { padding: 16px; border-radius: 12px; background: rgba(129,140,248,0.08); margin-bottom: 16px; } +.briefing-header-row { display: flex; justify-content: space-between; align-items: center; } +.briefing-meta { display: flex; gap: 12px; color: #94a3b8; font-size: 0.85rem; margin-top: 4px; flex-wrap: wrap; } +.briefing-confidence strong { color: #e2e8f0; } +.briefing-tokens { font-family: monospace; } +.briefing-confidence-bar { height: 4px; background: rgba(255,255,255,0.1); border-radius: 2px; margin-top: 8px; overflow: hidden; } +.briefing-confidence-bar > div { height: 100%; background: linear-gradient(90deg, #818cf8, #34d399); transition: width .3s; } +.briefing-summary { padding: 12px 16px; background: rgba(0,0,0,0.2); border-radius: 10px; margin-bottom: 16px; } +.briefing-summary h3 { margin: 0 0 8px; } +.briefing-3lines { margin: 0; padding-left: 20px; } +.briefing-hotcold { color: #fbbf24; margin-top: 8px; } +.briefing-warning { color: #f87171; margin-top: 8px; } +.pick-card { padding: 12px; border-radius: 10px; background: rgba(255,255,255,0.04); border-left: 3px solid #64748b; margin-bottom: 8px; } +.pick-card--안정 { border-left-color: #34d399; } +.pick-card--균형 { border-left-color: #fbbf24; } +.pick-card--공격 { border-left-color: #f87171; } +.pick-card-header { display: flex; justify-content: space-between; font-size: 0.85rem; color: #94a3b8; margin-bottom: 6px; } +.pick-card-balls { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 6px; } +.ball { width: 32px; height: 32px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; font-weight: bold; color: #fff; } +.ball--1 { background: #fbbf24; } .ball--2 { background: #60a5fa; } .ball--3 { background: #f87171; } +.ball--4 { background: #94a3b8; } .ball--5 { background: #34d399; } +.pick-card-reason { margin: 0; font-size: 0.85rem; color: #cbd5e1; } +.briefing-empty { text-align: center; padding: 40px 20px; color: #94a3b8; } +.briefing-empty button { margin-top: 12px; padding: 8px 20px; } +.briefing-empty-hint { font-size: 0.85rem; } +.briefing-error { color: #f87171; margin-top: 8px; } +.curator-usage-footer { display: flex; gap: 12px; padding: 10px 14px; background: rgba(0,0,0,0.25); border-radius: 8px; font-size: 0.8rem; color: #94a3b8; margin-top: 24px; flex-wrap: wrap; font-family: monospace; } +@media (max-width: 768px) { + .briefing-meta { font-size: 0.75rem; } + .briefing-tokens { width: 100%; } + .pick-card-balls { justify-content: center; } +} diff --git a/src/pages/lotto/components/briefing/BriefingEmpty.jsx b/src/pages/lotto/components/briefing/BriefingEmpty.jsx new file mode 100644 index 0000000..81f1d14 --- /dev/null +++ b/src/pages/lotto/components/briefing/BriefingEmpty.jsx @@ -0,0 +1,12 @@ +export default function BriefingEmpty({ regenerating, onRegenerate, error }) { + return ( +
+

아직 이번 주 브리핑이 없습니다.

+

매주 월요일 07:00에 자동 생성됩니다.

+ + {error &&

⚠️ {error}

} +
+ ); +} diff --git a/src/pages/lotto/components/briefing/BriefingHeader.jsx b/src/pages/lotto/components/briefing/BriefingHeader.jsx new file mode 100644 index 0000000..517ed09 --- /dev/null +++ b/src/pages/lotto/components/briefing/BriefingHeader.jsx @@ -0,0 +1,28 @@ +import { estimateCost, fmtUsd, fmtTokens } from './pricing'; + +export default function BriefingHeader({ briefing, regenerating, onRegenerate }) { + const cost = estimateCost(briefing); + const genDate = new Date(briefing.generated_at).toLocaleString('ko-KR'); + return ( +
+
+

🗓 #{briefing.draw_no}회 브리핑

+ +
+
+ {genDate} + + 신뢰도 {briefing.confidence}/100 + + + {fmtTokens(briefing.tokens_input)} in · {fmtTokens(briefing.tokens_output)} out · {fmtUsd(cost)} + +
+
+
+
+
+ ); +} diff --git a/src/pages/lotto/components/briefing/BriefingSummary.jsx b/src/pages/lotto/components/briefing/BriefingSummary.jsx new file mode 100644 index 0000000..1858ba9 --- /dev/null +++ b/src/pages/lotto/components/briefing/BriefingSummary.jsx @@ -0,0 +1,16 @@ +export default function BriefingSummary({ narrative }) { + return ( +
+

{narrative.headline}

+
    + {narrative.summary_3lines.map((line, i) =>
  • {line}
  • )} +
+ {narrative.hot_cold_comment && ( +

🔥❄️ {narrative.hot_cold_comment}

+ )} + {narrative.warnings && ( +

⚠️ {narrative.warnings}

+ )} +
+ ); +} diff --git a/src/pages/lotto/components/briefing/CuratorUsageFooter.jsx b/src/pages/lotto/components/briefing/CuratorUsageFooter.jsx new file mode 100644 index 0000000..d980371 --- /dev/null +++ b/src/pages/lotto/components/briefing/CuratorUsageFooter.jsx @@ -0,0 +1,17 @@ +import useCuratorUsage from '../../hooks/useCuratorUsage'; +import { estimateCost, fmtUsd, fmtTokens } from './pricing'; + +export default function CuratorUsageFooter() { + const { usage } = useCuratorUsage(30); + if (!usage) return null; + const cost = estimateCost(usage); + return ( +
+ 최근 30일 큐레이터: + {usage.calls}회 호출 + {fmtTokens(usage.tokens_input + usage.tokens_output)} tokens + {fmtUsd(cost)} + 캐시 {(usage.cache_hit_rate * 100).toFixed(0)}% +
+ ); +} diff --git a/src/pages/lotto/components/briefing/PickSetCard.jsx b/src/pages/lotto/components/briefing/PickSetCard.jsx new file mode 100644 index 0000000..3d82a71 --- /dev/null +++ b/src/pages/lotto/components/briefing/PickSetCard.jsx @@ -0,0 +1,18 @@ +const RISK_BADGE = { '안정': '🟢', '균형': '🟡', '공격': '🔴' }; + +export default function PickSetCard({ pick, index }) { + return ( +
+
+ Set {index + 1} + {RISK_BADGE[pick.risk_tag] || '⚪'} {pick.risk_tag} +
+
+ {pick.numbers.map(n => ( + {n} + ))} +
+

{pick.reason}

+
+ ); +} diff --git a/src/pages/lotto/components/briefing/pricing.js b/src/pages/lotto/components/briefing/pricing.js new file mode 100644 index 0000000..e83414a --- /dev/null +++ b/src/pages/lotto/components/briefing/pricing.js @@ -0,0 +1,23 @@ +const IN_PER_M = 3.00; +const OUT_PER_M = 15.00; +const CACHE_READ_PER_M = 0.30; +const CACHE_WRITE_PER_M = 3.75; + +export function estimateCost({ tokens_input = 0, tokens_output = 0, cache_read = 0, cache_write = 0 }) { + const usd = + (tokens_input / 1_000_000) * IN_PER_M + + (tokens_output / 1_000_000) * OUT_PER_M + + (cache_read / 1_000_000) * CACHE_READ_PER_M + + (cache_write / 1_000_000) * CACHE_WRITE_PER_M; + return usd; +} + +export function fmtUsd(usd) { + if (usd < 0.01) return `$${usd.toFixed(4)}`; + return `$${usd.toFixed(3)}`; +} + +export function fmtTokens(n) { + if (n >= 1000) return `${(n / 1000).toFixed(1)}K`; + return String(n); +}