// Backend API client. // NEXT_PUBLIC_API_BASE 는 docker-compose 에서 http://localhost:8000 으로 주입됨. const RAW_BASE = process.env.NEXT_PUBLIC_API_BASE ?? "http://localhost:8000"; export const API_BASE = RAW_BASE.replace(/\/$/, ""); export type Symbol = { code: string; name: string; market: string; sector: string | null; is_seed: boolean; }; export type SymbolSearch = { q: string; count: number; items: Symbol[]; }; export type OhlcvPoint = { date: string; open: number | null; high: number | null; low: number | null; close: number | null; volume: number | null; }; export type SentimentPoint = { date: string; n_articles: number; mean_score: number | null; weighted_score: number | null; }; export type TradingValuePoint = { date: string; foreign_net: number | null; institution_net: number | null; individual_net: number | null; }; export type ChartPayload = { code: string; name: string; market: string; range: { from: string; to: string }; ohlcv: OhlcvPoint[]; sentiment: SentimentPoint[]; trading_value: TradingValuePoint[]; }; export type PredictionStep = { horizon: number; target_idx?: number; point_close: number; ci_low: number; ci_high: number; prob_up: number; prob_flat: number; prob_down: number; direction: "up" | "flat" | "down"; expected_return: number; target_date?: string; }; export type PredictResponse = { code: string; base_date: string; base_close: number; sources_used: string[]; steps: PredictionStep[]; saved_prediction_ids: number[]; user_triggered: boolean; }; export type LatestPredictionStep = { predicted_at: string | null; target_date: string; horizon: number; direction: "up" | "flat" | "down" | string; prob_up: number | null; prob_flat: number | null; prob_down: number | null; expected_return: number | null; point_close: number | null; ci_low: number | null; ci_high: number | null; user_triggered: boolean; features_snapshot: unknown; }; export type LatestPredictionResponse = { code: string; name?: string; found: boolean; base_date?: string; base_close?: number | null; steps: LatestPredictionStep[]; }; export type MetricsRow = { model: string; horizon: number; n: number; hit_rate: number | null; mae: number | null; }; export type MetricsResponse = { code?: string; name?: string; window_days: number; range: { from: string; to: string }; by_model_horizon: MetricsRow[]; }; export type NewsItem = { source: string; published_at: string | null; title: string; url: string; sentiment_score: number | null; sentiment_label: string | null; }; export type NewsResponse = { code: string; name: string; count: number; items: NewsItem[]; }; async function getJson(path: string, init?: RequestInit): Promise { const res = await fetch(`${API_BASE}${path}`, { ...init, headers: { Accept: "application/json", ...(init?.headers ?? {}), }, cache: "no-store", }); if (!res.ok) { const text = await res.text().catch(() => ""); throw new Error(`API ${path} → ${res.status} ${text || res.statusText}`); } return (await res.json()) as T; } export const api = { search: (q: string, seedOnly = false, limit = 20) => getJson( `/api/symbols/search?q=${encodeURIComponent(q)}&limit=${limit}&seed_only=${seedOnly}`, ), getSymbol: (code: string) => getJson(`/api/symbols/${encodeURIComponent(code)}`), getChart: (code: string, days = 180) => getJson(`/api/chart/${encodeURIComponent(code)}?days=${days}`), predict: (code: string, horizons = "1,3,5") => getJson( `/api/predict/${encodeURIComponent(code)}?horizons=${encodeURIComponent(horizons)}`, { method: "POST" }, ), latestPrediction: (code: string) => getJson(`/api/predict/${encodeURIComponent(code)}/latest`), metrics: (code: string, windowDays = 30) => getJson( `/api/metrics/${encodeURIComponent(code)}?window_days=${windowDays}`, ), overallMetrics: (windowDays = 30) => getJson(`/api/metrics?window_days=${windowDays}`), news: (code: string, limit = 20, source?: string) => getJson( `/api/news/${encodeURIComponent(code)}?limit=${limit}${ source ? `&source=${encodeURIComponent(source)}` : "" }`, ), };