Skip to content

Comments

[FEAT] 모임 상세 조회 구현#50

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

[FEAT] 모임 상세 조회 구현#50
LimdaeIl merged 2 commits intomainfrom
feat/attend-group

Conversation

@LimdaeIl
Copy link
Collaborator

@LimdaeIl LimdaeIl commented Dec 9, 2025

📝 Pull Request

📌 PR 종류

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

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

✨ 변경 내용

모임에 참석할 수 있는 기능을 구현했습니다.

모임 참가자 여부도 enum으로 ATTEND, LEFT로 남겨두었습니다. 따라서 최초 모임에 참가하게 된다면 이력이 남게 됩니다.

🔍 관련 이슈

🧪 테스트

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

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

🚨 확인해야 할 사항 (Checklist)

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

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

🙋 기타 참고 사항

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

Summary by CodeRabbit

Release Notes

  • New Features

    • 그룹에 참여하고 재참여할 수 있는 기능 추가
    • 새로운 엔드포인트: POST /api/v1/groups/{groupId}/attend로 그룹 참여 요청 가능
    • 그룹 참여자 수 제한 및 참여 상태 관리 개선
  • Bug Fixes

    • 그룹 조회 시 삭제되지 않은 그룹만 반환하도록 개선

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

@LimdaeIl LimdaeIl self-assigned this Dec 9, 2025
Copilot AI review requested due to automatic review settings December 9, 2025 13:45
@LimdaeIl LimdaeIl added the ✨enhancement New feature or request label Dec 9, 2025
@LimdaeIl LimdaeIl moved this from Backlog to In progress in WeGo-Together Backend Dec 9, 2025
@coderabbitai
Copy link

coderabbitai bot commented Dec 9, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

개요

그룹 참여 기능을 구현하면서 GetGroupResponse 응답 DTO를 리팩토링합니다. attendGroup 엔드포인트는 그룹 및 회원 존재 여부와 수용 인원을 검증한 후 참여 상태를 업데이트합니다. 중첩 레코드와 팩토리 메서드를 통해 GetGroupResponse 구조를 개선합니다.

변경 사항

Cohort / File(s) 변경 요약
응답 DTO 구조 개선
src/main/java/team/wego/wegobackend/group/application/dto/response/GetGroupResponse.java
빌더 패턴에서 명시적 of() 팩토리 메서드로 변경. participantCount를 long에서 int로 변경. 세 개의 중첩 public 레코드 추가: CreatedByResponse, UserStatusResponse, JoinedMemberResponse. images 및 user-status 관련 필드 추가.
그룹 참여 비즈니스 로직
src/main/java/team/wego/wegobackend/group/application/service/GroupService.java
attendGroup(groupId, memberId) 메서드 추가: 그룹 존재, 회원 존재, 중복 참여, 수용 인원 검증 포함. buildGetGroupResponse() 헬퍼 메서드 추가. validateCreateGroupRequest의 endTime 검증 개선.
도메인 엔티티 상태 전환
src/main/java/team/wego/wegobackend/group/domain/entity/GroupUser.java
reAttend() 메서드 추가: 상태를 ATTEND로 설정, joinedAt 갱신, leftAt 초기화. leave() 메서드 추가: 상태를 LEFT로 설정, leftAt 갱신.
Enum 포맷팅
src/main/java/team/wego/wegobackend/group/domain/entity/GroupUserStatus.java
닫는 중괄호 앞 빈 줄 제거.
에러 코드 확장
src/main/java/team/wego/wegobackend/group/domain/exception/GroupErrorCode.java
세 개의 새로운 에러 코드 상수 추가: MEMBER_NOT_FOUND (NOT_FOUND), ALREADY_ATTEND_GROUP (BAD_REQUEST), GROUP_CAPACITY_EXCEEDED (BAD_REQUEST).
저장소 계층 쿼리 추가
src/main/java/team/wego/wegobackend/group/domain/repository/GroupRepository.java
삭제되지 않은 그룹 조회 메서드 findByIdAndDeletedAtIsNull(Long id) 추가.
GroupUser 저장소 쿼리
src/main/java/team/wego/wegobackend/group/domain/repository/GroupUserRepository.java
세 개의 쿼리 메서드 추가: findByGroupAndUser(), countByGroupAndStatus(), findByGroupAndStatusOrderByJoinedAtAsc().
컨트롤러 엔드포인트
src/main/java/team/wego/wegobackend/group/presentation/GroupController.java
POST /api/v1/groups/{groupId}/attend 엔드포인트 추가: attendGroup(groupId, userId) 메서드로 그룹 참여 처리, ApiResponse<GetGroupResponse> 반환.
통합 테스트 시나리오
src/test/http/group/attend.http
회원가입 → 이미지 업로드 → 그룹 생성 → 그룹 참여 의 완전한 HTTP 테스트 워크플로우 추가.

