Skip to content

Feature/#29 대회 정렬 기능 개발#71

Open
JJimini wants to merge 8 commits intodevelopfrom
Feature/#29-대회_정렬_기능_개발

Hidden character warning

The head ref may contain hidden characters: "Feature/#29-\ub300\ud68c_\uc815\ub82c_\uae30\ub2a5_\uac1c\ubc1c"
Open

Feature/#29 대회 정렬 기능 개발#71
JJimini wants to merge 8 commits intodevelopfrom
Feature/#29-대회_정렬_기능_개발

Conversation

@JJimini
Copy link
Collaborator

@JJimini JJimini commented Feb 6, 2026

🔥 연관된 이슈

close: #29

📜 작업 내용

  • 대회 정렬 기능을 개발했습니다!

💬 리뷰 요구사항

  • 원래 DTO Level에서 던지던 예외를 Service Level로 바꿨습니다. 확인해주세요 (Request 형태가 바뀌었기 때문)
  • team을 만들 때 전체 팀 개수 + 1해서 itemOrder 넣는 로직도 추가해야 하는데, 아직 팀 생성 API가 없어서 todo로 남겨두었습니다!

✨ 기타

  • 생각보다 검증하는 부분이 많아서,, 생각하느라,, 11시에 시작했는데,, 끝나니 지금이네요,,😢

@JJimini JJimini self-assigned this Feb 6, 2026
@JJimini JJimini added the ⭐️ 기능 구현 새로운 기능을 구현 label Feb 6, 2026
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

대회(Contest) 도메인에 “정렬 모드/수동 정렬” 기능을 추가하고, 이를 변경/조회하는 관리자 API 및 문서·테스트를 함께 보강한 PR입니다. 이슈 #29(기본값 RANDOM) 요구사항을 반영해 대회 생성 시 정렬 설정이 함께 생성되도록 확장했습니다.

Changes:

  • ContestSort(정렬 설정) 엔티티/리포지토리/Convenience를 추가하고, 대회 생성 시 기본 정렬 설정을 자동 생성
  • 관리자용 대회 정렬 API(모드 변경/조회, CUSTOM 수동 정렬 저장) 및 Query/Command 로직 추가
  • RestDocs/통합 테스트/스키마 및 Team itemOrder 업데이트 지원 코드 추가

Reviewed changes

Copilot reviewed 24 out of 24 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/main/java/com/opus/opus/modules/contest/application/ContestCommandService.java 대회 정렬 설정 생성/변경 및 CUSTOM 수동 정렬 저장 로직 추가
src/main/java/com/opus/opus/modules/contest/api/ContestController.java 대회 정렬 관련 관리자 API 엔드포인트 추가
src/main/java/com/opus/opus/modules/contest/application/ContestQueryService.java 대회 정렬 설정 조회 기능 추가
src/main/java/com/opus/opus/modules/contest/domain/ContestSort.java Contest 기준 정렬 설정 엔티티로 정리 및 모드 업데이트 메서드 추가
src/main/java/com/opus/opus/modules/contest/domain/SortType.java SortType의 올바른 패키지(contest)로 이동
src/main/java/com/opus/opus/modules/contest/exception/ContestExceptionType.java 정렬 기능 관련 예외 타입 추가
src/main/java/com/opus/opus/modules/contest/domain/dao/ContestSortRepository.java ContestSort 조회용 리포지토리 추가
src/main/java/com/opus/opus/modules/contest/domain/dao/ContestRepository.java 대회 row lock(PESSIMISTIC_WRITE) 조회 메서드 추가
src/main/java/com/opus/opus/modules/contest/application/convenience/ContestConvenience.java 락 조회 convenience 메서드 추가(MANDATORY)
src/main/java/com/opus/opus/modules/contest/application/convenience/ContestSortConvenience.java ContestSort 존재 검증/조회 convenience 추가
src/main/java/com/opus/opus/modules/team/domain/Team.java itemOrder 업데이트 메서드 추가
src/main/java/com/opus/opus/modules/team/domain/dao/TeamRepository.java contestId 기반 팀 목록 조회 메서드 추가
src/main/java/com/opus/opus/modules/team/application/convenience/TeamConvenience.java 대회 소속 팀 목록 조회 메서드 추가
src/main/java/com/opus/opus/modules/team/exception/TeamExceptionType.java itemOrder 범위 검증용 예외 추가
src/main/resources/schema.sql contest_sort 테이블 추가 및 team_sort 제거
src/test/java/com/opus/opus/restdocs/docs/ContestApiDocsTest.java 정렬 API RestDocs 테스트 추가
src/test/java/com/opus/opus/contest/application/ContestCommandServiceTest.java 정렬 기능 통합 테스트 추가
src/test/java/com/opus/opus/contest/ContestSortFixture.java ContestSort 테스트 fixture 추가
src/test/java/com/opus/opus/team/TeamFixture.java contestId/itemOrder 지정 팀 fixture 추가
src/main/java/com/opus/opus/docs/asciidoc/contest.adoc 정렬 관리 API 문서 추가

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.


