diff --git a/src/main/java/com/sports/server/common/advice/ControllerExceptionAdvice.java b/src/main/java/com/sports/server/common/advice/ControllerExceptionAdvice.java index dc779cd20..29b192642 100644 --- a/src/main/java/com/sports/server/common/advice/ControllerExceptionAdvice.java +++ b/src/main/java/com/sports/server/common/advice/ControllerExceptionAdvice.java @@ -11,6 +11,7 @@ import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.servlet.NoHandlerFoundException; @Slf4j @RestControllerAdvice @@ -38,4 +39,10 @@ protected ResponseEntity handleMethodArgumentsNotValidException(M return ResponseEntity.badRequest() .body(ErrorResponse.of(e.getBindingResult())); } + + @ExceptionHandler(NoHandlerFoundException.class) + protected ResponseEntity handleNotFoundEndpointException(NoHandlerFoundException e) { + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(ErrorResponse.of("요청한 엔드포인트를 찾을 수 없습니다.")); + } } diff --git a/src/main/java/com/sports/server/query/application/LeagueQueryService.java b/src/main/java/com/sports/server/query/application/LeagueQueryService.java index 9d9766026..990638478 100644 --- a/src/main/java/com/sports/server/query/application/LeagueQueryService.java +++ b/src/main/java/com/sports/server/query/application/LeagueQueryService.java @@ -70,7 +70,8 @@ public LeagueRecentSummaryResponse findRecentSummary(Integer year, Integer recor int safeTopScorerLimit = Math.max(topScorerLimit, 0); LocalDateTime now = LocalDateTime.now(); - LocalDateTime yearStart = LocalDateTime.of(year, 1, 1, 0, 0); + int targetYear = getTargetYear(year, now); + LocalDateTime yearStart = LocalDateTime.of(targetYear, 1, 1, 0, 0); LocalDateTime yearEnd = yearStart.plusYears(1); List records = safeRecordLimit == 0 @@ -81,7 +82,7 @@ public LeagueRecentSummaryResponse findRecentSummary(Integer year, Integer recor List topScorerResults = safeTopScorerLimit == 0 ? Collections.emptyList() - : leagueTopScorerRepository.findTopPlayersByYearWithTotalGoals(year, PageRequest.of(0, safeTopScorerLimit)); + : leagueTopScorerRepository.findTopPlayersByYearWithTotalGoals(targetYear, PageRequest.of(0, safeTopScorerLimit)); Map unitByPlayerId = getUnitByPlayerId(topScorerResults.stream() .map(PlayerGoalCountWithRank::playerId) @@ -101,6 +102,15 @@ public LeagueRecentSummaryResponse findRecentSummary(Integer year, Integer recor return new LeagueRecentSummaryResponse(records, topScorers); } + private int getTargetYear(Integer year, LocalDateTime now) { + if (year != null) { + return year; + } + return leagueQueryRepository.findRecentFinishedLeagueYears(now, PageRequest.of(0, 1)).stream() + .findFirst() + .orElse(now.getYear()); + } + private Map getUnitByPlayerId(List playerIds) { if (playerIds.isEmpty()) { return Collections.emptyMap(); diff --git a/src/main/java/com/sports/server/query/presentation/LeagueQueryController.java b/src/main/java/com/sports/server/query/presentation/LeagueQueryController.java index a47e3fbe9..d1ade4110 100644 --- a/src/main/java/com/sports/server/query/presentation/LeagueQueryController.java +++ b/src/main/java/com/sports/server/query/presentation/LeagueQueryController.java @@ -76,7 +76,7 @@ public ResponseEntity> findTopScorersByYear( @GetMapping("/recent-summary") public ResponseEntity findRecentSummary( - @RequestParam(defaultValue = "2025") Integer year, + @RequestParam(required = false) Integer year, @RequestParam(defaultValue = "5") Integer recordLimit, @RequestParam(defaultValue = "5") Integer topScorerLimit ) { diff --git a/src/main/java/com/sports/server/query/repository/LeagueQueryRepository.java b/src/main/java/com/sports/server/query/repository/LeagueQueryRepository.java index a767df020..e2183cb72 100644 --- a/src/main/java/com/sports/server/query/repository/LeagueQueryRepository.java +++ b/src/main/java/com/sports/server/query/repository/LeagueQueryRepository.java @@ -38,6 +38,19 @@ public interface LeagueQueryRepository extends Repository, LeagueQ ) Optional findByIdWithLeagueTeam(@Param("id") Long id); + @Query( + "SELECT YEAR(l.startAt) " + + "FROM League l " + + "JOIN LeagueStatistics ls ON ls.league = l " + + "WHERE l.endAt < :now " + + "AND ls.firstWinnerTeam IS NOT NULL " + + "ORDER BY l.startAt DESC, l.id DESC" + ) + List findRecentFinishedLeagueYears( + @Param("now") LocalDateTime now, + Pageable pageable + ); + @Query( "SELECT new com.sports.server.query.repository.LeagueRecentRecordResult(l.id, l.name, ls.firstWinnerTeam.name) " + "FROM League l " diff --git a/src/main/resources/static/docs/api.html b/src/main/resources/static/docs/api.html index df705526d..df0e8d5bc 100644 --- a/src/main/resources/static/docs/api.html +++ b/src/main/resources/static/docs/api.html @@ -5424,7 +5424,7 @@

HTTP request

-
GET /leagues/recent-summary?year=2025&recordLimit=2&topScorerLimit=2 HTTP/1.1
+
GET /leagues/recent-summary?recordLimit=2&topScorerLimit=2 HTTP/1.1
 Content-Type: application/json
 Host: www.api.hufstreaming.site
@@ -5446,7 +5446,7 @@

year

-

조회할 연도 (default 값 2025)

+

조회할 연도 (선택사항, 미입력 시 최근 종료 대회의 연도)

recordLimit

@@ -8421,7 +8421,7 @@

diff --git a/src/test/java/com/sports/server/common/advice/ControllerExceptionAdviceAcceptanceTest.java b/src/test/java/com/sports/server/common/advice/ControllerExceptionAdviceAcceptanceTest.java new file mode 100644 index 000000000..1bb35be2e --- /dev/null +++ b/src/test/java/com/sports/server/common/advice/ControllerExceptionAdviceAcceptanceTest.java @@ -0,0 +1,38 @@ +package com.sports.server.common.advice; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.sports.server.common.dto.ErrorResponse; +import com.sports.server.support.AcceptanceTest; +import io.restassured.RestAssured; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.test.context.TestPropertySource; + +@TestPropertySource(properties = { + "spring.mvc.throw-exception-if-no-handler-found=true", + "spring.web.resources.add-mappings=false" +}) +class ControllerExceptionAdviceAcceptanceTest extends AcceptanceTest { + + @Test + void 존재하지_않는_엔드포인트_요청시_NoHandlerFoundException_핸들러로_404를_반환한다() { + // when + ExtractableResponse response = RestAssured.given().log().all() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .get("/not-exists") + .then().log().all() + .extract(); + + // then + ErrorResponse actual = toResponse(response, ErrorResponse.class); + assertAll( + () -> assertThat(response.statusCode()).isEqualTo(HttpStatus.NOT_FOUND.value()), + () -> assertThat(actual.getMessage()).isEqualTo("요청한 엔드포인트를 찾을 수 없습니다.") + ); + } +} diff --git a/src/test/java/com/sports/server/query/acceptance/LeagueQueryAcceptanceTest.java b/src/test/java/com/sports/server/query/acceptance/LeagueQueryAcceptanceTest.java index 11279ae80..5a620afc7 100644 --- a/src/test/java/com/sports/server/query/acceptance/LeagueQueryAcceptanceTest.java +++ b/src/test/java/com/sports/server/query/acceptance/LeagueQueryAcceptanceTest.java @@ -200,7 +200,6 @@ class findLeagues{ void 최근_대회_요약_정보를_조회한다() { // when ExtractableResponse response = RestAssured.given().log().all() - .param("year", 2025) .param("recordLimit", 5) .param("topScorerLimit", 5) .contentType(MediaType.APPLICATION_JSON_VALUE) @@ -229,4 +228,17 @@ class findLeagues{ ); } + @Test + void 존재하지_않는_엔드포인트_요청시_404를_반환한다() { + // when + ExtractableResponse response = RestAssured.given().log().all() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .get("/not-exists") + .then().log().all() + .extract(); + + // then + assertThat(response.statusCode()).isEqualTo(HttpStatus.NOT_FOUND.value()); + } + } diff --git a/src/test/java/com/sports/server/query/application/LeagueQueryServiceTest.java b/src/test/java/com/sports/server/query/application/LeagueQueryServiceTest.java index 4ab2692ff..0b99e8305 100644 --- a/src/test/java/com/sports/server/query/application/LeagueQueryServiceTest.java +++ b/src/test/java/com/sports/server/query/application/LeagueQueryServiceTest.java @@ -425,9 +425,9 @@ class FindLeaguesByConditionTest { } @Test - void 최근_대회_요약_정보를_조회한다() { + void 연도_미입력시_최근_종료_대회_연도로_요약_정보를_조회한다() { // given - Integer year = 2025; + Integer year = null; Integer recordLimit = 5; Integer topScorerLimit = 5; diff --git a/src/test/java/com/sports/server/query/presentation/LeagueQueryControllerTest.java b/src/test/java/com/sports/server/query/presentation/LeagueQueryControllerTest.java index a80700d61..3d735f3a5 100644 --- a/src/test/java/com/sports/server/query/presentation/LeagueQueryControllerTest.java +++ b/src/test/java/com/sports/server/query/presentation/LeagueQueryControllerTest.java @@ -541,8 +541,6 @@ public class LeagueQueryControllerTest extends DocumentationTest { @Test void 최근_대회_요약_정보를_조회한다() throws Exception { // given - Integer year = 2025; - LeagueRecentSummaryResponse response = new LeagueRecentSummaryResponse( List.of( new LeagueRecentSummaryResponse.LeagueRecord(7L, "종료된 축구대회 7", "서어 뻬데뻬"), @@ -554,12 +552,11 @@ public class LeagueQueryControllerTest extends DocumentationTest { ) ); - given(leagueQueryService.findRecentSummary(year, 2, 2)) + given(leagueQueryService.findRecentSummary(null, 2, 2)) .willReturn(response); // when ResultActions result = mockMvc.perform(get("/leagues/recent-summary") - .queryParam("year", String.valueOf(year)) .queryParam("recordLimit", "2") .queryParam("topScorerLimit", "2") .contentType(MediaType.APPLICATION_JSON)); @@ -568,7 +565,7 @@ public class LeagueQueryControllerTest extends DocumentationTest { result.andExpect(status().isOk()) .andDo(restDocsHandler.document( queryParameters( - parameterWithName("year").description("조회할 연도 (default 값 2025)"), + parameterWithName("year").description("조회할 연도 (선택사항, 미입력 시 최근 종료 대회의 연도)").optional(), parameterWithName("recordLimit").description("대회 기록 최대 개수 (default 값 5)"), parameterWithName("topScorerLimit").description("득점왕 최대 개수 (default 값 5)") ),