diff --git a/src/main/java/server/gooroomi/domain/bus/api/BusAlertSseController.java b/src/main/java/server/gooroomi/domain/bus/api/BusAlertSseController.java index 31baede..6aa7814 100644 --- a/src/main/java/server/gooroomi/domain/bus/api/BusAlertSseController.java +++ b/src/main/java/server/gooroomi/domain/bus/api/BusAlertSseController.java @@ -5,10 +5,14 @@ import io.swagger.v3.oas.annotations.Parameters; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ResponseStatusException; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import server.gooroomi.domain.bus.application.SseEmitterService; @@ -16,6 +20,7 @@ @RequiredArgsConstructor @RequestMapping("/api/alerts") @Tag(name = "SSE", description = "SSE를 통해 사용자에게 원하는 버스 도착 정보를 실시간으로 제공합니다.") +@Slf4j public class BusAlertSseController { private final SseEmitterService sseEmitterService; @@ -24,9 +29,16 @@ public class BusAlertSseController { @Parameters({ @Parameter(name = "userId", description = "사용자 ID", required = true) }) - @GetMapping("/stream") + @GetMapping(value = "/stream") public SseEmitter stream(@RequestParam Long userId) { - // 사용자 ID를 기반으로 SSE 연결을 생성하고 반환 - return sseEmitterService.connect(userId); + log.info("[SSE 연결 요청] userId={}", userId); + try { + SseEmitter emitter = sseEmitterService.createEmitter(userId); + log.info("[SSE 연결 성공] userId={}", userId); + return emitter; + } catch (Exception e) { + log.error("[SSE 연결 실패] userId={}, error={}", userId, e.getMessage(), e); + throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "SSE 연결 실패", e); + } } } diff --git a/src/main/java/server/gooroomi/domain/bus/application/SseEmitterService.java b/src/main/java/server/gooroomi/domain/bus/application/SseEmitterService.java index b057b55..a8131c1 100644 --- a/src/main/java/server/gooroomi/domain/bus/application/SseEmitterService.java +++ b/src/main/java/server/gooroomi/domain/bus/application/SseEmitterService.java @@ -1,6 +1,7 @@ package server.gooroomi.domain.bus.application; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import server.gooroomi.domain.bus.entity.BusArrival; @@ -16,29 +17,52 @@ @Service @RequiredArgsConstructor +@Slf4j public class SseEmitterService { - private static final Long TIMEOUT = 60 * 1000L; + private static final Long TIMEOUT = 60 * 1000L; // 1시간 유지 private final Map emitters = new ConcurrentHashMap<>(); private final UserRepository userRepository; // 사용자가 SSE 연결을 맺고 이벤트 발생 시 알림을 보낼 수 있도록 설정 - public SseEmitter connect(Long userId) { + public SseEmitter createEmitter(Long userId) { SseEmitter emitter = new SseEmitter(TIMEOUT); // 타임아웃 설정 - emitters.put(userId, emitter); // 사용자 ID를 key로 emitter 저장 + emitters.put(userId, emitter); + log.info("[SSE Emitter 생성] userId={}", userId); - // 연결 종료 시 emitter 제거 - emitter.onCompletion(() -> emitters.remove(userId)); - emitter.onTimeout(() -> emitters.remove(userId)); - emitter.onError(e -> emitters.remove(userId)); + emitter.onCompletion(() -> { + emitters.remove(userId); + log.info("[SSE 연결 종료] userId={}", userId); + }); + emitter.onTimeout(() -> { + emitters.remove(userId); + log.warn("[SSE 타임아웃] userId={}", userId); + }); + + emitter.onError((e) -> { + emitters.remove(userId); + log.error("⚠[SSE 오류] userId={}, error={}", userId, e.getMessage(), e); + }); + + try { + emitter.send(SseEmitter.event() + .name("connect") + .data("SSE 연결 완료")); + log.info("[초기 이벤트 전송 완료] userId={}", userId); + } catch (IOException e) { + log.error("[초기 이벤트 전송 실패] userId={}, error={}", userId, e.getMessage(), e); + } return emitter; } // 도착 예정 버스 목록 중 사용자가 원하는 버스가 있으면 SSE 이벤트 전송 public void notifyUserIfBusArriving(Long userId, List busArrivals) { SseEmitter emitter = emitters.get(userId); - if (emitter == null) return; + if (emitter == null) { + log.warn("[알림 전송 실패] userId={} - Emitter 없음", userId); + return; + } User user = userRepository.findById(userId) .orElseThrow(() -> new BaseException(BaseResponseStatus.NOT_FOUND_USER)); @@ -50,9 +74,12 @@ public void notifyUserIfBusArriving(Long userId, List busArrivals) { try { emitter.send(SseEmitter.event() .name("bus-arrival") // 이벤트 이름 - .data(user.getBusNumber() + "번 버스가 곧 도착합니다.")); // 이벤트 ㅐ용 + .data(user.getBusNumber() + "번 버스가 곧 도착합니다.")); // 이벤트 내용 + log.info("[알림 전송 성공] userId={}, bus={}", userId, user.getBusNumber()); } catch (IOException e) { emitters.remove(userId); // 예외 발생 시 emitter 제거 + log.error("[알림 전송 실패] userId={}, error={}", userId, e.getMessage(), e); + } } }