Skip to content

Revert "Revert "[feature] 동아리 지원서 폼 제작 시에 학기를 선택할 수 있고 생성된 모든 지원서 폼을 …#762

Merged
alsdddk merged 1 commit intofeature/#748-set-active-form-MOA-239from
revert-750-revert-739-feature/#732-applicaton-semester-selection-MOA-231
Sep 26, 2025
Merged

Revert "Revert "[feature] 동아리 지원서 폼 제작 시에 학기를 선택할 수 있고 생성된 모든 지원서 폼을 …#762
alsdddk merged 1 commit intofeature/#748-set-active-form-MOA-239from
revert-750-revert-739-feature/#732-applicaton-semester-selection-MOA-231

Conversation

@alsdddk
Copy link
Collaborator

@alsdddk alsdddk commented Sep 26, 2025

…학기별로 분류하여 조회할 수 있다""

#️⃣연관된 이슈

ex) #이슈번호, #이슈번호

📝작업 내용

이번 PR에서 작업한 내용을 간략히 설명해주세요(이미지/동영상 첨부 가능)

중점적으로 리뷰받고 싶은 부분(선택)

리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요

ex) 메서드 XXX의 이름을 더 잘 짓고 싶은데 혹시 좋은 명칭이 있을까요?

논의하고 싶은 부분(선택)

논의하고 싶은 부분이 있다면 작성해주세요.

🫡 참고사항

Summary by CodeRabbit

  • New Features
    • 학기(년도/학기) 기반 “지원서 양식” 생성·수정·조회 기능 추가
    • 다가오는 학기 옵션 조회(존재 여부 표시) 제공
    • 지원서 목록 보기: 일반 목록과 학기별 그룹 보기 지원
    • 특정 양식으로 지원 제출 및 지원 현황 조회 가능
    • 양식별 지원자 관리 강화: 일괄 상태 수정 및 삭제
    • 오류 메시지 문구를 더 명확하게 개선

@alsdddk alsdddk self-assigned this Sep 26, 2025
@alsdddk alsdddk requested a review from lepitaaar September 26, 2025 06:50
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 26, 2025

Warning

.coderabbit.yaml has a parsing error

The CodeRabbit configuration file in this repository has a parsing error and default settings were used instead. Please fix the error(s) in the configuration file. You can initialize chat with CodeRabbit to get help with the configuration file.

💥 Parsing errors (1)
Validation error: Invalid regex pattern for base branch. Received: "**" at "reviews.auto_review.base_branches[0]"
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Walkthrough

클럽 지원 도메인을 “질문/지원서”에서 “지원서 양식/지원자” 중심으로 전환. 양식 생성/수정/조회/목록화(집계 포함), 학기 옵션 조회, 양식별 지원/지원 정보 조회, 지원자 일괄 수정/삭제를 추가. 엔티티·리포지토리·DTO/요청/응답·서비스·컨트롤러 전반을 이에 맞게 변경.

Changes

Cohort / File(s) Summary
Controller (엔드포인트 재구성)
backend/src/main/java/moadong/club/controller/ClubApplyController.java
지원서 양식 기반으로 엔드포인트 전면 개편: 학기 옵션 GET /semesters, 양식 생성/수정/조회/목록(agg/flat) 추가, 양식별 지원/정보/지원자 일괄 수정·삭제 경로에 applicationFormId 도입.
Entities (모델 전환/확장)
.../entity/ClubApplicant.java, .../entity/ClubApplicationForm.java, .../entity/ClubApplicationFormQuestion.java
ClubApplication→ClubApplicant로 전환 및 컬렉션명 변경, questionIdformId, 상태 enum 교체. ClubQuestion→ClubApplicationForm로 전환, 컬렉션명 변경, 질문 타입 교체, semesterYear/semesterTerm 필드 추가 및 업데이트 메서드 추가. 질문 엔티티명 변경.
Enums (신규/리네임)
.../enums/ApplicantStatus.java, .../enums/SemesterTerm.java
ApplicationStatus→ApplicantStatus로 리네임. 학기 구분 enum SemesterTerm(FIRST, SECOND) 추가.
DTOs (조회용 DTO 신설/변경)
.../payload/dto/ClubApplicantsResult.java, .../payload/dto/ClubApplicationFormSlim.java, .../payload/dto/ClubApplicationFormsResult.java, .../payload/dto/ClubApplicationFormsResultItem.java
Applicant 모델/enum 반영. 양식 슬림 프로젝션 인터페이스 추가. 양식 집계 결과 레코드와 아이템 레코드 추가.
Request DTOs (양식 생성/수정/지원자 편집)
.../payload/request/ClubApplicantEditRequest.java, .../payload/request/ClubApplicationFormCreateRequest.java, .../payload/request/ClubApplicationFormEditRequest.java
ApplicantStatus로 타입 교체. 양식 생성 요청 도입(질문 목록+학기 필드와 유효성). 양식 수정 요청 리네임.
Response DTOs (응답 타입 신설/교체)
.../payload/response/ClubApplicationFormResponse.java, .../payload/response/ClubApplicationFormsResponse.java, .../payload/response/SemesterOptionResponse.java, .../payload/response/ClubApplicationResponse.java
양식 상세/집계 응답 및 학기 옵션 응답 레코드 추가. 기존 ClubApplicationResponse 제거.
Repositories (신규/대체/제거)
.../repository/ClubApplicantsRepository.java, .../repository/ClubApplicationFormsRepository.java, .../repository/ClubApplicationFormsRepositoryCustom.java, .../repository/ClubApplicationRepository.java, .../repository/ClubQuestionRepository.java
Applicant용 리포지토리 신설(formId 기반 조회). 양식 리포지토리 신설(슬림 프로젝션/존재 여부). 커스텀 집계 리포지토리 추가(연도·학기 그룹핑). 구(old) Application/Question 리포지토리 제거.
Service (비즈니스 로직 대개편)
.../service/ClubApplyService.java
양식 생성/수정/조회/목록(집계/평면), 학기 옵션 계산, 양식별 지원 처리, 지원 정보 조회, 지원자 일괄 수정/삭제 추가. 구 타입을 양식/지원자 모델로 전환, 검증/암호화/정렬 로직 업데이트.
Exceptions (메시지 수정/포맷)
.../global/exception/ErrorCode.java, .../global/exception/GlobalExceptionHandler.java
APPLICATION_NOT_FOUND 메시지 문구 변경(“지원서 양식”). 핸들러 포맷팅 변경(동작 동일).
Tests/Fixtures (테스트 전환)
backend/src/test/java/moadong/club/service/ClubApplyServiceTest.java, backend/src/test/java/moadong/fixture/ClubApplicationEditFixture.java
테스트 전반을 양식 기반으로 수정(리포지토리/엔티티/요청 리네임 반영). 수정용 픽스처 반환 타입 변경.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant C as Client
  participant Cn as ClubApplyController
  participant S as ClubApplyService
  participant AFR as ClubApplicationFormsRepository(+Custom)
  participant AR as ClubApplicantsRepository

  rect rgb(245,248,255)
  note right of C: 양식 생성
  C->>Cn: POST /clubs/{clubId}/apply (FormCreateRequest)
  Cn->>S: createClubApplicationForm(clubId, user, req)
  S->>AFR: save(form)
  AFR-->>S: form(saved)
  S-->>Cn: Created
  Cn-->>C: 201
  end

  rect rgb(245,255,245)
  note right of C: 학기 옵션 조회
  C->>Cn: GET /clubs/{clubId}/semesters?option=3
  Cn->>S: getSemesterOption(clubId, count)
  S->>AFR: existsByClubIdAndSemesterYearAndSemesterTerm(...)
  AFR-->>S: boolean
  S-->>Cn: SemesterOptionResponse[]
  Cn-->>C: 200
  end

  rect rgb(255,250,240)
  note right of C: 양식 목록(집계)
  C->>Cn: GET /clubs/{clubId}/apply?mode=agg
  Cn->>S: getGroupedClubApplicationForms(clubId)
  S->>AFR: findClubApplicationFormsByClubId(clubId) [Aggregation]
  AFR-->>S: grouped results
  S-->>Cn: FormsResponse
  Cn-->>C: 200
  end

  rect rgb(255,245,250)
  note right of C: 양식별 지원
  C->>Cn: POST /clubs/{clubId}/apply/{applicationFormId}
  Cn->>S: applyToClub(clubId, formId, req)
  S->>AFR: findByClubIdAndId(clubId, formId)
  AFR-->>S: form
  S->>AR: save(ClubApplicant)
  AR-->>S: applicant(saved)
  S-->>Cn: OK
  Cn-->>C: 200
  end

  rect rgb(250,250,255)
  note right of C: 지원 정보 조회(양식별)
  C->>Cn: GET /clubs/{clubId}/apply/info/{applicationFormId}
  Cn->>S: getClubApplyInfo(clubId, formId, user)
  S->>AFR: findByClubIdAndId(...)
  AFR-->>S: form
  S->>AR: findAllByFormId(formId)
  AR-->>S: applicants
  S-->>Cn: stats + applicants
  Cn-->>C: 200
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested labels

