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
22 changes: 22 additions & 0 deletions .github/workflows/slack-notify-pr-open.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: slack-notify-pr-open

on:
pull_request:
types: [opened, reopened]

jobs:
notify:
runs-on: ubuntu-latest
steps:
- name: Send Slack notification
uses: rtCamp/action-slack-notify@v2
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
SLACK_USERNAME: Github CI
SLACK_ICON: https://github.com/github.png
MSG_MINIMAL: ref,event
SLACK_COLOR: '#36a64f'
SLACK_TITLE: 'New Pull Request 🚀'
SLACK_MESSAGE: |
#${{ github.event.pull_request.number }} ${{ github.event.pull_request.title }}
🔗 ${{ github.event.pull_request.html_url }}
1 change: 1 addition & 0 deletions backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ dependencies {
testAnnotationProcessor 'org.projectlombok:lombok'
testRuntimeOnly 'com.h2database:h2'
testImplementation 'org.springframework.security:spring-security-test'
testImplementation 'com.github.database-rider:rider-spring:1.44.0'

/* ETC */
implementation 'org.apache.commons:commons-lang3:3.12.0'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import io.f1.backend.domain.admin.dao.AdminRepository;
import io.f1.backend.domain.admin.dto.AdminPrincipal;
import io.f1.backend.domain.admin.entity.Admin;
import io.f1.backend.global.exception.errorcode.AdminErrorCode;

import lombok.RequiredArgsConstructor;

Expand All @@ -23,7 +24,9 @@ public UserDetails loadUserByUsername(String username) throws UsernameNotFoundEx
adminRepository
.findByUsername(username)
.orElseThrow(
() -> new UsernameNotFoundException("E404007: 존재하지 않는 관리자입니다."));
() ->
new UsernameNotFoundException(
AdminErrorCode.ADMIN_NOT_FOUND.getMessage()));
// 프론트엔드로 내려가지 않는 예외
return new AdminPrincipal(admin);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.fasterxml.jackson.databind.ObjectMapper;

