Skip to content

[FEAT] 모임 참여와 취소 기능 구현#131

Merged
LimdaeIl merged 2 commits intomainfrom
feat/group-attend-left
Dec 18, 2025
Merged

[FEAT] 모임 참여와 취소 기능 구현#131
LimdaeIl merged 2 commits intomainfrom
feat/group-attend-left

Conversation

@LimdaeIl
Copy link
Collaborator

@LimdaeIl LimdaeIl commented Dec 18, 2025

📝 Pull Request

📌 PR 종류

해당하는 항목에 체크해주세요.

  • 기능 추가 (Feature)
  • 버그 수정 (Fix)
  • 문서 수정 (Docs)
  • 코드 리팩터링 (Refactor)
  • 테스트 추가 (Test)
  • 기타 변경 (Chore)

✨ 변경 내용

비회원, HOST 를 고려해서 모임 참석, 취소를 구현했습니다.

🔍 관련 이슈

🧪 테스트

변경된 기능에 대한 테스트 범위 또는 테스트 결과를 작성해주세요.

  • 유닛 테스트 추가 / 수정
  • 통합 테스트 검증
  • 수동 테스트 완료

🚨 확인해야 할 사항 (Checklist)

PR을 제출하기 전에 아래 항목들을 확인해주세요.

  • 코드 포매팅 완료
  • 불필요한 파일/코드 제거
  • 로직 검증 완료
  • 프로젝트 빌드 성공
  • 린트/정적 분석 통과 (해당 시)

🙋 기타 참고 사항

리뷰어가 참고하면 좋을 만한 추가 설명이 있다면 적어주세요.

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 그룹 참가 및 탈퇴 기능 추가
    • 참가 상태, 참가자 수, 최대 인원 등 상세 정보를 포함한 새로운 응답 형식 제공
    • 자동 그룹 상태 관리(모집 중/정원 초과 상태 전환)
  • 개선 사항

    • 오류 처리 코드 표준화 및 명확화
    • 호스트 권한 검증 강화
    • 그룹 참가 조건 검증 개선

✏️ Tip: You can customize this high-level summary in your review settings.

@LimdaeIl LimdaeIl self-assigned this Dec 18, 2025
Copilot AI review requested due to automatic review settings December 18, 2025 01:31
@LimdaeIl LimdaeIl added the ✨enhancement New feature or request label Dec 18, 2025
@coderabbitai
Copy link

coderabbitai bot commented Dec 18, 2025

Walkthrough

그룹 참여 및 취소 기능이 V2 API에 추가되었습니다. 에러 코드 이름이 일관성 있게 변경되었고, 새로운 DTO와 서비스 메서드들이 구현되었으며, 저장소 쿼리 메서드가 확장되었습니다. 컨트롤러에 두 개의 새 엔드포인트(/attend, /left)가 추가되었습니다.

Changes

