Skip to content

fix: 지원서 상태 전이 및 문항 이력 보존#333

Open
dh2906 wants to merge 11 commits intodevelopfrom
fix/club-apply-status-transition-and-question-soft-delete
Open

fix: 지원서 상태 전이 및 문항 이력 보존#333
dh2906 wants to merge 11 commits intodevelopfrom
fix/club-apply-status-transition-and-question-soft-delete

Conversation

@dh2906
Copy link
Contributor

@dh2906 dh2906 commented Feb 28, 2026

🔍 개요

  • 기존 구조에서는 승인/거절 이후 재지원 시 유니크 충돌 위험이 있었고,

  • 관리자가 문항을 변경/삭제하면 과거 지원 답변 조회에서 질문 매핑 정합성이 깨질 여지가 있었습니다.

  • 이를 해결하기 위해 상태 전이 + 시점 기반 문항 조회로 모델을 명확화했습니다.

  • close #이슈번호

🚀 주요 변경 내용

  • club_apply 테이블의 status, state 컬럼을 추가했습니다.

    • status는 동아리 지원의 진행 과정인 PENDING/APPROVED/REJECTED을 나타냅니다.
    • 같은 유저가 같은 동아리에 여러 번 지원하는 케이스도 존재하므로 (club_id, user_id, status) 를 유니크로 걸면 승인과 거절에 대한 데이터가 1개만 존재해야 하는 문제가 있습니다.
    • 그러므로 (club_id, user_id, state)를 유니크로 걸고 statePENDING 상태에서는 1, 그 이외의 상태라면 null로 지정하여 이 문제를 해결했습니다.
  • 가입 문항(ClubApplyQuestion)에 소프트 딜리트(deleted_at)를 도입해 문항 이력을 보존하도록 변경했습니다.

  • 승인 멤버 답변 조회를 “현재 문항”이 아니라 “지원 시점에 유효한 문항” 기준으로 조회하도록 변경했습니다.

  • 문항 PUT 교체 시 하드 삭제/인플레이스 수정 대신 soft-delete + 신규 생성으로 이력 정합성을 유지합니다.


💬 참고 사항


✅ Checklist (완료 조건)

  • 코드 스타일 가이드 준수
  • 테스트 코드 포함됨
  • Reviewers / Assignees / Labels 지정 완료
  • 보안 및 민감 정보 검증 (API 키, 환경 변수, 개인정보 등)

@dh2906 dh2906 requested a review from Copilot February 28, 2026 11:31
@dh2906 dh2906 self-assigned this Feb 28, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 28, 2026

📝 Walkthrough

Walkthrough

클럽 가입 신청에 상태 필드(ClubApplyStatus)를 도입하고, 질문에 displayOrder·deletedAt 기반 소프트 삭제 및 상태 기반(PENDING/APPROVED/REJECTED) 조회·승인/거절 흐름을 추가했습니다. 관련 리포지토리·서비스·엔티티·마이그레이션이 업데이트되었습니다.

Changes

Cohort / File(s) Summary
Status enum & Apply model
src/main/java/gg/agit/konect/domain/club/enums/ClubApplyStatus.java, src/main/java/gg/agit/konect/domain/club/model/ClubApply.java
ClubApplyStatus(PENDING, APPROVED, REJECTED) 추가; ClubApply.status 필드·매핑·기본값(PENDING) 추가; approve()/reject() 메서드 추가; 테이블 유니크 제약에 state 포함.
Apply question model & repo
src/main/java/gg/agit/konect/domain/club/model/ClubApplyQuestion.java, src/main/java/gg/agit/konect/domain/club/repository/ClubApplyQuestionRepository.java
displayOrder(NOT NULL)·deletedAt 추가; 빌더/팩토리/생성자에 displayOrder 반영; softDelete()·updateDisplayOrder() 등 메서드 추가; JPQL 조회(가시성·displayOrder 정렬) 도입; 기존 하드 삭제 메서드 제거.
Repository 쿼리 변경들
src/main/java/gg/agit/konect/domain/club/repository/.../ClubApplyQueryRepository.java, src/main/java/gg/agit/konect/domain/club/repository/ClubApplyRepository.java, src/main/java/gg/agit/konect/domain/club/repository/ClubApplyAnswerRepository.java
여러 조회에 상태 필터(PENDING/APPROVED) 추가; 최신 승인된 신청만 선택하는 서브쿼리 도입; 일부 메서드 제거/추가(삭제 제거, status-aware 조회/존재체크, 최신 APPROVED 반환 디폴트 메서드); 응답 정렬을 question.displayOrder 우선으로 변경.
Service 로직 변경
src/main/java/gg/agit/konect/domain/club/service/...
신청 승인/거절을 삭제 대신 상태 변경으로 전환(approve/reject 호출); 가입 생성·검증에서 PENDING 기반 존재 검사 사용; 질문 교체 로직을 soft-delete 누적 방식으로 리팩터링; 질문 조회를 displayOrder 기준으로 사용.
DB Migration
src/main/resources/db/migration/V42__add_soft_delete_to_club_apply_and_question.sql
club_apply.status 컬럼(기본 'PENDING') 및 계산된 state 열 추가, 기존 멤버 기준 APPROVED 마이그레이션 및 unique 제약 재생성; club_apply_question.display_order·deleted_at 추가 및 데이터 채우기.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Service as ClubApplicationService
    participant Repo as ClubApplyRepository
    participant Model as ClubApply
    participant DB as Database

    rect rgba(100,150,200,0.5)
    Note over Client,DB: 신청 승인 흐름
    Client->>Service: approveClubApplication(clubId, userId)
    Service->>Repo: findFirstByClubIdAndUserIdAndStatus(... PENDING ...)
    Repo-->>Service: ClubApply (PENDING)
    Service->>Model: approve()
    Model->>Model: status = APPROVED
    Service->>Repo: save(ClubApply)
    Repo->>DB: UPDATE club_apply SET status='APPROVED'
    DB-->>Repo: OK
    end
