From c28bd9368c37ee1835bb81ddeedece3716a4449c Mon Sep 17 00:00:00 2001 From: gahusb Date: Thu, 5 Mar 2026 02:45:45 +0900 Subject: [PATCH] =?UTF-8?q?=EC=8B=9C=EC=9E=A5=20=EC=A3=BC=EC=9A=94=20?= =?UTF-8?q?=EC=A7=80=ED=91=9C=20=EC=B0=B8=EA=B3=A0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api.js | 29 ++++-- src/pages/stock/Stock.css | 193 ++++++++++++++++++++++++++++++++++++-- src/pages/stock/Stock.jsx | 161 +++++++++++++++++++++++++++---- vite.config.js | 21 +++++ 4 files changed, 372 insertions(+), 32 deletions(-) diff --git a/src/api.js b/src/api.js index 075bc55..f13eb97 100644 --- a/src/api.js +++ b/src/api.js @@ -156,16 +156,33 @@ export async function getFearAndGreed() { return res.json(); } -// VIX 지수 (Yahoo Finance 공개 API) -export async function getVix() { - const res = await fetch('/ext/vix', { headers: { Accept: 'application/json' } }); +// Yahoo Finance chart API 공통 파서 +async function fetchYahooPrice(extPath) { + const res = await fetch(extPath, { headers: { Accept: 'application/json' } }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); - const price = data?.chart?.result?.[0]?.meta?.regularMarketPrice; - if (price === undefined || price === null) throw new Error('VIX 데이터 없음'); - return { value: Math.round(price * 100) / 100 }; + const meta = data?.chart?.result?.[0]?.meta; + const price = meta?.regularMarketPrice; + const prevClose = meta?.previousClose ?? meta?.chartPreviousClose; + if (price == null) throw new Error('데이터 없음'); + const rounded = Math.round(price * 100) / 100; + const change = prevClose != null ? Math.round((price - prevClose) * 100) / 100 : null; + const changePercent = prevClose ? Math.round(((price - prevClose) / prevClose) * 10000) / 100 : null; + return { value: rounded, change, changePercent }; } +// VIX 지수 (Yahoo Finance 공개 API) +export function getVix() { return fetchYahooPrice('/ext/vix'); } + +// 미국 10년물 국채 금리 (^TNX) +export function getTreasury10Y() { return fetchYahooPrice('/ext/treasury'); } + +// WTI 원유 선물 (CL=F) +export function getWTI() { return fetchYahooPrice('/ext/wti'); } + +// Brent 원유 선물 (BZ=F) +export function getBrent() { return fetchYahooPrice('/ext/brent'); } + // ── TODO API ───────────────────────────────────────────────────────────────── export function getTodos() { diff --git a/src/pages/stock/Stock.css b/src/pages/stock/Stock.css index 309574a..789d405 100644 --- a/src/pages/stock/Stock.css +++ b/src/pages/stock/Stock.css @@ -1570,6 +1570,14 @@ gap: 6px; padding: 12px 0 8px; text-align: center; + width: 100%; +} + +.stock-vix__top { + display: flex; + align-items: center; + gap: 16px; + justify-content: center; } .stock-vix__score { @@ -1586,17 +1594,70 @@ transition: color 0.4s ease; } -.stock-vix__legend { - display: flex; - flex-wrap: wrap; - gap: 6px 12px; - justify-content: center; - margin-top: 10px; - font-size: 11px; +.stock-vix__change { + margin: 4px 0 0; + font-size: 12px; + color: var(--muted); } -.stock-vix__legend span { - font-weight: 500; +.stock-vix__change.is-up { color: #f04452; } +.stock-vix__change.is-down { color: #3b82f6; } + +/* VIX 구간별 설명 목록 */ +.stock-vix__levels { + display: grid; + gap: 6px; + margin-top: 12px; + padding-top: 12px; + border-top: 1px solid var(--line); + width: 100%; + text-align: left; +} + +.stock-vix__level { + padding: 8px 10px; + border-radius: 8px; + border: 1px solid transparent; + background: rgba(255, 255, 255, 0.02); + transition: background 0.2s ease, border-color 0.2s ease; +} + +.stock-vix__level.is-current { + background: rgba(255, 255, 255, 0.05); + border-color: rgba(255, 255, 255, 0.1); +} + +.stock-vix__level-head { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 4px; +} + +.stock-vix__level-dot { + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; +} + +.stock-vix__level-label { + font-size: 12px; + font-weight: 700; +} + +.stock-vix__level-range { + font-size: 11px; + color: var(--muted); + margin-left: auto; +} + +.stock-vix__level-desc { + margin: 0; + font-size: 12px; + color: var(--muted); + line-height: 1.6; + padding-left: 16px; } /* ══════════════════════════════════════════════════════════════════ @@ -1679,4 +1740,118 @@ .stock-news-grid { grid-template-columns: 1fr; } +} + +/* ══════════════════════════════════════════════════════════════════ + 매크로 지표 카드 + ══════════════════════════════════════════════════════════════════ */ + +.stock-macro-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + gap: 14px; +} + +.stock-macro-card { + border: 1px solid var(--line); + border-radius: 14px; + padding: 16px; + background: rgba(0, 0, 0, 0.2); + display: grid; + gap: 6px; +} + +.stock-macro-card__title { + margin: 0; + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.12em; + color: var(--muted); +} + +.stock-macro-card__value { + font-size: 32px; + font-weight: 800; + color: var(--text); + line-height: 1; +} + +.stock-macro-card__change { + margin: 0; + font-size: 12px; + color: var(--muted); +} + +.stock-macro-card__change.is-up { color: #f04452; } +.stock-macro-card__change.is-down { color: #3b82f6; } + +.stock-macro-card__desc { + margin: 6px 0 0; + font-size: 12px; + color: var(--muted); + line-height: 1.6; + border-top: 1px solid var(--line); + padding-top: 8px; +} + +/* ══════════════════════════════════════════════════════════════════ + 시장 건강 지표 (Placeholder) + ══════════════════════════════════════════════════════════════════ */ + +.stock-health-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 14px; +} + +.stock-placeholder-card { + border: 1px dashed var(--line); + border-radius: 14px; + padding: 16px; + background: rgba(0, 0, 0, 0.1); + display: grid; + gap: 8px; + opacity: 0.75; +} + +.stock-placeholder-card__title { + margin: 0; + font-size: 13px; + font-weight: 600; + color: var(--text); +} + +.stock-placeholder-card__status { + font-size: 13px; + color: var(--muted); +} + +.stock-placeholder-card__desc { + margin: 0; + font-size: 12px; + color: var(--muted); + line-height: 1.6; +} + +.stock-placeholder-card__api { + font-size: 11px; + color: var(--muted); + opacity: 0.7; + font-family: monospace; + background: rgba(0, 0, 0, 0.2); + padding: 4px 8px; + border-radius: 6px; + border: 1px solid var(--line); + display: inline-block; +} + +@media (max-width: 768px) { + .stock-macro-grid, + .stock-health-grid { + grid-template-columns: 1fr; + } + + .stock-macro-card__value { + font-size: 26px; + } } \ No newline at end of file diff --git a/src/pages/stock/Stock.jsx b/src/pages/stock/Stock.jsx index 0c3941e..df77847 100644 --- a/src/pages/stock/Stock.jsx +++ b/src/pages/stock/Stock.jsx @@ -1,6 +1,6 @@ import React, { useEffect, useMemo, useState } from 'react'; import { Link } from 'react-router-dom'; -import { getStockIndices, getStockNews, getFearAndGreed, getVix } from '../../api'; +import { getStockIndices, getStockNews, getFearAndGreed, getVix, getTreasury10Y, getWTI, getBrent } from '../../api'; import Loading from '../../components/Loading'; import FearGreedGauge from '../../components/FearGreedGauge'; import './Stock.css'; @@ -77,12 +77,35 @@ const getDirection = (change, percent, direction) => { return ''; }; +const VIX_LEVELS = [ + { + range: '0 – 12', label: '극히 낮음', color: '#22c55e', + desc: '시장이 극도로 안정적. 오히려 투자자 안일함의 신호일 수 있어, 갑작스러운 조정에 대비가 필요합니다.', + }, + { + range: '12 – 20', label: '정상', color: '#84cc16', + desc: '시장이 안정적인 상태. 보통 상승장에서 나타나며, 건강한 변동성 수준입니다.', + }, + { + range: '20 – 30', label: '주의', color: '#eab308', + desc: '불확실성이 높아지는 구간. 주가와 반대로 움직이며, 단기 바닥 신호로 해석되기도 합니다.', + }, + { + range: '30 – 40', label: '높음', color: '#f97316', + desc: '극도의 공포가 퍼진 상태. 급격한 매도세가 나타나지만, 역사적으로 역발상 매수 기회가 되기도 합니다.', + }, + { + range: '40+', label: '극단', color: '#ef4444', + desc: '패닉 수준의 공포. 2008 금융위기·2020 코로나 때 발생. VIX가 꺾이기 시작하면 심리적 진정의 시작입니다.', + }, +]; + const getVixLevel = (score) => { - if (score < 12) return { label: '극히 낮음', color: '#22c55e' }; - if (score < 20) return { label: '정상', color: '#84cc16' }; - if (score < 30) return { label: '보통', color: '#eab308' }; - if (score < 40) return { label: '높음', color: '#f97316' }; - return { label: '극단', color: '#ef4444' }; + if (score < 12) return VIX_LEVELS[0]; + if (score < 20) return VIX_LEVELS[1]; + if (score < 30) return VIX_LEVELS[2]; + if (score < 40) return VIX_LEVELS[3]; + return VIX_LEVELS[4]; }; const Stock = () => { @@ -99,6 +122,7 @@ const Stock = () => { const [fgData, setFgData] = useState(null); const [vixData, setVixData] = useState(null); + const [macroData, setMacroData] = useState({ treasury: null, wti: null, brent: null }); const combinedNews = useMemo( () => [...newsDomestic, ...newsOverseas], @@ -156,6 +180,14 @@ const Stock = () => { }) .catch(() => { }); getVix().then(setVixData).catch(() => { }); + Promise.allSettled([getTreasury10Y(), getWTI(), getBrent()]) + .then(([t, w, b]) => { + setMacroData({ + treasury: t.status === 'fulfilled' ? t.value : null, + wti: w.status === 'fulfilled' ? w.value : null, + brent: b.status === 'fulfilled' ? b.value : null, + }); + }); }, []); const indexOrder = [ @@ -307,18 +339,36 @@ const Stock = () => { {vixData ? (
-
- {vixData.value ?? vixData.vix ?? '--'} +
+
+ {vixData.value ?? '--'} +
+
+

