diff --git a/src/main/java/com/teamEWSN/gitdeun/common/config/SecurityPath.java b/src/main/java/com/teamEWSN/gitdeun/common/config/SecurityPath.java index 5636642..869ef43 100644 --- a/src/main/java/com/teamEWSN/gitdeun/common/config/SecurityPath.java +++ b/src/main/java/com/teamEWSN/gitdeun/common/config/SecurityPath.java @@ -14,13 +14,15 @@ public class SecurityPath { // hasRole("USER") public static final String[] USER_ENDPOINTS = { "/api/auth/connect/github/state", + "/api/auth/logout", + "/api/auth/social", "/api/users/me", "/api/users/me/**", - "/api/auth/logout", "/api/repos", "/api/repos/**", "/api/mindmaps/**", - "/api/history/**" + "/api/history/**", + "/api/s3/bucket/**" }; // hasRole("ADMIN") diff --git a/src/main/java/com/teamEWSN/gitdeun/mindmap/controller/MindmapController.java b/src/main/java/com/teamEWSN/gitdeun/mindmap/controller/MindmapController.java index 09f6c16..aebe97a 100644 --- a/src/main/java/com/teamEWSN/gitdeun/mindmap/controller/MindmapController.java +++ b/src/main/java/com/teamEWSN/gitdeun/mindmap/controller/MindmapController.java @@ -33,9 +33,10 @@ public ResponseEntity createMindmap( // 마인드맵 상세 조회 (유저 인가 확인필요?) @GetMapping("/{mapId}") public ResponseEntity getMindmap( - @PathVariable Long mapId + @PathVariable Long mapId, + @AuthenticationPrincipal CustomUserDetails userDetails ) { - MindmapDetailResponseDto responseDto = mindmapService.getMindmap(mapId); + MindmapDetailResponseDto responseDto = mindmapService.getMindmap(mapId, userDetails.getId()); return ResponseEntity.ok(responseDto); } diff --git a/src/main/java/com/teamEWSN/gitdeun/mindmap/controller/MindmapSseController.java b/src/main/java/com/teamEWSN/gitdeun/mindmap/controller/MindmapSseController.java new file mode 100644 index 0000000..98ac218 --- /dev/null +++ b/src/main/java/com/teamEWSN/gitdeun/mindmap/controller/MindmapSseController.java @@ -0,0 +1,22 @@ +package com.teamEWSN.gitdeun.mindmap.controller; + +import com.teamEWSN.gitdeun.mindmap.service.MindmapSseService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +@RestController +@RequiredArgsConstructor +public class MindmapSseController { + + private final MindmapSseService mindmapSseService; + + // 클라이언트의 특정 마인드맵의 업데이트를 구독 + @GetMapping(value = "/api/mindmaps/{mapId}/subscribe", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + public SseEmitter subscribeToMindmapUpdates(@PathVariable Long mapId) { + return mindmapSseService.subscribe(mapId); + } +} diff --git a/src/main/java/com/teamEWSN/gitdeun/mindmap/service/MindmapService.java b/src/main/java/com/teamEWSN/gitdeun/mindmap/service/MindmapService.java index 88feaf0..0fa3b76 100644 --- a/src/main/java/com/teamEWSN/gitdeun/mindmap/service/MindmapService.java +++ b/src/main/java/com/teamEWSN/gitdeun/mindmap/service/MindmapService.java @@ -14,6 +14,7 @@ import com.teamEWSN.gitdeun.mindmap.mapper.MindmapMapper; import com.teamEWSN.gitdeun.mindmapmember.repository.MindmapMemberRepository; import com.teamEWSN.gitdeun.mindmap.repository.MindmapRepository; +import com.teamEWSN.gitdeun.mindmapmember.service.MindmapAuthService; import com.teamEWSN.gitdeun.repo.entity.Repo; import com.teamEWSN.gitdeun.repo.repository.RepoRepository; import com.teamEWSN.gitdeun.repo.service.RepoService; @@ -36,6 +37,8 @@ public class MindmapService { private final VisitHistoryService visitHistoryService; private final RepoService repoService; + private final MindmapSseService mindmapSseService; + private final MindmapAuthService mindmapAuthService; private final MindmapMapper mindmapMapper; private final MindmapRepository mindmapRepository; private final MindmapMemberRepository mindmapMemberRepository; @@ -117,9 +120,15 @@ private long findNextCheckSequence(User user) { /** * 마인드맵 상세 정보 조회 + * 저장소 업데이트는 Fast API Webhook 알림 + * 마인드맵 변경이나 리뷰 생성 시 SSE 적용을 통한 실시간 업데이트 (새로고침 x) */ @Transactional - public MindmapDetailResponseDto getMindmap(Long mapId) { + public MindmapDetailResponseDto getMindmap(Long mapId, Long userId) { + if (!mindmapAuthService.hasView(mapId, userId)) { + throw new GlobalException(ErrorCode.FORBIDDEN_ACCESS); // 멤버 권한 확인 + } + Mindmap mindmap = mindmapRepository.findById(mapId) .orElseThrow(() -> new GlobalException(ErrorCode.MINDMAP_NOT_FOUND)); @@ -148,9 +157,14 @@ public MindmapDetailResponseDto refreshMindmap(Long mapId, Long userId) { // 데이터 최신화 mindmap.getRepo().updateWithAnalysis(dto); - mindmap.updateMapData(dto.getMapData()); // Mindmap 엔티티에 편의 메서드 추가 필요 + mindmap.updateMapData(dto.getMapData()); - return mindmapMapper.toDetailResponseDto(mindmap); + MindmapDetailResponseDto responseDto = mindmapMapper.toDetailResponseDto(mindmap); + + // 업데이트된 마인드맵 정보를 모든 구독자에게 방송 + mindmapSseService.broadcastUpdate(mapId, responseDto); + + return responseDto; } /** diff --git a/src/main/java/com/teamEWSN/gitdeun/mindmap/service/MindmapSseService.java b/src/main/java/com/teamEWSN/gitdeun/mindmap/service/MindmapSseService.java new file mode 100644 index 0000000..8e756de --- /dev/null +++ b/src/main/java/com/teamEWSN/gitdeun/mindmap/service/MindmapSseService.java @@ -0,0 +1,58 @@ +package com.teamEWSN.gitdeun.mindmap.service; + +import com.teamEWSN.gitdeun.mindmap.dto.MindmapDetailResponseDto; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Slf4j +@Service +public class MindmapSseService { + + // 스레드 안전(thread-safe)한 자료구조 사용 + private final Map> emitters = new ConcurrentHashMap<>(); + + // 클라이언트가 구독을 요청할 때 호출 + public SseEmitter subscribe(Long mapId) { + SseEmitter emitter = new SseEmitter(Long.MAX_VALUE); // 타임아웃을 매우 길게 설정 + // 특정 마인드맵 ID에 해당하는 Emitter 리스트에 추가 + this.emitters.computeIfAbsent(mapId, k -> new CopyOnWriteArrayList<>()).add(emitter); + + emitter.onCompletion(() -> this.emitters.get(mapId).remove(emitter)); + emitter.onTimeout(() -> this.emitters.get(mapId).remove(emitter)); + + // 연결 성공을 알리는 더미 이벤트 전송 + try { + emitter.send(SseEmitter.event().name("connect").data("Connected!")); + } catch (IOException e) { + log.error("SSE 연결 중 오류 발생", e); + } + + return emitter; + } + + // 마인드맵이 업데이트되면 이 메서드를 호출하여 모든 구독자에게 방송 + public void broadcastUpdate(Long mapId, MindmapDetailResponseDto updatedMindmap) { + List mapEmitters = this.emitters.get(mapId); + if (mapEmitters == null) { + return; + } + + mapEmitters.forEach(emitter -> { + try { + emitter.send(SseEmitter.event() + .name("mindmap-update") // 이벤트 이름 지정 + .data(updatedMindmap)); // 업데이트된 마인드맵 데이터 전송 + } catch (IOException e) { + log.error("SSE 데이터 전송 중 오류 발생, emitter 제거", e); + mapEmitters.remove(emitter); + } + }); + } +} diff --git a/src/main/java/com/teamEWSN/gitdeun/mindmapmember/repository/MindmapMemberRepository.java b/src/main/java/com/teamEWSN/gitdeun/mindmapmember/repository/MindmapMemberRepository.java index 177c596..2542a19 100644 --- a/src/main/java/com/teamEWSN/gitdeun/mindmapmember/repository/MindmapMemberRepository.java +++ b/src/main/java/com/teamEWSN/gitdeun/mindmapmember/repository/MindmapMemberRepository.java @@ -14,11 +14,9 @@ public interface MindmapMemberRepository extends JpaRepository roles); + boolean existsByMindmapIdAndUserIdAndRoleIn(Long mindmapId, Long userId, Collection roles); // 권한 변경 Optional findByIdAndMindmapId(Long memberId, Long mindmapId);