✨ Feature, 📬 API, 💾 BE

Suggested reviewers

  • lepitaaar
  • Zepelown
  • PororoAndFriends
  • oesnuj

Pre-merge checks and finishing touches

❌ Failed checks (3 warnings)
Check name Status Explanation Resolution
Title Check ⚠️ Warning 제안된 제목이 “Revert“ 처리된 커밋 메시지를 그대로 반영하고 있어 주요 변경사항인 학기 선택 기능 추가를 간결하고 명확하게 전달하지 못합니다. 제목을 “동아리 지원서 폼에 학기 선택 기능 추가”처럼 주요 변경사항을 직접적으로 요약하도록 간결하게 수정하세요.
Out of Scope Changes Check ⚠️ Warning 이번 PR에는 학기 선택 기능 외에도 ClubApplication→ClubApplicationForm, ClubApplicant 도메인 리팩터링, 신청자 관리 로직 추가 등 이슈 MOA-231 범위를 벗어난 대규모 구조 및 기능 변경이 포함되어 있습니다. 학기 선택 기능 구현과 무관한 리팩터링이나 신규 기능은 별도 PR로 분리해 리뷰 범위를 명확히 조정하세요.
Docstring Coverage ⚠️ Warning Docstring coverage is 5.17% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues Check ✅ Passed PR에서 생성 요청에 semesterYear와 semesterTerm 필드를 추가하고 학기 옵션 조회 엔드포인트 및 유효성 검사를 구현하여 이슈 MOA-231의 “동아리 지원서 제작 시에 학기를 선택할 수 있다” 요구사항을 충실히 만족합니다.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch revert-750-revert-739-feature/#732-applicaton-semester-selection-MOA-231

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@vercel
Copy link

vercel bot commented Sep 26, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
moadong Ready Ready Preview Comment Sep 26, 2025 6:50am

@alsdddk alsdddk requested a review from Zepelown September 26, 2025 06:50
@PororoAndFriends PororoAndFriends self-requested a review September 26, 2025 06:54
Copy link
Collaborator

@PororoAndFriends PororoAndFriends left a comment

Choose a reason for hiding this comment

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

수고하셨습니다

Copy link
Collaborator

@PororoAndFriends PororoAndFriends left a comment

Choose a reason for hiding this comment

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

수고하셨습니다

@alsdddk alsdddk merged commit 3398628 into feature/#748-set-active-form-MOA-239 Sep 26, 2025
2 of 3 checks passed
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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/ClubApplicationFormCreateRequest.java (1)

3-34: 중복 import 때문에 컴파일이 실패합니다

java.util.List를 동일하게 두 번 import 하고 있어 javac에서 중복 import 에러가 발생합니다. 아래처럼 두 번째 선언을 제거해 주세요.

-import jakarta.validation.constraints.Size;
-import java.util.List;
-import moadong.club.enums.SemesterTerm;
-
-import java.util.List;
+import jakarta.validation.constraints.Size;
+import java.util.List;
+import moadong.club.enums.SemesterTerm;
backend/src/main/java/moadong/club/service/ClubApplyService.java (2)