+ {getVixLevel(vixData.value ?? 0).label} +

+ {vixData.change != null && ( +

= 0 ? 'is-up' : 'is-down'}`}> + {vixData.change >= 0 ? '+' : ''}{vixData.change} + {vixData.changePercent != null && ` (${vixData.changePercent >= 0 ? '+' : ''}${vixData.changePercent}%)`} +

+ )} +
-

- {getVixLevel(vixData.value ?? vixData.vix ?? 0).label} -

-
- {'<12'} 극히낮음 - 12-20 정상 - 20-30 보통 - 30-40 높음 - {'40+'} 극단 +
+ {VIX_LEVELS.map((level) => ( +
+
+ + {level.label} + {level.range} +
+

{level.desc}

+
+ ))}
) : ( @@ -327,6 +377,83 @@ const Stock = () => {
+ {/* 매크로 지표 섹션 */} +
+
+
+

글로벌 매크로

+

매크로 지표

+

금리·원자재 등 주요 거시경제 지표를 확인합니다.

+
+
+
+
+

미국 10년물 국채 금리

+
+ {macroData.treasury ? `${macroData.treasury.value}%` : '--'} +
+ {macroData.treasury?.change != null && ( +

= 0 ? 'is-up' : 'is-down'}`}> + {macroData.treasury.change >= 0 ? '+' : ''}{macroData.treasury.change} + {macroData.treasury.changePercent != null && ` (${macroData.treasury.changePercent >= 0 ? '+' : ''}${macroData.treasury.changePercent}%)`} +

