Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 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
7 changes: 6 additions & 1 deletion backend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,9 @@ out/
.env

### .idea ###
.idea
.idea


src/main/resources/static/ws-test.html
src/main/resources/static/ws-test.js

Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import io.f1.backend.domain.game.app.RoomService;
import io.f1.backend.domain.game.dto.request.RoomCreateRequest;
import io.f1.backend.domain.game.dto.request.RoomValidationRequest;
import io.f1.backend.domain.game.dto.response.RoomCreateResponse;
import io.f1.backend.domain.game.dto.response.RoomListResponse;

Expand Down Expand Up @@ -38,6 +39,12 @@ public RoomCreateResponse saveRoom(@RequestBody @Valid RoomCreateRequest request
return roomService.saveRoom(request, loginUser);
}

@PostMapping("/validation")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void validateRoom(@RequestBody RoomValidationRequest request) {
roomService.validateRoom(request);
}

@GetMapping
public RoomListResponse getAllRooms() {
return roomService.getAllRooms();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,21 @@

import static io.f1.backend.domain.game.mapper.RoomMapper.*;

import io.f1.backend.domain.game.dto.RoomInitialData;
import io.f1.backend.domain.game.dto.request.RoomCreateRequest;
import io.f1.backend.domain.game.dto.request.RoomValidationRequest;
import io.f1.backend.domain.game.dto.response.GameSettingResponse;
import io.f1.backend.domain.game.dto.response.PlayerListResponse;
import io.f1.backend.domain.game.dto.response.QuizResponse;
import io.f1.backend.domain.game.dto.response.RoomCreateResponse;
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.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.entity.Quiz;
import io.f1.backend.domain.user.entity.User;
Expand Down Expand Up @@ -44,6 +51,58 @@ public RoomCreateResponse saveRoom(RoomCreateRequest request, Map<String, Object
return new RoomCreateResponse(newId);
}

public void validateRoom(RoomValidationRequest request) {

Room room =
roomRepository
.findRoom(request.roomId())
.orElseThrow(() -> new IllegalArgumentException("404 존재하지 않는 방입니다."));

if (room.getState().equals(RoomState.PLAYING)) {
throw new IllegalArgumentException("403 게임이 진행중입니다.");
}

int maxUserCnt = room.getRoomSetting().maxUserCount();
int currentCnt = room.getPlayerSessionMap().size();
if (maxUserCnt == currentCnt) {
throw new IllegalArgumentException("403 정원이 모두 찼습니다.");
}

if (room.getRoomSetting().locked()
&& !room.getRoomSetting().password().equals(request.password())) {
throw new IllegalArgumentException("401 비밀번호가 일치하지 않습니다.");
}
}

public RoomInitialData enterRoom(Long roomId, String sessionId) {

Room room =
roomRepository
.findRoom(roomId)
.orElseThrow(() -> new IllegalArgumentException("404 존재하지 않는 방입니다."));

// todo security
Player player = new Player(1L, "빵야빵야");

Map<String, Player> playerSessionMap = room.getPlayerSessionMap();

playerSessionMap.put(sessionId, player);

String destination = "/sub/room/" + roomId;

RoomSettingResponse roomSettingResponse = toRoomSettingResponse(room);
// todo quiz 생성 api 완성 후 수정
QuizResponse quiz =
new QuizResponse(room.getGameSetting().getQuizId(), "title", "설명", "url", 10);
GameSettingResponse gameSettingResponse =
toGameSettingResponse(room.getGameSetting(), quiz);

PlayerListResponse playerListResponse = toPlayerListResponse(room);

return new RoomInitialData(
destination, roomSettingResponse, gameSettingResponse, playerListResponse);
}

// todo quizService에서 퀴즈 조회 메서드로 변경
public RoomListResponse getAllRooms() {
List<Room> rooms = roomRepository.findAll();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.f1.backend.domain.game.dto;

public enum MessageType {
ROOM_SETTING,
GAME_SETTING,
PLAYER_LIST,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package io.f1.backend.domain.game.dto;

import io.f1.backend.domain.game.dto.response.GameSettingResponse;
import io.f1.backend.domain.game.dto.response.PlayerListResponse;
import io.f1.backend.domain.game.dto.response.RoomSettingResponse;

public record RoomInitialData(
String destination,
RoomSettingResponse roomSettingResponse,
GameSettingResponse gameSettingResponse,
PlayerListResponse playerListResponse) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.f1.backend.domain.game.dto;

public interface WebSocketDto<T> {
MessageType getType();

T getMessage();
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ public record RoomCreateRequest(
@Max(value = 8, message = "방 인원 수는 최대 8명 입니다.")
Integer maxUserCount,
@NotNull String password,
@NotNull boolean locked) {}
@NotNull Boolean locked) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package io.f1.backend.domain.game.dto.request;

public record RoomValidationRequest(Long roomId, String password) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package io.f1.backend.domain.game.dto.response;

import io.f1.backend.domain.game.dto.MessageType;
import io.f1.backend.domain.game.dto.WebSocketDto;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class DefaultWebSocketResponse<T> implements WebSocketDto<T> {
private final MessageType type;
private final T message;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

바로 WebSocketDto를 클래스로 사용하지 않고, 이를 implement 하는 식으로 구현한 이유가 궁금합니다 ! 추후에 확장 가능성이 있는 건가요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

우선은 WebSocket으로 주고받는 응답은 반드시 type과 message라는 구조를 가져야 한다는 의도를 명확히 하기 위해서 인터페이스로 분리했습니다.

추후에 확장 가능성을 생각할 때도 interface로 강제해두면 이후 응답 형태가 늘어나도 일관된 구조를 유지할 수 있다는 점에서 인터페이스를 설계하였습니다!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

옹 ! 이해했습니다. 답변 감사합니다 ! :)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package io.f1.backend.domain.game.dto.response;

public record GameSettingResponse(int round, int timeLimit, QuizResponse quiz) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package io.f1.backend.domain.game.dto.response;

import java.util.List;

public record PlayerListResponse(String host, List<PlayerResponse> players) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package io.f1.backend.domain.game.dto.response;

public record PlayerResponse(String nickname, boolean ready) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package io.f1.backend.domain.game.dto.response;

public record QuizResponse(
Long quizId, String title, String description, String thumbnailUrl, int numberOfQuestion) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package io.f1.backend.domain.game.dto.response;

public record RoomSettingResponse(String roomName, int maxUserCount, int currentUserCount) {}
Original file line number Diff line number Diff line change
@@ -1,18 +1,47 @@
package io.f1.backend.domain.game.mapper;

import io.f1.backend.domain.game.dto.request.RoomCreateRequest;
import io.f1.backend.domain.game.dto.response.GameSettingResponse;
import io.f1.backend.domain.game.dto.response.PlayerListResponse;
import io.f1.backend.domain.game.dto.response.PlayerResponse;
import io.f1.backend.domain.game.dto.response.QuizResponse;
import io.f1.backend.domain.game.dto.response.RoomResponse;
import io.f1.backend.domain.game.dto.response.RoomSettingResponse;
import io.f1.backend.domain.game.model.GameSetting;
import io.f1.backend.domain.game.model.Room;
import io.f1.backend.domain.game.model.RoomSetting;
import io.f1.backend.domain.quiz.entity.Quiz;

import java.util.List;

public class RoomMapper {

public static RoomSetting toRoomSetting(RoomCreateRequest request) {
return new RoomSetting(
request.roomName(), request.maxUserCount(), request.locked(), request.password());
}

public static RoomSettingResponse toRoomSettingResponse(Room room) {
return new RoomSettingResponse(
room.getRoomSetting().roomName(),
room.getRoomSetting().maxUserCount(),
room.getPlayerSessionMap().size());
}

public static GameSettingResponse toGameSettingResponse(
GameSetting gameSetting, QuizResponse quiz) {
return new GameSettingResponse(gameSetting.getRound(), gameSetting.getTimeLimit(), quiz);
}

public static PlayerListResponse toPlayerListResponse(Room room) {
List<PlayerResponse> playerResponseList =
room.getPlayerSessionMap().values().stream()
.map(player -> new PlayerResponse(player.getNickname(), false))
.toList();

return new PlayerListResponse(room.getHost().getNickname(), playerResponseList);
}

public static RoomResponse toRoomResponse(Room room, Quiz quiz) {
return new RoomResponse(
room.getId(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
import io.f1.backend.domain.game.model.Room;

import java.util.List;
import java.util.Optional;

public interface RoomRepository {

void saveRoom(Room room);

Optional<Room> findRoom(Long roomId);

List<Room> findAll();
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;

@Repository
Expand All @@ -19,6 +20,11 @@ public void saveRoom(Room room) {
roomMap.put(room.getId(), room);
}

@Override
public Optional<Room> findRoom(Long roomId) {
return Optional.ofNullable(roomMap.get(roomId));
}

@Override
public List<Room> findAll() {
return new ArrayList<>(roomMap.values());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package io.f1.backend.domain.game.websocket;

import io.f1.backend.domain.game.app.RoomService;
import io.f1.backend.domain.game.dto.MessageType;
import io.f1.backend.domain.game.dto.RoomInitialData;

import lombok.RequiredArgsConstructor;

import org.springframework.messaging.Message;
import org.springframework.messaging.handler.annotation.DestinationVariable;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.stereotype.Controller;

@Controller
@RequiredArgsConstructor
public class GameSocketController {

private final MessageSender messageSender;
private final RoomService roomService;

@MessageMapping("/room/enter/{roomId}")
public void roomEnter(@DestinationVariable Long roomId, Message<?> message) {

StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
String websocketSessionId = accessor.getSessionId();

RoomInitialData roomInitialData = roomService.enterRoom(roomId, websocketSessionId);
String destination = roomInitialData.destination();

messageSender.send(
destination, MessageType.ROOM_SETTING, roomInitialData.roomSettingResponse());
messageSender.send(
destination, MessageType.GAME_SETTING, roomInitialData.gameSettingResponse());
messageSender.send(
destination, MessageType.PLAYER_LIST, roomInitialData.playerListResponse());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package io.f1.backend.domain.game.websocket;

import io.f1.backend.domain.game.dto.MessageType;
import io.f1.backend.domain.game.dto.response.DefaultWebSocketResponse;

import lombok.RequiredArgsConstructor;

import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class MessageSender {

private final SimpMessagingTemplate messagingTemplate;

public <T> void send(String destination, MessageType type, T message) {
messagingTemplate.convertAndSend(
destination, new DefaultWebSocketResponse<>(type, message));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package io.f1.backend.global.config;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.stereotype.Component;

@Slf4j
@Component
@RequiredArgsConstructor
public class StompChannelInterceptor implements ChannelInterceptor {

@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {

StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
StompCommand command = accessor.getCommand();
String sessionId = accessor.getSessionId();
String destination = accessor.getDestination();

if (command == null) {
throw new IllegalArgumentException("Stomp command required");
}

switch (command) {
case CONNECT -> log.info("CONNECT : 세션 연결 - sessionId = {}", sessionId);

case SUBSCRIBE -> {
if (destination != null && sessionId != null) {
log.info("SUBSCRIBE : 구독 시작 destination = {}", destination);
}
}

case SEND -> log.info("SEND : 요청 destination = {}", destination);

case DISCONNECT -> log.info("DISCONNECT : 연결 해제 sessionId = {}", sessionId);

default -> throw new IllegalStateException("Unexpected command: " + command);
}

return message;
}
}
Loading