diff --git a/backend/src/main/java/io/f1/backend/domain/game/app/RoomService.java b/backend/src/main/java/io/f1/backend/domain/game/app/RoomService.java index a9d6f324..1ae0e82a 100644 --- a/backend/src/main/java/io/f1/backend/domain/game/app/RoomService.java +++ b/backend/src/main/java/io/f1/backend/domain/game/app/RoomService.java @@ -12,17 +12,19 @@ import io.f1.backend.domain.game.dto.response.RoomListResponse; import io.f1.backend.domain.game.dto.response.RoomResponse; import io.f1.backend.domain.game.dto.response.RoomSettingResponse; +import io.f1.backend.domain.game.event.RoomCreatedEvent; import io.f1.backend.domain.game.model.GameSetting; import io.f1.backend.domain.game.model.Player; import io.f1.backend.domain.game.model.Room; import io.f1.backend.domain.game.model.RoomSetting; import io.f1.backend.domain.game.model.RoomState; import io.f1.backend.domain.game.store.RoomRepository; +import io.f1.backend.domain.quiz.app.QuizService; import io.f1.backend.domain.quiz.entity.Quiz; -import io.f1.backend.domain.user.entity.User; import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import java.util.List; @@ -33,8 +35,10 @@ @RequiredArgsConstructor public class RoomService { + private final QuizService quizService; private final RoomRepository roomRepository; private final AtomicLong roomIdGenerator = new AtomicLong(0); + private final ApplicationEventPublisher eventPublisher; public RoomCreateResponse saveRoom(RoomCreateRequest request, Map loginUser) { @@ -46,7 +50,14 @@ public RoomCreateResponse saveRoom(RoomCreateRequest request, Map rooms = roomRepository.findAll(); List roomResponses = rooms.stream() .map( room -> { - User user = new User(); // 임시 유저 객체 - user.setNickname("임시 유저 닉네임"); - - Quiz quiz = new Quiz(); // 임시 퀴즈 객체 - quiz.setTitle("임시 퀴즈 제목"); - quiz.setDescription("임시 퀴즈 설명"); - quiz.setThumbnailUrl("임시 이미지"); - quiz.setQuestions(List.of()); - quiz.setCreator(user); + Long quizId = room.getGameSetting().getQuizId(); + Quiz quiz = quizService.getQuizById(quizId); return toRoomResponse(room, quiz); }) diff --git a/backend/src/main/java/io/f1/backend/domain/game/event/RoomCreatedEvent.java b/backend/src/main/java/io/f1/backend/domain/game/event/RoomCreatedEvent.java new file mode 100644 index 00000000..545ae3e1 --- /dev/null +++ b/backend/src/main/java/io/f1/backend/domain/game/event/RoomCreatedEvent.java @@ -0,0 +1,6 @@ +package io.f1.backend.domain.game.event; + +import io.f1.backend.domain.game.model.Room; +import io.f1.backend.domain.quiz.entity.Quiz; + +public record RoomCreatedEvent(Room room, Quiz quiz) {} diff --git a/backend/src/main/java/io/f1/backend/domain/game/event/RoomDeletedEvent.java b/backend/src/main/java/io/f1/backend/domain/game/event/RoomDeletedEvent.java new file mode 100644 index 00000000..caa9f408 --- /dev/null +++ b/backend/src/main/java/io/f1/backend/domain/game/event/RoomDeletedEvent.java @@ -0,0 +1,3 @@ +package io.f1.backend.domain.game.event; + +public record RoomDeletedEvent(Long roomId) {} diff --git a/backend/src/main/java/io/f1/backend/domain/game/event/RoomUpdatedEvent.java b/backend/src/main/java/io/f1/backend/domain/game/event/RoomUpdatedEvent.java new file mode 100644 index 00000000..204a21d8 --- /dev/null +++ b/backend/src/main/java/io/f1/backend/domain/game/event/RoomUpdatedEvent.java @@ -0,0 +1,6 @@ +package io.f1.backend.domain.game.event; + +import io.f1.backend.domain.game.model.Room; +import io.f1.backend.domain.quiz.entity.Quiz; + +public record RoomUpdatedEvent(Room room, Quiz quiz) {} diff --git a/backend/src/main/java/io/f1/backend/domain/game/sse/app/SseService.java b/backend/src/main/java/io/f1/backend/domain/game/sse/app/SseService.java index adfd41dc..71a54c19 100644 --- a/backend/src/main/java/io/f1/backend/domain/game/sse/app/SseService.java +++ b/backend/src/main/java/io/f1/backend/domain/game/sse/app/SseService.java @@ -1,5 +1,6 @@ package io.f1.backend.domain.game.sse.app; +import io.f1.backend.domain.game.sse.dto.LobbySseEvent; import io.f1.backend.domain.game.sse.store.SseEmitterRepository; import lombok.RequiredArgsConstructor; @@ -28,4 +29,15 @@ public SseEmitter subscribe() { } return emitter; } + + // 로비로 SSE 메시지를 쏘기위한 메서드 + public void notifyLobbyUpdate(LobbySseEvent event) { + for (SseEmitter emitter : emitterRepository.getAll()) { + try { + emitter.send(SseEmitter.event().name(event.type()).data(event)); + } catch (IOException e) { + emitterRepository.remove(emitter); + } + } + } } diff --git a/backend/src/main/java/io/f1/backend/domain/game/sse/dto/LobbySseEvent.java b/backend/src/main/java/io/f1/backend/domain/game/sse/dto/LobbySseEvent.java new file mode 100644 index 00000000..a0f73697 --- /dev/null +++ b/backend/src/main/java/io/f1/backend/domain/game/sse/dto/LobbySseEvent.java @@ -0,0 +1,3 @@ +package io.f1.backend.domain.game.sse.dto; + +public record LobbySseEvent(String type, T payload) {} diff --git a/backend/src/main/java/io/f1/backend/domain/game/sse/dto/RoomCreatedPayload.java b/backend/src/main/java/io/f1/backend/domain/game/sse/dto/RoomCreatedPayload.java new file mode 100644 index 00000000..08c7e4fd --- /dev/null +++ b/backend/src/main/java/io/f1/backend/domain/game/sse/dto/RoomCreatedPayload.java @@ -0,0 +1,14 @@ +package io.f1.backend.domain.game.sse.dto; + +public record RoomCreatedPayload( + Long roomId, + String roomName, + int maxUserCount, + int currentUserCount, + boolean locked, + String roomState, + String quizTitle, + String description, + String creator, + int numberOfQuestion, + String thumbnailUrl) {} diff --git a/backend/src/main/java/io/f1/backend/domain/game/sse/dto/RoomDeletedPayload.java b/backend/src/main/java/io/f1/backend/domain/game/sse/dto/RoomDeletedPayload.java new file mode 100644 index 00000000..0ddf6b18 --- /dev/null +++ b/backend/src/main/java/io/f1/backend/domain/game/sse/dto/RoomDeletedPayload.java @@ -0,0 +1,3 @@ +package io.f1.backend.domain.game.sse.dto; + +public record RoomDeletedPayload(Long roomId) {} diff --git a/backend/src/main/java/io/f1/backend/domain/game/sse/dto/RoomUpdatedPayload.java b/backend/src/main/java/io/f1/backend/domain/game/sse/dto/RoomUpdatedPayload.java new file mode 100644 index 00000000..54df4128 --- /dev/null +++ b/backend/src/main/java/io/f1/backend/domain/game/sse/dto/RoomUpdatedPayload.java @@ -0,0 +1,11 @@ +package io.f1.backend.domain.game.sse.dto; + +public record RoomUpdatedPayload( + Long roomId, + int currentUserCount, + String roomState, + String quizTitle, + String description, + String creator, + int numberOfQuestion, + String thumbnailUrl) {} diff --git a/backend/src/main/java/io/f1/backend/domain/game/sse/dto/SseEventType.java b/backend/src/main/java/io/f1/backend/domain/game/sse/dto/SseEventType.java new file mode 100644 index 00000000..ce913a58 --- /dev/null +++ b/backend/src/main/java/io/f1/backend/domain/game/sse/dto/SseEventType.java @@ -0,0 +1,7 @@ +package io.f1.backend.domain.game.sse.dto; + +public enum SseEventType { + CREATE, + UPDATE, + DELETE +} diff --git a/backend/src/main/java/io/f1/backend/domain/game/sse/listener/RoomCreatedEventListener.java b/backend/src/main/java/io/f1/backend/domain/game/sse/listener/RoomCreatedEventListener.java new file mode 100644 index 00000000..6806401c --- /dev/null +++ b/backend/src/main/java/io/f1/backend/domain/game/sse/listener/RoomCreatedEventListener.java @@ -0,0 +1,28 @@ +package io.f1.backend.domain.game.sse.listener; + +import static io.f1.backend.domain.game.sse.mapper.SseMapper.*; + +import io.f1.backend.domain.game.event.RoomCreatedEvent; +import io.f1.backend.domain.game.sse.app.SseService; +import io.f1.backend.domain.game.sse.dto.LobbySseEvent; +import io.f1.backend.domain.game.sse.dto.RoomCreatedPayload; + +import lombok.RequiredArgsConstructor; + +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class RoomCreatedEventListener { + + private final SseService sseService; + + @Async + @EventListener + public void roomCreate(RoomCreatedEvent event) { + LobbySseEvent sseEvent = fromRoomCreated(event); + sseService.notifyLobbyUpdate(sseEvent); + } +} diff --git a/backend/src/main/java/io/f1/backend/domain/game/sse/listener/RoomDeletedEventListener.java b/backend/src/main/java/io/f1/backend/domain/game/sse/listener/RoomDeletedEventListener.java new file mode 100644 index 00000000..eee8d11e --- /dev/null +++ b/backend/src/main/java/io/f1/backend/domain/game/sse/listener/RoomDeletedEventListener.java @@ -0,0 +1,26 @@ +package io.f1.backend.domain.game.sse.listener; + +import static io.f1.backend.domain.game.sse.mapper.SseMapper.*; + +import io.f1.backend.domain.game.event.RoomDeletedEvent; +import io.f1.backend.domain.game.sse.app.SseService; +import io.f1.backend.domain.game.sse.dto.LobbySseEvent; +import io.f1.backend.domain.game.sse.dto.RoomDeletedPayload; + +import lombok.RequiredArgsConstructor; + +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; + +@RequiredArgsConstructor +public class RoomDeletedEventListener { + + private final SseService sseService; + + @Async + @EventListener + public void roomDelete(RoomDeletedEvent event) { + LobbySseEvent sseEvent = fromRoomDeleted(event); + sseService.notifyLobbyUpdate(sseEvent); + } +} diff --git a/backend/src/main/java/io/f1/backend/domain/game/sse/listener/RoomUpdatedEventListener.java b/backend/src/main/java/io/f1/backend/domain/game/sse/listener/RoomUpdatedEventListener.java new file mode 100644 index 00000000..5f957de2 --- /dev/null +++ b/backend/src/main/java/io/f1/backend/domain/game/sse/listener/RoomUpdatedEventListener.java @@ -0,0 +1,25 @@ +package io.f1.backend.domain.game.sse.listener; + +import io.f1.backend.domain.game.event.RoomUpdatedEvent; +import io.f1.backend.domain.game.sse.app.SseService; +import io.f1.backend.domain.game.sse.dto.LobbySseEvent; +import io.f1.backend.domain.game.sse.dto.RoomUpdatedPayload; +import io.f1.backend.domain.game.sse.mapper.SseMapper; + +import lombok.RequiredArgsConstructor; + +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; + +@RequiredArgsConstructor +public class RoomUpdatedEventListener { + + private final SseService sseService; + + @Async + @EventListener + public void roomUpdate(RoomUpdatedEvent event) { + LobbySseEvent sseEvent = SseMapper.fromRoomUpdated(event); + sseService.notifyLobbyUpdate(sseEvent); + } +} diff --git a/backend/src/main/java/io/f1/backend/domain/game/sse/mapper/SseMapper.java b/backend/src/main/java/io/f1/backend/domain/game/sse/mapper/SseMapper.java new file mode 100644 index 00000000..efdd94c8 --- /dev/null +++ b/backend/src/main/java/io/f1/backend/domain/game/sse/mapper/SseMapper.java @@ -0,0 +1,56 @@ +package io.f1.backend.domain.game.sse.mapper; + +import io.f1.backend.domain.game.event.RoomCreatedEvent; +import io.f1.backend.domain.game.event.RoomDeletedEvent; +import io.f1.backend.domain.game.event.RoomUpdatedEvent; +import io.f1.backend.domain.game.model.Room; +import io.f1.backend.domain.game.sse.dto.LobbySseEvent; +import io.f1.backend.domain.game.sse.dto.RoomCreatedPayload; +import io.f1.backend.domain.game.sse.dto.RoomDeletedPayload; +import io.f1.backend.domain.game.sse.dto.RoomUpdatedPayload; +import io.f1.backend.domain.game.sse.dto.SseEventType; +import io.f1.backend.domain.quiz.entity.Quiz; + +public class SseMapper { + + public static LobbySseEvent fromRoomCreated(RoomCreatedEvent event) { + Room room = event.room(); + Quiz quiz = event.quiz(); + RoomCreatedPayload payload = + new RoomCreatedPayload( + room.getId(), + room.getRoomSetting().roomName(), + room.getRoomSetting().maxUserCount(), + room.getPlayerSessionMap().size(), + room.getRoomSetting().locked(), + room.getState().name(), + quiz.getTitle(), + quiz.getDescription(), + quiz.getCreator().getNickname(), + quiz.getQuestions().size(), + quiz.getThumbnailUrl()); + return new LobbySseEvent<>(SseEventType.CREATE.name(), payload); + } + + public static LobbySseEvent fromRoomUpdated(RoomUpdatedEvent event) { + Room room = event.room(); + Quiz quiz = event.quiz(); + RoomUpdatedPayload payload = + new RoomUpdatedPayload( + room.getId(), + room.getPlayerSessionMap().size(), + room.getState().name(), + quiz.getTitle(), + quiz.getDescription(), + quiz.getCreator().getNickname(), + quiz.getQuestions().size(), + quiz.getThumbnailUrl()); + return new LobbySseEvent<>(SseEventType.UPDATE.name(), payload); + } + + public static LobbySseEvent fromRoomDeleted(RoomDeletedEvent event) { + Long roomId = event.roomId(); + RoomDeletedPayload payload = new RoomDeletedPayload(roomId); + return new LobbySseEvent<>(SseEventType.DELETE.name(), payload); + } +} diff --git a/backend/src/main/java/io/f1/backend/domain/game/sse/store/SseEmitterRepository.java b/backend/src/main/java/io/f1/backend/domain/game/sse/store/SseEmitterRepository.java index 4cf61242..c5236dec 100644 --- a/backend/src/main/java/io/f1/backend/domain/game/sse/store/SseEmitterRepository.java +++ b/backend/src/main/java/io/f1/backend/domain/game/sse/store/SseEmitterRepository.java @@ -14,10 +14,7 @@ public class SseEmitterRepository { public void save(SseEmitter emitter) { emitters.add(emitter); // 연결종료 객체정리 - emitter.onCompletion( - () -> { - emitters.remove(emitter); - }); + emitter.onCompletion(() -> emitters.remove(emitter)); emitter.onTimeout(() -> emitters.remove(emitter)); emitter.onError(error -> emitters.remove(emitter)); } diff --git a/backend/src/main/java/io/f1/backend/domain/quiz/app/QuizService.java b/backend/src/main/java/io/f1/backend/domain/quiz/app/QuizService.java index ffe46c50..dde9f6f3 100644 --- a/backend/src/main/java/io/f1/backend/domain/quiz/app/QuizService.java +++ b/backend/src/main/java/io/f1/backend/domain/quiz/app/QuizService.java @@ -91,4 +91,10 @@ private String convertToThumbnailPath(MultipartFile thumbnailFile) throws IOExce private String getExtension(String filename) { return filename.substring(filename.lastIndexOf(".") + 1); } + + public Quiz getQuizById(Long quizId) { + return quizRepository + .findById(quizId) + .orElseThrow(() -> new RuntimeException("E404002: 존재하지 않는 퀴즈입니다.")); + } }