Loading
sequenceDiagram
    participant Service as ClubApplicationService
    participant RepoQ as ClubApplyQuestionRepository
    participant ModelQ as ClubApplyQuestion
    participant DB as Database

    rect rgba(150,200,100,0.5)
    Note over Service,DB: 질문 교체(soft-delete) 흐름
    Service->>RepoQ: findAllByClubIdOrderByDisplayOrderAsc(clubId)
    RepoQ->>DB: SELECT ... WHERE deleted_at IS NULL ORDER BY display_order,id
    DB-->>RepoQ: List<ClubApplyQuestion>
    Service->>ModelQ: model.softDelete(now) (for removed ones)
    ModelQ->>RepoQ: saveAll(updatedQuestions)
    RepoQ->>DB: UPDATE club_apply_question SET deleted_at=?
    DB-->>RepoQ: OK
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

버그

Poem

🐰 상태 한 알 쏙 집어먹고,
PENDING에서 APPROVED로 깡충 뛰네.
질문들은 번호표 달고 줄 서고,
지울 땐 살며시 시간만 남기네.
당근 한 입, 코드도 포근히 쉬어라 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 변경의 핵심을 명확히 설명하고 있습니다. 상태 전이(status transition)와 문항 이력 보존(question history preservation)이 주요 변경사항이며, 제목이 이를 정확히 반영합니다.
Description check ✅ Passed PR 설명이 변경사항과 관련이 있으며, 문제 상황, 해결 방법, 주요 변경 내용을 상세히 설명하고 있습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/club-apply-status-transition-and-question-soft-delete

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

@dh2906 dh2906 changed the title Fix/club apply status transition and question soft delete fix: 지원서 상태 전이 및 문항 이력 보존 Feb 28, 2026
Copy link

@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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/java/gg/agit/konect/domain/club/service/ClubApplicationService.java (1)

151-184: ⚠️ Potential issue | 🔴 Critical

[LEVEL: high] 승인/거절 상태 전이가 동시 요청에서 원자적으로 보장되지 않습니다.
문제는 approveClubApplication/rejectClubApplicationPENDING 조회 후 엔티티 상태를 직접 변경해 경쟁 트랜잭션에서 마지막 커밋이 상태를 덮어쓸 수 있다는 점입니다. 같은 신청건에 승인과 거절이 거의 동시에 들어오면 멤버 추가·승인 이벤트가 실행된 뒤 최종 statusREJECTED로 저장되는 불일치가 재현될 수 있습니다. WHERE id = :id AND status = PENDING 조건부 전이(영향 행 수 1건 검증) 또는 @Version 기반 낙관적 락으로 상태 전이를 단일 성공으로 제한해 주세요; As per coding guidelines, "보안, 트랜잭션 경계, 예외 처리, N+1, 성능 회귀 가능성을 우선 점검한다."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/gg/agit/konect/domain/club/service/ClubApplicationService.java`
around lines 151 - 184, approveClubApplication/rejectClubApplication mutate
ClubApply state after reading it, allowing race conditions; change to perform a
conditional state transition (e.g., UPDATE ClubApply SET status = :APPROVED ...
WHERE id = :id AND status = :PENDING) via clubApplyRepository (expose a
repository method like approveIfPending(id) / rejectIfPending(id) that returns
affected row count) and verify the update affected exactly 1 row (throw a
concurrency-specific exception otherwise), or alternatively add `@Version` to
ClubApply and rely on optimistic locking by saving and handling
OptimisticLockException; ensure these changes are used in the methods (replace
direct clubApply.approve()/reject() + save semantics with the conditional update
or versioned save) while preserving the surrounding `@Transactional` boundary and
event/publication only after a successful single-row transition.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@src/main/java/gg/agit/konect/domain/club/repository/ClubApplyQueryRepository.java`:
- Around line 207-219: The method isLatestApprovedApplicationByUser uses
latestApply.id.max() to determine "latest" which can diverge from chronological
order; update the subquery in isLatestApprovedApplicationByUser to pick the row
with the greatest createdAt (and use id as a tiebreaker) instead of max(id) —
e.g., change the subquery to select the apply with max(createdAt) (or
alternatively select one row using ORDER BY createdAt DESC, id DESC and limit 1)
while keeping the same filters (latestApply.club.id.eq(clubId),
latestApply.status.eq(ClubApplyStatus.APPROVED),
latestApply.user.id.eq(clubApply.user.id)).

