dev-front/src/utils/realtime-socket.ts

501 lines
15 KiB
TypeScript

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<string, DualTimerState>;
timestamp: number;
}
export interface AllCallsState {
calls: Record<string, CallState>;
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;