Skip to content

Commit f00a332

Browse files
committed
feat: 실시간 참여자 목록 동기화 버그 수정 및 보안 설정 업데이트(#47)
1 parent 24c15a0 commit f00a332

File tree

5 files changed

+61
-33
lines changed

5 files changed

+61
-33
lines changed

src/main/java/com/dmu/debug_visual/collab/rest/RoomController.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
@RequiredArgsConstructor
2828
public class RoomController {
2929

30-
// ✨ Controller는 이제 Service에만 의존합니다. 훨씬 깔끔해졌죠!
3130
private final RoomService roomService;
3231

3332
// --- 1. 방 관리 ---

src/main/java/com/dmu/debug_visual/collab/service/RoomService.java

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
import org.springframework.transaction.annotation.Transactional;
2121

2222
import java.util.List;
23+
import java.util.Objects;
24+
import java.util.Set;
2325
import java.util.stream.Collectors;
2426

2527
/**
@@ -32,6 +34,7 @@ public class RoomService {
3234

3335
private final RoomRepository roomRepository;
3436
private final UserRepository userRepository;
37+
private final WebSocketRoomService webSocketRoomService;
3538
private final RoomParticipantRepository roomParticipantRepository;
3639
private final SessionParticipantRepository sessionParticipantRepository;
3740
private final CodeSessionRepository codeSessionRepository;
@@ -254,35 +257,47 @@ public void joinRoom(String roomId, String userId) {
254257
}
255258

256259
/**
257-
* 특정 방의 최신 상태(방 이름, 방장, 참여자 목록)를 조회하여
258-
* 해당 방의 시스템 채널로 브로드캐스팅합니다.
260+
* 특정 방의 최신 '실시간 상태'를 조회하여 시스템 채널로 브로드캐스팅합니다.
259261
* @param roomId 상태를 방송할 방의 ID
260262
*/
261263
@Transactional(readOnly = true)
262264
public void broadcastRoomState(String roomId) {
263265
Room dbRoom = roomRepository.findByRoomId(roomId)
264-
.orElseThrow(() -> new RuntimeException("Room not found during state broadcast: " + roomId));
266+
.orElseThrow(() -> new RuntimeException("Room not found: " + roomId));
265267

268+
// ✨ 1. DB가 아닌, 메모리에서 현재 '실시간 접속자' ID 목록을 가져옵니다.
269+
Set<String> activeUserIds = webSocketRoomService.getActiveParticipants(roomId);
270+
if (activeUserIds == null) {
271+
log.warn("No active participants found in memory for room: {}", roomId);
272+
return;
273+
}
274+
275+
// 2. 방장 정보 DTO 생성
266276
ParticipantInfo ownerInfo = ParticipantInfo.builder()
267277
.userId(dbRoom.getOwner().getUserId())
268278
.userName(dbRoom.getOwner().getName())
269279
.build();
270280

271-
List<ParticipantInfo> participantInfos = dbRoom.getParticipants().stream()
272-
.filter(p -> !p.getUser().getUserId().equals(dbRoom.getOwner().getUserId()))
273-
.map(p -> ParticipantInfo.builder()
274-
.userId(p.getUser().getUserId())
275-
.userName(p.getUser().getName())
281+
// ✨ 3. '실시간 접속자' 중에서 방장을 제외한 나머지 참여자 목록 DTO 생성
282+
List<ParticipantInfo> participantInfos = activeUserIds.stream()
283+
.filter(userId -> !userId.equals(ownerInfo.getUserId()))
284+
.map(userId -> userRepository.findByUserId(userId).orElse(null))
285+
.filter(Objects::nonNull)
286+
.map(user -> ParticipantInfo.builder()
287+
.userId(user.getUserId())
288+
.userName(user.getName())
276289
.build())
277290
.collect(Collectors.toList());
278291

292+
// 4. 최종 업데이트 DTO 생성
279293
RoomStateUpdate roomStateUpdate = RoomStateUpdate.builder()
280294
.roomName(dbRoom.getName())
281295
.owner(ownerInfo)
282296
.participants(participantInfos)
283297
.build();
284298

299+
// 5. 시스템 채널로 브로드캐스팅
285300
messagingTemplate.convertAndSend("/topic/room/" + roomId + "/system", roomStateUpdate);
286-
log.info("Broadcasted room state update for room: {}", roomId);
301+
log.info("Broadcasted real-time state for room: {}. Active users: {}", roomId, activeUserIds.size());
287302
}
288303
}

src/main/java/com/dmu/debug_visual/collab/service/WebSocketRoomService.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,9 @@ public boolean isSessionEmpty(String sessionId) {
7777
// sessionId에 해당하는 참여자 목록이 없거나, 있더라도 비어있으면 true를 반환합니다.
7878
return !sessionParticipants.containsKey(sessionId) || sessionParticipants.get(sessionId).isEmpty();
7979
}
80+
81+
public Set<String> getActiveParticipants(String roomId) {
82+
WebSocketRoom activeRoom = findActiveRoomById(roomId);
83+
return (activeRoom != null) ? activeRoom.getParticipants().keySet() : null;
84+
}
8085
}

src/main/java/com/dmu/debug_visual/config/SecurityConfig.java

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,20 +51,19 @@ public SecurityFilterChain devSecurityFilterChain(HttpSecurity http) throws Exce
5151
.httpBasic(AbstractHttpConfigurer::disable)
5252
.authorizeHttpRequests(auth -> auth
5353
// 1. 누구나 접근 가능한 경로
54-
.requestMatchers("/ws/**").permitAll()
54+
.requestMatchers("/ws/**", "/ws-collab/**").permitAll()
5555
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
5656
.requestMatchers("/api/users/login", "/api/users/signup").permitAll()
5757
.requestMatchers("/api/code/**").permitAll()
5858
.requestMatchers(HttpMethod.GET, "/api/posts/**", "/api/comments/**").permitAll()
59-
.requestMatchers("/ws-collab/**").permitAll()
6059

6160
// 2. USER 권한이 필요한 경로
6261
.requestMatchers("/api/posts/**").hasRole("USER")
6362
.requestMatchers("/api/notifications/**").hasRole("USER")
6463
.requestMatchers("/api/report/**").hasRole("USER")
6564
.requestMatchers("/api/comments/**").hasRole("USER")
6665
.requestMatchers("/api/files/**").hasRole("USER")
67-
.requestMatchers("/api/collab-rooms").hasRole("USER")
66+
.requestMatchers("/api/collab").hasRole("USER")
6867

6968
// 3. 나머지 모든 요청은 인증된 사용자만 접근 가능 (ADMIN 경로 포함)
7069
.anyRequest().authenticated()
@@ -89,12 +88,11 @@ public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws
8988
.httpBasic(AbstractHttpConfigurer::disable)
9089
.authorizeHttpRequests(auth -> auth
9190
// 1. 누구나 접근 가능한 경로
92-
.requestMatchers("/ws/**").permitAll()
91+
.requestMatchers("/ws/**", "/ws-collab/**").permitAll()
9392
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
9493
.requestMatchers("/api/users/login", "/api/users/signup").permitAll()
9594
.requestMatchers("/api/code/**").permitAll()
9695
.requestMatchers(HttpMethod.GET, "/api/posts/**", "/api/comments/**").permitAll()
97-
.requestMatchers("/ws-collab/**").permitAll()
9896

9997
// 2. ADMIN 권한이 필요한 경로
10098
.requestMatchers("/api/admin/**").hasRole("ADMIN")
@@ -105,7 +103,7 @@ public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws
105103
.requestMatchers("/api/report/**").hasRole("USER")
106104
.requestMatchers("/api/comments/**").hasRole("USER")
107105
.requestMatchers("/api/files/**").hasRole("USER")
108-
.requestMatchers("/api/collab-rooms").hasRole("USER")
106+
.requestMatchers("/api/collab").hasRole("USER")
109107

110108

111109
// 4. 나머지 모든 요청은 인증된 사용자만 접근 가능

src/main/java/com/dmu/debug_visual/config/WebSocketEventListener.java

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package com.dmu.debug_visual.config;
22

33
import com.dmu.debug_visual.collab.domain.entity.CodeSession;
4+
import com.dmu.debug_visual.collab.domain.entity.Room;
45
import com.dmu.debug_visual.collab.domain.repository.CodeSessionRepository;
6+
import com.dmu.debug_visual.collab.domain.repository.RoomRepository;
57
import com.dmu.debug_visual.collab.service.RoomService;
68
import com.dmu.debug_visual.collab.service.WebSocketRoomService;
79
import com.dmu.debug_visual.security.CustomUserDetails;
@@ -26,6 +28,7 @@ public class WebSocketEventListener {
2628

2729
private final WebSocketRoomService webSocketRoomService;
2830
private final RoomService roomService;
31+
private final RoomRepository roomRepository;
2932
private final CodeSessionRepository codeSessionRepository;
3033

3134
@EventListener
@@ -40,24 +43,34 @@ public void handleWebSocketSubscribeListener(SessionSubscribeEvent event) {
4043
try {
4144
String roomId = destination.split("/")[3];
4245

46+
// ✨ 1. (핵심 수정!) 메모리에 방이 없으면, DB에서 정보를 가져와 활성화시킵니다.
47+
if (webSocketRoomService.findActiveRoomById(roomId) == null) {
48+
Room dbRoom = roomRepository.findByRoomId(roomId)
49+
.orElseThrow(() -> new RuntimeException("Subscribing to a non-existent room: " + roomId));
50+
webSocketRoomService.activateRoom(roomId, dbRoom.getOwner().getUserId());
51+
log.info("Room {} activated in memory.", roomId);
52+
}
53+
54+
// 2. 이제 안전하게 메모리에 실시간 사용자 추가
4355
Authentication authentication = (Authentication) userPrincipal;
4456
CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();
4557
String userId = userDetails.getUsername();
46-
4758
webSocketRoomService.addParticipant(roomId, userId);
4859

60+
// 3. 퇴장 이벤트를 위해 세션에 정보 저장
4961
Map<String, Object> sessionAttributes = Objects.requireNonNull(headerAccessor.getSessionAttributes());
5062
sessionAttributes.put("roomId", roomId);
5163
sessionAttributes.put("userId", userId);
5264

53-
// 만약 구독 주소에 "/session/"이 포함되어 있다면, 세션 참여자로도 등록합니다.
65+
// ... (세션 ID 저장 로직은 그대로)
5466
if (destination.contains("/session/")) {
5567
String sessionId = destination.split("/")[5];
56-
sessionAttributes.put("sessionId", sessionId); // 퇴장 시 사용하기 위해 세션 ID 저장
68+
sessionAttributes.put("sessionId", sessionId);
5769
webSocketRoomService.addSessionParticipant(sessionId, userId);
5870
log.info("User {} joined session {}", userId, sessionId);
5971
}
6072

73+
// 4. 변경된 상태를 모두에게 방송
6174
roomService.broadcastRoomState(roomId);
6275

6376
} catch (Exception e) {
@@ -80,27 +93,25 @@ public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) {
8093
if (roomId != null && userId != null) {
8194
log.info("[퇴장] 사용자: {}, 방: {}", userId, roomId);
8295

96+
// 1. 메모리에서 방/세션 참여자 모두 제거
8397
webSocketRoomService.removeParticipant(roomId, userId);
84-
85-
// --- 세션 자동 비활성화 로직 ---
8698
if (sessionId != null) {
8799
webSocketRoomService.removeSessionParticipant(sessionId, userId);
100+
}
88101

89-
// 방금 나간 사람이 마지막 참여자였는지 확인
90-
if (webSocketRoomService.isSessionEmpty(sessionId)) {
91-
log.info("Last user left session {}. Deactivating session.", sessionId);
92-
93-
// DB에서 세션을 찾아 상태를 INACTIVE로 변경
94-
codeSessionRepository.findBySessionId(sessionId).ifPresent(session -> {
95-
session.updateStatus(CodeSession.SessionStatus.INACTIVE);
96-
log.info("Session {} status updated to INACTIVE in DB.", sessionId);
97-
});
98-
}
102+
// 2. 세션 자동 비활성화 로직
103+
if (sessionId != null && webSocketRoomService.isSessionEmpty(sessionId)) {
104+
log.info("Last user left session {}. Deactivating session.", sessionId);
105+
codeSessionRepository.findBySessionId(sessionId).ifPresent(session -> {
106+
session.updateStatus(CodeSession.SessionStatus.INACTIVE);
107+
log.info("Session {} status updated to INACTIVE in DB.", sessionId);
108+
});
99109
}
100110

101-
// 변경된 방 상태(참여자 감소)를 모두에게 알림
111+
// 3. 최종적으로 변경된 상태를 모두에게 방송
102112
roomService.broadcastRoomState(roomId);
103113
}
104114
}
105115
}
106-
}
116+
}
117+

0 commit comments

Comments
 (0)