---

Outside diff comments:
In
`@src/main/java/gg/agit/konect/domain/club/service/ClubApplicationService.java`:
- Around line 151-184: approveClubApplication/rejectClubApplication mutate
ClubApply state after reading it, allowing race conditions; change to perform a
conditional state transition (e.g., UPDATE ClubApply SET status = :APPROVED ...
WHERE id = :id AND status = :PENDING) via clubApplyRepository (expose a
repository method like approveIfPending(id) / rejectIfPending(id) that returns
affected row count) and verify the update affected exactly 1 row (throw a
concurrency-specific exception otherwise), or alternatively add `@Version` to
ClubApply and rely on optimistic locking by saving and handling
OptimisticLockException; ensure these changes are used in the methods (replace
direct clubApply.approve()/reject() + save semantics with the conditional update
or versioned save) while preserving the surrounding `@Transactional` boundary and
event/publication only after a successful single-row transition.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9168d00 and 947a09b.

📒 Files selected for processing (8)
  • src/main/java/gg/agit/konect/domain/club/enums/ClubApplyStatus.java
  • src/main/java/gg/agit/konect/domain/club/model/ClubApply.java
  • src/main/java/gg/agit/konect/domain/club/model/ClubApplyQuestion.java
  • src/main/java/gg/agit/konect/domain/club/repository/ClubApplyQueryRepository.java
  • src/main/java/gg/agit/konect/domain/club/repository/ClubApplyQuestionRepository.java
  • src/main/java/gg/agit/konect/domain/club/repository/ClubApplyRepository.java
  • src/main/java/gg/agit/konect/domain/club/service/ClubApplicationService.java
  • src/main/resources/db/migration/V42__add_soft_delete_to_club_apply_and_question.sql
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Agent
🧰 Additional context used
📓 Path-based instructions (3)
src/main/resources/db/migration/**/*.sql

⚙️ CodeRabbit configuration file

src/main/resources/db/migration/**/*.sql: Flyway 마이그레이션 리뷰 규칙:

  • 버전 파일명 규칙(V{number}__{description}.sql) 위반 여부를 우선 확인한다.
  • 이미 배포된 마이그레이션 수정/재번호 부여 위험이 있으면 반드시 차단 코멘트를 남긴다.
  • 파괴적 변경(drop, rename 등)은 롤백 가능성과 운영 영향 관점에서 검토한다.

Files:

  • src/main/resources/db/migration/V42__add_soft_delete_to_club_apply_and_question.sql
**/*

⚙️ CodeRabbit configuration file

**/*: 공통 리뷰 톤 가이드:

  • 모든 코멘트는 첫 줄에 [LEVEL: ...] 태그를 포함한다.
  • 과장된 표현 없이 사실 기반으로 작성한다.
  • 한 코멘트에는 하나의 이슈만 다룬다.
  • 코드 예시가 필요하면 최소 수정 예시를 제시한다.
  • 가독성/단순화/확장성 이슈를 발견하면 우선순위를 높여 코멘트한다.

Files:

  • src/main/resources/db/migration/V42__add_soft_delete_to_club_apply_and_question.sql
  • src/main/java/gg/agit/konect/domain/club/enums/ClubApplyStatus.java
  • src/main/java/gg/agit/konect/domain/club/service/ClubApplicationService.java
  • src/main/java/gg/agit/konect/domain/club/repository/ClubApplyQueryRepository.java
  • src/main/java/gg/agit/konect/domain/club/repository/ClubApplyRepository.java
  • src/main/java/gg/agit/konect/domain/club/repository/ClubApplyQuestionRepository.java
  • src/main/java/gg/agit/konect/domain/club/model/ClubApply.java
  • src/main/java/gg/agit/konect/domain/club/model/ClubApplyQuestion.java
