This commit is contained in:
Dmitry 2026-01-11 10:11:59 +03:00
parent d349c5283e
commit 7ec72bc13e
5 changed files with 189 additions and 27 deletions

View file

@ -110,6 +110,14 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On
console.log(`👥 Clients in room ${payload.roomCode}: ${roomClients?.size || 0}`,
Array.from(roomClients || []));
// ВАЖНО: Отправляем подтверждение подключения клиенту
client.emit('roomJoined', {
roomCode: payload.roomCode,
success: true,
clientsInRoom: roomClients?.size || 0
});
console.log(`✅ Sent roomJoined acknowledgement to client ${client.id}`);
// Получаем полное состояние для отправки присоединившемуся клиенту
const room = (await this.prisma.room.findUnique({
where: { code: payload.roomCode },
@ -1035,4 +1043,42 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On
});
}
}
@SubscribeMessage('joinAsParticipant')
async handleJoinAsParticipant(
client: Socket,
payload: {
roomId: string;
roomCode: string;
userId: string;
name: string;
role: 'PLAYER' | 'SPECTATOR';
}
) {
try {
console.log(`🎭 Client ${client.id} joining as participant: ${payload.name} (${payload.role})`);
// Используем существующий сервис для создания участника
const participant = await this.roomsService.joinRoom(
payload.roomId,
payload.userId,
payload.name,
payload.role
);
console.log(`✅ Participant created: ${participant.id}, broadcasting to room ${payload.roomCode}`);
// broadcastFullState уже вызван внутри roomsService.joinRoom
// Отправляем подтверждение клиенту
client.emit('participantJoined', {
success: true,
participantId: participant.id
});
} catch (error) {
console.error('❌ Error joining as participant:', error);
client.emit('error', {
message: error.message || 'Failed to join as participant'
});
}
}
}

View file

