feat(market): 대시보드 전체시장 지수 바 (KOSPI·KOSDAQ·KOSPI200·S&P500·NASDAQ)
- GET /api/v1/market/indices — 네이버 JSON API(비-KIS), 45초 캐시 - 부분 실패 시 직전값 병합, 전부 실패 시 12초 쿨다운 + 직전 캐시 반환 - 방향 code 해석 불가 시 원 부호 폴백, NaN/inf 거부 - MarketIndexBar: 60초 갱신, 상승 빨강/하락 파랑, 장 마감 배지 - 테스트 12건 (전체 877 passed) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
60
frontend/src/components/MarketIndexBar.tsx
Normal file
60
frontend/src/components/MarketIndexBar.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* MarketIndexBar.tsx — 전체시장 지수 바 (KOSPI/KOSDAQ/KOSPI200/S&P500/NASDAQ)
|
||||
* 데이터: GET /market/indices (네이버 지수, 서버 45초 캐시) / 폴링 60초
|
||||
* 한국 관례: 상승 빨강, 하락 파랑
|
||||
*/
|
||||
import React from 'react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import api from '../lib/api'
|
||||
|
||||
interface IndexItem {
|
||||
code: string
|
||||
name: string
|
||||
market: 'KR' | 'US'
|
||||
price: number
|
||||
change: number
|
||||
change_pct: number
|
||||
market_status?: string | null
|
||||
}
|
||||
|
||||
interface IndicesResponse {
|
||||
indices: IndexItem[]
|
||||
}
|
||||
|
||||
const MarketIndexBar = React.memo(() => {
|
||||
const { data } = useQuery<IndicesResponse>({
|
||||
queryKey: ['market-indices'],
|
||||
queryFn: async () => (await api.get('/market/indices')).data,
|
||||
refetchInterval: 60000,
|
||||
staleTime: 30000,
|
||||
})
|
||||
|
||||
const items = data?.indices ?? []
|
||||
if (items.length === 0) return null
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-0 mb-2 bg-[#0c1522] border border-[#152035] rounded-sm overflow-x-auto">
|
||||
{items.map((it) => {
|
||||
const color = it.change > 0 ? 'text-[#ef4444]' : it.change < 0 ? 'text-[#3b82f6]' : 'text-[#8faabf]'
|
||||
const arrow = it.change > 0 ? '▲' : it.change < 0 ? '▼' : '-'
|
||||
const sign = it.change > 0 ? '+' : ''
|
||||
return (
|
||||
<div key={it.code} className="flex items-baseline gap-2 px-3 py-1.5 border-r border-[#152035] last:border-r-0 whitespace-nowrap">
|
||||
<span className="text-[10px] text-[#4a6580]">{it.name}</span>
|
||||
{it.market_status && it.market_status !== 'OPEN' && (
|
||||
<span className="text-[8px] px-1 border border-[#152035] rounded-sm text-[#4a6580]">마감</span>
|
||||
)}
|
||||
<span className={`text-[12px] font-mono font-semibold ${color}`}>
|
||||
{it.price.toLocaleString('ko-KR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
||||
</span>
|
||||
<span className={`text-[10px] font-mono ${color}`}>
|
||||
{arrow} {Math.abs(it.change).toLocaleString('ko-KR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} ({sign}{it.change_pct.toFixed(2)}%)
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export default MarketIndexBar
|
||||
@@ -36,6 +36,7 @@ import OrderbookPanel from '../components/OrderbookPanel'
|
||||
import RecommendationPanel from '../components/RecommendationPanel'
|
||||
import JournalPanel from '../components/JournalPanel'
|
||||
import AccountSetupNotice from '../components/AccountSetupNotice'
|
||||
import MarketIndexBar from '../components/MarketIndexBar'
|
||||
import { CandleData, Position, TradeHistory } from '../types/trading'
|
||||
import { isUsStock } from '../utils/stockUtils'
|
||||
import './Dashboard.css'
|
||||
@@ -884,6 +885,7 @@ const Dashboard = () => {
|
||||
|
||||
<main className="flex-1 p-4 max-w-[1920px] mx-auto w-full flex flex-col gap-3 overflow-hidden">
|
||||
<div className="flex-shrink-0">
|
||||
<MarketIndexBar />
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-0 bg-[#0c1522] border border-[#152035] rounded-sm overflow-hidden">
|
||||
{([['ALL', '전체'], ['KR', '국내'], ['US', '미국']] as const).map(([value, label]) => (
|
||||
|
||||
Reference in New Issue
Block a user