src/main/java/**/*.java

⚙️ CodeRabbit configuration file

src/main/java/**/*.java: 아래 원칙으로 리뷰 코멘트를 작성한다.

  • 코멘트는 반드시 한국어로 작성한다.
  • 반드시 수정이 필요한 항목만 코멘트로 남기고, 단순 취향 차이는 지적하지 않는다.
  • 각 코멘트 첫 줄에 심각도를 [LEVEL: high|medium|low] 형식으로 반드시 표기한다.
  • 심각도 기준: high=운영 장애 가능, medium=품질 저하, low=개선 권고.
  • 각 코멘트는 "문제 -> 영향 -> 제안" 순서로 3문장 이내로 간결하게 작성한다.
  • 가능하면 재현 조건 및 실패 시나리오도 포함한다.
  • 제안은 현재 코드베이스(Spring Boot + JPA + Flyway) 패턴과 일치해야 한다.
  • 보안, 트랜잭션 경계, 예외 처리, N+1, 성능 회귀 가능성을 우선 점검한다.
  • 가독성: 변수/메서드 이름이 의도를 바로 드러내는지, 중첩과 메서드 길이가 과도하지 않은지 점검한다.
  • 단순화: 불필요한 추상화, 중복 로직, 과한 방어 코드가 있으면 더 단순한 대안을 제시한다.
  • 확장성: 새 요구사항 추가 시 변경 범위가 최소화되는 구조인지(하드코딩 분기/값 여부 포함) 점검한다.

Files:

  • src/main/java/gg/agit/konect/domain/club/enums/ClubApplyStatus.java
  • src/main/java/gg/agit/konect/domain/club/service/ClubApplicationService.java
  • src/main/java/gg/agit/konect/domain/club/repository/ClubApplyQueryRepository.java
  • src/main/java/gg/agit/konect/domain/club/repository/ClubApplyRepository.java
  • src/main/java/gg/agit/konect/domain/club/repository/ClubApplyQuestionRepository.java
  • src/main/java/gg/agit/konect/domain/club/model/ClubApply.java
  • src/main/java/gg/agit/konect/domain/club/model/ClubApplyQuestion.java
🧬 Code graph analysis (1)
src/main/java/gg/agit/konect/domain/club/model/ClubApply.java (2)
src/main/java/gg/agit/konect/domain/club/model/ClubRecruitment.java (1)
  • Entity (28-146)
src/main/java/gg/agit/konect/domain/university/model/University.java (1)
  • Entity (20-53)

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

동아리 가입 신청(ClubApply)을 삭제 기반에서 상태 전이(PENDING/APPROVED/REJECTED) 기반으로 전환하고, 가입 문항(ClubApplyQuestion)에 소프트 딜리트를 도입해 “지원 시점 기준”으로 문항/답변 정합성을 유지하도록 개선하는 PR입니다.

Changes:

  • ClubApply에 status를 추가하고 승인/거절 시 삭제 대신 상태 전이로 처리
  • ClubApplyQuestion에 deleted_at(soft delete) 도입 및 문항 교체 시 soft-delete + 신규 생성 방식으로 이력 보존
  • 답변 조회 시 “현재 문항”이 아닌 “지원 시점에 유효한 문항”을 조회하도록 쿼리 변경

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/main/resources/db/migration/V42__add_soft_delete_to_club_apply_and_question.sql club_apply에 status/state(유니크 보조) 추가, club_apply_question에 deleted_at 추가
src/main/java/gg/agit/konect/domain/club/service/ClubApplicationService.java 승인/거절을 상태 전이로 변경, 답변 조회 시점 기반 문항 조회로 변경, 문항 교체 로직을 soft-delete + 신규 생성으로 변경
src/main/java/gg/agit/konect/domain/club/repository/ClubApplyRepository.java PENDING 중심 조회/존재 확인으로 변경, 최신 승인 지원서 조회 추가
src/main/java/gg/agit/konect/domain/club/repository/ClubApplyQuestionRepository.java soft-delete 반영 조회 및 “지원 시점 유효 문항” 조회 쿼리 추가
src/main/java/gg/agit/konect/domain/club/repository/ClubApplyQueryRepository.java 지원서 목록/승인 멤버 목록 조회에서 status 조건(PENDING/APPROVED) 반영
src/main/java/gg/agit/konect/domain/club/model/ClubApplyQuestion.java deletedAt 및 softDelete/isSame 유틸 추가
src/main/java/gg/agit/konect/domain/club/model/ClubApply.java status 필드/전이 메서드(approve/reject) 추가, 유니크 제약(state)으로 변경
src/main/java/gg/agit/konect/domain/club/enums/ClubApplyStatus.java 지원 상태 enum 신설

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