코호트 / 파일 변경 요약
에러 코드 정의
src/main/java/team/wego/wegobackend/group/domain/exception/GroupErrorCode.java
4개의 에러 코드 이름 변경 (MEMBER_NOT_FOUNDGROUP_USER_NOT_FOUND, GROUP_CAPACITY_EXCEEDEDGROUP_IS_FULL, HOST_CANNOT_LEAVE_OWN_GROUPGROUP_HOST_CANNOT_LEAVE). 4개의 새로운 에러 코드 추가 (USER_ID_NULL, GROUP_HOST_CANNOT_ATTEND, GROUP_NOT_RECRUITING, GROUP_NOT_ATTEND_STATUS). 메시지 현지화 및 조정.
에러 처리 업데이트
src/main/java/team/wego/wegobackend/group/application/service/v1/GroupService.java
변경된 에러 코드를 사용하도록 예외 처리 업데이트.
V2 공통 DTO
src/main/java/team/wego/wegobackend/group/v2/application/dto/common/MyMembership.java
신규 레코드. 사용자 멤버십 정보 (isJoined, groupUserId, role, status, joinedAt, leftAt) 포함. from() 정적 팩토리 메서드 제공.
V2 응답 DTO
src/main/java/team/wego/wegobackend/group/v2/application/dto/response/AttendGroupV2Response.java
신규 레코드. 그룹 참여 응답 정보 포함. of() 정적 팩토리 메서드 제공.
V2 응답 DTO 정리
src/main/java/team/wego/wegobackend/group/v2/application/dto/response/GetGroupV2Response.java
내부 MyMembership 클래스 제거. 외부 MyMembership import로 변경.
저장소 확장
src/main/java/team/wego/wegobackend/group/v2/domain/repository/GroupUserV2Repository.java
2개의 새로운 쿼리 메서드 추가: findByGroupIdAndUserId(), countByGroupIdAndStatus().
서비스 핵심 구현
src/main/java/team/wego/wegobackend/group/v2/application/service/GroupV2Service.java
attend(Long userId, Long groupId)left(Long userId, Long groupId) 메서드 추가. create() 메서드 시그니처 변경 (CustomUserDetails → userId). 멤버십 검증, 상태 전환, 용량 관리 로직 포함.
컨트롤러 엔드포인트
src/main/java/team/wego/wegobackend/group/v2/presentation/GroupV2Controller.java
POST /api/v2/groups/{groupId}/attendPOST /api/v2/groups/{groupId}/left 엔드포인트 추가. createGroup() 메서드 수정 (userId 파라미터 사용).
HTTP 테스트 시나리오
src/test/http/group/v2/v2-group-attend.http, V2-group-cancel.http, v2-group-get.http
참여, 취소 시나리오 및 엣지 케이스 테스트 추가. 동적 groupId 변수 사용으로 테스트 개선.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Controller as GroupV2Controller
    participant Service as GroupV2Service
    participant Repository as GroupUserV2Repository
    participant DB as Database
    
    rect rgb(200, 220, 240)
    Note over Client,DB: 그룹 참여 흐름 (attend)
    Client->>Controller: POST /api/v2/groups/{groupId}/attend
    activate Controller
    Controller->>Service: attend(userId, groupId)
    activate Service
    
    Service->>Service: userId null 검증
    Service->>DB: findGroupById(groupId)
    activate DB
    DB-->>Service: GroupV2 entity
    deactivate DB
    
    Service->>Service: 그룹 존재 확인
    Service->>Service: 호스트 제한 확인
    Service->>Service: 모집 상태 확인
    
    Service->>Repository: findByGroupIdAndUserId(groupId, userId)
    activate Repository
    Repository->>DB: query
    activate DB
    DB-->>Repository: Optional<GroupUserV2>
    deactivate DB
    deactivate Repository
    
    alt 기존 멤버십 있음
        Service->>Service: 상태 확인 (ATTEND 시 에러)
    else 새로운 참여
        Service->>Repository: save(new GroupUserV2)
        activate Repository
        Repository->>DB: insert
        deactivate Repository
    end
    
    Service->>Repository: countByGroupIdAndStatus(groupId, ATTEND)
    activate Repository
    Repository->>DB: count query
    DB-->>Repository: participantCount
    deactivate Repository
    
    Service->>Service: 용량 확인 및 상태 전환 (RECRUITING→FULL)
    Service->>Service: MyMembership 구성
    Service-->>Controller: AttendGroupV2Response
    deactivate Service
    
    Controller-->>Client: 200 OK + Response
    deactivate Controller
    end
    
    rect rgb(240, 200, 220)
    Note over Client,DB: 그룹 취소 흐름 (left)
    Client->>Controller: POST /api/v2/groups/{groupId}/left
    activate Controller
    Controller->>Service: left(userId, groupId)
    activate Service
    
    Service->>Service: userId null 검증
    Service->>DB: findGroupById(groupId)
    DB-->>Service: GroupV2 entity
    
    Service->>Service: 호스트 제한 확인
    Service->>Repository: findByGroupIdAndUserId(groupId, userId)
    Repository->>DB: query
    DB-->>Repository: Optional<GroupUserV2>
    
    alt ATTEND 상태 확인
        Service->>Service: 상태가 ATTEND인지 검증
        Service->>Repository: 상태 변경 (ATTEND→LEFT)
        Service->>Repository: countByGroupIdAndStatus(groupId, ATTEND)
        Repository->>DB: count query
        DB-->>Repository: updatedCount
        
        Service->>Service: 용량 확인 (FULL→RECRUITING 전환)
    else ATTEND 아님
        Service->>Service: 에러 반환
    end
    
    Service-->>Controller: AttendGroupV2Response
    deactivate Service
    
    Controller-->>Client: 200 OK + Response
    deactivate Controller
    end
