Skip to content

✨ Feat: MatchStatus 확장 및 Admin 매칭 취소 API 추가#61

Closed
angoroa wants to merge 1 commit intodevelopfrom
feat/admin-match-cancel
Closed

✨ Feat: MatchStatus 확장 및 Admin 매칭 취소 API 추가#61
angoroa wants to merge 1 commit intodevelopfrom
feat/admin-match-cancel

Conversation

@angoroa
Copy link
Copy Markdown
Member

@angoroa angoroa commented Mar 2, 2026

#️⃣ Issue Number

  • closed #

📝 요약(Summary)

  • MatchStatus enum에 CLOSED, COMPLETED, CANCELLED 추가
  • MatchRoomcancel() 도메인 메서드 추가
  • Admin 전용 매칭 취소 API 구현 (PATCH /api/admin/matches/{matchId}/cancel)
    • 이미 CANCELLED / CLOSED / COMPLETED 상태인 매칭은 취소 불가 (MATCH_CANNOT_CANCEL)

🛠️ PR 유형

어떤 변경 사항이 있나요?

  • 새로운 기능 추가
  • 버그 수정
  • CSS 등 사용자 UI 디자인 변경
  • 코드에 영향을 주지 않는 변경사항(오타 수정, 탭 사이즈 변경, 변수명 변경)
  • 코드 리팩토링
  • 주석 추가 및 수정
  • 문서 수정
  • 테스트 추가, 테스트 리팩토링
  • 빌드 부분 혹은 패키지 매니저 수정
  • 파일 혹은 폴더명 수정
  • 파일 혹은 폴더 삭제

📸스크린샷 (선택)

💬 공유사항 to 리뷰어

✅ PR Checklist

PR이 다음 요구 사항을 충족하는지 확인하세요.

  • 커밋 메시지 컨벤션에 맞게 작성했습니다.
  • 변경 사항에 대한 테스트를 했습니다.(버그 수정/기능에 대한 테스트).

Summary by CodeRabbit

새로운 기능

  • 관리자용 매칭 취소 기능이 추가되었습니다.
  • 매칭의 상태 표현이 확장되어 완료됨과 취소됨이라는 새로운 상태가 추가되었습니다.
  • 진행 중이거나 이미 종료된 매칭의 취소 시도는 오류로 처리됩니다.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 2, 2026

개요

새로운 매치 취소 기능을 관리자 계층과 매치 도메인에 추가합니다. 관리자용 엔드포인트, 서비스 메서드, 도메인 로직, 및 상태 검증을 도입하여 매치 생명주기 관리를 확장합니다.

변경 사항

코호트 / 파일(들) 요약
관리자 API 계층
src/main/java/com/be/sportizebe/domain/admin/controller/AdminController.java, src/main/java/com/be/sportizebe/domain/admin/service/AdminService.java, src/main/java/com/be/sportizebe/domain/admin/service/AdminServiceImpl.java
관리자 전용 매치 취소 엔드포인트 추가. PATCH /matches/{matchId}/cancel 엔드포인트를 통해 취소 작업 위임.
매치 도메인 모델
src/main/java/com/be/sportizebe/domain/match/entity/MatchRoom.java, src/main/java/com/be/sportizebe/domain/match/entity/MatchStatus.java
MatchRoom에 cancel() 메서드 추가. MatchStatus 열거형에 COMPLETED, CANCELLED 상태 신규 추가.
매치 서비스 및 예외 처리
src/main/java/com/be/sportizebe/domain/match/service/MatchService.java, src/main/java/com/be/sportizebe/domain/match/service/MatchServiceImpl.java, src/main/java/com/be/sportizebe/domain/match/exception/MatchErrorCode.java
MatchService에 cancelMatch 메서드 추가. 상태 검증 로직 포함 (CANCELLED, CLOSED, COMPLETED 상태 확인). MATCH_CANNOT_CANCEL 예외 코드 신규 추가.

시퀀스 다이어그램

sequenceDiagram
    participant Admin as Admin User
    participant Controller as AdminController
    participant AdminSvc as AdminService
    participant MatchSvc as MatchService
    participant MatchRepo as MatchRepository
    participant MatchRoom as MatchRoom Entity

    Admin->>Controller: PATCH /matches/{matchId}/cancel
    Controller->>AdminSvc: cancelMatch(matchId)
    AdminSvc->>MatchSvc: cancelMatch(matchId)
    MatchSvc->>MatchRepo: findById(matchId)
    MatchRepo-->>MatchSvc: MatchRoom
    alt MatchRoom not found
        MatchSvc-->>MatchSvc: throw MATCH_NOT_FOUND
    else Status is terminal
        MatchSvc->>MatchSvc: check if CANCELLED/CLOSED/COMPLETED
        MatchSvc-->>MatchSvc: throw MATCH_CANNOT_CANCEL
    else Status is valid
        MatchSvc->>MatchRoom: cancel()
        MatchRoom->>MatchRoom: setStatus(CANCELLED)
        MatchRoom-->>MatchSvc: success
    end
    MatchSvc-->>AdminSvc: void
    AdminSvc-->>Controller: void
    Controller-->>Admin: 200 OK {success message}
