diff --git a/frontend/src/components/MarketSearchPanel.tsx b/frontend/src/components/MarketSearchPanel.tsx index b0098ee..7b310eb 100644 --- a/frontend/src/components/MarketSearchPanel.tsx +++ b/frontend/src/components/MarketSearchPanel.tsx @@ -5,6 +5,7 @@ import { toggleFavorite, FAVORITES_CHANGED_EVENT, } from '../utils/marketStorage'; +import { UPBIT_API } from '../utils/upbitApi'; // ─── Types ─────────────────────────────────────────────────────────────────── interface MarketInfo { @@ -21,14 +22,21 @@ interface TickerInfo { } // ─── Constants ─────────────────────────────────────────────────────────────── -const UPBIT_API = import.meta.env.DEV ? '/upbit-api/v1' : 'https://api.upbit.com/v1'; const TICKER_TTL = 3000; // 3초마다 시세 갱신 -function formatVol(vol: number): string { - if (vol >= 1e12) return `${(vol / 1e12).toFixed(1)}조`; - if (vol >= 1e8) return `${(vol / 1e8).toFixed(0)}억`; - if (vol >= 1e4) return `${(vol / 1e4).toFixed(0)}만`; - return vol.toLocaleString(); +function formatVol(vol: number | string | null | undefined): string { + const v = Number(vol); + if (!Number.isFinite(v)) return '-'; + if (v >= 1e12) return `${(v / 1e12).toFixed(1)}조`; + if (v >= 1e8) return `${(v / 1e8).toFixed(0)}억`; + if (v >= 1e4) return `${(v / 1e4).toFixed(0)}만`; + return v.toLocaleString(); +} + +function formatChangeRate(raw: number | string | null | undefined): number | null { + const rate = Number(raw); + if (!Number.isFinite(rate)) return null; + return rate * 100; } // ─── Component ─────────────────────────────────────────────────────────────── @@ -86,7 +94,10 @@ export const MarketSearchPanel: React.FC = ({ useEffect(() => { setLoading(true); fetch(`${UPBIT_API}/market/all?isDetails=false`) - .then(r => r.json()) + .then(r => { + if (!r.ok) throw new Error(`market/all ${r.status}`); + return r.json(); + }) .then((data: Array<{ market: string; korean_name: string; english_name: string }>) => { const krw = data .filter(m => m.market.startsWith('KRW-')) @@ -129,9 +140,11 @@ export const MarketSearchPanel: React.FC = ({ for (let i = 0; i < allCodes.length; i += BATCH) { const batch = allCodes.slice(i, i + BATCH).join(','); try { - const data: Array<{ market: string } & TickerInfo> = - await fetch(`${UPBIT_API}/ticker?markets=${batch}`).then(r => r.json()); - results.push(...data); + const res = await fetch(`${UPBIT_API}/ticker?markets=${batch}`); + if (!res.ok) continue; + const data: unknown = await res.json(); + if (!Array.isArray(data)) continue; + results.push(...(data as Array<{ market: string } & TickerInfo>)); } catch { /* ignore partial failures */ } } @@ -328,7 +341,7 @@ export const MarketSearchPanel: React.FC = ({ const isFav = favs.has(m.market); const isAdded = addedMarkets?.has(m.market) ?? false; const isActive = m.market === currentMarket; - const rate = tk ? tk.signed_change_rate * 100 : null; + const rate = tk ? formatChangeRate(tk.signed_change_rate) : null; const isUp = rate !== null && rate >= 0; return ( @@ -373,7 +386,9 @@ export const MarketSearchPanel: React.FC = ({ {!embedded && ( <> - {tk ? tk.trade_price.toLocaleString() : '-'} + {tk && Number.isFinite(Number(tk.trade_price)) + ? Number(tk.trade_price).toLocaleString() + : '-'} {rate === null diff --git a/frontend/src/hooks/useMarketTicker.ts b/frontend/src/hooks/useMarketTicker.ts index 1d35ba5..7386af7 100644 --- a/frontend/src/hooks/useMarketTicker.ts +++ b/frontend/src/hooks/useMarketTicker.ts @@ -10,6 +10,7 @@ import { useState, useEffect, useRef, useCallback } from 'react'; import { setMarketNames } from '../utils/marketNameCache'; +import { UPBIT_API } from '../utils/upbitApi'; export type ChangeDir = 'RISE' | 'FALL' | 'EVEN'; @@ -69,7 +70,7 @@ function getMarketCode(raw: UpbitTickerRaw): string { } async function fetchAllMarkets(): Promise { - const res = await fetch('/upbit-api/v1/market/all?isDetails=true'); + const res = await fetch(`${UPBIT_API}/market/all?isDetails=true`); if (!res.ok) throw new Error(`market/all ${res.status}`); const data: UpbitMarketAll[] = await res.json(); const krw = data.filter(m => m.market.startsWith('KRW-')); @@ -88,7 +89,7 @@ async function fetchAllTickers(markets: string[]): Promise { for (let i = 0; i < markets.length; i += 100) { const chunk = markets.slice(i, i + 100); try { - const res = await fetch(`/upbit-api/v1/ticker?markets=${chunk.join(',')}`); + const res = await fetch(`${UPBIT_API}/ticker?markets=${chunk.join(',')}`); if (res.ok) results.push(...(await res.json())); } catch { /* 부분 실패 무시 */ } } diff --git a/frontend/src/utils/upbitApi.ts b/frontend/src/utils/upbitApi.ts index e77b1e7..01a678f 100644 --- a/frontend/src/utils/upbitApi.ts +++ b/frontend/src/utils/upbitApi.ts @@ -4,7 +4,8 @@ import type { OHLCVBar, Timeframe } from '../types'; // 개발: Vite proxy (/upbit-api) → https://api.upbit.com // 프로덕션: nginx proxy (/upbit-api) → https://api.upbit.com // 항상 동일한 상대 경로를 사용하므로 CORS 없이 동일 오리진으로 요청됨 -const UPBIT_API = '/upbit-api/v1'; +/** dev: Vite proxy, prod: nginx proxy — 항상 동일 오리진 상대 경로 */ +export const UPBIT_API = '/upbit-api/v1'; export const UPBIT_MARKETS = [ 'KRW-BTC', 'KRW-ETH', 'KRW-XRP', 'KRW-SOL', 'KRW-DOGE',