From ff7ac48c6bcebabe9e8049816559d6a4bdf9fdf6 Mon Sep 17 00:00:00 2001 From: gahusb Date: Mon, 11 May 2026 09:00:59 +0900 Subject: [PATCH] =?UTF-8?q?feat(lotto):=20DecisionCard=20+=20BulkPurchaseB?= =?UTF-8?q?utton,=20BriefingTab=20=EB=8B=A8=EC=9D=BC=20=ED=99=94=EB=A9=B4?= =?UTF-8?q?=20=EC=9E=AC=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../decision/BulkPurchaseButton.jsx | 33 ++++++ .../components/decision/DecisionCard.jsx | 102 ++++++++++++++++++ .../lotto/components/decision/decision.css | 5 + src/pages/lotto/tabs/BriefingTab.jsx | 15 +-- 4 files changed, 144 insertions(+), 11 deletions(-) create mode 100644 src/pages/lotto/components/decision/BulkPurchaseButton.jsx create mode 100644 src/pages/lotto/components/decision/DecisionCard.jsx diff --git a/src/pages/lotto/components/decision/BulkPurchaseButton.jsx b/src/pages/lotto/components/decision/BulkPurchaseButton.jsx new file mode 100644 index 0000000..afe996e --- /dev/null +++ b/src/pages/lotto/components/decision/BulkPurchaseButton.jsx @@ -0,0 +1,33 @@ +import { useState } from 'react'; +import { bulkPurchase } from '../../../../api'; +import { MODES } from './TierModeToggle'; + +export default function BulkPurchaseButton({ drawNo, tierMode, onSuccess }) { + const [busy, setBusy] = useState(false); + const mode = MODES.find(m => m.key === tierMode) || MODES[0]; + + const onClick = async () => { + if (busy) return; + setBusy(true); + try { + await bulkPurchase({ + draw_no: drawNo, + tier_mode: tierMode, + sets: mode.sets, + amount: mode.amount, + }); + onSuccess?.(); + alert(`${mode.sets}세트 구매 기록 완료!`); + } catch (e) { + alert(`구매 기록 실패: ${e?.message || e}`); + } finally { + setBusy(false); + } + }; + + return ( + + ); +} diff --git a/src/pages/lotto/components/decision/DecisionCard.jsx b/src/pages/lotto/components/decision/DecisionCard.jsx new file mode 100644 index 0000000..492510c --- /dev/null +++ b/src/pages/lotto/components/decision/DecisionCard.jsx @@ -0,0 +1,102 @@ +import { useEffect, useMemo, useState } from 'react'; +import RetrospectiveBox from './RetrospectiveBox'; +import TierModeToggle, { MODES } from './TierModeToggle'; +import TierSection from './TierSection'; +import BulkPurchaseButton from './BulkPurchaseButton'; +import './decision.css'; + +const TIER_CHAIN = { + core: ['core'], + core_bonus: ['core', 'bonus'], + core_bonus_extended: ['core', 'bonus', 'extended'], + full: ['core', 'bonus', 'extended', 'pool'], +}; + +const STORAGE_KEY = 'lotto.tier_mode'; + +export default function DecisionCard({ briefing, review, onPurchaseSuccess }) { + const [tierMode, setTierMode] = useState(() => + localStorage.getItem(STORAGE_KEY) || 'core' + ); + + useEffect(() => { + localStorage.setItem(STORAGE_KEY, tierMode); + }, [tierMode]); + + const visibleTiers = TIER_CHAIN[tierMode]; + + const totalSets = useMemo( + () => visibleTiers.reduce((sum, t) => sum + (briefing?.picks?.[t]?.length || 0), 0), + [briefing, visibleTiers] + ); + + // 분배 칩 — 보이는 계층의 risk_tag 합산 + const balance = useMemo(() => { + const acc = { '안정': 0, '균형': 0, '공격': 0 }; + for (const t of visibleTiers) { + for (const p of (briefing?.picks?.[t] || [])) { + if (acc[p.risk_tag] !== undefined) acc[p.risk_tag]++; + } + } + return acc; + }, [briefing, visibleTiers]); + + if (!briefing) return null; + + let cursor = 0; + + return ( +
+
+
+

Curator Briefing · {briefing.draw_no}회

+

{briefing.narrative.headline}

+
+
+
{briefing.confidence}
+
CONFIDENCE
+
+
+ + + +

+ {(briefing.narrative.summary_3lines || []).join(' · ')} +

+ +
+
+ {balance['안정'] > 0 && 안정 ×{balance['안정']}} + {balance['균형'] > 0 && 균형 ×{balance['균형']}} + {balance['공격'] > 0 && 공격 ×{balance['공격']}} +
+
+ + + + {visibleTiers.map(tier => { + const picks = briefing.picks?.[tier] || []; + const idxBase = cursor; + cursor += picks.length; + return ( + + ); + })} + +
+ +
+
+ ); +} diff --git a/src/pages/lotto/components/decision/decision.css b/src/pages/lotto/components/decision/decision.css index 73eb4c0..ab1721c 100644 --- a/src/pages/lotto/components/decision/decision.css +++ b/src/pages/lotto/components/decision/decision.css @@ -42,6 +42,11 @@ .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; } +.lc-btn { padding: 12px 16px; border-radius: 10px; border: none; font-weight: 600; cursor: pointer; + font-size: 14px; min-width: 160px; } +.lc-btn--prim { background: linear-gradient(135deg, #b8a8ff, #8a78db); color: #14101e; } +.lc-btn--prim:disabled { opacity: 0.5; cursor: not-allowed; } +.lc-btn--ghost { background: transparent; border: 1px solid rgba(255,255,255,0.15); color: #ece6f7; } @media (max-width: 480px) { .lc-toggle { grid-template-columns: repeat(2, 1fr); } } diff --git a/src/pages/lotto/tabs/BriefingTab.jsx b/src/pages/lotto/tabs/BriefingTab.jsx index 8e2210a..cdbf7d9 100644 --- a/src/pages/lotto/tabs/BriefingTab.jsx +++ b/src/pages/lotto/tabs/BriefingTab.jsx @@ -1,25 +1,18 @@ import useBriefing from '../hooks/useBriefing'; -import BriefingHeader from '../components/briefing/BriefingHeader'; -import BriefingSummary from '../components/briefing/BriefingSummary'; -import PickSetCard from '../components/briefing/PickSetCard'; +import useReview from '../hooks/useReview'; +import DecisionCard from '../components/decision/DecisionCard'; import BriefingEmpty from '../components/briefing/BriefingEmpty'; -import CuratorUsageFooter from '../components/briefing/CuratorUsageFooter'; export default function BriefingTab() { const { briefing, loading, error, regenerating, regenerate } = useBriefing(); + const { latest: review } = useReview(); if (loading) return

로딩 중...

; if (!briefing) return ; return (
- - -
-

이번 주 5세트

- {briefing.picks.map((p, i) => )} -
- +
); }