[feature] 동아리 지원서 폼 제작 시에 학기를 선택할 수 있고 생성된 모든 지원서 폼을 학기별로 분류하여 조회할 수 있다#739
Conversation
- semesterYear : semester에 사용될 연도 - semesterTerm : 1학기, 여름계절학기, 2학기, 겨울계절학기
현재 날짜 기준으로 default로 3개 조회 ex) 2025-SECOND, 2025-WINTER, 2026-FIRST
- semesterOption을 semesterOptionResponse로
1. 생성 : service에서 getClubQuestion 사용을 제거하여 한 클럽 내 다중 지원서 생성을 허용하고, semester 값 반영 2. 수정 : clubQuestionId를 경로변수로 받아서 특정 지원서만 수정되도록 변경
기존 코드에 있던 클럽 지원 시, questionId 빌드를 임시로 clcubId로 해둔 거 같은데, 이를 수정함
학기 별로 분류, 최신순 정렬
- aggregation 사용 x - 성능비교를 위해 둘 다 구현 - 파라미터 값 agg/sever 로 구분
- 지원서 양식(괸리자) : ClubApplicationForm - 지원자 또는 지원서(사용자) : ClubApplicant
지원서 양식 생성에서 허용 범위가 아닌 학기 요청 검증
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning
|
| Cohort / File(s) | Summary |
|---|---|
Controller: 양식 중심 엔드포인트backend/src/main/java/moadong/club/controller/ClubApplyController.java |
GET /api/club/{clubId}/semesters 추가. 생성/조회/수정/지원 경로를 applicationFormId 기반으로 변경(여러 GET/POST/PUT/DELETE 경로 갱신) 및 getClubApplications 추가. |
Service: 폼·학기·그룹핑 로직backend/src/main/java/moadong/club/service/ClubApplyService.java |
도메인을 ClubApplicationForm/ClubApplicant로 전환. 학기 옵션 생성·검증, 폼 생성/수정/단건/목록/그룹화 API 추가. applyToClub 등에 applicationFormId 적용 및 AES 암호화 저장·조회 로직 조정. |
엔티티/열거형 변경backend/src/main/java/moadong/club/entity/ClubApplicant.java, .../ClubApplicationForm.java, .../ClubApplicationFormQuestion.java, .../enums/ApplicantStatus.java, .../enums/SemesterTerm.java |
ClubApplication→ClubApplicant(컬렉션명 변경), questionId→formId, ApplicationStatus→ApplicantStatus 치환. ClubQuestion→ClubApplicationForm로 개명 및 semesterYear/semesterTerm 필드·갱신 메서드 추가. SemesterTerm enum 추가. |
레포지토리: 신규/삭제/커스텀 집계backend/src/main/java/moadong/club/repository/ClubApplicantsRepository.java, .../ClubApplicationFormsRepository.java, .../ClubApplicationFormsRepositoryCustom.java, (삭제) .../ClubApplicationRepository.java, (삭제) .../ClubQuestionRepository.java |
지원자·폼 전용 리포지토리 추가, 폼 slim 투영 쿼리 및 학기별 그룹화 Aggregation 구현. 기존 구형 레포지토리 2건 제거. |
DTO/요청/응답: 추가·교체·삭제.../payload/dto/ClubApplicantsResult.java, .../payload/dto/ClubApplicationFormsResult*.java, .../payload/dto/ClubApplicationFormSlim.java, .../payload/request/ClubApplicationFormCreateRequest.java, .../payload/request/ClubApplicationFormEditRequest.java, .../payload/request/ClubApplicantEditRequest.java, .../payload/response/ClubApplicationFormResponse.java, .../payload/response/ClubApplicationFormsResponse.java, .../payload/response/SemesterOptionResponse.java, (삭제) .../payload/response/ClubApplicationResponse.java |
ApplicantsResult가 ClubApplicant/ApplicantStatus 사용으로 변경. 폼 생성/수정 요청에 학기 필드 및 검증 추가. 폼 응답·목록·학기 옵션 DTO 추가. 이전 응답 타입 삭제. |
테스트·픽스처 업데이트backend/src/test/java/moadong/club/service/ClubApplyServiceTest.java, backend/src/test/java/moadong/fixture/ClubApplicationEditFixture.java |
테스트/픽스처를 폼 기반 타입으로 갱신(리포지토리 및 요청 타입 교체), 동시성 테스트 경로 수정. |
오류 메시지·핸들러backend/src/main/java/moadong/global/exception/ErrorCode.java, .../GlobalExceptionHandler.java |
APPLICATION_NOT_FOUND 메시지 텍스트 변경(지원서 → 지원서 양식). 예외 핸들러 포맷 정리(동작 불변). |
Sequence Diagram(s)
sequenceDiagram
autonumber
actor U as 사용자
participant C as ClubApplyController
participant S as ClubApplyService
participant R as ClubApplicationFormsRepository(+Custom)
rect rgb(245,248,255)
note over U,C: 폼 목록 조회 (mode 쿼리)
U->>C: GET /api/club/{clubId}/apply?mode=server|agg
C->>S: getGroupedClubApplicationForms(clubId) / getClubApplicationForms(clubId)
S->>R: aggregation / findClubApplicationFormsByClubId(clubId)
R-->>S: 그룹화된 결과 / 슬림 목록
S-->>C: ClubApplicationFormsResponse
C-->>U: 200 OK (forms)
end
sequenceDiagram
autonumber
actor A as 지원자
participant C as ClubApplyController
participant S as ClubApplyService
participant FR as ClubApplicationFormsRepository
participant AR as ClubApplicantsRepository
participant ENC as AESCipher
rect rgb(245,255,245)
note over A,C: 지원 제출 (폼 기반)
A->>C: POST /api/club/{clubId}/apply/{applicationFormId}
C->>S: applyToClub(clubId, applicationFormId, request)
S->>FR: findByClubIdAndId(clubId, applicationFormId)
FR-->>S: ClubApplicationForm
S->>ENC: encrypt(answers)
ENC-->>S: encryptedAnswers
S->>AR: save(ClubApplicant{formId, answers, status=SUBMITTED})
AR-->>S: saved
S-->>C: 200 OK
C-->>A: 응답 반환
end
sequenceDiagram
autonumber
actor O as 동아리_소유자
participant C as ClubApplyController
participant S as ClubApplyService
rect rgb(255,248,240)
note over O,C: 학기 옵션 조회
O->>C: GET /api/club/{clubId}/semesters?option=3
C->>S: getSemesterOption(clubId, count)
S-->>C: List<SemesterOptionResponse>
C-->>O: 200 OK (options)
end
Estimated code review effort
🎯 4 (Complex) | ⏱️ ~60 minutes
Possibly related issues
- [feature] MOA-231 동아리 지원서 제작 시에 학기를 선택할 수 있다 #732: 학기 선택으로 지원서(양식) 생성 기능 목표와 직접적으로 일치 — 이번 PR이 semesterYear/semesterTerm 추가 및 학기 옵션·그룹화 구현을 포함함.
- [feature] MOA-231 동아리 지원서 제작 시에 학기를 선택할 수 있다 #732 (중복 검색 항목) — repository/DTO/컨트롤러 레이어에서 동일한 학기 기반 요구사항 반영 여부와 관련.
Possibly related PRs
- [feature] 지원서 제출 상태 변경 API 추가 #621 — applicant 편집 API 변형 및 서비스 레벨 변경과 밀접 연관(컨트롤러/서비스의 applicant-edit 관련 변경 유사).
- [refactor] be 리팩토링 #673 — applicant 상태(enum) 및 getClubApplyInfo 관련 로직 변경과 중복되는 타입/흐름 수정이 있음.
- [Release] BE v1.0.7 배포 #734 — 컨트롤러의 지원자 편집/삭제 엔드포인트 변경과 공유되는 코드 레이어(엔드포인트 시그니처 변경) 관련성 높음.
Suggested reviewers
- oesnuj
- PororoAndFriends
- suhyun113
Pre-merge checks and finishing touches
❌ Failed checks (2 warnings)
| Check name | Status | Explanation | Resolution |
|---|---|---|---|
| Out of Scope Changes Check | PR에는 학기 선택 기능을 넘는 광범위한 리팩터와 API 시그니처 변경이 포함되어 있어 일부 변경이 원래 이슈 범위를 벗어날 가능성이 높습니다. 예를 들어 엔티티·도큐먼트명 변경(ClubQuestion→ClubApplicationForm, ClubApplication→ClubApplicant), enum명 변경(ApplicationStatus→ApplicantStatus), 기존 저장소 인터페이스 삭제 및 신규 저장소/커스텀 리포지토리 추가, 다수 엔드포인트의 경로·DTO·메서드 시그니처 변경(applicationFormId 필수화), 기존 응답 타입 삭제 등이 확인됩니다. 이러한 변경은 데이터 마이그레이션, 프론트엔드 영향, 후방호환성 검토가 필요하므로 별도 검토나 PR 분리를 권장합니다. | 권장 조치: 학기 선택/조회 관련 변경만 별도 작은 PR로 분리하거나, 현재 PR을 유지할 경우 변경된 엔드포인트·시그니처·DTO 목록과 데이터 마이그레이션 계획 및 프론트엔드 영향 분석을 PR 설명에 명확히 추가하십시오. 또한 도큐먼트/저장소 이름 변경에 대한 마이그레이션 스크립트 또는 호환 레이어와 통합 테스트를 포함해 배포 안전성을 확보해야 합니다. 최종적으로 프론트엔드 팀과 API 계약(버전·경로·필드)을 조정한 뒤 병합 승인을 받으십시오. | |
| Docstring Coverage | Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. | You can run @coderabbitai generate docstrings to improve docstring coverage. |
✅ Passed checks (3 passed)
| Check name | Status | Explanation |
|---|---|---|
| Description Check | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled. |
| Title Check | ✅ Passed | 제목은 PR의 핵심 변경사항인 "지원서 폼에 학기 선택 추가"와 "생성된 폼을 학기별로 분류하여 조회"를 명확히 요약하고 있으며 실제 변경 내용과 일치합니다. 문장이 다소 길지만 불필요한 파일 목록이나 이모지 없이 구체적이고 스캔하는 동료에게 주요 포인트를 전달합니다. 따라서 제목은 명확하고 적절합니다. |
| Linked Issues Check | ✅ Passed | PR은 ClubApplicationForm에 semesterYear·semesterTerm 필드를 추가하고 생성·수정·조회 API와 /api/club/{clubId}/semester 엔드포인트를 업데이트하여 서버에서 학기 옵션(isExist 포함)을 제공하는 등 MOA-231의 주요 요구사항(폼 생성 시 학기 선택 제공 및 서버 측 학기 옵션 노출)을 충족합니다. DTO·서비스·저장소 계층이 메타데이터를 처리하도록 변경되었고 관련 응답 타입도 추가되어 클라이언트에 필요한 정보가 제공됩니다. 링크된 이슈의 체크리스트 일부가 미완료로 보이나 본 PR은 주된 목표를 달성하고 있습니다. |
✨ Finishing touches
- 📝 Generate Docstrings
🧪 Generate unit tests
- Create PR with unit tests
- Post copyable unit tests in a comment
- Commit unit tests in branch
feature/#732-applicaton-semester-selection-MOA-231
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
Comment @coderabbitai help to get the list of available commands and usage tips.
There was a problem hiding this comment.
Actionable comments posted: 10
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
backend/src/main/java/moadong/club/payload/request/ClubApplicationFormEditRequest.java (1)
10-22: 긴급: Edit 요청에 semesterYear/semesterTerm 필드 누락 — 서비스에서 사용되어 빌드 실패 발생
- 문제: backend/src/main/java/moadong/club/payload/request/ClubApplicationFormEditRequest.java에 semesterYear/semesterTerm가 없음. 하지만 backend/src/main/java/moadong/club/service/ClubApplyService.java에서 request.semesterYear()/request.semesterTerm()를 호출함(예: 라인 95, 100, 408–409).
- 조치: CreateRequest와 동일한 제약으로 필드 추가 필요(예: @NotNull Integer semesterYear with @min(2000) @max(2999) 및 @NotNull SemesterTerm semesterTerm). 관련 테스트/fixture(backend/src/test/.../ClubApplicationEditFixture.java)와 프론트 API(예: frontend/src/apis/application/updateApplication.ts)도 함께 수정/검증.
backend/src/main/java/moadong/club/entity/ClubApplicant.java (1)
3-5: Mongo 엔티티에서 JPA @id 사용 중 — Spring Data @id로 교체 필요MongoDB 매핑에는
org.springframework.data.annotation.Id를 사용해야 합니다. 현재jakarta.persistence.Id를 쓰고 있어 혼동을 유발합니다. 동일한 문제를 예방하려면 Spring Data 어노테이션으로 통일하세요.적용 예:
-import jakarta.persistence.Id; +import org.springframework.data.annotation.Id;Also applies to: 24-25
backend/src/test/java/moadong/club/service/ClubApplyServiceTest.java (1)
85-96: 실제 동시성 테스트가 되지 않음
numberOfThreads = 1이면 충돌이 재현되지 않습니다. 5 이상으로 올리고, 타임아웃을 추가하세요.- int numberOfThreads = 1; + int numberOfThreads = 5; ... - latch.await(); + if (!latch.await(java.time.Duration.ofSeconds(10))) { + throw new AssertionError("동시성 테스트 타임아웃"); + }
🧹 Nitpick comments (33)
backend/src/main/java/moadong/club/entity/ClubApplicationFormQuestion.java (1)
27-33: 유효성 제약 정합성 확인 요청.CHOICE/다중선택 유형에서 options/items가 비어 있으면 유효하지 않을 수 있습니다. 현재 @NotNull만 있으므로, 타입별로 items 비어 있음 금지(@notempty) 같은 검증이 서비스/컨트롤러 어디에서든 보장되는지 확인 바랍니다.
backend/src/main/java/moadong/club/payload/request/ClubApplicantEditRequest.java (1)
6-18: ID/메모 제약 재검토(선택).
- applicantId가 Mongo ObjectId라면 패턴 제약을 두면 API 조기 검증에 유리합니다.
- memo는 @NotNull인데 빈 문자열 허용 여부 합의 필요합니다(빈 문자열 허용하려면 @notblank로 상향, 아니면 @SiZe만 유지).
-import jakarta.validation.constraints.Size; +import jakarta.validation.constraints.Size; +import jakarta.validation.constraints.Pattern; ... @NotBlank - String applicantId, + @Pattern(regexp = "^[a-fA-F0-9]{24}$", message = "invalid ObjectId") + String applicantId,backend/src/main/java/moadong/club/payload/dto/ClubApplicationFormsResultItem.java (1)
5-9: 시간대 명시 고려: LocalDateTime → OffsetDateTime 권장(선택).API 응답 직렬화 시 타임존 혼선을 줄이기 위해 OffsetDateTime/Instant 사용을 권장합니다. KST 기준이면 서버/문서에 명시해 주세요.
-import java.time.LocalDateTime; +import java.time.OffsetDateTime; ... - LocalDateTime editedAt + OffsetDateTime editedAtbackend/src/test/java/moadong/fixture/ClubApplicationEditFixture.java (1)
8-21: Edit 요청 스키마 변경 반영 필요.위에서 제안한 학기 필드 추가 시, 픽스처도 함께 업데이트해야 테스트가 컴파일/통과합니다.
- public static ClubApplicationFormEditRequest createClubApplicationEditRequest(){ + public static ClubApplicationFormEditRequest createClubApplicationEditRequest(){ ... - return new ClubApplicationFormEditRequest( - "테스트123", - "테스트 지원서입니다", - new ArrayList<>() - ); + return new ClubApplicationFormEditRequest( + "테스트123", + "테스트 지원서입니다", + 2025, + moadong.club.enums.SemesterTerm.FIRST, + new ArrayList<>() + );backend/src/main/java/moadong/club/enums/ApplicantStatus.java (1)
3-8: 타입명 변경 영향 점검 — ApplicationStatus 참조 없음 / 주석 오탈자 수정 권장
- ApplicationStatus → ApplicantStatus 변경: 레포 검색 결과 ApplicationStatus 사용처 없음(ApplicantStatus만 사용됨).
- enum 주석 "불합"을 "불합격"으로 수정 제안.
- DECLINED, // 불합 + DECLINED, // 불합격backend/src/main/java/moadong/club/repository/ClubApplicantsRepository.java (2)
10-14: 메서드 파라미터 명이 도메인과 불일치합니다
findAllByFormId(String questionId)에서 파라미터 명은formId가 맞습니다.findAllByIdInAndFormId(List<String> ids, String clubId)도formId로 바꾸는 것이 혼동을 줄입니다.아래처럼 정리해 주세요.
- List<ClubApplicant> findAllByFormId(String questionId); + List<ClubApplicant> findAllByFormId(String formId); - List<ClubApplicant> findAllByIdInAndFormId(List<String> ids, String clubId); + List<ClubApplicant> findAllByIdInAndFormId(List<String> ids, String formId);
10-11: 조회 성능을 위한 인덱스 제안
@Query("{ 'formId': ?0, 'status': { $exists: true, $ne: 'DRAFT' } }")패턴이 빈번하다면formId, status에 복합 인덱스 추가를 고려해 주세요. 엔티티에@CompoundIndex를 두거나 마이그레이션 스크립트로 생성하면 좋습니다.backend/src/main/java/moadong/club/payload/response/SemesterOptionResponse.java (1)
7-12: 필드 명을 일관되게semesterTerm로 맞추는 것을 권장다른 DTO/엔티티가
semesterTerm를 사용합니다. 응답도 동일하게 맞추면 혼란을 줄일 수 있습니다.- public record SemesterOptionResponse( - int semesterYear, - SemesterTerm term - //boolean isExist //해당 학기에 이미 지원서가 존재하는지 - ) { + public record SemesterOptionResponse( + int semesterYear, + SemesterTerm semesterTerm + //boolean isExist //해당 학기에 이미 지원서가 존재하는지 + ) {현재 PR 범위에서
isExist포함 여부(스펙) 확정 부탁드립니다. 포함한다면 의미/계산 방식 정의가 필요합니다.backend/src/main/java/moadong/club/payload/dto/ClubApplicantsResult.java (1)
25-47: AES 복호화 에러 처리 및 DTO 변환 로직 적절예외 래핑과 로깅이 일관적입니다. 가독성을 위해 파라미터명을
applicant로 바꾸면 도메인 용어와 더 잘 맞습니다.- public static ClubApplicantsResult of(ClubApplicant application, AESCipher cipher) { + public static ClubApplicantsResult of(ClubApplicant applicant, AESCipher cipher) { ... - for (ClubQuestionAnswer answer : application.getAnswers()) { + for (ClubQuestionAnswer answer : applicant.getAnswers()) { ... - .id(application.getId()) - .status(application.getStatus()) + .id(applicant.getId()) + .status(applicant.getStatus()) ... - .memo(application.getMemo()) - .createdAt(application.getCreatedAt()) + .memo(applicant.getMemo()) + .createdAt(applicant.getCreatedAt())backend/src/main/java/moadong/club/repository/ClubApplicationFormsRepository.java (2)
12-24: 커스텀 리포지토리와 메서드 명 충돌/혼동 가능성별도
ClubApplicationFormsRepositoryCustom클래스(템플릿 기반)와 본 인터페이스의 메서드명이 동일해 사용처에서 혼동될 여지가 있습니다. Aggregation 전용 컴포넌트라면 이름을ClubApplicationFormsAggregationRepository(또는 메서드aggregateBySemester) 등으로 명확히 구분하는 것을 권장합니다.
12-24: 조회 패턴에 맞춘 인덱스 구성 제안자주 쓰이는 조건이
clubId필터 + 최신순(editedAtDESC), 그리고 그룹 정렬 시semesterYear,semesterTerm입니다. 엔티티에 아래와 같은 인덱스를 고려해 주세요.
{ clubId: 1, editedAt: -1 }{ clubId: 1, semesterYear: -1, semesterTerm: -1 }backend/src/main/java/moadong/club/entity/ClubApplicant.java (3)
29-31: JPA @Enumerated는 Mongo에서 무의미합니다
@Enumerated(EnumType.STRING)는 JPA 전용입니다. Spring Data Mongo는 기본 변환으로 Enum을 문자열로 저장합니다. 해당 어노테이션은 제거하거나, 명시가 필요하면 커스텀 컨버터를 고려하세요.- @Enumerated(EnumType.STRING) @Builder.Default ApplicantStatus status = ApplicantStatus.SUBMITTED;
39-41: 동시성 보호(낙관적 락) 필요성 검토지원자 상태/메모가 동시 수정될 수 있다면
@Version필드를 추가해 충돌을 감지하세요. 서비스 계층에서 재시도/충돌 처리와 함께 사용하면 안정성이 올라갑니다.+import org.springframework.data.annotation.Version; ... @Builder.Default LocalDateTime createdAt = ZonedDateTime.now(ZoneId.of("Asia/Seoul")).toLocalDateTime(); + @Version + private Long version;
50-53: 방어적 복사/널 가드
updateAnswers에서answers가null이면 NPE가 납니다. 방어적 복사로 안전하게 처리하세요.- public void updateAnswers(List<ClubQuestionAnswer> answers) { - this.answers.clear(); - this.answers.addAll(answers); - } + public void updateAnswers(List<ClubQuestionAnswer> answers) { + this.answers.clear(); + if (answers != null) { + this.answers.addAll(List.copyOf(answers)); + } + }backend/src/main/java/moadong/club/repository/ClubApplicationFormsRepositoryCustom.java (3)
3-3: 불필요한 import 정리
BasicDBObject,ConditionalOperators는 사용되지 않습니다. 정리해 주세요.Also applies to: 11-11
52-59: 학기 정렬 인덱스 값(-1) 처리
indexOfArray([...], "$_id.semesterTerm")가 미정의 학기(예: WINTER/SUMMER)에 대해-1을 반환할 수 있습니다. 해당 값이 존재할 가능성이 있다면 기본 순서를 보장하는 가드가 필요합니다(예:cond로 대체값 부여).operations.add(Aggregation.addFields() - .addFieldWithValue("termOrder", new Document("$indexOfArray", - Arrays.asList(Arrays.asList("FIRST", "SECOND"), "$_id.semesterTerm"))) + .addFieldWithValue("termOrder", + new Document("$let", new Document("vars", new Document("idx", + new Document("$indexOfArray", Arrays.asList(Arrays.asList("FIRST","SECOND"), "$_id.semesterTerm")))) + .append("in", new Document("$cond", Arrays.asList( + new Document("$lt", Arrays.asList("$$idx", 0)), // idx < 0 + -99, // 기본 최하위 + "$$idx"))))) .build());
64-67: 대용량 대비 allowDiskUse 옵션 활성화 제안폼이 많아지면 sort/group에서 메모리 사용이 커질 수 있습니다.
AggregationOptions.allowDiskUse(true)적용을 권장합니다.-import org.springframework.data.mongodb.core.aggregation.Aggregation; +import org.springframework.data.mongodb.core.aggregation.Aggregation; +import org.springframework.data.mongodb.core.aggregation.AggregationOptions; ... - Aggregation aggregation = Aggregation.newAggregation(operations); + Aggregation aggregation = Aggregation.newAggregation(operations) + .withOptions(AggregationOptions.builder().allowDiskUse(true).build());backend/src/test/java/moadong/club/service/ClubApplyServiceTest.java (2)
50-52: 예외 메시지의 도메인 용어 일치메시지에 여전히
ClubQuestion이 남아 있습니다.ClubApplicationForm으로 교체하세요.- .orElseThrow(() -> new NoSuchElementException("테스트를 위한 ClubQuestion 문서가 DB에 존재하지 않습니다. 먼저 문서를 생성해주세요.")); + .orElseThrow(() -> new NoSuchElementException("테스트를 위한 ClubApplicationForm 문서가 DB에 존재하지 않습니다. 먼저 문서를 생성해주세요."));
44-52: 테스트 독립성 보강 제안DB 사전 데이터에 의존하지 말고,
setUp()에서 테스트 전용ClubApplicationForm을 생성/저장하여 일관성을 확보하는 편이 좋습니다.필요하시면 픽스처/리포지토리 호출을 포함한 셋업 코드 초안을 드리겠습니다.
backend/src/main/java/moadong/club/entity/ClubApplicationForm.java (2)
67-70: 널 가드/불변성 확보
newQuestions가null일 경우 NPE가 납니다. 또한 외부 리스트 참조가 내부로 그대로 들어와 불변성이 깨질 수 있습니다. 방어적 복사를 권장합니다.- public void updateQuestions(List<ClubApplicationFormQuestion> newQuestions) { - this.questions.clear(); - this.questions.addAll(newQuestions); - } + public void updateQuestions(List<ClubApplicationFormQuestion> newQuestions) { + this.questions.clear(); + if (newQuestions != null) { + this.questions.addAll(List.copyOf(newQuestions)); + } + }
72-74: 시간 생성 로직 중복 최소화
ZonedDateTime.now(ZoneId.of("Asia/Seoul"))가 반복됩니다. 상수화하거나 유틸 메서드로 추출하면 테스트 용이성과 유지보수성이 향상됩니다.backend/src/main/java/moadong/club/controller/ClubApplyController.java (4)
29-37: 학기 옵션 API 문서 vs Enum/집계 불일치 확인예시에
겨울학기가 포함되어 있으나, 엔티티/집계는FIRST/SECOND만 전제로 보입니다. 실제 반환 가능 값과 예시를 일치시켜 주세요(예:2025-2학기, 2026-1학기, 2026-2학기).
50-58: 목록 모드 플래그 의미 명확화
mode=agg/server의 의미가 직관적이지 않습니다. Enum 파라미터로 제한하거나 스웨거 문서에 두 모드의 차이를 명시해 주세요. 기본값(agg)이 어떤 데이터 구조를 반환하는지(그룹 vs 평탄)도 문서화가 필요합니다.
79-86: 지원 API 인증 요구사항 확인
applyToClub는 인증 어노테이션이 없습니다. 익명 지원을 허용하는 정책이라면 OK입니다. 아니라면@PreAuthorize("isAuthenticated()")추가를 검토하세요.
60-69: 메서드·파라미터명 일관성 필요 — Controller: editClubApplicationForm(applicationFormId) vs Service: editClubApplication(clubQuestionId)
컨트롤러 메서드명과 서비스 메서드명이 불일치하고 파라미터명(applicationFormId vs clubQuestionId)도 다릅니다. 네이밍을 통일해 탐색성과 일관성을 확보하세요.
위치: backend/src/main/java/moadong/club/controller/ClubApplyController.java, backend/src/main/java/moadong/club/service/ClubApplyService.javabackend/src/main/java/moadong/club/service/ClubApplyService.java (8)
97-105: 불필요한createQuestions중복 호출같은 객체에 두 번 적용하고 저장하고 있습니다. 불필요한 연산이며, 잠재적 부작용 우려가 있습니다.
적용 패치:
ClubApplicationForm clubApplicationForm = createQuestions( ClubApplicationForm.builder() .clubId(clubId) .semesterYear(request.semesterYear()) .semesterTerm(request.semesterTerm()) .build(), request); - clubApplicationFormsRepository.save(createQuestions(clubApplicationForm, request)); + clubApplicationFormsRepository.save(clubApplicationForm);
80-86: 학기 옵션 개수 검증 로직 불일치 가능성
getSemesterOption(count)는 가변count를 허용하지만validateSemester는 고정SEMESTER_OPTION_COUNT=3만 허용합니다. 프론트가 4개 이상 옵션을 표시하면 서버에서 거부될 수 있습니다.
- 하나의 소스(예: 설정값 또는 파라미터)로 통일하거나
validateSemester에count를 인자로 받도록 변경하세요.Also applies to: 45-57
107-116: 에러 코드/파라미터 명명 일관성
QUESTION_NOT_FOUND,clubQuestionId등 과거 도메인 명칭이 남아 있습니다. 현재 도메인(폼 중심)에 맞춰APPLICATION_NOT_FOUND,applicationFormId등으로 정리하세요.Also applies to: 118-126
343-346: NPE 가능성: 옵션이 null인 기존 데이터 대비
question.getOptions()가 null일 경우 NPE가 발생합니다. 방어 코드 추가가 필요합니다.적용 패치:
- if (question.getOptions().getRequired() && !answerIds.contains(question.getId())) { + if (question.getOptions() != null + && question.getOptions().getRequired() + && !answerIds.contains(question.getId())) { throw new RestApiException(ErrorCode.REQUIRED_QUESTION_MISSING); }
362-375: NPE 가능성: 답변 값이 null일 때 길이 검증
value.length()호출 전에 null 처리가 필요합니다. 또한 기타 타입 대비 기본 분기를 두는 것이 안전합니다.적용 패치:
- private void validateAnswerLength(String value, ClubApplicationQuestionType type) { + private void validateAnswerLength(String value, ClubApplicationQuestionType type) { + if (value == null) value = ""; switch (type) { case SHORT_TEXT -> { if (value.length() > 100) { throw new RestApiException(ErrorCode.SHORT_EXCEED_LENGTH); } } case LONG_TEXT -> { if (value.length() > 1000) { throw new RestApiException(ErrorCode.LONG_EXCEED_LENGTH); } } + default -> { + // 길이 제한 없는 타입은 패스 + } } }
273-275: 중복 답변 ID 입력 시 IllegalStateException 위험
Collectors.toMap병합 함수 미지정으로 중복 키가 들어오면 런타임 예외가 발생합니다. 병합 전략을 명시하세요.적용 패치:
- Map<Long, ClubQuestionAnswer> answerMap = app.getAnswers().stream() - .collect(Collectors.toMap(ClubQuestionAnswer::getId, answer -> answer)); + Map<Long, ClubQuestionAnswer> answerMap = app.getAnswers().stream() + .collect(Collectors.toMap( + ClubQuestionAnswer::getId, + Function.identity(), + (prev, next) -> next, + LinkedHashMap::new));
45-57: 기능 개선 제안: 학기 옵션에 “이미 해당 학기 폼 존재 여부(exists)” 포함TODO로 남겨둔 부분을 실제 반영하면 UX가 좋아집니다.
clubId + (year, term)로 존재 여부를 계산해SemesterOptionResponse에 boolean 필드를 추가하는 것을 권장합니다.원하시면 리포지토리 쿼리 시그니처/인덱스 설계까지 제안드릴게요.
27-31: 테스트 용이성/일관성: 시간 소스 주입
ZonedDateTime.now(Asia/Seoul)가 여러 곳에 하드코딩되어 있어 테스트가 어렵습니다.Clock또는Supplier<LocalDate>주입으로 치환을 권장합니다.Also applies to: 82-83
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Jira integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (26)
backend/src/main/java/moadong/club/controller/ClubApplyController.java(1 hunks)backend/src/main/java/moadong/club/entity/ClubApplicant.java(3 hunks)backend/src/main/java/moadong/club/entity/ClubApplicationForm.java(5 hunks)backend/src/main/java/moadong/club/entity/ClubApplicationFormQuestion.java(1 hunks)backend/src/main/java/moadong/club/enums/ApplicantStatus.java(1 hunks)backend/src/main/java/moadong/club/enums/SemesterTerm.java(1 hunks)backend/src/main/java/moadong/club/payload/dto/ClubApplicantsResult.java(2 hunks)backend/src/main/java/moadong/club/payload/dto/ClubApplicationFormsResult.java(1 hunks)backend/src/main/java/moadong/club/payload/dto/ClubApplicationFormsResultItem.java(1 hunks)backend/src/main/java/moadong/club/payload/request/ClubApplicantEditRequest.java(2 hunks)backend/src/main/java/moadong/club/payload/request/ClubApplicationEditRequest.java(0 hunks)backend/src/main/java/moadong/club/payload/request/ClubApplicationFormCreateRequest.java(1 hunks)backend/src/main/java/moadong/club/payload/request/ClubApplicationFormEditRequest.java(1 hunks)backend/src/main/java/moadong/club/payload/response/ClubApplicationFormResponse.java(1 hunks)backend/src/main/java/moadong/club/payload/response/ClubApplicationFormsResponse.java(1 hunks)backend/src/main/java/moadong/club/payload/response/ClubApplicationResponse.java(0 hunks)backend/src/main/java/moadong/club/payload/response/SemesterOptionResponse.java(1 hunks)backend/src/main/java/moadong/club/repository/ClubApplicantsRepository.java(1 hunks)backend/src/main/java/moadong/club/repository/ClubApplicationFormSlim.java(1 hunks)backend/src/main/java/moadong/club/repository/ClubApplicationFormsRepository.java(1 hunks)backend/src/main/java/moadong/club/repository/ClubApplicationFormsRepositoryCustom.java(1 hunks)backend/src/main/java/moadong/club/repository/ClubApplicationRepository.java(0 hunks)backend/src/main/java/moadong/club/repository/ClubQuestionRepository.java(0 hunks)backend/src/main/java/moadong/club/service/ClubApplyService.java(13 hunks)backend/src/test/java/moadong/club/service/ClubApplyServiceTest.java(4 hunks)backend/src/test/java/moadong/fixture/ClubApplicationEditFixture.java(2 hunks)
💤 Files with no reviewable changes (4)
- backend/src/main/java/moadong/club/repository/ClubQuestionRepository.java
- backend/src/main/java/moadong/club/payload/request/ClubApplicationEditRequest.java
- backend/src/main/java/moadong/club/repository/ClubApplicationRepository.java
- backend/src/main/java/moadong/club/payload/response/ClubApplicationResponse.java
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-08-25T14:43:52.320Z
Learnt from: lepitaaar
PR: Moadong/moadong#703
File: backend/src/main/java/moadong/club/controller/ClubApplyController.java:84-84
Timestamp: 2025-08-25T14:43:52.320Z
Learning: In the Moadong codebase, questionId and clubId are equivalent identifiers that represent the same entity. The ClubApplicationRepository.findAllByIdInAndQuestionId method correctly uses clubId as the questionId parameter for filtering club applications.
Applied to files:
backend/src/main/java/moadong/club/repository/ClubApplicantsRepository.javabackend/src/test/java/moadong/fixture/ClubApplicationEditFixture.javabackend/src/main/java/moadong/club/repository/ClubApplicationFormsRepositoryCustom.javabackend/src/main/java/moadong/club/entity/ClubApplicationForm.javabackend/src/main/java/moadong/club/payload/dto/ClubApplicantsResult.javabackend/src/main/java/moadong/club/repository/ClubApplicationFormsRepository.javabackend/src/main/java/moadong/club/entity/ClubApplicationFormQuestion.javabackend/src/main/java/moadong/club/entity/ClubApplicant.javabackend/src/main/java/moadong/club/service/ClubApplyService.javabackend/src/test/java/moadong/club/service/ClubApplyServiceTest.java
📚 Learning: 2025-05-19T05:45:52.957Z
Learnt from: lepitaaar
PR: Moadong/moadong#406
File: backend/src/main/java/moadong/club/service/ClubApplyService.java:34-38
Timestamp: 2025-05-19T05:45:52.957Z
Learning: The code duplication between createClubApplication and editClubApplication methods in ClubApplyService.java is acknowledged but will be addressed in a future refactoring, as per the developer's plan.
Applied to files:
backend/src/test/java/moadong/fixture/ClubApplicationEditFixture.javabackend/src/main/java/moadong/club/payload/request/ClubApplicationFormEditRequest.javabackend/src/main/java/moadong/club/payload/dto/ClubApplicantsResult.javabackend/src/main/java/moadong/club/entity/ClubApplicant.javabackend/src/main/java/moadong/club/service/ClubApplyService.javabackend/src/test/java/moadong/club/service/ClubApplyServiceTest.javabackend/src/main/java/moadong/club/controller/ClubApplyController.java
🧬 Code graph analysis (5)
backend/src/main/java/moadong/club/entity/ClubApplicationForm.java (1)
backend/src/main/java/moadong/club/entity/ClubApplicationFormQuestion.java (1)
AllArgsConstructor(13-37)
backend/src/main/java/moadong/club/repository/ClubApplicationFormsRepository.java (1)
backend/src/main/java/moadong/club/repository/ClubApplicationFormsRepositoryCustom.java (1)
Repository(20-71)
backend/src/main/java/moadong/club/entity/ClubApplicant.java (1)
backend/src/main/java/moadong/club/entity/ClubApplicationForm.java (1)
Document(19-88)
backend/src/main/java/moadong/club/service/ClubApplyService.java (1)
backend/src/main/java/moadong/club/service/ClubProfileService.java (1)
Service(23-63)
backend/src/test/java/moadong/club/service/ClubApplyServiceTest.java (1)
backend/src/test/java/moadong/fixture/ClubApplicationEditFixture.java (1)
ClubApplicationEditFixture(7-23)
🔇 Additional comments (14)
backend/src/main/java/moadong/club/enums/SemesterTerm.java (1)
1-6: 용어/경계 정의 확인(1학기/2학기 경계 월).FIRST/SECOND 경계(예: 3–8월/9–2월 등)를 서버가 어떻게 계산하는지 합의가 필요합니다. 경계 로직이 컨트롤러/서비스에 구현된다면 타임존(KST)도 명시해 주세요.
backend/src/main/java/moadong/club/payload/response/ClubApplicationFormsResponse.java (1)
8-12: LGTM – 응답 래퍼 타입 적절.@builder가 record에 잘 적용되고, 계층 응답(forms: List<...>) 구조 명확합니다.
backend/src/main/java/moadong/club/repository/ClubApplicationFormSlim.java (1)
7-13: 프로젝션 정의 좋습니다. 다만 _id → getId() 매핑 확인 필요Mongo @query(fields)로 '_id'만 노출할 때 인터페이스 프로젝션의
getId()에 자동 매핑되는지(특히 엔티티가 아닌 인터페이스 프로젝션에서) 동작 확인 부탁드립니다. 필요하면 Aggregation에서as("id")로 별칭을 주거나,@Value("#{target.id}")를 고려해 주세요.backend/src/main/java/moadong/club/payload/dto/ClubApplicationFormsResult.java (1)
6-10: LGTM학기 단위 그룹 결과를 담는 DTO로 적절합니다.
backend/src/main/java/moadong/club/payload/request/ClubApplicationFormCreateRequest.java (2)
22-28: 연도/학기 검증 범위 OK
@Min(2000) @Max(2999)및@NotNull조합 적절합니다. 서버 제공 학기 옵션과도 범위 일관성 유지 부탁드립니다.
18-21: 검증 결과 — 리뷰 지적이 부정확합니다backend/src/main/java/moadong/club/payload/request/ClubApplyQuestion.java에 public record ClubApplyQuestion가 정의되어 있으며, backend/src/main/java/moadong/club/payload/request/ClubApplicationFormEditRequest.java(questions 필드)과 테스트(backend/src/test/java/moadong/fixture/ClubApplicationEditFixture.java)에서 사용 중입니다. 따라서 'ClubApplyQuestion' 미정/오타 및 moadong.club.entity.ClubApplicationFormQuestion로의 교체·임포트 제안은 부적절합니다. 요청 바운더리에서 엔티티 직접 노출 문제는 해당되지 않음 — 필요 시 별도 요청 DTO로 분리하는 리팩터는 선택적으로 권장합니다.
Likely an incorrect or invalid review comment.
backend/src/main/java/moadong/club/payload/response/ClubApplicationFormResponse.java (1)
9-16: LGTM응답 DTO 구성 및 Lombok @builder 사용이 적절합니다. 생성 요청 DTO와 질문 타입 일관성만 위 파일 수정과 함께 맞춰 주세요.
backend/src/main/java/moadong/club/repository/ClubApplicationFormsRepository.java (1)
18-23: 프로젝션의_id→getId()매핑 동작 확인
fields에서_id만 노출하고 인터페이스 프로젝션에서getId()를 사용합니다. Spring Data Mongo가 인터페이스 프로젝션에도_id를id로 매핑하는지 확인 부탁드립니다. 문제가 있으면 Aggregation 기반으로as("id")별칭을 주거나, 프로젝션 메서드에 SpEL(@Value("#{target.id}"))을 적용하는 방법을 검토하세요.backend/src/main/java/moadong/club/repository/ClubApplicationFormsRepositoryCustom.java (2)
38-41: 사전 정렬 기준 재확인그룹 내부 정렬을 위해
editedAt desc, id desc로 1차 정렬하는 접근은 적절합니다. 👍
29-36: DTO 매핑/필드 투영 적절
_id→id,title,editedAt,semesterYear/Term투영과 결과 매핑 구성이 명확합니다. 👍Also applies to: 60-67
backend/src/main/java/moadong/club/entity/ClubApplicationForm.java (1)
47-55: 학기 기본값/도큐먼트 불일치 가능성엔티티는
FIRST/SECOND만 고려하지만, 컨트롤러/테스트 주석에는WINTER/SUMMER가 언급됩니다. Enum 정의, 저장 값, 집계 정렬 규칙이 모두 일치하는지 확인이 필요합니다. 불일치 시 옵션 API/집계 결과가 잘못될 수 있습니다.backend/src/main/java/moadong/club/controller/ClubApplyController.java (1)
39-47: 생성 API 요청 검증 범위 확인
semesterYear/semesterTerm가 요청에 포함된다면 입력 범위(연도 하한/상한, 허용 가능한 Term) 검증을 서비스/Validator에서 보장하는지 확인해 주세요.backend/src/test/java/moadong/club/service/ClubApplyServiceTest.java (1)
100-104: 서비스 메서드 시그니처 정합성 확인 필요서비스에 editClubApplication(String clubId, String clubQuestionId, CustomUserDetails, ClubApplicationFormEditRequest)와 editClubApplicationQuestion(String questionId, CustomUserDetails, ClubApplicationFormEditRequest)이 모두 존재합니다. 테스트는 editClubApplicationQuestion(this.clubApplicationForm.getId(), ...)를 호출(backend/src/test/java/moadong/club/service/ClubApplyServiceTest.java:103)하고, 컨트롤러는 editClubApplication(clubId, applicationFormId, user, request)를 호출(backend/src/main/java/moadong/club/controller/ClubApplyController.java:68)합니다. 의도 확인 후 조치: 테스트를 컨트롤러 시그니처에 맞춰 변경하거나, editClubApplicationQuestion을 유지하는 이유(예: 소유자 검증 우회)를 테스트 주석/이름에 명확히 남기십시오. (서비스 정의: backend/src/main/java/moadong/club/service/ClubApplyService.java:108,118)
backend/src/main/java/moadong/club/service/ClubApplyService.java (1)
149-190: 그룹 정렬은 OK, 단 아이템 정렬 기준 확인 권장그룹 내 아이템은 상단
questionSlims소팅(editedAt DESC, id DESC)을 그대로 따릅니다. 의도된 UX인지 확인 바랍니다.
| @@ -1,12 +1,11 @@ | |||
| package moadong.club.entity; | |||
|
|
|||
| import jakarta.persistence.Id; | |||
There was a problem hiding this comment.
Mongo 엔티티에서 JPA @id 사용 — Spring Data @id로 교체
jakarta.persistence.Id 대신 org.springframework.data.annotation.Id를 사용하세요.
-import jakarta.persistence.Id;
+import org.springframework.data.annotation.Id;Also applies to: 25-26
🤖 Prompt for AI Agents
In backend/src/main/java/moadong/club/entity/ClubApplicationForm.java around
lines 3 and 25-26, the class imports and/or uses the JPA @Id
(jakarta.persistence.Id); replace that with Spring Data's @Id by changing the
import to org.springframework.data.annotation.Id and remove the
jakarta.persistence.Id import, ensuring only the Spring Data @Id is referenced
on the field(s) or getter(s).
| @AllArgsConstructor | ||
| @Getter | ||
| @Builder(toBuilder = true) | ||
| public class ClubApplicationQuestion { | ||
| public class ClubApplicationFormQuestion { |
There was a problem hiding this comment.
❓ Verification inconclusive
엔티티/임베디드 매핑 안전성 보강 필요 (기본 생성자 누락).
영속 계층(JPA/Mongo)에서 매핑할 가능성이 높은데, @AllArgsConstructor만 있고 기본 생성자가 없어 런타임 매핑 실패 위험이 있습니다. JPA를 쓴다면 @embeddable(+ protected no-arg), Mongo를 쓴다면 기본 생성자 추가가 안전합니다. 또한 @Enumerated는 JPA 전용이라 Mongo 환경이면 의미 없습니다. 환경에 맞게 정리 부탁드립니다.
아래처럼 최소한 기본 생성자만 추가하는 패치를 제안합니다:
import lombok.AllArgsConstructor;
+import lombok.NoArgsConstructor;
+import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
@AllArgsConstructor
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Builder(toBuilder = true)
public class ClubApplicationFormQuestion {기본 생성자 추가 (JPA/Mongo 매핑 안전성 보강)
File: backend/src/main/java/moadong/club/entity/ClubApplicationFormQuestion.java (lines 13–16)
- JPA/ORM 또는 Mongo 매핑 시 protected no-arg 생성자가 필요합니다(현재 @AllArgsConstructor만 있어 런타임 매핑 실패 위험). 아래 제안 패치 적용하세요:
import lombok.AllArgsConstructor;
+import lombok.NoArgsConstructor;
+import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
@AllArgsConstructor
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Builder(toBuilder = true)
public class ClubApplicationFormQuestion {- @Enumerated는 JPA 전용 어노테이션입니다. Mongo 환경이면 무의미하니 사용 환경에 맞게 정리(제거 또는 조건부 적용)하세요.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| @AllArgsConstructor | |
| @Getter | |
| @Builder(toBuilder = true) | |
| public class ClubApplicationQuestion { | |
| public class ClubApplicationFormQuestion { | |
| import lombok.AllArgsConstructor; | |
| import lombok.NoArgsConstructor; | |
| import lombok.AccessLevel; | |
| import lombok.Builder; | |
| import lombok.Getter; | |
| @AllArgsConstructor | |
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | |
| @Getter | |
| @Builder(toBuilder = true) | |
| public class ClubApplicationFormQuestion { |
🤖 Prompt for AI Agents
backend/src/main/java/moadong/club/entity/ClubApplicationFormQuestion.java lines
13-16: The class currently only has @AllArgsConstructor which risks JPA/Mongo
mapping failures because a protected no-arg constructor is required; add a
protected no-arg constructor (either with Lombok @NoArgsConstructor(access =
AccessLevel.PROTECTED) or an explicit protected ClubApplicationFormQuestion() {
}) so ORM can instantiate the entity, and address the @Enumerated usage by
removing it if this entity is persisted to MongoDB or making its presence
conditional for JPA-only environments (i.e., remove @Enumerated for Mongo
mappings or keep it only when using JPA).
backend/src/main/java/moadong/club/repository/ClubApplicationFormsRepository.java
Outdated
Show resolved
Hide resolved
backend/src/main/java/moadong/club/service/ClubApplyService.java
Outdated
Show resolved
Hide resolved
backend/src/main/java/moadong/club/service/ClubApplyService.java
Outdated
Show resolved
Hide resolved
backend/src/main/java/moadong/club/service/ClubApplyService.java
Outdated
Show resolved
Hide resolved
backend/src/main/java/moadong/club/service/ClubApplyService.java
Outdated
Show resolved
Hide resolved
Test Results72 tests 69 ✅ 7s ⏱️ Results for commit 5168b70. ♻️ This comment has been updated with latest results. |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (12)
backend/src/main/java/moadong/club/payload/dto/ClubApplicantsResult.java (5)
20-21: DTO에서 엔티티 타입 누출: answers는 얕은 엔티티가 아니라 전용 DTO로 캡슐화 권장API 응답에
entity.ClubQuestionAnswer가 직접 노출되면 영속 계층 변화가 API 계약에 전파됩니다. 단순id/value만 필요하므로 내부 전용 DTO(중첩 record)로 치환을 권합니다.아래와 같이 타입을 교체하세요:
- List<ClubQuestionAnswer> answers, + List<Answer> answers,매핑도 함께 갱신합니다:
- List<ClubQuestionAnswer> decryptedAnswers = new ArrayList<>(); + List<Answer> decryptedAnswers = new ArrayList<>(); @@ - decryptedAnswers.add(ClubQuestionAnswer.builder() - .id(answer.getId()) - .value(decryptedValue) - .build()); + decryptedAnswers.add(new Answer(answer.getId(), decryptedValue));그리고 파일 내부(메서드 위쪽/아래쪽 아무 곳)에는 다음 중첩 record를 추가하세요:
public static record Answer(String id, String value) {}
25-33: NPE 예방 및 사소한 성능 개선: null/blank 방어 + pre-size
application.getAnswers()또는answer.getValue()가 null일 경우 NPE가 납니다. 또한ArrayList용량을 미리 잡아 미세 최적화할 수 있습니다.- List<ClubQuestionAnswer> decryptedAnswers = new ArrayList<>(); + List<ClubQuestionAnswer> decryptedAnswers = + new ArrayList<>(application.getAnswers() == null ? 0 : application.getAnswers().size()); try { - for (ClubQuestionAnswer answer : application.getAnswers()) { - String decryptedValue = cipher.decrypt(answer.getValue()); + for (ClubQuestionAnswer answer : (application.getAnswers() == null ? List.<ClubQuestionAnswer>of() : application.getAnswers())) { + final String raw = answer.getValue(); + final String decryptedValue = (raw == null || raw.isBlank()) ? raw : cipher.decrypt(raw); decryptedAnswers.add(ClubQuestionAnswer.builder() .id(answer.getId()) .value(decryptedValue) .build()); }
34-36: 복호화 오류 로그에 컨텍스트 추가오류 추적 시 어떤 지원자/응답에서 실패했는지 알 수 있도록 최소 컨텍스트를 포함하세요. 스택트레이스는 유지합니다.
- log.error("AES_CIPHER_ERROR", e); + log.error("AES_CIPHER_ERROR applicantId={} answersCount={}", + application.getId(), + application.getAnswers() == null ? 0 : application.getAnswers().size(), + e);
39-45: 응답 리스트 불변화로 외부 변이 차단레코드는 불변이지만 보관하는 리스트는 가변입니다.
List.copyOf로 불변화하세요.- .answers(decryptedAnswers) + .answers(List.copyOf(decryptedAnswers))
8-8: DTO 팩토리 입력으로 엔티티(ClubApplicant)를 직접 받는 결합도현 구현도 동작상 문제는 없지만, 테스트/재사용성을 위해 최소 프로젝션(인터페이스 또는 전용 읽기 DTO)을 받는 쪽이 계층 결합을 낮춥니다. 이번 PR 범위를 벗어나면 보류해도 됩니다.
Also applies to: 24-24
backend/src/main/java/moadong/club/payload/request/ClubApplicationFormCreateRequest.java (3)
12-13: 중복 import 제거 필요
java.util.List가 두 번 import되었습니다. 불필요한 중복을 제거하세요.-import java.util.List; -
23-26: 질문 목록은 비어 있지 않도록 검증 강화지원서 양식에 질문이 0개인 상태를 방지하려면 리스트에 최소 크기 제약을 추가하세요.
- 의도: 최소 1개 질문이 필수인가요? 맞다면 아래처럼 제안합니다.
+import jakarta.validation.constraints.NotEmpty; @@ - @NotNull + @NotEmpty @Valid - List<ClubApplyQuestion> questions, + @Size(min = 1) + List<ClubApplyQuestion> questions,
27-34: 학기 중복 생성 방지(고유 제약/교차 검증 필요)
semesterYear와semesterTerm조합이 같은 클럽에서 중복 생성되지 않도록 서비스/DB 계층에서 고유 제약을 확인해주세요. 또한/semesters옵션에 없는 학기 조합이 들어올 경우를 막는 교차 검증도 필요합니다.
- DB 예시(엔티티에서):
@Table( name = "club_application_form", uniqueConstraints = @UniqueConstraint( name = "uk_club_semester", columnNames = {"club_id","semester_year","semester_term"} ) )
- 서비스 검증:
getSemesterOption결과와 요청 값이 일치하는지 체크.backend/src/main/java/moadong/club/controller/ClubApplyController.java (4)
33-37: 메서드 파라미터 검증 활성화를 위해 클래스에 @validated 추가
@RequestParam등에 부여한 Bean Validation 애너테이션이 동작하도록 클래스 레벨에@Validated를 추가하세요.@RestController @RequestMapping("/api/club/{clubId}") @AllArgsConstructor @Tag(name = "Club_Apply", description = "클럽 지원서 API") +@Validated public class ClubApplyController {
41-49: 학기 옵션 개수 파라미터에 하한 검증 추가
option(count) 값이 0 또는 음수로 들어오는 것을 방지하세요. (상한도 필요하면 함께 설정)+import jakarta.validation.constraints.Min; @@ - public ResponseEntity<?> getSemesterOption(@PathVariable String clubId, - @RequestParam(value = "option", required = false, defaultValue = "3") int count) { + public ResponseEntity<?> getSemesterOption(@PathVariable String clubId, + @RequestParam(value = "option", required = false, defaultValue = "3") + @Min(1) int count) { return Response.ok(clubApplyService.getSemesterOption(clubId, count)); }
62-70: mode 파라미터 허용값 제한(agg|server)오입력/오타를 줄이기 위해
mode를 정규식으로 제한하세요. 클래스에@Validated가 있어야 동작합니다.+import jakarta.validation.constraints.Pattern; @@ - public ResponseEntity<?> getClubApplications(@PathVariable String clubId, - @RequestParam(defaultValue = "agg") String mode) { //agg면 aggregation사용, server면, 서비스에서 그룹 및 정렬 + public ResponseEntity<?> getClubApplications(@PathVariable String clubId, + @RequestParam(defaultValue = "agg") + @Pattern(regexp = "^(?i)(agg|server)$") String mode) { // agg 또는 server만 허용
51-70: 리소스 경로 일관성 제안:/applicationvs/apply생성/수정은
/application, 조회는/apply로 혼재되어 있습니다. REST 리소스 명을 통일하면 사용성과 문서 품질이 좋아집니다(예:/applications고정 + 액션별 HTTP 메서드로 구분).
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Jira integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (12)
backend/src/main/java/moadong/club/controller/ClubApplyController.java(3 hunks)backend/src/main/java/moadong/club/entity/ClubApplicant.java(2 hunks)backend/src/main/java/moadong/club/entity/ClubApplicationForm.java(4 hunks)backend/src/main/java/moadong/club/entity/ClubApplicationFormQuestion.java(1 hunks)backend/src/main/java/moadong/club/payload/dto/ClubApplicantsResult.java(2 hunks)backend/src/main/java/moadong/club/payload/request/ClubApplicationFormCreateRequest.java(2 hunks)backend/src/main/java/moadong/club/payload/request/ClubApplicationFormEditRequest.java(1 hunks)backend/src/main/java/moadong/club/repository/ClubApplicantsRepository.java(1 hunks)backend/src/main/java/moadong/club/repository/ClubApplicationFormsRepository.java(1 hunks)backend/src/main/java/moadong/club/service/ClubApplyService.java(13 hunks)backend/src/test/java/moadong/club/service/ClubApplyServiceTest.java(4 hunks)backend/src/test/java/moadong/fixture/ClubApplicationEditFixture.java(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (9)
- backend/src/main/java/moadong/club/entity/ClubApplicationFormQuestion.java
- backend/src/main/java/moadong/club/repository/ClubApplicationFormsRepository.java
- backend/src/main/java/moadong/club/entity/ClubApplicant.java
- backend/src/test/java/moadong/fixture/ClubApplicationEditFixture.java
- backend/src/main/java/moadong/club/payload/request/ClubApplicationFormEditRequest.java
- backend/src/test/java/moadong/club/service/ClubApplyServiceTest.java
- backend/src/main/java/moadong/club/repository/ClubApplicantsRepository.java
- backend/src/main/java/moadong/club/entity/ClubApplicationForm.java
- backend/src/main/java/moadong/club/service/ClubApplyService.java
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-05-19T05:45:52.957Z
Learnt from: lepitaaar
PR: Moadong/moadong#406
File: backend/src/main/java/moadong/club/service/ClubApplyService.java:34-38
Timestamp: 2025-05-19T05:45:52.957Z
Learning: The code duplication between createClubApplication and editClubApplication methods in ClubApplyService.java is acknowledged but will be addressed in a future refactoring, as per the developer's plan.
Applied to files:
backend/src/main/java/moadong/club/payload/dto/ClubApplicantsResult.javabackend/src/main/java/moadong/club/controller/ClubApplyController.java
📚 Learning: 2025-08-25T14:43:52.320Z
Learnt from: lepitaaar
PR: Moadong/moadong#703
File: backend/src/main/java/moadong/club/controller/ClubApplyController.java:84-84
Timestamp: 2025-08-25T14:43:52.320Z
Learning: In the Moadong codebase, questionId and clubId are equivalent identifiers that represent the same entity. The ClubApplicationRepository.findAllByIdInAndQuestionId method correctly uses clubId as the questionId parameter for filtering club applications.
Applied to files:
backend/src/main/java/moadong/club/payload/dto/ClubApplicantsResult.javabackend/src/main/java/moadong/club/controller/ClubApplyController.java
🔇 Additional comments (5)
backend/src/main/java/moadong/club/payload/dto/ClubApplicantsResult.java (3)
19-19: ApplicantStatus 타입 전환 LGTM도메인 용어 정합성 측면에서 적절합니다. 역직렬화/프론트 처리 쪽 enum 매핑만 한번 더 확인해 주세요.
15-17: 호환성 확인 필요 — Lombok @builder + record 사용Lombok은 v1.18.20부터 Java record에 대한 @builder를 지원합니다. 리포지토리 내 Lombok 버전 및 Java 타깃(JDK) 확인이 필요합니다 — 제공된 스크립트 출력으로는 확인되지 않았습니다. (확인 방법: pom.xml 또는 build.gradle에서 Lombok 버전과 sourceCompatibility/targetCompatibility/maven.compiler.release 확인) Lombok < 1.18.20이면 빌더 사용 불가하므로 정적 팩토리(new ClubApplicantsResult(...))로 전환하거나 Lombok을 업그레이드하세요.
파일: backend/src/main/java/moadong/club/payload/dto/ClubApplicantsResult.java (lines 15–17)
24-46: 복호화된 answers 노출 범위 재확인 필요 (권한/프라이버시)backend/src/main/java/moadong/club/payload/dto/ClubApplicantsResult.java — ClubApplicantsResult.of에서 answers를 복호화해 반환합니다. 이 DTO가 응답으로 노출되는 모든 엔드포인트가 운영진/관리자 권한으로만 접근 제한되는지 확인하고, 로그에 복호화된 값이 남지 않도록 조치하세요.
자동 검색이 실패해 호출 지점을 찾지 못했습니다. ClubApplicantsResult.of를 호출하는 컨트롤러·엔드포인트 경로를 알려주거나 직접 검증해 주세요.
backend/src/main/java/moadong/club/controller/ClubApplyController.java (2)
72-81: 서비스 메서드 네이밍 불일치 가능성컨트롤러는
editClubApplicationForm인데 서비스 호출은editClubApplication입니다. 리네이밍 중 혼선이 없는지 확인해주세요.
- 변경 의도대로면 서비스도
editClubApplicationForm(...)로 맞추는 것이 가독성에 좋습니다.
41-49: 문서/경로 명세 싱크 확인:/semestersvs PR 요약의/semesterPR 설명에는 단수
/semester가 언급되어 있고, 코드에는 복수/semesters입니다. 최종 경로를 확정하고 문서/프론트 호출부와 일치시키세요. 필요하면 alias 엔드포인트를 추가해도 좋습니다.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
backend/src/test/java/moadong/club/service/ClubApplyServiceTest.java (1)
41-53: 필수: setUp에서 clubApplicationFormId를 채워야 함 — 테스트가 ID null로 실패검증: backend/src/main/java/moadong/club/entity/ClubApplicationForm.java에 @builder(toBuilder = true)가 있어 ClubApplicationForm.builder()로 인스턴스 생성 가능; static testOf() 팩토리는 없음.
조치: backend/src/test/java/moadong/club/service/ClubApplyServiceTest.java의 setUp()에서 아래 중 하나 적용하세요.
- ClubApplicationForm.builder()로 테스트 폼 생성 → clubApplicationFormsRepository.save(created) → this.clubApplicationFormId = saved.getId()
- 또는 clubApplicationFormsRepository에서 해당 clubId의 최신 폼을 조회해 this.clubApplicationFormId에 할당
♻️ Duplicate comments (1)
backend/src/main/java/moadong/club/controller/ClubApplyController.java (1)
91-98: 지원 제출 API에 인증이 없습니다(보안 취약점) — 이전 코멘트와 동일 이슈익명 제출이 의도된 것이 아니라면 인증/보안 스키마 추가 및 사용자 컨텍스트 전달이 필요합니다.
@PostMapping("/apply/{applicationFormId}") @Operation(summary = "클럽 지원", description = "클럽에 지원합니다") -public ResponseEntity<?> applyToClub(@PathVariable String clubId, - @PathVariable String applicationFormId, - @RequestBody @Validated ClubApplyRequest request) { - clubApplyService.applyToClub(clubId, applicationFormId, request); +@PreAuthorize("isAuthenticated()") +@SecurityRequirement(name = "BearerAuth") +public ResponseEntity<?> applyToClub(@PathVariable String clubId, + @PathVariable String applicationFormId, + @CurrentUser CustomUserDetails user, + @RequestBody @Valid ClubApplyRequest request) { + clubApplyService.applyToClub(clubId, applicationFormId, user, request); return Response.ok("success apply"); }다음 스크립트로 OpenAPI 보안 스키마 이름이
"BearerAuth"(대소문자 포함)로 정의되어 있는지 확인하세요.#!/bin/bash set -euo pipefail echo "SecurityScheme 정의 위치:" rg -nP '@SecurityScheme\s*\(' -C2 echo -e "\nSecurityRequirement(name=...) 사용 목록:" rg -nP 'SecurityRequirement\(name\s*=\s*"([^"]+)"' -n -C0 echo -e "\n정의된 스키마명 후보 추출:" rg -nP '@SecurityScheme\s*\(' -n -C2 | rg -nP 'name\s*=\s*"([^"]+)"' -o | sed -n 's/.*name\s*=\s*"\([^"]\+\)".*/\1/p' | sort -u
🧹 Nitpick comments (14)
backend/src/main/java/moadong/global/exception/ErrorCode.java (1)
40-40: 상수명 구체화 제안 — APPLICATION_FORM_NOT_FOUND 추가, 기존 APPLICATION_NOT_FOUND @deprecated 처리 (점진 교체)메시지('지원서 양식')와 상수명이 불일치하므로 새 상수 APPLICATION_FORM_NOT_FOUND를 추가하고 기존 APPLICATION_NOT_FOUND는 @deprecated로 표기한 뒤 참조를 단계적으로 교체하세요.
확인된 참조: backend/src/main/java/moadong/club/service/ClubApplyService.java (lines 123, 131, 141, 219, 250)
backend/src/main/java/moadong/club/repository/ClubApplicantsRepository.java (4)
9-10: 파라미터/도메인 용어 불일치: questionId → formId로 정정 필요메서드명과 @query는 formId를 가리키는데, 파라미터명이 questionId로 남아 있어 혼란을 유발합니다. 시그니처와 호출부 전반을 formId로 정리해 주세요.
- List<ClubApplicant> findAllByFormId(String questionId); + List<ClubApplicant> findAllByFormId(String formId);
9-10: status 필터 조건 강화 필요(null/빈값 통과 가능성)
$exists: true+$ne: 'DRAFT'는 status가null이거나 빈 문자열인 문서도 통과시킬 수 있습니다. 명시적으로 허용 상태를 지정하거나$nin으로 차단하세요. 또는 @query 제거 후 파생쿼리로 상태를 받는 메서드를 추가하는 것을 권장합니다.- @Query("{ 'formId': ?0, 'status': { $exists: true, $ne: 'DRAFT' } }") - List<ClubApplicant> findAllByFormId(String formId); + // 권장 1) 파생 쿼리 + // List<ClubApplicant> findAllByFormIdAndStatusIn(String formId, Collection<ApplicantStatus> statuses); + + // 권장 2) 명시적 @Query 사용 시 + @Query("{ 'formId': ?0, 'status': { $nin: ['DRAFT', null, ''] } }") + List<ClubApplicant> findAllByFormId(String formId);
8-13: 조회 성능 대비 인덱스 제안(formId, status)해당 조회 패턴이 잦다면 (formId, status) 복합 인덱스를 추천합니다. 엔티티에 인덱스를 추가해 주세요.
// ClubApplicant.java (예시) @CompoundIndex(name = "formId_status_idx", def = "{'formId': 1, 'status': 1}") public class ClubApplicant { ... }
12-12: 입력 중복/순서 요구사항 점검(Set 권장)
findAllByIdInAndFormId(List<String> ids, ...)에서 중복 ID가 들어올 수 있습니다. 중복 허용이 불필요하면Set<String>으로 바꾸는 것을 고려해 주세요.- List<ClubApplicant> findAllByIdInAndFormId(List<String> ids, String formId); + List<ClubApplicant> findAllByIdInAndFormId(Set<String> ids, String formId);backend/src/test/java/moadong/club/service/ClubApplyServiceTest.java (3)
118-126: 테스트에서 스택트레이스 출력 대신 단정 사용 권장
e.printStackTrace()는 노이즈입니다. 실패 의도면 명시적으로 실패 처리하세요.- } catch (InterruptedException | BrokenBarrierException e) { - Thread.currentThread().interrupt(); - e.printStackTrace(); + } catch (InterruptedException | BrokenBarrierException e) { + Thread.currentThread().interrupt(); + throw new AssertionError("동기화 장애 발생", e); } catch (Exception e) { - e.printStackTrace(); + throw new AssertionError("예상치 못한 예외", e); } finally {
131-137: 스레드 종료 보장 및 추가 검증
latch.await()로 대부분 충분하지만, 종료 보장을 위해awaitTermination을 추가하는 것을 권장합니다. 또한 실패 시 디버깅을 돕도록 성공/충돌 카운트를 메시지로 출력하세요.@@ executorService.shutdown(); + // 5초 내 종료 보장 + executorService.awaitTermination(5, java.util.concurrent.TimeUnit.SECONDS); @@ - assertEquals(1, successCount.get(), "성공한 요청은 1개여야 합니다."); - assertEquals(numberOfThreads - 1, conflictCount.get(), "실패(충돌)한 요청은 " + (numberOfThreads - 1) + "개여야 합니다."); + assertEquals(1, successCount.get(), "성공한 요청은 1개여야 합니다. (success=" + successCount.get() + ", conflict=" + conflictCount.get() + ")"); + assertEquals(numberOfThreads - 1, conflictCount.get(), "실패(충돌)한 요청은 " + (numberOfThreads - 1) + "개여야 합니다. (success=" + successCount.get() + ", conflict=" + conflictCount.get() + ")");
56-85: 주석 처리된 학기 옵션 테스트는 별도 단위 테스트로 복구 권장옵션 생성 로직은 순서·경계값 버그가 생기기 쉬운 영역입니다. 파라미터라이즈드 테스트로 계절 학기 전환 경계를 검증해 주세요.
원하시면 JUnit5 ParameterizedTest 템플릿을 드립니다.
backend/src/main/java/moadong/club/controller/ClubApplyController.java (6)
30-31: 요청 파라미터 유효성 검증 대비 클래스 레벨 @validated 추가 권장RequestParam 활용이 늘어 유효성 애노테이션(@Min/@Max/@pattern) 적용을 위해 컨트롤러에 @validated를 추가하세요.
적용 예:
@RestController @RequestMapping("/api/club/{clubId}") @AllArgsConstructor +@Validated @Tag(name = "Club_Apply", description = "클럽 지원서 API") public class ClubApplyController {
52-59: 생성 API 전반은 적절함; @Valid 사용으로 일관성 개선 제안본문 DTO 검증은
@Valid로 통일하는 편이 보편적입니다.- @RequestBody @Validated ClubApplicationFormCreateRequest request) { + @RequestBody @Valid ClubApplicationFormCreateRequest request) {
62-70: 목록 API: mode 허용값 검증 추가
mode가 임의 문자열이면 예측불가 동작이 됩니다. 허용값을 검증하세요(agg|server).+import jakarta.validation.constraints.Pattern; - @RequestParam(defaultValue = "agg") String mode) { //agg면 aggregation사용, server면, 서비스에서 그룹 및 정렬 + @RequestParam(defaultValue = "agg") @Pattern(regexp = "(?i)agg|server") String mode) { // agg|server만 허용추가로, 잘못된 값은 400을 반환하도록 글로벌 예외 처리(ConstraintViolationException) 매핑이 있는지 확인 바랍니다.
72-82: 수정 API: 서비스 메서드 네이밍 불일치컨트롤러는
editClubApplicationForm인데 서비스는editClubApplication입니다. 추후 검색성/일관성을 위해 통일하세요.
84-89: 리소스 경로 혼동: GET /apply/{id}는 "양식"을 반환합니다
/apply는 일반적으로 “지원 행위”로 인식됩니다. 양식 단건 조회는/application/{id}로 일관화하는 것이 REST/가독성 측면에서 낫습니다(POST/PUT가 이미/application을 사용).- @GetMapping("/apply/{applicationFormId}") + @GetMapping("/application/{applicationFormId}")이 변경이 가능하면 클라이언트/문서 동시 반영을 권장합니다.
110-123: 일괄 수정 API: 요청 크기 제한 및 부분 실패 정책 명시
- 대량 요청 방지를 위해 리스트 크기 제한(@SiZe(max=100 등))을 권장합니다.
- 부분 실패 시 동작(모두 성공/부분 성공/트랜잭션 롤백) 정책을 명확히 하세요.
+import jakarta.validation.constraints.Size; - @RequestBody @Valid @NotEmpty List<ClubApplicantEditRequest> request, + @RequestBody @Valid @NotEmpty @Size(max = 100) List<ClubApplicantEditRequest> request,원하시면 실패 항목을 수집해 함께 반환하는 응답 스키마 초안 드리겠습니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Jira integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (7)
backend/src/main/java/moadong/club/controller/ClubApplyController.java(3 hunks)backend/src/main/java/moadong/club/repository/ClubApplicantsRepository.java(1 hunks)backend/src/main/java/moadong/club/repository/ClubApplicationFormsRepository.java(1 hunks)backend/src/main/java/moadong/club/service/ClubApplyService.java(11 hunks)backend/src/main/java/moadong/global/exception/ErrorCode.java(1 hunks)backend/src/main/java/moadong/global/exception/GlobalExceptionHandler.java(2 hunks)backend/src/test/java/moadong/club/service/ClubApplyServiceTest.java(4 hunks)
✅ Files skipped from review due to trivial changes (1)
- backend/src/main/java/moadong/global/exception/GlobalExceptionHandler.java
🚧 Files skipped from review as they are similar to previous changes (2)
- backend/src/main/java/moadong/club/repository/ClubApplicationFormsRepository.java
- backend/src/main/java/moadong/club/service/ClubApplyService.java
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-08-25T14:43:52.320Z
Learnt from: lepitaaar
PR: Moadong/moadong#703
File: backend/src/main/java/moadong/club/controller/ClubApplyController.java:84-84
Timestamp: 2025-08-25T14:43:52.320Z
Learning: In the Moadong codebase, questionId and clubId are equivalent identifiers that represent the same entity. The ClubApplicationRepository.findAllByIdInAndQuestionId method correctly uses clubId as the questionId parameter for filtering club applications.
Applied to files:
backend/src/main/java/moadong/club/repository/ClubApplicantsRepository.javabackend/src/test/java/moadong/club/service/ClubApplyServiceTest.javabackend/src/main/java/moadong/club/controller/ClubApplyController.java
📚 Learning: 2025-05-19T05:45:52.957Z
Learnt from: lepitaaar
PR: Moadong/moadong#406
File: backend/src/main/java/moadong/club/service/ClubApplyService.java:34-38
Timestamp: 2025-05-19T05:45:52.957Z
Learning: The code duplication between createClubApplication and editClubApplication methods in ClubApplyService.java is acknowledged but will be addressed in a future refactoring, as per the developer's plan.
Applied to files:
backend/src/test/java/moadong/club/service/ClubApplyServiceTest.javabackend/src/main/java/moadong/club/controller/ClubApplyController.java
🧬 Code graph analysis (1)
backend/src/test/java/moadong/club/service/ClubApplyServiceTest.java (1)
backend/src/test/java/moadong/fixture/ClubApplicationEditFixture.java (1)
ClubApplicationEditFixture(6-23)
🪛 GitHub Actions: PR Test
backend/src/test/java/moadong/club/service/ClubApplyServiceTest.java
[error] 105-105: IllegalArgumentException: The given id must not be null (SimpleMongoRepository.findById) during ClubApplyServiceTest.concurrentUpdateOnExistingDocumentTest; repository.findById was invoked with a null id.
[error] 135-135: Assertion failed: expected 1 successful request, but 0 in ClubApplyServiceTest.concurrentUpdateOnExistingDocumentTest.
🔇 Additional comments (5)
backend/src/main/java/moadong/global/exception/ErrorCode.java (1)
40-40: 용어 수정 적절 — '지원서 양식'으로 명확화도메인 전환(Question → ApplicationForm)에 맞춰 사용자 메시지를 구체화한 점 좋습니다. APPLICANT_NOT_FOUND(지원서 제출물)와의 구분도 더 명확해졌습니다.
backend/src/test/java/moadong/club/service/ClubApplyServiceTest.java (1)
99-115: 테스트: 낙관적 락 예외(OptimisticLockingFailureException)를 기대하도록 수정backend/src/main/java/moadong/club/entity/ClubApplicationForm.java:55에 이미 @Version이 존재합니다 — 엔티티 추가 불필요. 대신 테스트에서 낙관적 락 충돌을 명시적으로 검사하도록 변경하세요 (backend/src/test/java/moadong/club/service/ClubApplyServiceTest.java, 약 99–115행).
@@ - } catch (DataAccessException e) { - // DataAccessException은 WriteConflict 등을 포함 + } catch (org.springframework.dao.OptimisticLockingFailureException e) { + // 버전 충돌 conflictCount.incrementAndGet();Likely an incorrect or invalid review comment.
backend/src/main/java/moadong/club/controller/ClubApplyController.java (3)
12-13: 새 DTO 임포트 적절폼 기반 전환에 맞는 DTO 임포트 추가, 이상 없습니다.
100-108: 지원자 현황 API: 인증 처리 적절컨트롤러 레벨 인증은 적절합니다. 서비스에서 클럽 관리자/운영진 권한 체크가 수행되는지 확인 바랍니다.
125-137: 지원자 삭제 API: 처리 적절인증/권한이 적용되어 있고 서비스 위임도 명확합니다. 서비스에서 소유권/권한 검증이 강제되는지 최종 확인만 부탁드립니다.
| @GetMapping("/semesters") | ||
| @Operation(summary = "클럽 지원서 생성 가능 학기 불러오기", description = "생성 가능한 학기를 불러옵니다<br>" | ||
| + "<br>" | ||
| + "기준일로부터 이번 학기, 다음 학기, 다다음 학기를 불러옴<br>" | ||
| + "ex) 2025/09/01 -> 2025-2학기, 2025-겨울학기, 2026-1학기") | ||
| public ResponseEntity<?> getSemesterOption(@PathVariable String clubId, | ||
| @RequestParam(value = "option", required = false, defaultValue = "3") int count) { | ||
| return Response.ok(clubApplyService.getSemesterOption(clubId, count)); | ||
| } |
There was a problem hiding this comment.
학기 옵션 API: 인증 부재 및 파라미터 네이밍/검증 개선 필요
- 인증 없이 생성용 옵션을 노출하면 정보 유출/오남용 소지가 있습니다. 최소 인증 요구(+ 필요 시 권한 체크)하세요.
- 쿼리 파라미터명
option은 의미가 불명확합니다.count로 명확화하고 범위를 제한하세요(예: 1..6).
적용 예:
import moadong.user.payload.CustomUserDetails;
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.Max;
@GetMapping("/semesters")
@Operation(summary = "클럽 지원서 생성 가능 학기 불러오기", description = "생성 가능한 학기를 불러옵니다<br>"
...
- public ResponseEntity<?> getSemesterOption(@PathVariable String clubId,
- @RequestParam(value = "option", required = false, defaultValue = "3") int count) {
- return Response.ok(clubApplyService.getSemesterOption(clubId, count));
+@PreAuthorize("isAuthenticated()")
+@SecurityRequirement(name = "BearerAuth")
+public ResponseEntity<?> getSemesterOption(@PathVariable String clubId,
+ @RequestParam(name = "count", defaultValue = "3") @Min(1) @Max(6) int count) {
+ return Response.ok(clubApplyService.getSemesterOption(clubId, count));
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| @GetMapping("/semesters") | |
| @Operation(summary = "클럽 지원서 생성 가능 학기 불러오기", description = "생성 가능한 학기를 불러옵니다<br>" | |
| + "<br>" | |
| + "기준일로부터 이번 학기, 다음 학기, 다다음 학기를 불러옴<br>" | |
| + "ex) 2025/09/01 -> 2025-2학기, 2025-겨울학기, 2026-1학기") | |
| public ResponseEntity<?> getSemesterOption(@PathVariable String clubId, | |
| @RequestParam(value = "option", required = false, defaultValue = "3") int count) { | |
| return Response.ok(clubApplyService.getSemesterOption(clubId, count)); | |
| } | |
| @GetMapping("/semesters") | |
| @Operation(summary = "클럽 지원서 생성 가능 학기 불러오기", description = "생성 가능한 학기를 불러옵니다<br>" | |
| "<br>" | |
| "기준일로부터 이번 학기, 다음 학기, 다다음 학기를 불러옴<br>" | |
| "ex) 2025/09/01 -> 2025-2학기, 2025-겨울학기, 2026-1학기") | |
| @PreAuthorize("isAuthenticated()") | |
| @SecurityRequirement(name = "BearerAuth") | |
| public ResponseEntity<?> getSemesterOption(@PathVariable String clubId, | |
| @RequestParam(name = "count", defaultValue = "3") @Min(1) @Max(6) int count) { | |
| return Response.ok(clubApplyService.getSemesterOption(clubId, count)); | |
| } |
🤖 Prompt for AI Agents
In backend/src/main/java/moadong/club/controller/ClubApplyController.java around
lines 41-49, the getSemesterOption endpoint currently allows unauthenticated
access and uses an ambiguous query parameter name; require authentication (and
apply role/ownership checks if needed) by adding the appropriate security
annotation (e.g., @PreAuthorize or @Authenticated/@RolesAllowed) or verifying
the authenticated principal inside the method, rename the query parameter from
option to count in the method signature and API docs, and enforce/validate its
range (e.g., 1..6) using bean validation (@RequestParam int count with @Min(1)
@Max(6) or explicit range check returning 400 on invalid values) before calling
clubApplyService.getSemesterOption.
There was a problem hiding this comment.
@Zepelown 동의해주셔서 감사합니다! 맞습니다. 학기 옵션 API에 인증이 필요합니다.
인증 없이 노출되면 클럽의 지원서 생성 계획이나 학기 정보가 의도치 않게 공개될 수 있어서, @PreAuthorize("isAuthenticated()") 추가가 보안상 중요합니다.
🧠 Learnings used
Learnt from: lepitaaar
PR: Moadong/moadong#406
File: backend/src/main/java/moadong/club/service/ClubApplyService.java:34-38
Timestamp: 2025-05-19T05:45:52.957Z
Learning: The code duplication between createClubApplication and editClubApplication methods in ClubApplyService.java is acknowledged but will be addressed in a future refactoring, as per the developer's plan.
There was a problem hiding this comment.
이 api 사용여부를 확인하는 대로 수정하겠습니다
There was a problem hiding this comment.
Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!
| @GetMapping("/semesters") | ||
| @Operation(summary = "클럽 지원서 생성 가능 학기 불러오기", description = "생성 가능한 학기를 불러옵니다<br>" | ||
| + "<br>" | ||
| + "기준일로부터 이번 학기, 다음 학기, 다다음 학기를 불러옴<br>" | ||
| + "ex) 2025/09/01 -> 2025-2학기, 2025-겨울학기, 2026-1학기") | ||
| public ResponseEntity<?> getSemesterOption(@PathVariable String clubId, | ||
| @RequestParam(value = "option", required = false, defaultValue = "3") int count) { | ||
| return Response.ok(clubApplyService.getSemesterOption(clubId, count)); | ||
| } |
backend/src/main/java/moadong/club/repository/ClubApplicationFormSlim.java
Show resolved
Hide resolved
프론트한테 이야기하면 될 것 같습니다. |
lepitaaar
left a comment
There was a problem hiding this comment.
전체적인 리팩토링과 학기선택 기능구현 수고하셨습니다~
| @GetMapping("/apply") | ||
| @Operation(summary = "클럽 지원서 양식들 불러오기", description = "클럽 지원서 양식들을 학기별로 분류하여 불러옵니다") | ||
| public ResponseEntity<?> getClubApplications(@PathVariable String clubId, | ||
| @RequestParam(defaultValue = "agg") String mode) { //agg면 aggregation사용, server면, 서비스에서 그룹 및 정렬 |
There was a problem hiding this comment.
String mode 를 Enum으로 분리하면 좋을꺼같아요
| if("server".equalsIgnoreCase(mode)) { | ||
| return Response.ok(clubApplyService.getGroupedClubApplicationForms(clubId)); | ||
| } | ||
| return Response.ok(clubApplyService.getClubApplicationForms(clubId)); |
| private SemesterTerm semesterTerm = (ZonedDateTime.now(ZoneId.of("Asia/Seoul")).toLocalDate().getMonthValue() < 7) | ||
| ? SemesterTerm.FIRST : SemesterTerm.SECOND; //1학기, 2학기 |
There was a problem hiding this comment.
SemesterTerm startTerm = (month >= 1 && month <=6) ? SemesterTerm.FIRST : SemesterTerm.SECOND;아래 사용하신 코드와 기능은 같은거같습니다 통일성있게 바꾸면 좋을꺼같습니다~
backend/src/main/java/moadong/club/repository/ClubApplicationFormSlim.java
Show resolved
Hide resolved
| PASSWORD_SAME_AS_OLD(HttpStatus.BAD_REQUEST,"702-2","이전 비밀번호와 동일한 비밀번호는 설정할 수 없습니다."), | ||
|
|
||
| APPLICATION_NOT_FOUND(HttpStatus.NOT_FOUND, "800-1", "지원서가 존재하지 않습니다."), | ||
| APPLICATION_NOT_FOUND(HttpStatus.NOT_FOUND, "800-1", "지원서 양식이 존재하지 않습니다."), |
네네 일단 프론트 선생님들 리뷰 요청 드려놨고, 따로 말씀드리겠습니다. |
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (2)
backend/src/main/java/moadong/club/payload/dto/ClubApplicationFormSlim.java (2)
10-10: LocalDateTime의 TZ 부재로 직렬화/클라이언트 표시 혼선 가능API 응답에서 편집 시각의 시간대가 불분명합니다. 전역 UTC 고정 직렬화를 사용하지 않는다면 OffsetDateTime/Instant 사용을 고려하세요.
가능한 변경 예시:
-import java.time.LocalDateTime; +import java.time.OffsetDateTime; public interface ClubApplicationFormSlim { String getId(); String getTitle(); - LocalDateTime getEditedAt(); + OffsetDateTime getEditedAt(); Integer getSemesterYear(); SemesterTerm getSemesterTerm(); }또는 현재 LocalDateTime 유지 시, 서버의 Jackson 시간대 설정(예: UTC 고정)과 API 스펙에 명시가 필요한지 확인 부탁드립니다.
11-11: semesterYear가 null 불가라면 원시형(int) 권장DB 제약이 NOT NULL이면 Integer 대신 int가 명확합니다. 반대로 null 허용이라면 현재 타입 유지가 맞습니다.
가능한 변경 예시:
- Integer getSemesterYear(); + int getSemesterYear();
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Jira integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (3)
backend/src/main/java/moadong/club/payload/dto/ClubApplicationFormSlim.java(1 hunks)backend/src/main/java/moadong/club/repository/ClubApplicationFormsRepository.java(1 hunks)backend/src/main/java/moadong/club/service/ClubApplyService.java(11 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- backend/src/main/java/moadong/club/repository/ClubApplicationFormsRepository.java
- backend/src/main/java/moadong/club/service/ClubApplyService.java
🔇 Additional comments (3)
backend/src/main/java/moadong/club/payload/dto/ClubApplicationFormSlim.java (3)
7-13: 슬림 프로젝션 설계는 적절합니다.목록 조회용 최소 필드만 노출하는 인터페이스 기반 프로젝션으로 목적에 부합합니다.
7-13: 프로젝션 필드(alias)와 인터페이스 매핑 일치 — 확인됨Repository의 @query fields = "{'_id': 1, 'title': 1, 'editedAt': 1, 'semesterYear': 1, 'semesterTerm': 1}"가 ClubApplicationFormSlim의 getId(), getTitle(), getEditedAt(), getSemesterYear(), getSemesterTerm()와 일치합니다 (위치: backend/src/main/java/moadong/club/repository/ClubApplicationFormsRepository.java, lines 18–21).
8-8: 확인: getId()가 String으로 일치 — 문제 없음ClubApplicationForm의 @id가 String이며 ClubApplicationFormSlim.getId()와 컨트롤러의 PathVariable(applicationFormId, clubId)도 모두 String으로 일치합니다. 수정을 요구하지 않습니다.
seongwon030
left a comment
There was a problem hiding this comment.
요거 서버에서 가능한 학기를 미리 정해서 보여주는거죠? 좋은 것 같습니다~
클라이언트에서는 get만 하면 되는 구조라 복잡하지 않을 것 같아요
GET /semesters 로그인 인증 구현 및 isExist 활성화
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (4)
backend/src/main/java/moadong/club/service/ClubApplyService.java (3)
98-100: 예외 타입 일관성: IllegalArgumentException 대신 RestApiException 사용 권장서비스 계층 전반에서
RestApiException을 사용 중입니다. 학기 검증 실패도 동일한 예외 체계를 따르세요.
128-137: 소유자 검증 누락으로 수정 API 무단 접근 가능
editClubApplicationQuestion(...)는 양식 식별자만으로 수정 가능하며, 클럽 소유자 검증이 없습니다.
455-455: 여분의 닫는 중괄호로 인한 컴파일 에러파일 말미의
}}는 하나가 불필요합니다.backend/src/main/java/moadong/club/controller/ClubApplyController.java (1)
67-67: String mode를 Enum으로 분리하면 좋을 것 같아요
🧹 Nitpick comments (1)
backend/src/main/java/moadong/club/service/ClubApplyService.java (1)
202-209: Record 접근자 메서드 활용으로 Enum 내부로 이동 가능
termRank와termName메서드를SemesterTermEnum 내부로 이동하면 응집도가 향상됩니다.SemesterTerm Enum에 다음을 추가하세요:
public enum SemesterTerm { FIRST(1), SECOND(2); private final int rank; SemesterTerm(int rank) { this.rank = rank; } public int getRank() { return rank; } public static int termRank(SemesterTerm term) { return term == null ? -1 : term.rank; } public static String termName(SemesterTerm term) { return term == null ? "" : term.name(); } }그리고 서비스 코드를 다음과 같이 수정하세요:
- e -> termRank(e.getKey().term()), + e -> SemesterTerm.termRank(e.getKey().term()), Comparator.nullsLast(Comparator.reverseOrder())) .thenComparing( - e -> termName(e.getKey().term()), + e -> SemesterTerm.termName(e.getKey().term()),
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Jira integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (4)
backend/src/main/java/moadong/club/controller/ClubApplyController.java(3 hunks)backend/src/main/java/moadong/club/payload/response/SemesterOptionResponse.java(1 hunks)backend/src/main/java/moadong/club/repository/ClubApplicationFormsRepository.java(1 hunks)backend/src/main/java/moadong/club/service/ClubApplyService.java(11 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- backend/src/main/java/moadong/club/repository/ClubApplicationFormsRepository.java
- backend/src/main/java/moadong/club/payload/response/SemesterOptionResponse.java
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-05-19T05:45:52.957Z
Learnt from: lepitaaar
PR: Moadong/moadong#406
File: backend/src/main/java/moadong/club/service/ClubApplyService.java:34-38
Timestamp: 2025-05-19T05:45:52.957Z
Learning: The code duplication between createClubApplication and editClubApplication methods in ClubApplyService.java is acknowledged but will be addressed in a future refactoring, as per the developer's plan.
Applied to files:
backend/src/main/java/moadong/club/service/ClubApplyService.javabackend/src/main/java/moadong/club/controller/ClubApplyController.java
📚 Learning: 2025-08-25T14:43:52.320Z
Learnt from: lepitaaar
PR: Moadong/moadong#703
File: backend/src/main/java/moadong/club/controller/ClubApplyController.java:84-84
Timestamp: 2025-08-25T14:43:52.320Z
Learning: In the Moadong codebase, questionId and clubId are equivalent identifiers that represent the same entity. The ClubApplicationRepository.findAllByIdInAndQuestionId method correctly uses clubId as the questionId parameter for filtering club applications.
Applied to files:
backend/src/main/java/moadong/club/service/ClubApplyService.javabackend/src/main/java/moadong/club/controller/ClubApplyController.java
🧬 Code graph analysis (1)
backend/src/main/java/moadong/club/service/ClubApplyService.java (2)
backend/src/main/java/moadong/club/entity/ClubApplicationFormQuestion.java (1)
AllArgsConstructor(12-36)backend/src/main/java/moadong/club/entity/ClubQuestionAnswer.java (1)
AllArgsConstructor(7-16)
🔇 Additional comments (1)
backend/src/main/java/moadong/club/controller/ClubApplyController.java (1)
93-100: 지원 제출 API에 인증이 없습니다누구나 지원서를 제출할 수 있어야 하는 의도적 설계가 맞는지 확인이 필요합니다.
만약 인증이 필요하다면 다음과 같이 수정하세요:
@PostMapping("/apply/{applicationFormId}") @Operation(summary = "클럽 지원", description = "클럽에 지원합니다") +@PreAuthorize("isAuthenticated()") +@SecurityRequirement(name = "BearerAuth") public ResponseEntity<?> applyToClub(@PathVariable String clubId, @PathVariable String applicationFormId, + @CurrentUser CustomUserDetails user, @RequestBody @Validated ClubApplyRequest request) { - clubApplyService.applyToClub(clubId, applicationFormId, request); + clubApplyService.applyToClub(clubId, applicationFormId, user, request); return Response.ok("success apply"); }
Zepelown
left a comment
There was a problem hiding this comment.
고생하셨습니다.
그 뭐냐 머지하기 전에
db 셋팅하고 테스트 통과되는지 확인 부탁드릴게요
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (5)
backend/src/test/java/moadong/club/service/ClubApplyServiceTest.java (5)
56-85: 주석 처리된 테스트는 제거하거나 @disabled로 명시적 비활성화 권장주석 상태로 두면 장기적으로 부채가 됩니다. 필요 시 @disabled(사유)로 남기거나 별도 이슈로 분리해 주세요.
예:
@Disabled("OptionItem 가시성 변경 대기중(MOA-231)") @Test void 현재학기_SECOND이면_내년_FIRST까지() { ... }
105-105:Optional.get()직접 호출 지양 → 친절한 실패 메시지로 교체데이터 미존재 시 테스트가 불친절하게 실패합니다.
다음으로 교체:
- ClubApplicationForm formToUpdate = clubApplicationFormsRepository.findById(this.clubApplicationFormId).get(); + ClubApplicationForm formToUpdate = clubApplicationFormsRepository.findById(this.clubApplicationFormId) + .orElseThrow(() -> new NoSuchElementException("테스트 대상 폼이 존재하지 않습니다: id=" + this.clubApplicationFormId));
96-98: 스레드 내 예외 수집용 컨테이너 추가로 테스트 신뢰도 개선
printStackTrace()는 테스트를 통과시킬 수 있습니다. 예외를 수집해 최종 단정으로 확인하세요.다음 추가:
AtomicInteger successCount = new AtomicInteger(0); AtomicInteger conflictCount = new AtomicInteger(0); + java.util.concurrent.ConcurrentLinkedQueue<Throwable> errors = new java.util.concurrent.ConcurrentLinkedQueue<>();
118-126: 스레드 예외는 기록 후 테스트를 실패시켜야 함현재는 스택트레이스만 출력합니다. 수집 후 최종 assert로 실패시켜 주세요.
적용:
- } catch (InterruptedException | BrokenBarrierException e) { - Thread.currentThread().interrupt(); - e.printStackTrace(); - } catch (Exception e) { - e.printStackTrace(); + } catch (InterruptedException | BrokenBarrierException e) { + Thread.currentThread().interrupt(); + errors.add(e); + } catch (Exception e) { + errors.add(e);그리고 최종 단정 추가(아래 코멘트 참고).
134-137: 예기치 않은 예외가 없었는지도 함께 단정성공/충돌 수만 검사하면 숨겨진 실패를 놓칠 수 있습니다.
다음 한 줄을 맨 위나 마지막에 추가:
assertEquals(1, successCount.get(), "성공한 요청은 1개여야 합니다."); assertEquals(numberOfThreads - 1, conflictCount.get(), "실패(충돌)한 요청은 " + (numberOfThreads - 1) + "개여야 합니다."); + org.junit.jupiter.api.Assertions.assertTrue(errors.isEmpty(), "스레드 실행 중 예기치 않은 예외: " + errors);
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Jira integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
backend/src/test/java/moadong/club/service/ClubApplyServiceTest.java(4 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-08-25T14:43:52.320Z
Learnt from: lepitaaar
PR: Moadong/moadong#703
File: backend/src/main/java/moadong/club/controller/ClubApplyController.java:84-84
Timestamp: 2025-08-25T14:43:52.320Z
Learning: In the Moadong codebase, questionId and clubId are equivalent identifiers that represent the same entity. The ClubApplicationRepository.findAllByIdInAndQuestionId method correctly uses clubId as the questionId parameter for filtering club applications.
Applied to files:
backend/src/test/java/moadong/club/service/ClubApplyServiceTest.java
📚 Learning: 2025-05-19T05:45:52.957Z
Learnt from: lepitaaar
PR: Moadong/moadong#406
File: backend/src/main/java/moadong/club/service/ClubApplyService.java:34-38
Timestamp: 2025-05-19T05:45:52.957Z
Learning: The code duplication between createClubApplication and editClubApplication methods in ClubApplyService.java is acknowledged but will be addressed in a future refactoring, as per the developer's plan.
Applied to files:
backend/src/test/java/moadong/club/service/ClubApplyServiceTest.java
🧬 Code graph analysis (1)
backend/src/test/java/moadong/club/service/ClubApplyServiceTest.java (1)
backend/src/test/java/moadong/fixture/ClubApplicationEditFixture.java (1)
ClubApplicationEditFixture(6-23)
🪛 GitHub Actions: PR Test
backend/src/test/java/moadong/club/service/ClubApplyServiceTest.java
[error] 51-51: NoSuchElementException: 테스트를 위한 ClubApplicationForm 문서가 DB에 존재하지 않습니다. 먼저 문서를 생성해주세요.
[error] 1-1: Gradle test task failed. There were failing tests (1 failed, 3 skipped). See report: backend/build/reports/tests/test/index.html
🔇 Additional comments (4)
backend/src/test/java/moadong/club/service/ClubApplyServiceTest.java (4)
13-16: ClubApplicationForm 전환에 따른 import 정리 OK엔티티/요청/레포지토리 import 치환이 일관됩니다.
38-38: 레포지토리 주입 변경 LGTM타입 일치하며 명명(ClubApplicationFormsRepository)도 도메인 의도에 부합합니다.
41-41: 확인: 엔티티 ID 타입(String) 일치ClubApplicationForm이 Persistable을 구현하고 @id가 private String id이므로 테스트의 private String clubApplicationFormId와 타입이 일치합니다.
112-115: 낙관적 락 검증 — MongoDB(@Version) 사용으로 save()만으로 충돌 감지됨; JPA(saveAndFlush) 제안 불필요ClubApplicationFormsRepository가 MongoRepository를 확장하고 ClubApplicationForm에 @Version이 있으므로 clubApplicationFormsRepository.save(formToUpdate)는 버전 불일치 시 OptimisticLockingFailureException을 발생시킵니다. JPA 전용인 saveAndFlush 제안은 이 코드베이스에 적용되지 않습니다.
참고: backend/src/main/java/moadong/club/repository/ClubApplicationFormsRepository.java, backend/src/main/java/moadong/club/entity/ClubApplicationForm.java
Likely an incorrect or invalid review comment.
| ClubApplicationForm clubApplicationForm = clubApplicationFormsRepository.findById(club.getId()) //clubid 대신 formId 넣어야 함 | ||
| .orElseThrow(() -> new NoSuchElementException("테스트를 위한 ClubApplicationForm 문서가 DB에 존재하지 않습니다. 먼저 문서를 생성해주세요.")); | ||
| this.clubApplicationFormId = clubApplicationForm.getId(); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
파이프라인 실패 원인: clubId를 findById에 넘기는 조회 버그 + 테스트 데이터 의존성 제거 필요
현재 clubId로 findById를 호출하고 있어 NoSuchElementException이 재현됩니다(파이프라인 로그와 일치). 테스트는 외부 데이터에 의존하지 말고, “클럽 기준 최신 폼 조회 → 없으면 생성”으로 안정화하세요.
적용 제안(레포지토리에 클럽 기준 조회 메서드가 존재한다고 가정):
- ClubApplicationForm clubApplicationForm = clubApplicationFormsRepository.findById(club.getId()) //clubid 대신 formId 넣어야 함
- .orElseThrow(() -> new NoSuchElementException("테스트를 위한 ClubApplicationForm 문서가 DB에 존재하지 않습니다. 먼저 문서를 생성해주세요."));
- this.clubApplicationFormId = clubApplicationForm.getId();
+ // clubId로 findById 호출 → 버그. (formId 필요)
+ // 1) 클럽 기준 최신 폼 조회, 2) 없으면 테스트용 폼 생성
+ ClubApplicationForm clubApplicationForm = clubApplicationFormsRepository
+ .findFirstByClubIdOrderByCreatedAtDesc(club.getId())
+ .orElseGet(() -> {
+ // TODO: 실제 생성 API/팩토리로 치환 (예: clubApplyService.createForm(...))
+ return clubApplicationFormsRepository.save(
+ ClubApplicationForm.builder()
+ .clubId(club.getId())
+ .title("concurrency-test")
+ .description("generated by ClubApplyServiceTest")
+ // .semesterYear(2025)
+ // .semesterTerm(SemesterTerm.FIRST)
+ .build()
+ );
+ });
+ this.clubApplicationFormId = clubApplicationForm.getId();레포지토리에 클럽 기준 조회 메서드가 없다면, 아래 중 하나를 추가해 주세요.
Optional<ClubApplicationForm> findFirstByClubIdOrderByCreatedAtDesc(String clubId);- 또는
List<ClubApplicationForm> findByClubId(String clubId);후 첫 요소 사용.
원하시면 실제 엔티티 생성에 맞춘 테스트용 팩토리/픽스처를 생성해 드립니다.
테스트가 기대하는 낙관적 락이 동작하려면 엔티티에 @Version이 있어야 합니다. 존재 여부와 레포지토리 메서드 유무를 확인해 주세요:
🏁 Script executed:
#!/bin/bash
rg -nP '@Version' backend/src/main/java
rg -nP '(class|record)\s+ClubApplicationForm\b' -n backend/src/main/java -C3
rg -nP 'interface\s+ClubApplicationFormsRepository\s+extends\s+\w+' backend/src/main/java -n -C2
rg -nP 'find(First|Top)?ByClubId' backend/src/main/java -nLength of output: 2139
테스트에서 clubId를 formId로 잘못 전달해 findById가 실패합니다 — 즉시 수정 필요
- 원인(검증됨): backend/src/test/java/moadong/club/service/ClubApplyServiceTest.java (줄 50–53)에서 club.getId()를 findById에 넘겨 NoSuchElementException 발생.
- 검증 결과: backend/src/main/java/moadong/club/entity/ClubApplicationForm.java:55에 @Version 존재(낙관적 락 구현). backend/src/main/java/moadong/club/repository/ClubApplicationFormsRepository.java에는 findById(String)와 findByClubIdAndId(String,String)만 정의되어 있어 "클럽 기준 최신 폼 조회" 메서드는 없음.
- 권장 해결책(우선순위): 테스트 격리 — 테스트에서 ClubApplicationForm을 직접 생성/저장하고 그 id를 사용하도록 변경(예: clubApplicationFormsRepository.save(...); this.clubApplicationFormId = saved.getId()).
- 대안: 리포지토리에 Optional findFirstByClubIdOrderByCreatedAtDesc(String clubId); 추가 후 해당 조회로 사용.
- 조치: 테스트 수정(위 파일 경로) 필요.
🧰 Tools
🪛 GitHub Actions: PR Test
[error] 51-51: NoSuchElementException: 테스트를 위한 ClubApplicationForm 문서가 DB에 존재하지 않습니다. 먼저 문서를 생성해주세요.
🤖 Prompt for AI Agents
In backend/src/test/java/moadong/club/service/ClubApplyServiceTest.java around
lines 50–53, the test incorrectly calls
clubApplicationFormsRepository.findById(club.getId()), causing a
NoSuchElementException because a formId (not clubId) is required; fix by
creating and saving a ClubApplicationForm within the test
(clubApplicationFormsRepository.save(...)) and assign this.clubApplicationFormId
= saved.getId() so the test uses the actual form id (alternatively, add a
repository method like findFirstByClubIdOrderByCreatedAtDesc and use that, but
prefer test isolation by saving a form in the setup).
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (2)
backend/src/main/java/moadong/club/repository/ClubApplicationFormsRepository.java (2)
6-6: 미사용 import 제거 권장
Club엔티티가 현재 코드에서 사용되지 않습니다.다음과 같이 수정하여 미사용 import를 제거하세요:
-import moadong.club.entity.Club; import moadong.club.entity.ClubApplicationForm;
20-20: 메서드명 일관성 개선 제안
findByClubId와findClubApplicationFormsByClubId가 유사한 기능을 수행하지만 명명 규칙이 일관되지 않습니다. Spring Data JPA 표준 명명 규칙을 따르면 더 직관적일 것 같습니다.다음과 같이 메서드명을 통일하는 것을 고려해보세요:
-List<ClubApplicationFormSlim> findClubApplicationFormsByClubId(String clubId, Sort sort); +List<ClubApplicationFormSlim> findByClubId(String clubId, Sort sort);또는 더 명확하게:
List<ClubApplicationForm> findByClubId(String clubId); -List<ClubApplicationFormSlim> findClubApplicationFormsByClubId(String clubId, Sort sort); +List<ClubApplicationFormSlim> findSlimByClubId(String clubId, Sort sort);Also applies to: 26-26
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Jira integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (2)
backend/src/main/java/moadong/club/repository/ClubApplicationFormsRepository.java(1 hunks)backend/src/test/java/moadong/club/service/ClubApplyServiceTest.java(4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- backend/src/test/java/moadong/club/service/ClubApplyServiceTest.java
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-08-25T14:43:52.320Z
Learnt from: lepitaaar
PR: Moadong/moadong#703
File: backend/src/main/java/moadong/club/controller/ClubApplyController.java:84-84
Timestamp: 2025-08-25T14:43:52.320Z
Learning: In the Moadong codebase, questionId and clubId are equivalent identifiers that represent the same entity. The ClubApplicationRepository.findAllByIdInAndQuestionId method correctly uses clubId as the questionId parameter for filtering club applications.
Applied to files:
backend/src/main/java/moadong/club/repository/ClubApplicationFormsRepository.java
📚 Learning: 2025-05-19T05:45:52.957Z
Learnt from: lepitaaar
PR: Moadong/moadong#406
File: backend/src/main/java/moadong/club/service/ClubApplyService.java:34-38
Timestamp: 2025-05-19T05:45:52.957Z
Learning: The code duplication between createClubApplication and editClubApplication methods in ClubApplyService.java is acknowledged but will be addressed in a future refactoring, as per the developer's plan.
Applied to files:
backend/src/main/java/moadong/club/repository/ClubApplicationFormsRepository.java
🧬 Code graph analysis (1)
backend/src/main/java/moadong/club/repository/ClubApplicationFormsRepository.java (1)
backend/src/main/java/moadong/club/repository/ClubApplicationFormsRepositoryCustom.java (1)
Repository(18-69)
🔇 Additional comments (3)
backend/src/main/java/moadong/club/repository/ClubApplicationFormsRepository.java (3)
20-20: 기존 리뷰 의견이 반영되어 안전한 메서드로 변경되었습니다.이전 리뷰에서 지적된
Optional<ClubApplicationForm> findByClubId의 다중 결과 예외 위험이List<ClubApplicationForm> findByClubId로 변경되어 해결되었습니다. 이제 동일 clubId에 대한 여러 학기별 폼이 존재해도 안전하게 모든 결과를 반환할 수 있습니다.
22-26: 쿼리 프로젝션 최적화가 잘 구현되었습니다.필요한 필드만 선택적으로 조회하는
@Query어노테이션 사용이 적절합니다.ClubApplicationFormSlimDTO와의 조합으로 메모리 사용량과 네트워크 전송량을 최적화했습니다.
28-28: 학기별 중복 검증 메서드가 잘 설계되었습니다.
existsByClubIdAndSemesterYearAndSemesterTerm메서드는 동일 학기에 대한 중복 생성을 방지하는 데 효과적입니다. PR 목표에 언급된 "isExist 플래그" 기능 구현에 핵심적인 역할을 할 것으로 보입니다.
#️⃣연관된 이슈
📝작업 내용
중점적으로 리뷰받고 싶은 부분(선택)
논의하고 싶은 부분(선택)
🫡 참고사항
ClubQuestion -> ClubApplicationForm 리팩토링
Summary by CodeRabbit
New Features
Refactor
Style
Tests