import { query, queryOne, execute } from './db' // 상수 정의 const INTERVAL_SUCCESS = 5 * 60 * 1000 // 5분 const INTERVAL_FAILURE = 1 * 60 * 1000 // 1분 const REQUEST_TIMEOUT = 10 * 1000 // 10초 // 타입 정의 interface PubnetTarget { id: number name: string url: string is_active: number } interface PubnetStatus { id: number current_index: number check_interval: number is_healthy: number last_target_id: number | null last_checked_at: string | null scheduler_running: number } interface CheckResult { targetId: number targetName: string url: string isSuccess: boolean } class PubnetScheduler { private timer: ReturnType | null = null private isRunning: boolean = false /** * 스케줄러 시작 */ start(): void { if (this.isRunning) { console.log('[PubnetScheduler] Already running') return } this.isRunning = true this.updateSchedulerRunning(1) console.log('[PubnetScheduler] Started') // 즉시 첫 체크 실행 this.runCheck() } /** * 스케줄러 중지 */ stop(): void { if (this.timer) { clearTimeout(this.timer) this.timer = null } this.isRunning = false this.updateSchedulerRunning(0) console.log('[PubnetScheduler] Stopped') } /** * 실행 상태 확인 */ getIsRunning(): boolean { return this.isRunning } /** * 체크 실행 */ private async runCheck(): Promise { if (!this.isRunning) return try { const result = await this.checkCurrentTargets() // 결과에 따라 다음 간격 결정 const nextInterval = result.isSuccess ? INTERVAL_SUCCESS : INTERVAL_FAILURE // 로그 저장 (1개만) await this.saveLog(result) // 상태 업데이트 (인덱스 +1) await this.updateStatus(result, nextInterval) console.log( `[PubnetScheduler] ${result.targetName} (${result.url}) - ` + `${result.isSuccess ? 'SUCCESS' : 'FAILED'} - ` + `Next check in ${nextInterval / 1000}s` ) // 다음 체크 예약 this.timer = setTimeout(() => this.runCheck(), nextInterval) } catch (error) { console.error('[PubnetScheduler] Error:', error) // 에러 발생 시 1분 후 재시도 this.timer = setTimeout(() => this.runCheck(), INTERVAL_FAILURE) } } /** * 현재 타겟 1개 체크 */ private async checkCurrentTargets(): Promise { // 활성화된 타겟 목록 조회 const targets = await query(` SELECT * FROM pubnet_targets WHERE is_active = 1 ORDER BY id ASC `) if (targets.length === 0) { throw new Error('No active targets found') } // 현재 인덱스 조회 const status = await queryOne('SELECT * FROM pubnet_status WHERE id = 1') if (!status) { throw new Error('Status not found') } // 1개 타겟 선택 const idx = status.current_index % targets.length const target = targets[idx] // 단일 타겟 체크 let isSuccess = false try { const controller = new AbortController() const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT) const response = await fetch(target.url, { method: 'HEAD', signal: controller.signal, headers: { 'User-Agent': 'OSOLIT-Monitor/1.0' } }) clearTimeout(timeoutId) isSuccess = response.status === 200 } catch (err: any) { isSuccess = false } return { targetId: target.id, targetName: target.name, url: target.url, isSuccess } } /** * 체크 결과 로그 저장 */ private async saveLog(result: CheckResult): Promise { await execute( `INSERT INTO pubnet_logs (target_id, is_success) VALUES ($1, $2)`, [result.targetId, result.isSuccess ? 1 : 0] ) } /** * 상태 업데이트 (인덱스 +1 순환) */ private async updateStatus(result: CheckResult, nextInterval: number): Promise { // 활성 타겟 수 조회 const countResult = await queryOne<{ cnt: string }>( `SELECT COUNT(*) as cnt FROM pubnet_targets WHERE is_active = 1` ) const totalCount = parseInt(countResult?.cnt || '0') const status = await queryOne<{ current_index: number }>( 'SELECT current_index FROM pubnet_status WHERE id = 1' ) // 인덱스 +1 (순환) const nextIndex = ((status?.current_index || 0) + 1) % totalCount await execute(` UPDATE pubnet_status SET current_index = $1, check_interval = $2, is_healthy = $3, last_target_id = $4, last_checked_at = to_char(NOW(), 'YYYY-MM-DD HH24:MI:SS'), updated_at = to_char(NOW(), 'YYYY-MM-DD HH24:MI:SS') WHERE id = 1 `, [nextIndex, nextInterval, result.isSuccess ? 1 : 0, result.targetId]) } /** * 스케줄러 실행 상태 업데이트 */ private async updateSchedulerRunning(running: number): Promise { await execute(` UPDATE pubnet_status SET scheduler_running = $1, updated_at = to_char(NOW(), 'YYYY-MM-DD HH24:MI:SS') WHERE id = 1 `, [running]) } } // 싱글톤 인스턴스 export const pubnetScheduler = new PubnetScheduler()