Loading

예상 코드 리뷰 소요 시간

🎯 2 (Simple) | ⏱️ ~12분

관련된 가능성 있는 PR

  • Feat/match #58: 매치 도메인의 MatchRoom 엔드포인트 추가 (join/leave 작업) 및 서비스 레벨 동작을 다루며, 본 PR과 매치 생명주기 관리 측면에서 연관성 있음.

🐰 취소의 춤을 춘단다, 매치는 멈추고
CANCELLED 상태로 평온해지며
관리자의 손길이 종료의 길을 열때,
MatchRoom은 조용히 인사를 건넨다네

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.29% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목은 변경 사항의 주요 내용인 MatchStatus 확장과 Admin 매칭 취소 API 추가를 명확하게 요약하고 있습니다.

✏️ 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 feat/admin-match-cancel

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.

Copy link
Copy Markdown

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

🤖 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/com/be/sportizebe/domain/match/entity/MatchStatus.java`:
- Line 7: The javadoc/comment for the MatchStatus enum value COMPLETED contains
a broken/garbled fragment ("...필요 시COM"); open the MatchStatus enum and fix the
comment for COMPLETED to a clear, complete sentence (e.g., "매칭 종료 (scheduledAt +
durationMinutes 이후) — 매칭 완료 후 리뷰/평점 처리 및 포인트 지급 등의 사이드 이펙트 처리"), removing the
stray characters and ensuring the meaning is unambiguous.

In `@src/main/java/com/be/sportizebe/domain/match/service/MatchServiceImpl.java`:
- Around line 130-142: The cancelMatch method can suffer TOCTOU races because it
reads with matchRoomRepository.findById then checks status before cancelling;
add a pessimistic write lock to serialize concurrent cancels by adding a
repository method (e.g. findByIdForUpdate) annotated with
`@Lock`(LockModeType.PESSIMISTIC_WRITE) and use that method inside cancelMatch to
load the MatchRoom (instead of findById), then perform the same status checks
and call matchRoom.cancel(); keep the same exception flows (CustomException with
MatchErrorCode) so behavior is unchanged but the read-modify is now atomic.

ℹ️ Review info

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 49c2144 and b848a8a.

📒 Files selected for processing (8)
  • src/main/java/com/be/sportizebe/domain/admin/controller/AdminController.java
  • src/main/java/com/be/sportizebe/domain/admin/service/AdminService.java
  • src/main/java/com/be/sportizebe/domain/admin/service/AdminServiceImpl.java
  • src/main/java/com/be/sportizebe/domain/match/entity/MatchRoom.java
  • src/main/java/com/be/sportizebe/domain/match/entity/MatchStatus.java
  • src/main/java/com/be/sportizebe/domain/match/exception/MatchErrorCode.java
  • src/main/java/com/be/sportizebe/domain/match/service/MatchService.java
  • src/main/java/com/be/sportizebe/domain/match/service/MatchServiceImpl.java

OPEN, // 참여 가능
FULL, // 정원 마감
CLOSED, // 매칭 시작됨 (scheduledAt 이후, 참여 불가) -> "지금 진행중인 매칭" 탭 보여줄 때
COMPLETED, // 매칭 종료 (scheduledAt + durationMinutes 이후) -> "매칭 완료 후 리뷰/평점. 포인트 지금 등 사이드 이펙트 필요 시COM
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

주석 문구가 중간에서 깨져 의미 전달이 어렵습니다.

Line 7의 ...필요 시COM은 오탈자/잔여 문자열로 보이며, 상태 정의 주석은 배포 전 정리하는 것이 좋겠습니다.

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

In `@src/main/java/com/be/sportizebe/domain/match/entity/MatchStatus.java` at line
7, The javadoc/comment for the MatchStatus enum value COMPLETED contains a
broken/garbled fragment ("...필요 시COM"); open the MatchStatus enum and fix the
comment for COMPLETED to a clear, complete sentence (e.g., "매칭 종료 (scheduledAt +
durationMinutes 이후) — 매칭 완료 후 리뷰/평점 처리 및 포인트 지급 등의 사이드 이펙트 처리"), removing the
stray characters and ensuring the meaning is unambiguous.

Comment on lines +130 to +142
@Override
public void cancelMatch(Long matchId) {
MatchRoom matchRoom = matchRoomRepository.findById(matchId)
.orElseThrow(() -> new CustomException(MatchErrorCode.MATCH_NOT_FOUND));

if (matchRoom.getStatus() == MatchStatus.CANCELLED
|| matchRoom.getStatus() == MatchStatus.CLOSED
|| matchRoom.getStatus() == MatchStatus.COMPLETED) {
throw new CustomException(MatchErrorCode.MATCH_CANNOT_CANCEL);
}

matchRoom.cancel();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, locate the MatchRoom entity and MatchRoomRepository files
find src/main/java/com/be/sportizebe -name "MatchRoom.java" -o -name "MatchRoomRepository.java"

Repository: Sportize/be

Length of output: 202


🏁 Script executed:

# Check for `@Version`, `@Lock`, and locking-related annotations in the match domain
rg -n "@Version|@Lock|LockModeType|pessimistic|optimistic" src/main/java/com/be/sportizebe/domain/match --type=java

Repository: Sportize/be

Length of output: 37


🏁 Script executed:

# Find the MatchRoomRepository and examine its current methods
fd "MatchRoomRepository" src/main/java --type=java

Repository: Sportize/be

Length of output: 226


🏁 Script executed:

# Check the MatchRoom entity for version control
fd "MatchRoom\.java" src/main/java --type=java | head -5

Repository: Sportize/be

Length of output: 226


🏁 Script executed:

# Look for existing usage of locking patterns in similar cancel/update operations
rg -n "findByIdForUpdate|PESSIMISTIC_WRITE" src/main/java/com/be/sportizebe --type=java

Repository: Sportize/be

Length of output: 37


🏁 Script executed:

# Read the MatchRoom entity to check for `@Version` and other annotations
cat -n src/main/java/com/be/sportizebe/domain/match/entity/MatchRoom.java | head -100

Repository: Sportize/be

Length of output: 2955


🏁 Script executed:

# Read the MatchRoomRepository to check current methods
cat -n src/main/java/com/be/sportizebe/domain/match/repository/MatchRoomRepository.java

Repository: Sportize/be

Length of output: 2113


🏁 Script executed:

# Search for locking annotations/imports across match domain
rg -n "@Version|@Lock|LockModeType" src/main/java/com/be/sportizebe/domain/match

Repository: Sportize/be

Length of output: 37


🏁 Script executed:

# Check for pessimistic/optimistic locking patterns
rg -n "findByIdForUpdate|PESSIMISTIC_WRITE|OPTIMISTIC" src/main/java/com/be/sportizebe/domain

Repository: Sportize/be

Length of output: 37


🏁 Script executed:

# Read the full MatchServiceImpl to understand the context better
cat -n src/main/java/com/be/sportizebe/domain/match/service/MatchServiceImpl.java | sed -n '120,150p'

Repository: Sportize/be

Length of output: 1559


🏁 Script executed:

# Check if cancelMatch method has `@Transactional` or other synchronization annotations
cat -n src/main/java/com/be/sportizebe/domain/match/service/MatchServiceImpl.java | head -50

Repository: Sportize/be

Length of output: 2596


🏁 Script executed:

# Check class-level annotations on MatchServiceImpl
rg -A 10 "class MatchServiceImpl" src/main/java/com/be/sportizebe/domain/match/service/MatchServiceImpl.java

Repository: Sportize/be

Length of output: 445


🏁 Script executed:

# Check if there are any other concurrent cancel/update patterns in the codebase that use locking
rg -B 2 "cancel\(\)" src/main/java/com/be/sportizebe/domain --context=3

Repository: Sportize/be

Length of output: 1349


동시 취소 요청 시 상태 체크와 변경이 원자적이지 않아 중복 성공이 발생할 수 있습니다.

@Transactional만으로는 애플리케이션 계층의 TOCTOU 경합 조건을 방지하지 못합니다. 현재 cancelMatch() 메서드는:

  1. findById()로 조회
  2. 상태 조건 검사 (라인 135-138)
  3. cancel() 호출로 변경

이 경우 동시 요청이 모두 상태 검사를 통과한 후 실행될 수 있으므로, 두 번째 요청도 성공하게 됩니다. MatchRoom 엔티티에 @Version이 없고 MatchRoomRepository에 락 메서드가 없는 상태이므로, 비관적 락(PESSIMISTIC_WRITE)을 추가하여 원자성을 보장해야 합니다.

🔧 제안 수정안
-        MatchRoom matchRoom = matchRoomRepository.findById(matchId)
+        MatchRoom matchRoom = matchRoomRepository.findByIdForUpdate(matchId)
                 .orElseThrow(() -> new CustomException(MatchErrorCode.MATCH_NOT_FOUND));
// MatchRoomRepository.java (추가)
`@Lock`(LockModeType.PESSIMISTIC_WRITE)
`@Query`("select m from MatchRoom m where m.id = :matchId")
Optional<MatchRoom> findByIdForUpdate(`@Param`("matchId") Long matchId);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/be/sportizebe/domain/match/service/MatchServiceImpl.java`
around lines 130 - 142, The cancelMatch method can suffer TOCTOU races because
it reads with matchRoomRepository.findById then checks status before cancelling;
add a pessimistic write lock to serialize concurrent cancels by adding a
repository method (e.g. findByIdForUpdate) annotated with
`@Lock`(LockModeType.PESSIMISTIC_WRITE) and use that method inside cancelMatch to
load the MatchRoom (instead of findById), then perform the same status checks
and call matchRoom.cancel(); keep the same exception flows (CustomException with
MatchErrorCode) so behavior is unchanged but the read-modify is now atomic.

@angoroa angoroa closed this Mar 2, 2026
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