Loading

Estimated code review effort

🎯 3 (보통) | ⏱️ ~20-25 분

추가 주의 사항:

  • GroupV2Service.attend()left() 메서드의 멤버십 검증 및 상태 전환 로직 검증 필요
  • 용량 제한(GROUP_IS_FULL) 및 상태 자동 전환 (RECRUITINGFULL) 로직 동작 확인
  • MyMembership DTO의 from() 정적 팩토리 메서드가 모든 null 케이스를 올바르게 처리하는지 확인
  • 에러 코드 이름 변경으로 인한 기존 V1 API 및 다른 서비스의 호환성 확인
  • HTTP 테스트의 엣지 케이스 (호스트의 자신의 그룹 참여, 중복 참여 시도 등) 실제 동작 확인

Possibly related PRs

Poem

🐰 모임에 쏘옥 들어가고, 떠나갈 때도 스르르~
참여와 취소, 에러 코드 정리하며 춤을 춘다네!
V2 API, 더욱 매끈해지니 정말 좋군요,
상태 전환은 자동이고, 검증도 촘촘하니
한 발 두 발, 모임 기능이 완성되었습니다! 🎉

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Out of Scope Changes check ❓ Inconclusive 에러 코드 리팩토링(GroupErrorCode 상수 이름 변경 및 추가)이 이슈 #130의 범위와 명시적 연관이 불명확합니다. 에러 코드 변경사항이 필수인지 또는 별도 이슈의 일부인지 확인이 필요합니다. 필요시 이를 별도 PR로 분리하거나 관련 이슈를 연결해주세요.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목 '[FEAT] 모임 참여와 취소 기능 구현'은 변경사항의 주요 내용인 모임 참여 및 취소 기능 구현을 명확하게 요약하고 있습니다.
Linked Issues check ✅ Passed 변경사항이 연결된 이슈 #130의 요구사항을 충족합니다. 모임 참석 기능(attend)과 모임 참석 취소 기능(left)이 GroupV2Service와 GroupV2Controller에 모두 구현되었습니다.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/group-attend-left

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.

@LimdaeIl LimdaeIl moved this from Backlog to In progress in WeGo-Together Backend Dec 18, 2025
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: 3

🧹 Nitpick comments (2)
src/main/java/team/wego/wegobackend/group/v2/application/service/GroupV2Service.java (1)

211-218: getReferenceById 사용 시 사용자 존재 여부 미검증

getReferenceById는 DB 조회 없이 프록시 객체를 생성합니다. 해당 userId가 실제로 존재하지 않는 경우, 저장 시점에 외래 키 제약 조건 위반이 발생할 수 있습니다.

인증된 사용자만 이 API를 호출한다는 가정이 있다면 괜찮을 수 있으나, 명시적으로 검증하는 것이 더 안전합니다.

