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 PrivnetTarget { id: number name: string url: string is_active: number } interface PrivnetStatus { 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 PrivnetScheduler { private timer: ReturnType | null = null private isRunning: boolean = false /** * 스케줄러 시작 */ start(): void { if (this.isRunning) { console.log('[PrivnetScheduler] Already running') return } this.isRunning = true this.updateSchedulerRunning(1) console.log('[PrivnetScheduler] Started') // 즉시 첫 체크 실행 this.runCheck() } /** * 스케줄러 중지 */ stop(): void { if (this.timer) { clearTimeout(this.timer) this.timer = null } this.isRunning = false this.updateSchedulerRunning(0) console.log('[PrivnetScheduler] Stopped') } /** * 실행 상태 확인 */ getIsRunning(): boolean { return this.isRunning } /** * 체크 실행 */ private async runCheck(): Promise { if (!this.isRunning) return try { const result = await this.checkCurrentTarget() // 결과에 따라 다음 간격 결정 const nextInterval = result.isSuccess ? INTERVAL_SUCCESS : INTERVAL_FAILURE // 로그 저장 await this.saveLog(result) // 상태 업데이트 (인덱스 증가) await this.updateStatus(result, nextInterval) console.log( `[PrivnetScheduler] ${result.targetName} (${result.url}) - ` + `${result.isSuccess ? 'SUCCESS' : 'FAILED'} - ` + `Next check in ${nextInterval / 1000}s` ) // 다음 체크 예약 this.timer = setTimeout(() => this.runCheck(), nextInterval) } catch (error) { console.error('[PrivnetScheduler] Error:', error) // 에러 발생 시 1분 후 재시도 this.timer = setTimeout(() => this.runCheck(), INTERVAL_FAILURE) } } /** * 현재 타겟 URL 체크 */ private async checkCurrentTarget(): Promise { // 활성화된 타겟 목록 조회 const targets = await query(` SELECT * FROM privnet_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 privnet_status WHERE id = 1') if (!status) { throw new Error('Status not found') } const currentIndex = status.current_index % targets.length const target = targets[currentIndex] // HTTP 요청 실행 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 privnet_logs (target_id, is_success) VALUES ($1, $2)`, [result.targetId, result.isSuccess ? 1 : 0] ) } /** * 상태 업데이트 (인덱스 순환) */ private async updateStatus(result: CheckResult, nextInterval: number): Promise { // 활성 타겟 수 조회 const countResult = await queryOne<{ cnt: string }>( `SELECT COUNT(*) as cnt FROM privnet_targets WHERE is_active = 1` ) const totalCount = parseInt(countResult?.cnt || '0') const status = await queryOne<{ current_index: number }>( 'SELECT current_index FROM privnet_status WHERE id = 1' ) const nextIndex = ((status?.current_index || 0) + 1) % totalCount await execute(` UPDATE privnet_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 privnet_status SET scheduler_running = $1, updated_at = to_char(NOW(), 'YYYY-MM-DD HH24:MI:SS') WHERE id = 1 `, [running]) } } // 싱글톤 인스턴스 export const privnetScheduler = new PrivnetScheduler()