import axios from 'axios'; import { getApiBaseUrl } from './apiConfig'; // API 베이스 URL (통합 설정 사용) const API_BASE_URL = getApiBaseUrl(); const alertApi = axios.create({ baseURL: API_BASE_URL, timeout: 60000, // 60초로 증가 (외부 DB 연결 시간 고려) headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, withCredentials: false, }); // ==================== Types ==================== export type IndicatorType = | 'VOLUME' | 'MACD' | 'STOCHASTIC' | 'STOCHASTIC_K' | 'STOCHASTIC_D' | 'RSI' | 'ADX' | 'CCI' | 'TRIX' | 'DISPARITY' | 'NEW_PSYCHOLOGICAL' | 'INVEST_PSYCHOLOGICAL' | 'WILLIAMS_R' | 'BWI' | 'OBV' | 'VOLUME_OSC' | 'VR' | 'DMI'; export type ConditionType = | 'CROSS_UP' | 'CROSS_DOWN' | 'ABOVE' | 'BELOW' | 'EQUAL' | 'BETWEEN' | 'ZERO_CROSS_UP' | 'ZERO_CROSS_DOWN' | 'SIGNAL_CROSS_UP' | 'SIGNAL_CROSS_DOWN' | 'HISTOGRAM_POSITIVE' | 'HISTOGRAM_NEGATIVE' | 'HISTOGRAM_INCREASE' | 'HISTOGRAM_DECREASE' | 'K_CROSS_D_UP' | 'K_CROSS_D_DOWN' | 'VOLUME_SPIKE' | 'VOLUME_ABOVE_AVG' | 'VOLUME_BELOW_AVG' | 'INCREASE' | 'DECREASE' | 'GOLDEN_CROSS' | 'DEAD_CROSS'; export interface AlertConditionDto { id?: number; cryptoAlertId?: number; indicatorType: IndicatorType; conditionType: ConditionType; targetValue?: number; secondaryValue?: number; period?: number; comparePeriod?: number; enabled?: boolean; orderIndex?: number; description?: string; logicalOperator?: 'AND' | 'OR'; } export interface CryptoAlertDto { id?: number; symbol: string; koreanName: string; englishName?: string; enabled?: boolean; userId?: number; conditions?: AlertConditionDto[]; // 하위 호환성 createdAt?: string; updatedAt?: string; } export interface AlertNotificationDto { id?: number; cryptoAlertId?: number; symbol?: string; koreanName?: string; message: string; conditionDescription?: string; isRead?: boolean; triggeredAt?: string; notifiedAt?: string; userId?: number; /** 조건 충족한 봉 시각(백엔드 LocalDateTime → ISO 또는 배열) */ candleTime?: string; /** 알림 스케줄/조건의 캔들 간격 (예: 3m, 1h, 1d) */ timeInterval?: string; } export interface IndicatorDataDto { time?: string; open?: number; high?: number; low?: number; price?: number; ma10?: number; ma20?: number; ma60?: number; ma120?: number; rsi?: number; macdLine?: number; signalLine?: number; histogram?: number; stochK?: number; stochD?: number; cci?: number; adx?: number; plusDI?: number; minusDI?: number; williamsR?: number; newPsy?: number; investPsy?: number; bwi?: number; obv?: number; obvSignal?: number; // OBV Signal Line (이동평균) volumeOsc?: number; vr?: number; trix?: number; trixSignal?: number; // TRIX Signal Line ichimokuTenkan?: number; ichimokuKijun?: number; ichimokuSenkouA?: number; ichimokuSenkouB?: number; ichimokuChikou?: number; disparityMid?: number; volume?: number; } export interface AlertNotificationDetailDto extends AlertNotificationDto { englishName?: string; indicatorData?: IndicatorDataDto; /** 알림 발생 시점 전후 지표 시계열 (조건 관련 차트 렌더링용) */ indicatorDataSeries?: IndicatorDataDto[]; satisfiedConditions?: AlertConditionDto[]; currentPrice?: number; changeRate?: number; volume?: number; /** 백엔드: 알림 발생 봉 기준 OHLCV 스냅샷 */ snapshotOpen?: number; snapshotHigh?: number; snapshotLow?: number; snapshotClose?: number; snapshotVolume?: number; } export interface AlertScheduleConfigDto { id?: number; userId?: number; cronExpression?: string; timeInterval?: string; // 시간봉 간격 (1m, 3m, 5m, 10m, 15m, 30m, 1h, 4h, 1d, 1w) /** 전략 조건 평가 주기(초) 3~30, 스케줄러 실행 간격 */ strategyCheckIntervalSeconds?: number; enabled?: boolean; lastRunTime?: string; nextRunTime?: string; createdAt?: string; updatedAt?: string; } // ==================== CryptoAlert APIs ==================== /** * 전체 알림 목록 조회 */ export const getAllAlerts = async (): Promise => { const response = await alertApi.get('/alerts'); return response.data; }; /** * 활성화된 알림 목록 조회 */ export const getEnabledAlerts = async (): Promise => { const response = await alertApi.get('/alerts/enabled'); return response.data; }; /** * 알림 상세 조회 */ export const getAlert = async (alertId: number): Promise => { const response = await alertApi.get(`/alerts/${alertId}`); return response.data; }; /** * 심볼로 알림 조회 */ export const getAlertBySymbol = async (symbol: string): Promise => { try { const response = await alertApi.get(`/alerts/symbol/${symbol}`); return response.data; } catch (error: any) { if (error.response?.status === 404) { return null; } throw error; } }; /** * 알림 생성 */ export const createAlert = async (dto: CryptoAlertDto): Promise => { const response = await alertApi.post('/alerts', dto); return response.data; }; /** * 알림 ON/OFF 토글 */ export const toggleAlert = async (alertId: number, enabled: boolean): Promise => { const response = await alertApi.put( `/alerts/${alertId}/toggle`, null, { params: { enabled } } ); return response.data; }; /** * 알림 삭제 */ export const deleteAlert = async (alertId: number): Promise => { await alertApi.delete(`/alerts/${alertId}`); }; // ==================== AlertCondition APIs ==================== /** * 전역 알림 조건 목록 조회 */ export const getGlobalConditions = async (): Promise => { const response = await alertApi.get('/alerts/conditions/global'); return response.data; }; /** * 알림 조건 목록 조회 (개별 종목용 - 하위 호환) */ export const getConditions = async (alertId: number): Promise => { const response = await alertApi.get(`/alerts/${alertId}/conditions`); return response.data; }; /** * 전역 조건 추가 */ export const addGlobalCondition = async (dto: AlertConditionDto): Promise => { const response = await alertApi.post('/alerts/conditions/global', dto); return response.data; }; /** * 조건 추가 (개별 종목용 - 하위 호환) */ export const addCondition = async ( alertId: number, dto: AlertConditionDto ): Promise => { const response = await alertApi.post( `/alerts/${alertId}/conditions`, dto ); return response.data; }; /** * 조건 수정 */ export const updateCondition = async ( conditionId: number, dto: AlertConditionDto ): Promise => { const response = await alertApi.put( `/alerts/conditions/${conditionId}`, dto ); return response.data; }; /** * 조건 삭제 */ export const deleteCondition = async (conditionId: number): Promise => { await alertApi.delete(`/alerts/conditions/${conditionId}`); }; /** * 조건 활성화/비활성화 */ export const toggleCondition = async ( conditionId: number, enabled: boolean ): Promise => { const response = await alertApi.put( `/alerts/conditions/${conditionId}/toggle`, null, { params: { enabled } } ); return response.data; }; // ==================== AlertNotification APIs ==================== /** * 미읽음 알림 조회 */ export const getUnreadNotifications = async (userId?: number): Promise => { const response = await alertApi.get( '/alerts/notifications/unread', { params: { userId } } ); return response.data; }; /** * 알림 목록 조회 (페이징) */ export const getNotifications = async ( userId?: number, page: number = 0, size: number = 20 ): Promise<{ content: AlertNotificationDto[]; totalPages: number; totalElements: number }> => { const response = await alertApi.get('/alerts/notifications', { params: { userId, page, size }, }); return response.data; }; /** * 알림 읽음 처리 */ export const markAsRead = async (notificationId: number): Promise => { await alertApi.put(`/alerts/notifications/${notificationId}/read`); }; /** * 모든 미읽은 알림 읽음 처리 */ export const markAllAsRead = async (): Promise => { await alertApi.put('/alerts/notifications/read-all'); }; /** * 알림 상세 정보 조회 (분석 데이터 포함) * @param symbol 통합 알림 등 행별 마켓(예: KRW-BTC). 지정 시 해당 종목 캔들·지표로 스냅샷 (알림 봉 시각은 동일). */ export const getNotificationDetail = async ( notificationId: number, options?: { symbol?: string | null } ): Promise => { const sym = options?.symbol?.trim(); const response = await alertApi.get( `/alerts/notifications/${notificationId}/detail`, sym ? { params: { symbol: sym } } : undefined ); return response.data; }; // ==================== AlertScheduleConfig APIs ==================== /** * 스케줄 설정 조회 */ export const getScheduleConfig = async (userId?: number): Promise => { const response = await alertApi.get( '/alerts/schedule', { params: { userId } } ); return response.data; }; /** * 스케줄 설정 저장/업데이트 */ export const saveScheduleConfig = async ( dto: AlertScheduleConfigDto ): Promise => { const response = await alertApi.put('/alerts/schedule', dto); return response.data; }; /** * 알림 삭제 */ export const deleteNotification = async (notificationId: number): Promise => { await alertApi.delete(`/alerts/notifications/${notificationId}`); }; /** * 읽은 알림 모두 삭제 */ export const deleteReadNotifications = async (userId?: number): Promise => { await alertApi.delete('/alerts/notifications/read', { params: { userId }, }); }; // ==================== Scheduler Service APIs ==================== export interface SchedulerStatusDto { running: boolean; status?: string; error?: string; schedulerRunning?: boolean; kafkaConnected?: boolean; } export interface SchedulerStartResultDto { success: boolean; message: string; manualCommand?: string; } /** * 스케줄러 서비스 상태 조회 */ export const getSchedulerStatus = async (): Promise => { const response = await alertApi.get('/scheduler/status'); return response.data; }; /** * 스케줄러 서비스 시작 시도 */ export const startScheduler = async (): Promise => { const response = await alertApi.post('/scheduler/start'); return response.data; }; /** Internal `/internal/alerts/evaluate` 요청 본문 */ export interface AlertEvaluateRequest { symbol: string; strategyRuleId: number; timeInterval: string; liveCandles?: Record[]; useLiveCandleTail?: boolean; } export interface AlertEvaluateResponse { symbol?: string; strategyRuleId?: number; conditionMet: boolean; message?: string; } /** * 알림 전략 조건 평가 (스케줄러와 동일 백엔드 경로) * `liveCandles`가 있으면 WS 버퍼 기준, 없으면 서버 Redis/REST 200봉. */ export const evaluateAlertStrategy = async (body: AlertEvaluateRequest): Promise => { const response = await alertApi.post('/internal/alerts/evaluate', body); return response.data; }; /** * 테스트 알림 생성 (파이프라인 검증용) * Internal API - 첫 번째 종목으로 테스트 알림 생성 */ export const createTestNotification = async (): Promise => { const response = await alertApi.post('/internal/alerts/test'); return response.data; }; /** * 알림 읽음 처리 */ export const markNotificationAsRead = async (notificationId: number): Promise => { await alertApi.put(`/alerts/notifications/${notificationId}/read`); }; export default { getAllAlerts, getEnabledAlerts, getAlert, getAlertBySymbol, createAlert, toggleAlert, deleteAlert, getGlobalConditions, getConditions, addGlobalCondition, addCondition, updateCondition, deleteCondition, toggleCondition, getUnreadNotifications, getNotifications, markAsRead, getScheduleConfig, saveScheduleConfig, deleteNotification, deleteReadNotifications, markNotificationAsRead, };