🔎 명시적 검증 추가
         } else {
             // 최초 참석 생성
-            groupUserV2 = GroupUserV2.create(group, userRepository.getReferenceById(userId),
+            User user = userRepository.findById(userId)
+                    .orElseThrow(() -> new GroupException(GroupErrorCode.GROUP_USER_NOT_FOUND, userId));
+            groupUserV2 = GroupUserV2.create(group, user,
                     GroupUserV2Role.MEMBER);
src/main/java/team/wego/wegobackend/group/v2/application/dto/common/MyMembership.java (1)

5-5: 사용되지 않는 import 제거 필요

GetGroupV2Response가 이 파일에서 사용되지 않습니다.

🔎 수정 제안
-import team.wego.wegobackend.group.v2.application.dto.response.GetGroupV2Response;
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8bd38df and 89d0fd8.

📒 Files selected for processing (11)
  • src/main/java/team/wego/wegobackend/group/application/service/v1/GroupService.java (3 hunks)
  • src/main/java/team/wego/wegobackend/group/domain/exception/GroupErrorCode.java (3 hunks)
  • src/main/java/team/wego/wegobackend/group/v2/application/dto/common/MyMembership.java (1 hunks)
  • src/main/java/team/wego/wegobackend/group/v2/application/dto/response/AttendGroupV2Response.java (1 hunks)
  • src/main/java/team/wego/wegobackend/group/v2/application/dto/response/GetGroupV2Response.java (1 hunks)
  • src/main/java/team/wego/wegobackend/group/v2/application/service/GroupV2Service.java (4 hunks)
  • src/main/java/team/wego/wegobackend/group/v2/domain/repository/GroupUserV2Repository.java (2 hunks)
  • src/main/java/team/wego/wegobackend/group/v2/presentation/GroupV2Controller.java (3 hunks)
  • src/test/http/group/v2/V2-group-cancel.http (1 hunks)
  • src/test/http/group/v2/v2-group-attend.http (1 hunks)
  • src/test/http/group/v2/v2-group-get.http (1 hunks)
🔇 Additional comments (14)
src/test/http/group/v2/v2-group-get.http (1)

94-94: 동적 groupId 사용으로 테스트 유연성 향상

하드코딩된 ID 대신 동적 변수를 사용하여 테스트 시나리오가 더 유연해졌습니다.

src/main/java/team/wego/wegobackend/group/application/service/v1/GroupService.java (3)

210-210: 에러 코드 네이밍 개선

GROUP_USER_NOT_FOUND로 변경하여 에러 코드가 더 명확하고 일관성 있게 되었습니다.


222-222: 에러 코드 네이밍 개선

GROUP_IS_FULL로 변경하여 그룹 정원 초과 상황을 더 직관적으로 표현합니다.


280-280: 에러 코드 네이밍 개선

GROUP_HOST_CANNOT_LEAVE로 변경하여 호스트가 자신의 그룹을 떠날 수 없다는 제약을 명확히 표현합니다.

src/test/http/group/v2/v2-group-attend.http (1)

1-146: 포괄적인 참여 기능 테스트 시나리오

성공 케이스와 예외 케이스(중복 참여, 호스트 자체 참여)를 모두 커버하는 잘 구성된 테스트입니다. 참여 상태 확인까지 포함되어 있어 기능 검증이 철저합니다.

src/main/java/team/wego/wegobackend/group/v2/presentation/GroupV2Controller.java (2)

37-37: userId 직접 전달로 서비스 레이어 결합도 감소

CustomUserDetails 객체 대신 userId만 전달하여 서비스 레이어의 보안 관련 의존성을 제거했습니다. 깔끔한 레이어 분리입니다.


57-82: 모임 참여/취소 엔드포인트 구현 완료

/attend/left 엔드포인트가 RESTful하게 구현되었습니다. 빈 요청 본문을 받는 것은 의도된 설계로, 인증된 사용자와 경로 변수만으로 충분한 정보를 제공합니다.

src/main/java/team/wego/wegobackend/group/v2/application/dto/response/AttendGroupV2Response.java (1)

1-31: 참여/취소 응답 DTO 잘 설계됨

AttendGroupV2Response가 참여/취소 작업 후 필요한 모든 정보(그룹 상태, 참가자 수, 내 멤버십 정보, 서버 시간)를 포함하고 있습니다. 공통 MyMembership 타입을 재사용하여 일관성도 확보했습니다.

src/test/http/group/v2/V2-group-cancel.http (1)

1-186: 포괄적인 취소 기능 테스트 시나리오

성공 케이스(멤버 취소)와 예외 케이스(중복 취소, 호스트 취소 시도)를 모두 검증하는 체계적인 테스트입니다. v2-group-attend.http와 함께 참여/취소 기능의 전체 흐름을 커버합니다.

src/main/java/team/wego/wegobackend/group/v2/domain/repository/GroupUserV2Repository.java (1)

21-23: 새로운 저장소 메서드는 정상 작동합니다

findByGroupIdAndUserIdcountByGroupIdAndStatus 메서드의 필드 매핑이 올바르게 구현되었습니다. GroupUserV2 엔티티가 group과 user 관계를 @manytoone으로 정의하고 있으며, groupId와 userId 직접 필드는 없어 Spring Data JPA가 자동으로 group.id와 user.id로 해석합니다. 기존 JPQL 쿼리(line 17)에서 사용하는 gu.group.id 패턴과 동일하게 동작하므로 별도의 수정이 필요하지 않습니다.

src/main/java/team/wego/wegobackend/group/v2/application/service/GroupV2Service.java (2)

178-195: 참여 전 검증 로직 LGTM

userId null 체크, 그룹 존재 여부, 호스트 참여 제한, 모집 상태 검증 등 필수 검증이 적절히 구현되어 있습니다.


241-278: 탈퇴 로직 구현 LGTM

호스트 탈퇴 제한, 멤버십 존재 확인, ATTEND 상태에서만 LEFT 전이 허용, 그리고 FULLRECRUITING 자동 복귀 로직이 잘 구현되어 있습니다.

src/main/java/team/wego/wegobackend/group/v2/application/dto/common/MyMembership.java (1)

10-32: Record 구조 및 팩토리 메서드 LGTM

MyMembership 레코드가 깔끔하게 정의되어 있고, from() 팩토리 메서드가 스트림을 활용하여 사용자 멤버십을 적절히 매핑합니다. 비회원인 경우의 기본값 처리도 적절합니다.

src/main/java/team/wego/wegobackend/group/domain/exception/GroupErrorCode.java (1)

43-45: 새 에러 코드 추가 LGTM

GROUP_HOST_CANNOT_ATTEND, GROUP_NOT_RECRUITING, GROUP_NOT_ATTEND_STATUS 에러 코드가 참여/탈퇴 플로우에 맞게 적절히 정의되어 있습니다. 메시지가 명확하고 HTTP 상태 코드도 적절합니다.

GROUP_STATUS_REQUIRED(HttpStatus.BAD_REQUEST, "모임: 모임 상태는 필수입니다."),
GROUP_STATUS_TRANSFER_IMPOSSIBLE(HttpStatus.BAD_REQUEST, "모임: 상태 전이가 불가능합니다. 현재 상태: %s, 요청한 상태: %s");
GROUP_STATUS_TRANSFER_IMPOSSIBLE(HttpStatus.BAD_REQUEST, "모임: 상태 전이가 불가능합니다. 현재 상태: %s, 요청한 상태: %s"),
USER_ID_NULL(HttpStatus.NOT_FOUND, "모임: 회원 ID가 null 입니다."),
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

USER_ID_NULL HTTP 상태 코드 부적절

userIdnull인 경우는 잘못된 입력(클라이언트 오류)이므로 NOT_FOUND보다 BAD_REQUEST가 더 적절합니다. NOT_FOUND는 리소스가 존재하지 않을 때 사용하는 것이 RESTful 컨벤션입니다.

🔎 수정 제안
-    USER_ID_NULL(HttpStatus.NOT_FOUND, "모임: 회원 ID가 null 입니다."),
+    USER_ID_NULL(HttpStatus.BAD_REQUEST, "모임: 회원 ID가 null 입니다."),
📝 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
USER_ID_NULL(HttpStatus.NOT_FOUND, "모임: 회원 ID가 null 입니다."),
USER_ID_NULL(HttpStatus.BAD_REQUEST, "모임: 회원 ID가 null 입니다."),
🤖 Prompt for AI Agents
In
src/main/java/team/wego/wegobackend/group/domain/exception/GroupErrorCode.java
around line 42, the enum constant USER_ID_NULL currently uses
HttpStatus.NOT_FOUND but should use HttpStatus.BAD_REQUEST since a null userId
is a client input error; update the enum entry to use HttpStatus.BAD_REQUEST
instead of HttpStatus.NOT_FOUND so the API returns 400 for this validation
error.

import team.wego.wegobackend.group.v2.application.dto.common.Address;
import team.wego.wegobackend.group.v2.application.dto.common.CreatedBy;
import team.wego.wegobackend.group.v2.application.dto.common.GroupImageItem;
import team.wego.wegobackend.group.v2.application.dto.common.MyMembership;
Copy link

@coderabbitai coderabbitai bot Dec 18, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

MyMembership 외부 클래스로 추출하여 재사용성 향상

MyMembership을 공통 패키지로 분리하여 AttendGroupV2Response 등 다른 DTO에서도 재사용할 수 있게 되었습니다. 코드 중복을 줄이고 일관성을 높이는 좋은 리팩토링입니다.

🤖 Prompt for AI Agents
In
src/main/java/team/wego/wegobackend/group/v2/application/dto/response/GetGroupV2Response.java
around line 8, update the class to use the extracted common MyMembership: ensure
the import points to
team.wego.wegobackend.group.v2.application.dto.common.MyMembership (or the
actual new common package path) and remove any local/duplicate MyMembership
class usages; then replace local definitions in other DTOs (e.g.,
AttendGroupV2Response) to reference this common MyMembership to eliminate
duplication and keep imports consistent.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

좋은것도 칭칭찬하려고 멘트 달아주네

Copy link

Choose a reason for hiding this comment

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

@LimdaeIl 네! 좋은 코드 개선 사항도 함께 공유하는 것이 중요하다고 생각합니다. 😊 코드 리뷰가 단순히 문제점만 지적하는 것이 아니라, 잘된 부분도 인정하고 팀 전체가 좋은 패턴을 배울 수 있도록 돕는 것이 목표입니다. 이번 MyMembership 리팩토링은 재사용성을 고려한 좋은 설계 결정이었습니다! 👍

Comment on lines +220 to +226
// 5) 정원 체크 수행. 재참여 포함해서 체크하는 게 안전
long attendCount = groupUserV2Repository.countByGroupIdAndStatus(groupId,
GroupUserV2Status.ATTEND);
if (attendCount > group.getMaxParticipants()) {
// 방금 reAttend로 늘었는데 초과하면 롤백시키기 위해 예외
throw new GroupException(GroupErrorCode.GROUP_IS_FULL, groupId);
}
Copy link

@coderabbitai coderabbitai bot Dec 18, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

정원 체크 타이밍 문제: 동시성 이슈 가능성

현재 정원 체크가 reAttend() 또는 save() 이후에 수행됩니다. 동시에 여러 사용자가 참여 요청을 보내면, 모두가 정원 체크를 통과한 뒤 예외가 발생하기 전까지 초과 참여가 허용될 수 있습니다.

더 안전한 방식은 참여 처리 전에 SELECT ... FOR UPDATE 또는 비관적 락을 사용하여 정원을 먼저 확인하는 것입니다.

🔎 개선 제안
+    // 정원 체크를 먼저 수행
+    long currentAttendCount = groupUserV2Repository.countByGroupIdAndStatus(groupId,
+            GroupUserV2Status.ATTEND);
+    if (currentAttendCount >= group.getMaxParticipants()) {
+        throw new GroupException(GroupErrorCode.GROUP_IS_FULL, groupId);
+    }
+
     // 기존 멤버십 조회
     GroupUserV2 groupUserV2 = groupUserV2Repository.findByGroupIdAndUserId(groupId, userId)
             .orElse(null);

락을 사용한 조회 메서드 추가도 고려해보세요:

@Lock(LockModeType.PESSIMISTIC_WRITE)
Optional<GroupV2> findByIdWithLock(Long groupId);

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
src/main/java/team/wego/wegobackend/group/v2/application/service/GroupV2Service.java
around lines 220 to 226, the current capacity check runs after calling
reAttend()/save() which can allow race conditions; change the flow to acquire a
pessimistic write lock on the Group row before modifying membership, perform the
attendCount check while holding the lock, then call reAttend()/save() only if
the check passes. Add a repository method (e.g., findByIdWithLock(Long id)
annotated with @Lock(LockModeType.PESSIMISTIC_WRITE)), ensure the service method
executes in a transactional context, use that locked entity to re-check capacity
(including pending re-attendance) and then persist—this prevents concurrent
overbooking.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

이거 나중에 V3 로 나누거나 V2에서 찢을 생각이야.

Copy link

Choose a reason for hiding this comment

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

@LimdaeIl, 향후 리팩토링 계획을 이해했습니다! 동시성 이슈를 V3이나 V2 리팩토링 시 처리하시겠다는 거군요.

이 내용을 추적하기 위해 GitHub 이슈를 생성해드릴까요? 이슈에 현재 PR과 이 대화 링크를 포함하여 나중에 참고하기 편하도록 할 수 있습니다.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This pull request implements group attendance and cancellation functionality for V2 API endpoints, with proper handling for non-members and HOST users. The implementation includes business logic for joining and leaving groups, capacity management, and automatic status transitions between RECRUITING and FULL states.

Key Changes:

  • Added two new endpoints: /api/v2/groups/{groupId}/attend and /api/v2/groups/{groupId}/left for joining and leaving groups
  • Implemented business logic with validations for HOST restrictions, status checks, capacity management, and re-attendance scenarios
  • Refactored MyMembership into a common DTO and updated error codes for consistency across V1 and V2

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/test/http/group/v2/v2-group-get.http Updated test to use parameterized groupId
src/test/http/group/v2/v2-group-attend.http Added comprehensive HTTP test scenarios for group attendance functionality
src/test/http/group/v2/V2-group-cancel.http Added comprehensive HTTP test scenarios for group cancellation functionality
src/main/java/team/wego/wegobackend/group/v2/presentation/GroupV2Controller.java Added attend and left endpoints with proper authentication and parameter handling
src/main/java/team/wego/wegobackend/group/v2/domain/repository/GroupUserV2Repository.java Added query methods for finding group users and counting attendees by status
src/main/java/team/wego/wegobackend/group/v2/application/service/GroupV2Service.java Implemented attend and left business logic with validations, capacity checks, and status transitions
src/main/java/team/wego/wegobackend/group/v2/application/dto/response/GetGroupV2Response.java Moved MyMembership record to common package for reusability
src/main/java/team/wego/wegobackend/group/v2/application/dto/response/AttendGroupV2Response.java Created response DTO for attendance operations with group status and participant count
src/main/java/team/wego/wegobackend/group/v2/application/dto/common/MyMembership.java Extracted MyMembership as a common DTO shared across multiple responses
src/main/java/team/wego/wegobackend/group/domain/exception/GroupErrorCode.java Added new error codes and renamed existing ones for better clarity and V2 support
src/main/java/team/wego/wegobackend/group/application/service/v1/GroupService.java Updated to use renamed error codes for consistency

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

Comment on lines +193 to +195
if (group.getStatus() != GroupV2Status.RECRUITING) {
throw new GroupException(GroupErrorCode.GROUP_NOT_RECRUITING, group.getStatus().name());
}
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

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

The attendance validation only checks if the group status is RECRUITING, but does not prevent attendance when the group status is FULL. This could allow users to join a full group if they were previously LEFT or KICKED. The validation should also reject attendance for FULL status groups, or alternatively, allow attendance to FULL groups with capacity checks.

Copilot uses AI. Check for mistakes.
import org.springframework.web.bind.annotation.RestController;
import team.wego.wegobackend.common.response.ApiResponse;
import team.wego.wegobackend.common.security.CustomUserDetails;
import team.wego.wegobackend.group.application.dto.v1.response.GetGroupResponse;
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

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

The import team.wego.wegobackend.group.application.dto.v1.response.GetGroupResponse is not used in this file. This import should be removed to keep the code clean.

Suggested change
import team.wego.wegobackend.group.application.dto.v1.response.GetGroupResponse;

Copilot uses AI. Check for mistakes.

import java.time.LocalDateTime;
import java.util.List;
import team.wego.wegobackend.group.v2.application.dto.response.GetGroupV2Response;
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

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

The import team.wego.wegobackend.group.v2.application.dto.response.GetGroupV2Response is not used in this file. This import should be removed to keep the code clean.

Suggested change
import team.wego.wegobackend.group.v2.application.dto.response.GetGroupV2Response;

Copilot uses AI. Check for mistakes.
Comment on lines +220 to +226
// 5) 정원 체크 수행. 재참여 포함해서 체크하는 게 안전
long attendCount = groupUserV2Repository.countByGroupIdAndStatus(groupId,
GroupUserV2Status.ATTEND);
if (attendCount > group.getMaxParticipants()) {
// 방금 reAttend로 늘었는데 초과하면 롤백시키기 위해 예외
throw new GroupException(GroupErrorCode.GROUP_IS_FULL, groupId);
}
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

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

There is a potential race condition between checking capacity and saving the attendance. If two users attempt to join simultaneously when there is only one spot left, both could pass the capacity check and exceed maxParticipants. Consider using pessimistic locking on the group entity or implementing a database-level constraint to prevent this race condition.

Copilot uses AI. Check for mistakes.
Comment on lines +178 to +239
@Transactional
public AttendGroupV2Response attend(Long userId, Long groupId) {
if (userId == null) {
throw new GroupException(GroupErrorCode.USER_ID_NULL);
}

GroupV2 group = groupV2Repository.findById(groupId)
.orElseThrow(
() -> new GroupException(GroupErrorCode.GROUP_NOT_FOUND_BY_ID, groupId));

if (group.getHost().getId().equals(userId)) {
throw new GroupException(GroupErrorCode.GROUP_HOST_CANNOT_ATTEND);
}

// 모임 상태 체크
if (group.getStatus() != GroupV2Status.RECRUITING) {
throw new GroupException(GroupErrorCode.GROUP_NOT_RECRUITING, group.getStatus().name());
}

// 기존 멤버십 조회
GroupUserV2 groupUserV2 = groupUserV2Repository.findByGroupIdAndUserId(groupId, userId)
.orElse(null);

if (groupUserV2 != null) {
if (groupUserV2.getStatus() == GroupUserV2Status.BANNED) {
throw new GroupException(GroupErrorCode.GROUP_BANNED_USER);
}
// 이미 참석중이면 충돌
if (groupUserV2.getStatus() == GroupUserV2Status.ATTEND) {
throw new GroupException(GroupErrorCode.ALREADY_ATTEND_GROUP, groupId, userId);
}
// LEFT/KICKED면 재참여 허용
groupUserV2.reAttend();
} else {
// 최초 참석 생성
groupUserV2 = GroupUserV2.create(group, userRepository.getReferenceById(userId),
GroupUserV2Role.MEMBER);
// create에서 group.addUser로 연관관계 맞추는 구조.
// group은 영속 상태여야 한다. (위에서 findById 했으니 ok)
groupUserV2Repository.save(groupUserV2);
}

// 5) 정원 체크 수행. 재참여 포함해서 체크하는 게 안전
long attendCount = groupUserV2Repository.countByGroupIdAndStatus(groupId,
GroupUserV2Status.ATTEND);
if (attendCount > group.getMaxParticipants()) {
// 방금 reAttend로 늘었는데 초과하면 롤백시키기 위해 예외
throw new GroupException(GroupErrorCode.GROUP_IS_FULL, groupId);
}

// FULL 자동 전환
if (attendCount == group.getMaxParticipants()
&& group.getStatus() == GroupV2Status.RECRUITING) {
group.changeStatus(GroupV2Status.FULL);
}

// 내 멤버십 + 최신 카운트 + 모임 상태 응답
MyMembership membership =
MyMembership.from(List.of(groupUserV2), userId);

return AttendGroupV2Response.of(group, attendCount, membership);
}
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

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

The new attend() method lacks unit test coverage. Given that v1 GroupService has corresponding test coverage, consider adding unit tests for this method to verify business logic including edge cases like capacity checks, status validations, and concurrent access scenarios.

Copilot uses AI. Check for mistakes.
Comment on lines +241 to +278
@Transactional
public AttendGroupV2Response left(Long userId, Long groupId) {
if (userId == null) {
throw new GroupException(GroupErrorCode.USER_ID_NULL);
}

GroupV2 group = groupV2Repository.findById(groupId)
.orElseThrow(
() -> new GroupException(GroupErrorCode.GROUP_NOT_FOUND_BY_ID, groupId));

if (group.getHost().getId().equals(userId)) {
throw new GroupException(GroupErrorCode.GROUP_HOST_CANNOT_LEAVE, groupId, userId);
}

GroupUserV2 groupUserV2 = groupUserV2Repository.findByGroupIdAndUserId(groupId, userId)
.orElseThrow(() -> new GroupException(GroupErrorCode.GROUP_USER_NOT_FOUND, userId));

// ATTEND만 LEFT 가능
if (groupUserV2.getStatus() != GroupUserV2Status.ATTEND) {
throw new GroupException(GroupErrorCode.GROUP_NOT_ATTEND_STATUS);
}

groupUserV2.leave();

long attendCount = groupUserV2Repository.countByGroupIdAndStatus(groupId,
GroupUserV2Status.ATTEND);

// FULL -> RECRUITING 자동 복귀(선택)
if (group.getStatus() == GroupV2Status.FULL && attendCount < group.getMaxParticipants()) {
group.changeStatus(GroupV2Status.RECRUITING);
}

// 응답
MyMembership membership =
MyMembership.from(List.of(groupUserV2), userId);

return AttendGroupV2Response.of(group, attendCount, membership);
}
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

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

The new left() method lacks unit test coverage. Given that v1 GroupService has corresponding test coverage, consider adding unit tests for this method to verify business logic including edge cases like HOST restrictions, status validations, and automatic status transitions.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨enhancement New feature or request

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

[FEAT] 모임 참여와 취소 기능 구현

1 participant