+ )} +

금리 상승 시 주식 밸류에이션 압박. 4% 이상 지속은 주식 하락 압력 신호. 단기 급등은 인플레이션 우려를 반영합니다.

+
+
+

WTI 유가

+
+ {macroData.wti ? `$${macroData.wti.value}` : '--'} +
+ {macroData.wti?.change != null && ( +

= 0 ? 'is-up' : 'is-down'}`}> + {macroData.wti.change >= 0 ? '+' : ''}{macroData.wti.change} + {macroData.wti.changePercent != null && ` (${macroData.wti.changePercent >= 0 ? '+' : ''}${macroData.wti.changePercent}%)`} +

+ )} +

에너지 인플레이션 지표. $80 이상 지속 시 물가 상승 우려 확대. 급락은 경기침체 가능성을 반영하기도 합니다.

+
+
+

Brent 유가

+
+ {macroData.brent ? `$${macroData.brent.value}` : '--'} +
+ {macroData.brent?.change != null && ( +

= 0 ? 'is-up' : 'is-down'}`}> + {macroData.brent.change >= 0 ? '+' : ''}{macroData.brent.change} + {macroData.brent.changePercent != null && ` (${macroData.brent.changePercent >= 0 ? '+' : ''}${macroData.brent.changePercent}%)`} +

