11package com .teamEWSN .gitdeun .mindmap .service ;
22
3+ import com .fasterxml .jackson .core .JsonProcessingException ;
34import com .fasterxml .jackson .databind .ObjectMapper ;
45import com .teamEWSN .gitdeun .common .jwt .CustomUserDetails ;
56import com .teamEWSN .gitdeun .mindmap .dto .MindmapDetailResponseDto ;
67import com .teamEWSN .gitdeun .mindmap .dto .prompt .PromptPreviewResponseDto ;
78import lombok .RequiredArgsConstructor ;
89import lombok .extern .slf4j .Slf4j ;
10+ import org .springframework .data .redis .core .RedisTemplate ;
911import org .springframework .stereotype .Service ;
1012import org .springframework .web .servlet .mvc .method .annotation .SseEmitter ;
1113
1214import java .io .IOException ;
13- import java .util .List ;
14- import java .util .Map ;
15+ import java .util .*;
1516import java .util .concurrent .ConcurrentHashMap ;
1617import java .util .concurrent .CopyOnWriteArrayList ;
18+ import java .util .concurrent .TimeUnit ;
1719import java .util .stream .Collectors ;
1820
1921@ Slf4j
2224public class MindmapSseService {
2325
2426 private final ObjectMapper objectMapper ;
25-
27+ private final RedisTemplate < String , String > redisTemplate ;
2628 private record SseConnection (Long userId , String nickname , String profileImage , SseEmitter emitter ) {}
2729 private final Map <Long , List <SseConnection >> connectionsByMapId = new ConcurrentHashMap <>();
2830
@@ -45,19 +47,27 @@ public SseEmitter createConnection(Long mapId, CustomUserDetails userDetails) {
4547
4648 connectionsByMapId .computeIfAbsent (mapId , k -> new CopyOnWriteArrayList <>()).add (connection );
4749
50+ try {
51+ // Redis Set에 현재 접속자 정보 추가
52+ String redisKey = "mindmap:" + mapId + ":users" ;
53+ String userData = objectMapper .writeValueAsString (
54+ new ConnectedUserDto (userDetails .getId (), userDetails .getNickname (), userDetails .getProfileImage ())
55+ );
56+ redisTemplate .opsForSet ().add (redisKey , userData );
57+
58+ // Redis Set의 만료 시간을 설정하여 비정상 종료된 연결 처리
59+ redisTemplate .expire (redisKey , 1 , TimeUnit .HOURS );
60+
61+ } catch (JsonProcessingException e ) {
62+ log .error ("사용자 정보 직렬화 실패" , e );
63+ }
64+
4865 // 연결 종료 시 정리 (기존 로직 개선)
49- emitter .onCompletion (() -> {
50- removeConnection (mapId , connection );
51- broadcastUserListUpdate (mapId );
52- });
53- emitter .onTimeout (() -> {
54- removeConnection (mapId , connection );
55- broadcastUserListUpdate (mapId );
56- });
66+ emitter .onCompletion (() -> removeConnection (mapId , connection ));
67+ emitter .onTimeout (() -> removeConnection (mapId , connection ));
5768 emitter .onError (throwable -> {
5869 log .error ("SSE 연결 오류 - 마인드맵 ID: {}, 사용자 ID: {}" , mapId , userDetails .getId (), throwable );
5970 removeConnection (mapId , connection );
60- broadcastUserListUpdate (mapId );
6171 });
6272
6373 // 연결 확인용 초기 메시지
@@ -158,14 +168,20 @@ private void sendToEmitter(SseEmitter emitter, Object data) {
158168 // 연결 종료 시 SseConnection 객체를 찾아 제거
159169 private void removeConnection (Long mapId , SseConnection connection ) {
160170 List <SseConnection > connections = connectionsByMapId .get (mapId );
161- if (connections != null ) {
162- connections .remove (connection );
163- log .info ("SSE 연결 해제 - 마인드맵 ID: {}, 사용자 ID: {}" , mapId , connection .userId ());
164- if (connections .isEmpty ()) {
165- connectionsByMapId .remove (mapId );
166- log .info ("마인드맵 ID {} 의 모든 구독자 연결 종료" , mapId );
171+ if (connections != null && connections .remove (connection )) {
172+ try {
173+ // Redis Set에서 접속 종료한 사용자 정보 제거
174+ String redisKey = "mindmap:" + mapId + ":users" ;
175+ String userData = objectMapper .writeValueAsString (
176+ new ConnectedUserDto (connection .userId (), connection .nickname (), connection .profileImage ())
177+ );
178+ redisTemplate .opsForSet ().remove (redisKey , userData );
179+
180+ } catch (JsonProcessingException e ) {
181+ log .error ("사용자 정보 직렬화 실패 (연결 종료)" , e );
167182 }
168183 broadcastUserListUpdate (mapId );
184+ log .info ("SSE 연결 해제 및 Redis 업데이트 - 마인드맵 ID: {}, 사용자 ID: {}" , mapId , connection .userId ());
169185 }
170186 }
171187
@@ -188,13 +204,23 @@ public record ConnectedUserDto(Long userId, String nickname, String profileImage
188204
189205 // 현재 접속 중인 사용자 목록을 반환하는 서비스 메서드
190206 public List <ConnectedUserDto > getConnectedUsers (Long mapId ) {
191- List <SseConnection > connections = connectionsByMapId .getOrDefault (mapId , new CopyOnWriteArrayList <>());
207+ String redisKey = "mindmap:" + mapId + ":users" ;
208+ Set <String > usersJson = redisTemplate .opsForSet ().members (redisKey );
192209
193- // 중복된 userId를 제거하고 DTO로 변환하여 반환 (여러 탭 접속 시 중복 방지)
194- return connections .stream ()
195- .map (conn -> new ConnectedUserDto (conn .userId (), conn .nickname (), conn .profileImage ()))
196- .distinct () // userId 기준으로 중복 제거
210+ if (usersJson == null ) {
211+ return Collections .emptyList ();
212+ }
213+
214+ return usersJson .stream ()
215+ .map (json -> {
216+ try {
217+ return objectMapper .readValue (json , ConnectedUserDto .class );
218+ } catch (JsonProcessingException e ) {
219+ log .error ("사용자 정보 역직렬화 실패" , e );
220+ return null ;
221+ }
222+ })
223+ .filter (Objects ::nonNull )
197224 .collect (Collectors .toList ());
198225 }
199-
200226}
0 commit comments