final ContestSort afterContestSort = contestSortRepository.findByContestId(contest.getId()).orElseThrow();
assertThat(afterContestSort.getMode()).isEqualTo(ASC);
assertThat(afterContestSort.getMode()).isNotEqualTo(beforeContestSort);
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

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

assertThat(afterContestSort.getMode()).isNotEqualTo(beforeContestSort); 는 SortType과 ContestSort를 비교하고 있어 항상 true가 되어 테스트가 의미를 잃습니다. 변경 전/후를 비교하려면 beforeContestSort.getMode() 또는 beforeContestSort.getId() 등 동일 타입/값을 비교하도록 수정해야 합니다.

Suggested change
assertThat(afterContestSort.getMode()).isNotEqualTo(beforeContestSort);
assertThat(afterContestSort.getMode()).isNotEqualTo(beforeContestSort.getMode());

Copilot uses AI. Check for mistakes.
Comment on lines +16 to +21
NOT_FOUND_CONTEST_SORT(HttpStatus.NOT_FOUND, "존재하는 팀 정렬이 없습니다"),
ONLY_CUSTOM_MODE_CAN_CHANGE(HttpStatus.FORBIDDEN, "CUSTOM 모드에서만 정렬을 수정할 수 있습니다."),
DUPLICATE_TEAM_ID_IN_SORT_REQUEST(HttpStatus.BAD_REQUEST, "중복된 팀ID가 있습니다."),
DUPLICATE_ITEM_ORDER_IN_SORT_REQUEST(HttpStatus.BAD_REQUEST, "중복된 itemOrder가 있습니다."),
NOT_EXIST_TEAM_IN_CONTEST(HttpStatus.NOT_FOUND, "현재 대회에 소속된 팀이 아닙니다"),
INVALID_CONTEST_SORT_CUSTOM_REQUEST(HttpStatus.BAD_REQUEST, "저장된 팀 개수와 request의 팀 개수가 다릅니다"),
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

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

NOT_FOUND_CONTEST_SORT/NOT_EXIST_TEAM_IN_CONTEST/INVALID_CONTEST_SORT_CUSTOM_REQUEST 등의 errorMessage가 '팀 정렬'로 표기되어 있거나 문장 끝 마침표가 누락되어 기존 메시지들과 톤이 불일치합니다. 특히 NOT_FOUND_CONTEST_SORT는 현재 기능이 '대회 정렬'이므로 메시지 내용 자체가 잘못되어 클라이언트/문서에 혼동을 줄 수 있습니다.

Suggested change
NOT_FOUND_CONTEST_SORT(HttpStatus.NOT_FOUND, "존재하는 팀 정렬이 없습니다"),
ONLY_CUSTOM_MODE_CAN_CHANGE(HttpStatus.FORBIDDEN, "CUSTOM 모드에서만 정렬을 수정할 수 있습니다."),
DUPLICATE_TEAM_ID_IN_SORT_REQUEST(HttpStatus.BAD_REQUEST, "중복된 팀ID가 있습니다."),
DUPLICATE_ITEM_ORDER_IN_SORT_REQUEST(HttpStatus.BAD_REQUEST, "중복된 itemOrder가 있습니다."),
NOT_EXIST_TEAM_IN_CONTEST(HttpStatus.NOT_FOUND, "현재 대회에 소속된 팀이 아닙니다"),
INVALID_CONTEST_SORT_CUSTOM_REQUEST(HttpStatus.BAD_REQUEST, "저장된 팀 개수와 request의 팀 개수가 다릅니다"),
NOT_FOUND_CONTEST_SORT(HttpStatus.NOT_FOUND, "존재하지 않는 대회 정렬입니다."),
ONLY_CUSTOM_MODE_CAN_CHANGE(HttpStatus.FORBIDDEN, "CUSTOM 모드에서만 정렬을 수정할 수 있습니다."),
DUPLICATE_TEAM_ID_IN_SORT_REQUEST(HttpStatus.BAD_REQUEST, "중복된 팀ID가 있습니다."),
DUPLICATE_ITEM_ORDER_IN_SORT_REQUEST(HttpStatus.BAD_REQUEST, "중복된 itemOrder가 있습니다."),
NOT_EXIST_TEAM_IN_CONTEST(HttpStatus.NOT_FOUND, "현재 대회에 소속된 팀이 아닙니다."),
INVALID_CONTEST_SORT_CUSTOM_REQUEST(HttpStatus.BAD_REQUEST, "저장된 팀 개수와 요청한 팀 개수가 다릅니다."),

