Skip to content

Commit 8fbc9fd

Browse files
authored
๐Ÿ”– release: v0.0.14
2 parents 9d34bfb + 739ebc9 commit 8fbc9fd

File tree

23 files changed

+772
-61
lines changed

23 files changed

+772
-61
lines changed

โ€Žbackend/build.gradleโ€Ž

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ dependencies {
3737

3838
/* DATABASE */
3939
runtimeOnly 'com.mysql:mysql-connector-j'
40+
implementation 'org.flywaydb:flyway-mysql'
41+
implementation 'org.flywaydb:flyway-core'
4042

4143
/* MONITORING */
4244
implementation 'io.micrometer:micrometer-registry-prometheus'

โ€Žbackend/src/main/java/io/f1/backend/BackendApplication.javaโ€Ž

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
import org.springframework.boot.autoconfigure.SpringBootApplication;
77
import org.springframework.boot.context.properties.EnableConfigurationProperties;
88
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
9+
import org.springframework.scheduling.annotation.EnableScheduling;
910

11+
@EnableScheduling
1012
@EnableJpaAuditing
1113
@SpringBootApplication
1214
@EnableConfigurationProperties(OAuthRedirectProperties.class)

โ€Žbackend/src/main/java/io/f1/backend/domain/game/app/ChatService.javaโ€Ž

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
public class ChatService {
2121

2222
private final RoomService roomService;
23-
private final TimerService timerService;
2423
private final MessageSender messageSender;
2524
private final ApplicationEventPublisher eventPublisher;
2625

@@ -41,7 +40,12 @@ public void chat(Long roomId, UserPrincipal userPrincipal, ChatMessage chatMessa
4140

4241
String answer = currentQuestion.getAnswer();
4342

44-
if (answer.equals(chatMessage.message())) {
43+
if (!answer.equals(chatMessage.message())) {
44+
return;
45+
}
46+
47+
// false -> true
48+
if (room.compareAndSetAnsweredFlag(false, true)) {
4549
eventPublisher.publishEvent(
4650
new GameCorrectAnswerEvent(
4751
room, userPrincipal.getUserId(), chatMessage, answer));

โ€Žbackend/src/main/java/io/f1/backend/domain/game/app/GameService.javaโ€Ž

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ public void onCorrectAnswer(GameCorrectAnswerEvent event) {
123123
ofPlayerEvent(chatMessage.nickname(), RoomEventType.CORRECT_ANSWER));
124124

125125
timerService.cancelTimer(room);
126+
room.compareAndSetAnsweredFlag(true, false);
126127

127128
if (!timerService.validateCurrentRound(room)) {
128129
gameEnd(room);
@@ -142,6 +143,12 @@ public void onCorrectAnswer(GameCorrectAnswerEvent event) {
142143
@EventListener
143144
public void onTimeout(GameTimeoutEvent event) {
144145
Room room = event.room();
146+
147+
// false -> true ์—ฌ์•ผ ํ•˜๋Š”๋ฐ ์‹คํŒจํ–ˆ์„ ๋•Œ => ์ด๋ฏธ ์ •๋‹ต ์ฒ˜๋ฆฌ๊ฐ€ ๋œ ๊ฒฝ์šฐ (onCorrectAnswer ๋กœ์ง ์‹คํ–‰ ์ค‘)
148+
if (!room.compareAndSetAnsweredFlag(false, true)) {
149+
return;
150+
}
151+
145152
log.debug(room.getId() + "๋ฒˆ ๋ฐฉ ํƒ€์ž„์•„์›ƒ! ํ˜„์žฌ ๋ผ์šด๋“œ : " + room.getCurrentRound());
146153

147154
String destination = getDestination(room.getId());
@@ -167,6 +174,8 @@ public void onTimeout(GameTimeoutEvent event) {
167174
destination,
168175
MessageType.QUESTION_START,
169176
toQuestionStartResponse(room, CONTINUE_DELAY));
177+
178+
room.compareAndSetAnsweredFlag(true, false);
170179
}
171180

172181
public void gameEnd(Room room) {

โ€Žbackend/src/main/java/io/f1/backend/domain/game/app/RoomService.javaโ€Ž

Lines changed: 37 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import static io.f1.backend.domain.game.mapper.RoomMapper.toRoomSetting;
1010
import static io.f1.backend.domain.game.mapper.RoomMapper.toRoomSettingResponse;
1111
import static io.f1.backend.domain.game.websocket.WebSocketUtils.getDestination;
12+
import static io.f1.backend.domain.game.websocket.WebSocketUtils.getUserDestination;
1213
import static io.f1.backend.domain.quiz.mapper.QuizMapper.toGameStartResponse;
1314
import static io.f1.backend.global.security.util.SecurityUtils.getCurrentUserId;
1415
import static io.f1.backend.global.security.util.SecurityUtils.getCurrentUserNickname;
@@ -93,7 +94,7 @@ public RoomCreateResponse saveRoom(RoomCreateRequest request) {
9394
roomRepository.saveRoom(room);
9495

9596
/* ๋‹ค๋ฅธ ๋ฐฉ ์ ‘์† ์‹œ ๊ธฐ์กด ๋ฐฉ์€ exit ์ฒ˜๋ฆฌ - ํƒญ ๋™์‹œ ๋กœ๊ทธ์ธ ์‹œ (disconnected ๋ฆฌ์Šค๋„ˆ ์ž‘๋™x) */
96-
exitIfInAnotherRoom(room, host.getId());
97+
exitIfInAnotherRoom(room, getCurrentUserPrincipal());
9798

9899
eventPublisher.publishEvent(new RoomCreatedEvent(room, quiz));
99100

@@ -112,7 +113,7 @@ public void enterRoom(RoomValidationRequest request) {
112113
Long userId = getCurrentUserId();
113114

114115
/* ๋‹ค๋ฅธ ๋ฐฉ ์ ‘์† ์‹œ ๊ธฐ์กด ๋ฐฉ์€ exit ์ฒ˜๋ฆฌ - ํƒญ ๋™์‹œ ๋กœ๊ทธ์ธ ์‹œ (disconnected ๋ฆฌ์Šค๋„ˆ ์ž‘๋™x) */
115-
exitIfInAnotherRoom(room, userId);
116+
exitIfInAnotherRoom(room, getCurrentUserPrincipal());
116117

117118
/* reconnect */
118119
if (room.hasPlayer(userId)) {
@@ -137,16 +138,12 @@ public void enterRoom(RoomValidationRequest request) {
137138
}
138139
}
139140

140-
private void exitIfInAnotherRoom(Room room, Long userId) {
141-
142-
Long joinedRoomId = userRoomRepository.getRoomId(userId);
141+
private void exitIfInAnotherRoom(Room room, UserPrincipal userPrincipal) {
142+
Long userId = userPrincipal.getUserId();
143+
Long joinedRoomId = getRoomIdByUserId(userId);
143144

144145
if (joinedRoomId != null && !room.isSameRoom(joinedRoomId)) {
145-
if (room.isPlaying()) {
146-
changeConnectedStatus(userId, ConnectionState.DISCONNECTED);
147-
} else {
148-
exitRoom(joinedRoomId, getCurrentUserPrincipal());
149-
}
146+
disconnectOrExitRoom(joinedRoomId, userPrincipal);
150147
}
151148
}
152149

@@ -161,7 +158,7 @@ public void initializeRoomSocket(Long roomId, UserPrincipal principal) {
161158

162159
/* ์žฌ์—ฐ๊ฒฐ */
163160
if (room.isPlayerInState(userId, ConnectionState.DISCONNECTED)) {
164-
changeConnectedStatus(userId, ConnectionState.CONNECTED);
161+
changeConnectedStatus(roomId, userId, ConnectionState.CONNECTED);
165162
cancelTask(userId);
166163
reconnectSendResponse(roomId, principal);
167164
return;
@@ -187,7 +184,10 @@ public void initializeRoomSocket(Long roomId, UserPrincipal principal) {
187184
userRoomRepository.addUser(player, room);
188185

189186
messageSender.sendPersonal(
190-
getUserDestination(), MessageType.GAME_SETTING, gameSettingResponse, principal);
187+
getUserDestination(),
188+
MessageType.GAME_SETTING,
189+
gameSettingResponse,
190+
principal.getName());
191191

192192
messageSender.sendBroadcast(destination, MessageType.ROOM_SETTING, roomSettingResponse);
193193
messageSender.sendBroadcast(destination, MessageType.PLAYER_LIST, playerListResponse);
@@ -215,7 +215,7 @@ public void exitRoom(Long roomId, UserPrincipal principal) {
215215
getUserDestination(),
216216
MessageType.EXIT_SUCCESS,
217217
new ExitSuccessResponse(true),
218-
principal);
218+
principal.getName());
219219

220220
SystemNoticeResponse systemNoticeResponse =
221221
ofPlayerEvent(removePlayer.nickname, RoomEventType.EXIT);
@@ -260,17 +260,17 @@ public void reconnectSendResponse(Long roomId, UserPrincipal principal) {
260260
MessageType.SYSTEM_NOTICE,
261261
ofPlayerEvent(
262262
principal.getUserNickname(), RoomEventType.RECONNECT_PRIVATE_NOTICE),
263-
principal);
263+
principal.getName());
264264
messageSender.sendPersonal(
265265
userDestination,
266266
MessageType.RANK_UPDATE,
267267
toRankUpdateResponse(room),
268-
principal);
268+
principal.getName());
269269
messageSender.sendPersonal(
270270
userDestination,
271271
MessageType.GAME_START,
272272
toGameStartResponse(room.getQuestions()),
273-
principal);
273+
principal.getName());
274274
} else {
275275
RoomSettingResponse roomSettingResponse = toRoomSettingResponse(room);
276276

@@ -284,33 +284,40 @@ public void reconnectSendResponse(Long roomId, UserPrincipal principal) {
284284
PlayerListResponse playerListResponse = toPlayerListResponse(room);
285285

286286
messageSender.sendPersonal(
287-
userDestination, MessageType.ROOM_SETTING, roomSettingResponse, principal);
287+
userDestination,
288+
MessageType.ROOM_SETTING,
289+
roomSettingResponse,
290+
principal.getName());
288291
messageSender.sendPersonal(
289-
userDestination, MessageType.PLAYER_LIST, playerListResponse, principal);
292+
userDestination,
293+
MessageType.PLAYER_LIST,
294+
playerListResponse,
295+
principal.getName());
290296
messageSender.sendPersonal(
291-
userDestination, MessageType.GAME_SETTING, gameSettingResponse, principal);
297+
userDestination,
298+
MessageType.GAME_SETTING,
299+
gameSettingResponse,
300+
principal.getName());
292301
}
293302
}
294303

295-
public Long changeConnectedStatus(Long userId, ConnectionState newState) {
296-
Long roomId = userRoomRepository.getRoomId(userId);
304+
public void changeConnectedStatus(Long roomId, Long userId, ConnectionState newState) {
297305
Room room = findRoom(roomId);
298-
299306
room.updatePlayerConnectionState(userId, newState);
300-
301-
return roomId;
302307
}
303308

304309
public void cancelTask(Long userId) {
305310
disconnectTasks.cancelDisconnectTask(userId);
306311
}
307312

308-
public void exitIfNotPlaying(Long roomId, UserPrincipal principal) {
313+
public void disconnectOrExitRoom(Long roomId, UserPrincipal principal) {
309314
Room room = findRoom(roomId);
310315
if (room.isPlaying()) {
316+
changeConnectedStatus(
317+
room.getId(), principal.getUserId(), ConnectionState.DISCONNECTED);
311318
removeUserRepository(principal.getUserId(), roomId);
312319
} else {
313-
exitRoom(roomId, principal);
320+
exitRoom(room.getId(), principal);
314321
}
315322
}
316323

@@ -349,10 +356,6 @@ private void changeHost(Room room, Player host) {
349356
nextHost.orElseThrow(() -> new CustomException(RoomErrorCode.PLAYER_NOT_FOUND)));
350357
}
351358

352-
private String getUserDestination() {
353-
return "/queue";
354-
}
355-
356359
public void exitRoomForDisconnectedPlayer(Long roomId, Player player) {
357360

358361
Object lock = roomLocks.computeIfAbsent(roomId, k -> new Object());
@@ -417,4 +420,8 @@ public void removeUserRepository(Long userId, Long roomId) {
417420
public boolean isUserInAnyRoom(Long userId) {
418421
return userRoomRepository.isUserInAnyRoom(userId);
419422
}
423+
424+
public Long getRoomIdByUserId(Long userId) {
425+
return userRoomRepository.getRoomId(userId);
426+
}
420427
}

โ€Žbackend/src/main/java/io/f1/backend/domain/game/dto/MessageType.javaโ€Ž

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,6 @@ public enum MessageType {
1111
RANK_UPDATE,
1212
QUESTION_START,
1313
GAME_RESULT,
14-
EXIT_SUCCESS
14+
EXIT_SUCCESS,
15+
HEARTBEAT
1516
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package io.f1.backend.domain.game.dto.response;
2+
3+
public record HeartbeatResponse(String direction) {}

โ€Žbackend/src/main/java/io/f1/backend/domain/game/model/Room.javaโ€Ž

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import java.util.concurrent.Executors;
1717
import java.util.concurrent.ScheduledExecutorService;
1818
import java.util.concurrent.ScheduledFuture;
19+
import java.util.concurrent.atomic.AtomicBoolean;
1920

2021
@Getter
2122
public class Room {
@@ -42,6 +43,8 @@ public class Room {
4243

4344
private ScheduledFuture<?> timer;
4445

46+
private final AtomicBoolean answered = new AtomicBoolean(false);
47+
4548
public Room(Long id, RoomSetting roomSetting, GameSetting gameSetting, Player host) {
4649
this.id = id;
4750
this.roomSetting = roomSetting;
@@ -194,4 +197,12 @@ public boolean isPlayerInState(Long userId, ConnectionState state) {
194197
public boolean isPasswordIncorrect(String password) {
195198
return roomSetting.locked() && !roomSetting.password().equals(password);
196199
}
200+
201+
public boolean compareAndSetAnsweredFlag(boolean expected, boolean newValue) {
202+
return answered.compareAndSet(expected, newValue);
203+
}
204+
205+
public AtomicBoolean getAnsweredFlag() {
206+
return this.answered;
207+
}
197208
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package io.f1.backend.domain.game.websocket;
2+
3+
import static io.f1.backend.domain.game.websocket.WebSocketUtils.getUserDestination;
4+
5+
import io.f1.backend.domain.game.app.RoomService;
6+
import io.f1.backend.domain.game.dto.MessageType;
7+
import io.f1.backend.domain.game.dto.response.HeartbeatResponse;
8+
9+
import lombok.RequiredArgsConstructor;
10+
import lombok.extern.slf4j.Slf4j;
11+
12+
import org.springframework.messaging.simp.user.SimpSession;
13+
import org.springframework.messaging.simp.user.SimpUser;
14+
import org.springframework.messaging.simp.user.SimpUserRegistry;
15+
import org.springframework.scheduling.annotation.Scheduled;
16+
import org.springframework.stereotype.Component;
17+
18+
import java.util.Map;
19+
import java.util.concurrent.ConcurrentHashMap;
20+
21+
@Slf4j
22+
@Component
23+
@RequiredArgsConstructor
24+
public class HeartbeatMonitor {
25+
26+
private static final String DIRECTION = "serverToClient";
27+
private static final int MAX_MISSED_HEARTBEATS = 3;
28+
private static final long HEARTBEAT_CHECK_INTERVAL_MS = 15000L;
29+
30+
private final Map<String, Integer> missedPongCounter = new ConcurrentHashMap<>();
31+
32+
private final MessageSender messageSender;
33+
private final RoomService roomService;
34+
private final SimpUserRegistry simpUserRegistry;
35+
36+
@Scheduled(fixedDelay = HEARTBEAT_CHECK_INTERVAL_MS)
37+
public void monitorClientHeartbeat() {
38+
/* user ์—†์œผ๋ฉด skip */
39+
if (simpUserRegistry.getUserCount() == 0) {
40+
return;
41+
}
42+
43+
simpUserRegistry
44+
.getUsers()
45+
.forEach(
46+
user ->
47+
user.getSessions()
48+
.forEach(session -> handleSessionHeartbeat(user, session)));
49+
}
50+
51+
private void handleSessionHeartbeat(SimpUser user, SimpSession session) {
52+
String sessionId = session.getId();
53+
54+
/* ping */
55+
messageSender.sendPersonal(
56+
getUserDestination(),
57+
MessageType.HEARTBEAT,
58+
new HeartbeatResponse(DIRECTION),
59+
user.getName());
60+
61+
// todo FE ๊ฐœ๋ฐœ ๋ ๋•Œ๊นŒ์ง€ ์ฃผ์„ ์ฒ˜๋ฆฌ
62+
// missedPongCounter.merge(sessionId, 1, Integer::sum);
63+
// int missedCnt = missedPongCounter.get(sessionId);
64+
//
65+
// /* max_missed_heartbeats ์ด์ƒ pong ์ด ์•ˆ์™”์„๋•Œ - disconnect ์ฒ˜๋ฆฌ */
66+
// if (missedCnt >= MAX_MISSED_HEARTBEATS) {
67+
//
68+
// Principal principal = user.getPrincipal();
69+
//
70+
// if (principal instanceof UsernamePasswordAuthenticationToken token
71+
// && token.getPrincipal() instanceof UserPrincipal userPrincipal) {
72+
//
73+
// Long userId = userPrincipal.getUserId();
74+
// Long roomId = roomService.getRoomIdByUserId(userId);
75+
//
76+
// roomService.disconnectOrExitRoom(roomId, userPrincipal);
77+
// }
78+
// cleanSession(sessionId);
79+
// }
80+
}
81+
82+
public void resetMissedPongCount(String sessionId) {
83+
missedPongCounter.put(sessionId, 0);
84+
}
85+
86+
public void cleanSession(String sessionId) {
87+
missedPongCounter.remove(sessionId);
88+
}
89+
}

โ€Žbackend/src/main/java/io/f1/backend/domain/game/websocket/MessageSender.javaโ€Ž

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import io.f1.backend.domain.game.dto.MessageType;
44
import io.f1.backend.domain.game.dto.response.DefaultWebSocketResponse;
5-
import io.f1.backend.domain.user.dto.UserPrincipal;
65

76
import lombok.RequiredArgsConstructor;
87

@@ -21,8 +20,8 @@ public <T> void sendBroadcast(String destination, MessageType type, T message) {
2120
}
2221

2322
public <T> void sendPersonal(
24-
String destination, MessageType type, T message, UserPrincipal principal) {
23+
String destination, MessageType type, T message, String principalName) {
2524
messagingTemplate.convertAndSendToUser(
26-
principal.getName(), destination, new DefaultWebSocketResponse<>(type, message));
25+
principalName, destination, new DefaultWebSocketResponse<>(type, message));
2726
}
2827
}

0 commit comments

Comments
ย (0)