diff --git a/src/docs/asciidoc/api.adoc b/src/docs/asciidoc/api.adoc index 857329d9..496b848c 100644 --- a/src/docs/asciidoc/api.adoc +++ b/src/docs/asciidoc/api.adoc @@ -38,6 +38,10 @@ operation::game-query-controller-test/응원_횟수를_조회한다[snippets='ht operation::game-query-controller-test/라인업을_조회한다[snippets='http-request,path-parameters,http-response,response-fields'] +=== 게임 등록 + +operation::game-controller-test/경기를_등록한다[snippets='http-request,path-parameters,http-response'] + == 라인업 API === 라인업 선수 선발로 변경 diff --git a/src/main/java/com/sports/server/auth/config/SecurityConfig.java b/src/main/java/com/sports/server/auth/config/SecurityConfig.java index 2fa1807d..e25f9ceb 100644 --- a/src/main/java/com/sports/server/auth/config/SecurityConfig.java +++ b/src/main/java/com/sports/server/auth/config/SecurityConfig.java @@ -49,6 +49,7 @@ SecurityFilterChain filterChain(HttpSecurity http, MvcRequestMatcher.Builder mvc mvc.pattern(HttpMethod.PUT, "/leagues/{leagueId}/teams/{teamId}"), mvc.pattern(HttpMethod.DELETE, "/leagues/{leagueId}/teams/{teamId}"), mvc.pattern(HttpMethod.POST, "/leagues/{leagueId}/teams/{teamId}/delete-logo"), + mvc.pattern(HttpMethod.POST, "/leagues/{leagueId}/games"), mvc.pattern(HttpMethod.POST, "/games/*/timelines/**") ) .authenticated() diff --git a/src/main/java/com/sports/server/command/game/application/GameService.java b/src/main/java/com/sports/server/command/game/application/GameService.java new file mode 100644 index 00000000..e20353e6 --- /dev/null +++ b/src/main/java/com/sports/server/command/game/application/GameService.java @@ -0,0 +1,72 @@ +package com.sports.server.command.game.application; + +import com.sports.server.auth.exception.AuthorizationErrorMessages; +import com.sports.server.command.game.domain.Game; +import com.sports.server.command.game.domain.GameRepository; +import com.sports.server.command.game.domain.GameTeam; +import com.sports.server.command.game.dto.GameRequestDto; +import com.sports.server.command.league.domain.League; +import com.sports.server.command.leagueteam.domain.LeagueTeam; +import com.sports.server.command.member.domain.Member; +import com.sports.server.command.sport.domain.Sport; +import com.sports.server.command.sport.domain.SportRepository; +import com.sports.server.common.application.EntityUtils; +import com.sports.server.common.exception.NotFoundException; +import com.sports.server.common.exception.UnauthorizedException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class GameService { + + private final EntityUtils entityUtils; + private final GameRepository gameRepository; + private final SportRepository sportRepository; + private static final String NAME_OF_SPORT = "축구"; + + @Transactional + public void register(final Long leagueId, + final GameRequestDto.Register requestDto, + final Member manager) { + Game game = saveGame(leagueId, manager, requestDto); + saveGameTeams(game, requestDto); + } + + private void saveGameTeams(Game game, GameRequestDto.Register requestDto) { + + LeagueTeam leagueTeam1 = entityUtils.getEntity(requestDto.idOfTeam1(), LeagueTeam.class); + LeagueTeam leagueTeam2 = entityUtils.getEntity(requestDto.idOfTeam2(), LeagueTeam.class); + + GameTeam gameTeam1 = new GameTeam(game, leagueTeam1); + GameTeam gameTeam2 = new GameTeam(game, leagueTeam2); + + game.addTeam(gameTeam1); + game.addTeam(gameTeam2); + + leagueTeam1.getLeagueTeamPlayers().stream() + .forEach(gameTeam1::registerLineup); + leagueTeam2.getLeagueTeamPlayers().stream() + .forEach(gameTeam2::registerLineup); + } + + private Game saveGame(Long leagueId, Member manager, GameRequestDto.Register requestDto) { + Sport sport = sportRepository.findByName(NAME_OF_SPORT) + .orElseThrow(() -> new NotFoundException("해당 이름을 가진 스포츠가 존재하지 않습니다.")); + League league = getLeagueAndCheckPermission(leagueId, manager); + Game game = requestDto.toEntity(sport, manager, league); + gameRepository.save(game); + return game; + } + + private League getLeagueAndCheckPermission(final Long leagueId, final Member manager) { + League league = entityUtils.getEntity(leagueId, League.class); + + if (!league.isManagedBy(manager)) { + throw new UnauthorizedException(AuthorizationErrorMessages.PERMISSION_DENIED); + } + + return league; + } +} \ No newline at end of file diff --git a/src/main/java/com/sports/server/command/game/domain/Game.java b/src/main/java/com/sports/server/command/game/domain/Game.java index 883d6787..5f4a6187 100644 --- a/src/main/java/com/sports/server/command/game/domain/Game.java +++ b/src/main/java/com/sports/server/command/game/domain/Game.java @@ -5,6 +5,7 @@ import com.sports.server.command.member.domain.Member; import com.sports.server.command.sport.domain.Sport; import com.sports.server.common.domain.BaseEntity; +import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; @@ -40,7 +41,7 @@ public class Game extends BaseEntity { @JoinColumn(name = "league_id") private League league; - @OneToMany(mappedBy = "game") + @OneToMany(mappedBy = "game", cascade = CascadeType.ALL, orphanRemoval = true) private List teams = new ArrayList<>(); @Column(name = "name", nullable = false) @@ -110,4 +111,17 @@ public void cancelScore(LineupPlayer scorer) { scoredTeam.cancelScore(); } + + public Game(Sport sport, Member manager, League league, String name, LocalDateTime startTime, + String videoId, String gameQuarter, GameState state, Round round) { + this.sport = sport; + this.manager = manager; + this.league = league; + this.name = name; + this.startTime = startTime; + this.videoId = videoId; + this.gameQuarter = gameQuarter; + this.state = state; + this.round = round; + } } diff --git a/src/main/java/com/sports/server/command/game/domain/GameRepository.java b/src/main/java/com/sports/server/command/game/domain/GameRepository.java new file mode 100644 index 00000000..df05d2ed --- /dev/null +++ b/src/main/java/com/sports/server/command/game/domain/GameRepository.java @@ -0,0 +1,7 @@ +package com.sports.server.command.game.domain; + +import org.springframework.data.repository.Repository; + +public interface GameRepository extends Repository { + Game save(Game game); +} diff --git a/src/main/java/com/sports/server/command/game/domain/GameTeam.java b/src/main/java/com/sports/server/command/game/domain/GameTeam.java index a7694c61..8f465d15 100644 --- a/src/main/java/com/sports/server/command/game/domain/GameTeam.java +++ b/src/main/java/com/sports/server/command/game/domain/GameTeam.java @@ -1,12 +1,10 @@ package com.sports.server.command.game.domain; -import java.util.ArrayList; -import java.util.List; - import com.sports.server.command.leagueteam.domain.LeagueTeam; +import com.sports.server.command.leagueteam.domain.LeagueTeamPlayer; import com.sports.server.common.domain.BaseEntity; import com.sports.server.common.exception.CustomException; - +import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; @@ -14,10 +12,11 @@ import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToMany; import jakarta.persistence.Table; +import java.util.ArrayList; +import java.util.List; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; - import org.springframework.http.HttpStatus; @Entity @@ -39,7 +38,7 @@ public class GameTeam extends BaseEntity { @JoinColumn(name = "league_team_id") private LeagueTeam leagueTeam; - @OneToMany(mappedBy = "gameTeam") + @OneToMany(mappedBy = "gameTeam", cascade = CascadeType.ALL, orphanRemoval = true) private List lineupPlayers = new ArrayList<>(); @Column(name = "cheer_count", nullable = false) @@ -84,6 +83,25 @@ public void cancelScore() { this.score -= SCORE_VALUE; } } + + public GameTeam(Game game, LeagueTeam leagueTeam) { + this.game = game; + this.leagueTeam = leagueTeam; + this.cheerCount = 0; + this.score = 0; + } + + public void registerLineup(LeagueTeamPlayer player) { + LineupPlayer lineupPlayer = new LineupPlayer( + this, + player.getId(), + player.getName(), + player.getNumber(), + false, + LineupPlayerState.CANDIDATE); + + this.lineupPlayers.add(lineupPlayer); + } } diff --git a/src/main/java/com/sports/server/command/game/domain/LineupPlayer.java b/src/main/java/com/sports/server/command/game/domain/LineupPlayer.java index 4a59694c..ac945581 100644 --- a/src/main/java/com/sports/server/command/game/domain/LineupPlayer.java +++ b/src/main/java/com/sports/server/command/game/domain/LineupPlayer.java @@ -1,12 +1,10 @@ package com.sports.server.command.game.domain; -import static com.sports.server.command.game.domain.LineupPlayerState.*; - -import org.springframework.http.HttpStatus; +import static com.sports.server.command.game.domain.LineupPlayerState.CANDIDATE; +import static com.sports.server.command.game.domain.LineupPlayerState.STARTER; import com.sports.server.common.domain.BaseEntity; import com.sports.server.common.exception.CustomException; - import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; @@ -15,11 +13,11 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; +import java.util.Objects; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; - -import java.util.Objects; +import org.springframework.http.HttpStatus; @Entity @Getter @@ -71,4 +69,14 @@ public boolean isSameTeam(LineupPlayer other) { public boolean isInTeam(GameTeam team) { return Objects.equals(this.gameTeam, team); } + + public LineupPlayer(GameTeam gameTeam, Long leagueTeamPlayerId, String name, int number, + boolean isCaptain, LineupPlayerState state) { + this.gameTeam = gameTeam; + this.leagueTeamPlayerId = leagueTeamPlayerId; + this.name = name; + this.number = number; + this.isCaptain = isCaptain; + this.state = state; + } } diff --git a/src/main/java/com/sports/server/command/game/dto/GameRequestDto.java b/src/main/java/com/sports/server/command/game/dto/GameRequestDto.java new file mode 100644 index 00000000..61f45c07 --- /dev/null +++ b/src/main/java/com/sports/server/command/game/dto/GameRequestDto.java @@ -0,0 +1,27 @@ +package com.sports.server.command.game.dto; + +import com.sports.server.command.game.domain.Game; +import com.sports.server.command.game.domain.GameState; +import com.sports.server.command.league.domain.League; +import com.sports.server.command.league.domain.Round; +import com.sports.server.command.member.domain.Member; +import com.sports.server.command.sport.domain.Sport; +import java.time.LocalDateTime; + +public class GameRequestDto { + public record Register( + String name, + String round, + String quarter, + String state, + LocalDateTime startTime, + Long idOfTeam1, + Long idOfTeam2, + String videoId + ) { + public Game toEntity(Sport sport, Member manager, League league) { + return new Game(sport, manager, league, name, startTime, videoId, quarter, GameState.from(state), + Round.from(round)); + } + } +} diff --git a/src/main/java/com/sports/server/command/game/presentation/GameController.java b/src/main/java/com/sports/server/command/game/presentation/GameController.java index 28b0576f..2184788a 100644 --- a/src/main/java/com/sports/server/command/game/presentation/GameController.java +++ b/src/main/java/com/sports/server/command/game/presentation/GameController.java @@ -1,44 +1,55 @@ package com.sports.server.command.game.presentation; +import com.sports.server.command.game.application.GameService; import com.sports.server.command.game.application.GameTeamService; import com.sports.server.command.game.application.LineupPlayerService; import com.sports.server.command.game.dto.CheerCountUpdateRequest; +import com.sports.server.command.game.dto.GameRequestDto; +import com.sports.server.command.member.domain.Member; import jakarta.validation.Valid; +import java.net.URI; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequiredArgsConstructor -@RequestMapping("/games") public class GameController { - private final GameTeamService gameTeamService; - private final LineupPlayerService lineupPlayerService; + private final GameTeamService gameTeamService; + private final LineupPlayerService lineupPlayerService; + private final GameService gameService; - @PostMapping("/{gameId}/cheer") - public ResponseEntity updateCheerCount(@PathVariable final Long gameId, - @RequestBody @Valid CheerCountUpdateRequest cheerRequestDto) { - gameTeamService.updateCheerCount(gameId, cheerRequestDto); - return ResponseEntity.ok().build(); - } + @PostMapping("/games/{gameId}/cheer") + public ResponseEntity updateCheerCount(@PathVariable final Long gameId, + @RequestBody @Valid CheerCountUpdateRequest cheerRequestDto) { + gameTeamService.updateCheerCount(gameId, cheerRequestDto); + return ResponseEntity.ok().build(); + } - @PatchMapping("/{gameId}/lineup-players/{lineupPlayerId}/starter") - public ResponseEntity changePlayerStateToStarter(@PathVariable final Long gameId, - @PathVariable final Long lineupPlayerId) { - lineupPlayerService.changePlayerStateToStarter(gameId, lineupPlayerId); - return ResponseEntity.ok().build(); - } + @PatchMapping("/games/{gameId}/lineup-players/{lineupPlayerId}/starter") + public ResponseEntity changePlayerStateToStarter(@PathVariable final Long gameId, + @PathVariable final Long lineupPlayerId) { + lineupPlayerService.changePlayerStateToStarter(gameId, lineupPlayerId); + return ResponseEntity.ok().build(); + } - @PatchMapping("/{gameId}/lineup-players/{lineupPlayerId}/candidate") - public ResponseEntity changePlayerStateToCandidate(@PathVariable final Long gameId, - @PathVariable final Long lineupPlayerId) { - lineupPlayerService.changePlayerStateToCandidate(gameId, lineupPlayerId); - return ResponseEntity.ok().build(); - } + @PatchMapping("/games/{gameId}/lineup-players/{lineupPlayerId}/candidate") + public ResponseEntity changePlayerStateToCandidate(@PathVariable final Long gameId, + @PathVariable final Long lineupPlayerId) { + lineupPlayerService.changePlayerStateToCandidate(gameId, lineupPlayerId); + return ResponseEntity.ok().build(); + } + + @PostMapping("/leagues/{leagueId}/games") + public ResponseEntity registerGame(@PathVariable final Long leagueId, + @RequestBody final GameRequestDto.Register requestDto, + final Member member) { + gameService.register(leagueId, requestDto, member); + return ResponseEntity.created(URI.create("")).build(); + } } diff --git a/src/main/java/com/sports/server/command/leagueteam/domain/LeagueTeamPlayerFixtureRepository.java b/src/main/java/com/sports/server/command/leagueteam/domain/LeagueTeamPlayerFixtureRepository.java deleted file mode 100644 index b8066fc2..00000000 --- a/src/main/java/com/sports/server/command/leagueteam/domain/LeagueTeamPlayerFixtureRepository.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.sports.server.command.leagueteam.domain; - -import java.util.Optional; -import org.springframework.data.repository.Repository; - -public interface LeagueTeamPlayerFixtureRepository extends Repository { - Optional findById(Long id); -} diff --git a/src/main/java/com/sports/server/command/sport/domain/SportRepository.java b/src/main/java/com/sports/server/command/sport/domain/SportRepository.java new file mode 100644 index 00000000..a4194987 --- /dev/null +++ b/src/main/java/com/sports/server/command/sport/domain/SportRepository.java @@ -0,0 +1,8 @@ +package com.sports.server.command.sport.domain; + +import java.util.Optional; +import org.springframework.data.repository.Repository; + +public interface SportRepository extends Repository { + Optional findByName(String name); +} diff --git a/src/main/resources/static/docs/api.html b/src/main/resources/static/docs/api.html index d82b15bc..d4c71b2a 100644 --- a/src/main/resources/static/docs/api.html +++ b/src/main/resources/static/docs/api.html @@ -460,6 +460,7 @@

훕치치 서버 API 문서

  • 게임 목록 조회
  • 게임 응원 횟수 조회
  • 게임 라인업 조회
  • +
  • 게임 등록
  • 라인업 API @@ -489,7 +490,6 @@

    훕치치 서버 API 문서

  • 리그팀 삭제
  • 리그 상세 조회
  • 리그팀 상세 조회
  • -
  • 리그 삭제
  • 타임라인 API @@ -1388,6 +1388,66 @@

    +

    게임 등록

    +
    +

    HTTP request

    +
    +
    +
    POST /leagues/1/games HTTP/1.1
    +Content-Type: application/json
    +Content-Length: 212
    +Host: www.api.hufstreaming.site
    +Cookie: HCC_SES=temp-cookie
    +
    +{
    +  "name" : "경기 이름",
    +  "round" : "16강",
    +  "quarter" : "경기전",
    +  "state" : "SCHEDULED",
    +  "startTime" : "2024-08-07T22:37:29.125287",
    +  "idOfTeam1" : 1,
    +  "idOfTeam2" : 2,
    +  "videoId" : "videoId"
    +}
    +
    +
    +
    +
    +

    Path parameters

    + + ++++ + + + + + + + + + + + + +
    Table 1. /leagues/{leagueId}/games
    ParameterDescription

    leagueId

    리그의 ID

    +
    +
    +

    HTTP response

    +
    +
    +
    HTTP/1.1 201 Created
    +Vary: Origin
    +Vary: Access-Control-Request-Method
    +Vary: Access-Control-Request-Headers
    +Location:
    +
    +
    +
    + @@ -2978,7 +3038,7 @@

    response = RestAssured.given().log().all() - .when() - .contentType(MediaType.APPLICATION_JSON_VALUE) - .get("/games/{gameId}/lineup", gameId) - .then().log().all() - .extract(); + .when() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .get("/games/{gameId}/lineup", gameId) + .then().log().all() + .extract(); // then List lineupPlayerResponses = toResponses(response, LineupPlayerResponse.class).stream() - .filter(lineupPlayerResponse -> lineupPlayerResponse.gameTeamId().equals(gameTeamId)) - .toList(); + .filter(lineupPlayerResponse -> lineupPlayerResponse.gameTeamId().equals(gameTeamId)) + .toList(); List actual = lineupPlayerResponses.get(0).gameTeamPlayers().stream() - .filter(playerResponse -> playerResponse.id().equals(lineupPlayerId)) - .toList(); + .filter(playerResponse -> playerResponse.id().equals(lineupPlayerId)) + .toList(); assertAll( - () -> assertThat(lineupPlayerResponses.get(0).gameTeamId().equals(gameTeamId)), - () -> assertThat(actual.get(0).id().equals(lineupPlayerId)), - () -> assertThat(actual.get(0).state().equals(LineupPlayerState.STARTER)) + () -> assertThat(lineupPlayerResponses.get(0).gameTeamId().equals(gameTeamId)), + () -> assertThat(actual.get(0).id().equals(lineupPlayerId)), + () -> assertThat(actual.get(0).state().equals(LineupPlayerState.STARTER)) ); } @@ -103,32 +105,58 @@ public class GameAcceptanceTest extends AcceptanceTest { // when RestAssured.given().log().all() - .when() - .contentType(MediaType.APPLICATION_JSON_VALUE) - .patch("/games/{gameId}/lineup-players/{lineupPlayerId}/starter", gameId, lineupPlayerId) - .then().log().all() - .extract(); + .when() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .patch("/games/{gameId}/lineup-players/{lineupPlayerId}/starter", gameId, lineupPlayerId) + .then().log().all() + .extract(); ExtractableResponse response = RestAssured.given().log().all() - .when() - .contentType(MediaType.APPLICATION_JSON_VALUE) - .get("/games/{gameId}/lineup", gameId) - .then().log().all() - .extract(); + .when() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .get("/games/{gameId}/lineup", gameId) + .then().log().all() + .extract(); // then List lineupPlayerResponses = toResponses(response, LineupPlayerResponse.class).stream() - .filter(lineupPlayerResponse -> lineupPlayerResponse.gameTeamId().equals(gameTeamId)) - .toList(); + .filter(lineupPlayerResponse -> lineupPlayerResponse.gameTeamId().equals(gameTeamId)) + .toList(); List actual = lineupPlayerResponses.get(0).gameTeamPlayers().stream() - .filter(playerResponse -> playerResponse.id().equals(lineupPlayerId)) - .toList(); + .filter(playerResponse -> playerResponse.id().equals(lineupPlayerId)) + .toList(); assertAll( - () -> assertThat(lineupPlayerResponses.get(0).gameTeamId().equals(gameTeamId)), - () -> assertThat(actual.get(0).id().equals(lineupPlayerId)), - () -> assertThat(actual.get(0).state().equals(LineupPlayerState.CANDIDATE)) + () -> assertThat(lineupPlayerResponses.get(0).gameTeamId().equals(gameTeamId)), + () -> assertThat(actual.get(0).id().equals(lineupPlayerId)), + () -> assertThat(actual.get(0).state().equals(LineupPlayerState.CANDIDATE)) ); } + + @Test + void 새로운_경기를_등록한다() { + + //given + Long leagueId = 1L; + Long idOfTeam1 = 1L; + Long idOfTeam2 = 2L; + GameRequestDto.Register requestDto = new GameRequestDto.Register("경기 이름", "16강", "전반전", "SCHEDULED", + LocalDateTime.now(), idOfTeam1, idOfTeam2, null); + + configureMockJwtForEmail("john.doe@example.com"); + + // when + ExtractableResponse response = RestAssured.given().log().all() + .cookie(COOKIE_NAME, mockToken) + .pathParam("leagueId", leagueId) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(requestDto) + .post("/leagues/{leagueId}/games", leagueId) + .then().log().all() + .extract(); + + // then + AssertionsForClassTypes.assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value()); + } } diff --git a/src/test/java/com/sports/server/command/game/application/GameServiceTest.java b/src/test/java/com/sports/server/command/game/application/GameServiceTest.java new file mode 100644 index 00000000..b13765dc --- /dev/null +++ b/src/test/java/com/sports/server/command/game/application/GameServiceTest.java @@ -0,0 +1,140 @@ +package com.sports.server.command.game.application; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.sports.server.command.game.domain.Game; +import com.sports.server.command.game.domain.GameTeam; +import com.sports.server.command.game.domain.LineupPlayer; +import com.sports.server.command.game.dto.GameRequestDto; +import com.sports.server.command.league.domain.Round; +import com.sports.server.command.leagueteam.domain.LeagueTeam; +import com.sports.server.command.leagueteam.domain.LeagueTeamPlayer; +import com.sports.server.command.member.domain.Member; +import com.sports.server.common.application.EntityUtils; +import com.sports.server.common.exception.UnauthorizedException; +import com.sports.server.support.ServiceTest; +import com.sports.server.support.fixture.GameFixtureRepository; +import com.sports.server.support.fixture.GameTeamFixtureRepository; +import com.sports.server.support.fixture.LeagueTeamPlayerFixtureRepository; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.jdbc.Sql; + +@Sql("/game-fixture.sql") +public class GameServiceTest extends ServiceTest { + + @Autowired + private GameService gameService; + + @Autowired + private EntityUtils entityUtils; + + @Autowired + private GameFixtureRepository gameFixtureRepository; + + @Autowired + private GameTeamFixtureRepository gameTeamFixtureRepository; + + @Autowired + private LeagueTeamPlayerFixtureRepository leagueTeamPlayerFixtureRepository; + + private GameRequestDto.Register requestDto; + private String nameOfGame; + private Long idOfTeam1; + private Long idOfTeam2; + + @BeforeEach + void setUp() { + this.nameOfGame = "경기 이름"; + this.idOfTeam1 = 1L; + this.idOfTeam2 = 2L; + this.requestDto = new GameRequestDto.Register(nameOfGame, "16강", "전반전", "SCHEDULED", + LocalDateTime.now(), idOfTeam1, idOfTeam2, null); + } + + @Nested + @DisplayName("게임을 저장할 때") + class registerGameTest { + @Test + void 정상적으로_게임팀이_저장된다() { + // given + Member manager = entityUtils.getEntity(1L, Member.class); + + // when + gameService.register(1L, requestDto, manager); + + // then + Optional gameOptional = gameFixtureRepository.findByName(nameOfGame); + assertThat(gameOptional).isPresent().withFailMessage("게임이 등록되지 않았습니다."); + + Game game = gameOptional.get(); + assertInFormationOfGame(game); + + List gameTeams = gameTeamFixtureRepository.findByGame(game); + assertGameTeams(gameTeams); + } + + @Test + void 정상적으로_라인업이_복사된다() { + // given + Member manager = entityUtils.getEntity(1L, Member.class); + + // when + gameService.register(1L, requestDto, manager); + + // then + Optional gameOptional = gameFixtureRepository.findByName(nameOfGame); + assertThat(gameOptional).isPresent().withFailMessage("게임이 등록되지 않았습니다."); + + List gameTeams = gameTeamFixtureRepository.findByGame(gameOptional.get()); + assertLineupPlayers(gameTeams); + } + + private void assertInFormationOfGame(Game game) { + assertAll( + () -> assertThat(game).isNotNull(), + () -> assertThat(game.getName()).isEqualTo(nameOfGame), + () -> assertThat(game.getRound()).isEqualTo(Round.from("16강")) + ); + + } + + private void assertGameTeams(List gameTeams) { + List expectedTeamIds = List.of(idOfTeam1, idOfTeam2); + List actualTeamIds = gameTeams.stream().map(gt -> gt.getLeagueTeam().getId()).toList(); + assertThat(actualTeamIds).isEqualTo(expectedTeamIds); + } + + private void assertLineupPlayers(List gameTeams) { + for (GameTeam gameTeam : gameTeams) { + LeagueTeam leagueTeam = gameTeam.getLeagueTeam(); + List expectedPlayerIds = leagueTeamPlayerFixtureRepository.findByLeagueTeam(leagueTeam).stream() + .map(LeagueTeamPlayer::getId).toList(); + List actualPlayerIds = gameTeam.getLineupPlayers().stream() + .map(LineupPlayer::getLeagueTeamPlayerId).toList(); + assertThat(actualPlayerIds).isEqualTo(expectedPlayerIds); + } + } + + @Test + void 리그의_매니저가_아닌_회원이_리그팀을_등록하려고_하면_예외가_발생한다() { + // given + Long leagueId = 1L; + Member nonManager = entityUtils.getEntity(2L, Member.class); + + // when & then + assertThatThrownBy(() -> gameService.register(leagueId, requestDto, nonManager)) + .isInstanceOf(UnauthorizedException.class); + } + } + + +} diff --git a/src/test/java/com/sports/server/command/game/presentation/GameControllerTest.java b/src/test/java/com/sports/server/command/game/presentation/GameControllerTest.java index 665b0cdf..42ac3b55 100644 --- a/src/test/java/com/sports/server/command/game/presentation/GameControllerTest.java +++ b/src/test/java/com/sports/server/command/game/presentation/GameControllerTest.java @@ -1,7 +1,10 @@ package com.sports.server.command.game.presentation; -import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*; +import static org.springframework.restdocs.cookies.CookieDocumentation.cookieWithName; +import static org.springframework.restdocs.cookies.CookieDocumentation.requestCookies; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.patch; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; @@ -9,7 +12,10 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.sports.server.command.game.dto.CheerCountUpdateRequest; +import com.sports.server.command.game.dto.GameRequestDto; import com.sports.server.support.DocumentationTest; +import jakarta.servlet.http.Cookie; +import java.time.LocalDateTime; import org.junit.jupiter.api.Test; import org.springframework.http.MediaType; import org.springframework.restdocs.payload.JsonFieldType; @@ -53,18 +59,18 @@ public class GameControllerTest extends DocumentationTest { //when ResultActions result = mockMvc.perform( - patch("/games/{gameId}/lineup-players/{lineupPlayerId}/starter", gameId, lineupPlayerId) - .contentType(MediaType.APPLICATION_JSON) + patch("/games/{gameId}/lineup-players/{lineupPlayerId}/starter", gameId, lineupPlayerId) + .contentType(MediaType.APPLICATION_JSON) ); //then result.andExpect((status().isOk())) - .andDo(restDocsHandler.document( - pathParameters( - parameterWithName("gameId").description("게임의 ID"), - parameterWithName("lineupPlayerId").description("라인업 선수의 ID") - ) - )); + .andDo(restDocsHandler.document( + pathParameters( + parameterWithName("gameId").description("게임의 ID"), + parameterWithName("lineupPlayerId").description("라인업 선수의 ID") + ) + )); } @Test @@ -76,17 +82,63 @@ public class GameControllerTest extends DocumentationTest { //when ResultActions result = mockMvc.perform( - patch("/games/{gameId}/lineup-players/{lineupPlayerId}/candidate", gameId, lineupPlayerId) - .contentType(MediaType.APPLICATION_JSON) + patch("/games/{gameId}/lineup-players/{lineupPlayerId}/candidate", gameId, lineupPlayerId) + .contentType(MediaType.APPLICATION_JSON) ); //then result.andExpect((status().isOk())) - .andDo(restDocsHandler.document( - pathParameters( - parameterWithName("gameId").description("게임의 ID"), - parameterWithName("lineupPlayerId").description("라인업 선수의 ID") - ) - )); + .andDo(restDocsHandler.document( + pathParameters( + parameterWithName("gameId").description("게임의 ID"), + parameterWithName("lineupPlayerId").description("라인업 선수의 ID") + ) + )); + } + + @Test + void 경기를_등록한다() throws Exception { + + // given + Long leagueId = 1L; + Long idOfTeam1 = 1L; + Long idOfTeam2 = 2L; + GameRequestDto.Register requestDto = new GameRequestDto.Register("경기 이름", "16강", "경기전", "SCHEDULED", + LocalDateTime.now(), idOfTeam1, idOfTeam2, "videoId"); + + Cookie cookie = new Cookie(COOKIE_NAME, "temp-cookie"); + + // when + ResultActions result = mockMvc.perform(post("/leagues/{leagueId}/games", leagueId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestDto)) + .cookie(cookie) + ); + + // then + result.andExpect(status().isCreated()) + .andDo(restDocsHandler.document( + pathParameters( + parameterWithName("leagueId").description("리그의 ID") + ), + requestFields( + fieldWithPath("name").type(JsonFieldType.STRING).description("경기의 이름"), + fieldWithPath("round").type(JsonFieldType.STRING).description("라운드의 설명 ex. 4강, 결승"), + fieldWithPath("quarter").type(JsonFieldType.STRING).description("쿼터"), + fieldWithPath("state").type(JsonFieldType.STRING).description("경기의 상태"), + fieldWithPath("startTime").type(JsonFieldType.STRING).description("시작 날짜 및 시각"), + fieldWithPath("idOfTeam1").type(JsonFieldType.NUMBER) + .description("경기게 참여하는 첫번째 리그팀의 아이디"), + fieldWithPath("idOfTeam2").type(JsonFieldType.NUMBER) + .description("경기게 참여하는 두번째 리그팀의 아이디"), + fieldWithPath("videoId").type(JsonFieldType.STRING) + .description("경기 영상 링크") + ), + requestCookies( + cookieWithName(COOKIE_NAME).description("로그인을 통해 얻은 토큰") + ) + )); } + + } diff --git a/src/test/java/com/sports/server/command/leagueteam/application/LeagueTeamServiceTest.java b/src/test/java/com/sports/server/command/leagueteam/application/LeagueTeamServiceTest.java index 8a97186f..dfc5e93c 100644 --- a/src/test/java/com/sports/server/command/leagueteam/application/LeagueTeamServiceTest.java +++ b/src/test/java/com/sports/server/command/leagueteam/application/LeagueTeamServiceTest.java @@ -9,7 +9,6 @@ import com.sports.server.command.league.domain.League; import com.sports.server.command.leagueteam.domain.LeagueTeam; import com.sports.server.command.leagueteam.domain.LeagueTeamPlayer; -import com.sports.server.command.leagueteam.domain.LeagueTeamPlayerFixtureRepository; import com.sports.server.command.leagueteam.domain.LeagueTeamRepository; import com.sports.server.command.leagueteam.dto.LeagueTeamPlayerRequest; import com.sports.server.command.leagueteam.dto.LeagueTeamRequest; @@ -17,6 +16,7 @@ import com.sports.server.common.application.EntityUtils; import com.sports.server.common.exception.UnauthorizedException; import com.sports.server.support.ServiceTest; +import com.sports.server.support.fixture.LeagueTeamPlayerFixtureRepository; import java.util.List; import java.util.Optional; import org.junit.jupiter.api.BeforeEach; diff --git a/src/test/java/com/sports/server/query/acceptance/SportQueryAcceptanceTest.java b/src/test/java/com/sports/server/query/acceptance/SportQueryAcceptanceTest.java index 56ef18c0..4b574fad 100644 --- a/src/test/java/com/sports/server/query/acceptance/SportQueryAcceptanceTest.java +++ b/src/test/java/com/sports/server/query/acceptance/SportQueryAcceptanceTest.java @@ -34,10 +34,10 @@ public class SportQueryAcceptanceTest extends AcceptanceTest { () -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()), () -> assertThat(actual) .map(SportResponse::id) - .containsExactly(1L, 2L), + .containsExactly(1L, 2L, 3L), () -> assertThat(actual) .map(SportResponse::name) - .containsExactly("농구", "루미큐브")); + .containsExactly("농구", "루미큐브", "축구")); } } diff --git a/src/test/java/com/sports/server/support/DocumentationTest.java b/src/test/java/com/sports/server/support/DocumentationTest.java index 3e7eda73..fa4918c2 100644 --- a/src/test/java/com/sports/server/support/DocumentationTest.java +++ b/src/test/java/com/sports/server/support/DocumentationTest.java @@ -7,6 +7,7 @@ import com.sports.server.auth.utils.JwtUtil; import com.sports.server.command.cheertalk.application.CheerTalkService; import com.sports.server.command.cheertalk.presentation.CheerTalkController; +import com.sports.server.command.game.application.GameService; import com.sports.server.command.game.application.GameTeamService; import com.sports.server.command.game.application.LineupPlayerService; import com.sports.server.command.game.presentation.GameController; @@ -148,6 +149,9 @@ public class DocumentationTest { @MockBean protected MemberQueryService memberQueryService; + @MockBean + protected GameService gameService; + @BeforeEach void setUp() { setupMockAuthentication(); diff --git a/src/test/java/com/sports/server/support/fixture/GameFixtureRepository.java b/src/test/java/com/sports/server/support/fixture/GameFixtureRepository.java new file mode 100644 index 00000000..06770f10 --- /dev/null +++ b/src/test/java/com/sports/server/support/fixture/GameFixtureRepository.java @@ -0,0 +1,9 @@ +package com.sports.server.support.fixture; + +import com.sports.server.command.game.domain.Game; +import java.util.Optional; +import org.springframework.data.repository.Repository; + +public interface GameFixtureRepository extends Repository { + Optional findByName(String name); +} \ No newline at end of file diff --git a/src/test/java/com/sports/server/support/fixture/GameTeamFixtureRepository.java b/src/test/java/com/sports/server/support/fixture/GameTeamFixtureRepository.java index eec38213..c46856df 100644 --- a/src/test/java/com/sports/server/support/fixture/GameTeamFixtureRepository.java +++ b/src/test/java/com/sports/server/support/fixture/GameTeamFixtureRepository.java @@ -1,7 +1,16 @@ package com.sports.server.support.fixture; +import com.sports.server.command.game.domain.Game; import com.sports.server.command.game.domain.GameTeam; +import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface GameTeamFixtureRepository extends JpaRepository { + @Query("SELECT gt FROM GameTeam gt " + + "JOIN FETCH gt.leagueTeam lt " + + "JOIN FETCH gt.lineupPlayers lup " + + "WHERE gt.game = :game") + List findByGame(@Param("game") Game game); } diff --git a/src/test/java/com/sports/server/support/fixture/LeagueTeamPlayerFixtureRepository.java b/src/test/java/com/sports/server/support/fixture/LeagueTeamPlayerFixtureRepository.java new file mode 100644 index 00000000..22d683ac --- /dev/null +++ b/src/test/java/com/sports/server/support/fixture/LeagueTeamPlayerFixtureRepository.java @@ -0,0 +1,19 @@ +package com.sports.server.support.fixture; + +import com.sports.server.command.leagueteam.domain.LeagueTeam; +import com.sports.server.command.leagueteam.domain.LeagueTeamPlayer; +import java.util.List; +import java.util.Optional; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.query.Param; + +public interface LeagueTeamPlayerFixtureRepository extends Repository { + @Query( + "SELECT ltp FROM LeagueTeamPlayer ltp " + + "WHERE ltp.leagueTeam =:leagueTeam" + ) + List findByLeagueTeam(@Param("leagueTeam") LeagueTeam leagueTeam); + + Optional findById(Long id); +} diff --git a/src/test/resources/game-fixture.sql b/src/test/resources/game-fixture.sql index 82dd3ce9..d0eaa7c8 100644 --- a/src/test/resources/game-fixture.sql +++ b/src/test/resources/game-fixture.sql @@ -68,10 +68,27 @@ VALUES ('팀 D', 'http://example.com/logo_c.png', 1, 1, 1); INSERT INTO league_teams (name, logo_image_url, manager_id, organization_id, league_id) VALUES ('팀 E', 'http://example.com/logo_c.png', 3, 2, 2); +-- league_team_id가 1인 선수들 +INSERT INTO league_team_players (league_team_id, name, description, number) +VALUES (1, '김철수', '능숙한 포워드', 10), + (1, '이영희', '민첩한 미드필더', 8), + (1, '박지훈', '강력한 수비수', 5), + (1, '최수진', '빠른 윙어', 7), + (1, '정민수', '신뢰할 수 있는 골키퍼', 1); + +-- league_team_id가 2인 선수들 +INSERT INTO league_team_players (league_team_id, name, description, number) +VALUES (2, '홍길동', '경험 많은 포워드', 11), + (2, '김민아', '다재다능한 미드필더', 6), + (2, '박성호', '탄탄한 수비수', 4), + (2, '이하나', '빠른 윙어', 9), + (2, '최준혁', '재능 있는 골키퍼', 2); + -- 스포츠 INSERT INTO sports(id, name) VALUES (1, '농구'), - (2, '루미큐브'); + (2, '루미큐브'), + (3, '축구'); -- 농구 대전(game_id = 1) A팀 선수 INSERT INTO lineup_players (id, game_team_id, name, description, number, is_captain, league_team_player_id, state) @@ -145,5 +162,12 @@ INSERT INTO leagues (id, manager_id, organization_id, name, start_at, end_at, max_round, in_progress_round, is_deleted) VALUES (1, 1, 1, '삼건물 대회', '2023-11-09 00:00:00', '2023-11-20 00:00:00', '16강', '16강', false); +INSERT INTO members (id, organization_id, email, password, is_manager, last_login) +VALUES (1, 1, 'john.doe@example.com', 'password123', TRUE, '2024-07-01 10:00:00'), + (2, 1, 'jane.smith@example.com', 'password456', FALSE, '2024-07-02 12:30:00'), + (3, 1, 'alice.johnson@example.com', 'password789', TRUE, '2024-07-03 09:45:00'), + (4, 1, 'bob.brown@example.com', 'password321', FALSE, '2024-07-04 14:20:00'), + (5, 1, 'carol.white@example.com', 'password654', TRUE, '2024-07-05 16:10:00'); + SET foreign_key_checks = 1;