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:
2026-10-01 11:41:37 +09:00
parent 9de0715165
commit b4e958b477
6 changed files with 392 additions and 1 deletions

View 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

View File

@@ -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]) => (