Copy link

@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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/main/java/gg/agit/konect/domain/club/service/ClubApplicationService.java (2)

151-183: ⚠️ Potential issue | 🔴 Critical

[LEVEL: high] 승인/거절 상태 전이에 동시성 제어가 없습니다.
문제: Line [151]~Line [183]에서 동일 신청 건을 조회 후 처리할 때 락/버전 검증이 없어 두 관리자가 동시에 approve/reject를 실행하면 둘 다 진행될 수 있습니다.
영향: 한 트랜잭션은 멤버 생성을 완료하고 다른 트랜잭션은 거절 알림을 발송하는 식으로 상태와 부수효과가 불일치하는 운영 장애가 발생합니다(동일 applicationId에 동시 요청으로 재현 가능).
제안: 승인/거절 조회를 PESSIMISTIC_WRITE로 고정하거나 WHERE status = PENDING 조건부 갱신 후 영향 행 수를 검증해 단일 전이만 허용해 주세요; As per coding guidelines, '보안, 트랜잭션 경계, 예외 처리, N+1, 성능 회귀 가능성을 우선 점검한다.'

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/gg/agit/konect/domain/club/service/ClubApplicationService.java`
around lines 151 - 183, Concurrent transitions on the same ClubApply (seen where
clubApplyRepository.getByIdAndClubId, clubApply.approve(), and
clubApply.reject() are called in the approve and reject flows) are not
protected, so add concurrency control: either load the application with a
PESSIMISTIC_WRITE lock (replace getByIdAndClubId with a locking query or use
EntityManager.lock) or perform a conditional update that includes "WHERE status
= PENDING" and verify the affected row count to ensure only one transition
succeeds; after the conditional update, throw a conflict/duplicate-transition
exception if no row was updated. Ensure the fix is applied in both the approve
and reject flows and that side effects (ClubMember save,
chatRoomMembershipService.addClubMember, and event publish) only run after the
successful guarded state transition.

201-213: ⚠️ Potential issue | 🟠 Major

[LEVEL: medium] 중복 신청 경쟁 조건에서 500 에러가 노출될 수 있습니다.
문제: Line [201]~Line [213]은 existsPendingByClubIdAndUserId 확인 후 save를 수행하는 TOCTOU 구조라 동일 사용자/동아리로 동시 신청 시 둘 다 검증을 통과할 수 있습니다.
영향: DB 유니크 제약 충돌이 커밋 시점 DataIntegrityViolationException으로 터지고 비즈니스 예외로 변환되지 않으면 간헐적 500 응답이 발생합니다(더블클릭/재시도 트래픽에서 재현 가능).
제안: 저장 시점 예외를 ALREADY_APPLIED_CLUB로 매핑할 수 있도록 flush 지점에서 처리하거나, 사용자-동아리 키 단위 락으로 신청 생성 구간을 직렬화해 주세요; As per coding guidelines, '보안, 트랜잭션 경계, 예외 처리, N+1, 성능 회귀 가능성을 우선 점검한다.'

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/gg/agit/konect/domain/club/service/ClubApplicationService.java`
around lines 201 - 213, The existsPendingByClubIdAndUserId pre-check can race
and cause a DB unique-constraint failure on clubApplyRepository.save (TOCTOU);
wrap the save/flush in a try/catch that catches DataIntegrityViolationException
and rethrow CustomException.of(ALREADY_APPLIED_CLUB) so DB uniqueness errors map
to the business error (e.g., try { ClubApply apply =
clubApplyRepository.save(ClubApply.of(...)); clubApplyRepository.flush(); }
catch (DataIntegrityViolationException e) { throw
CustomException.of(ALREADY_APPLIED_CLUB); }). Alternatively, serialize creation
by acquiring a lock on the user-club key before calling
existsPendingByClubIdAndUserId/save if you prefer pessimistic locking. Ensure
the change references existsPendingByClubIdAndUserId, clubApplyRepository.save,
clubApplyRepository.flush, ClubApply.of and ALREADY_APPLIED_CLUB.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@src/main/java/gg/agit/konect/domain/club/repository/ClubApplyQuestionRepository.java`:
- Around line 27-29: The JPQL/SQL filter in ClubApplyQuestionRepository excludes
questions deleted exactly at the application time because it uses
"question.deletedAt > :appliedAt"; change that predicate to "question.deletedAt
>= :appliedAt" (i.e., include equality) in the query string where the query
orders by question.displayOrder and question.id, and add a unit/integration test
that creates a question and an application with identical timestamps to assert
the question is still returned/mapped; update any repository method name
referencing this query accordingly so the behaviour is covered.

---

Outside diff comments:
In
`@src/main/java/gg/agit/konect/domain/club/service/ClubApplicationService.java`:
- Around line 151-183: Concurrent transitions on the same ClubApply (seen where
clubApplyRepository.getByIdAndClubId, clubApply.approve(), and
clubApply.reject() are called in the approve and reject flows) are not
protected, so add concurrency control: either load the application with a
PESSIMISTIC_WRITE lock (replace getByIdAndClubId with a locking query or use
EntityManager.lock) or perform a conditional update that includes "WHERE status
= PENDING" and verify the affected row count to ensure only one transition
succeeds; after the conditional update, throw a conflict/duplicate-transition
exception if no row was updated. Ensure the fix is applied in both the approve
and reject flows and that side effects (ClubMember save,
chatRoomMembershipService.addClubMember, and event publish) only run after the
successful guarded state transition.
- Around line 201-213: The existsPendingByClubIdAndUserId pre-check can race and
cause a DB unique-constraint failure on clubApplyRepository.save (TOCTOU); wrap
the save/flush in a try/catch that catches DataIntegrityViolationException and
rethrow CustomException.of(ALREADY_APPLIED_CLUB) so DB uniqueness errors map to
the business error (e.g., try { ClubApply apply =
clubApplyRepository.save(ClubApply.of(...)); clubApplyRepository.flush(); }
catch (DataIntegrityViolationException e) { throw
CustomException.of(ALREADY_APPLIED_CLUB); }). Alternatively, serialize creation
by acquiring a lock on the user-club key before calling
existsPendingByClubIdAndUserId/save if you prefer pessimistic locking. Ensure
the change references existsPendingByClubIdAndUserId, clubApplyRepository.save,
clubApplyRepository.flush, ClubApply.of and ALREADY_APPLIED_CLUB.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 947a09b and 60e4d81.

📒 Files selected for processing (10)
  • src/main/java/gg/agit/konect/domain/club/model/ClubApplyQuestion.java
  • src/main/java/gg/agit/konect/domain/club/repository/ClubApplyAnswerRepository.java
  • src/main/java/gg/agit/konect/domain/club/repository/ClubApplyQueryRepository.java
  • src/main/java/gg/agit/konect/domain/club/repository/ClubApplyQuestionRepository.java
  • src/main/java/gg/agit/konect/domain/club/repository/ClubApplyRepository.java
  • src/main/java/gg/agit/konect/domain/club/service/ClubApplicationService.java
  • src/main/java/gg/agit/konect/domain/club/service/ClubRecruitmentService.java
  • src/main/java/gg/agit/konect/domain/club/service/ClubService.java
  • src/main/java/gg/agit/konect/domain/club/service/ClubSettingsService.java
  • src/main/resources/db/migration/V42__add_soft_delete_to_club_apply_and_question.sql
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Agent
🧰 Additional context used
📓 Path-based instructions (3)
src/main/java/**/*.java

⚙️ CodeRabbit configuration file

src/main/java/**/*.java: 아래 원칙으로 리뷰 코멘트를 작성한다.

  • 코멘트는 반드시 한국어로 작성한다.
  • 반드시 수정이 필요한 항목만 코멘트로 남기고, 단순 취향 차이는 지적하지 않는다.
  • 각 코멘트 첫 줄에 심각도를 [LEVEL: high|medium|low] 형식으로 반드시 표기한다.
  • 심각도 기준: high=운영 장애 가능, medium=품질 저하, low=개선 권고.
  • 각 코멘트는 "문제 -> 영향 -> 제안" 순서로 3문장 이내로 간결하게 작성한다.
  • 가능하면 재현 조건 및 실패 시나리오도 포함한다.
  • 제안은 현재 코드베이스(Spring Boot + JPA + Flyway) 패턴과 일치해야 한다.
  • 보안, 트랜잭션 경계, 예외 처리, N+1, 성능 회귀 가능성을 우선 점검한다.
  • 가독성: 변수/메서드 이름이 의도를 바로 드러내는지, 중첩과 메서드 길이가 과도하지 않은지 점검한다.
  • 단순화: 불필요한 추상화, 중복 로직, 과한 방어 코드가 있으면 더 단순한 대안을 제시한다.
  • 확장성: 새 요구사항 추가 시 변경 범위가 최소화되는 구조인지(하드코딩 분기/값 여부 포함) 점검한다.

Files:

  • src/main/java/gg/agit/konect/domain/club/model/ClubApplyQuestion.java
  • src/main/java/gg/agit/konect/domain/club/repository/ClubApplyQuestionRepository.java
  • src/main/java/gg/agit/konect/domain/club/service/ClubSettingsService.java
  • src/main/java/gg/agit/konect/domain/club/repository/ClubApplyQueryRepository.java
  • src/main/java/gg/agit/konect/domain/club/service/ClubService.java
  • src/main/java/gg/agit/konect/domain/club/service/ClubApplicationService.java
  • src/main/java/gg/agit/konect/domain/club/service/ClubRecruitmentService.java
  • src/main/java/gg/agit/konect/domain/club/repository/ClubApplyAnswerRepository.java
  • src/main/java/gg/agit/konect/domain/club/repository/ClubApplyRepository.java
**/*

⚙️ CodeRabbit configuration file

**/*: 공통 리뷰 톤 가이드:

  • 모든 코멘트는 첫 줄에 [LEVEL: ...] 태그를 포함한다.
  • 과장된 표현 없이 사실 기반으로 작성한다.
  • 한 코멘트에는 하나의 이슈만 다룬다.
  • 코드 예시가 필요하면 최소 수정 예시를 제시한다.
  • 가독성/단순화/확장성 이슈를 발견하면 우선순위를 높여 코멘트한다.

Files:

  • src/main/java/gg/agit/konect/domain/club/model/ClubApplyQuestion.java
  • src/main/resources/db/migration/V42__add_soft_delete_to_club_apply_and_question.sql
  • src/main/java/gg/agit/konect/domain/club/repository/ClubApplyQuestionRepository.java
  • src/main/java/gg/agit/konect/domain/club/service/ClubSettingsService.java
  • src/main/java/gg/agit/konect/domain/club/repository/ClubApplyQueryRepository.java
  • src/main/java/gg/agit/konect/domain/club/service/ClubService.java
  • src/main/java/gg/agit/konect/domain/club/service/ClubApplicationService.java
  • src/main/java/gg/agit/konect/domain/club/service/ClubRecruitmentService.java
  • src/main/java/gg/agit/konect/domain/club/repository/ClubApplyAnswerRepository.java
  • src/main/java/gg/agit/konect/domain/club/repository/ClubApplyRepository.java
src/main/resources/db/migration/**/*.sql

⚙️ CodeRabbit configuration file

src/main/resources/db/migration/**/*.sql: Flyway 마이그레이션 리뷰 규칙:

  • 버전 파일명 규칙(V{number}__{description}.sql) 위반 여부를 우선 확인한다.
  • 이미 배포된 마이그레이션 수정/재번호 부여 위험이 있으면 반드시 차단 코멘트를 남긴다.
  • 파괴적 변경(drop, rename 등)은 롤백 가능성과 운영 영향 관점에서 검토한다.

Files:

  • src/main/resources/db/migration/V42__add_soft_delete_to_club_apply_and_question.sql

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 1 comment.

Comment on lines 27 to 33
@Table(
name = "club_apply",
uniqueConstraints = @UniqueConstraint(
name = "uq_club_apply_club_id_user_id",
columnNames = {"club_id", "user_id"}
name = "uq_club_apply_club_id_user_id_state",
columnNames = {"club_id", "user_id", "state"}
)
)
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

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

club_apply 테이블의 유니크 제약이 state 컬럼을 참조하도록 바뀌었는데, 엔티티에는 state 필드 매핑이 없습니다. 현재 테스트 프로필에서 spring.jpa.hibernate.ddl-auto=create-drop을 사용하고 있어, Hibernate가 스키마를 생성할 때 state 컬럼 없이 유니크 제약을 만들려다 DDL 오류가 발생할 가능성이 큽니다. state를 읽기 전용 컬럼(예: insertable=false, updatable=false)으로 엔티티에 매핑하거나, 테스트 환경에서는 Flyway로 스키마를 생성하도록 바꾸는 방식으로 해결해 주세요.

Copilot uses AI. Check for mistakes.
Copy link

@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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/java/gg/agit/konect/domain/club/model/ClubApply.java`:
- Around line 81-87: The approve() and reject() methods on ClubApply currently
change this.status unconditionally; update them to validate the current state
(use ClubApplyStatus, e.g., ensure status == ClubApplyStatus.PENDING) before
transitioning to APPROVED or REJECTED and throw an appropriate exception (e.g.,
IllegalStateException or a domain-specific exception) when the transition is
invalid; alternatively, if you intend to enforce this in the service layer, add
a comment documenting that responsibility and ensure service methods perform the
same check for approve/reject calls.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 60e4d81 and ca12ec5.

