배포스크립트
This commit is contained in:
179
frontend/src/lib/api.ts
Normal file
179
frontend/src/lib/api.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
import axios, { AxiosError, InternalAxiosRequestConfig, AxiosResponse } from 'axios'
|
||||
import { config } from '../config'
|
||||
|
||||
// 재시도 설정
|
||||
interface RetryConfig extends InternalAxiosRequestConfig {
|
||||
retryCount?: number
|
||||
retryMaxCount?: number
|
||||
}
|
||||
|
||||
// 오프라인 상태 추적
|
||||
let isOffline = false
|
||||
|
||||
// 온/오프라인 이벤트 리스너 등록
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('online', () => {
|
||||
isOffline = false
|
||||
console.log('✅ Connection restored')
|
||||
showOfflineIndicator(false)
|
||||
})
|
||||
window.addEventListener('offline', () => {
|
||||
isOffline = true
|
||||
console.log('❌ Connection lost')
|
||||
showOfflineIndicator(true)
|
||||
})
|
||||
}
|
||||
|
||||
// 오프라인 표시기 UI 업데이트
|
||||
function showOfflineIndicator(offline: boolean) {
|
||||
// 오프라인 배너 표시 (필요시 구현)
|
||||
if (offline) {
|
||||
console.warn('⚠️ You are offline. Some features may not work properly.')
|
||||
}
|
||||
}
|
||||
|
||||
// 재시도 가능 여부 판단
|
||||
function isRetryableError(error: AxiosError): boolean {
|
||||
if (!error.response) {
|
||||
// 네트워크 오류 (재시도 가능)
|
||||
return true
|
||||
}
|
||||
|
||||
const status = error.response.status
|
||||
// 429 (Too Many Requests), 500+, 503 (Service Unavailable) 재시도 가능
|
||||
return status === 429 || status >= 500
|
||||
}
|
||||
|
||||
// 지수 백오프 계산
|
||||
function getBackoffDelay(retryCount: number): number {
|
||||
// 1000ms, 2000ms, 4000ms (3회 재시도)
|
||||
return Math.min(1000 * Math.pow(2, retryCount), 8000)
|
||||
}
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: config.apiUrl,
|
||||
timeout: 15000, // 기본 타임아웃 15초
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
// 요청 인터셉터 - 토큰 추가 및 재시도 카운트 초기화
|
||||
api.interceptors.request.use(
|
||||
(config: RetryConfig) => {
|
||||
// API 키가 있으면 X-API-Key 헤더에 추가
|
||||
const apiKey = localStorage.getItem('api_key')
|
||||
if (apiKey && config.headers) {
|
||||
config.headers['X-API-Key'] = apiKey
|
||||
}
|
||||
|
||||
// 재시도 카운트 초기화 (첫 요청인 경우)
|
||||
if (!config.retryCount) {
|
||||
config.retryCount = 0
|
||||
config.retryMaxCount = 3
|
||||
}
|
||||
|
||||
return config
|
||||
},
|
||||
(error: AxiosError) => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 응답 인터셉터 - 글로벌 에러 처리 및 재시도 로직
|
||||
api.interceptors.response.use(
|
||||
(response: AxiosResponse) => response,
|
||||
async (error: AxiosError) => {
|
||||
const config = error.config as RetryConfig
|
||||
|
||||
// 재시도 설정이 없으면 기본값 설정
|
||||
if (!config) {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
if (!config.retryCount) {
|
||||
config.retryCount = 0
|
||||
config.retryMaxCount = 3
|
||||
}
|
||||
|
||||
// 재시도 가능 여부 확인
|
||||
const maxCount = config.retryMaxCount ?? 3
|
||||
const shouldRetry =
|
||||
isRetryableError(error) &&
|
||||
config.retryCount < maxCount
|
||||
|
||||
if (shouldRetry) {
|
||||
config.retryCount += 1
|
||||
const delay = getBackoffDelay(config.retryCount - 1)
|
||||
console.log(
|
||||
`🔄 Retrying request (${config.retryCount}/${config.retryMaxCount}) ` +
|
||||
`after ${delay}ms: ${config.method?.toUpperCase()} ${config.url}`
|
||||
)
|
||||
|
||||
// 지수 백오프로 재시도
|
||||
await new Promise((resolve) => setTimeout(resolve, delay))
|
||||
return api(config)
|
||||
}
|
||||
|
||||
// 에러 상태코드별 처리
|
||||
if (error.response) {
|
||||
const status = error.response.status
|
||||
const message = (error.response.data as any)?.message || error.message
|
||||
|
||||
switch (status) {
|
||||
case 401:
|
||||
// 인증 실패 - API 키 제거 후 새로고침 (로그인 화면으로)
|
||||
console.warn('Authentication failed:', config.url)
|
||||
localStorage.removeItem('api_key')
|
||||
window.location.reload()
|
||||
break
|
||||
|
||||
case 403:
|
||||
// 권한 부족
|
||||
console.error('🚫 Permission denied:', message)
|
||||
// 필요시 권한 오류 토스트 표시
|
||||
break
|
||||
|
||||
case 429:
|
||||
// 요청 제한
|
||||
console.warn('⏱️ Rate limited. Please try again later.')
|
||||
const retryAfter = error.response.headers['retry-after']
|
||||
if (retryAfter) {
|
||||
console.log(`Retry after: ${retryAfter}s`)
|
||||
}
|
||||
break
|
||||
|
||||
case 500:
|
||||
case 502:
|
||||
case 503:
|
||||
case 504:
|
||||
// 서버 오류 (재시도 완료됨)
|
||||
console.error(`❌ Server error (${status}): ${message}`)
|
||||
break
|
||||
|
||||
default:
|
||||
console.error(`❌ Error (${status}): ${message}`)
|
||||
}
|
||||
} else if (error.request) {
|
||||
// 요청은 보내졌지만 응답을 받지 못함 (네트워크 오류)
|
||||
if (isOffline) {
|
||||
console.error('❌ No response from server. You appear to be offline.')
|
||||
} else {
|
||||
console.error('❌ No response from server. Network error or server is unreachable.')
|
||||
}
|
||||
} else {
|
||||
// 요청 설정 중 오류
|
||||
console.error('❌ Error setting up request:', error.message)
|
||||
}
|
||||
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export const getUsStockPrice = (ticker: string) =>
|
||||
api.get(`/market-data/overseas/price/${ticker}`)
|
||||
|
||||
export const getUsStockChart = (ticker: string, period: string = 'D') =>
|
||||
api.get(`/market-data/overseas/chart/${ticker}`, { params: { period } })
|
||||
|
||||
export default api
|
||||
Reference in New Issue
Block a user