시퀀스 다이어그램

sequenceDiagram
    participant Client
    participant Controller as GroupController
    participant Service as GroupService
    participant GroupRepo as GroupRepository
    participant UserRepo as UserRepository
    participant GroupUserRepo as GroupUserRepository
    participant Domain as GroupUser

    Client->>Controller: POST /groups/{groupId}/attend
    activate Controller
    Controller->>Service: attendGroup(groupId, userId)
    deactivate Controller
    
    activate Service
    Service->>GroupRepo: findByIdAndDeletedAtIsNull(groupId)
    activate GroupRepo
    GroupRepo-->>Service: Group or empty
    deactivate GroupRepo
    
    alt Group not found
        Service-->>Client: GroupErrorCode.GROUP_NOT_FOUND
    end
    
    Service->>UserRepo: findById(userId)
    activate UserRepo
    UserRepo-->>Service: User or empty
    deactivate UserRepo
    
    alt User not found
        Service-->>Client: GroupErrorCode.MEMBER_NOT_FOUND
    end
    
    Service->>GroupUserRepo: findByGroupAndUser(group, user)
    activate GroupUserRepo
    GroupUserRepo-->>Service: GroupUser or empty
    deactivate GroupUserRepo
    
    alt GroupUser exists and ATTEND status
        Service-->>Client: GroupErrorCode.ALREADY_ATTEND_GROUP
    end
    
    Service->>Service: Check group capacity
    alt Capacity exceeded
        Service-->>Client: GroupErrorCode.GROUP_CAPACITY_EXCEEDED
    end
    
    alt GroupUser exists (previous LEFT)
        Service->>Domain: reAttend()
        activate Domain
        Domain->>Domain: status = ATTEND, update joinedAt, clear leftAt
        deactivate Domain
    else GroupUser not exists
        Service->>Domain: Create new GroupUser(ATTEND status)
        activate Domain
        deactivate Domain
    end
    
    Service->>Service: buildGetGroupResponse(group, userId)
    Service-->>Client: ApiResponse(GetGroupResponse)
    deactivate Service
Loading

예상 코드 리뷰 노력

🎯 3 (중간) | ⏱️ ~25분

  • 주의가 필요한 영역:
    • attendGroup() 메서드의 다중 검증 로직 및 상태 전환 흐름 (GroupService.java)
    • 중첩 레코드의 정확한 필드 매핑 및 팩토리 메서드 (GetGroupResponse.java)
    • 저장소 쿼리 메서드 선언과 실제 사용 패턴의 일관성 (GroupUserRepository.java)
    • reAttend() vs 신규 GroupUser 생성의 분기 로직 (GroupUser.java, GroupService.java)
    • HTTP 테스트 파일의 실제 엔드포인트 응답 구조와의 정렬 (attend.http)

관련 가능성이 있는 PR들

토끼의 축하 시

🐰 모임에 참여하는 길을 열었네요,
그룹의 문을 누구나 두드릴 수 있게!
검증하고, 기록하고, 상태를 갱신하며,
누적된 모임 참여자들이 함께 춤추네~ ✨
팩토리 메서드로 우아하게 구성된,
응답과 함께 새로운 동료들을 맞이합니다! 🎉

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.05% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed 제목은 모임 상세 조회 구현이라는 주요 변경사항을 명확하게 요약하고 있습니다.
Linked Issues check ✅ Passed 단일 모임 상세 조회 API 구현 요구사항이 충족되었으며, attendGroup 메서드와 GetGroupResponse 구조가 이를 지원합니다.
Out of Scope Changes check ✅ Passed 모든 변경사항이 모임 상세 조회 및 참석 기능 구현이라는 범위 내에 있으며, 무관한 변경은 없습니다.
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/attend-group

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

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 PR implements group attendance functionality, allowing users to join meetings. Note that the PR title mentions "모임 상세 조회 구현" (group detail view implementation), but the actual implementation focuses on the attendance feature.

Key Changes:

  • Added attendGroup endpoint to handle user attendance for groups
  • Implemented attendance state management with ATTEND and LEFT statuses, preserving attendance history
  • Enhanced GetGroupResponse to include detailed member information, user status, and host details

Reviewed changes

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