293-314: 권한 우회 취약점: 타 클럽 양식의 지원자 일괄수정 가능

오너 검증은 clubId 기준으로만 수행되고, applicationFormId가 해당 club 소속인지 검증이 없습니다. 소속 검증을 추가하세요.

수정:

     public void editApplicantDetail(String clubId, String applicationFormId, List<ClubApplicantEditRequest> request, CustomUserDetails user) {
         validateClubOwner(clubId, user);
+        clubApplicationFormsRepository.findByClubIdAndId(clubId, applicationFormId)
+                .orElseThrow(() -> new RestApiException(ErrorCode.APPLICATION_NOT_FOUND));

346-356: NPE 및 요구값 공백 허용 문제

answer.value()가 null이면 NPE로 500이 발생하고, required 텍스트 질문도 공백이 통과합니다. null/blank 가드와 길이 검증 순서를 보강하세요.

수정:

         for (ClubApplyRequest.Answer answer : answers) {
             ClubApplicationFormQuestion question = questionMap.get(answer.id());
             // 질문이 없을 경우 예외 처리
             if (question == null) {
                 throw new RestApiException(ErrorCode.QUESTION_NOT_FOUND);
             }
-            validateAnswerLength(answer.value(), question.getType());
+            // required 텍스트형은 공백 불가
+            if (question.getOptions().getRequired()
+                    && (answer.value() == null || answer.value().trim().isEmpty())) {
+                throw new RestApiException(ErrorCode.REQUIRED_QUESTION_MISSING);
+            }
+            validateAnswerLength(answer.value(), question.getType());
         }

또한 아래 메서드에 null 가드 추가:

-    private void validateAnswerLength(String value, ClubApplicationQuestionType type) {
+    private void validateAnswerLength(String value, ClubApplicationQuestionType type) {
+        if (value == null) return; // required 검증에서 처리
         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);
                 }
             }
         }
     }
🧹 Nitpick comments (8)
backend/src/test/java/moadong/club/service/ClubApplyServiceTest.java (1)

51-56: 테스트 데이터 의존성 주의

테스트가 DB에 이미 존재하는 ClubApplicationForm 데이터에 의존하고 있습니다. 테스트 격리를 위해 @BeforeEach에서 테스트용 폼을 생성하거나 @Sql 어노테이션을 사용한 데이터 초기화를 고려해보세요.

 @BeforeEach
 void setUp() {
     User user = userRepository.findUserByUserId(UserFixture.collectUserId).get();
     userDetails = new CustomUserDetails(user);
     Club club = clubRepository.findClubByUserId(user.getId()).get();

-    // 테스트 전에 문서를 한 번만 조회하여 ID를 확보
-    if (clubApplicationFormsRepository.findByClubId(club.getId()).isEmpty()){
-        throw new NoSuchElementException("테스트를 위한 ClubApplicationForm 문서가 DB에 존재하지 않습니다. 먼저 문서를 생성해주세요.");
-    }
-    ClubApplicationForm clubApplicationForm = clubApplicationFormsRepository.findByClubId(club.getId()).get(0);
-    this.clubApplicationFormId = clubApplicationForm.getId();
+    // 테스트 격리를 위해 새로운 폼 생성
+    ClubApplicationForm testForm = ClubApplicationForm.builder()
+        .clubId(club.getId())
+        .title("테스트 폼")
+        .description("테스트용 지원서")
+        .build();
+    ClubApplicationForm savedForm = clubApplicationFormsRepository.save(testForm);
+    this.clubApplicationFormId = savedForm.getId();
 }
backend/src/main/java/moadong/club/repository/ClubApplicationFormsRepositoryCustom.java (1)

42-47: Document 내 필드명 일관성

Document 생성 시 _id를 사용하고 있는데, 다른 부분에서는 id로 alias를 사용합니다. 일관성을 위해 통일하는 것이 좋겠습니다.

 GroupOperation groupOperation = Aggregation.group("semesterYear","semesterTerm")
