// API 기본 설정 const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:18001/api/v1'; // Fetch 기반 API 클라이언트 const createApiClient = () => { const request = async ( endpoint: string, options: RequestInit = {} ): Promise => { const url = `${API_BASE_URL}${endpoint}`; console.log(`🚀 API Request: ${options.method || 'GET'} ${url}`); const config: RequestInit = { signal: AbortSignal.timeout(30000), headers: { 'Content-Type': 'application/json', ...options.headers, }, ...options, }; try { const response = await fetch(url, config); console.log(`✅ API Response: ${response.status} ${endpoint}`); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response; } catch (error) { console.error('❌ API Error:', error); throw error; } }; return { get: async (endpoint: string) => { const response = await request(endpoint, { method: 'GET' }); return { data: await response.json() }; }, post: async (endpoint: string, data?: any) => { const response = await request(endpoint, { method: 'POST', body: data ? JSON.stringify(data) : undefined, }); return { data: await response.json() }; }, patch: async (endpoint: string, data?: any) => { const response = await request(endpoint, { method: 'PATCH', body: data ? JSON.stringify(data) : undefined, }); return { data: await response.json() }; }, delete: async (endpoint: string) => { const response = await request(endpoint, { method: 'DELETE' }); return { data: await response.json() }; } }; }; export const api = createApiClient(); // 타입 정의 export interface Company { ticker: string; name: string; cik?: string; sector?: string; industry?: string; business_description?: string; } export interface FinancialData { period_date: string; period_type: string; filing_type?: string; revenue?: number; gross_profit?: number; operating_income?: number; net_income?: number; eps?: number; total_assets?: number; total_equity?: number; total_debt?: number; cash?: number; shares_outstanding?: number; operating_cash_flow?: number; free_cash_flow?: number; capex?: number; data_source: string; is_estimated: boolean; // Calculated metrics pe_ratio?: number; pb_ratio?: number; ps_ratio?: number; roe?: number; roa?: number; gross_margin?: number; operating_margin?: number; net_margin?: number; debt_to_equity?: number; debt_to_assets?: number; ocf_margin?: number; fcf_margin?: number; market_cap?: number; } export interface PriceData { date: string; open: number; high: number; low: number; close: number; volume: number; adj_close?: number; } export interface FinancialDataRequest { ticker: string; start_date: string; end_date: string; period_type?: 'quarterly' | 'annual' | 'all'; include_metrics?: boolean; force_refresh?: boolean; } export interface PriceDataRequest { ticker: string; start_date: string; end_date: string; interval?: '1d' | '1wk' | '1mo'; } export interface FinancialDataResponse { company: Company; financial_data: FinancialData[]; metadata: { request_id: string; data_points: number; period_type: string; date_range: { start: string; end: string; }; last_updated: string; }; } export interface PriceDataResponse { ticker: string; data: PriceData[]; interval: string; } export interface DatabaseStats { companies: { total: number; with_financial_data: number; with_price_data: number; }; financial_data: { total_records: number; real_data: number; estimated_data: number; date_range: { earliest: string; latest: string; }; by_source: { [key: string]: number; }; }; price_data: { total_records: number; date_range: { earliest: string; latest: string; }; tickers: string[]; }; calculated_metrics: { total_records: number; date_range: { earliest: string; latest: string; }; }; } // News and Social Media Types export interface NewsArticle { title: string; summary?: string; content?: string; url: string; source: string; published_at?: string; author?: string; sentiment_score?: number; relevance_score?: number; image_url?: string; tags: string[]; } export interface SocialPost { title: string; content: string; url: string; platform: string; author: string; published_at?: string; score?: number; comments_count?: number; upvotes?: number; downvotes?: number; sentiment_score?: number; subreddit?: string; } export interface NewsSocialRequest { ticker: string; days_back?: number; max_articles?: number; max_social_posts?: number; include_social?: boolean; } export interface NewsSocialResponse { ticker: string; retrieved_at: string; news: { total_articles: number; sources: { yahoo_finance: number; newsapi: number; }; articles: NewsArticle[]; }; social_media: { total_posts: number; platforms: { reddit: number; }; posts: SocialPost[]; }; summary: { total_items: number; time_range_days: number; oldest_item?: string; newest_item?: string; }; } // API 함수들 export const stockApi = { // 재무 데이터 조회 getFinancialData: async (request: FinancialDataRequest): Promise => { const response = await api.post('/financial/data', request); return response.data; }, // 주가 데이터 조회 getPriceData: async (request: PriceDataRequest): Promise => { const response = await api.post('/price/data', request); return response.data; }, // 데이터베이스 통계 조회 getDatabaseStats: async (): Promise => { const response = await api.get('/database/stats'); return response.data; }, // ETF snapshots list getEtfSnapshots: async (params?: { ticker?: string; start_date?: string; end_date?: string; limit?: number; offset?: number }) => { const qp = new URLSearchParams(); if (params?.ticker) qp.set('ticker', params.ticker); if (params?.start_date) qp.set('start_date', params.start_date); if (params?.end_date) qp.set('end_date', params.end_date); if (params?.limit) qp.set('limit', String(params.limit)); if (params?.offset) qp.set('offset', String(params.offset)); const suffix = qp.toString() ? `?${qp.toString()}` : ''; const { data } = await api.get(`/database/etf/snapshots${suffix}`); return data as { results: Array<{ id: string; ticker: string; snapshot_date: string; source?: string; cik?: string; holdings_count: number }>; count: number }; }, // Single ETF snapshot by id getEtfSnapshotById: async (snapshotId: string) => { const { data } = await api.get(`/database/etf/snapshot/${snapshotId}`); return data as { snapshot: { id: string; ticker: string; snapshot_date: string; source?: string; cik?: string; filing_accession?: string; xml_url?: string }; holdings: Array<{ name?: string; cusip?: string; ticker?: string; shares?: number; value?: number; percentage?: number }>; holdings_count: number }; }, // Financial records list getFinancialRecords: async (params?: { ticker?: string; period_type?: 'quarterly' | 'annual'; start_date?: string; end_date?: string; limit?: number; offset?: number }) => { const qp = new URLSearchParams(); if (params?.ticker) qp.set('ticker', params.ticker); if (params?.period_type) qp.set('period_type', params.period_type); if (params?.start_date) qp.set('start_date', params.start_date); if (params?.end_date) qp.set('end_date', params.end_date); if (params?.limit) qp.set('limit', String(params.limit)); if (params?.offset) qp.set('offset', String(params.offset)); const suffix = qp.toString() ? `?${qp.toString()}` : ''; const { data } = await api.get(`/database/financial/records${suffix}`); return data as { results: Array<{ ticker: string; period_date: string; period_type: string; data_source: string; is_estimated: boolean; revenue?: number; net_income?: number }>; count: number }; }, // 사용 가능한 종목 목록 조회 getAvailableTickers: async (): Promise => { const response = await api.get('/tickers'); return response.data.tickers || []; }, // 회사 정보 조회 getCompanyInfo: async (ticker: string): Promise => { const response = await api.get(`/company/${ticker}`); return response.data; }, // API 상태 확인 getHealthCheck: async (): Promise<{ status: string; timestamp: string }> => { const response = await api.get('/health'); return response.data; }, // 뉴스 & 소셜 미디어 데이터 조회 getNewsSocialData: async (params: NewsSocialRequest): Promise => { const { ticker, days_back = 7, max_articles = 20, max_social_posts = 15, include_social = true } = params; const queryParams = new URLSearchParams({ days_back: days_back.toString(), max_articles: max_articles.toString(), max_social_posts: max_social_posts.toString(), include_social: include_social.toString(), }); const response = await api.get(`/news/${ticker}?${queryParams}`); return response.data; }, // 뉴스만 조회 (더 빠른 응답) getNewsOnly: async (params: Pick) => { const { ticker, days_back = 7, max_articles = 30 } = params; const queryParams = new URLSearchParams({ days_back: days_back.toString(), max_articles: max_articles.toString(), }); const response = await api.get(`/news/${ticker}/news-only?${queryParams}`); return response.data; }, // 소셜 미디어만 조회 getSocialOnly: async (params: Pick) => { const { ticker, days_back = 7, max_social_posts = 20 } = params; const queryParams = new URLSearchParams({ days_back: days_back.toString(), max_social_posts: max_social_posts.toString(), }); const response = await api.get(`/news/${ticker}/social-only?${queryParams}`); return response.data; }, }; // FRED API Types export interface FredUsageStats { daily_limit: number; used_today: number; remaining_today: number; usage_percentage: number; can_make_requests: boolean; daily_stats: Array<{ date: string; total_calls: number; successful_calls: number; total_records: number; success_rate: number; }>; endpoint_stats: Array<{ endpoint: string; call_count: number; }>; cache_stats: { cached_series: number; cached_observations: number; cache_duration_hours: number; }; proxy_info: { mode: string; cache_duration_hours: number; supported_endpoints: string; permanent_storage: boolean; smart_caching: boolean; }; } export interface FredSeriesData { id: string; title: string; units: string; frequency: string; last_updated: string; popularity?: number; notes?: string; cached?: boolean; cached_at?: string; } export interface FredObservation { date: string; value: string; realtime_start?: string; realtime_end?: string; } export interface FredObservationsData { series_id: string; observations: FredObservation[]; count: number; cached?: boolean; cached_at?: string; } export interface FredEndpointsData { series_endpoints: string[]; category_endpoints: string[]; release_endpoints: string[]; source_endpoints: string[]; tag_endpoints: string[]; other_endpoints: string[]; note: string; } export interface FredProxyRequest { endpoint: string; params?: { series_id?: string; category_id?: number; release_id?: number; source_id?: number; search_text?: string; limit?: number; offset?: number; observation_start?: string; observation_end?: string; realtime_start?: string; realtime_end?: string; order_by?: string; sort_order?: 'asc' | 'desc'; frequency?: string; aggregation_method?: string; [key: string]: any; }; force_refresh?: boolean; bypass_limit_check?: boolean; } // FRED API 함수들 export const fredApi = { // FRED API 사용량 통계 조회 getUsageStats: async (days: number = 7, useProxyStats: boolean = true): Promise => { const queryParams = new URLSearchParams({ days: days.toString(), use_proxy_stats: useProxyStats.toString(), }); const response = await api.get(`/fred/stats/usage?${queryParams}`); return response.data.data; }, // FRED 지원 엔드포인트 목록 조회 getSupportedEndpoints: async (): Promise => { const response = await api.get('/fred/endpoints'); return response.data.data; }, // FRED API 프록시 요청 (범용) proxyRequest: async (request: FredProxyRequest): Promise => { const { endpoint, params = {}, force_refresh = false, bypass_limit_check = false } = request; const queryParams = new URLSearchParams(); // 기본 파라미터들 추가 Object.entries(params).forEach(([key, value]) => { if (value !== undefined && value !== null) { queryParams.set(key, value.toString()); } }); // 제어 파라미터들 추가 if (force_refresh) queryParams.set('force_refresh', 'true'); if (bypass_limit_check) queryParams.set('bypass_limit_check', 'true'); const response = await api.get(`/fred/proxy/${endpoint}?${queryParams}`); return response.data; }, // 인기 경제 지표 시리즈 조회 (미리 정의된) getPopularSeries: async (): Promise => { const popularSeriesIds = ['GDP', 'UNRATE', 'FEDFUNDS', 'CPIAUCSL', 'PAYEMS', 'HOUST', 'INDPRO']; const promises = popularSeriesIds.map(async (seriesId) => { try { const queryParams = new URLSearchParams(); queryParams.set('series_id', seriesId); const response = await api.get(`/fred/proxy/series?${queryParams}`); if (response.data.success && response.data.data.seriess && response.data.data.seriess.length > 0) { return response.data.data.seriess[0]; } return null; } catch (error) { console.error(`Failed to fetch series ${seriesId}:`, error); return null; } }); const results = await Promise.all(promises); return results.filter(series => series !== null); }, // 시리즈 검색 searchSeries: async (searchText: string, limit: number = 25): Promise => { const queryParams = new URLSearchParams(); queryParams.set('search_text', searchText); queryParams.set('limit', limit.toString()); const response = await api.get(`/fred/proxy/series/search?${queryParams}`); if (response.data.success && response.data.data.seriess) { return response.data.data.seriess; } return []; }, // 시리즈 관측값 조회 getSeriesObservations: async ( seriesId: string, limit?: number, observationStart?: string, observationEnd?: string ): Promise => { const queryParams = new URLSearchParams(); queryParams.set('series_id', seriesId); if (limit) queryParams.set('limit', limit.toString()); if (observationStart) queryParams.set('observation_start', observationStart); if (observationEnd) queryParams.set('observation_end', observationEnd); queryParams.set('sort_order', 'desc'); const response = await api.get(`/fred/proxy/series/observations?${queryParams}`); if (response.data.success && response.data.data.observations) { return { series_id: seriesId, observations: response.data.data.observations, count: response.data.data.count || response.data.data.observations.length, cached: response.data.metadata?.cached, cached_at: response.data.metadata?.cached_at }; } return { series_id: seriesId, observations: [], count: 0 }; }, }; // Admin API 함수들 (로그 관리) export const adminApi = { getRequestLogs: async (params?: Record) => { const qp = new URLSearchParams(params); const { data } = await api.get(`/admin/requests/logs?${qp}`); return data; }, getErrorLogs: async (params?: Record) => { const qp = new URLSearchParams(params); const { data } = await api.get(`/admin/errors/logs?${qp}`); return data; }, getRequestStats: async (params?: Record) => { const qp = new URLSearchParams(params); const { data } = await api.get(`/admin/requests/stats?${qp}`); return data; }, getErrorStats: async (params?: Record) => { const qp = new URLSearchParams(params); const { data } = await api.get(`/admin/errors/stats?${qp}`); return data; }, deleteRequestLogs: async (params?: Record) => { const qp = new URLSearchParams(params); const { data } = await api.delete(`/admin/requests/logs?${qp}`); return data; }, deleteErrorLogs: async (params?: Record) => { const qp = new URLSearchParams(params); const { data } = await api.delete(`/admin/errors/logs?${qp}`); return data; }, getErrorByRequestId: async (requestId: string) => { const { data } = await api.get(`/admin/errors/by-request/${requestId}`); return data; }, resolveError: async (logId: number, isResolved: boolean, notes?: string) => { const { data } = await api.patch(`/admin/errors/logs/${logId}`, { is_resolved: isResolved, resolution_notes: notes || undefined, }); return data; }, }; // 유틸리티 함수들 export const formatCurrency = (value: number | undefined): string => { if (value === undefined || value === null) return 'N/A'; if (Math.abs(value) >= 1e12) { return `$${(value / 1e12).toFixed(1)}T`; } else if (Math.abs(value) >= 1e9) { return `$${(value / 1e9).toFixed(1)}B`; } else if (Math.abs(value) >= 1e6) { return `$${(value / 1e6).toFixed(1)}M`; } else if (Math.abs(value) >= 1e3) { return `$${(value / 1e3).toFixed(1)}K`; } else { return `$${value.toFixed(2)}`; } }; export const formatNumber = (value: number | undefined): string => { if (value === undefined || value === null) return 'N/A'; if (Math.abs(value) >= 1e9) { return `${(value / 1e9).toFixed(2)}B`; } else if (Math.abs(value) >= 1e6) { return `${(value / 1e6).toFixed(2)}M`; } else if (Math.abs(value) >= 1e3) { return `${(value / 1e3).toFixed(2)}K`; } else { return value.toFixed(2); } }; export const formatPercentage = (value: number | undefined): string => { if (value === undefined || value === null) return 'N/A'; return `${(value * 100).toFixed(2)}%`; }; export const formatDate = (date: string, format?: string): string => { const dateObj = new Date(date); if (format === 'MM/dd') { return dateObj.toLocaleDateString('en-US', { month: '2-digit', day: '2-digit', }); } if (format === 'MM/yy') { return dateObj.toLocaleDateString('en-US', { month: '2-digit', year: '2-digit', }); } return dateObj.toLocaleDateString('ko-KR', { year: 'numeric', month: '2-digit', day: '2-digit', }); };