diff --git a/src/api.js b/src/api.js index 02ba9bf..9664594 100644 --- a/src/api.js +++ b/src/api.js @@ -697,6 +697,10 @@ export const refreshScreenerSnap = () => apiPost('/api/stock/screener export const listScreenerRuns = (limit = 30) => apiGet (`/api/stock/screener/runs?limit=${limit}`); export const getScreenerRun = (id) => apiGet (`/api/stock/screener/runs/${id}`); +// ---- Stock Holdings Intelligence ---- +export const stockHoldingsIntel = () => apiGet('/api/stock/holdings/intel'); +export const stockHoldingsHistory = (ticker, days = 30) => apiGet(`/api/stock/holdings/intel/history?ticker=${ticker}&days=${days}`); + // --- Lotto Weight Evolver --- export async function fetchEvolverStatus() { diff --git a/src/pages/stock/Stock.css b/src/pages/stock/Stock.css index 885adf0..cb0979a 100644 --- a/src/pages/stock/Stock.css +++ b/src/pages/stock/Stock.css @@ -3016,3 +3016,213 @@ grid-template-columns: 1fr; } } + +/* ══════════════════════════════════════════════════════ + Holdings Intelligence Tab +══════════════════════════════════════════════════════ */ + +.hi-panel { + /* reuses stock-panel--wide layout */ +} + +/* ── 포트 건강 요약 줄 ── */ +.hi-health { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 6px 0; + font-size: 13px; + color: #94a3b8; + background: rgba(148, 163, 184, 0.06); + border: 1px solid rgba(148, 163, 184, 0.12); + border-radius: 8px; + padding: 10px 14px; + margin-bottom: 16px; +} + +.hi-health__sep { + margin: 0 8px; + color: rgba(148, 163, 184, 0.4); +} + +.hi-health__pnl { + font-weight: 700; + font-size: 14px; +} + +.hi-health__pnl.is-up { color: #22c55e; } +.hi-health__pnl.is-down { color: #ef4444; } + +/* ── 분석 기준일 ── */ +.hi-date { + font-size: 11px; + color: #64748b; + margin: 0 0 12px; +} + +/* ── 카드 그리드 ── */ +.hi-cards { + display: flex; + flex-direction: column; + gap: 10px; + margin-bottom: 16px; +} + +/* ── 개별 카드 ── */ +.hi-card { + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(148, 163, 184, 0.12); + border-radius: 10px; + padding: 14px 16px; + display: flex; + flex-direction: column; + gap: 8px; +} + +.hi-card__head { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} + +.hi-action-badge { + display: inline-block; + font-size: 11px; + font-weight: 700; + padding: 2px 8px; + border-radius: 999px; + letter-spacing: 0.02em; + flex-shrink: 0; +} + +.hi-card__name { + font-size: 14px; + font-weight: 700; + color: #e2e8f0; +} + +.hi-card__ticker { + font-size: 11px; + color: #64748b; + font-family: monospace; +} + +.hi-card__pnl { + margin-left: auto; + font-size: 13px; + font-weight: 700; +} + +.hi-card__pnl.is-up { color: #22c55e; } +.hi-card__pnl.is-down { color: #ef4444; } + +.hi-card__reasons { + font-size: 12px; + color: #94a3b8; + line-height: 1.5; +} + +/* ── 기술강도 미니 바 ── */ +.hi-card__score { + display: flex; + align-items: center; + gap: 8px; + font-size: 11px; + color: #64748b; +} + +.hi-card__score strong { + color: #93c5fd; + font-size: 12px; +} + +.hi-score-bar { + flex: 1; + height: 4px; + background: rgba(148, 163, 184, 0.15); + border-radius: 2px; + position: relative; + overflow: hidden; + max-width: 120px; +} + +.hi-score-bar::after { + content: ''; + position: absolute; + left: 0; top: 0; bottom: 0; + width: var(--score, 0%); + background: #93c5fd; + border-radius: 2px; + transition: width 0.4s ease; +} + +/* ── 이슈 목록 ── */ +.hi-card__issues { + display: flex; + flex-direction: column; + gap: 4px; +} + +.hi-issue { + display: flex; + align-items: flex-start; + gap: 6px; + font-size: 11px; + line-height: 1.5; +} + +.hi-issue__dot { + width: 6px; + height: 6px; + border-radius: 50%; + flex-shrink: 0; + margin-top: 4px; +} + +/* ── 빈 상태 ── */ +.hi-empty { + text-align: center; + padding: 48px 16px; + color: #64748b; +} + +.hi-empty__icon { + font-size: 36px; + display: block; + margin-bottom: 12px; +} + +.hi-empty__sub { + font-size: 12px; + margin-top: 6px; + color: #475569; +} + +/* ── 면책 고지 ── */ +.hi-disclaimer { + font-size: 11px; + color: #475569; + margin-top: 4px; + padding-top: 12px; + border-top: 1px solid rgba(148, 163, 184, 0.08); +} + +/* ── 탭 버튼 (holdings intel) ── */ +.stock-main-tab--holdings-intel { + /* reuses stock-main-tab base styles */ +} + +@media (max-width: 640px) { + .hi-card__head { + gap: 6px; + } + + .hi-health { + font-size: 12px; + } + + .hi-score-bar { + display: none; + } +} diff --git a/src/pages/stock/StockTrade.jsx b/src/pages/stock/StockTrade.jsx index d801e9c..e751d72 100644 --- a/src/pages/stock/StockTrade.jsx +++ b/src/pages/stock/StockTrade.jsx @@ -6,7 +6,7 @@ import SwipeableView from '../../components/SwipeableView'; import { formatNumber, formatPercent, toNumeric, profitColorClass, - TAB_PORTFOLIO, TAB_REPORT, TAB_ADVISOR, + TAB_PORTFOLIO, TAB_REPORT, TAB_ADVISOR, TAB_HOLDINGS_INTEL, } from './stockUtils'; /* ── hooks ──────────────────────────────────────────────────────── */ @@ -22,6 +22,7 @@ import useAdvisor from './hooks/useAdvisor'; import PortfolioTab from './components/PortfolioTab'; import ReportTab from './components/ReportTab'; import AdvisorTab from './components/AdvisorTab'; +import HoldingsIntelTab from './components/HoldingsIntelTab'; import SellHistoryDrawer from './components/SellHistoryDrawer'; /* ── component ───────────────────────────────────────────────────── */ @@ -30,8 +31,8 @@ const StockTrade = () => { const [activeTab, setActiveTab] = React.useState(TAB_REPORT); const isMobile = useIsMobile(); - const TAB_ORDER = [TAB_PORTFOLIO, TAB_REPORT, TAB_ADVISOR]; - const tabLabels = ['포트폴리오', '리포트', '어드바이저']; + const TAB_ORDER = [TAB_PORTFOLIO, TAB_REPORT, TAB_ADVISOR, TAB_HOLDINGS_INTEL]; + const tabLabels = ['포트폴리오', '리포트', '어드바이저', '보유종목 인텔']; const tabIndex = TAB_ORDER.indexOf(activeTab); const handleTabChange = useCallback((idx) => setActiveTab(TAB_ORDER[idx]), []); // eslint-disable-line react-hooks/exhaustive-deps @@ -166,7 +167,9 @@ const StockTrade = () => { ? : tabId === TAB_REPORT ? - : , + : tabId === TAB_ADVISOR + ? + : , }))} activeIndex={tabIndex} onTabChange={handleTabChange} @@ -178,6 +181,7 @@ const StockTrade = () => { { id: TAB_PORTFOLIO, icon: '💼', label: '쟁승토리 계좌', badge: pf.portfolioHoldings.length || null }, { id: TAB_REPORT, icon: '📊', label: '리포트', sub: '분석·AI코치' }, { id: TAB_ADVISOR, icon: '🧠', label: 'AI 어드바이저', sub: 'Gemini Pro', className: 'stock-main-tab--advisor' }, + { id: TAB_HOLDINGS_INTEL, icon: '🔍', label: '보유종목 인텔', sub: '신호·이슈', className: 'stock-main-tab--holdings-intel' }, ].map(({ id, icon, label, sub, badge, className: cls }) => (