Conversation
|
Note Other AI code review bot(s) detectedCodeRabbit 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 구조를 개선합니다. 변경 사항
시퀀스 다이어그램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
예상 코드 리뷰 노력🎯 3 (중간) | ⏱️ ~25분
관련 가능성이 있는 PR들
토끼의 축하 시
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
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
attendGroupendpoint to handle user attendance for groups - Implemented attendance state management with
ATTENDandLEFTstatuses, preserving attendance history - Enhanced
GetGroupResponseto 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) |
There was a problem hiding this comment.
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?
| // 참여 중인 멤버 리스트 (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. |
| @PostMapping("/{groupId}/attend") | ||
| public ResponseEntity<ApiResponse<GetGroupResponse>> attendGroup( | ||
| @PathVariable Long groupId, | ||
| @RequestParam Long userId |
There was a problem hiding this comment.
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.
| // 모임 참여 취소 | ||
|
|
||
| // 모임 상세 조회 | ||
| @PostMapping("/{groupId}") |
There was a problem hiding this comment.
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.
| @PostMapping("/{groupId}") | |
| @GetMapping("/{groupId}") |
| ) { | ||
| GetGroupResponse response = groupService.attendGroup(groupId, userId); | ||
|
|
||
| return ResponseEntity |
There was a problem hiding this comment.
Extra space in ResponseEntity - there are two spaces between return and ResponseEntity. This should be a single space for consistency.
| return ResponseEntity | |
| return ResponseEntity |
|
|
||
| public void reAttend() { | ||
| this.status = GroupUserStatus.ATTEND; | ||
| this.joinedAt = LocalDateTime.now(); |
There was a problem hiding this comment.
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.
| this.joinedAt = LocalDateTime.now(); | |
| // this.joinedAt = LocalDateTime.now(); // Preserve original join time |
| --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-- |
There was a problem hiding this comment.
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 --).
| 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"), |
There was a problem hiding this comment.
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: "모임: 이미 참여 중인 모임입니다."
| ALREADY_ATTEND_GROUP(HttpStatus.BAD_REQUEST, "모임: 이미 참여 중인 모임입니다. 모임 ID: %s 회원 ID: %s"), | |
| ALREADY_ATTEND_GROUP(HttpStatus.BAD_REQUEST, "모임: 이미 참여 중인 모임입니다."), |
| public ResponseEntity<ApiResponse<GetGroupResponse>> attendGroup( | ||
| @PathVariable Long groupId, | ||
| @RequestParam Long userId | ||
| ) { | ||
| GetGroupResponse response = groupService.attendGroup(groupId, userId); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 제목/목표가 “단 건 모임 상세 조회 구현”인 점을 고려하면:
- HTTP 메서드: 단순 조회에는
@GetMapping("/{groupId}")가 더 REST 관점에서 자연스럽습니다.- 구현 상태: 실제로
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
📒 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: 종료 시간 검증 로직 개선 방향이 타당합니다.
endTime이null인 경우 검증을 건너뛰고, 값이 있을 때만startTime이후인지 검사하도록 변경한 점이 자연스럽습니다.
이로써 “종료 시간이 없는 오픈 모임” 같은 케이스도 지원 가능해져, 도메인 유연성이 좋아졌습니다.
248-293:buildGetGroupResponse로 조회용 DTO를 중앙에서 조립하는 구조가 좋습니다.
- 이미지:
GroupImage.sortOrder기준 정렬 후 URL 리스트로 변환- 태그:
GroupTag → Tag를 따라가 이름만 추출- 참여자 수:
status == ATTEND필터 후 count- 만든 사람:
CreatedByResponse.from(group.getHost())로 캡슐화- 현재 사용자 상태:
isJoined + joinedAt를UserStatusResponse로 표현- 참여 중인 멤버 목록:
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>추가participantCount를int로 단순화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 스키마 변경을 인지하고 있는지 한 번만 확인해 보시면 좋겠습니다.
| @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); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's find the GroupUser entity definition to check for unique constraints
find . -type f -name "GroupUser.java" | head -20Repository: 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 -30Repository: 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 -100Repository: WeGo-Together/WeGo_BackEnd
Length of output: 2018
🏁 Script executed:
cat -n ./src/main/java/team/wego/wegobackend/group/domain/entity/GroupUser.javaRepository: WeGo-Together/WeGo_BackEnd
Length of output: 2828
🏁 Script executed:
cat -n ./src/main/java/team/wego/wegobackend/group/domain/repository/GroupUserRepository.javaRepository: 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 -30Repository: 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 -30Repository: 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 targetRepository: 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.ymlRepository: WeGo-Together/WeGo_BackEnd
Length of output: 2571
🏁 Script executed:
cat -n ./src/main/java/team/wego/wegobackend/group/domain/entity/Group.javaRepository: 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 -20Repository: 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 플로우의 구조는 명확하지만, 데이터 무결성 측면에서 두 가지 실질적인 문제가 있습니다.
-
정원 초과 race condition 확정
countByGroupAndStatus(group, ATTEND)(line 228)과save()/reAttend()(line 237, 240) 사이에 쓰기 윈도우가 존재합니다. 여러 사용자가 동시에 접근할 때:- 모두가 동일한
currentAttendCount값을 읽고 - 모두 용량 체크를 통과한 후
- 각각 insert/update를 수행하면
정원 초과 상태가 발생 가능합니다.
해결책:
- Group 행에 대해 비관적 락(
@Lock(LockModeType.PESSIMISTIC_WRITE))으로 capacity 체크/참여를 원자성 보장 - 또는 DB 트리거/제약으로 "참석 인원 ≤ maxParticipants" 강제
- 모두가 동일한
-
GroupUser 테이블에 (group_id, user_id) 유니크 인덱스 부재
GroupUser 엔티티(line 23, @table)에@UniqueConstraint없고, 코드는 단일 row 가정(line 218 주석)만 하고 있습니다. DB 스키마에도 제약이 없으면 중복 row 생성 가능성이 있습니다.해결책:
@UniqueConstraint(columnNames = {"group_id", "user_id"})추가 필수
트래픽과 동시성 수준을 고려하여 두 조치를 모두 우선순위로 처리하세요. 최소한 유니크 제약은 즉시 추가하기를 권장합니다.
There was a problem hiding this comment.
알고 있는데, 지금은 당장 구현부터 마치고 고도화에서 시도할게
There was a problem hiding this comment.
@LimdaeIl 알겠습니다! 우선 구현을 완료하는 것이 중요하시군요.
나중에 이 기술 부채를 추적하기 위해 GitHub 이슈를 생성해 드릴까요? 이슈에는 다음 내용을 포함할 수 있습니다:
- 정원 체크 race condition 해결 (비관적 락 또는 DB 제약)
- GroupUser 테이블에 (group_id, user_id) 유니크 제약 추가
이렇게 하면 고도화 단계에서 놓치지 않고 처리하실 수 있습니다. 이슈 생성을 원하시면 말씀해 주세요! 👍
| ### 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 | ||
|
|
There was a problem hiding this comment.
🧩 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 -nRepository: 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.httpRepository: 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 -nRepository: 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.
-
multipart boundary delimiters (lines 5–15)
Per RFC 7578, each part starts with--boundaryand 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)
- Line 5 should be
-
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.
There was a problem hiding this comment.
@LimdaeIl 확인해주셔서 감사합니다! 현재 환경에서 정상적으로 실행되고 있다니 다행입니다. REST Client가 제안한 형식도 잘 처리하고 있는 것 같네요. 👍
📝 Pull Request
📌 PR 종류
해당하는 항목에 체크해주세요.
✨ 변경 내용
모임에 참석할 수 있는 기능을 구현했습니다.
모임 참가자 여부도 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.