-        .push(new Document("_id", "$_id")
+        .push(new Document("id", "$id")
                 .append("title", "$title")
                 .append("editedAt", "$editedAt"))
         .as("forms");
backend/src/main/java/moadong/club/entity/ClubApplicationForm.java (1)

75-81: setter 메서드 검증 추가 고려

updateSemesterYearupdateSemesterTerm 메서드에 유효성 검증이 없습니다. 비즈니스 로직에 따라 연도나 학기 값의 유효성을 확인하는 것이 좋을 수 있습니다.

 public void updateSemesterYear(Integer semesterYear) {
+    if (semesterYear == null || semesterYear < 2000 || semesterYear > 2100) {
+        throw new IllegalArgumentException("유효하지 않은 연도입니다: " + semesterYear);
+    }
     this.semesterYear = semesterYear;
 }

 public void updateSemesterTerm(SemesterTerm semesterTerm) {
+    if (semesterTerm == null) {
+        throw new IllegalArgumentException("학기 정보는 null일 수 없습니다");
+    }
     this.semesterTerm = semesterTerm;
 }
backend/src/main/java/moadong/club/controller/ClubApplyController.java (2)

48-51: /semesters 파라미터 이름 및 유효성 보강

파라미터명이 option(실제 용도: count)이라 혼동됩니다. 이름 정정 및 범위 제한을 권장합니다(예: 1~6).

아래처럼 변경:

-    public ResponseEntity<?> getSemesterOption(@PathVariable String clubId,
-                                                @RequestParam(value = "option", required = false, defaultValue = "3") int count) {
+    public ResponseEntity<?> getSemesterOption(@PathVariable String clubId,
+                                               @RequestParam(value = "count", required = false, defaultValue = "3")
+                                               @jakarta.validation.constraints.Min(1)
+                                               @jakarta.validation.constraints.Max(6)
+                                               int count) {

클래스에 파라미터 검증 활성화를 위해 @validated 추가 필요(아래 참고).
추가 변경(클래스 선언부 근처):

// 클래스 상단 어노테이션에 추가
@Validated

64-72: 매직 스트링(mode) 제거 권장

"agg/server" 문자열 비교 대신 enum 파라미터로 모델링하세요. 오타/대소문자 문제를 제거합니다.

예시:

enum ListMode { AGG, SERVER }
// 컨트롤러
public ResponseEntity<?> getClubApplications(@PathVariable String clubId, @RequestParam(defaultValue = "AGG") ListMode mode) {
    return mode == ListMode.SERVER
        ? Response.ok(clubApplyService.getGroupedClubApplicationForms(clubId))
        : Response.ok(clubApplyService.getClubApplicationForms(clubId));
}
backend/src/main/java/moadong/club/service/ClubApplyService.java (3)

3-3: 트랜잭션 어노테이션 소스 통일

Spring의 트랜잭션 관리에 맞춰 org.springframework.transaction.annotation.Transactional 사용을 권장합니다.

아래처럼 교체:

-import jakarta.transaction.Transactional;
+import org.springframework.transaction.annotation.Transactional;

91-101: 예외 타입 일관성

IllegalArgumentException 대신 RestApiException(ErrorCode...)으로 통일해 API 에러 응답 규약을 유지하세요.


374-409: create/update 질문 매핑 중복

createQuestions와 updateQuestions가 거의 동일합니다. 매핑 로직을 단일 유틸/프라이빗 메서드로 통합하세요. 유지보수 비용을 줄입니다. 기존에도 중복 인지되어 있던 사항입니다.

Based on learnings

Also applies to: 414-447

📜 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 1f9f275 and 0a90ed0.

📒 Files selected for processing (27)
  • 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/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/ClubApplicationFormSlim.java (1 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/ClubApplicationFormCreateRequest.java (2 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/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 (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)
  • backend/src/test/java/moadong/fixture/ClubApplicationEditFixture.java (2 hunks)
💤 Files with no reviewable changes (3)
  • backend/src/main/java/moadong/club/payload/response/ClubApplicationResponse.java
  • backend/src/main/java/moadong/club/repository/ClubQuestionRepository.java
  • backend/src/main/java/moadong/club/repository/ClubApplicationRepository.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/test/java/moadong/fixture/ClubApplicationEditFixture.java
  • backend/src/main/java/moadong/club/service/ClubApplyService.java
  • backend/src/main/java/moadong/club/payload/dto/ClubApplicantsResult.java
  • backend/src/test/java/moadong/club/service/ClubApplyServiceTest.java
  • backend/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/entity/ClubApplicationFormQuestion.java
  • backend/src/main/java/moadong/club/entity/ClubApplicant.java
  • backend/src/main/java/moadong/club/repository/ClubApplicantsRepository.java
  • backend/src/main/java/moadong/club/repository/ClubApplicationFormsRepository.java
  • backend/src/main/java/moadong/club/service/ClubApplyService.java
  • backend/src/main/java/moadong/club/entity/ClubApplicationForm.java
  • backend/src/main/java/moadong/club/repository/ClubApplicationFormsRepositoryCustom.java
  • backend/src/main/java/moadong/club/payload/dto/ClubApplicantsResult.java
  • backend/src/test/java/moadong/club/service/ClubApplyServiceTest.java
  • backend/src/main/java/moadong/club/controller/ClubApplyController.java
🧬 Code graph analysis (7)
backend/src/main/java/moadong/club/entity/ClubApplicant.java (1)
backend/src/main/java/moadong/club/entity/ClubApplicationForm.java (1)
  • Document (18-87)
backend/src/main/java/moadong/club/repository/ClubApplicantsRepository.java (1)
backend/src/main/java/moadong/club/repository/ClubApplicationRepository.java (1)
  • ClubApplicationRepository (10-17)
backend/src/main/java/moadong/club/repository/ClubApplicationFormsRepository.java (1)
backend/src/main/java/moadong/club/repository/ClubApplicationFormsRepositoryCustom.java (1)
  • Repository (18-69)
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)
backend/src/main/java/moadong/club/entity/ClubApplicationForm.java (1)
backend/src/main/java/moadong/club/entity/ClubApplicationFormQuestion.java (1)
  • AllArgsConstructor (12-36)
backend/src/main/java/moadong/club/repository/ClubApplicationFormsRepositoryCustom.java (1)
backend/src/main/java/moadong/club/entity/ClubApplicationForm.java (1)
  • Document (18-87)
backend/src/test/java/moadong/club/service/ClubApplyServiceTest.java (2)
backend/src/test/java/moadong/fixture/ClubApplicationEditFixture.java (1)
  • ClubApplicationEditFixture (6-23)
backend/src/test/java/moadong/fixture/UserFixture.java (1)
  • UserFixture (9-33)
🔇 Additional comments (19)
backend/src/main/java/moadong/global/exception/GlobalExceptionHandler.java (1)

24-25: 들여쓰기 통일 감사합니다

메서드 체이닝 구간의 정렬이 정돈돼서 가독성이 좋아졌습니다.

Also applies to: 44-45

backend/src/main/java/moadong/global/exception/ErrorCode.java (1)

40-40: 메시지 변경 적절

용어를 ‘지원서 양식’으로 통일한 흐름과 잘 맞습니다. 상태 코드와 식별자는 그대로 유지되어 있어 기존 처리에도 영향이 없네요. 👍

backend/src/main/java/moadong/club/entity/ClubApplicationFormQuestion.java (1)

12-36: 클래스 명칭 변경이 도메인 전환에 잘 부합합니다

폼 중심 모델로의 전환에 맞게 엔티티 이름이 정리돼 가독성과 추후 유지보수성이 좋아졌습니다.

backend/src/main/java/moadong/club/payload/request/ClubApplicationFormEditRequest.java (1)

9-22: Edit 요청 레코드 구조 깔끔합니다

필수 필드 검증과 중첩 질문 유효성 검증이 그대로 유지돼 있어 기존 동작을 안전하게 이어갈 수 있겠습니다.

backend/src/main/java/moadong/club/enums/SemesterTerm.java (1)

3-6: SemesterTerm enum 추가 좋습니다

명확한 학기 구분 값으로 서빙 · 검증 로직이 훨씬 명시적으로 바뀌겠네요.

backend/src/main/java/moadong/club/payload/request/ClubApplicantEditRequest.java (1)

6-18: ApplicantStatus로의 전환 확인했습니다

지원자 상태 표현이 새로운 도메인 모델과 일관돼 직관성이 좋아졌습니다.

backend/src/main/java/moadong/club/enums/ApplicantStatus.java (1)

3-8: 상태 전환 명확화 확인

DECLINED가 추가되어 지원자 여정 전반을 담을 수 있게 된 점 좋습니다. 다른 레이어에서도 동일 enum을 참조하도록 이미 일괄 변경된 것으로 보이며, 별도 이슈 없어 보입니다.

backend/src/main/java/moadong/club/payload/dto/ClubApplicationFormsResult.java (1)

6-9: 학기 단위 DTO 구성 적절

학년·학기·폼 목록으로 레코드를 묶은 구조가 조회 응답 목적에 잘 맞습니다. 단일 위치에서 그룹핑 결과를 전달하기에 충분해 보입니다.

backend/src/test/java/moadong/fixture/ClubApplicationEditFixture.java (1)

7-21: 테스트 픽스처 갱신 OK

폼 기반 요청 객체로 픽스처가 정리되어 새 서비스 흐름과 일치합니다. 기본 필드 값도 테스트 목적에 적절합니다.

backend/src/main/java/moadong/club/payload/dto/ClubApplicationFormsResultItem.java (1)

5-9: 개별 폼 슬라이스 표현 괜찮습니다

ID/제목/수정 시각만 노출하는 레코드 형태가 가볍고 재사용성이 높습니다. 추가 조정 필요 없어 보입니다.

backend/src/main/java/moadong/club/payload/response/SemesterOptionResponse.java (1)

6-12: 학기 옵션 응답 구조 확인

@Builder(toBuilder = true) 적용으로 선택지 재조합이 쉬워지고, 학기·존재 여부 필드 구성도 명확합니다. 추가 변경 사항 없습니다.

backend/src/main/java/moadong/club/payload/response/ClubApplicationFormsResponse.java (1)

8-12: 폼 응답 래퍼 적절

상위 응답에서 리스트만 감싸는 전용 레코드를 둔 설계가 API 일관성 유지에 도움이 됩니다. 문제 없습니다.

backend/src/main/java/moadong/club/payload/dto/ClubApplicationFormSlim.java (1)

7-12: 슬림 Projection 정의 적합

Spring Data projection으로 쓰기 좋은 인터페이스 형태이며, 노출 필드도 서비스 요구사항과 일치해 보입니다. 👍

backend/src/main/java/moadong/club/payload/dto/ClubApplicantsResult.java (1)

24-45: 지원자 DTO 전환 정상 동작

ClubApplicant로의 전환과 상태 enum 변경이 일관되게 반영되었습니다. AES 복호화 로직도 기존 흐름 그대로 유지됩니다.

backend/src/main/java/moadong/club/payload/response/ClubApplicationFormResponse.java (1)

1-17: LGTM! 새로운 응답 DTO 구조가 명확합니다.

학기 정보(semesterYear, semesterTerm)를 포함한 폼 응답 구조가 잘 설계되었습니다. record와 @builder를 함께 사용하여 불변성과 편의성을 모두 확보했네요.

backend/src/main/java/moadong/club/entity/ClubApplicant.java (1)

14-14: 엔티티 리네이밍이 잘 반영되었습니다.

ClubApplicationClubApplicant, ApplicationStatusApplicantStatus, questionIdformId로의 도메인 용어 변경이 일관되게 적용되었습니다. 기본 상태값도 SUBMITTED로 적절히 설정되어 있네요.

Also applies to: 17-17, 21-21, 26-26, 30-30, 45-46

backend/src/test/java/moadong/club/service/ClubApplyServiceTest.java (1)

89-140: 동시성 테스트 구현이 우수합니다!

CyclicBarrier를 사용하여 스레드들이 동시에 업데이트를 시도하도록 보장하는 것이 좋은 접근입니다. MongoDB의 낙관적 잠금이 제대로 작동하는지 검증하는 테스트로 적절합니다.

backend/src/main/java/moadong/club/repository/ClubApplicationFormsRepositoryCustom.java (1)

51-53: 누락된 케이스 없음 확인
SemesterTerm enum에 정의된 값이 FIRST, SECOND 두 가지뿐이므로 정렬 로직 수정 불필요합니다.

Likely an incorrect or invalid review comment.

backend/src/main/java/moadong/club/controller/ClubApplyController.java (1)

93-101: 비인증 지원 의도 확인

POST /apply/{applicationFormId}는 인증이 필요 없습니다. 비회원 지원을 의도한 것인지 확인 바랍니다.

의도와 다르면 @PreAuthorize("isAuthenticated()") 추가를 권장합니다.

Comment on lines +42 to +46
@Operation(summary = "클럽 지원서 생성 가능 학기 불러오기", description = "생성 가능한 학기를 불러옵니다<br>"
+ "<br>"
+ "기준일로부터 이번 학기, 다음 학기, 다다음 학기를 불러옴<br>"
+ "ex) 2025/09/01 -> 2025-2학기, 2025-겨울학기, 2026-1학기")
@PreAuthorize("isAuthenticated()")
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

문서/구현 불일치: 겨울학기 표기

설명에 ‘겨울학기’ 예시가 있으나 서비스는 FIRST/SECOND 2학기 체계만 반환합니다. 문구를 정합하게 업데이트하세요.

아래와 같이 수정 권장:

-    @Operation(summary = "클럽 지원서 생성 가능 학기 불러오기", description = "생성 가능한 학기를 불러옵니다<br>"
-    + "<br>"
-    + "기준일로부터 이번 학기, 다음 학기, 다다음 학기를 불러옴<br>"
-    + "ex) 2025/09/01 -> 2025-2학기, 2025-겨울학기, 2026-1학기")
+    @Operation(summary = "클럽 지원서 생성 가능 학기 불러오기", description = "생성 가능한 학기를 불러옵니다.<br>"
+    + "<br>"
+    + "기준일로부터 이번 학기부터 최대 3개의 학기를 반환합니다.<br>"
+    + "예) 2025/09/01 -> 2025-2학기, 2026-1학기, 2026-2학기")
🤖 Prompt for AI Agents
In backend/src/main/java/moadong/club/controller/ClubApplyController.java around
lines 42 to 46, the method JavaDoc/example mentions "겨울학기" but the service only
returns a two-term system (FIRST/SECOND); update the documentation to match
implementation by removing or replacing the "겨울학기" example and adjusting the
example string to use only the two-term notation (e.g., "2025-2학기, 2026-1학기") or
otherwise describe that only FIRST/SECOND terms are returned so the comment and
description align with the actual returned values.

Comment on lines +52 to +53
private SemesterTerm semesterTerm = (ZonedDateTime.now(ZoneId.of("Asia/Seoul")).toLocalDate().getMonthValue() < 7)
? SemesterTerm.FIRST : SemesterTerm.SECOND; //1학기, 2학기
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

학기 결정 로직 확인 필요

현재 7월 기준으로 1학기/2학기를 구분하고 있는데, 실제 학사일정과 맞는지 확인이 필요합니다. 일반적으로 한국 대학은 3월이 1학기, 9월이 2학기 시작입니다.

학기 구분 기준이 올바른지 확인하겠습니다:

필요하다면 다음과 같이 수정을 제안합니다:

-    private SemesterTerm semesterTerm = (ZonedDateTime.now(ZoneId.of("Asia/Seoul")).toLocalDate().getMonthValue() < 7)
-            ? SemesterTerm.FIRST : SemesterTerm.SECOND; //1학기, 2학기
+    private SemesterTerm semesterTerm = (ZonedDateTime.now(ZoneId.of("Asia/Seoul")).toLocalDate().getMonthValue() < 9)
+            ? SemesterTerm.FIRST : SemesterTerm.SECOND; // 1-8월: 1학기, 9-12월: 2학기

🌐 Web query:

한국 대학 학기 시작 월은 언제인가요? 1학기와 2학기는 각각 몇월에 시작하나요?

💡 Result:

한국 대학의 정규 학기는 보통 1학기(봄학기)가 3월(대부분 3월 첫째주)에 시작하고, 2학기(가을학기)는 9월(대부분 9월 첫째주)에 시작됩니다. [1][2]

참고: 대학별로 세부 일정(예: 예비 신입생 일정, 계절학기, 일부 교과 과정)은 차이가 있을 수 있습니다. [1]


학기 결정 로직을 9월 기준으로 변경
한국 대학의 정규 학기는 1학기(봄학기)가 3월, 2학기(가을학기)가 9월에 시작하므로 분기 기준을 7월(<7)에서 9월(<9)로 수정해야 합니다.

-    private SemesterTerm semesterTerm = (ZonedDateTime.now(ZoneId.of("Asia/Seoul")).toLocalDate().getMonthValue() < 7)
-            ? SemesterTerm.FIRST : SemesterTerm.SECOND;
+    private SemesterTerm semesterTerm = (ZonedDateTime.now(ZoneId.of("Asia/Seoul")).toLocalDate().getMonthValue() < 9)
+            ? SemesterTerm.FIRST : SemesterTerm.SECOND; // 1-8월: 1학기, 9-12월: 2학기
📝 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.

Suggested change
private SemesterTerm semesterTerm = (ZonedDateTime.now(ZoneId.of("Asia/Seoul")).toLocalDate().getMonthValue() < 7)
? SemesterTerm.FIRST : SemesterTerm.SECOND; //1학기, 2학기
private SemesterTerm semesterTerm = (ZonedDateTime.now(ZoneId.of("Asia/Seoul")).toLocalDate().getMonthValue() < 9)
? SemesterTerm.FIRST : SemesterTerm.SECOND; // 1-8월: 1학기, 9-12월: 2학기
🤖 Prompt for AI Agents
In backend/src/main/java/moadong/club/entity/ClubApplicationForm.java around
lines 52-53, the semester determination currently uses a July breakpoint
(getMonthValue() < 7) but should use September as the boundary; change the
condition to use < 9 (e.g.,
ZonedDateTime.now(ZoneId.of("Asia/Seoul")).toLocalDate().getMonthValue() < 9) so
months Jan–Aug map to FIRST and Sep–Dec map to SECOND, keeping the existing
ZoneId and overall assignment pattern.


public interface ClubApplicantsRepository extends MongoRepository<ClubApplicant, String> {
@Query("{ 'formId': ?0, 'status': { $exists: true, $ne: 'DRAFT' } }")
List<ClubApplicant> findAllByFormId(String questionId);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

파라미터명과 실제 필드 불일치

메서드 시그니처의 파라미터명이 questionId로 되어있지만, 쿼리에서는 formId를 사용하고 있습니다. 일관성을 위해 파라미터명도 formId로 변경하는 것이 좋겠습니다.

-    List<ClubApplicant> findAllByFormId(String questionId);
+    List<ClubApplicant> findAllByFormId(String formId);
🤖 Prompt for AI Agents
In backend/src/main/java/moadong/club/repository/ClubApplicantsRepository.java
around line 10, the method parameter is named questionId while the query expects
formId; rename the parameter to formId to match the field and update any call
sites to use the new name so the method signature becomes findAllByFormId(String
formId) and remains consistent with the repository query.

import java.util.List;
import java.util.Optional;

import moadong.club.entity.Club;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

사용하지 않는 import 제거

Club 엔티티가 import되어 있지만 실제로 사용되지 않습니다.

-import moadong.club.entity.Club;
 import moadong.club.entity.ClubApplicationForm;
📝 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.

Suggested change
import moadong.club.entity.Club;
import moadong.club.entity.ClubApplicationForm;
🤖 Prompt for AI Agents
backend/src/main/java/moadong/club/repository/ClubApplicationFormsRepository.java
around line 6: remove the unused import of moadong.club.entity.Club because it
is not referenced in this file; delete the import line to clean up imports and
avoid compiler/IDE warnings, then save and run a build or organize imports to
ensure no other unused imports remain.

@Repository
public interface ClubApplicationFormsRepository extends MongoRepository<ClubApplicationForm, String> {

Optional<ClubApplicationForm> findById(String formId);
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

불필요한 메서드 선언

findById는 이미 MongoRepository에서 제공하는 메서드입니다. 재선언할 필요가 없습니다.

-    Optional<ClubApplicationForm> findById(String formId);
📝 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.

Suggested change
Optional<ClubApplicationForm> findById(String formId);
🤖 Prompt for AI Agents
In
backend/src/main/java/moadong/club/repository/ClubApplicationFormsRepository.java
around line 18, remove the explicit declaration of Optional<ClubApplicationForm>
findById(String formId); because MongoRepository already provides findById;
delete that line from the interface and ensure no unused imports or compilation
issues remain.

Comment on lines +56 to 67
public List<SemesterOptionResponse> getSemesterOption(String clubId, int count) {
LocalDate baseDate = ZonedDateTime.now(ZoneId.of("Asia/Seoul")).toLocalDate();
List<OptionItem> items = buildOptionItems(baseDate, count);

public void createClubApplication(String clubId, CustomUserDetails user, ClubApplicationCreateRequest request) {
ClubQuestion clubQuestion = getClubQuestion(clubId, user);
return items.stream()
.map(it -> SemesterOptionResponse.builder()
.semesterYear(it.year())
.term(it.term())
.isExist(clubApplicationFormsRepository.existsByClubIdAndSemesterYearAndSemesterTerm(clubId, it.year(), it.term()))
.build())
.toList();

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

/semesters count 입력값 가드 추가

대량 요청 방지를 위해 서비스 레벨에서도 범위를 강제하세요(예: 1~6).

아래처럼 보강:

-        List<OptionItem> items = buildOptionItems(baseDate, count);
+        int safeCount = Math.max(1, Math.min(6, count));
+        List<OptionItem> items = buildOptionItems(baseDate, safeCount);
📝 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.

Suggested change
public List<SemesterOptionResponse> getSemesterOption(String clubId, int count) {
LocalDate baseDate = ZonedDateTime.now(ZoneId.of("Asia/Seoul")).toLocalDate();
List<OptionItem> items = buildOptionItems(baseDate, count);
public void createClubApplication(String clubId, CustomUserDetails user, ClubApplicationCreateRequest request) {
ClubQuestion clubQuestion = getClubQuestion(clubId, user);
return items.stream()
.map(it -> SemesterOptionResponse.builder()
.semesterYear(it.year())
.term(it.term())
.isExist(clubApplicationFormsRepository.existsByClubIdAndSemesterYearAndSemesterTerm(clubId, it.year(), it.term()))
.build())
.toList();
public List<SemesterOptionResponse> getSemesterOption(String clubId, int count) {
LocalDate baseDate = ZonedDateTime.now(ZoneId.of("Asia/Seoul")).toLocalDate();
int safeCount = Math.max(1, Math.min(6, count));
List<OptionItem> items = buildOptionItems(baseDate, safeCount);
return items.stream()
.map(it -> SemesterOptionResponse.builder()
.semesterYear(it.year())
.term(it.term())
.isExist(clubApplicationFormsRepository.existsByClubIdAndSemesterYearAndSemesterTerm(clubId, it.year(), it.term()))
.build())
.toList();
}
🤖 Prompt for AI Agents
In backend/src/main/java/moadong/club/service/ClubApplyService.java around lines
56-67, add a guard on the incoming count parameter to enforce an allowed range
(e.g. 1..6) to prevent mass requests; at the start of getSemesterOption validate
if count < 1 || count > 6 and return a clear failure (throw an
IllegalArgumentException or a service-level BadRequest exception) with a
descriptive message, or alternatively clamp the value to the allowed range
before calling buildOptionItems.

Comment on lines +108 to 116
ClubApplicationForm clubApplicationForm = createQuestions(
ClubApplicationForm.builder()
.clubId(clubId)
.semesterYear(request.semesterYear())
.semesterTerm(request.semesterTerm())
.build(),
request);
clubApplicationFormsRepository.save(createQuestions(clubApplicationForm, request));
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

불필요한 createQuestions 중복 호출

createQuestions가 두 번 호출됩니다(동일 객체를 반복 갱신). 한 번만 호출하도록 정리하세요.

아래로 수정:

-        ClubApplicationForm clubApplicationForm = createQuestions(
+        ClubApplicationForm clubApplicationForm = createQuestions(
                 ClubApplicationForm.builder()
                         .clubId(clubId)
                         .semesterYear(request.semesterYear())
                         .semesterTerm(request.semesterTerm())
                         .build(),
                 request);
-        clubApplicationFormsRepository.save(createQuestions(clubApplicationForm, request));
+        clubApplicationFormsRepository.save(clubApplicationForm);
📝 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.

Suggested change
ClubApplicationForm clubApplicationForm = createQuestions(
ClubApplicationForm.builder()
.clubId(clubId)
.semesterYear(request.semesterYear())
.semesterTerm(request.semesterTerm())
.build(),
request);
clubApplicationFormsRepository.save(createQuestions(clubApplicationForm, request));
}
ClubApplicationForm clubApplicationForm = createQuestions(
ClubApplicationForm.builder()
.clubId(clubId)
.semesterYear(request.semesterYear())
.semesterTerm(request.semesterTerm())
.build(),
request);
clubApplicationFormsRepository.save(clubApplicationForm);
}
🤖 Prompt for AI Agents
In backend/src/main/java/moadong/club/service/ClubApplyService.java around lines
108 to 116, createQuestions is called twice creating redundant updates; call it
only once, assign its return to clubApplicationForm and pass that same instance
to clubApplicationFormsRepository.save(...) instead of invoking createQuestions
a second time.

Comment on lines +119 to 127
public void editClubApplication(String clubId, String applicationFormId, CustomUserDetails user, ClubApplicationFormEditRequest request) {
validateClubOwner(clubId, user);

updateQuestions(clubQuestion, request);
clubQuestion.updateEditedAt();
ClubApplicationForm clubApplicationForm = clubApplicationFormsRepository.findById(applicationFormId)
.orElseThrow(() -> new RestApiException(ErrorCode.APPLICATION_NOT_FOUND));

clubQuestionRepository.save(clubQuestion);
clubApplicationForm.updateEditedAt();
clubApplicationFormsRepository.save(updateQuestions(clubApplicationForm, request));
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

권한 우회 취약점: 타 클럽 양식 수정 가능

findById만 사용하여 applicationFormId가 다른 클럽 소속이어도 수정이 가능합니다. clubId와 함께 조회로 변경하세요.

수정:

-        ClubApplicationForm clubApplicationForm = clubApplicationFormsRepository.findById(applicationFormId)
+        ClubApplicationForm clubApplicationForm = clubApplicationFormsRepository.findByClubIdAndId(clubId, applicationFormId)
                 .orElseThrow(() -> new RestApiException(ErrorCode.APPLICATION_NOT_FOUND));
📝 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.

Suggested change
public void editClubApplication(String clubId, String applicationFormId, CustomUserDetails user, ClubApplicationFormEditRequest request) {
validateClubOwner(clubId, user);
updateQuestions(clubQuestion, request);
clubQuestion.updateEditedAt();
ClubApplicationForm clubApplicationForm = clubApplicationFormsRepository.findById(applicationFormId)
.orElseThrow(() -> new RestApiException(ErrorCode.APPLICATION_NOT_FOUND));
clubQuestionRepository.save(clubQuestion);
clubApplicationForm.updateEditedAt();
clubApplicationFormsRepository.save(updateQuestions(clubApplicationForm, request));
}
public void editClubApplication(String clubId, String applicationFormId, CustomUserDetails user, ClubApplicationFormEditRequest request) {
validateClubOwner(clubId, user);
ClubApplicationForm clubApplicationForm = clubApplicationFormsRepository.findByClubIdAndId(clubId, applicationFormId)
.orElseThrow(() -> new RestApiException(ErrorCode.APPLICATION_NOT_FOUND));
clubApplicationForm.updateEditedAt();
clubApplicationFormsRepository.save(updateQuestions(clubApplicationForm, request));
}
🤖 Prompt for AI Agents
backend/src/main/java/moadong/club/service/ClubApplyService.java around lines
119 to 127: the current lookup uses findById(applicationFormId) only, allowing
editing of an application form that belongs to a different club; change the
retrieval to require both clubId and applicationFormId (e.g., use repository
method findByIdAndClubId or findByIdAndClub_Id) or fetch by id and then verify
the form's clubId equals the provided clubId, throwing
RestApiException(ErrorCode.APPLICATION_NOT_FOUND) if not; then proceed to
updateEditedAt and save the validated entity.

Comment on lines +128 to +137
@Transactional //test 사용
public void editClubApplicationQuestion(String applicationFormId, CustomUserDetails user, ClubApplicationFormEditRequest request) {
ClubApplicationForm clubApplicationForm = clubApplicationFormsRepository.findById(applicationFormId)
.orElseThrow(() -> new RestApiException(ErrorCode.APPLICATION_NOT_FOUND));

updateQuestions(clubApplicationForm, request);
clubApplicationForm.updateEditedAt();

public ResponseEntity<?> getClubApplication(String clubId) {
ClubQuestion clubQuestion = clubQuestionRepository.findByClubId(clubId)
clubApplicationFormsRepository.save(clubApplicationForm);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

테스트용 메서드라도 오너 검증 필요 또는 가시성 축소

public 메서드가 오너 검증 없이 임의 양식 수정이 가능합니다. 최소한 오너 검증 추가 또는 패키지 프라이빗/테스트 전용으로 축소하세요.

권장 수정:

     public void editClubApplicationQuestion(String applicationFormId, CustomUserDetails user, ClubApplicationFormEditRequest request) {
         ClubApplicationForm clubApplicationForm = clubApplicationFormsRepository.findById(applicationFormId)
                 .orElseThrow(() -> new RestApiException(ErrorCode.APPLICATION_NOT_FOUND));
+        validateClubOwner(clubApplicationForm.getClubId(), user);
         updateQuestions(clubApplicationForm, request);
         clubApplicationForm.updateEditedAt();
         clubApplicationFormsRepository.save(clubApplicationForm);
     }

Comment on lines +317 to 327
public void deleteApplicant(String clubId, String applicationFormId, ClubApplicantDeleteRequest request, CustomUserDetails user) {
validateClubOwner(clubId, user);

List<ClubApplication> applicants = clubApplicationRepository.findAllByIdInAndQuestionId(request.applicantIds(), clubId);
List<ClubApplicant> applicants = clubApplicantsRepository.findAllByIdInAndFormId(request.applicantIds(), applicationFormId);

if (applicants.size() != request.applicantIds().size()) {
throw new RestApiException(ErrorCode.APPLICANT_NOT_FOUND);
}

clubApplicationRepository.deleteAll(applicants);
clubApplicantsRepository.deleteAll(applicants);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

권한 우회 취약점: 타 클럽 양식의 지원자 삭제 가능

위와 동일하게 applicationFormId의 소속 클럽 검증이 필요합니다.

수정:

     public void deleteApplicant(String clubId, String applicationFormId, ClubApplicantDeleteRequest request, CustomUserDetails user) {
         validateClubOwner(clubId, user);
+        clubApplicationFormsRepository.findByClubIdAndId(clubId, applicationFormId)
+                .orElseThrow(() -> new RestApiException(ErrorCode.APPLICATION_NOT_FOUND));
         List<ClubApplicant> applicants = clubApplicantsRepository.findAllByIdInAndFormId(request.applicantIds(), applicationFormId);
🤖 Prompt for AI Agents
In backend/src/main/java/moadong/club/service/ClubApplyService.java around lines
317-327, the method deletes applicants using applicationFormId without verifying
that the form belongs to the given club, allowing deletion across clubs; fix by
loading the ApplicationForm (or equivalent) for applicationFormId and assert its
clubId matches the provided clubId (throw APPLICANT_NOT_FOUND or a suitable
error/forbidden code if not), or use a repository query that joins/form-checks
club ownership (e.g., find form by id and clubId) before fetching/deleting
applicants so only applicants for forms owned by the club can be removed.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants