배포스크립트
Some checks failed
CI/CD Pipeline / test-backend (push) Has been cancelled
CI/CD Pipeline / test-frontend (push) Has been cancelled
CI/CD Pipeline / build-and-push (push) Has been cancelled

This commit is contained in:
2026-03-28 11:45:37 +09:00
parent 1497fa2207
commit 5f121638a7
3 changed files with 455 additions and 2 deletions

179
frontend/src/lib/api.ts Normal file
View 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