Show a summary per file
File Description
src/test/http/group/attend.http HTTP test file for group attendance workflow including user signup, group creation, and attendance
src/main/java/team/wego/wegobackend/group/presentation/GroupController.java Added attendGroup endpoint for users to join groups
src/main/java/team/wego/wegobackend/group/domain/repository/GroupUserRepository.java Added query methods for finding group users, counting attendees, and retrieving member lists
src/main/java/team/wego/wegobackend/group/domain/repository/GroupRepository.java Added soft-delete-aware query method to find active groups
src/main/java/team/wego/wegobackend/group/domain/exception/GroupErrorCode.java Added error codes for attendance-related validations (member not found, already attending, capacity exceeded)
src/main/java/team/wego/wegobackend/group/domain/entity/GroupUserStatus.java Removed trailing empty line from enum definition
src/main/java/team/wego/wegobackend/group/domain/entity/GroupUser.java Added reAttend() and leave() methods for managing user attendance state
src/main/java/team/wego/wegobackend/group/application/service/GroupService.java Implemented attendance logic with capacity checking and re-join support; added helper method to build detailed group responses
src/main/java/team/wego/wegobackend/group/application/dto/response/GetGroupResponse.java Restructured response to include images, member details, user status, and host information using nested record types

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

.map(gu -> UserStatusResponse.fromJoined(gu.getJoinedAt()))
.orElse(UserStatusResponse.notJoined());

// 참여 중인 멤버 리스트 (status == ATTEND)
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

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

The joinedMembers list is sorted by joinedAt in ascending order (line 280), but users who re-join will have their joinedAt timestamp updated (as set in the reAttend() method at line 66). This means re-joined users will appear at the end of the list rather than maintaining their original join position, which could be confusing. Consider the desired behavior: should re-joined users maintain their original position or be treated as new joiners?

Suggested change
// 참여 중인 멤버 리스트 (status == ATTEND)
// 참여 중인 멤버 리스트 (status == ATTEND)
// Note: This sorting assumes that GroupUser.joinedAt is never reset when a user re-joins.
// If joinedAt is updated on re-join, original join order will not be preserved.