@ -161,28 +161,31 @@ export const useRoom = (roomCode, onGameStarted = null, password = null) => {
const joinRoom = useCallback(async (roomId, userId, name, role = 'PLAYER') => {
try {
// ВАЖНО: Сначала подключаемся к WebSocket комнате
// Это гарантирует, что мы получим broadcast от backend после создания участника
if (roomCode) {
console.log('🚀 Starting join process...');
// Шаг 1: Подключаемся к WebSocket комнате и ЖДЕМ подтверждения
socketService.connect();
socketService.joinRoom(roomCode, userId);
// Даем время на установку WebSocket соединения
await new Promise(resolve => setTimeout(resolve, 100));
}
console.log('⏳ Waiting for WebSocket room join confirmation...');
const joinAck = await socketService.joinRoomWithAck(roomCode, userId);
console.log('✅ WebSocket connected to room:', joinAck);
// Теперь вызываем REST API для создания участника
const response = await roomsApi.join(roomId, userId, name, role);
// Шаг 2: Теперь ГАРАНТИРОВАННО подключены, создаем участника через WebSocket
console.log('⏳ Creating participant in database...');
const participantAck = await socketService.joinAsParticipant(
roomId,
roomCode,
userId,
name,
role
);
console.log('✅ Participant created:', participantAck);
// После успешного присоединения запрашиваем полное состояние через WebSocket
// (дополнительная гарантия получения актуального состояния)
if (roomCode) {
setTimeout(() => {
socketService.emit('requestFullState', { roomCode });
}, 50);
}
// broadcastFullState уже вызван backend, события gameStateUpdated придут автоматически
// Все клиенты (включая хоста) получат обновление
return response.data;
return { success: true, participantId: participantAck.participantId };
} catch (err) {
console.error('❌ Join error:', err);
setError(err.message);
throw err;
}

View file

@ -1,11 +1,12 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
import { useRoom } from '../hooks/useRoom';
import NameInputModal from '../components/NameInputModal';
const CreateRoom = () => {
const navigate = useNavigate();
const { user } = useAuth();
const { user, loading: authLoading, loginAnonymous } = useAuth();
const { createRoom, loading: roomLoading } = useRoom();
const [settings, setSettings] = useState({
@ -13,11 +14,32 @@ const CreateRoom = () => {
allowSpectators: true,
password: '',
});
const [isNameModalOpen, setIsNameModalOpen] = useState(false);
// Проверка авторизации и показ модального окна для ввода имени
// Только если загрузка завершена и пользователя нет
useEffect(() => {
if (!authLoading && !user) {
setIsNameModalOpen(true);
} else if (user) {
setIsNameModalOpen(false);
}
}, [authLoading, user]);
// Обработка ввода имени и авторизация
const handleNameSubmit = async (name) => {
try {
await loginAnonymous(name);
setIsNameModalOpen(false);
} catch (error) {
console.error('Login error:', error);
alert('Ошибка при авторизации. Попробуйте еще раз.');
}
};
const handleCreateRoom = async () => {
if (!user) {
alert('Пожалуйста, задайте имя на главном экране');
navigate('/');
setIsNameModalOpen(true);
return;
}
@ -92,7 +114,7 @@ const CreateRoom = () => {
<div className="button-group">
<button
onClick={handleCreateRoom}
disabled={roomLoading || !user}
disabled={roomLoading}
className="primary"
>
{roomLoading ? 'Создание...' : 'Создать комнату'}
@ -100,6 +122,12 @@ const CreateRoom = () => {
<button onClick={() => navigate('/')}>Назад</button>
</div>
</div>
<NameInputModal
isOpen={isNameModalOpen}
onSubmit={handleNameSubmit}
onCancel={() => navigate('/')}
/>
</div>
);
};

View file

@ -7,6 +7,7 @@ import { questionsApi } from '../services/api';
import QRCode from 'qrcode';
import socketService from '../services/socket';
import QRModal from '../components/QRModal';
import NameInputModal from '../components/NameInputModal';
import PasswordModal from '../components/PasswordModal';
import RoleSelectionModal from '../components/RoleSelectionModal';
import GameManagementModal from '../components/GameManagementModal';
@ -14,7 +15,7 @@ import GameManagementModal from '../components/GameManagementModal';
const RoomPage = () => {
const { roomCode } = useParams();
const navigate = useNavigate();
const { user, loading: authLoading } = useAuth();
const { user, loading: authLoading, loginAnonymous } = useAuth();
const { changeTheme } = useTheme();
// Храним предыдущий themeId комнаты для отслеживания изменений
@ -40,6 +41,7 @@ const RoomPage = () => {
const [qrCode, setQrCode] = useState('');
const [joined, setJoined] = useState(false);
const [isQRModalOpen, setIsQRModalOpen] = useState(false);
const [isNameModalOpen, setIsNameModalOpen] = useState(false);
const [isPasswordModalOpen, setIsPasswordModalOpen] = useState(false);
const [isRoleSelectionModalOpen, setIsRoleSelectionModalOpen] = useState(false);
const [isQuestionsModalOpen, setIsQuestionsModalOpen] = useState(false);
@ -87,6 +89,16 @@ const RoomPage = () => {
}
}, [requiresPassword, isPasswordModalOpen, loading]);
// Проверка имени: показываем модальное окно для ввода имени
// Только если загрузка завершена, пользователя нет, комната загружена и пароль не требуется
useEffect(() => {
if (!authLoading && !user && room && !loading && !requiresPassword) {
setIsNameModalOpen(true);
} else if (user) {
setIsNameModalOpen(false);
}
}, [authLoading, user, room, loading, requiresPassword]);
// Обработка ввода пароля
const handlePasswordSubmit = async (enteredPassword) => {
try {
@ -104,6 +116,17 @@ const RoomPage = () => {
}
};
// Обработка ввода имени и авторизация
const handleNameSubmit = async (name) => {
try {
await loginAnonymous(name);
setIsNameModalOpen(false);
} catch (error) {
console.error('Login error:', error);
alert('Ошибка при авторизации. Попробуйте еще раз.');
}
};
// Единая логика присоединения к комнате
useEffect(() => {
const handleJoin = async () => {
@ -112,10 +135,8 @@ const RoomPage = () => {
return;
}
// Перенаправляем на главный экран, если нет пользователя
// Если пользователя нет - модальное окно имени покажется через другой useEffect
if (!user) {
alert('Пожалуйста, задайте имя на главном экране');
navigate('/');
return;
}
@ -429,6 +450,12 @@ const RoomPage = () => {
roomCode={roomCode}
/>
<NameInputModal
isOpen={isNameModalOpen}
onSubmit={handleNameSubmit}
onCancel={() => navigate('/')}
/>
<PasswordModal
isOpen={isPasswordModalOpen}
onSubmit={handlePasswordSubmit}

View file

@ -92,6 +92,64 @@ class SocketService {
this.emit('joinRoom', { roomCode, userId });
}
// Присоединение к WebSocket комнате с ожиданием подтверждения
joinRoomWithAck(roomCode, userId) {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('WebSocket join timeout - room not joined within 5 seconds'));
}, 5000); // 5 секунд максимум
// Слушаем подтверждение один раз
this.socket.once('roomJoined', (data) => {
clearTimeout(timeout);
console.log('✅ WebSocket room joined:', data);
resolve(data);
});
// Отправляем запрос на присоединение
console.log(`📤 Requesting to join WebSocket room: ${roomCode}`);
this.emit('joinRoom', { roomCode, userId });
});
}
// Присоединение как участник (создание записи в БД) через WebSocket
joinAsParticipant(roomId, roomCode, userId, name, role) {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('Participant join timeout - not created within 5 seconds'));
}, 5000);
// Обработчик успешного присоединения
const handleSuccess = (data) => {
clearTimeout(timeout);
this.socket.off('error', handleError); // Убираем обработчик ошибки
console.log('✅ Participant joined successfully:', data);
resolve(data);
};
// Обработчик ошибки
const handleError = (error) => {
clearTimeout(timeout);
this.socket.off('participantJoined', handleSuccess); // Убираем обработчик успеха
console.error('❌ Participant join error:', error);
reject(new Error(error.message || 'Failed to join as participant'));
};
this.socket.once('participantJoined', handleSuccess);
this.socket.once('error', handleError);
// Отправляем запрос
console.log(`📤 Requesting to join as participant: ${name} (${role})`);
this.emit('joinAsParticipant', {
roomId,
roomCode,
userId,
name,
role
});
});
}
startGame(roomId, roomCode, userId) {
this.emit('startGame', { roomId, roomCode, userId });
}