Copilot uses AI. Check for mistakes.
@Secured("ROLE_관리자")
@PutMapping("/{contestId}/sort/custom")
public ResponseEntity<Void> updateContestSortCustom(@PathVariable final Long contestId,
@RequestBody final List<@Valid ContestSortCustomRequest> requests) {
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

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

updateContestSortCustom의 request body는 List 타입인데 파라미터에 @Valid가 없어서(현재는 List<@Valid ...>만 사용) Bean Validation이 실행되지 않을 수 있습니다. Spring MVC에서 @RequestBody 검증을 보장하려면 파라미터 자체에 @Valid(또는 @Validated)를 추가해 요소의 @NotNull 등이 실제로 검증되도록 해주세요.

Suggested change
@RequestBody final List<@Valid ContestSortCustomRequest> requests) {
@Valid @RequestBody final List<ContestSortCustomRequest> requests) {

Copilot uses AI. Check for mistakes.
}
}

public List<Team> getTeamsOfContest(final Long contestId){
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

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

메서드 선언부에서 { 앞 공백이 빠져 있어(getTeamsOfContest(final Long contestId){) 같은 파일의 다른 메서드들과 포맷이 불일치합니다. 포맷터/컨벤션에 맞게 공백을 추가해주세요.

Suggested change
public List<Team> getTeamsOfContest(final Long contestId){
public List<Team> getTeamsOfContest(final Long contestId) {

Copilot uses AI. Check for mistakes.
Comment on lines +462 to +468
willThrow(new ContestException(ONLY_CUSTOM_MODE_CAN_CHANGE)).given(contestCommandService)
.updateContestSortCustom(any(), any());

mockMvc.perform(put("/contests/{contestId}/sort/custom", 1)
.header(HttpHeaders.AUTHORIZATION, ADMIN_TOKEN)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isBadRequest())
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

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

CUSTOM모드가_아니라면_수동_정렬_저장은_실패한다 테스트는 request body 없이 application/json으로 호출하고 있어 컨트롤러에서 JSON 파싱 단계에서 400이 나고 서비스 stub(ONLY_CUSTOM_MODE_CAN_CHANGE)가 실행되지 않을 가능성이 큽니다. 또한 ONLY_CUSTOM_MODE_CAN_CHANGE는 HttpStatus.FORBIDDEN(403)인데 여기서는 400을 기대하고 있어 실제 예외 매핑(ExceptionAdvice)과 불일치합니다. (유효한 JSON 배열을 보내고 상태 코드를 403으로 맞추거나, 예외 타입의 status를 조정해주세요.)

Suggested change
willThrow(new ContestException(ONLY_CUSTOM_MODE_CAN_CHANGE)).given(contestCommandService)
.updateContestSortCustom(any(), any());
mockMvc.perform(put("/contests/{contestId}/sort/custom", 1)
.header(HttpHeaders.AUTHORIZATION, ADMIN_TOKEN)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isBadRequest())
final List<ContestSortCustomRequest> requests = List.of(
new ContestSortCustomRequest(1L, 1),
new ContestSortCustomRequest(2L, 2)
);
willThrow(new ContestException(ONLY_CUSTOM_MODE_CAN_CHANGE)).given(contestCommandService)
.updateContestSortCustom(any(), any());
mockMvc.perform(put("/contests/{contestId}/sort/custom", 1)
.header(HttpHeaders.AUTHORIZATION, ADMIN_TOKEN)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(requests)))
.andExpect(status().isForbidden())

Copilot uses AI. Check for mistakes.
Comment on lines +524 to +528
mockMvc.perform(put("/contests/{contestId}/sort/custom", 1)
.header(HttpHeaders.AUTHORIZATION, ADMIN_TOKEN)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isBadRequest())
.andDo(document("update-contest-sort-custom-fail-different-size"));
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

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

요청_팀_개수와_저장된_팀_개수가_다르면_수동_정렬_저장은_실패한다 테스트도 request body 없이 JSON 요청을 보내고 있어(빈 body) 서비스의 INVALID_CONTEST_SORT_CUSTOM_REQUEST 예외가 아니라 HttpMessageNotReadable 등으로 400이 발생할 수 있습니다. 문서화하려는 실패 케이스가 서비스 레벨 검증이라면 최소한 [] 또는 유효한 JSON 배열을 body에 포함시켜 컨트롤러가 정상적으로 서비스까지 도달하도록 해주세요.

Copilot uses AI. Check for mistakes.
Comment on lines +534 to +541
willThrow(new TeamException(INVALID_ITEM_ORDER)).given(contestCommandService)
.updateContestSortCustom(any(), any());

mockMvc.perform(put("/contests/{contestId}/sort/custom", 1)
.header(HttpHeaders.AUTHORIZATION, ADMIN_TOKEN)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isBadRequest())
.andDo(document("update-contest-sort-custom-fail-over-itemOrder"));
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

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

저장된_팀_개수보다_itemOrder가_크면_수동_정렬_저장은_실패한다 테스트도 request body 없이 JSON 요청을 보내고 있어 서비스의 INVALID_ITEM_ORDER 예외 대신 컨트롤러 단계 파싱 오류로 400이 날 수 있습니다. 유효한 JSON 배열 body를 포함시켜 실제로 TeamException(INVALID_ITEM_ORDER) 경로를 문서화하도록 수정하는 편이 안전합니다.

Suggested change
willThrow(new TeamException(INVALID_ITEM_ORDER)).given(contestCommandService)
.updateContestSortCustom(any(), any());
mockMvc.perform(put("/contests/{contestId}/sort/custom", 1)
.header(HttpHeaders.AUTHORIZATION, ADMIN_TOKEN)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isBadRequest())
.andDo(document("update-contest-sort-custom-fail-over-itemOrder"));
final List<ContestSortCustomRequest> requests = List.of(
new ContestSortCustomRequest(1L, 2),
new ContestSortCustomRequest(2L, 3)
);
willThrow(new TeamException(INVALID_ITEM_ORDER)).given(contestCommandService)
.updateContestSortCustom(any(), any());
mockMvc.perform(put("/contests/{contestId}/sort/custom", 1)
.header(HttpHeaders.AUTHORIZATION, ADMIN_TOKEN)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(requests)))
.andExpect(status().isBadRequest())
.andDo(document("update-contest-sort-custom-fail-over-itemOrder",
requestFields(
arrayFieldWithPath("[]", "정렬 순서를 담은 팀 배열"),
numberFieldWithPath("[].teamId", "팀 ID"),
numberFieldWithPath("[].itemOrder", "저장된 팀 개수보다 큰 팀의 정렬 순서")
)
));

Copilot uses AI. Check for mistakes.
Copy link
Contributor

@sjmoon00 sjmoon00 left a comment

Choose a reason for hiding this comment

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

수고하셨습니다~
검증을 엄청 꼼꼼하게 하신 부분이 잘 느껴집니다

Comment on lines +465 to +467
mockMvc.perform(put("/contests/{contestId}/sort/custom", 1)
.header(HttpHeaders.AUTHORIZATION, ADMIN_TOKEN)
.contentType(MediaType.APPLICATION_JSON))
Copy link
Contributor

Choose a reason for hiding this comment

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

.content() 설정이 빠져 있습니다
본문 없이 요청을 보내면 비즈니스 로직에 도달하기 전 예외가 발생하여 테스트가 의도와 다르게 작동하지 않을까 생각합니다

Comment on lines +525 to +526
.header(HttpHeaders.AUTHORIZATION, ADMIN_TOKEN)
.contentType(MediaType.APPLICATION_JSON))
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

@pykido pykido left a comment

Choose a reason for hiding this comment

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

수고하셨습니다!
검증 부분을 꼼꼼하게 잘 작성해주셨네요! 간단한 코멘트 확인 부탁드립니다 ㅎ

for (ContestSortCustomRequest r : requests) {
final int order = r.itemOrder();
if (order < 1 || order > teamCount) {
throw new TeamException(INVALID_ITEM_ORDER);
Copy link
Contributor

Choose a reason for hiding this comment

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

Contest 관련 로직에서 TeamException을 던지는게 조금 어색한 거 같습니다!

@Builder
private TeamSort(final Team team) {
private ContestSort(final Contest contest) {
this.mode = RANDOM;
Copy link
Contributor

Choose a reason for hiding this comment

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

기본값이 RANDOM이군요! 👍👍👍👍

final List<Team> teams = teamConvenience.getTeamsOfContest(contestId);
validateRequestSizeMatchesTeams(requests, teams);
validateItemOrderRange(requests, teams.size());
validateDuplicateItemOrders(requests);
Copy link
Contributor

Choose a reason for hiding this comment

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

이 부분도 DB 조회 전에 검증 가능할 거 같은데 validateDupliacteTeamIds아래로 올리면 어떨까요?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

⭐️ 기능 구현 새로운 기능을 구현

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] 대회 정렬 기능 개발

3 participants