📒 Files selected for processing (1)
  • src/main/java/gg/agit/konect/domain/club/model/ClubApply.java
📜 Review details
🧰 Additional context used
📓 Path-based instructions (2)
src/main/java/**/*.java

⚙️ CodeRabbit configuration file

src/main/java/**/*.java: 아래 원칙으로 리뷰 코멘트를 작성한다.

  • 코멘트는 반드시 한국어로 작성한다.
  • 반드시 수정이 필요한 항목만 코멘트로 남기고, 단순 취향 차이는 지적하지 않는다.
  • 각 코멘트 첫 줄에 심각도를 [LEVEL: high|medium|low] 형식으로 반드시 표기한다.
  • 심각도 기준: high=운영 장애 가능, medium=품질 저하, low=개선 권고.
  • 각 코멘트는 "문제 -> 영향 -> 제안" 순서로 3문장 이내로 간결하게 작성한다.
  • 가능하면 재현 조건 및 실패 시나리오도 포함한다.
  • 제안은 현재 코드베이스(Spring Boot + JPA + Flyway) 패턴과 일치해야 한다.
  • 보안, 트랜잭션 경계, 예외 처리, N+1, 성능 회귀 가능성을 우선 점검한다.
  • 가독성: 변수/메서드 이름이 의도를 바로 드러내는지, 중첩과 메서드 길이가 과도하지 않은지 점검한다.
  • 단순화: 불필요한 추상화, 중복 로직, 과한 방어 코드가 있으면 더 단순한 대안을 제시한다.
  • 확장성: 새 요구사항 추가 시 변경 범위가 최소화되는 구조인지(하드코딩 분기/값 여부 포함) 점검한다.