+ )} +

국제 기준 유가. WTI와 함께 에너지 시장 방향을 파악하는 데 활용. 지정학 리스크 시 WTI 대비 프리미엄 형성.

+
+
+
+ + {/* 시장 건강 지표 (Placeholder) */} +
+
+
+

시장 건강

+

시장 건강 지표

+

백엔드 API 연동 후 실시간 데이터를 표시합니다.

+
+
+
+
+

ADR (등락주선 비율)

+
🔧 데이터 준비 중
+

일정 기간 상승종목 ÷ (상승+하락) 종목 비율. 0.5 이상 = 폭넓은 상승장. 0.3 이하 = 일부 대형주만 오르는 약세 신호.

+ GET /api/stock/adr +
+
+

고객예탁금 / 신용융자

+
🔧 데이터 준비 중
+

고객예탁금 증가 = 투자 대기자금 유입 = 강세. 신용융자 급증 = 과열 경고. 예탁금 감소 + 신용 급증 = 위험 구간.

+ GET /api/stock/deposit +
+
+
+
diff --git a/vite.config.js b/vite.config.js index f62394d..8e4cb45 100644 --- a/vite.config.js +++ b/vite.config.js @@ -38,6 +38,27 @@ export default defineConfig({ secure: true, rewrite: () => '/v8/finance/chart/%5EVIX?interval=1d&range=1d', }, + // 미국 10년물 국채 금리 (^TNX) + '/ext/treasury': { + target: 'https://query1.finance.yahoo.com', + changeOrigin: true, + secure: true, + rewrite: () => '/v8/finance/chart/%5ETNX?interval=1d&range=1d', + }, + // WTI 원유 선물 (CL=F) + '/ext/wti': { + target: 'https://query1.finance.yahoo.com', + changeOrigin: true, + secure: true, + rewrite: () => '/v8/finance/chart/CL%3DF?interval=1d&range=1d', + }, + // Brent 원유 선물 (BZ=F) + '/ext/brent': { + target: 'https://query1.finance.yahoo.com', + changeOrigin: true, + secure: true, + rewrite: () => '/v8/finance/chart/BZ%3DF?interval=1d&range=1d', + }, }, }, })