import { io, Socket } from 'socket.io-client'; // ======================== 서버 설정 ======================== const SERVER_URL = 'https://yapi.0bin.in'; // ======================== 타입 정의 ======================== export enum TimerStatus { STOPPED = "stopped", RUNNING = "running", PAUSED = "paused", FINISHED = "finished" // ✨ 치료완료 상태 추가 } export enum CallStatus { NONE = "none", CALLING = "calling", ACKNOWLEDGED = "acknowledged" } export enum TimerType { PHYSICAL = "physical", LASER = "laser" } export interface SingleTimerState { tablet_id: string; timer_type: TimerType; countdown: number; status: TimerStatus; } export interface DualTimerState { physical: SingleTimerState; laser: SingleTimerState; } export interface TimerState { tablet_id: string; countdown: number; status: TimerStatus; start_time?: number; pause_time?: number; last_tick?: number; completion_time?: number; // ✨ 치료완료 시간 추가 } export interface CallState { tablet_id: string; status: CallStatus; call_time?: number; acknowledged_time?: number; message?: string; } export interface AllTimersState { timers: Record; timestamp: number; } export interface AllCallsState { calls: Record; timestamp: number; } export interface ConnectionInfo { session_id: string; server_time: number; message: string; } export interface RegistrationSuccess { tablet_id?: string; session_id: string; timer_state?: TimerState; call_state?: CallState; timers_state?: AllTimersState; calls_state?: AllCallsState; slides?: SlideData[]; message: string; } export interface TimerStateChanged { tablet_id: string; timer_type: TimerType; countdown: number; status: string; timestamp: number; } export interface TimerTick { tablet_id: string; timer_type: TimerType; countdown: number; status: string; timestamp: number; } export interface ActionSuccess { action: string; tablet_id?: string; stopped_tablets?: string[]; } export interface ErrorResponse { code: string; message: string; } export interface MedicalCallCreated { tablet_id: string; message: string; call_time: number; status: string; } export interface MedicalCallAcknowledged { tablet_id: string; acknowledged_time: number; status: string; } export interface MedicalCallCancelled { tablet_id: string; cancelled_time: number; status: string; } // 슬라이드 데이터 구조 (백엔드 변경사항 반영) export interface SlideData { id: number; // string에서 int로 변경 title: string; subtitle?: string; image_url: string; // media_url에서 image_url로 변경 duration: number; sequence: number; created_by: string; updated_by: string; created_at: string; updated_at: string; is_active: boolean; } // ======================== Socket 이벤트 리스너 인터페이스 ======================== export interface SocketEventListeners { onConnect?: () => void; onDisconnect?: (reason: string) => void; onConnectError?: (error: Error) => void; onConnectionEstablished?: (data: ConnectionInfo) => void; onRegistrationSuccess?: (data: RegistrationSuccess) => void; onTimerStateChanged?: (data: TimerStateChanged) => void; onTimerTick?: (data: TimerTick) => void; onAllTimersState?: (data: AllTimersState) => void; onAllCallsState?: (data: AllCallsState) => void; onMedicalCallCreated?: (data: MedicalCallCreated) => void; onMedicalCallAcknowledged?: (data: MedicalCallAcknowledged) => void; onMedicalCallCancelled?: (data: MedicalCallCancelled) => void; onTimerFinished?: (data: { tablet_id: string; timer_type: TimerType; timestamp: number }) => void; onTreatmentCompleted?: (data: { tablet_id: string; timer_type: TimerType; timestamp: number; completion_time: number; status: string }) => void; // ✨ 치료완료 이벤트 onTimerReset?: (data: { tablet_id: string; timer_type: TimerType; countdown: number; status: string; timestamp: number; reset_source: string }) => void; // ✨ 리셋 이벤트 onActionSuccess?: (data: ActionSuccess) => void; onError?: (data: ErrorResponse) => void; onReconnect?: (attemptNumber: number) => void; onReconnectError?: (error: Error) => void; onReconnectFailed?: () => void; } // ======================== Socket 관리자 클래스 ======================== export class RealtimeSocketManager { private static instance: RealtimeSocketManager; private socket: Socket | null = null; private listeners: SocketEventListeners = {}; private reconnectAttempts = 0; private maxReconnectAttempts = 10; private constructor() {} static getInstance(): RealtimeSocketManager { if (!RealtimeSocketManager.instance) { RealtimeSocketManager.instance = new RealtimeSocketManager(); } return RealtimeSocketManager.instance; } initialize(listeners: SocketEventListeners): void { this.listeners = listeners; } connect(listeners?: SocketEventListeners): void { if (listeners) { this.listeners = listeners; } if (this.socket?.connected) { console.log('🔄 이미 연결된 Socket.IO를 재사용합니다'); return; } console.log(`🚀 Socket.IO 서버 연결 시도: ${SERVER_URL}`); this.socket = io(SERVER_URL, { transports: ['websocket', 'polling'], timeout: 20000, forceNew: false, reconnection: true, reconnectionAttempts: this.maxReconnectAttempts, reconnectionDelay: 1000, reconnectionDelayMax: 5000, maxHttpBufferSize: 1e8 }); this.setupEventListeners(); } private setupEventListeners(): void { if (!this.socket) return; // 연결 이벤트 this.socket.on('connect', () => { console.log('✅ Socket.IO 연결 성공!'); this.reconnectAttempts = 0; this.listeners.onConnect?.(); }); this.socket.on('disconnect', (reason) => { console.log('❌ Socket.IO 연결 끊김:', reason); this.listeners.onDisconnect?.(reason); }); this.socket.on('connect_error', (error) => { console.error('❌ Socket.IO 연결 오류:', error); this.listeners.onConnectError?.(error); }); // 재연결 이벤트 this.socket.on('reconnect', (attemptNumber) => { console.log(`🔄 Socket.IO 재연결 성공! (시도 ${attemptNumber}회)`); this.listeners.onReconnect?.(attemptNumber); }); this.socket.on('reconnect_error', (error) => { console.error('❌ Socket.IO 재연결 실패:', error); this.listeners.onReconnectError?.(error); }); this.socket.on('reconnect_failed', () => { console.error('❌ Socket.IO 재연결 완전 실패'); this.listeners.onReconnectFailed?.(); }); // 서버 응답 이벤트 this.socket.on('connection_established', (data: ConnectionInfo) => { console.log('🤝 서버와 핸드셰이크 완료:', data); this.listeners.onConnectionEstablished?.(data); }); this.socket.on('registration_success', (data: RegistrationSuccess) => { console.log('✅ 등록 성공:', data); this.listeners.onRegistrationSuccess?.(data); }); // 타이머 이벤트 this.socket.on('timer_state_changed', (data: TimerStateChanged) => { console.log('🔄 타이머 상태 변경:', data); this.listeners.onTimerStateChanged?.(data); }); this.socket.on('timer_tick', (data: TimerTick) => { this.listeners.onTimerTick?.(data); }); this.socket.on('all_timers_state', (data: AllTimersState) => { console.log('📊 모든 타이머 상태:', data); this.listeners.onAllTimersState?.(data); }); this.socket.on('timer_finished', (data: { tablet_id: string; timestamp: number }) => { console.log('🏁 타이머 완료:', data); this.listeners.onTimerFinished?.(data); }); // ✨ 새로운 치료완료 이벤트 this.socket.on('treatment_completed', (data: { tablet_id: string; timestamp: number; completion_time: number; status: string }) => { console.log('🎉 치료 완료:', data); this.listeners.onTreatmentCompleted?.(data); }); // ✨ 새로운 타이머 리셋 이벤트 this.socket.on('timer_reset', (data: { tablet_id: string; countdown: number; status: string; timestamp: number; reset_source: string }) => { console.log('🔄 타이머 리셋:', data); this.listeners.onTimerReset?.(data); }); // 의료진 호출 이벤트 this.socket.on('medical_call_created', (data: MedicalCallCreated) => { console.log('📞 의료진 호출 생성:', data); this.listeners.onMedicalCallCreated?.(data); }); this.socket.on('medical_call_acknowledged', (data: MedicalCallAcknowledged) => { console.log('✅ 의료진 호출 인지:', data); this.listeners.onMedicalCallAcknowledged?.(data); }); this.socket.on('medical_call_cancelled', (data: MedicalCallCancelled) => { console.log('❌ 의료진 호출 취소:', data); this.listeners.onMedicalCallCancelled?.(data); }); this.socket.on('all_calls_state', (data: AllCallsState) => { console.log('📞 모든 호출 상태:', data); this.listeners.onAllCallsState?.(data); }); // 성공/오류 이벤트 this.socket.on('action_success', (data: ActionSuccess) => { console.log('✅ 액션 성공:', data); this.listeners.onActionSuccess?.(data); }); this.socket.on('error', (data: ErrorResponse) => { console.error('❌ 서버 오류:', data); this.listeners.onError?.(data); }); } // ======================== 등록 메서드 ======================== registerTablet(tabletId: string): boolean { if (!this.socket?.connected) { console.error('❌ Socket이 연결되지 않았습니다'); return false; } console.log(`📱 태블릿 등록 요청: ${tabletId}`); this.socket.emit('register_tablet', { tablet_id: tabletId }); return true; } registerControl(): boolean { if (!this.socket?.connected) { console.error('❌ Socket이 연결되지 않았습니다'); return false; } console.log('🎛️ 관제 등록 요청'); this.socket.emit('register_control'); return true; } // ======================== 타이머 제어 메서드 ======================== startTimer(tabletId: string, timerType: 'physical' | 'laser' = 'physical'): boolean { if (!this.socket?.connected) { console.error('❌ Socket이 연결되지 않았습니다'); return false; } console.log(`▶️ 타이머 시작 요청: ${tabletId} (${timerType})`); this.socket.emit('start_timer', { tablet_id: tabletId, timer_type: timerType }); return true; } pauseTimer(tabletId: string, timerType: 'physical' | 'laser' = 'physical'): boolean { if (!this.socket?.connected) { console.error('❌ Socket이 연결되지 않았습니다'); return false; } console.log(`⏸️ 타이머 일시정지 요청: ${tabletId} (${timerType})`); this.socket.emit('pause_timer', { tablet_id: tabletId, timer_type: timerType }); return true; } resumeTimer(tabletId: string, timerType: 'physical' | 'laser' = 'physical'): boolean { if (!this.socket?.connected) { console.error('❌ Socket이 연결되지 않았습니다'); return false; } console.log(`▶️ 타이머 재개 요청: ${tabletId} (${timerType})`); this.socket.emit('resume_timer', { tablet_id: tabletId, timer_type: timerType }); return true; } stopTimer(tabletId: string, timerType: 'physical' | 'laser' = 'physical'): boolean { if (!this.socket?.connected) { console.error('❌ Socket이 연결되지 않았습니다'); return false; } console.log(`⏹️ 타이머 정지 요청: ${tabletId} (${timerType})`); this.socket.emit('stop_timer', { tablet_id: tabletId, timer_type: timerType }); return true; } stopAllTimers(): boolean { if (!this.socket?.connected) { console.error('❌ Socket이 연결되지 않았습니다'); return false; } console.log('⏹️ 모든 타이머 정지 요청'); this.socket.emit('stop_all_timers'); return true; } getAllTimersState(): boolean { if (!this.socket?.connected) { console.error('❌ Socket이 연결되지 않았습니다'); return false; } console.log('📊 모든 타이머 상태 요청'); this.socket.emit('get_all_timers'); return true; } // ======================== 의료진 호출 메서드 ======================== createMedicalCall(tabletId: string, message: string = '의료진 호출'): boolean { if (!this.socket?.connected) { console.error('❌ Socket이 연결되지 않았습니다'); return false; } console.log(`📞 의료진 호출 요청: ${tabletId} - ${message}`); this.socket.emit('create_medical_call', { tablet_id: tabletId, message }); return true; } acknowledgeMedicalCall(tabletId: string): boolean { if (!this.socket?.connected) { console.error('❌ Socket이 연결되지 않았습니다'); return false; } console.log(`✅ 의료진 호출 인지: ${tabletId}`); this.socket.emit('acknowledge_medical_call', { tablet_id: tabletId }); return true; } cancelMedicalCall(tabletId: string): boolean { if (!this.socket?.connected) { console.error('❌ Socket이 연결되지 않았습니다'); return false; } console.log(`❌ 의료진 호출 취소: ${tabletId}`); this.socket.emit('cancel_medical_call', { tablet_id: tabletId }); return true; } getAllCallsState(): boolean { if (!this.socket?.connected) { console.error('❌ Socket이 연결되지 않았습니다'); return false; } console.log('📞 모든 호출 상태 요청'); this.socket.emit('get_all_calls'); return true; } // ✨ 새로운 메서드: 타이머 리셋 (관제센터 + 태블릿용) resetTimer(tabletId: string, source: 'control' | 'tablet' = 'control', timerType: 'physical' | 'laser' = 'physical'): boolean { if (!this.socket?.connected) { console.error('❌ Socket이 연결되지 않았습니다'); return false; } console.log(`🔄 타이머 리셋 요청: ${tabletId} (출처: ${source}, 타입: ${timerType})`); this.socket.emit('reset_timer', { tablet_id: tabletId, source: source, timer_type: timerType }); return true; } // ======================== 연결 관리 메서드 ======================== isConnected(): boolean { return this.socket?.connected ?? false; } disconnect(): void { if (this.socket) { console.log('🔌 Socket.IO 연결 해제'); this.socket.disconnect(); this.socket = null; } } reconnect(): void { if (this.socket) { console.log('🔄 Socket.IO 수동 재연결 시도'); this.socket.connect(); } } } export default RealtimeSocketManager;