import io.f1.backend.domain.admin.dto.AdminLoginFailResponse;
import io.f1.backend.global.exception.errorcode.AuthErrorCode;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
Expand Down Expand Up @@ -31,7 +32,9 @@ public void onAuthenticationFailure(
response.setContentType("application/json;charset=UTF-8");

AdminLoginFailResponse errorResponse =
new AdminLoginFailResponse("E401005", "아이디 또는 비밀번호가 일치하지 않습니다.");
new AdminLoginFailResponse(
AuthErrorCode.LOGIN_FAILED.getCode(),
AuthErrorCode.LOGIN_FAILED.getMessage());

response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import io.f1.backend.domain.admin.dao.AdminRepository;
import io.f1.backend.domain.admin.dto.AdminPrincipal;
import io.f1.backend.domain.admin.entity.Admin;
import io.f1.backend.global.exception.CustomException;
import io.f1.backend.global.exception.errorcode.AdminErrorCode;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
Expand Down Expand Up @@ -36,7 +38,7 @@ public void onAuthenticationSuccess(
Admin admin =
adminRepository
.findByUsername(principal.getUsername())
.orElseThrow(() -> new RuntimeException("E404007: 존재하지 않는 관리자입니다."));
.orElseThrow(() -> new CustomException(AdminErrorCode.ADMIN_NOT_FOUND));

admin.updateLastLogin(LocalDateTime.now());
adminRepository.save(admin);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
package io.f1.backend.domain.game.app;

import io.f1.backend.domain.game.dto.GameStartData;
import static io.f1.backend.domain.quiz.mapper.QuizMapper.toGameStartResponse;

import io.f1.backend.domain.game.dto.request.GameStartRequest;
import io.f1.backend.domain.game.dto.response.GameStartResponse;
import io.f1.backend.domain.game.event.RoomUpdatedEvent;
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.RoomState;
import io.f1.backend.domain.game.store.RoomRepository;
import io.f1.backend.domain.question.entity.Question;
import io.f1.backend.domain.quiz.app.QuizService;
import io.f1.backend.domain.quiz.entity.Quiz;
import io.f1.backend.global.exception.CustomException;
import io.f1.backend.global.exception.errorcode.GameErrorCode;
import io.f1.backend.global.exception.errorcode.RoomErrorCode;

import lombok.RequiredArgsConstructor;

import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Map;

@Service
Expand All @@ -26,15 +33,17 @@ public class GameService {
private final RoomRepository roomRepository;
private final ApplicationEventPublisher eventPublisher;

public GameStartData gameStart(Long roomId, Long quizId) {
public GameStartResponse gameStart(Long roomId, GameStartRequest gameStartRequest) {

Long quizId = gameStartRequest.quizId();

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

if (!validateReadyStatus(room)) {
throw new IllegalArgumentException("E403004 : 레디 상태가 아닙니다.");
throw new CustomException(RoomErrorCode.PLAYER_NOT_READY);
}

// 방의 gameSetting에 설정된 퀴즈랑 요청 퀴즈랑 같은지 체크 후 GameSetting에서 라운드 가져오기
Expand All @@ -43,22 +52,25 @@ public GameStartData gameStart(Long roomId, Long quizId) {
Quiz quiz = quizService.getQuizWithQuestionsById(quizId);

// 라운드 수만큼 랜덤 Question 추출
GameStartResponse questions = quizService.getRandomQuestionsWithoutAnswer(quizId, round);
List<Question> questions = quizService.getRandomQuestionsWithoutAnswer(quizId, round);
room.updateQuestions(questions);

GameStartResponse gameStartResponse = toGameStartResponse(questions);

// 방 정보 게임 중으로 변경
room.updateRoomState(RoomState.PLAYING);

eventPublisher.publishEvent(new RoomUpdatedEvent(room, quiz));

return new GameStartData(getDestination(roomId), questions);
return gameStartResponse;
}

private Integer checkGameSetting(Room room, Long quizId) {

GameSetting gameSetting = room.getGameSetting();

if (!gameSetting.checkQuizId(quizId)) {
throw new IllegalArgumentException("E409002 : 게임 설정이 다릅니다. (게임을 시작할 수 없습니다.)");
if (!gameSetting.validateQuizId(quizId)) {
throw new CustomException(GameErrorCode.GAME_SETTING_CONFLICT);
}

return gameSetting.getRound();
Expand All @@ -70,8 +82,4 @@ private boolean validateReadyStatus(Room room) {

return playerSessionMap.values().stream().allMatch(Player::isReady);
}

private static String getDestination(Long roomId) {
return "/sub/room/" + roomId;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,19 @@
import static io.f1.backend.domain.game.mapper.RoomMapper.toGameSetting;
import static io.f1.backend.domain.game.mapper.RoomMapper.toGameSettingResponse;
import static io.f1.backend.domain.game.mapper.RoomMapper.toPlayerListResponse;
import static io.f1.backend.domain.game.mapper.RoomMapper.toQuestionResultResponse;
import static io.f1.backend.domain.game.mapper.RoomMapper.toRankUpdateResponse;
import static io.f1.backend.domain.game.mapper.RoomMapper.toRoomResponse;
import static io.f1.backend.domain.game.mapper.RoomMapper.toRoomSetting;
import static io.f1.backend.domain.game.mapper.RoomMapper.toRoomSettingResponse;
import static io.f1.backend.global.util.SecurityUtils.getCurrentUserId;
import static io.f1.backend.global.util.SecurityUtils.getCurrentUserNickname;

import io.f1.backend.domain.game.dto.ChatMessage;
import io.f1.backend.domain.game.dto.RoomEventType;
import io.f1.backend.domain.game.dto.RoomExitData;
import io.f1.backend.domain.game.dto.RoomInitialData;
import io.f1.backend.domain.game.dto.RoundResult;
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;
Expand All @@ -22,15 +26,17 @@
import io.f1.backend.domain.game.dto.response.RoomResponse;
import io.f1.backend.domain.game.dto.response.RoomSettingResponse;
import io.f1.backend.domain.game.dto.response.SystemNoticeResponse;
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.question.entity.Question;
import io.f1.backend.domain.quiz.app.QuizService;
import io.f1.backend.domain.quiz.dto.QuizMinData;
import io.f1.backend.domain.quiz.entity.Quiz;
import io.f1.backend.domain.user.dto.UserPrincipal;
import io.f1.backend.global.exception.CustomException;
import io.f1.backend.global.exception.errorcode.RoomErrorCode;

Expand Down Expand Up @@ -60,10 +66,10 @@ public class RoomService {

public RoomCreateResponse saveRoom(RoomCreateRequest request) {

Long quizMinId = quizService.getQuizMinId();
Quiz quiz = quizService.getQuizWithQuestionsById(quizMinId);
QuizMinData quizMinData = quizService.getQuizMinData();
// Quiz quiz = quizService.getQuizWithQuestionsById(quizMinId);

GameSetting gameSetting = toGameSetting(quiz);
GameSetting gameSetting = toGameSetting(quizMinData);

Player host = createPlayer();

Expand All @@ -77,7 +83,7 @@ public RoomCreateResponse saveRoom(RoomCreateRequest request) {

roomRepository.saveRoom(room);

eventPublisher.publishEvent(new RoomCreatedEvent(room, quiz));
// eventPublisher.publishEvent(new RoomCreatedEvent(room, quiz));

return new RoomCreateResponse(newId);
}
Expand Down Expand Up @@ -110,11 +116,12 @@ public void enterRoom(RoomValidationRequest request) {
}
}

public RoomInitialData initializeRoomSocket(Long roomId, String sessionId) {
public RoomInitialData initializeRoomSocket(
Long roomId, String sessionId, UserPrincipal principal) {

Room room = findRoom(roomId);

Player player = createPlayer();
Player player = createPlayer(principal);

Map<String, Player> playerSessionMap = room.getPlayerSessionMap();
Map<Long, String> userIdSessionMap = room.getUserIdSessionMap();
Expand All @@ -140,30 +147,25 @@ public RoomInitialData initializeRoomSocket(Long roomId, String sessionId) {

PlayerListResponse playerListResponse = toPlayerListResponse(room);

SystemNoticeResponse systemNoticeResponse = ofPlayerEvent(player, RoomEventType.ENTER);
SystemNoticeResponse systemNoticeResponse =
ofPlayerEvent(player.getNickname(), RoomEventType.ENTER);

return new RoomInitialData(
getDestination(roomId),
roomSettingResponse,
gameSettingResponse,
playerListResponse,
systemNoticeResponse);
roomSettingResponse, gameSettingResponse, playerListResponse, systemNoticeResponse);
}

public RoomExitData exitRoom(Long roomId, String sessionId) {
public RoomExitData exitRoom(Long roomId, String sessionId, UserPrincipal principal) {

Object lock = roomLocks.computeIfAbsent(roomId, k -> new Object());

synchronized (lock) {
Room room = findRoom(roomId);

String destination = getDestination(roomId);

Player removePlayer = getRemovePlayer(room, sessionId);
Player removePlayer = getRemovePlayer(room, sessionId, principal);

/* 방 삭제 */
if (isLastPlayer(room, sessionId)) {
return removeRoom(room, destination);
return removeRoom(room);
}

/* 방장 변경 */
Expand All @@ -175,14 +177,27 @@ public RoomExitData exitRoom(Long roomId, String sessionId) {
removePlayer(room, sessionId, removePlayer);

SystemNoticeResponse systemNoticeResponse =
ofPlayerEvent(removePlayer, RoomEventType.EXIT);
ofPlayerEvent(removePlayer.nickname, RoomEventType.EXIT);

PlayerListResponse playerListResponse = toPlayerListResponse(room);

return new RoomExitData(destination, playerListResponse, systemNoticeResponse, false);
return new RoomExitData(playerListResponse, systemNoticeResponse, false);
}
}

public PlayerListResponse handlePlayerReady(Long roomId, String sessionId) {
Player player =
roomRepository
.findPlayerInRoomBySessionId(roomId, sessionId)
.orElseThrow(() -> new CustomException(RoomErrorCode.PLAYER_NOT_FOUND));

player.toggleReady();

Room room = findRoom(roomId);

return toPlayerListResponse(room);
}

public RoomListResponse getAllRooms() {
List<Room> rooms = roomRepository.findAll();
List<RoomResponse> roomResponses =
Expand All @@ -198,17 +213,44 @@ public RoomListResponse getAllRooms() {
return new RoomListResponse(roomResponses);
}

private Player getRemovePlayer(Room room, String sessionId) {
// todo 동시성적용
public RoundResult chat(Long roomId, String sessionId, ChatMessage chatMessage) {
Room room = findRoom(roomId);

if (!room.isPlaying()) {
return buildResultOnlyChat(chatMessage);
}

Question currentQuestion = room.getCurrentQuestion();

String answer = currentQuestion.getAnswer();

if (!answer.equals(chatMessage.message())) {
return buildResultOnlyChat(chatMessage);
}

room.increasePlayerCorrectCount(sessionId);

return RoundResult.builder()
.questionResult(
toQuestionResultResponse(currentQuestion.getId(), chatMessage, answer))
.rankUpdate(toRankUpdateResponse(room))
.systemNotice(ofPlayerEvent(chatMessage.nickname(), RoomEventType.ENTER))
.chat(chatMessage)
.build();
}

private Player getRemovePlayer(Room room, String sessionId, UserPrincipal principal) {
Player removePlayer = room.getPlayerSessionMap().get(sessionId);
if (removePlayer == null) {
room.removeUserId(getCurrentUserId());
room.removeUserId(principal.getUserId());
throw new CustomException(RoomErrorCode.SOCKET_SESSION_NOT_FOUND);
}
return removePlayer;
}

private static String getDestination(Long roomId) {
return "/sub/room/" + roomId;
private Player createPlayer(UserPrincipal principal) {
return new Player(principal.getUserId(), principal.getUserNickname());
}

private Player createPlayer() {
Expand All @@ -226,12 +268,12 @@ private boolean isLastPlayer(Room room, String sessionId) {
return playerSessionMap.size() == 1 && playerSessionMap.containsKey(sessionId);
}

private RoomExitData removeRoom(Room room, String destination) {
private RoomExitData removeRoom(Room room) {
Long roomId = room.getId();
roomRepository.removeRoom(roomId);
roomLocks.remove(roomId);
log.info("{}번 방 삭제", roomId);
return RoomExitData.builder().destination(destination).removedRoom(true).build();
return RoomExitData.builder().removedRoom(true).build();
}

private void changeHost(Room room, String hostSessionId) {
Expand All @@ -255,4 +297,8 @@ private void removePlayer(Room room, String sessionId, Player removePlayer) {
room.removeUserId(removePlayer.getId());
room.removeSessionId(sessionId);
}

private RoundResult buildResultOnlyChat(ChatMessage chatMessage) {
return RoundResult.builder().chat(chatMessage).build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package io.f1.backend.domain.game.dto;

import java.time.Instant;

public record ChatMessage(String nickname, String message, Instant timestamp) {}
Loading