Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Copy link
Contributor

Choose a reason for hiding this comment

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

커밋 두개로 분리해주셨으면 더 좋았을 것 같아요! 👍👍


@Slf4j
@RestControllerAdvice
Expand Down Expand Up @@ -38,4 +39,10 @@ protected ResponseEntity<ErrorResponse> handleMethodArgumentsNotValidException(M
return ResponseEntity.badRequest()
.body(ErrorResponse.of(e.getBindingResult()));
}

@ExceptionHandler(NoHandlerFoundException.class)
Copy link
Contributor

Choose a reason for hiding this comment

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

요거 테스트 추가 해주시면 좋을 것 같아요! RestAssured 활용 해 보시면 될 것 같습니당

Copy link
Contributor Author

Choose a reason for hiding this comment

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

넵 확인했습니다! 수정이랑 추가해서 머지해둘게요

protected ResponseEntity<ErrorResponse> handleNotFoundEndpointException(NoHandlerFoundException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(ErrorResponse.of("요청한 엔드포인트를 찾을 수 없습니다."));

Choose a reason for hiding this comment

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

medium

오류 메시지를 문자열 리터럴로 직접 사용하는 것보다 상수로 정의하여 관리하는 것이 좋습니다. 이렇게 하면 메시지의 일관성을 유지하고, 향후 수정이 필요할 때 더 쉽게 관리할 수 있습니다. 이 클래스 내에 private static final String 상수로 정의하거나, 별도의 ErrorMessage 클래스를 만들어 관리하는 것을 고려해 보세요.

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<LeagueRecentSummaryResponse.LeagueRecord> records = safeRecordLimit == 0
Expand All @@ -81,7 +82,7 @@ public LeagueRecentSummaryResponse findRecentSummary(Integer year, Integer recor

List<PlayerGoalCountWithRank> topScorerResults = safeTopScorerLimit == 0
? Collections.emptyList()
: leagueTopScorerRepository.findTopPlayersByYearWithTotalGoals(year, PageRequest.of(0, safeTopScorerLimit));
: leagueTopScorerRepository.findTopPlayersByYearWithTotalGoals(targetYear, PageRequest.of(0, safeTopScorerLimit));

Choose a reason for hiding this comment

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

security-medium medium

The topScorerLimit parameter is used to create a PageRequest without an upper bound check. An attacker can provide an extremely large value for this parameter (e.g., 2147483647), which may lead to excessive memory allocation or database performance issues, potentially causing a Denial of Service (DoS). It is recommended to validate the input and enforce a maximum allowable limit (e.g., 100).


Map<Long, String> unitByPlayerId = getUnitByPlayerId(topScorerResults.stream()
.map(PlayerGoalCountWithRank::playerId)
Expand All @@ -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<Long, String> getUnitByPlayerId(List<Long> playerIds) {
if (playerIds.isEmpty()) {
return Collections.emptyMap();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ public ResponseEntity<List<TopScorerResponse>> findTopScorersByYear(

@GetMapping("/recent-summary")
public ResponseEntity<LeagueRecentSummaryResponse> findRecentSummary(
@RequestParam(defaultValue = "2025") Integer year,
@RequestParam(required = false) Integer year,
@RequestParam(defaultValue = "5") Integer recordLimit,
@RequestParam(defaultValue = "5") Integer topScorerLimit
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,19 @@ public interface LeagueQueryRepository extends Repository<League, Long>, LeagueQ
)
Optional<League> 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<Integer> 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 "
Expand Down
6 changes: 3 additions & 3 deletions src/main/resources/static/docs/api.html
Original file line number Diff line number Diff line change
Expand Up @@ -5424,7 +5424,7 @@ <h3 id="_최근_대회_요약_정보_조회"><a class="link" href="#_최근_대
<h4 id="_최근_대회_요약_정보_조회_http_request"><a class="link" href="#_최근_대회_요약_정보_조회_http_request">HTTP request</a></h4>
<div class="listingblock">
<div class="content">
<pre class="highlightjs highlight nowrap"><code class="language-http hljs" data-lang="http">GET /leagues/recent-summary?year=2025&amp;recordLimit=2&amp;topScorerLimit=2 HTTP/1.1
<pre class="highlightjs highlight nowrap"><code class="language-http hljs" data-lang="http">GET /leagues/recent-summary?recordLimit=2&amp;topScorerLimit=2 HTTP/1.1
Content-Type: application/json
Host: www.api.hufstreaming.site</code></pre>
</div>
Expand All @@ -5446,7 +5446,7 @@ <h4 id="_최근_대회_요약_정보_조회_query_parameters"><a class="link" hr
<tbody>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>year</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">조회할 연도 (default 값 2025)</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">조회할 연도 (선택사항, 미입력 시 최근 종료 대회의 연도)</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>recordLimit</code></p></td>
Expand Down Expand Up @@ -8421,7 +8421,7 @@ <h4 id="_팀별보기_데이터_조회_response_fields"><a class="link" href="#_
<div id="footer">
<div id="footer-text">
Version 0.0.1-SNAPSHOT<br>
Last updated 2026-02-10 15:21:15 +0900
Last updated 2026-02-12 11:49:57 +0900
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.18.3/highlight.min.js"></script>
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
})
Comment on lines +16 to +19
Copy link
Contributor

Choose a reason for hiding this comment

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

이건 왜 필요한가요? 저희 기존 패턴이랑 안맞는 것 같아요!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

404 자체가 아니라 NoHandlerFoundException 핸들러가 실제로 호출되는지 검증하려고 넣었습니다!
기존에는 없는 엔드포인트 요청이 404 처리로만 끝날 수도 있어서 핸들러 테스트가 안되더라구요. 테스트에서만 예외 강제로 실행시키려고 넣었는데 빼야한다면 다시 빼서 커밋할게요

Copy link
Contributor

@Jin409 Jin409 Feb 14, 2026

Choose a reason for hiding this comment

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

아 MVC 레이어에서 바로 걸러져서 그렇군요 이해했습니다 👍👍 머지 고고
머지 하시고 성민님한테 전달 주시면 좋을 것 같아요!

class ControllerExceptionAdviceAcceptanceTest extends AcceptanceTest {

@Test
void 존재하지_않는_엔드포인트_요청시_NoHandlerFoundException_핸들러로_404를_반환한다() {
// when
ExtractableResponse<Response> 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("요청한 엔드포인트를 찾을 수 없습니다.")
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,6 @@ class findLeagues{
void 최근_대회_요약_정보를_조회한다() {
// when
ExtractableResponse<Response> response = RestAssured.given().log().all()
.param("year", 2025)
.param("recordLimit", 5)
.param("topScorerLimit", 5)
.contentType(MediaType.APPLICATION_JSON_VALUE)
Expand Down Expand Up @@ -229,4 +228,17 @@ class findLeagues{
);
}

@Test
void 존재하지_않는_엔드포인트_요청시_404를_반환한다() {
// when
ExtractableResponse<Response> 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());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -425,9 +425,9 @@ class FindLeaguesByConditionTest {
}

@Test
void 최근_대회_요약_정보를_조회한다() {
void 연도_미입력시_최근_종료_대회_연도로_요약_정보를_조회한다() {
// given
Integer year = 2025;
Integer year = null;
Integer recordLimit = 5;
Integer topScorerLimit = 5;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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", "서어 뻬데뻬"),
Expand All @@ -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));
Expand All @@ -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)")
),
Expand Down
Loading