Copilot uses AI. Check for mistakes.
@PostMapping("/{groupId}/attend")
public ResponseEntity<ApiResponse<GetGroupResponse>> attendGroup(
@PathVariable Long groupId,
@RequestParam Long userId
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

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

The userId is passed as a query parameter (@RequestParam) in a POST request. For POST requests, it's more appropriate to include such data in the request body for better REST API design and security. Query parameters in POST requests can be logged in server access logs and browser history. Consider creating a request DTO (e.g., AttendGroupRequest) that contains the userId in the body, or extract the user ID from authentication context if authentication is implemented.

Copilot uses AI. Check for mistakes.
// 모임 참여 취소

// 모임 상세 조회
@PostMapping("/{groupId}")
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

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

The endpoint for retrieving group details uses POST method (@PostMapping), but it should use GET method (@GetMapping) since it's a read operation that doesn't modify server state. GET is the appropriate HTTP method for retrieval operations according to REST conventions.

Suggested change
@PostMapping("/{groupId}")
@GetMapping("/{groupId}")

Copilot uses AI. Check for mistakes.
) {
GetGroupResponse response = groupService.attendGroup(groupId, userId);

return ResponseEntity
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

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

Extra space in ResponseEntity - there are two spaces between return and ResponseEntity. This should be a single space for consistency.

Suggested change
return ResponseEntity
return ResponseEntity

Copilot uses AI. Check for mistakes.

public void reAttend() {
this.status = GroupUserStatus.ATTEND;
this.joinedAt = LocalDateTime.now();
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

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

The reAttend() method updates joinedAt to the current time, which means the original join timestamp is lost when a user re-joins. This could be problematic if you need to track the history of when users first joined. Consider whether you want to preserve the original joinedAt timestamp or if you need a separate field to track re-join events.

Suggested change
this.joinedAt = LocalDateTime.now();
// this.joinedAt = LocalDateTime.now(); // Preserve original join time

Copilot uses AI. Check for mistakes.
Comment on lines +22 to +32
--boundary--
Content-Disposition: form-data; name="images"; filename="img1.png"
Content-Type: image/png

< ../image/resources/img1.png
--boundary--
Content-Disposition: form-data; name="images"; filename="img2.jpg"
Content-Type: image/jpeg

< ../image/resources/img2.jpg
--boundary--
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

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

The multipart boundary delimiter is incorrectly formatted. The closing boundary should have an additional -- at the end (i.e., --boundary-- after the last part). Currently, lines 22, 27, and 32 all use --boundary-- which represents the closing delimiter, but only the last one should have the trailing --. The intermediate boundaries should be --boundary (without trailing --).

Copilot uses AI. Check for mistakes.
IMAGE_UPLOAD_EXCEED(HttpStatus.BAD_REQUEST, "모임: 모임 이미지는 최대 3개 입니다. 현재 이미지 수: %s"),

MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "모임: 회원을 찾을 수 없습니다. 회원 ID: %s"),
ALREADY_ATTEND_GROUP(HttpStatus.BAD_REQUEST, "모임: 이미 참여 중인 모임입니다. 모임 ID: %s 회원 ID: %s"),
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

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

The error message format includes both groupId and memberId parameters, but this creates an overly detailed error message that might expose internal system information. Consider simplifying to just mention that the user is already attending the group, without exposing both IDs: "모임: 이미 참여 중인 모임입니다."

Suggested change
ALREADY_ATTEND_GROUP(HttpStatus.BAD_REQUEST, "모임: 이미 참여 중인 모임입니다. 모임 ID: %s 회원 ID: %s"),
ALREADY_ATTEND_GROUP(HttpStatus.BAD_REQUEST, "모임: 이미 참여 중인 모임입니다."),

Copilot uses AI. Check for mistakes.
Comment on lines +42 to +46
public ResponseEntity<ApiResponse<GetGroupResponse>> attendGroup(
@PathVariable Long groupId,
@RequestParam Long userId
) {
GetGroupResponse response = groupService.attendGroup(groupId, userId);
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

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

The attendGroup endpoint returns the full GetGroupResponse which includes detailed information about all members, images, and tags. For a simple attend action, this response may be unnecessarily large. Consider creating a lighter response DTO specifically for the attend action (e.g., AttendGroupResponse) that returns only essential information like success status, updated participant count, and the user's join timestamp. The full group details can be fetched separately if needed.

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

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/team/wego/wegobackend/group/presentation/GroupController.java (1)

55-63: 단건 모임 상세 조회 API의 HTTP 메서드 및 구현이 목표 대비 미완성 상태입니다.

현재 getGroupResponse는 다음과 같습니다.

  • @PostMapping("/{groupId}") 사용 (조회인데 POST)
  • groupService.getGroup(groupId) 호출은 주석 처리
  • 항상 ResponseEntity.noContent().build()(204)만 반환

PR 제목/목표가 “단 건 모임 상세 조회 구현”인 점을 고려하면:

  1. HTTP 메서드: 단순 조회에는 @GetMapping("/{groupId}")가 더 REST 관점에서 자연스럽습니다.
  2. 구현 상태: 실제로 GetGroupResponse를 내려주는 서비스 메서드(getGroup 또는 buildGetGroupResponse 재사용)가 필요합니다.

예시 스켈레톤은 대략 아래와 같이 정리할 수 있을 것 같습니다.

@GetMapping("/{groupId}")
public ResponseEntity<ApiResponse<GetGroupResponse>> getGroupResponse(
        @PathVariable Long groupId,
        @RequestParam Long userId // 또는 인증 정보에서 추출
) {
    GetGroupResponse response = groupService.getGroup(groupId, userId);
    return ResponseEntity
            .ok(ApiResponse.success(response));
}

현재 상태로는 클라이언트가 상세 정보를 얻을 수 없으므로, 이 부분은 PR 병합 전 정리가 필요해 보입니다.

🧹 Nitpick comments (3)
src/main/java/team/wego/wegobackend/group/domain/entity/GroupUser.java (1)

64-73: 재참여 시 joinedAt 갱신 설계 의도 확인이 필요해 보입니다.

reAttend()에서 joinedAt을 현재 시각으로 덮어써서, 이전 참여 이력은 이 엔티티 기준으로는 사라집니다.
UI/통계에서 “처음 참여일”이 필요한지, 아니면 “현재 참여 세션 시작일”만 있으면 되는지에 따라 설계가 달라질 수 있습니다.

  • 이전 참여 이력을 유지해야 한다면: joinedAt은 최초 참여일로 고정하고, 재참여 정보를 별도 필드나 히스토리 테이블로 관리하는 방식을 고려해 주세요.
  • 현재 세션 기준 시간이 맞다면: 지금 구현이 맞지만, 주석으로 의도를 명시해 두면 이후 유지보수에 도움이 될 것 같습니다.
src/main/java/team/wego/wegobackend/group/domain/repository/GroupUserRepository.java (1)

3-18: 참여/정원/멤버 조회에 필요한 쿼리 메서드 구성이 적절합니다.

  • findByGroupAndUser로 단일 참여행 존재 여부 확인
  • countByGroupAndStatus로 상태별 인원 수 집계
  • findByGroupAndStatusOrderByJoinedAtAsc로 참여자 정렬 조회

현 비즈니스 요구사항에 잘 맞는 Repository 레이어 구조로 보이며, 별도 이슈는 없습니다.
(대량 트래픽/대규모 데이터가 예상되면 group_id, status 조합 인덱스 추가를 DB 레벨에서 고려할 수 있습니다.)

src/main/java/team/wego/wegobackend/group/presentation/GroupController.java (1)

40-51: 모임 참여 엔드포인트는 흐름이 명확합니다.

POST /api/v1/groups/{groupId}/attend?userId=...groupService.attendGroup(...)로 연결되는 플로우가 직관적이고, 응답 타입도 ApiResponse<GetGroupResponse>로 통일되어 있어 사용성이 좋습니다.

다만 추후 인증이 붙으면 @RequestParam Long userId 대신 SecurityContext나 토큰에서 유저 정보를 파싱하는 방식으로 전환될 것이므로, 상단 TODO(유저 정보 파싱 전, 임시 userId)와 동일한 TODO 코멘트를 이 메서드에도 남겨 두면 혼선을 줄일 수 있습니다.

📜 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 cc9a74c and 6f2c78a.

📒 Files selected for processing (9)
  • src/main/java/team/wego/wegobackend/group/application/dto/response/GetGroupResponse.java (1 hunks)
  • src/main/java/team/wego/wegobackend/group/application/service/GroupService.java (4 hunks)
  • src/main/java/team/wego/wegobackend/group/domain/entity/GroupUser.java (1 hunks)
  • src/main/java/team/wego/wegobackend/group/domain/entity/GroupUserStatus.java (0 hunks)
  • src/main/java/team/wego/wegobackend/group/domain/exception/GroupErrorCode.java (1 hunks)
  • src/main/java/team/wego/wegobackend/group/domain/repository/GroupRepository.java (1 hunks)
  • src/main/java/team/wego/wegobackend/group/domain/repository/GroupUserRepository.java (1 hunks)
  • src/main/java/team/wego/wegobackend/group/presentation/GroupController.java (1 hunks)
  • src/test/http/group/attend.http (1 hunks)
💤 Files with no reviewable changes (1)
  • src/main/java/team/wego/wegobackend/group/domain/entity/GroupUserStatus.java
🔇 Additional comments (5)
src/main/java/team/wego/wegobackend/group/domain/repository/GroupRepository.java (1)

3-10: 소프트 삭제 모임 필터링용 쿼리 메서드 추가 좋습니다.

findByIdAndDeletedAtIsNull로 삭제되지 않은 모임만 조회하도록 한 점이 도메인 의도와 잘 맞습니다. 별도 이슈는 없어 보입니다.

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

51-59: 종료 시간 검증 로직 개선 방향이 타당합니다.

endTimenull인 경우 검증을 건너뛰고, 값이 있을 때만 startTime 이후인지 검사하도록 변경한 점이 자연스럽습니다.
이로써 “종료 시간이 없는 오픈 모임” 같은 케이스도 지원 가능해져, 도메인 유연성이 좋아졌습니다.


248-293: buildGetGroupResponse로 조회용 DTO를 중앙에서 조립하는 구조가 좋습니다.

  • 이미지: GroupImage.sortOrder 기준 정렬 후 URL 리스트로 변환
  • 태그: GroupTag → Tag를 따라가 이름만 추출
  • 참여자 수: status == ATTEND 필터 후 count
  • 만든 사람: CreatedByResponse.from(group.getHost())로 캡슐화
  • 현재 사용자 상태: isJoined + joinedAtUserStatusResponse로 표현
  • 참여 중인 멤버 목록: GroupUser를 정렬 후 JoinedMemberResponse로 매핑

조회용 조립 로직이 한 곳에 모여 있어서, 추후 “상세 조회(getGroup)”를 구현할 때도 그대로 재사용하기 좋습니다.
다만 이 메서드는 group.getImages()/getGroupTags()/getUsers() 등 LAZY 관계에 의존하므로, 항상 트랜잭션 안에서 호출된다는 전제가 유지되도록 주의해 주시면 좋겠습니다.

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

18-23: 참여 도메인에 필요한 에러 코드가 잘 분리되었습니다.

  • MEMBER_NOT_FOUND → 회원 미존재
  • ALREADY_ATTEND_GROUP → 이미 참석 중
  • GROUP_CAPACITY_EXCEEDED → 정원 초과

HTTP Status, 한글 메시지 포맷(%s 개수) 모두 현재 사용처와 잘 맞고, 이후 에러 응답 처리에도 재사용성이 좋아 보입니다.

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

10-110: 상세 조회 응답 DTO 구조가 도메인 개념별로 잘 분리되었습니다.

  • 필드 확장

    • images: List<String> 추가
    • participantCountint로 단순화
    • createdBy, userStatus, joinedMembers를 별도 레코드로 분리
  • 팩토리 메서드

    • of(Group, List<String>, List<String>, int, CreatedByResponse, UserStatusResponse, List<JoinedMemberResponse>)로 조립 책임을 서비스 레이어에 위임하여, DTO 자체는 “그릇”에 집중하고 있습니다.
  • 중첩 레코드

    • CreatedByResponse.from(User)
    • UserStatusResponse.notJoined()/fromJoined(LocalDateTime)
    • JoinedMemberResponse.from(GroupUser)
      로 변환 로직이 응답 타입 안에 깔끔하게 캡슐화되어 있어서, 재사용성과 가독성이 좋습니다.

단, 기존에 이 응답을 사용하던 클라이언트가 있다면:

  • images 타입/구조 변경
  • 신규 필드(createdBy, userStatus, joinedMembers) 추가
    에 따른 JSON 스키마 변경을 인지하고 있는지 한 번만 확인해 보시면 좋겠습니다.

Comment on lines +207 to +245
@Transactional
public GetGroupResponse attendGroup(Long groupId, Long memberId) {
// 0. Group 조회 (삭제된 모임 제외)
Group group = groupRepository.findByIdAndDeletedAtIsNull(groupId)
.orElseThrow(
() -> new GroupException(GroupErrorCode.GROUP_NOT_FOUND_BY_ID, groupId));

// 1. member 조회
User member = userRepository.findById(memberId)
.orElseThrow(() -> new GroupException(GroupErrorCode.MEMBER_NOT_FOUND, memberId));

// 2. 이미 GroupUser row가 있는지 확인 (한 모임/유저당 한 row 가정)
GroupUser groupUser = groupUserRepository.findByGroupAndUser(group, member)
.orElse(null);

if (groupUser != null && groupUser.getStatus() == ATTEND) {
// 이미 참여 중인 경우
throw new GroupException(GroupErrorCode.ALREADY_ATTEND_GROUP, groupId, memberId);
}

// 3. 정원 체크 (ATTEND 상태인 인원 수 기준)
long currentAttendCount = groupUserRepository.countByGroupAndStatus(group, ATTEND);
if (currentAttendCount >= group.getMaxParticipants()) {
throw new GroupException(GroupErrorCode.GROUP_CAPACITY_EXCEEDED, groupId);
}

// 4. 참여 처리
if (groupUser == null) {
// 처음 참여
GroupUser newGroupUser = GroupUser.create(group, member, GroupRole.MEMBER);
groupUserRepository.save(newGroupUser);
} else if (groupUser.getStatus() == LEFT) {
// 이전에 나갔다가 다시 참여
groupUser.reAttend();
// dirty checking 으로 업데이트
}

return buildGetGroupResponse(group, memberId);
}
Copy link

@coderabbitai coderabbitai bot Dec 9, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's find the GroupUser entity definition to check for unique constraints
find . -type f -name "GroupUser.java" | head -20

Repository: WeGo-Together/WeGo_BackEnd

Length of output: 142


🏁 Script executed:

# Search for database schema files or migrations
find . -type f \( -name "*.sql" -o -name "*migration*" -o -name "*schema*" \) | grep -E "\.(sql|java)$" | head -30

Repository: WeGo-Together/WeGo_BackEnd

Length of output: 52


🏁 Script executed:

# Look for GroupUserRepository to understand the data access layer
find . -type f -name "GroupUserRepository.java"

Repository: WeGo-Together/WeGo_BackEnd

Length of output: 156


🏁 Script executed:

# Search for any unique constraints or database constraints in the codebase
rg -A 5 -B 5 "UniqueConstraint|unique.*constraint" --type java | head -100

Repository: WeGo-Together/WeGo_BackEnd

Length of output: 2018


🏁 Script executed:

cat -n ./src/main/java/team/wego/wegobackend/group/domain/entity/GroupUser.java

Repository: WeGo-Together/WeGo_BackEnd

Length of output: 2828


🏁 Script executed:

cat -n ./src/main/java/team/wego/wegobackend/group/domain/repository/GroupUserRepository.java

Repository: WeGo-Together/WeGo_BackEnd

Length of output: 918


🏁 Script executed:

# Look for database initialization files (schema.sql, data.sql, liquibase, flyway files)
find . -type f \( -name "*.sql" -o -name "*.xml" \) | grep -E "(db|migration|schema|sql)" | head -30

Repository: WeGo-Together/WeGo_BackEnd

Length of output: 52


🏁 Script executed:

find ./src/main/resources -type f \( -name "*.sql" -o -name "*.xml" -o -name "*.yml" -o -name "*.yaml" -o -name "*.properties" \) | head -30

Repository: WeGo-Together/WeGo_BackEnd

Length of output: 106


🏁 Script executed:

# Check for any application configuration files
find . -path ./target -prune -o -type f -name "application*.properties" -o -name "application*.yml" -o -name "application*.yaml" | grep -v target

Repository: WeGo-Together/WeGo_BackEnd

Length of output: 106


🏁 Script executed:

# Check Group entity for locking or transaction configuration
find . -type f -name "Group.java" -path "*/group/domain/entity/*"

Repository: WeGo-Together/WeGo_BackEnd

Length of output: 138


🏁 Script executed:

cat -n ./src/main/resources/application.yml

Repository: WeGo-Together/WeGo_BackEnd

Length of output: 2571


🏁 Script executed:

cat -n ./src/main/java/team/wego/wegobackend/group/domain/entity/Group.java

Repository: WeGo-Together/WeGo_BackEnd

Length of output: 4019


🏁 Script executed:

# Check if there are any database constraint definitions or migrations
find . -path ./target -prune -o -type f -name "*.sql" -print | head -20

Repository: WeGo-Together/WeGo_BackEnd

Length of output: 52


🏁 Script executed:

# Check GroupService for any locking mechanisms or additional transaction configuration
cat -n ./src/main/java/team/wego/wegobackend/group/application/service/GroupService.java | sed -n '200,250p'

Repository: WeGo-Together/WeGo_BackEnd

Length of output: 2416


정원 체크 동시성과 (group, user) 유니크 제약을 반드시 보강하세요.

현재 attendGroup 플로우의 구조는 명확하지만, 데이터 무결성 측면에서 두 가지 실질적인 문제가 있습니다.

  1. 정원 초과 race condition 확정
    countByGroupAndStatus(group, ATTEND) (line 228)과 save()/reAttend() (line 237, 240) 사이에 쓰기 윈도우가 존재합니다. 여러 사용자가 동시에 접근할 때:

    • 모두가 동일한 currentAttendCount 값을 읽고
    • 모두 용량 체크를 통과한 후
    • 각각 insert/update를 수행하면
      정원 초과 상태가 발생 가능합니다.

    해결책:

    • Group 행에 대해 비관적 락(@Lock(LockModeType.PESSIMISTIC_WRITE))으로 capacity 체크/참여를 원자성 보장
    • 또는 DB 트리거/제약으로 "참석 인원 ≤ maxParticipants" 강제
  2. GroupUser 테이블에 (group_id, user_id) 유니크 인덱스 부재
    GroupUser 엔티티(line 23, @table)에 @UniqueConstraint 없고, 코드는 단일 row 가정(line 218 주석)만 하고 있습니다. DB 스키마에도 제약이 없으면 중복 row 생성 가능성이 있습니다.

    해결책:

    • @UniqueConstraint(columnNames = {"group_id", "user_id"}) 추가 필수

트래픽과 동시성 수준을 고려하여 두 조치를 모두 우선순위로 처리하세요. 최소한 유니크 제약은 즉시 추가하기를 권장합니다.

Copy link
Collaborator Author

@LimdaeIl LimdaeIl Dec 9, 2025

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 알겠습니다! 우선 구현을 완료하는 것이 중요하시군요.

나중에 이 기술 부채를 추적하기 위해 GitHub 이슈를 생성해 드릴까요? 이슈에는 다음 내용을 포함할 수 있습니다:

  • 정원 체크 race condition 해결 (비관적 락 또는 DB 제약)
  • GroupUser 테이블에 (group_id, user_id) 유니크 제약 추가

이렇게 하면 고도화 단계에서 놓치지 않고 처리하실 수 있습니다. 이슈 생성을 원하시면 말씀해 주세요! 👍

Comment on lines +18 to +100
### 1-1. 모임 이미지 선 업로드 (png / jpg 2장)
POST http://localhost:8080/api/v1/groups/images/upload?userId={{userId}}
Content-Type: multipart/form-data; boundary=boundary

--boundary--
Content-Disposition: form-data; name="images"; filename="img1.png"
Content-Type: image/png

< ../image/resources/img1.png
--boundary--
Content-Disposition: form-data; name="images"; filename="img2.jpg"
Content-Type: image/jpeg

< ../image/resources/img2.jpg
--boundary--

> {%
const images = response.body.data.images;

client.global.set("img0_main", images[0].imageUrl440x240);
client.global.set("img0_thumb", images[0].imageUrl100x100);

client.global.set("img1_main", images[1].imageUrl440x240);
client.global.set("img1_thumb", images[1].imageUrl100x100);
%}

### 1-2. 모임 생성 (png/jpg 업로드 결과 URL로 생성)
POST http://localhost:8080/api/v1/groups/create?userId={{userId}}
Content-Type: application/json

{
"title": "강남에서 하는 자바 스터디 - PNG/JPG 테스트",
"location": "서울 강남구",
"locationDetail": "강남역 2번 출구 근처 카페",
"startTime": "2025-12-10T19:00:00",
"tags": [
"자바",
"백엔드",
"스터디"
],
"description": "PNG/JPG 업로드 후 URL을 이용해서 모임을 생성하는 테스트입니다.",
"maxParticipants": 12,
"images": [
{
"sortOrder": 0,
"imageUrl440x240": "{{img0_main}}",
"imageUrl100x100": "{{img0_thumb}}"
},
{
"sortOrder": 1,
"imageUrl440x240": "{{img1_main}}",
"imageUrl100x100": "{{img1_thumb}}"
}
]
}

> {%
// 그룹 ID 저장 (응답 구조에 맞게 조정)
client.global.set("groupId_png_jpg", response.body.data.id);
%}

### 회원가입(MEMBER 1)
POST http://localhost:8080/api/v1/auth/signup
Content-Type: application/json

{
"email": "test1@example.com",
"password": "Test1234!@#",
"nickName": "Heemo",
"phoneNumber": "010-1234-5678"
}

> {%
// 응답 구조에 따라 수정 필요
// 예: { "userId": 1, ... } 라면 아래처럼 유지
client.global.set("memberId1", response.body.userId);
%}

### 모임 참여
POST http://localhost:8080/api/v1/groups/{{groupId_png_jpg}}/attend
?userId={{memberId1}}
Content-Type: application/json

Copy link

@coderabbitai coderabbitai bot Dec 9, 2025

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, let's read the actual file to see the exact formatting
head -n 100 src/test/http/group/attend.http | tail -n +18 | cat -n

Repository: WeGo-Together/WeGo_BackEnd

Length of output: 2660


🏁 Script executed:

# Let's also check the full file structure to understand the context better
wc -l src/test/http/group/attend.http

Repository: WeGo-Together/WeGo_BackEnd

Length of output: 105


🏁 Script executed:

# Get the exact lines mentioned in the review (18-100)
sed -n '18,100p' src/test/http/group/attend.http | cat -n

Repository: WeGo-Together/WeGo_BackEnd

Length of output: 2660


🌐 Web query:

RFC 7578 multipart form-data boundary format specification

💡 Result:

Key points from RFC 7578 about the multipart/form-data "boundary":

  • The Content-Type MUST include a boundary parameter (Content-Type: multipart/form-data; boundary=...). [1].
  • Each part is delimited by a boundary delimiter constructed as CRLF, "--", then the boundary value (i.e. CRLF "--" ). The final delimiter is the boundary preceded by "--" and followed by "--". The boundary delimiter MUST NOT appear inside any encapsulated part. [1][2].
  • Because the boundary value may contain characters that need quoting in header parameters, it is often necessary to enclose the boundary parameter value in quotes. [1].

Sources:
[1] RFC 7578, "Returning Values from Forms: multipart/form-data" (section 4.1).
[2] RFC 2046, "MIME Part Two: Media Types" (section 5.1) — referenced by RFC 7578 for multipart delimiter rules.


🌐 Web query:

REST Client .http file format URL query parameter line break support

💡 Result:

Short answer: Yes — in the VS Code REST Client (.http/.rest) you can split query parameters across lines, but only when each continuation line begins with ? or & (or you build the URL by concatenating variables). You cannot arbitrarily break the request-line mid-token and expect the extension to auto‑concatenate lines.

Example (works):
GET https://api.example.com/resource
?foo=1
&bar=2
&baz=3
(REST Client treats the lines immediately after the request line that start with ? or & as query parameters.) [1][2][3]

Notes / limitation:

  • If you put a plain line break inside the request line (not starting with ? or &), REST Client will not join those lines into a single URL. To split other parts you must use variables and concatenate them ({{var1}}{{var2}}). [4][5]

Sources:

  • REST Client examples and docs showing multi-line query syntax. [2][3]
  • Example .http gist demonstrating ? / & continuation lines. [1]
  • Discussion/issue and Stack Overflow confirming the limitation (no automatic concatenation of arbitrary broken lines). [4][5]

Fix multipart boundary delimiters and URL query parameter formatting.

  1. multipart boundary delimiters (lines 5–15)
    Per RFC 7578, each part starts with --boundary and the final boundary ends with --boundary--. Currently all delimiters use --boundary--:

    • Line 5 should be --boundary (first part delimiter)
    • Line 10 should be --boundary (second part delimiter)
    • Line 15 should remain --boundary-- (final delimiter)
  2. attend URL query parameter (lines 80–81)
    Move ?userId={{memberId1}} to the same line as the request URL:

    POST http://localhost:8080/api/v1/groups/{{groupId_png_jpg}}/attend?userId={{memberId1}}

    REST Client parsing is more reliable when the query string starts on the same line as the request method.

🤖 Prompt for AI Agents
In src/test/http/group/attend.http around lines 5–15 and 80–81, the multipart
boundaries and the attend request query placement are incorrect; change the
first and second part delimiters from `--boundary--` to `--boundary` (keep the
final delimiter as `--boundary--`) so each part uses `--boundary` and the end
uses `--boundary--`, and move the `?userId={{memberId1}}` so it is appended to
the same line as the POST URL (i.e., `POST
http://localhost:8080/api/v1/groups/{{groupId_png_jpg}}/attend?userId={{memberId1}}`)
to ensure correct REST client parsing.

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 확인해주셔서 감사합니다! 현재 환경에서 정상적으로 실행되고 있다니 다행입니다. REST Client가 제안한 형식도 잘 처리하고 있는 것 같네요. 👍

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