Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,22 @@
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;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/alerts")
@Tag(name = "SSE", description = "SSE를 통해 사용자에게 원하는 버스 도착 정보를 실시간으로 제공합니다.")
@Slf4j
public class BusAlertSseController {

private final SseEmitterService sseEmitterService;
Expand All @@ -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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<Long, SseEmitter> 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<BusArrival> 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));
Expand All @@ -50,9 +74,12 @@ public void notifyUserIfBusArriving(Long userId, List<BusArrival> 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);

}
}
}
Expand Down
Loading