diff --git a/src/main/java/team/wego/wegobackend/group/domain/exception/GroupErrorCode.java b/src/main/java/team/wego/wegobackend/group/domain/exception/GroupErrorCode.java index 34ad09d..da48c87 100644 --- a/src/main/java/team/wego/wegobackend/group/domain/exception/GroupErrorCode.java +++ b/src/main/java/team/wego/wegobackend/group/domain/exception/GroupErrorCode.java @@ -8,7 +8,46 @@ @Getter @RequiredArgsConstructor public enum GroupErrorCode implements ErrorCode { + NO_PERMISSION_TO_REJECT_JOIN(HttpStatus.FORBIDDEN, + "모임: 참여 거절 권한이 없습니다. 모임 ID: %s 회원 ID: %s" + ), + CANNOT_APPROVE_SELF(HttpStatus.BAD_REQUEST, + "모임: 자기 자신을 승인할 수 없습니다. 모임 ID: %s 회원 ID: %s" + ), + CANNOT_REJECT_SELF(HttpStatus.BAD_REQUEST, + "모임: 자기 자신을 거절할 수 없습니다. 모임 ID: %s 회원 ID: %s" + ), + GROUP_USER_STATUS_NOT_ALLOWED_TO_APPROVE(HttpStatus.BAD_REQUEST, + "모임: 승인 처리는 PENDING 상태에서만 가능합니다. 모임 ID: %s 회원 ID: %s 현재 상태: %s" + ), + + GROUP_USER_STATUS_NOT_ALLOWED_TO_REJECT(HttpStatus.BAD_REQUEST, + "모임: 거절 처리는 PENDING 상태에서만 가능합니다. 모임 ID: %s 회원 ID: %s 현재 상태: %s" + ), + GROUP_JOIN_POLICY_NOT_APPROVAL_REQUIRED(HttpStatus.BAD_REQUEST, + "모임: 승인제 모임이 아니어서 승인/거절이 불가능합니다. 모임 ID: %s 참여 방식: %s" + ), + + GROUP_CANNOT_APPROVE_IN_STATUS(HttpStatus.BAD_REQUEST, + "모임: 현재 모임 상태에서는 승인 처리가 불가능합니다. 모임 ID: %s 모임 상태: %s" + ), + + GROUP_CANNOT_REJECT_IN_STATUS(HttpStatus.BAD_REQUEST, + "모임: 현재 모임 상태에서는 거절 처리가 불가능합니다. 모임 ID: %s 모임 상태: %s" + ), + + NO_PERMISSION_TO_APPROVE_JOIN(HttpStatus.FORBIDDEN, + "모임: 참여 승인/거절 권한이 없습니다. 모임 ID: %s 회원 ID: %s" + ), + + GROUP_USER_NOT_PENDING_STATUS(HttpStatus.BAD_REQUEST, + "모임: 승인/거절은 PENDING 상태에서만 가능합니다. 모임 ID: %s 회원 ID: %s 현재 상태: %s" + ), + // 필요하면(선택): 이미 처리된 요청을 더 명확히 구분하고 싶을 때 + GROUP_JOIN_REQUEST_ALREADY_PROCESSED(HttpStatus.BAD_REQUEST, + "모임: 이미 처리된 참여 요청입니다. 모임 ID: %s 회원 ID: %s 현재 상태: %s" + ), GROUP_CANNOT_LEAVE_IN_STATUS(HttpStatus.BAD_REQUEST, "모임: 현재 모임 상태에서는 나가기/신청취소가 불가능합니다. 모임 ID: %s 모임 상태: %s" ), diff --git a/src/main/java/team/wego/wegobackend/group/v2/application/dto/common/TargetMembership.java b/src/main/java/team/wego/wegobackend/group/v2/application/dto/common/TargetMembership.java new file mode 100644 index 0000000..a898bcb --- /dev/null +++ b/src/main/java/team/wego/wegobackend/group/v2/application/dto/common/TargetMembership.java @@ -0,0 +1,23 @@ +package team.wego.wegobackend.group.v2.application.dto.common; + +import java.time.LocalDateTime; +import team.wego.wegobackend.group.v2.domain.entity.GroupUserV2; +import team.wego.wegobackend.group.v2.domain.entity.GroupUserV2Status; + +public record TargetMembership( + Long userId, + Long groupUserId, + GroupUserV2Status status, + LocalDateTime joinedAt, + LocalDateTime leftAt +) { + public static TargetMembership of(Long userId, GroupUserV2 groupUserV2) { + return new TargetMembership( + userId, + groupUserV2.getId(), + groupUserV2.getStatus(), + groupUserV2.getJoinedAt(), + groupUserV2.getLeftAt() + ); + } +} \ No newline at end of file diff --git a/src/main/java/team/wego/wegobackend/group/v2/application/dto/response/ApproveRejectGroupV2Response.java b/src/main/java/team/wego/wegobackend/group/v2/application/dto/response/ApproveRejectGroupV2Response.java new file mode 100644 index 0000000..e3212b8 --- /dev/null +++ b/src/main/java/team/wego/wegobackend/group/v2/application/dto/response/ApproveRejectGroupV2Response.java @@ -0,0 +1,38 @@ +package team.wego.wegobackend.group.v2.application.dto.response; + +import java.time.LocalDateTime; +import team.wego.wegobackend.group.v2.application.dto.common.TargetMembership; +import team.wego.wegobackend.group.v2.domain.entity.GroupUserV2; +import team.wego.wegobackend.group.v2.domain.entity.GroupV2; +import team.wego.wegobackend.group.v2.domain.entity.GroupV2JoinPolicy; +import team.wego.wegobackend.group.v2.domain.entity.GroupV2Status; + +public record ApproveRejectGroupV2Response( + Long groupId, + GroupV2Status groupStatus, + GroupV2JoinPolicy joinPolicy, + long participantCount, + int maxParticipants, + + TargetMembership targetMembership, + + LocalDateTime serverTime +) { + + public static ApproveRejectGroupV2Response of( + GroupV2 group, + long participantCount, + Long targetUserId, + GroupUserV2 target + ) { + return new ApproveRejectGroupV2Response( + group.getId(), + group.getStatus(), + group.getJoinPolicy(), + participantCount, + group.getMaxParticipants(), + TargetMembership.of(targetUserId, target), + LocalDateTime.now() + ); + } +} \ No newline at end of file diff --git a/src/main/java/team/wego/wegobackend/group/v2/application/service/GroupV2AttendanceService.java b/src/main/java/team/wego/wegobackend/group/v2/application/service/GroupV2AttendanceService.java index bf0d7a7..8521e5b 100644 --- a/src/main/java/team/wego/wegobackend/group/v2/application/service/GroupV2AttendanceService.java +++ b/src/main/java/team/wego/wegobackend/group/v2/application/service/GroupV2AttendanceService.java @@ -6,6 +6,7 @@ import team.wego.wegobackend.group.domain.exception.GroupErrorCode; import team.wego.wegobackend.group.domain.exception.GroupException; import team.wego.wegobackend.group.v2.application.dto.common.MyMembership; +import team.wego.wegobackend.group.v2.application.dto.response.ApproveRejectGroupV2Response; import team.wego.wegobackend.group.v2.application.dto.response.AttendanceGroupV2Response; import team.wego.wegobackend.group.v2.domain.entity.GroupUserV2; import team.wego.wegobackend.group.v2.domain.entity.GroupUserV2Role; @@ -82,7 +83,7 @@ public AttendanceGroupV2Response attend(Long userId, Long groupId) { } // 즉시 참여인 경우 - if (group.getJoinPolicy() == GroupV2JoinPolicy.INSTANT) { + if (group.getJoinPolicy() == GroupV2JoinPolicy.FREE) { if (groupUserV2 != null) { // LEFT, KICKED, REJECTED, CANCELLED -> 재참여 groupUserV2.reAttend(); // 내부에서 BANNED만 막고, ATTEND로 변경 @@ -192,4 +193,138 @@ public AttendanceGroupV2Response left(Long userId, Long groupId) { return AttendanceGroupV2Response.of(group, attendCount, membership); } + @Transactional + public ApproveRejectGroupV2Response approve(Long approverUserId, Long groupId, + Long targetUserId) { + if (approverUserId == null || targetUserId == null) { + throw new GroupException(GroupErrorCode.USER_ID_NULL); + } + + if (approverUserId.equals(targetUserId)) { + throw new GroupException(GroupErrorCode.CANNOT_APPROVE_SELF, groupId, approverUserId); + } + + GroupV2 group = groupV2Repository.findById(groupId) + .orElseThrow( + () -> new GroupException(GroupErrorCode.GROUP_NOT_FOUND_BY_ID, groupId)); + + // 승인제 모임만 가능 + if (group.getJoinPolicy() != GroupV2JoinPolicy.APPROVAL_REQUIRED) { + throw new GroupException(GroupErrorCode.GROUP_JOIN_POLICY_NOT_APPROVAL_REQUIRED, + groupId); + } + + // 모임 상태 정책 + if (group.getStatus() != GroupV2Status.RECRUITING + && group.getStatus() != GroupV2Status.FULL) { + throw new GroupException( + GroupErrorCode.GROUP_CANNOT_APPROVE_IN_STATUS, + groupId, + group.getStatus().name() + ); + } + + // 권한 체크: HOST 또는 (그룹 내 MANAGER/… 정책)만 승인 가능 + // host는 group.getHost()로 체크 + boolean isHost = group.getHost().getId().equals(approverUserId); + boolean canApprove = isHost; + + if (!isHost) { + GroupUserV2 approverMembership = groupUserV2Repository.findByGroupIdAndUserId(groupId, + approverUserId) + .orElseThrow(() -> new GroupException(GroupErrorCode.GROUP_USER_NOT_FOUND, + approverUserId)); + + canApprove = approverMembership.getGroupRole() != null + && approverMembership.getGroupRole() != GroupUserV2Role.MEMBER; + } + + if (!canApprove) { + throw new GroupException(GroupErrorCode.NO_PERMISSION_TO_APPROVE_JOIN, groupId, + approverUserId); + } + + GroupUserV2 target = groupUserV2Repository.findByGroupIdAndUserId(groupId, targetUserId) + .orElseThrow(() -> new GroupException(GroupErrorCode.GROUP_USER_NOT_FOUND, + targetUserId)); + + // PENDING만 승인 가능 (도메인에서 검증) + target.approveJoin(); + + long attendCount = groupUserV2Repository.countByGroupIdAndStatus(groupId, + GroupUserV2Status.ATTEND); + + if (attendCount > group.getMaxParticipants()) { + throw new GroupException(GroupErrorCode.GROUP_IS_FULL, groupId); + } + + // FULL 자동 전환 + if (attendCount == group.getMaxParticipants() + && group.getStatus() == GroupV2Status.RECRUITING) { + group.changeStatus(GroupV2Status.FULL); + } + + return ApproveRejectGroupV2Response.of(group, attendCount, targetUserId, target); + } + + @Transactional + public ApproveRejectGroupV2Response reject(Long approverUserId, Long groupId, + Long targetUserId) { + if (approverUserId == null || targetUserId == null) { + throw new GroupException(GroupErrorCode.USER_ID_NULL); + } + + if (approverUserId.equals(targetUserId)) { + throw new GroupException(GroupErrorCode.CANNOT_REJECT_SELF, groupId, approverUserId); + } + + GroupV2 group = groupV2Repository.findById(groupId) + .orElseThrow( + () -> new GroupException(GroupErrorCode.GROUP_NOT_FOUND_BY_ID, groupId)); + + if (group.getJoinPolicy() != GroupV2JoinPolicy.APPROVAL_REQUIRED) { + throw new GroupException(GroupErrorCode.GROUP_JOIN_POLICY_NOT_APPROVAL_REQUIRED, + groupId); + } + + if (group.getStatus() != GroupV2Status.RECRUITING + && group.getStatus() != GroupV2Status.FULL) { + throw new GroupException( + GroupErrorCode.GROUP_CANNOT_REJECT_IN_STATUS, + groupId, + group.getStatus().name() + ); + } + + boolean isHost = group.getHost().getId().equals(approverUserId); + boolean canReject = isHost; + + if (!isHost) { + GroupUserV2 approverMembership = groupUserV2Repository.findByGroupIdAndUserId(groupId, + approverUserId) + .orElseThrow(() -> new GroupException(GroupErrorCode.GROUP_USER_NOT_FOUND, + approverUserId)); + + canReject = approverMembership.getGroupRole() != null + && approverMembership.getGroupRole() != GroupUserV2Role.MEMBER; // 예시 + } + + if (!canReject) { + throw new GroupException(GroupErrorCode.NO_PERMISSION_TO_REJECT_JOIN, groupId, + approverUserId); + } + + GroupUserV2 target = groupUserV2Repository.findByGroupIdAndUserId(groupId, targetUserId) + .orElseThrow(() -> new GroupException(GroupErrorCode.GROUP_USER_NOT_FOUND, + targetUserId)); + + target.rejectJoin(); + + long attendCount = groupUserV2Repository.countByGroupIdAndStatus(groupId, + GroupUserV2Status.ATTEND); + + // reject는 ATTEND 수가 바뀌지 않는 게 일반적이지만(대상은 PENDING), + // 혹시라도 상태 정책이 바뀌더라도 count는 최신으로 내려가도록 유지 + return ApproveRejectGroupV2Response.of(group, attendCount, targetUserId, target); + } } diff --git a/src/main/java/team/wego/wegobackend/group/v2/domain/entity/GroupUserV2.java b/src/main/java/team/wego/wegobackend/group/v2/domain/entity/GroupUserV2.java index 2bc9ed7..acad4df 100644 --- a/src/main/java/team/wego/wegobackend/group/v2/domain/entity/GroupUserV2.java +++ b/src/main/java/team/wego/wegobackend/group/v2/domain/entity/GroupUserV2.java @@ -178,4 +178,33 @@ public void leaveOrCancel() { ); } } + + public void approveJoin() { + if (this.status != GroupUserV2Status.PENDING) { + throw new GroupException( + GroupErrorCode.GROUP_USER_STATUS_NOT_ALLOWED_TO_APPROVE, + this.group.getId(), + this.user.getId(), + this.status.name() + ); + } + this.status = GroupUserV2Status.ATTEND; + this.joinedAt = LocalDateTime.now(); + this.leftAt = null; + } + + public void rejectJoin() { + if (this.status != GroupUserV2Status.PENDING) { + throw new GroupException( + GroupErrorCode.GROUP_USER_STATUS_NOT_ALLOWED_TO_REJECT, + this.group.getId(), + this.user.getId(), + this.status.name() + ); + } + this.status = GroupUserV2Status.REJECTED; + this.leftAt = LocalDateTime.now(); + } + } + diff --git a/src/main/java/team/wego/wegobackend/group/v2/domain/entity/GroupV2.java b/src/main/java/team/wego/wegobackend/group/v2/domain/entity/GroupV2.java index db55397..595c9c6 100644 --- a/src/main/java/team/wego/wegobackend/group/v2/domain/entity/GroupV2.java +++ b/src/main/java/team/wego/wegobackend/group/v2/domain/entity/GroupV2.java @@ -96,7 +96,7 @@ public static GroupV2 create( group.description = description; group.maxParticipants = maxParticipants; group.host = host; - group.joinPolicy = (joinPolicy == null) ? GroupV2JoinPolicy.INSTANT : joinPolicy; + group.joinPolicy = (joinPolicy == null) ? GroupV2JoinPolicy.FREE : joinPolicy; group.status = GroupV2Status.RECRUITING; return group; } diff --git a/src/main/java/team/wego/wegobackend/group/v2/domain/entity/GroupV2JoinPolicy.java b/src/main/java/team/wego/wegobackend/group/v2/domain/entity/GroupV2JoinPolicy.java index 84d94d5..6bc9089 100644 --- a/src/main/java/team/wego/wegobackend/group/v2/domain/entity/GroupV2JoinPolicy.java +++ b/src/main/java/team/wego/wegobackend/group/v2/domain/entity/GroupV2JoinPolicy.java @@ -1,7 +1,7 @@ package team.wego.wegobackend.group.v2.domain.entity; public enum GroupV2JoinPolicy { - INSTANT, // 참여 버튼 누르면 즉시 ATTEND + FREE, // 참여 버튼 누르면 즉시 ATTEND APPROVAL_REQUIRED, // 참여 버튼 누르면 신청 상태로 들어가고 HOST 승인 후 ATTEND INVITE_ONLY // (미정)초대 받은 사람만 참여 } diff --git a/src/main/java/team/wego/wegobackend/group/v2/presentation/GroupV2Controller.java b/src/main/java/team/wego/wegobackend/group/v2/presentation/GroupV2Controller.java index e7848c4..0acd41e 100644 --- a/src/main/java/team/wego/wegobackend/group/v2/presentation/GroupV2Controller.java +++ b/src/main/java/team/wego/wegobackend/group/v2/presentation/GroupV2Controller.java @@ -21,6 +21,7 @@ import team.wego.wegobackend.group.v2.application.dto.request.GroupListFilter; import team.wego.wegobackend.group.v2.application.dto.request.MyGroupTypeV2; import team.wego.wegobackend.group.v2.application.dto.request.UpdateGroupV2Request; +import team.wego.wegobackend.group.v2.application.dto.response.ApproveRejectGroupV2Response; import team.wego.wegobackend.group.v2.application.dto.response.AttendanceGroupV2Response; import team.wego.wegobackend.group.v2.application.dto.response.CreateGroupV2Response; import team.wego.wegobackend.group.v2.application.dto.response.GetGroupListV2Response; @@ -166,4 +167,29 @@ public ResponseEntity delete( return ResponseEntity.noContent().build(); } + + + @PostMapping("/{groupId}/attendance/{targetUserId}/approve") + public ResponseEntity> approve( + @AuthenticationPrincipal CustomUserDetails userDetails, + @PathVariable Long groupId, + @PathVariable Long targetUserId + ) { + ApproveRejectGroupV2Response response = + groupV2AttendanceService.approve(userDetails.getId(), groupId, targetUserId); + + return ResponseEntity.ok(ApiResponse.success(HttpStatus.OK.value(), response)); + } + + @PostMapping("/{groupId}/attendance/{targetUserId}/reject") + public ResponseEntity> reject( + @AuthenticationPrincipal CustomUserDetails userDetails, + @PathVariable Long groupId, + @PathVariable Long targetUserId + ) { + ApproveRejectGroupV2Response response = + groupV2AttendanceService.reject(userDetails.getId(), groupId, targetUserId); + + return ResponseEntity.ok(ApiResponse.success(HttpStatus.OK.value(), response)); + } } diff --git a/src/test/http/group/v2/V2-group-left.http b/src/test/http/group/v2/V2-group-left.http index d116329..8245235 100644 --- a/src/test/http/group/v2/V2-group-left.http +++ b/src/test/http/group/v2/V2-group-left.http @@ -58,6 +58,7 @@ Authorization: Bearer {{host2AccessToken}} { "title": "V2 left() 테스트용 모임", "location": "서울 서초구", + "joinPolicy": "FREE", "locationDetail": "교대역 1번 출구 근처 카페", "startTime": "2026-12-20T19:00:00", "endTime": "2026-12-20T21:00:00", @@ -162,7 +163,6 @@ Authorization: Bearer {{left_member1AccessToken}} {} ### 5-2. 예외: MEMBER 1 다시 left() 시도 -# ✅ 현재 도메인 로직 기준: LEFT 상태면 ALREADY_LEFT_GROUP (또는 유사 에러코드)로 떨어지는 게 자연스러움 POST http://localhost:8080/api/v2/groups/{{groupId_leftTest}}/left Content-Type: application/json Authorization: Bearer {{left_member1AccessToken}} diff --git a/src/test/http/group/v2/v2-group-approve-reject.http b/src/test/http/group/v2/v2-group-approve-reject.http new file mode 100644 index 0000000..92b9103 --- /dev/null +++ b/src/test/http/group/v2/v2-group-approve-reject.http @@ -0,0 +1,202 @@ +### 0. 회원가입(HOST) +POST http://localhost:8080/api/v1/auth/signup +Content-Type: application/json + +{ + "email": "approve_host@example.com", + "password": "Test1234!@#", + "nickName": "ApproveHost", + "phoneNumber": "010-0000-9000" +} + +> {% + client.global.set("ap_hostUserId", response.body.data.userId); +%} + +### 0-1. 로그인(HOST) +POST http://localhost:8080/api/v1/auth/login +Content-Type: application/json + +{ + "email": "approve_host@example.com", + "password": "Test1234!@#" +} + +> {% + client.global.set("ap_hostAccessToken", response.body.data.accessToken); +%} + +### 1-1. 모임 V2 이미지 선 업로드 (png/jpg 2장) +POST http://localhost:8080/api/v2/groups/images/upload +Content-Type: multipart/form-data; boundary=boundary +Authorization: Bearer {{ap_hostAccessToken}} + +--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("ap_img0_key", images[0].imageKey); + client.global.set("ap_img1_key", images[1].imageKey); +%} + +### 1-2. 승인제 모임 생성 (joinPolicy = APPROVAL_REQUIRED) +POST http://localhost:8080/api/v2/groups/create +Content-Type: application/json +Authorization: Bearer {{ap_hostAccessToken}} + +{ + "title": "승인제 참여 테스트 모임 (V2)", + "joinPolicy": "APPROVAL_REQUIRED", + "location": "서울 강남구", + "locationDetail": "강남역 근처 카페", + "startTime": "2026-12-20T19:00:00", + "endTime": "2026-12-20T21:00:00", + "tags": ["v2", "approval", "approve-reject"], + "description": "PENDING 신청 -> HOST approve/reject -> 상태 변화 테스트", + "maxParticipants": 5, + "images": [ + { "sortOrder": 0, "imageKey": "{{ap_img0_key}}" }, + { "sortOrder": 1, "imageKey": "{{ap_img1_key}}" } + ] +} + +> {% + client.global.set("ap_groupId", response.body.data.id); +%} + +### 2. 회원가입(MEMBER1) +POST http://localhost:8080/api/v1/auth/signup +Content-Type: application/json + +{ + "email": "approve_member1@example.com", + "password": "Test1234!@#", + "nickName": "ApproveMember1", + "phoneNumber": "010-1111-9001" +} + +> {% + client.global.set("ap_member1UserId", response.body.data.userId); +%} + +### 2-1. 로그인(MEMBER1) +POST http://localhost:8080/api/v1/auth/login +Content-Type: application/json + +{ + "email": "approve_member1@example.com", + "password": "Test1234!@#" +} + +> {% + client.global.set("ap_member1AccessToken", response.body.data.accessToken); +%} + +### 3. 회원가입(MEMBER2) - reject 테스트용 +POST http://localhost:8080/api/v1/auth/signup +Content-Type: application/json + +{ + "email": "approve_member2@example.com", + "password": "Test1234!@#", + "nickName": "ApproveMember2", + "phoneNumber": "010-2222-9002" +} + +> {% + client.global.set("ap_member2UserId", response.body.data.userId); +%} + +### 3-1. 로그인(MEMBER2) +POST http://localhost:8080/api/v1/auth/login +Content-Type: application/json + +{ + "email": "approve_member2@example.com", + "password": "Test1234!@#" +} + +> {% + client.global.set("ap_member2AccessToken", response.body.data.accessToken); +%} + +### 4-1. MEMBER1 모임 참여 신청 (승인제 -> PENDING 기대) +POST http://localhost:8080/api/v2/groups/{{ap_groupId}}/attend +Content-Type: application/json +Authorization: Bearer {{ap_member1AccessToken}} + +{} + +### 4-2. MEMBER2 모임 참여 신청 (승인제 -> PENDING 기대) +POST http://localhost:8080/api/v2/groups/{{ap_groupId}}/attend +Content-Type: application/json +Authorization: Bearer {{ap_member2AccessToken}} + +{} + +### 5-1. (선택) 모임 상세 조회 - HOST 기준으로 신청 상태 확인 +GET http://localhost:8080/api/v2/groups/{{ap_groupId}} +Authorization: Bearer {{ap_hostAccessToken}} + +### 6-1. HOST가 MEMBER1 승인 (PENDING -> ATTEND) +POST http://localhost:8080/api/v2/groups/{{ap_groupId}}/attendance/{{ap_member1UserId}}/approve +Content-Type: application/json +Authorization: Bearer {{ap_hostAccessToken}} + +{} + +### 6-2. HOST가 MEMBER2 거절 (PENDING -> REJECTED) +POST http://localhost:8080/api/v2/groups/{{ap_groupId}}/attendance/{{ap_member2UserId}}/reject +Content-Type: application/json +Authorization: Bearer {{ap_hostAccessToken}} + +{} + +### 7-1. 예외: MEMBER1을 다시 approve 시도 (이미 ATTEND -> PENDING 아님) +POST http://localhost:8080/api/v2/groups/{{ap_groupId}}/attendance/{{ap_member1UserId}}/approve +Content-Type: application/json +Authorization: Bearer {{ap_hostAccessToken}} + +{} + +### 7-2. 예외: MEMBER2를 다시 reject 시도 (이미 REJECTED -> PENDING 아님) +POST http://localhost:8080/api/v2/groups/{{ap_groupId}}/attendance/{{ap_member2UserId}}/reject +Content-Type: application/json +Authorization: Bearer {{ap_hostAccessToken}} + +{} + +### 8-1. 예외: MEMBER가 approve 호출 (권한 없음) +POST http://localhost:8080/api/v2/groups/{{ap_groupId}}/attendance/{{ap_member2UserId}}/approve +Content-Type: application/json +Authorization: Bearer {{ap_member1AccessToken}} + +{} + +### 9-1. MEMBER1 나가기(ATTEND -> LEFT) +POST http://localhost:8080/api/v2/groups/{{ap_groupId}}/left +Content-Type: application/json +Authorization: Bearer {{ap_member1AccessToken}} + +{} + +### 9-2. MEMBER2 나가기 시도(이미 REJECTED -> leaveOrCancel에서 예외 기대) +POST http://localhost:8080/api/v2/groups/{{ap_groupId}}/left +Content-Type: application/json +Authorization: Bearer {{ap_member2AccessToken}} + +{} + +### 10-1. (선택) 모임 상세 조회 - 최종 상태/인원 확인 +GET http://localhost:8080/api/v2/groups/{{ap_groupId}} +Authorization: Bearer {{ap_hostAccessToken}} diff --git a/src/test/http/group/v2/v2-group-attend-instant.http b/src/test/http/group/v2/v2-group-attend-free.http similarity index 99% rename from src/test/http/group/v2/v2-group-attend-instant.http rename to src/test/http/group/v2/v2-group-attend-free.http index 3b5fbb8..577eacd 100644 --- a/src/test/http/group/v2/v2-group-attend-instant.http +++ b/src/test/http/group/v2/v2-group-attend-free.http @@ -56,7 +56,7 @@ Authorization: Bearer {{host2AccessToken}} { "title": "V2 즉시참여(INSTANT) - attend/left/reAttend 테스트", - "joinPolicy": "INSTANT", + "joinPolicy": "FREE", "location": "서울 서초구", "locationDetail": "교대역 1번 출구 근처 카페", "startTime": "2026-12-20T19:00:00", diff --git a/src/test/http/group/v2/v2-group-create.http b/src/test/http/group/v2/v2-group-create.http index 946f57e..837e38c 100644 --- a/src/test/http/group/v2/v2-group-create.http +++ b/src/test/http/group/v2/v2-group-create.http @@ -59,7 +59,7 @@ Authorization: Bearer {{accessToken}} { "title": "강남에서 하는 자바 스터디 - PNG/JPG 테스트", "location": "서울 강남구", - "joinPolicy": "INSTANT", + "joinPolicy": "FREE", "locationDetail": "강남역 2번 출구 근처 카페", "startTime": "2026-12-10T19:00:00", "endTime": "2026-12-10T21:00:00", @@ -126,7 +126,7 @@ Authorization: Bearer {{accessToken}} { "title": "강남에서 하는 자바 스터디 - WEBP 테스트", "location": "서울 강남구", - "joinPolicy": "INSTANT", + "joinPolicy": "FREE", "locationDetail": "강남역 2번 출구 근처 카페", "startTime": "2026-12-10T19:00:00", "endTime": "2026-12-10T21:00:00", @@ -187,7 +187,7 @@ Authorization: Bearer {{accessToken}} { "title": "강남에서 하는 자바 스터디 - endTime null 테스트", "location": "서울 강남구", - "joinPolicy": "INSTANT", + "joinPolicy": "FREE", "locationDetail": "강남역 2번 출구 근처 카페", "startTime": "2026-12-10T19:00:00", "endTime": null, @@ -278,7 +278,7 @@ Authorization: Bearer {{accessToken}} { "title": "강남에서 하는 자바 스터디 - 지난 날짜 예외 테스트", "location": "서울 강남구", - "joinPolicy": "INSTANT", + "joinPolicy": "FREE", "locationDetail": "강남역 2번 출구 근처 카페", "startTime": "2025-12-10T19:00:00", "endTime": "2025-12-10T21:00:00", diff --git a/src/test/http/group/v2/v2-group-delete.http b/src/test/http/group/v2/v2-group-delete.http index 2acd519..a21dfac 100644 --- a/src/test/http/group/v2/v2-group-delete.http +++ b/src/test/http/group/v2/v2-group-delete.http @@ -57,7 +57,7 @@ Authorization: Bearer {{accessToken}} { "title": "모임 삭제 테스트 - 기본 생성", "location": "서울 강남구", - "joinPolicy": "INSTANT", + "joinPolicy": "FREE", "locationDetail": "강남역 2번 출구 근처 카페", "startTime": "2026-12-10T19:00:00", "endTime": "2026-12-10T21:00:00", @@ -152,7 +152,7 @@ Authorization: Bearer {{accessToken}} { "title": "모임 삭제 테스트 - 권한 예외", "location": "서울 강남구", - "joinPolicy": "INSTANT", + "joinPolicy": "FREE", "locationDetail": "강남역 2번 출구 근처 카페", "startTime": "2026-12-10T19:00:00", "endTime": "2026-12-10T21:00:00", diff --git a/src/test/http/group/v2/v2-group-get-list.http b/src/test/http/group/v2/v2-group-get-list.http index 1687a58..6bc41ef 100644 --- a/src/test/http/group/v2/v2-group-get-list.http +++ b/src/test/http/group/v2/v2-group-get-list.http @@ -60,7 +60,7 @@ Authorization: Bearer {{host2AccessToken}} { "title": "참여 취소 테스트용 자바 스터디", "location": "서울 서초구", - "joinPolicy": "INSTANT", + "joinPolicy": "FREE", "locationDetail": "교대역 1번 출구 근처 카페", "startTime": "2026-12-20T19:00:00", "endTime": "2026-12-20T21:00:00", diff --git a/src/test/http/group/v2/v2-group-get-me.http b/src/test/http/group/v2/v2-group-get-me.http index 5c2a417..a25e86b 100644 --- a/src/test/http/group/v2/v2-group-get-me.http +++ b/src/test/http/group/v2/v2-group-get-me.http @@ -57,7 +57,7 @@ Authorization: Bearer {{host2AccessToken}} { "title": "내 모임(current) 테스트용 - ACTIVE", "location": "서울 서초구", - "joinPolicy": "INSTANT", + "joinPolicy": "FREE", "locationDetail": "교대역 1번 출구 근처 카페", "startTime": "2026-12-20T19:00:00", "endTime": "2026-12-20T21:00:00", diff --git a/src/test/http/group/v2/v2-group-get.http b/src/test/http/group/v2/v2-group-get.http index 5da077a..9daac6e 100644 --- a/src/test/http/group/v2/v2-group-get.http +++ b/src/test/http/group/v2/v2-group-get.http @@ -61,7 +61,7 @@ Authorization: Bearer {{hostAccessToken}} { "title": "강남에서 하는 자바 스터디 - PNG/JPG 테스트", "location": "서울 강남구", - "joinPolicy": "INSTANT", + "joinPolicy": "FREE", "locationDetail": "강남역 2번 출구 근처 카페", "startTime": "2026-12-20T19:00:00", "endTime": "2026-12-20T21:00:00", @@ -102,7 +102,7 @@ Authorization: Bearer {{hostAccessToken}} { "title": "강남에서 하는 자바 스터디 - 지난 날짜 예외 테스트", "location": "서울 강남구", - "joinPolicy": "INSTANT", + "joinPolicy": "FREE", "locationDetail": "강남역 2번 출구 근처 카페", "startTime": "2026-12-10T19:00:00", "endTime": "2026-12-10T21:00:00", diff --git a/src/test/http/group/v2/v2-group-update.http b/src/test/http/group/v2/v2-group-update.http index aabdc8c..335750a 100644 --- a/src/test/http/group/v2/v2-group-update.http +++ b/src/test/http/group/v2/v2-group-update.http @@ -60,6 +60,7 @@ Authorization: Bearer {{accessToken}} { "title": "UPDATE 이미지 테스트 모임", "location": "서울 강남구", + "joinPolicy": "FREE", "locationDetail": "강남역 2번 출구 근처 카페", "startTime": "2026-12-10T19:00:00", "endTime": "2026-12-10T21:00:00", @@ -211,6 +212,7 @@ Authorization: Bearer {{accessToken}} { "title": "A,B 이미지 모임", "location": "서울 강남구", + "joinPolicy": "FREE", "locationDetail": "어딘가", "startTime": "2026-12-10T19:00:00", "endTime": "2026-12-10T21:00:00", @@ -295,6 +297,7 @@ Authorization: Bearer {{accessToken}} { "title": "주말 러닝 + 백엔드 토크 모임 (예제)", + "joinPolicy": "FREE", "location": "서울 강남구", "locationDetail": "역삼역 2번 출구 앞 (집결 후 이동)", "startTime": "2026-12-20T09:30:00",