501 lines
15 KiB
TypeScript
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; |