Files:

  • src/main/java/gg/agit/konect/domain/club/model/ClubApply.java
**/*

⚙️ CodeRabbit configuration file

**/*: 공통 리뷰 톤 가이드:

  • 모든 코멘트는 첫 줄에 [LEVEL: ...] 태그를 포함한다.
  • 과장된 표현 없이 사실 기반으로 작성한다.
  • 한 코멘트에는 하나의 이슈만 다룬다.
  • 코드 예시가 필요하면 최소 수정 예시를 제시한다.
  • 가독성/단순화/확장성 이슈를 발견하면 우선순위를 높여 코멘트한다.

Files:

  • src/main/java/gg/agit/konect/domain/club/model/ClubApply.java

Comment on lines +81 to +87
public void approve() {
this.status = ClubApplyStatus.APPROVED;
}

public void reject() {
this.status = ClubApplyStatus.REJECTED;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

[LEVEL: medium] approve()/reject() 메서드에서 현재 상태 검증 누락

문제: 현재 상태가 PENDING인지 확인하지 않고 상태를 변경합니다.
영향: 이미 REJECTED된 지원서를 APPROVED로 변경하거나, APPROVED를 REJECTED로 변경하는 잘못된 상태 전이가 발생할 수 있습니다.
제안: 상태 전이 전 현재 상태 검증 로직을 추가하거나, 서비스 레이어에서 검증이 보장되는지 확인하세요.

제안 코드
 public void approve() {
+    if (this.status != ClubApplyStatus.PENDING) {
+        throw new IllegalStateException("PENDING 상태에서만 승인할 수 있습니다.");
+    }
     this.status = ClubApplyStatus.APPROVED;
 }

 public void reject() {
+    if (this.status != ClubApplyStatus.PENDING) {
+        throw new IllegalStateException("PENDING 상태에서만 거절할 수 있습니다.");
+    }
     this.status = ClubApplyStatus.REJECTED;
 }
📝 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 approve() {
this.status = ClubApplyStatus.APPROVED;
}
public void reject() {
this.status = ClubApplyStatus.REJECTED;
}
public void approve() {
if (this.status != ClubApplyStatus.PENDING) {
throw new IllegalStateException("PENDING 상태에서만 승인할 수 있습니다.");
}
this.status = ClubApplyStatus.APPROVED;
}
public void reject() {
if (this.status != ClubApplyStatus.PENDING) {
throw new IllegalStateException("PENDING 상태에서만 거절할 수 있습니다.");
}
this.status = ClubApplyStatus.REJECTED;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/gg/agit/konect/domain/club/model/ClubApply.java` around lines
81 - 87, The approve() and reject() methods on ClubApply currently change
this.status unconditionally; update them to validate the current state (use
ClubApplyStatus, e.g., ensure status == ClubApplyStatus.PENDING) before
transitioning to APPROVED or REJECTED and throw an appropriate exception (e.g.,
IllegalStateException or a domain-specific exception) when the transition is
invalid; alternatively, if you intend to enforce this in the service layer, add
a comment documenting that responsibility and ensure service methods perform the
same check for approve/reject calls.

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