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 da48c87..f353781 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,6 +8,65 @@ @Getter @RequiredArgsConstructor public enum GroupErrorCode implements ErrorCode { + NO_PERMISSION_TO_VIEW_BANNED_TARGETS(HttpStatus.FORBIDDEN, + "모임: BANNED(차단) 대상 목록 조회 권한이 없습니다. 모임 ID: %s 회원 ID: %s" + ), + NO_PERMISSION_TO_VIEW_KICK_TARGETS(HttpStatus.FORBIDDEN, + "모임: 강퇴 대상 목록 조회 권한이 없습니다. 모임 ID: %s 회원 ID: %s" + ), + NO_PERMISSION_TO_VIEW_BAN_TARGETS(HttpStatus.FORBIDDEN, + "모임: 차단 대상 목록 조회 권한이 없습니다. 모임 ID: %s 회원 ID: %s" + ), + + NO_PERMISSION_TO_UNBAN(HttpStatus.FORBIDDEN, + "모임: 차단 해제 권한이 없습니다. 모임 ID: %s 회원 ID: %s" + ), + GROUP_USER_STATUS_NOT_ALLOWED_TO_UNBAN(HttpStatus.BAD_REQUEST, + "모임: 현재 멤버십 상태에서는 차단 해제가 불가능합니다. 모임 ID: %s 회원 ID: %s 상태: %s" + ), + GROUP_CANNOT_UNBAN_HOST(HttpStatus.BAD_REQUEST, + "모임: HOST는 차단 해제 대상이 될 수 없습니다. 모임 ID: %s 회원 ID: %s" + ), + GROUP_CANNOT_BAN_IN_STATUS(HttpStatus.BAD_REQUEST, + "모임: 현재 모임 상태에서는 차단이 불가능합니다. 모임 ID: %s 모임 상태: %s" + ), + + NO_PERMISSION_TO_BAN(HttpStatus.FORBIDDEN, + "모임: 차단 권한이 없습니다. 모임 ID: %s 회원 ID: %s" + ), + + GROUP_USER_STATUS_NOT_ALLOWED_TO_BAN(HttpStatus.BAD_REQUEST, + "모임: 현재 멤버십 상태에서는 차단이 불가능합니다. 모임 ID: %s 회원 ID: %s 상태: %s" + ), + + GROUP_CANNOT_BAN_SELF(HttpStatus.BAD_REQUEST, + "모임: 자기 자신을 차단할 수 없습니다. 모임 ID: %s 회원 ID: %s" + ), + + GROUP_CANNOT_BAN_HOST(HttpStatus.BAD_REQUEST, + "모임: HOST는 차단할 수 없습니다. 모임 ID: %s 회원 ID: %s" + ), + + + GROUP_CANNOT_KICK_IN_STATUS(HttpStatus.BAD_REQUEST, + "모임: 현재 모임 상태에서는 강퇴가 불가능합니다. 모임 ID: %s 모임 상태: %s" + ), + + NO_PERMISSION_TO_KICK(HttpStatus.FORBIDDEN, + "모임: 강퇴 권한이 없습니다. 모임 ID: %s 회원 ID: %s" + ), + + GROUP_USER_STATUS_NOT_ALLOWED_TO_KICK(HttpStatus.BAD_REQUEST, + "모임: 현재 멤버십 상태에서는 강퇴가 불가능합니다. 모임 ID: %s 회원 ID: %s 상태: %s" + ), + + GROUP_CANNOT_KICK_SELF(HttpStatus.BAD_REQUEST, + "모임: 자기 자신을 강퇴할 수 없습니다. 모임 ID: %s 회원 ID: %s" + ), + + GROUP_CANNOT_KICK_HOST(HttpStatus.BAD_REQUEST, + "모임: HOST는 강퇴할 수 없습니다. 모임 ID: %s 회원 ID: %s" + ), NO_PERMISSION_TO_REJECT_JOIN(HttpStatus.FORBIDDEN, "모임: 참여 거절 권한이 없습니다. 모임 ID: %s 회원 ID: %s" ), diff --git a/src/main/java/team/wego/wegobackend/group/v2/application/dto/common/AttendanceTargetItem.java b/src/main/java/team/wego/wegobackend/group/v2/application/dto/common/AttendanceTargetItem.java new file mode 100644 index 0000000..9170196 --- /dev/null +++ b/src/main/java/team/wego/wegobackend/group/v2/application/dto/common/AttendanceTargetItem.java @@ -0,0 +1,15 @@ +package team.wego.wegobackend.group.v2.application.dto.common; + +import java.time.LocalDateTime; +import team.wego.wegobackend.group.v2.domain.entity.GroupUserV2Status; + +public record AttendanceTargetItem( + Long userId, + String nickName, + String profileImage, + Long groupUserId, + GroupUserV2Status status, + LocalDateTime joinedAt +) { + +} 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 index a898bcb..2dd2f55 100644 --- 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 @@ -1,23 +1,24 @@ 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 + GroupUserV2Status status ) { - public static TargetMembership of(Long userId, GroupUserV2 groupUserV2) { + + public static TargetMembership of(Long userId, Long groupUserId, GroupUserV2Status status) { + return new TargetMembership(userId, groupUserId, status); + } + + public static TargetMembership from(Long userId, GroupUserV2 target) { return new TargetMembership( userId, - groupUserV2.getId(), - groupUserV2.getStatus(), - groupUserV2.getJoinedAt(), - groupUserV2.getLeftAt() + target.getId(), + target.getStatus() ); } -} \ No newline at end of file +} + diff --git a/src/main/java/team/wego/wegobackend/group/v2/application/dto/response/GetBanTargetsResponse.java b/src/main/java/team/wego/wegobackend/group/v2/application/dto/response/GetBanTargetsResponse.java new file mode 100644 index 0000000..3d071bf --- /dev/null +++ b/src/main/java/team/wego/wegobackend/group/v2/application/dto/response/GetBanTargetsResponse.java @@ -0,0 +1,16 @@ +package team.wego.wegobackend.group.v2.application.dto.response; + +import java.time.LocalDateTime; +import java.util.List; +import team.wego.wegobackend.group.v2.application.dto.common.AttendanceTargetItem; + +public record GetBanTargetsResponse( + Long groupId, + List targets, + LocalDateTime serverTime +) { + + public static GetBanTargetsResponse of(Long groupId, List targets) { + return new GetBanTargetsResponse(groupId, targets, LocalDateTime.now()); + } +} \ No newline at end of file diff --git a/src/main/java/team/wego/wegobackend/group/v2/application/dto/response/GetBannedTargetsResponse.java b/src/main/java/team/wego/wegobackend/group/v2/application/dto/response/GetBannedTargetsResponse.java new file mode 100644 index 0000000..a6829bd --- /dev/null +++ b/src/main/java/team/wego/wegobackend/group/v2/application/dto/response/GetBannedTargetsResponse.java @@ -0,0 +1,16 @@ +package team.wego.wegobackend.group.v2.application.dto.response; + +import java.time.LocalDateTime; +import java.util.List; +import team.wego.wegobackend.group.v2.application.dto.common.AttendanceTargetItem; + +public record GetBannedTargetsResponse( + Long groupId, + List targets, + LocalDateTime serverTime +) { + + public static GetBannedTargetsResponse of(Long groupId, List targets) { + return new GetBannedTargetsResponse(groupId, targets, LocalDateTime.now()); + } +} diff --git a/src/main/java/team/wego/wegobackend/group/v2/application/dto/response/GetKickTargetsResponse.java b/src/main/java/team/wego/wegobackend/group/v2/application/dto/response/GetKickTargetsResponse.java new file mode 100644 index 0000000..badfa55 --- /dev/null +++ b/src/main/java/team/wego/wegobackend/group/v2/application/dto/response/GetKickTargetsResponse.java @@ -0,0 +1,17 @@ +package team.wego.wegobackend.group.v2.application.dto.response; + + +import java.time.LocalDateTime; +import java.util.List; +import team.wego.wegobackend.group.v2.application.dto.common.AttendanceTargetItem; + +public record GetKickTargetsResponse( + Long groupId, + List targets, + LocalDateTime serverTime +) { + + public static GetKickTargetsResponse of(Long groupId, List targets) { + return new GetKickTargetsResponse(groupId, targets, LocalDateTime.now()); + } +} 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/GroupUserV2StatusResponse.java similarity index 83% rename from src/main/java/team/wego/wegobackend/group/v2/application/dto/response/ApproveRejectGroupV2Response.java rename to src/main/java/team/wego/wegobackend/group/v2/application/dto/response/GroupUserV2StatusResponse.java index e3212b8..f89f0f9 100644 --- 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/GroupUserV2StatusResponse.java @@ -7,32 +7,31 @@ import team.wego.wegobackend.group.v2.domain.entity.GroupV2JoinPolicy; import team.wego.wegobackend.group.v2.domain.entity.GroupV2Status; -public record ApproveRejectGroupV2Response( +public record GroupUserV2StatusResponse( Long groupId, GroupV2Status groupStatus, GroupV2JoinPolicy joinPolicy, long participantCount, int maxParticipants, - TargetMembership targetMembership, - LocalDateTime serverTime ) { - public static ApproveRejectGroupV2Response of( + public static GroupUserV2StatusResponse of( GroupV2 group, long participantCount, Long targetUserId, GroupUserV2 target ) { - return new ApproveRejectGroupV2Response( + return new GroupUserV2StatusResponse( group.getId(), group.getStatus(), group.getJoinPolicy(), participantCount, group.getMaxParticipants(), - TargetMembership.of(targetUserId, target), + TargetMembership.from(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 8521e5b..80d40b0 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 @@ -1,19 +1,25 @@ package team.wego.wegobackend.group.v2.application.service; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; 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.AttendanceTargetItem; 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.application.dto.response.GetBanTargetsResponse; +import team.wego.wegobackend.group.v2.application.dto.response.GetBannedTargetsResponse; +import team.wego.wegobackend.group.v2.application.dto.response.GetKickTargetsResponse; +import team.wego.wegobackend.group.v2.application.dto.response.GroupUserV2StatusResponse; import team.wego.wegobackend.group.v2.domain.entity.GroupUserV2; import team.wego.wegobackend.group.v2.domain.entity.GroupUserV2Role; import team.wego.wegobackend.group.v2.domain.entity.GroupUserV2Status; 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; +import team.wego.wegobackend.group.v2.domain.repository.GroupUserV2QueryRepository; import team.wego.wegobackend.group.v2.domain.repository.GroupUserV2Repository; import team.wego.wegobackend.group.v2.domain.repository.GroupV2Repository; import team.wego.wegobackend.user.repository.UserRepository; @@ -24,6 +30,7 @@ public class GroupV2AttendanceService { private final GroupUserV2Repository groupUserV2Repository; private final GroupV2Repository groupV2Repository; + private final GroupUserV2QueryRepository groupUserV2QueryRepository; // 회원 호출 private final UserRepository userRepository; @@ -123,7 +130,8 @@ public AttendanceGroupV2Response attend(Long userId, Long groupId) { groupUserV2Repository.save(groupUserV2); } - // 정원 체크 수행. 재참여 포함해서 체크하는 게 안전 + // 승인을 할 때만 정원 체크(이미 approve에서 하고 있으니 충분) + // approval_required에서 attend는 정원 체크 생략 가능 long attendCount = groupUserV2Repository.countByGroupIdAndStatus( groupId, GroupUserV2Status.ATTEND); @@ -194,7 +202,7 @@ public AttendanceGroupV2Response left(Long userId, Long groupId) { } @Transactional - public ApproveRejectGroupV2Response approve(Long approverUserId, Long groupId, + public GroupUserV2StatusResponse approve(Long approverUserId, Long groupId, Long targetUserId) { if (approverUserId == null || targetUserId == null) { throw new GroupException(GroupErrorCode.USER_ID_NULL); @@ -264,11 +272,11 @@ public ApproveRejectGroupV2Response approve(Long approverUserId, Long groupId, group.changeStatus(GroupV2Status.FULL); } - return ApproveRejectGroupV2Response.of(group, attendCount, targetUserId, target); + return GroupUserV2StatusResponse.of(group, attendCount, targetUserId, target); } @Transactional - public ApproveRejectGroupV2Response reject(Long approverUserId, Long groupId, + public GroupUserV2StatusResponse reject(Long approverUserId, Long groupId, Long targetUserId) { if (approverUserId == null || targetUserId == null) { throw new GroupException(GroupErrorCode.USER_ID_NULL); @@ -325,6 +333,240 @@ public ApproveRejectGroupV2Response reject(Long approverUserId, Long groupId, // reject는 ATTEND 수가 바뀌지 않는 게 일반적이지만(대상은 PENDING), // 혹시라도 상태 정책이 바뀌더라도 count는 최신으로 내려가도록 유지 - return ApproveRejectGroupV2Response.of(group, attendCount, targetUserId, target); + return GroupUserV2StatusResponse.of(group, attendCount, targetUserId, target); + } + + @Transactional + public GroupUserV2StatusResponse kick(Long kickerUserId, Long groupId, Long targetUserId) { + if (kickerUserId == null || targetUserId == null) { + throw new GroupException(GroupErrorCode.USER_ID_NULL); + } + + if (kickerUserId.equals(targetUserId)) { + throw new GroupException(GroupErrorCode.GROUP_CANNOT_KICK_SELF, groupId, kickerUserId); + } + + GroupV2 group = groupV2Repository.findById(groupId) + .orElseThrow( + () -> new GroupException(GroupErrorCode.GROUP_NOT_FOUND_BY_ID, groupId)); + + // ✅ HOST only + if (!group.getHost().getId().equals(kickerUserId)) { + throw new GroupException(GroupErrorCode.NO_PERMISSION_TO_KICK, groupId, kickerUserId); + } + + if (group.getHost().getId().equals(targetUserId)) { + throw new GroupException(GroupErrorCode.GROUP_CANNOT_KICK_HOST, groupId, targetUserId); + } + + if (group.getStatus() != GroupV2Status.RECRUITING + && group.getStatus() != GroupV2Status.FULL) { + throw new GroupException(GroupErrorCode.GROUP_CANNOT_KICK_IN_STATUS, groupId, + group.getStatus().name()); + } + + GroupUserV2 target = groupUserV2Repository.findByGroupIdAndUserId(groupId, targetUserId) + .orElseThrow(() -> new GroupException(GroupErrorCode.GROUP_USER_NOT_FOUND, + targetUserId)); + + if (target.getStatus() != GroupUserV2Status.ATTEND) { + throw new GroupException( + GroupErrorCode.GROUP_USER_STATUS_NOT_ALLOWED_TO_KICK, + groupId, targetUserId, target.getStatus().name() + ); + } + + target.kick(); + + long attendCount = groupUserV2Repository.countByGroupIdAndStatus(groupId, + GroupUserV2Status.ATTEND); + + if (group.getStatus() == GroupV2Status.FULL && attendCount < group.getMaxParticipants()) { + group.changeStatus(GroupV2Status.RECRUITING); + } + + return GroupUserV2StatusResponse.of(group, attendCount, targetUserId, target); + } + + @Transactional + public GroupUserV2StatusResponse ban(Long bannerUserId, Long groupId, Long targetUserId) { + if (bannerUserId == null || targetUserId == null) { + throw new GroupException(GroupErrorCode.USER_ID_NULL); + } + + if (bannerUserId.equals(targetUserId)) { + throw new GroupException(GroupErrorCode.GROUP_CANNOT_BAN_SELF, groupId, bannerUserId); + } + + GroupV2 group = groupV2Repository.findById(groupId) + .orElseThrow( + () -> new GroupException(GroupErrorCode.GROUP_NOT_FOUND_BY_ID, groupId)); + + // HOST only + if (!group.getHost().getId().equals(bannerUserId)) { + throw new GroupException(GroupErrorCode.NO_PERMISSION_TO_BAN, groupId, bannerUserId); + } + + if (group.getHost().getId().equals(targetUserId)) { + throw new GroupException(GroupErrorCode.GROUP_CANNOT_BAN_HOST, groupId, targetUserId); + } + + if (group.getStatus() != GroupV2Status.RECRUITING + && group.getStatus() != GroupV2Status.FULL) { + throw new GroupException(GroupErrorCode.GROUP_CANNOT_BAN_IN_STATUS, groupId, + group.getStatus().name()); + } + + GroupUserV2 target = groupUserV2Repository.findByGroupIdAndUserId(groupId, targetUserId) + .orElseThrow(() -> new GroupException(GroupErrorCode.GROUP_USER_NOT_FOUND, + targetUserId)); + + if (target.getStatus() != GroupUserV2Status.ATTEND) { + throw new GroupException( + GroupErrorCode.GROUP_USER_STATUS_NOT_ALLOWED_TO_BAN, + groupId, targetUserId, target.getStatus().name() + ); + } + + target.ban(); + + long attendCount = groupUserV2Repository.countByGroupIdAndStatus(groupId, + GroupUserV2Status.ATTEND); + + if (group.getStatus() == GroupV2Status.FULL && attendCount < group.getMaxParticipants()) { + group.changeStatus(GroupV2Status.RECRUITING); + } + + return GroupUserV2StatusResponse.of(group, attendCount, targetUserId, target); + } + + @Transactional(readOnly = true) + public GetKickTargetsResponse getKickTargets(Long requesterUserId, Long groupId) { + if (requesterUserId == null) { + throw new GroupException(GroupErrorCode.USER_ID_NULL); + } + + GroupV2 group = groupV2Repository.findById(groupId) + .orElseThrow( + () -> new GroupException(GroupErrorCode.GROUP_NOT_FOUND_BY_ID, groupId)); + + // HOST만 조회 가능 + if (!group.getHost().getId().equals(requesterUserId)) { + throw new GroupException(GroupErrorCode.NO_PERMISSION_TO_VIEW_KICK_TARGETS, groupId, + requesterUserId); + } + + List targets = groupUserV2QueryRepository + .fetchAttendMembersExceptHost(groupId) + .stream() + .map(r -> new AttendanceTargetItem( + r.userId(), + r.nickName(), + r.profileImage(), + r.groupUserId(), + r.status(), + r.joinedAt() + )) + .toList(); + + return GetKickTargetsResponse.of(groupId, targets); + } + + @Transactional(readOnly = true) + public GetBanTargetsResponse getBanTargets(Long requesterUserId, Long groupId) { + if (requesterUserId == 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(requesterUserId)) { + throw new GroupException(GroupErrorCode.NO_PERMISSION_TO_VIEW_BAN_TARGETS, groupId, + requesterUserId); + } + + List targets = groupUserV2QueryRepository + .fetchAttendMembersExceptHost(groupId) + .stream() + .map(r -> new AttendanceTargetItem( + r.userId(), + r.nickName(), + r.profileImage(), + r.groupUserId(), + r.status(), + r.joinedAt() + )) + .toList(); + + return GetBanTargetsResponse.of(groupId, targets); + } + + @Transactional + public GroupUserV2StatusResponse unban(Long requesterUserId, Long groupId, Long targetUserId) { + if (requesterUserId == null || targetUserId == null) { + throw new GroupException(GroupErrorCode.USER_ID_NULL); + } + + GroupV2 group = groupV2Repository.findById(groupId) + .orElseThrow( + () -> new GroupException(GroupErrorCode.GROUP_NOT_FOUND_BY_ID, groupId)); + + // HOST만 가능 + if (!group.getHost().getId().equals(requesterUserId)) { + throw new GroupException(GroupErrorCode.NO_PERMISSION_TO_UNBAN, groupId, + requesterUserId); + } + + // HOST는 unban 대상이 될 수 없음(애초에 ban도 불가지만 방어) + if (group.getHost().getId().equals(targetUserId)) { + throw new GroupException(GroupErrorCode.GROUP_CANNOT_UNBAN_HOST, groupId, targetUserId); + } + + GroupUserV2 target = groupUserV2Repository.findByGroupIdAndUserId(groupId, targetUserId) + .orElseThrow(() -> new GroupException(GroupErrorCode.GROUP_USER_NOT_FOUND, + targetUserId)); + + target.unban(); // BANNED만 허용, KICKED로 전환 + + long attendCount = groupUserV2Repository.countByGroupIdAndStatus(groupId, + GroupUserV2Status.ATTEND); + + // 상태 복귀는 unban에서 인원수가 바뀌지 않지만(어차피 BANNED는 ATTEND가 아님), + // 안전하게 FULL 복귀 로직은 건드릴 필요 없음. + return GroupUserV2StatusResponse.of(group, attendCount, targetUserId, target); + } + + @Transactional(readOnly = true) + public GetBannedTargetsResponse getBannedTargets(Long requesterUserId, Long groupId) { + if (requesterUserId == null) { + throw new GroupException(GroupErrorCode.USER_ID_NULL); + } + + GroupV2 group = groupV2Repository.findById(groupId) + .orElseThrow( + () -> new GroupException(GroupErrorCode.GROUP_NOT_FOUND_BY_ID, groupId)); + + // HOST만 조회 가능 + if (!group.getHost().getId().equals(requesterUserId)) { + throw new GroupException(GroupErrorCode.NO_PERMISSION_TO_VIEW_BANNED_TARGETS, groupId, + requesterUserId); + } + + List targets = groupUserV2QueryRepository + .fetchBannedMembersExceptHost(groupId) + .stream() + .map(r -> new AttendanceTargetItem( + r.userId(), + r.nickName(), + r.profileImage(), + r.groupUserId(), + r.status(), + r.joinedAt() + )) + .toList(); + + return GetBannedTargetsResponse.of(groupId, targets); } } 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 acad4df..cf78662 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 @@ -58,16 +58,16 @@ public class GroupUserV2 extends BaseTimeEntity { @Column(name = "group_user_status", nullable = false, length = 20) private GroupUserV2Status status; - private GroupUserV2(User user, GroupUserV2Role groupRole) { + private GroupUserV2(User user, GroupUserV2Role groupRole, GroupUserV2Status status) { this.user = user; this.groupRole = groupRole; + this.status = status; this.joinedAt = LocalDateTime.now(); - this.status = GroupUserV2Status.ATTEND; this.leftAt = null; } public static GroupUserV2 create(GroupV2 group, User user, GroupUserV2Role role) { - GroupUserV2 groupUser = new GroupUserV2(user, role); + GroupUserV2 groupUser = new GroupUserV2(user, role, GroupUserV2Status.ATTEND); group.addUser(groupUser); return groupUser; } @@ -107,13 +107,24 @@ public void cancelRequest() { public void kick() { if (this.status != GroupUserV2Status.ATTEND) { - throw new GroupException(GroupErrorCode.GROUP_NOT_ATTEND_STATUS); + throw new GroupException( + GroupErrorCode.GROUP_USER_STATUS_NOT_ALLOWED_TO_KICK, + this.group.getId(), this.user.getId(), this.status.name() + ); } this.status = GroupUserV2Status.KICKED; this.leftAt = LocalDateTime.now(); } public void ban() { + if (this.status != GroupUserV2Status.ATTEND) { + throw new GroupException( + GroupErrorCode.GROUP_USER_STATUS_NOT_ALLOWED_TO_BAN, + this.group.getId(), + this.user.getId(), + this.status.name() + ); + } this.status = GroupUserV2Status.BANNED; this.leftAt = LocalDateTime.now(); } @@ -127,12 +138,10 @@ void unassign() { } public static GroupUserV2 createPending(GroupV2 group, User user) { - GroupUserV2 groupUserV2 = new GroupUserV2(user, GroupUserV2Role.MEMBER); - groupUserV2.status = GroupUserV2Status.PENDING; // 신청 상태로 시작 - groupUserV2.joinedAt = LocalDateTime.now(); - groupUserV2.leftAt = null; - group.addUser(groupUserV2); - return groupUserV2; + GroupUserV2 groupUser = new GroupUserV2(user, GroupUserV2Role.MEMBER, + GroupUserV2Status.PENDING); + group.addUser(groupUser); + return groupUser; } public void requestJoin() { @@ -206,5 +215,17 @@ public void rejectJoin() { this.leftAt = LocalDateTime.now(); } + public void unban() { + if (this.status != GroupUserV2Status.BANNED) { + throw new GroupException( + GroupErrorCode.GROUP_USER_STATUS_NOT_ALLOWED_TO_UNBAN, + this.group.getId(), + this.user.getId(), + this.status.name() + ); + } + this.status = GroupUserV2Status.KICKED; // 재참여는 스스로 가능 + this.leftAt = LocalDateTime.now(); + } } diff --git a/src/main/java/team/wego/wegobackend/group/v2/domain/entity/GroupUserV2Role.java b/src/main/java/team/wego/wegobackend/group/v2/domain/entity/GroupUserV2Role.java index 3f172c5..05586d9 100644 --- a/src/main/java/team/wego/wegobackend/group/v2/domain/entity/GroupUserV2Role.java +++ b/src/main/java/team/wego/wegobackend/group/v2/domain/entity/GroupUserV2Role.java @@ -2,7 +2,7 @@ public enum GroupUserV2Role { HOST, - MANAGER, + MANAGER, // 미정 MEMBER } diff --git a/src/main/java/team/wego/wegobackend/group/v2/domain/repository/GroupUserV2QueryRepository.java b/src/main/java/team/wego/wegobackend/group/v2/domain/repository/GroupUserV2QueryRepository.java new file mode 100644 index 0000000..df9cd75 --- /dev/null +++ b/src/main/java/team/wego/wegobackend/group/v2/domain/repository/GroupUserV2QueryRepository.java @@ -0,0 +1,12 @@ +package team.wego.wegobackend.group.v2.domain.repository; + +import java.util.List; +import team.wego.wegobackend.group.v2.infrastructure.querydsl.projection.AttendanceTargetRow; + +public interface GroupUserV2QueryRepository { + + List fetchAttendMembersExceptHost(Long groupId); + + List fetchBannedMembersExceptHost(Long groupId); +} + diff --git a/src/main/java/team/wego/wegobackend/group/v2/infrastructure/querydsl/GroupUserV2QueryRepositoryImpl.java b/src/main/java/team/wego/wegobackend/group/v2/infrastructure/querydsl/GroupUserV2QueryRepositoryImpl.java new file mode 100644 index 0000000..d3f26a0 --- /dev/null +++ b/src/main/java/team/wego/wegobackend/group/v2/infrastructure/querydsl/GroupUserV2QueryRepositoryImpl.java @@ -0,0 +1,70 @@ +package team.wego.wegobackend.group.v2.infrastructure.querydsl; + +import static team.wego.wegobackend.group.v2.domain.entity.QGroupUserV2.groupUserV2; +import static team.wego.wegobackend.user.domain.QUser.user; + +import com.querydsl.core.types.Projections; +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; +import team.wego.wegobackend.group.v2.domain.entity.GroupUserV2Status; +import team.wego.wegobackend.group.v2.domain.repository.GroupUserV2QueryRepository; +import team.wego.wegobackend.group.v2.infrastructure.querydsl.projection.AttendanceTargetRow; + +@RequiredArgsConstructor +@Repository +public class GroupUserV2QueryRepositoryImpl implements GroupUserV2QueryRepository { + + private final JPAQueryFactory queryFactory; + + @Override + public List fetchAttendMembersExceptHost(Long groupId) { + return queryFactory + .select(Projections.constructor( + AttendanceTargetRow.class, + user.id, + user.nickName, + user.profileImage, + groupUserV2.id, + groupUserV2.status, + groupUserV2.joinedAt, + groupUserV2.leftAt + )) + .from(groupUserV2) + .join(groupUserV2.user, user) + .where( + groupUserV2.group.id.eq(groupId), + groupUserV2.status.eq(GroupUserV2Status.ATTEND), + groupUserV2.groupRole.ne( + team.wego.wegobackend.group.v2.domain.entity.GroupUserV2Role.HOST) + ) + .orderBy(groupUserV2.joinedAt.desc()) + .fetch(); + } + + @Override + public List fetchBannedMembersExceptHost(Long groupId) { + return queryFactory + .select(Projections.constructor( + AttendanceTargetRow.class, + user.id, + user.nickName, + user.profileImage, + groupUserV2.id, + groupUserV2.status, + groupUserV2.joinedAt, + groupUserV2.leftAt + )) + .from(groupUserV2) + .join(groupUserV2.user, user) + .where( + groupUserV2.group.id.eq(groupId), + groupUserV2.status.eq(GroupUserV2Status.BANNED), + groupUserV2.groupRole.ne( + team.wego.wegobackend.group.v2.domain.entity.GroupUserV2Role.HOST) + ) + .orderBy(groupUserV2.leftAt.desc().nullsLast()) + .fetch(); + } +} diff --git a/src/main/java/team/wego/wegobackend/group/v2/infrastructure/querydsl/projection/AttendanceTargetRow.java b/src/main/java/team/wego/wegobackend/group/v2/infrastructure/querydsl/projection/AttendanceTargetRow.java new file mode 100644 index 0000000..926a75c --- /dev/null +++ b/src/main/java/team/wego/wegobackend/group/v2/infrastructure/querydsl/projection/AttendanceTargetRow.java @@ -0,0 +1,17 @@ +package team.wego.wegobackend.group.v2.infrastructure.querydsl.projection; + +import java.time.LocalDateTime; +import team.wego.wegobackend.group.v2.domain.entity.GroupUserV2Status; + +public record AttendanceTargetRow( + Long userId, + String nickName, + String profileImage, + Long groupUserId, + GroupUserV2Status status, + LocalDateTime joinedAt, + LocalDateTime leftAt +) { + +} + 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 0acd41e..f27fa80 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,12 +21,15 @@ 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.GetBanTargetsResponse; +import team.wego.wegobackend.group.v2.application.dto.response.GetBannedTargetsResponse; import team.wego.wegobackend.group.v2.application.dto.response.GetGroupListV2Response; import team.wego.wegobackend.group.v2.application.dto.response.GetGroupV2Response; +import team.wego.wegobackend.group.v2.application.dto.response.GetKickTargetsResponse; import team.wego.wegobackend.group.v2.application.dto.response.GetMyGroupListV2Response; +import team.wego.wegobackend.group.v2.application.dto.response.GroupUserV2StatusResponse; import team.wego.wegobackend.group.v2.application.dto.response.UpdateGroupV2Response; import team.wego.wegobackend.group.v2.application.service.GroupMyGetV2Service; import team.wego.wegobackend.group.v2.application.service.GroupV2AttendanceService; @@ -79,7 +82,8 @@ public ResponseEntity> attend( @PathVariable Long groupId ) { - AttendanceGroupV2Response response = groupV2AttendanceService.attend(userDetails.getId(), groupId); + AttendanceGroupV2Response response = groupV2AttendanceService.attend(userDetails.getId(), + groupId); return ResponseEntity .status(HttpStatus.OK) @@ -93,7 +97,8 @@ public ResponseEntity> left( @AuthenticationPrincipal CustomUserDetails userDetails, @PathVariable Long groupId ) { - AttendanceGroupV2Response response = groupV2AttendanceService.left(userDetails.getId(), groupId); + AttendanceGroupV2Response response = groupV2AttendanceService.left(userDetails.getId(), + groupId); return ResponseEntity .status(HttpStatus.OK) @@ -170,26 +175,96 @@ public ResponseEntity delete( @PostMapping("/{groupId}/attendance/{targetUserId}/approve") - public ResponseEntity> approve( + public ResponseEntity> approve( @AuthenticationPrincipal CustomUserDetails userDetails, @PathVariable Long groupId, @PathVariable Long targetUserId ) { - ApproveRejectGroupV2Response response = + GroupUserV2StatusResponse response = groupV2AttendanceService.approve(userDetails.getId(), groupId, targetUserId); return ResponseEntity.ok(ApiResponse.success(HttpStatus.OK.value(), response)); } @PostMapping("/{groupId}/attendance/{targetUserId}/reject") - public ResponseEntity> reject( + public ResponseEntity> reject( @AuthenticationPrincipal CustomUserDetails userDetails, @PathVariable Long groupId, @PathVariable Long targetUserId ) { - ApproveRejectGroupV2Response response = + GroupUserV2StatusResponse response = groupV2AttendanceService.reject(userDetails.getId(), groupId, targetUserId); return ResponseEntity.ok(ApiResponse.success(HttpStatus.OK.value(), response)); } + + @PostMapping("/{groupId}/attendance/{targetUserId}/kick") + public ResponseEntity> kick( + @AuthenticationPrincipal CustomUserDetails userDetails, + @PathVariable Long groupId, + @PathVariable Long targetUserId + ) { + GroupUserV2StatusResponse response = + groupV2AttendanceService.kick(userDetails.getId(), groupId, targetUserId); + + return ResponseEntity.ok(ApiResponse.success(HttpStatus.OK.value(), response)); + } + + @PostMapping("/{groupId}/attendance/{targetUserId}/ban") + public ResponseEntity> ban( + @AuthenticationPrincipal CustomUserDetails userDetails, + @PathVariable Long groupId, + @PathVariable Long targetUserId + ) { + GroupUserV2StatusResponse response = + groupV2AttendanceService.ban(userDetails.getId(), groupId, targetUserId); + + return ResponseEntity.ok(ApiResponse.success(HttpStatus.OK.value(), response)); + } + + @GetMapping("/{groupId}/attendance/kick-targets") + public ResponseEntity> getKickTargets( + @AuthenticationPrincipal CustomUserDetails userDetails, + @PathVariable Long groupId + ) { + GetKickTargetsResponse response = + groupV2AttendanceService.getKickTargets(userDetails.getId(), groupId); + + return ResponseEntity.ok(ApiResponse.success(HttpStatus.OK.value(), response)); + } + + @GetMapping("/{groupId}/attendance/ban-targets") + public ResponseEntity> getBanTargets( + @AuthenticationPrincipal CustomUserDetails userDetails, + @PathVariable Long groupId + ) { + GetBanTargetsResponse response = + groupV2AttendanceService.getBanTargets(userDetails.getId(), groupId); + + return ResponseEntity.ok(ApiResponse.success(HttpStatus.OK.value(), response)); + } + + @PostMapping("/{groupId}/attendance/{targetUserId}/unban") + public ResponseEntity> unban( + @AuthenticationPrincipal CustomUserDetails userDetails, + @PathVariable Long groupId, + @PathVariable Long targetUserId + ) { + GroupUserV2StatusResponse response = + groupV2AttendanceService.unban(userDetails.getId(), groupId, targetUserId); + + return ResponseEntity.ok(ApiResponse.success(HttpStatus.OK.value(), response)); + } + + @GetMapping("/{groupId}/attendance/banned-targets") + public ResponseEntity> getBannedTargets( + @AuthenticationPrincipal CustomUserDetails userDetails, + @PathVariable Long groupId + ) { + GetBannedTargetsResponse response = + groupV2AttendanceService.getBannedTargets(userDetails.getId(), groupId); + + return ResponseEntity.ok(ApiResponse.success(HttpStatus.OK.value(), response)); + } } + diff --git a/src/test/http/group/v2/v2-group-ban-targets.http b/src/test/http/group/v2/v2-group-ban-targets.http new file mode 100644 index 0000000..1abbdc4 --- /dev/null +++ b/src/test/http/group/v2/v2-group-ban-targets.http @@ -0,0 +1,180 @@ +### 0. 회원가입(HOST) +POST http://localhost:8080/api/v1/auth/signup +Content-Type: application/json + +{ + "email": "bantargets_host@example.com", + "password": "Test1234!@#", + "nickName": "BanTargetsHost", + "phoneNumber": "010-0000-9400" +} + +> {% + client.global.set("bt_hostUserId", response.body.data.userId); +%} + +### 0-1. 로그인(HOST) +POST http://localhost:8080/api/v1/auth/login +Content-Type: application/json + +{ + "email": "bantargets_host@example.com", + "password": "Test1234!@#" +} + +> {% + client.global.set("bt_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 {{bt_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("bt_img0_key", images[0].imageKey); + client.global.set("bt_img1_key", images[1].imageKey); +%} + +### 1-2. FREE 모임 생성 (ban targets 조회용) +POST http://localhost:8080/api/v2/groups/create +Content-Type: application/json +Authorization: Bearer {{bt_hostAccessToken}} + +{ + "title": "getBanTargets 테스트 모임 (V2)", + "joinPolicy": "FREE", + "location": "서울 송파구", + "locationDetail": "잠실역 근처", + "startTime": "2026-12-23T19:00:00", + "endTime": "2026-12-23T21:00:00", + "tags": ["v2", "ban-targets", "query"], + "description": "HOST only getBanTargets 조회 테스트", + "maxParticipants": 5, + "images": [ + { "sortOrder": 0, "imageKey": "{{bt_img0_key}}" }, + { "sortOrder": 1, "imageKey": "{{bt_img1_key}}" } + ] +} + +> {% + client.global.set("bt_groupId", response.body.data.id); +%} + +### 2. 회원가입(MEMBER1) +POST http://localhost:8080/api/v1/auth/signup +Content-Type: application/json + +{ + "email": "bantargets_member1@example.com", + "password": "Test1234!@#", + "nickName": "BanMember1", + "phoneNumber": "010-1111-9401" +} + +> {% + client.global.set("bt_member1UserId", response.body.data.userId); +%} + +### 2-1. 로그인(MEMBER1) +POST http://localhost:8080/api/v1/auth/login +Content-Type: application/json + +{ + "email": "bantargets_member1@example.com", + "password": "Test1234!@#" +} + +> {% + client.global.set("bt_member1AccessToken", response.body.data.accessToken); +%} + +### 3. 회원가입(MEMBER2) +POST http://localhost:8080/api/v1/auth/signup +Content-Type: application/json + +{ + "email": "bantargets_member2@example.com", + "password": "Test1234!@#", + "nickName": "BanMember2", + "phoneNumber": "010-2222-9402" +} + +> {% + client.global.set("bt_member2UserId", response.body.data.userId); +%} + +### 3-1. 로그인(MEMBER2) +POST http://localhost:8080/api/v1/auth/login +Content-Type: application/json + +{ + "email": "bantargets_member2@example.com", + "password": "Test1234!@#" +} + +> {% + client.global.set("bt_member2AccessToken", response.body.data.accessToken); +%} + +### 4-1. MEMBER1 모임 참여 (ATTEND) +POST http://localhost:8080/api/v2/groups/{{bt_groupId}}/attend +Content-Type: application/json +Authorization: Bearer {{bt_member1AccessToken}} + +{} + +### 4-2. MEMBER2 모임 참여 (ATTEND) +POST http://localhost:8080/api/v2/groups/{{bt_groupId}}/attend +Content-Type: application/json +Authorization: Bearer {{bt_member2AccessToken}} + +{} + +### 5-1. HOST가 ban 대상 조회 (ban-targets) - 성공 기대 (HOST 제외 ATTEND 멤버들) +GET http://localhost:8080/api/v2/groups/{{bt_groupId}}/attendance/ban-targets +Authorization: Bearer {{bt_hostAccessToken}} + +### 5-2. 예외: MEMBER가 ban-targets 조회 (HOST only) +GET http://localhost:8080/api/v2/groups/{{bt_groupId}}/attendance/ban-targets +Authorization: Bearer {{bt_member1AccessToken}} + +### 6-1. (선택) 참여자 없는 새 모임 생성 -> ban-targets 빈 배열 확인 +POST http://localhost:8080/api/v2/groups/create +Content-Type: application/json +Authorization: Bearer {{bt_hostAccessToken}} + +{ + "title": "getBanTargets 빈 targets 테스트 (V2)", + "joinPolicy": "FREE", + "location": "서울 마포구", + "locationDetail": "홍대입구역", + "startTime": "2026-12-24T19:00:00", + "endTime": "2026-12-24T21:00:00", + "tags": ["v2", "ban-targets", "empty"], + "description": "아무도 참석 안 했을 때 targets=[] 확인", + "maxParticipants": 5, + "images": [ + ] +} + +> {% + client.global.set("bt_emptyGroupId", response.body.data.id); +%} + +### 6-2. (선택) HOST가 ban-targets 조회 (targets 빈 배열 기대) +GET http://localhost:8080/api/v2/groups/{{bt_emptyGroupId}}/attendance/ban-targets +Authorization: Bearer {{bt_hostAccessToken}} diff --git a/src/test/http/group/v2/v2-group-ban-unban.http b/src/test/http/group/v2/v2-group-ban-unban.http new file mode 100644 index 0000000..d9102d3 --- /dev/null +++ b/src/test/http/group/v2/v2-group-ban-unban.http @@ -0,0 +1,221 @@ +### 0. 회원가입(HOST) +POST http://localhost:8080/api/v1/auth/signup +Content-Type: application/json + +{ + "email": "ban_host@example.com", + "password": "Test1234!@#", + "nickName": "BanHost", + "phoneNumber": "010-0000-9200" +} + +> {% + client.global.set("ban_hostUserId", response.body.data.userId); +%} + +### 0-1. 로그인(HOST) +POST http://localhost:8080/api/v1/auth/login +Content-Type: application/json + +{ + "email": "ban_host@example.com", + "password": "Test1234!@#" +} + +> {% + client.global.set("ban_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 {{ban_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("ban_img0_key", images[0].imageKey); + client.global.set("ban_img1_key", images[1].imageKey); +%} + +### 1-2. FREE 모임 생성 (joinPolicy = FREE) +POST http://localhost:8080/api/v2/groups/create +Content-Type: application/json +Authorization: Bearer {{ban_hostAccessToken}} + +{ + "title": "BAN/UNBAN 테스트 모임 (V2)", + "joinPolicy": "FREE", + "location": "서울 마포구", + "locationDetail": "홍대 근처", + "startTime": "2026-12-22T19:00:00", + "endTime": "2026-12-22T21:00:00", + "tags": ["v2", "ban", "unban"], + "description": "HOST only ban/unban + banned-targets 조회 + 예외 케이스 점검", + "maxParticipants": 5, + "images": [ + { "sortOrder": 0, "imageKey": "{{ban_img0_key}}" }, + { "sortOrder": 1, "imageKey": "{{ban_img1_key}}" } + ] +} + +> {% + client.global.set("ban_groupId", response.body.data.id); +%} + +### 2. 회원가입(MEMBER1) - ban 대상 +POST http://localhost:8080/api/v1/auth/signup +Content-Type: application/json + +{ + "email": "ban_member1@example.com", + "password": "Test1234!@#", + "nickName": "BanMembers1", + "phoneNumber": "010-1111-9201" +} + +> {% + client.global.set("ban_member1UserId", response.body.data.userId); +%} + +### 2-1. 로그인(MEMBER1) +POST http://localhost:8080/api/v1/auth/login +Content-Type: application/json + +{ + "email": "ban_member1@example.com", + "password": "Test1234!@#" +} + +> {% + client.global.set("ban_member1AccessToken", response.body.data.accessToken); +%} + +### 3. 회원가입(MEMBER2) - 권한 예외 테스트용 +POST http://localhost:8080/api/v1/auth/signup +Content-Type: application/json + +{ + "email": "ban_member2@example.com", + "password": "Test1234!@#", + "nickName": "BanMembers2", + "phoneNumber": "010-2222-9202" +} + +> {% + client.global.set("ban_member2UserId", response.body.data.userId); +%} + +### 3-1. 로그인(MEMBER2) +POST http://localhost:8080/api/v1/auth/login +Content-Type: application/json + +{ + "email": "ban_member2@example.com", + "password": "Test1234!@#" +} + +> {% + client.global.set("ban_member2AccessToken", response.body.data.accessToken); +%} + +### 4-1. MEMBER1 모임 참여 (FREE -> ATTEND 기대) +POST http://localhost:8080/api/v2/groups/{{ban_groupId}}/attend +Content-Type: application/json +Authorization: Bearer {{ban_member1AccessToken}} + +{} + +### 4-2. MEMBER2 모임 참여 (FREE -> ATTEND 기대) +POST http://localhost:8080/api/v2/groups/{{ban_groupId}}/attend +Content-Type: application/json +Authorization: Bearer {{ban_member2AccessToken}} + +{} + +### 5-1. HOST가 ban 대상 조회 (ban-targets) - HOST only +GET http://localhost:8080/api/v2/groups/{{ban_groupId}}/attendance/ban-targets +Authorization: Bearer {{ban_hostAccessToken}} + +### 5-2. 예외: MEMBER가 ban-targets 조회 (HOST only) +GET http://localhost:8080/api/v2/groups/{{ban_groupId}}/attendance/ban-targets +Authorization: Bearer {{ban_member1AccessToken}} + +### 6-1. HOST가 MEMBER1 ban (ATTEND -> BANNED) +POST http://localhost:8080/api/v2/groups/{{ban_groupId}}/attendance/{{ban_member1UserId}}/ban +Content-Type: application/json +Authorization: Bearer {{ban_hostAccessToken}} + +{} + +### 6-2. 예외: MEMBER2가 MEMBER1 ban 시도 (권한 없음) +POST http://localhost:8080/api/v2/groups/{{ban_groupId}}/attendance/{{ban_member1UserId}}/ban +Content-Type: application/json +Authorization: Bearer {{ban_member2AccessToken}} + +{} + +### 6-3. 예외: HOST가 HOST 자신 ban 시도 +POST http://localhost:8080/api/v2/groups/{{ban_groupId}}/attendance/{{ban_hostUserId}}/ban +Content-Type: application/json +Authorization: Bearer {{ban_hostAccessToken}} + +{} + +### 7-1. HOST가 banned 대상 조회 (banned-targets) - HOST only +GET http://localhost:8080/api/v2/groups/{{ban_groupId}}/attendance/banned-targets +Authorization: Bearer {{ban_hostAccessToken}} + +### 7-2. 예외: MEMBER가 banned-targets 조회 (HOST only) +GET http://localhost:8080/api/v2/groups/{{ban_groupId}}/attendance/banned-targets +Authorization: Bearer {{ban_member2AccessToken}} + +### 8-1. 예외: BANNED 상태의 MEMBER1이 재참여 시도 (attend 시 GROUP_BANNED_USER 기대) +POST http://localhost:8080/api/v2/groups/{{ban_groupId}}/attend +Content-Type: application/json +Authorization: Bearer {{ban_member1AccessToken}} + +{} + +### 9-1. HOST가 MEMBER1 unban (BANNED -> KICKED 기대) +POST http://localhost:8080/api/v2/groups/{{ban_groupId}}/attendance/{{ban_member1UserId}}/unban +Content-Type: application/json +Authorization: Bearer {{ban_hostAccessToken}} + +{} + +### 9-2. 예외: MEMBER2가 unban 시도 (권한 없음) +POST http://localhost:8080/api/v2/groups/{{ban_groupId}}/attendance/{{ban_member1UserId}}/unban +Content-Type: application/json +Authorization: Bearer {{ban_member2AccessToken}} + +{} + +### 9-3. 예외: HOST가 HOST unban 시도 (대상 host 불가) +POST http://localhost:8080/api/v2/groups/{{ban_groupId}}/attendance/{{ban_hostUserId}}/unban +Content-Type: application/json +Authorization: Bearer {{ban_hostAccessToken}} + +{} + +### 10-1. MEMBER1 재참여 시도 (KICKED -> FREE면 reAttend로 ATTEND 가능 기대) +POST http://localhost:8080/api/v2/groups/{{ban_groupId}}/attend +Content-Type: application/json +Authorization: Bearer {{ban_member1AccessToken}} + +{} + +### 10-2. (선택) 최종 모임 상세 조회 - 상태/인원 확인 +GET http://localhost:8080/api/v2/groups/{{ban_groupId}} +Authorization: Bearer {{ban_hostAccessToken}} diff --git a/src/test/http/group/v2/v2-group-kick-targets.http b/src/test/http/group/v2/v2-group-kick-targets.http new file mode 100644 index 0000000..2a5eaab --- /dev/null +++ b/src/test/http/group/v2/v2-group-kick-targets.http @@ -0,0 +1,180 @@ +### 0. 회원가입(HOST) +POST http://localhost:8080/api/v1/auth/signup +Content-Type: application/json + +{ + "email": "kicktargets_host@example.com", + "password": "Test1234!@#", + "nickName": "KickHost", + "phoneNumber": "010-0000-9300" +} + +> {% + client.global.set("kt_hostUserId", response.body.data.userId); +%} + +### 0-1. 로그인(HOST) +POST http://localhost:8080/api/v1/auth/login +Content-Type: application/json + +{ + "email": "kicktargets_host@example.com", + "password": "Test1234!@#" +} + +> {% + client.global.set("kt_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 {{kt_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("kt_img0_key", images[0].imageKey); + client.global.set("kt_img1_key", images[1].imageKey); +%} + +### 1-2. FREE 모임 생성 (kick targets 조회용) +POST http://localhost:8080/api/v2/groups/create +Content-Type: application/json +Authorization: Bearer {{kt_hostAccessToken}} + +{ + "title": "getKickTargets 테스트 모임 (V2)", + "joinPolicy": "FREE", + "location": "서울 성동구", + "locationDetail": "성수역 근처", + "startTime": "2026-12-23T19:00:00", + "endTime": "2026-12-23T21:00:00", + "tags": ["v2", "kick-targets", "query"], + "description": "HOST only getKickTargets 조회 테스트", + "maxParticipants": 5, + "images": [ + { "sortOrder": 0, "imageKey": "{{kt_img0_key}}" }, + { "sortOrder": 1, "imageKey": "{{kt_img1_key}}" } + ] +} + +> {% + client.global.set("kt_groupId", response.body.data.id); +%} + +### 2. 회원가입(MEMBER1) +POST http://localhost:8080/api/v1/auth/signup +Content-Type: application/json + +{ + "email": "kicktargets_member1@example.com", + "password": "Test1234!@#", + "nickName": "KickMember1", + "phoneNumber": "010-1111-9301" +} + +> {% + client.global.set("kt_member1UserId", response.body.data.userId); +%} + +### 2-1. 로그인(MEMBER1) +POST http://localhost:8080/api/v1/auth/login +Content-Type: application/json + +{ + "email": "kicktargets_member1@example.com", + "password": "Test1234!@#" +} + +> {% + client.global.set("kt_member1AccessToken", response.body.data.accessToken); +%} + +### 3. 회원가입(MEMBER2) +POST http://localhost:8080/api/v1/auth/signup +Content-Type: application/json + +{ + "email": "kicktargets_member2@example.com", + "password": "Test1234!@#", + "nickName": "KickMember2", + "phoneNumber": "010-2222-9302" +} + +> {% + client.global.set("kt_member2UserId", response.body.data.userId); +%} + +### 3-1. 로그인(MEMBER2) +POST http://localhost:8080/api/v1/auth/login +Content-Type: application/json + +{ + "email": "kicktargets_member2@example.com", + "password": "Test1234!@#" +} + +> {% + client.global.set("kt_member2AccessToken", response.body.data.accessToken); +%} + +### 4-1. MEMBER1 모임 참여 (ATTEND) +POST http://localhost:8080/api/v2/groups/{{kt_groupId}}/attend +Content-Type: application/json +Authorization: Bearer {{kt_member1AccessToken}} + +{} + +### 4-2. MEMBER2 모임 참여 (ATTEND) +POST http://localhost:8080/api/v2/groups/{{kt_groupId}}/attend +Content-Type: application/json +Authorization: Bearer {{kt_member2AccessToken}} + +{} + +### 5-1. HOST가 kick 대상 조회 (kick-targets) - 성공 기대 (HOST 제외 ATTEND 멤버들) +GET http://localhost:8080/api/v2/groups/{{kt_groupId}}/attendance/kick-targets +Authorization: Bearer {{kt_hostAccessToken}} + +### 5-2. 예외: MEMBER가 kick-targets 조회 (HOST only) +GET http://localhost:8080/api/v2/groups/{{kt_groupId}}/attendance/kick-targets +Authorization: Bearer {{kt_member1AccessToken}} + +### 6-1. (선택) 참여자 없는 새 모임 생성 -> kick-targets 빈 배열 확인 +POST http://localhost:8080/api/v2/groups/create +Content-Type: application/json +Authorization: Bearer {{kt_hostAccessToken}} + +{ + "title": "getKickTargets 빈 targets 테스트 (V2)", + "joinPolicy": "FREE", + "location": "서울 중구", + "locationDetail": "시청역", + "startTime": "2026-12-24T19:00:00", + "endTime": "2026-12-24T21:00:00", + "tags": ["v2", "kick-targets", "empty"], + "description": "아무도 참석 안 했을 때 targets=[] 확인", + "maxParticipants": 5, + "images": [ + ] +} + +> {% + client.global.set("kt_emptyGroupId", response.body.data.id); +%} + +### 6-2. (선택) HOST가 kick-targets 조회 (targets 빈 배열 기대) +GET http://localhost:8080/api/v2/groups/{{kt_emptyGroupId}}/attendance/kick-targets +Authorization: Bearer {{kt_hostAccessToken}} diff --git a/src/test/http/group/v2/v2-group-kick.http b/src/test/http/group/v2/v2-group-kick.http new file mode 100644 index 0000000..2442385 --- /dev/null +++ b/src/test/http/group/v2/v2-group-kick.http @@ -0,0 +1,192 @@ +### 0. 회원가입(HOST) +POST http://localhost:8080/api/v1/auth/signup +Content-Type: application/json + +{ + "email": "kick_host@example.com", + "password": "Test1234!@#", + "nickName": "KickHost", + "phoneNumber": "010-0000-9100" +} + +> {% + client.global.set("kick_hostUserId", response.body.data.userId); +%} + +### 0-1. 로그인(HOST) +POST http://localhost:8080/api/v1/auth/login +Content-Type: application/json + +{ + "email": "kick_host@example.com", + "password": "Test1234!@#" +} + +> {% + client.global.set("kick_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 {{kick_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("kick_img0_key", images[0].imageKey); + client.global.set("kick_img1_key", images[1].imageKey); +%} + +### 1-2. FREE 모임 생성 (joinPolicy = FREE) +POST http://localhost:8080/api/v2/groups/create +Content-Type: application/json +Authorization: Bearer {{kick_hostAccessToken}} + +{ + "title": "KICK 테스트 모임 (V2)", + "joinPolicy": "FREE", + "location": "서울 송파구", + "locationDetail": "잠실 근처", + "startTime": "2026-12-21T19:00:00", + "endTime": "2026-12-21T21:00:00", + "tags": ["v2", "kick"], + "description": "ATTEND 상태 MEMBER를 HOST가 kick하고, 상태/권한/타겟조회 예외까지 점검", + "maxParticipants": 5, + "images": [ + { "sortOrder": 0, "imageKey": "{{kick_img0_key}}" }, + { "sortOrder": 1, "imageKey": "{{kick_img1_key}}" } + ] +} + +> {% + client.global.set("kick_groupId", response.body.data.id); +%} + +### 2. 회원가입(MEMBER1) - kick 대상 +POST http://localhost:8080/api/v1/auth/signup +Content-Type: application/json + +{ + "email": "kick_member1@example.com", + "password": "Test1234!@#", + "nickName": "KickMember1", + "phoneNumber": "010-1111-9101" +} + +> {% + client.global.set("kick_member1UserId", response.body.data.userId); +%} + +### 2-1. 로그인(MEMBER1) +POST http://localhost:8080/api/v1/auth/login +Content-Type: application/json + +{ + "email": "kick_member1@example.com", + "password": "Test1234!@#" +} + +> {% + client.global.set("kick_member1AccessToken", response.body.data.accessToken); +%} + +### 3. 회원가입(MEMBER2) - 권한 예외 테스트용 +POST http://localhost:8080/api/v1/auth/signup +Content-Type: application/json + +{ + "email": "kick_member2@example.com", + "password": "Test1234!@#", + "nickName": "KickMember2", + "phoneNumber": "010-2222-9102" +} + +> {% + client.global.set("kick_member2UserId", response.body.data.userId); +%} + +### 3-1. 로그인(MEMBER2) +POST http://localhost:8080/api/v1/auth/login +Content-Type: application/json + +{ + "email": "kick_member2@example.com", + "password": "Test1234!@#" +} + +> {% + client.global.set("kick_member2AccessToken", response.body.data.accessToken); +%} + +### 4-1. MEMBER1 모임 참여 (FREE -> ATTEND 기대) +POST http://localhost:8080/api/v2/groups/{{kick_groupId}}/attend +Content-Type: application/json +Authorization: Bearer {{kick_member1AccessToken}} + +{} + +### 4-2. MEMBER2 모임 참여 (FREE -> ATTEND 기대) +POST http://localhost:8080/api/v2/groups/{{kick_groupId}}/attend +Content-Type: application/json +Authorization: Bearer {{kick_member2AccessToken}} + +{} + +### 5-1. HOST가 kick 대상 조회 (kick-targets) - HOST only +GET http://localhost:8080/api/v2/groups/{{kick_groupId}}/attendance/kick-targets +Authorization: Bearer {{kick_hostAccessToken}} + +### 5-2. 예외: MEMBER가 kick-targets 조회 (HOST only) +GET http://localhost:8080/api/v2/groups/{{kick_groupId}}/attendance/kick-targets +Authorization: Bearer {{kick_member1AccessToken}} + +### 6-1. HOST가 MEMBER1 kick (ATTEND -> KICKED) +POST http://localhost:8080/api/v2/groups/{{kick_groupId}}/attendance/{{kick_member1UserId}}/kick +Content-Type: application/json +Authorization: Bearer {{kick_hostAccessToken}} + +{} + +### 6-2. 예외: MEMBER2가 MEMBER1 kick 시도 (권한 없음) +POST http://localhost:8080/api/v2/groups/{{kick_groupId}}/attendance/{{kick_member1UserId}}/kick +Content-Type: application/json +Authorization: Bearer {{kick_member2AccessToken}} + +{} + +### 6-3. 예외: HOST가 HOST 자신 kick 시도 +POST http://localhost:8080/api/v2/groups/{{kick_groupId}}/attendance/{{kick_hostUserId}}/kick +Content-Type: application/json +Authorization: Bearer {{kick_hostAccessToken}} + +{} + +### 7-1. 예외: 이미 KICKED인 MEMBER1 다시 kick 시도 (status not allowed) +POST http://localhost:8080/api/v2/groups/{{kick_groupId}}/attendance/{{kick_member1UserId}}/kick +Content-Type: application/json +Authorization: Bearer {{kick_hostAccessToken}} + +{} + +### 8-1. (선택) 모임 상세 조회 - 상태/인원 확인 +GET http://localhost:8080/api/v2/groups/{{kick_groupId}} +Authorization: Bearer {{kick_hostAccessToken}} + +### 9-1. MEMBER1 재참여 시도 (FREE 정책이면 reAttend로 ATTEND 가능 기대) +POST http://localhost:8080/api/v2/groups/{{kick_groupId}}/attend +Content-Type: application/json +Authorization: Bearer {{kick_member1AccessToken}} + +{}