diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/data/response/NearbyARCapsuleSummaryResponse.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/data/response/NearbyARCapsuleSummaryResponse.java index 66eaa0398..fe4777a1d 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/data/response/NearbyARCapsuleSummaryResponse.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/data/response/NearbyARCapsuleSummaryResponse.java @@ -34,6 +34,7 @@ public record NearbyARCapsuleSummaryResponse( @Schema(description = "캡슐 타입") CapsuleType capsuleType ) { + public NearbyARCapsuleSummaryResponse { if (dueDate != null) { dueDate = dueDate.withZoneSameInstant(ResponseMappingConstant.ZONE_ID); diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/repository/CapsuleRepository.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/repository/CapsuleRepository.java index 4f5378f46..03908bba4 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/repository/CapsuleRepository.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/repository/CapsuleRepository.java @@ -19,7 +19,7 @@ Optional findCapsuleByMemberIdAndCapsuleId( @Modifying(clearAutomatically = true) @Query("UPDATE Capsule c SET c.isOpened = true WHERE c.id = :capsuleId and c.member.id = :memberId") - void updateIsOpenedTrue( + int updateIsOpenedTrue( @Param("memberId") Long memberId, @Param("capsuleId") Long capsuleId ); diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/service/CapsuleService.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/service/CapsuleService.java index f1414845b..150af31c1 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/service/CapsuleService.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/service/CapsuleService.java @@ -74,7 +74,11 @@ public Capsule findCapsuleByMemberIdAndCapsuleId(final Long memberId, final Long @Transactional public void updateIsOpenedTrue(final Long memberId, final Long capsuleId) { - capsuleRepository.updateIsOpenedTrue(memberId, capsuleId); + int isOpenedTrue = capsuleRepository.updateIsOpenedTrue(memberId, capsuleId); + + if (isOpenedTrue != 1) { + throw new CapsuleNotFondException(); + } } @Transactional diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/secret_capsule/data/dto/MySecreteCapsuleDto.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/secret_capsule/data/dto/MySecreteCapsuleDto.java index 9b3c37704..19d661802 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/secret_capsule/data/dto/MySecreteCapsuleDto.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/secret_capsule/data/dto/MySecreteCapsuleDto.java @@ -4,7 +4,6 @@ import java.util.function.Function; import site.timecapsulearchive.core.domain.capsule.entity.CapsuleType; import site.timecapsulearchive.core.domain.capsule.secret_capsule.data.response.MySecreteCapsuleResponse; -import site.timecapsulearchive.core.global.common.response.ResponseMappingConstant; public record MySecreteCapsuleDto( Long capsuleId, diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/api/FriendApi.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/api/FriendApi.java index 436c43636..e42ccca02 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/api/FriendApi.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/api/FriendApi.java @@ -12,7 +12,6 @@ import org.springframework.http.ResponseEntity; import site.timecapsulearchive.core.domain.friend.data.request.SearchFriendsRequest; import site.timecapsulearchive.core.domain.friend.data.request.SendFriendRequest; -import site.timecapsulearchive.core.domain.friend.data.response.FriendReqStatusResponse; import site.timecapsulearchive.core.domain.friend.data.response.FriendRequestsSliceResponse; import site.timecapsulearchive.core.domain.friend.data.response.FriendsSliceResponse; import site.timecapsulearchive.core.domain.friend.data.response.SearchFriendsResponse; @@ -129,7 +128,7 @@ ResponseEntity> denyFriendRequest( description = "외부 API 요청 실패" ) }) - ResponseEntity> requestFriend( + ResponseEntity> requestFriend( Long memberId, @Parameter(in = ParameterIn.PATH, description = "친구 아이디", required = true, schema = @Schema()) diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/api/FriendApiController.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/api/FriendApiController.java index 56c2e0755..3eb3fc44c 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/api/FriendApiController.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/api/FriendApiController.java @@ -19,7 +19,6 @@ import site.timecapsulearchive.core.domain.friend.data.dto.SearchFriendSummaryDto; import site.timecapsulearchive.core.domain.friend.data.request.SearchFriendsRequest; import site.timecapsulearchive.core.domain.friend.data.request.SendFriendRequest; -import site.timecapsulearchive.core.domain.friend.data.response.FriendReqStatusResponse; import site.timecapsulearchive.core.domain.friend.data.response.FriendRequestsSliceResponse; import site.timecapsulearchive.core.domain.friend.data.response.FriendsSliceResponse; import site.timecapsulearchive.core.domain.friend.data.response.SearchFriendsResponse; @@ -89,7 +88,9 @@ public ResponseEntity> deleteFriend( friendService.deleteFriend(memberId, friendId); return ResponseEntity.ok( - ApiSpec.empty(SuccessCode.SUCCESS) + ApiSpec.empty( + SuccessCode.SUCCESS + ) ); } @@ -101,23 +102,26 @@ public ResponseEntity> denyFriendRequest( friendService.denyRequestFriend(memberId, friendId); - return ResponseEntity.ok(ApiSpec.empty(SuccessCode.SUCCESS)); - + return ResponseEntity.ok( + ApiSpec.empty( + SuccessCode.SUCCESS + ) + ); } @PostMapping(value = "/{friend_id}/request") @Override - public ResponseEntity> requestFriend( + public ResponseEntity> requestFriend( @AuthenticationPrincipal final Long memberId, @PathVariable("friend_id") final Long friendId) { - return ResponseEntity.accepted() - .body( - ApiSpec.success( - SuccessCode.SUCCESS, - friendService.requestFriend(memberId, friendId) - ) - ); + friendService.requestFriend(memberId, friendId); + + return ResponseEntity.ok( + ApiSpec.empty( + SuccessCode.ACCEPTED + ) + ); } @PostMapping(value = "/requests") @@ -128,8 +132,11 @@ public ResponseEntity> requestFriends( ) { friendFacade.requestFriends(memberId, request.friendIds()); - return ResponseEntity.accepted() - .body(ApiSpec.empty(SuccessCode.ACCEPTED)); + return ResponseEntity.ok( + ApiSpec.empty( + SuccessCode.ACCEPTED + ) + ); } @PostMapping(value = "/{friend_id}/accept-request") @@ -140,7 +147,11 @@ public ResponseEntity> acceptFriendRequest( ) { friendService.acceptFriend(memberId, friendId); - return ResponseEntity.ok(ApiSpec.empty(SuccessCode.SUCCESS)); + return ResponseEntity.ok( + ApiSpec.empty( + SuccessCode.SUCCESS + ) + ); } @PostMapping( @@ -173,12 +184,11 @@ public ResponseEntity> searchFriendByTag @AuthenticationPrincipal Long memberId, @RequestParam(value = "friend_tag") final String tag ) { - return ResponseEntity.ok() - .body( - ApiSpec.success( - SuccessCode.SUCCESS, - friendService.searchFriend(memberId, tag) - ) - ); + return ResponseEntity.ok( + ApiSpec.success( + SuccessCode.SUCCESS, + friendService.searchFriend(memberId, tag) + ) + ); } } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/data/response/FriendReqStatusResponse.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/data/response/FriendReqStatusResponse.java deleted file mode 100644 index 307da6a73..000000000 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/data/response/FriendReqStatusResponse.java +++ /dev/null @@ -1,13 +0,0 @@ -package site.timecapsulearchive.core.domain.friend.data.response; - -import org.springframework.http.HttpStatus; - -public record FriendReqStatusResponse( - HttpStatus httpStatus, - String result -) { - - public static FriendReqStatusResponse success() { - return new FriendReqStatusResponse(HttpStatus.ACCEPTED, "친구 요청 메시지 전송 성공!"); - } -} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/repository/FriendInviteQueryRepository.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/repository/FriendInviteQueryRepository.java index 640c2077d..3e6859eea 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/repository/FriendInviteQueryRepository.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/repository/FriendInviteQueryRepository.java @@ -2,7 +2,9 @@ import java.sql.PreparedStatement; import java.sql.SQLException; +import java.sql.Timestamp; import java.sql.Types; +import java.time.ZonedDateTime; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.jdbc.core.BatchPreparedStatementSetter; @@ -19,8 +21,8 @@ public void bulkSave(final Long ownerId, final List friendIds) { jdbcTemplate.batchUpdate( """ INSERT INTO friend_invite ( - friend_invite_id, owner_id, friend_id - ) values (?, ?, ?) + friend_invite_id, owner_id, friend_id, created_at, updated_at + ) values (?, ?, ?, ?, ?) """, new BatchPreparedStatementSetter() { @@ -30,6 +32,8 @@ public void setValues(final PreparedStatement ps, final int i) throws SQLExcepti ps.setNull(1, Types.BIGINT); ps.setLong(2, ownerId); ps.setLong(3, friendId); + ps.setTimestamp(4, Timestamp.valueOf(ZonedDateTime.now().toLocalDateTime())); + ps.setTimestamp(5, Timestamp.valueOf(ZonedDateTime.now().toLocalDateTime())); } @Override diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/repository/FriendInviteRepository.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/repository/FriendInviteRepository.java index c7c94a40a..df8643a77 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/repository/FriendInviteRepository.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/repository/FriendInviteRepository.java @@ -24,7 +24,7 @@ List findFriendInviteWithMembersByOwnerIdAndFriendId( Optional findFriendInviteByOwnerIdAndFriendId(Long memberId, Long targetId); - void deleteFriendInviteById(Long id); + int deleteFriendInviteByOwnerIdAndFriendId(Long memberId, Long targetId); void delete(FriendInvite friendInvite); } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/service/FriendService.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/service/FriendService.java index 913700af2..68fb8cf6f 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/service/FriendService.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/service/FriendService.java @@ -13,7 +13,6 @@ import site.timecapsulearchive.core.domain.friend.data.dto.FriendSummaryDto; import site.timecapsulearchive.core.domain.friend.data.dto.SearchFriendSummaryDto; import site.timecapsulearchive.core.domain.friend.data.dto.SearchFriendSummaryDtoByTag; -import site.timecapsulearchive.core.domain.friend.data.response.FriendReqStatusResponse; import site.timecapsulearchive.core.domain.friend.data.response.SearchTagFriendSummaryResponse; import site.timecapsulearchive.core.domain.friend.entity.FriendInvite; import site.timecapsulearchive.core.domain.friend.entity.MemberFriend; @@ -43,7 +42,7 @@ public class FriendService { private final SocialNotificationManager socialNotificationManager; private final TransactionTemplate transactionTemplate; - public FriendReqStatusResponse requestFriend(final Long memberId, final Long friendId) { + public void requestFriend(final Long memberId, final Long friendId) { validateFriendDuplicateId(memberId, friendId); validateTwoWayInvite(memberId, friendId); @@ -63,8 +62,6 @@ protected void doInTransactionWithoutResult(TransactionStatus status) { }); socialNotificationManager.sendFriendReqMessage(owner.getNickname(), friendId); - - return FriendReqStatusResponse.success(); } private void validateFriendDuplicateId(final Long memberId, final Long friendId) { @@ -114,20 +111,22 @@ protected void doInTransactionWithoutResult(TransactionStatus status) { } @Transactional - public void denyRequestFriend(Long memberId, Long friendId) { + public void denyRequestFriend(final Long memberId, final Long friendId) { validateFriendDuplicateId(memberId, friendId); - final FriendInvite friendInvite = friendInviteRepository - .findFriendInviteByOwnerIdAndFriendId(memberId, friendId).orElseThrow( - FriendNotFoundException::new); - friendInviteRepository.deleteFriendInviteById(friendInvite.getId()); + int isDenyRequest = friendInviteRepository.deleteFriendInviteByOwnerIdAndFriendId(friendId, + memberId); + + if (isDenyRequest != 1) { + throw new FriendTwoWayInviteException(); + } } @Transactional public void deleteFriend(final Long memberId, final Long friendId) { validateFriendDuplicateId(memberId, friendId); - List memberFriends = memberFriendRepository + final List memberFriends = memberFriendRepository .findMemberFriendByOwnerIdAndFriendId(memberId, friendId); memberFriends.forEach(memberFriendRepository::delete); diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/api/GroupApi.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/api/GroupApi.java index d90e76ef5..7f79e682a 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/api/GroupApi.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/api/GroupApi.java @@ -13,7 +13,6 @@ import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; import site.timecapsulearchive.core.domain.group.data.reqeust.GroupCreateRequest; import site.timecapsulearchive.core.domain.group.data.reqeust.GroupUpdateRequest; import site.timecapsulearchive.core.domain.group.data.response.GroupDetailResponse; @@ -32,15 +31,24 @@ public interface GroupApi { @ApiResponse( responseCode = "200", description = "처리 완료" + ), + @ApiResponse( + responseCode = "404", + description = "그룹 초대 찾기 실패" + ), + @ApiResponse( + responseCode = "500", + description = "외부 API 요청 실패" ) }) - @PostMapping(value = "/groups/{group_id}/members/{member_id}/accept-invitation") - ResponseEntity acceptGroupInvitation( - @Parameter(in = ParameterIn.PATH, description = "그룹 아이디", required = true, schema = @Schema()) - @PathVariable("group_id") Long groupId, + ResponseEntity> acceptGroupInvitation( + Long memberId, - @Parameter(in = ParameterIn.PATH, description = "대상 회원 아이디", required = true, schema = @Schema()) - @PathVariable("member_id") Long memberId + @Parameter(in = ParameterIn.PATH, description = "그룹 아이디", required = true) + Long groupId, + + @Parameter(in = ParameterIn.PATH, description = "대상 회원 아이디", required = true) + Long targetId ); @Operation( @@ -53,6 +61,10 @@ ResponseEntity acceptGroupInvitation( @ApiResponse( responseCode = "200", description = "처리완료" + ), + @ApiResponse( + responseCode = "500", + description = "외부 API 요청 실패" ) }) ResponseEntity> createGroup( @@ -111,13 +123,13 @@ ResponseEntity deleteGroupMember( description = "처리 완료" ) }) - @PostMapping(value = "/groups/{group_id}/members/{member_id}/deny-invitation") - ResponseEntity denyGroupInvitation( - @Parameter(in = ParameterIn.PATH, description = "그룹 아이디", required = true, schema = @Schema()) - @PathVariable("group_id") Long groupId, + ResponseEntity> rejectGroupInvitation( + Long memberId, - @Parameter(in = ParameterIn.PATH, description = "대상 회원 아이디", required = true, schema = @Schema()) - @PathVariable("member_id") Long memberId + Long groupId, + + @Parameter(in = ParameterIn.PATH, description = "그룹 초대 대상 아이디", required = true) + Long groupOwnerId ); @Operation( @@ -181,13 +193,14 @@ ResponseEntity> findGroups( description = "처리 시작" ) }) - @PostMapping(value = "/groups/{group_id}/members/{member_id}/invitation") - ResponseEntity inviteGroup( - @Parameter(in = ParameterIn.PATH, description = "그룹 아이디", required = true, schema = @Schema()) - @PathVariable("group_id") Long groupId, + ResponseEntity> inviteGroup( + Long memberId, - @Parameter(in = ParameterIn.PATH, description = "대상 회원 아이디", required = true, schema = @Schema()) - @PathVariable("member_id") Long memberId + @Parameter(in = ParameterIn.PATH, description = "그룹 아이디", required = true) + Long groupId, + + @Parameter(in = ParameterIn.PATH, description = "대상 회원 아이디", required = true) + Long targetId ); @Operation( diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/api/GroupApiController.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/api/GroupApiController.java index e8a067e35..3efb49539 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/api/GroupApiController.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/api/GroupApiController.java @@ -6,6 +6,7 @@ import org.springframework.data.domain.Slice; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -36,9 +37,20 @@ public class GroupApiController implements GroupApi { private final S3UrlGenerator s3UrlGenerator; private final S3PreSignedUrlManager s3PreSignedUrlManager; + @PostMapping(value = "/accept/{group_id}/member/{target_id}") @Override - public ResponseEntity acceptGroupInvitation(Long groupId, Long memberId) { - return null; + public ResponseEntity> acceptGroupInvitation( + @AuthenticationPrincipal final Long memberId, + @PathVariable("group_id") final Long groupId, + @PathVariable("target_id") final Long targetId + ) { + groupService.acceptGroupInvite(memberId, groupId, targetId); + + return ResponseEntity.ok( + ApiSpec.empty( + SuccessCode.ACCEPTED + ) + ); } @Override @@ -55,7 +67,7 @@ public ResponseEntity> createGroup( return ResponseEntity.ok( ApiSpec.empty( - SuccessCode.SUCCESS + SuccessCode.ACCEPTED ) ); } @@ -70,9 +82,19 @@ public ResponseEntity deleteGroupMember(Long groupId, Long memberId) { return null; } - @Override - public ResponseEntity denyGroupInvitation(Long groupId, Long memberId) { - return null; + @DeleteMapping(value = "/reject/{group_id}/member/{target_id}") + public ResponseEntity> rejectGroupInvitation( + @AuthenticationPrincipal final Long memberId, + @PathVariable("group_id") final Long groupId, + @PathVariable("target_id") final Long targetId) { + + groupService.rejectRequestGroup(memberId, groupId, targetId); + + return ResponseEntity.ok( + ApiSpec.empty( + SuccessCode.SUCCESS + ) + ); } @GetMapping( @@ -84,7 +106,8 @@ public ResponseEntity> findGroupDetailById( @AuthenticationPrincipal final Long memberId, @PathVariable("group_id") final Long groupId ) { - final GroupDetailDto groupDetailDto = groupService.findGroupDetailByGroupId(memberId, groupId); + final GroupDetailDto groupDetailDto = groupService.findGroupDetailByGroupId(memberId, + groupId); return ResponseEntity.ok( ApiSpec.success( @@ -118,9 +141,20 @@ public ResponseEntity> findGroups( ); } + @PostMapping(value = "/invite/{group_id}/member/{target_id}") @Override - public ResponseEntity inviteGroup(Long groupId, Long memberId) { - return null; + public ResponseEntity> inviteGroup( + @AuthenticationPrincipal final Long memberId, + @PathVariable("group_id") final Long groupId, + @PathVariable("target_id") final Long targetId + ) { + groupService.inviteGroup(memberId, groupId, targetId); + + return ResponseEntity.ok( + ApiSpec.empty( + SuccessCode.ACCEPTED + ) + ); } @Override diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/dto/GroupDetailDto.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/dto/GroupDetailDto.java index c45a8a46e..d97da178e 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/dto/GroupDetailDto.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/dto/GroupDetailDto.java @@ -5,7 +5,6 @@ import java.util.function.Function; import site.timecapsulearchive.core.domain.group.data.response.GroupDetailResponse; import site.timecapsulearchive.core.domain.group.data.response.GroupMemberResponse; -import site.timecapsulearchive.core.domain.group.data.response.GroupMemberSummaryResponse; public record GroupDetailDto( String groupName, diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/dto/GroupOwnerSummaryDto.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/dto/GroupOwnerSummaryDto.java new file mode 100644 index 000000000..3496d3ec8 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/dto/GroupOwnerSummaryDto.java @@ -0,0 +1,9 @@ +package site.timecapsulearchive.core.domain.group.data.dto; + +public record GroupOwnerSummaryDto( + String nickname, + Boolean isOwner, + String groupProfileUrl +) { + +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/entity/GroupInvite.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/entity/GroupInvite.java index 41c184b35..690014c01 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/entity/GroupInvite.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/entity/GroupInvite.java @@ -26,8 +26,25 @@ public class GroupInvite extends BaseEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Column(name = "group_id", nullable = false) + private Long groupId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "group_owner_id", nullable = false) + private Member groupOwner; + @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "member_id", nullable = false) - private Member member; + @JoinColumn(name = "group_member_id", nullable = false) + private Member groupMember; + + private GroupInvite(Long groupId, Member groupOwner, Member groupMember) { + this.groupId = groupId; + this.groupOwner = groupOwner; + this.groupMember = groupMember; + } + + public static GroupInvite createOf(Long groupId, Member groupOwner, Member groupMember) { + return new GroupInvite(groupId, groupOwner, groupMember); + } } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/entity/MemberGroup.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/entity/MemberGroup.java index d9c4f5192..67c37404c 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/entity/MemberGroup.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/entity/MemberGroup.java @@ -46,4 +46,8 @@ private MemberGroup(Boolean isOwner, Member member, Group group) { public static MemberGroup createGroupOwner(Member member, Group group) { return new MemberGroup(true, member, group); } + + public static MemberGroup createGroupMember(Member member, Group group) { + return new MemberGroup(false, member, group); + } } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/exception/GroupInviteNotFoundException.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/exception/GroupInviteNotFoundException.java new file mode 100644 index 000000000..39ad8bb61 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/exception/GroupInviteNotFoundException.java @@ -0,0 +1,11 @@ +package site.timecapsulearchive.core.domain.group.exception; + +import site.timecapsulearchive.core.global.error.ErrorCode; +import site.timecapsulearchive.core.global.error.exception.BusinessException; + +public class GroupInviteNotFoundException extends BusinessException { + + public GroupInviteNotFoundException() { + super(ErrorCode.GROUP_INVITATION_NOT_FOUND_ERROR); + } +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/exception/GroupOwnerAuthenticateException.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/exception/GroupOwnerAuthenticateException.java new file mode 100644 index 000000000..2c6382b85 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/exception/GroupOwnerAuthenticateException.java @@ -0,0 +1,11 @@ +package site.timecapsulearchive.core.domain.group.exception; + +import site.timecapsulearchive.core.global.error.ErrorCode; +import site.timecapsulearchive.core.global.error.exception.BusinessException; + +public class GroupOwnerAuthenticateException extends BusinessException { + + public GroupOwnerAuthenticateException() { + super(ErrorCode.GROUP_OWNER_AUTHENTICATE_ERROR); + } +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/GroupInviteRepository.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/GroupInviteRepository.java deleted file mode 100644 index edd27e243..000000000 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/GroupInviteRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package site.timecapsulearchive.core.domain.group.repository; - -import org.springframework.data.jpa.repository.JpaRepository; -import site.timecapsulearchive.core.domain.group.entity.GroupInvite; - -public interface GroupInviteRepository extends JpaRepository { - -} - diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/groupInviteRepository/GroupInviteQueryRepository.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/groupInviteRepository/GroupInviteQueryRepository.java new file mode 100644 index 000000000..757acc930 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/groupInviteRepository/GroupInviteQueryRepository.java @@ -0,0 +1,8 @@ +package site.timecapsulearchive.core.domain.group.repository.groupInviteRepository; + +import java.util.List; + +public interface GroupInviteQueryRepository { + + void bulkSave(final Long groupOwnerId, final List groupMemberIds); +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/groupInviteRepository/GroupInviteQueryRepositoryImpl.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/groupInviteRepository/GroupInviteQueryRepositoryImpl.java new file mode 100644 index 000000000..8d71332c7 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/groupInviteRepository/GroupInviteQueryRepositoryImpl.java @@ -0,0 +1,48 @@ +package site.timecapsulearchive.core.domain.group.repository.groupInviteRepository; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.sql.Types; +import java.time.ZonedDateTime; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.jdbc.core.BatchPreparedStatementSetter; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class GroupInviteQueryRepositoryImpl implements GroupInviteQueryRepository { + + private final JdbcTemplate jdbcTemplate; + + @Override + public void bulkSave(Long groupOwnerId, List groupMemberIds) { + jdbcTemplate.batchUpdate( + """ + INSERT INTO group_invite ( + group_invite_id, group_owner_id, group_member_id, created_at, updated_at + ) values (?, ?, ?, ?, ?) + """, + new BatchPreparedStatementSetter() { + + @Override + public void setValues(final PreparedStatement ps, final int i) throws SQLException { + final Long groupMember = groupMemberIds.get(i); + ps.setNull(1, Types.BIGINT); + ps.setLong(2, groupOwnerId); + ps.setLong(3, groupMember); + ps.setTimestamp(4, Timestamp.valueOf(ZonedDateTime.now().toLocalDateTime())); + ps.setTimestamp(5, Timestamp.valueOf(ZonedDateTime.now().toLocalDateTime())); + } + + @Override + public int getBatchSize() { + return groupMemberIds.size(); + } + } + ); + } + +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/groupInviteRepository/GroupInviteRepository.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/groupInviteRepository/GroupInviteRepository.java new file mode 100644 index 000000000..1331805dd --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/groupInviteRepository/GroupInviteRepository.java @@ -0,0 +1,14 @@ +package site.timecapsulearchive.core.domain.group.repository.groupInviteRepository; + +import org.springframework.data.repository.Repository; +import site.timecapsulearchive.core.domain.group.entity.GroupInvite; + +public interface GroupInviteRepository extends Repository, + GroupInviteQueryRepository { + + void save(GroupInvite groupInvite); + + int deleteGroupInviteByGroupIdAndGroupOwnerIdAndGroupMemberId(Long groupId, Long groupOwnerId, + Long groupMemberId); +} + diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/groupRepository/GroupQueryRepository.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/groupRepository/GroupQueryRepository.java new file mode 100644 index 000000000..0ce6327a9 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/groupRepository/GroupQueryRepository.java @@ -0,0 +1,20 @@ +package site.timecapsulearchive.core.domain.group.repository.groupRepository; + +import java.time.ZonedDateTime; +import java.util.Optional; +import org.springframework.data.domain.Slice; +import site.timecapsulearchive.core.domain.group.data.dto.GroupDetailDto; +import site.timecapsulearchive.core.domain.group.data.dto.GroupSummaryDto; + + +public interface GroupQueryRepository { + + Slice findGroupsSlice( + final Long memberId, + final int size, + final ZonedDateTime createdAt + ); + + Optional findGroupDetailByGroupId(final Long groupId); + +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/GroupQueryRepository.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/groupRepository/GroupQueryRepositoryImpl.java similarity index 95% rename from backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/GroupQueryRepository.java rename to backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/groupRepository/GroupQueryRepositoryImpl.java index 9b7e5a201..21ff3e10b 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/GroupQueryRepository.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/groupRepository/GroupQueryRepositoryImpl.java @@ -1,4 +1,4 @@ -package site.timecapsulearchive.core.domain.group.repository; +package site.timecapsulearchive.core.domain.group.repository.groupRepository; import static com.querydsl.core.group.GroupBy.groupBy; import static com.querydsl.core.group.GroupBy.list; @@ -22,7 +22,7 @@ @Repository @RequiredArgsConstructor -public class GroupQueryRepository { +public class GroupQueryRepositoryImpl implements GroupQueryRepository { private final JPAQueryFactory jpaQueryFactory; diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/GroupRepository.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/groupRepository/GroupRepository.java similarity index 73% rename from backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/GroupRepository.java rename to backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/groupRepository/GroupRepository.java index 383258106..24eb4c0c7 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/GroupRepository.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/groupRepository/GroupRepository.java @@ -1,10 +1,10 @@ -package site.timecapsulearchive.core.domain.group.repository; +package site.timecapsulearchive.core.domain.group.repository.groupRepository; import java.util.Optional; import org.springframework.data.repository.Repository; import site.timecapsulearchive.core.domain.group.entity.Group; -public interface GroupRepository extends Repository { +public interface GroupRepository extends Repository, GroupQueryRepository { void save(Group group); diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/memberGroupRepository/MemberGroupQueryRepository.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/memberGroupRepository/MemberGroupQueryRepository.java new file mode 100644 index 000000000..c39ef58b2 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/memberGroupRepository/MemberGroupQueryRepository.java @@ -0,0 +1,10 @@ +package site.timecapsulearchive.core.domain.group.repository.memberGroupRepository; + +import java.util.Optional; +import site.timecapsulearchive.core.domain.group.data.dto.GroupOwnerSummaryDto; + +public interface MemberGroupQueryRepository { + + Optional findOwnerInMemberGroup(Long groupId, Long memberId); + +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/memberGroupRepository/MemberGroupQueryRepositoryImpl.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/memberGroupRepository/MemberGroupQueryRepositoryImpl.java new file mode 100644 index 000000000..6e240515a --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/memberGroupRepository/MemberGroupQueryRepositoryImpl.java @@ -0,0 +1,40 @@ +package site.timecapsulearchive.core.domain.group.repository.memberGroupRepository; + +import static site.timecapsulearchive.core.domain.group.entity.QGroup.group; +import static site.timecapsulearchive.core.domain.group.entity.QMemberGroup.memberGroup; +import static site.timecapsulearchive.core.domain.member.entity.QMember.member; + +import com.querydsl.core.types.Projections; +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; +import site.timecapsulearchive.core.domain.group.data.dto.GroupOwnerSummaryDto; + +@Repository +@RequiredArgsConstructor +public class MemberGroupQueryRepositoryImpl implements MemberGroupQueryRepository { + + private final JPAQueryFactory jpaQueryFactory; + + @Override + public Optional findOwnerInMemberGroup(final Long groupId, + final Long memberId) { + return Optional.ofNullable(jpaQueryFactory + .select( + Projections.constructor( + GroupOwnerSummaryDto.class, + member.nickname, + memberGroup.isOwner, + group.groupProfileUrl + ) + ) + .from(memberGroup) + .join(memberGroup.member, member) + .join(memberGroup.group, group) + .where(memberGroup.group.id.eq(groupId) + .and(memberGroup.member.id.eq(memberId))) + .fetchFirst() + ); + } +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/MemberGroupRepository.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/memberGroupRepository/MemberGroupRepository.java similarity index 64% rename from backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/MemberGroupRepository.java rename to backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/memberGroupRepository/MemberGroupRepository.java index b5e46ade4..d5ded67da 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/MemberGroupRepository.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/memberGroupRepository/MemberGroupRepository.java @@ -1,9 +1,10 @@ -package site.timecapsulearchive.core.domain.group.repository; +package site.timecapsulearchive.core.domain.group.repository.memberGroupRepository; import org.springframework.data.repository.Repository; import site.timecapsulearchive.core.domain.group.entity.MemberGroup; -public interface MemberGroupRepository extends Repository { +public interface MemberGroupRepository extends Repository, + MemberGroupQueryRepository { void save(MemberGroup memberGroup); } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/service/GroupService.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/service/GroupService.java index 98b3b1603..5fa63bedf 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/service/GroupService.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/service/GroupService.java @@ -1,87 +1,8 @@ package site.timecapsulearchive.core.domain.group.service; -import lombok.RequiredArgsConstructor; -import java.time.ZonedDateTime; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Slice; -import org.springframework.stereotype.Service; -import org.springframework.transaction.TransactionStatus; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.transaction.support.TransactionCallbackWithoutResult; -import org.springframework.transaction.support.TransactionTemplate; -import site.timecapsulearchive.core.domain.group.data.dto.GroupCreateDto; -import site.timecapsulearchive.core.domain.group.data.dto.GroupDetailDto; -import site.timecapsulearchive.core.domain.group.data.dto.GroupSummaryDto; -import site.timecapsulearchive.core.domain.group.entity.Group; -import site.timecapsulearchive.core.domain.group.entity.MemberGroup; -import site.timecapsulearchive.core.domain.group.exception.GroupNotFoundException; -import site.timecapsulearchive.core.domain.group.repository.GroupQueryRepository; -import site.timecapsulearchive.core.domain.group.repository.GroupRepository; -import site.timecapsulearchive.core.domain.group.repository.MemberGroupRepository; -import site.timecapsulearchive.core.domain.member.entity.Member; -import site.timecapsulearchive.core.domain.member.exception.MemberNotFoundException; -import site.timecapsulearchive.core.domain.member.repository.MemberRepository; -import site.timecapsulearchive.core.infra.queue.manager.SocialNotificationManager; +import site.timecapsulearchive.core.domain.group.service.read.GroupReadService; +import site.timecapsulearchive.core.domain.group.service.write.GroupWriteService; -@Service -@RequiredArgsConstructor -public class GroupService { +public interface GroupService extends GroupReadService, GroupWriteService { - private final GroupRepository groupRepository; - private final MemberRepository memberRepository; - private final MemberGroupRepository memberGroupRepository; - private final TransactionTemplate transactionTemplate; - private final SocialNotificationManager socialNotificationManager; - private final GroupQueryRepository groupQueryRepository; - - public void createGroup(final Long memberId, final GroupCreateDto dto) { - final Member member = memberRepository.findMemberById(memberId) - .orElseThrow(MemberNotFoundException::new); - - final Group group = dto.toEntity(); - - final MemberGroup memberGroup = MemberGroup.createGroupOwner(member, group); - - transactionTemplate.execute(new TransactionCallbackWithoutResult() { - @Override - protected void doInTransactionWithoutResult(TransactionStatus status) { - groupRepository.save(group); - memberGroupRepository.save(memberGroup); - } - }); - - socialNotificationManager.sendGroupInviteMessage(member.getNickname(), - dto.groupProfileUrl(), dto.targetIds()); - } - - @Transactional(readOnly = true) - public Group findGroupById(Long groupId) { - return groupRepository.findGroupById(groupId) - .orElseThrow(GroupNotFoundException::new); - } - - @Transactional(readOnly = true) - public Slice findGroupsSlice( - final Long memberId, - final int size, - final ZonedDateTime createdAt - ) { - return groupQueryRepository.findGroupsSlice(memberId, size, createdAt); - } - - @Transactional(readOnly = true) - public GroupDetailDto findGroupDetailByGroupId(final Long memberId, final Long groupId) { - final GroupDetailDto groupDetailDto = groupQueryRepository.findGroupDetailByGroupId(groupId) - .orElseThrow(GroupNotFoundException::new); - - final boolean isGroupMember = groupDetailDto.members() - .stream() - .anyMatch(m -> m.memberId().equals(memberId)); - - if (!isGroupMember) { - throw new GroupNotFoundException(); - } - - return groupDetailDto; - } } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/service/GroupServiceImpl.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/service/GroupServiceImpl.java new file mode 100644 index 000000000..b1252d0ea --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/service/GroupServiceImpl.java @@ -0,0 +1,60 @@ +package site.timecapsulearchive.core.domain.group.service; + +import java.time.ZonedDateTime; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Slice; +import org.springframework.stereotype.Service; +import site.timecapsulearchive.core.domain.group.data.dto.GroupCreateDto; +import site.timecapsulearchive.core.domain.group.data.dto.GroupDetailDto; +import site.timecapsulearchive.core.domain.group.data.dto.GroupSummaryDto; +import site.timecapsulearchive.core.domain.group.entity.Group; +import site.timecapsulearchive.core.domain.group.service.read.GroupReadService; +import site.timecapsulearchive.core.domain.group.service.write.GroupWriteService; + +@Service +@RequiredArgsConstructor +public class GroupServiceImpl implements GroupService { + + private final GroupReadService groupReadService; + private final GroupWriteService groupWriteService; + + @Override + public Group findGroupById(final Long groupId) { + return groupReadService.findGroupById(groupId); + } + + @Override + public Slice findGroupsSlice( + final Long memberId, + final int size, + final ZonedDateTime createdAt + ) { + return groupReadService.findGroupsSlice(memberId, size, createdAt); + } + + @Override + public GroupDetailDto findGroupDetailByGroupId(final Long memberId, final Long groupId) { + return groupReadService.findGroupDetailByGroupId(memberId, groupId); + } + + @Override + public void createGroup(final Long memberId, final GroupCreateDto dto) { + groupWriteService.createGroup(memberId, dto); + } + + @Override + public void inviteGroup(final Long memberId, final Long groupId, final Long targetId) { + groupWriteService.inviteGroup(memberId, groupId, targetId); + } + + @Override + public void rejectRequestGroup(final Long groupMemberId, final Long groupId, + final Long targetId) { + groupWriteService.rejectRequestGroup(groupMemberId, groupId, targetId); + } + + @Override + public void acceptGroupInvite(final Long memberId, final Long groupId, final Long targetId) { + groupWriteService.acceptGroupInvite(memberId, groupId, targetId); + } +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/service/read/GroupReadService.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/service/read/GroupReadService.java new file mode 100644 index 000000000..635b1dbb1 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/service/read/GroupReadService.java @@ -0,0 +1,20 @@ +package site.timecapsulearchive.core.domain.group.service.read; + +import java.time.ZonedDateTime; +import org.springframework.data.domain.Slice; +import site.timecapsulearchive.core.domain.group.data.dto.GroupDetailDto; +import site.timecapsulearchive.core.domain.group.data.dto.GroupSummaryDto; +import site.timecapsulearchive.core.domain.group.entity.Group; + +public interface GroupReadService { + + Group findGroupById(final Long groupId); + + Slice findGroupsSlice( + final Long memberId, + final int size, + final ZonedDateTime createdAt + ); + + GroupDetailDto findGroupDetailByGroupId(final Long memberId, final Long groupId); +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/service/read/GroupReadServiceImpl.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/service/read/GroupReadServiceImpl.java new file mode 100644 index 000000000..d3066d287 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/service/read/GroupReadServiceImpl.java @@ -0,0 +1,48 @@ +package site.timecapsulearchive.core.domain.group.service.read; + +import java.time.ZonedDateTime; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Slice; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import site.timecapsulearchive.core.domain.group.data.dto.GroupDetailDto; +import site.timecapsulearchive.core.domain.group.data.dto.GroupSummaryDto; +import site.timecapsulearchive.core.domain.group.entity.Group; +import site.timecapsulearchive.core.domain.group.exception.GroupNotFoundException; +import site.timecapsulearchive.core.domain.group.repository.groupRepository.GroupRepository; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class GroupReadServiceImpl implements GroupReadService { + + private final GroupRepository groupRepository; + + public Group findGroupById(final Long groupId) { + return groupRepository.findGroupById(groupId) + .orElseThrow(GroupNotFoundException::new); + } + + public Slice findGroupsSlice( + final Long memberId, + final int size, + final ZonedDateTime createdAt + ) { + return groupRepository.findGroupsSlice(memberId, size, createdAt); + } + + public GroupDetailDto findGroupDetailByGroupId(final Long memberId, final Long groupId) { + final GroupDetailDto groupDetailDto = groupRepository.findGroupDetailByGroupId(groupId) + .orElseThrow(GroupNotFoundException::new); + + final boolean isGroupMember = groupDetailDto.members() + .stream() + .anyMatch(m -> m.memberId().equals(memberId)); + + if (!isGroupMember) { + throw new GroupNotFoundException(); + } + + return groupDetailDto; + } +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/service/write/GroupWriteService.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/service/write/GroupWriteService.java new file mode 100644 index 000000000..6eb08e6f6 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/service/write/GroupWriteService.java @@ -0,0 +1,15 @@ +package site.timecapsulearchive.core.domain.group.service.write; + +import site.timecapsulearchive.core.domain.group.data.dto.GroupCreateDto; + +public interface GroupWriteService { + + void createGroup(final Long memberId, final GroupCreateDto dto); + + void inviteGroup(final Long memberId, final Long groupId, final Long targetId); + + void rejectRequestGroup(final Long memberId, final Long groupId, final Long targetId); + + void acceptGroupInvite(final Long memberId, final Long groupId, final Long targetId); + +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/service/write/GroupWriteServiceImpl.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/service/write/GroupWriteServiceImpl.java new file mode 100644 index 000000000..b2065bee5 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/service/write/GroupWriteServiceImpl.java @@ -0,0 +1,121 @@ +package site.timecapsulearchive.core.domain.group.service.write; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionCallbackWithoutResult; +import org.springframework.transaction.support.TransactionTemplate; +import site.timecapsulearchive.core.domain.group.data.dto.GroupCreateDto; +import site.timecapsulearchive.core.domain.group.data.dto.GroupOwnerSummaryDto; +import site.timecapsulearchive.core.domain.group.entity.Group; +import site.timecapsulearchive.core.domain.group.entity.GroupInvite; +import site.timecapsulearchive.core.domain.group.entity.MemberGroup; +import site.timecapsulearchive.core.domain.group.exception.GroupInviteNotFoundException; +import site.timecapsulearchive.core.domain.group.exception.GroupNotFoundException; +import site.timecapsulearchive.core.domain.group.exception.GroupOwnerAuthenticateException; +import site.timecapsulearchive.core.domain.group.repository.groupInviteRepository.GroupInviteRepository; +import site.timecapsulearchive.core.domain.group.repository.groupRepository.GroupRepository; +import site.timecapsulearchive.core.domain.group.repository.memberGroupRepository.MemberGroupRepository; +import site.timecapsulearchive.core.domain.member.entity.Member; +import site.timecapsulearchive.core.domain.member.exception.MemberNotFoundException; +import site.timecapsulearchive.core.domain.member.repository.MemberRepository; +import site.timecapsulearchive.core.infra.queue.manager.SocialNotificationManager; + +@Service +@RequiredArgsConstructor +public class GroupWriteServiceImpl implements GroupWriteService { + + private final MemberRepository memberRepository; + private final GroupRepository groupRepository; + private final MemberGroupRepository memberGroupRepository; + private final GroupInviteRepository groupInviteRepository; + private final TransactionTemplate transactionTemplate; + private final SocialNotificationManager socialNotificationManager; + + public void createGroup(final Long memberId, final GroupCreateDto dto) { + final Member member = memberRepository.findMemberById(memberId) + .orElseThrow(MemberNotFoundException::new); + + final Group group = dto.toEntity(); + + final MemberGroup memberGroup = MemberGroup.createGroupOwner(member, group); + + transactionTemplate.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + groupRepository.save(group); + memberGroupRepository.save(memberGroup); + groupInviteRepository.bulkSave(memberId, dto.targetIds()); + } + }); + + socialNotificationManager.sendGroupInviteMessage(member.getNickname(), + dto.groupProfileUrl(), dto.targetIds()); + } + + public void inviteGroup(final Long memberId, final Long groupId, final Long targetId) { + final Member groupOwner = memberRepository.findMemberById(memberId).orElseThrow( + MemberNotFoundException::new); + + final Member groupMember = memberRepository.findMemberById(targetId).orElseThrow( + MemberNotFoundException::new); + + final GroupInvite groupInvite = GroupInvite.createOf(groupId, groupOwner, groupMember); + + final GroupOwnerSummaryDto[] summaryDto = new GroupOwnerSummaryDto[1]; + + transactionTemplate.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + summaryDto[0] = memberGroupRepository.findOwnerInMemberGroup( + groupId, memberId).orElseThrow(GroupNotFoundException::new); + + if (!summaryDto[0].isOwner()) { + throw new GroupOwnerAuthenticateException(); + } + + groupInviteRepository.save(groupInvite); + } + }); + + socialNotificationManager.sendGroupInviteMessage(summaryDto[0].nickname(), + summaryDto[0].groupProfileUrl(), List.of(targetId)); + } + + @Transactional + public void rejectRequestGroup(final Long memberId, final Long groupId, final Long targetId) { + final int isDenyRequest = groupInviteRepository.deleteGroupInviteByGroupIdAndGroupOwnerIdAndGroupMemberId( + groupId, targetId, memberId); + + if (isDenyRequest != 1) { + throw new GroupInviteNotFoundException(); + } + } + + public void acceptGroupInvite(final Long memberId, final Long groupId, final Long targetId) { + final Member groupMember = memberRepository.findMemberById(memberId) + .orElseThrow(MemberNotFoundException::new); + final Group group = groupRepository.findGroupById(groupId) + .orElseThrow(GroupNotFoundException::new); + + transactionTemplate.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + final int isDenyRequest = groupInviteRepository.deleteGroupInviteByGroupIdAndGroupOwnerIdAndGroupMemberId( + groupId, targetId, memberId); + + if (isDenyRequest != 1) { + throw new GroupInviteNotFoundException(); + } + + memberGroupRepository.save(MemberGroup.createGroupMember(groupMember, group)); + } + }); + + socialNotificationManager.sendGroupAcceptMessage(groupMember.getNickname(), targetId); + } + + +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/member/entity/Member.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/member/entity/Member.java index 730084363..a99bd8fff 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/member/entity/Member.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/member/entity/Member.java @@ -19,7 +19,6 @@ import site.timecapsulearchive.core.domain.capsule.entity.Capsule; import site.timecapsulearchive.core.domain.friend.entity.FriendInvite; import site.timecapsulearchive.core.domain.friend.entity.MemberFriend; -import site.timecapsulearchive.core.domain.group.entity.GroupInvite; import site.timecapsulearchive.core.domain.group.entity.MemberGroup; import site.timecapsulearchive.core.domain.history.entity.History; import site.timecapsulearchive.core.global.entity.BaseEntity; @@ -77,9 +76,6 @@ public class Member extends BaseEntity { @OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true) private List capsules; - @OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true) - private List groupInvites; - @OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true) private List groups; diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/member/repository/MemberRepository.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/member/repository/MemberRepository.java index 62042ca10..9e8f9b31d 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/member/repository/MemberRepository.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/member/repository/MemberRepository.java @@ -26,6 +26,4 @@ int updateMemberNotificationEnabled( @Param("memberId") Long memberId, @Param("notificationEnabled") Boolean notificationEnabled ); - - Optional findMemberByTag(String tag); } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/global/config/rabbitmq/RabbitFailedComponentConfig.java b/backend/core/src/main/java/site/timecapsulearchive/core/global/config/rabbitmq/RabbitFailedComponentConfig.java index fda8ce67c..82dd8241b 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/global/config/rabbitmq/RabbitFailedComponentConfig.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/global/config/rabbitmq/RabbitFailedComponentConfig.java @@ -31,13 +31,14 @@ public Binding capsuleSkinFailBinding() { @Bean public Queue groupInviteFailQueue() { - return new Queue(RabbitmqComponentConstants.GROUP_INVITE_QUEUE.getFailComponent(), true); + return new Queue( + RabbitmqComponentConstants.GROUP_INVITE_NOTIFICATION_QUEUE.getFailComponent(), true); } @Bean public DirectExchange groupInviteFailExchange() { return new DirectExchange( - RabbitmqComponentConstants.GROUP_INVITE_EXCHANGE.getFailComponent()); + RabbitmqComponentConstants.GROUP_INVITE_NOTIFICATION_EXCHANGE.getFailComponent()); } @Bean @@ -48,6 +49,26 @@ public Binding groupInviteFailBinding() { .withQueueName(); } + @Bean + public Queue groupAcceptFailQueue() { + return new Queue( + RabbitmqComponentConstants.GROUP_ACCEPT_NOTIFICATION_QUEUE.getFailComponent(), true); + } + + @Bean + public DirectExchange groupAcceptFailExchange() { + return new DirectExchange( + RabbitmqComponentConstants.GROUP_ACCEPT_NOTIFICATION_EXCHANGE.getFailComponent()); + } + + @Bean + public Binding groupAcceptFailBinding() { + return BindingBuilder + .bind(groupAcceptFailQueue()) + .to(groupAcceptFailExchange()) + .withQueueName(); + } + @Bean public Queue friendRequestFailQueue() { return new Queue( diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/global/config/rabbitmq/RabbitmqComponentConstants.java b/backend/core/src/main/java/site/timecapsulearchive/core/global/config/rabbitmq/RabbitmqComponentConstants.java index cf0b91e0f..4f615029c 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/global/config/rabbitmq/RabbitmqComponentConstants.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/global/config/rabbitmq/RabbitmqComponentConstants.java @@ -17,9 +17,15 @@ public enum RabbitmqComponentConstants { "fail.notification.friendAccept.queue"), FRIEND_ACCEPT_NOTIFICATION_EXCHANGE("notification.friendAccept.exchange", "fail.notification.friendAccept.exchange"), - GROUP_INVITE_QUEUE("notification.groupInvite.queue", "fail.notification.groupInvite.queue"), - GROUP_INVITE_EXCHANGE("notification.groupInvite.exchange", - "fail.notification.groupInvite.exchange"); + GROUP_INVITE_NOTIFICATION_QUEUE("notification.groupInvite.queue", + "fail.notification.groupInvite.queue"), + GROUP_INVITE_NOTIFICATION_EXCHANGE("notification.groupInvite.exchange", + "fail.notification.groupInvite.exchange"), + + GROUP_ACCEPT_NOTIFICATION_QUEUE("notification.groupAccept.queue", + "fail.notification.groupAccept.queue"), + GROUP_ACCEPT_NOTIFICATION_EXCHANGE("notification.groupAccept.exchange", + "fail.notification.groupAccept.exchange"); private final String successComponent; private final String failComponent; diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/global/config/rabbitmq/RabbitmqConfig.java b/backend/core/src/main/java/site/timecapsulearchive/core/global/config/rabbitmq/RabbitmqConfig.java index 3b9763aff..8343c1f3a 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/global/config/rabbitmq/RabbitmqConfig.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/global/config/rabbitmq/RabbitmqConfig.java @@ -43,13 +43,14 @@ public Binding capsuleSkinBinding() { @Bean public Queue groupInviteQueue() { - return new Queue(RabbitmqComponentConstants.GROUP_INVITE_QUEUE.getSuccessComponent(), true); + return new Queue( + RabbitmqComponentConstants.GROUP_INVITE_NOTIFICATION_QUEUE.getSuccessComponent(), true); } @Bean public DirectExchange groupInviteExchange() { return new DirectExchange( - RabbitmqComponentConstants.GROUP_INVITE_EXCHANGE.getSuccessComponent()); + RabbitmqComponentConstants.GROUP_INVITE_NOTIFICATION_EXCHANGE.getSuccessComponent()); } @Bean @@ -60,6 +61,26 @@ public Binding groupInviteBinding() { .withQueueName(); } + @Bean + public Queue groupAcceptQueue() { + return new Queue( + RabbitmqComponentConstants.GROUP_ACCEPT_NOTIFICATION_QUEUE.getSuccessComponent(), true); + } + + @Bean + public DirectExchange groupAcceptExchange() { + return new DirectExchange( + RabbitmqComponentConstants.GROUP_ACCEPT_NOTIFICATION_EXCHANGE.getSuccessComponent()); + } + + @Bean + public Binding groupAcceptBinding() { + return BindingBuilder + .bind(groupAcceptQueue()) + .to(groupAcceptExchange()) + .withQueueName(); + } + @Bean public Queue friendRequestQueue() { return new Queue( diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/global/error/ErrorCode.java b/backend/core/src/main/java/site/timecapsulearchive/core/global/error/ErrorCode.java index d1f5dffa2..434fbf85e 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/global/error/ErrorCode.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/global/error/ErrorCode.java @@ -60,6 +60,8 @@ public enum ErrorCode { //group GROUP_CREATE_ERROR(400, "GROUP-001", "그룹 생성에 실패하였습니다."), GROUP_NOT_FOUND_ERROR(404, "GROUP-002", "그룹을 찾을 수 없습니다"), + GROUP_OWNER_AUTHENTICATE_ERROR(400, "GROUP-003", "그룹장이 아닙니다."), + GROUP_INVITATION_NOT_FOUND_ERROR(404, "GROUP-004", "그룹 초대를 찾을 수 없습니다"), //friend invite FRIEND_INVITE_NOT_FOUND_ERROR(404, "FRIEND-INVITE-001", "친구 요청을 찾지 못하였습니다."), diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/infra/queue/data/dto/GroupAcceptNotificationDto.java b/backend/core/src/main/java/site/timecapsulearchive/core/infra/queue/data/dto/GroupAcceptNotificationDto.java new file mode 100644 index 000000000..cb4e24be3 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/infra/queue/data/dto/GroupAcceptNotificationDto.java @@ -0,0 +1,27 @@ +package site.timecapsulearchive.core.infra.queue.data.dto; + +import lombok.Builder; +import site.timecapsulearchive.core.domain.member.entity.NotificationStatus; + +@Builder +public record GroupAcceptNotificationDto( + Long targetId, + NotificationStatus notificationStatus, + String title, + String text +) { + + public static GroupAcceptNotificationDto createOf( + final Long targetId, + final String groupMemberNickname + ) { + final NotificationRequestMessage groupAcceptRequest = NotificationRequestMessage.GROUP_ACCEPT; + + return new GroupAcceptNotificationDto( + targetId, + groupAcceptRequest.getStatus(), + groupAcceptRequest.getTitle(), + groupAcceptRequest.buildPrefixText(groupMemberNickname) + ); + } +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/infra/queue/data/dto/NotificationRequestMessage.java b/backend/core/src/main/java/site/timecapsulearchive/core/infra/queue/data/dto/NotificationRequestMessage.java index 5cffeba8e..c0bdc06b4 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/infra/queue/data/dto/NotificationRequestMessage.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/infra/queue/data/dto/NotificationRequestMessage.java @@ -6,7 +6,8 @@ public enum NotificationRequestMessage { CAPSULE_SKIN(NotificationStatus.SUCCESS, "캡슐 스킨 생성 알림", "이 생성되었습니다."), FRIEND_REQUEST(NotificationStatus.SUCCESS, "친구 요청 알림", "님 으로부터 친구 요청이 왔습니다."), FRIEND_ACCEPT(NotificationStatus.SUCCESS, "친구 수락 알림", "님이 친구 요청을 수락하였습니다."), - GROUP_INVITE(NotificationStatus.SUCCESS, "그룹 초대 알림", "님으로부터 그룹 초대 요청이 왔습니다."); + GROUP_INVITE(NotificationStatus.SUCCESS, "그룹 초대 알림", "님으로부터 그룹 초대 요청이 왔습니다."), + GROUP_ACCEPT(NotificationStatus.SUCCESS, "그룹 수락 알림", "님이 그룹 요청을 수락하였습니다."); private final NotificationStatus status; private final String title; diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/infra/queue/manager/SocialNotificationManager.java b/backend/core/src/main/java/site/timecapsulearchive/core/infra/queue/manager/SocialNotificationManager.java index b41e4da39..888748a02 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/infra/queue/manager/SocialNotificationManager.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/infra/queue/manager/SocialNotificationManager.java @@ -8,6 +8,7 @@ import site.timecapsulearchive.core.infra.queue.data.dto.FriendAcceptNotificationDto; import site.timecapsulearchive.core.infra.queue.data.dto.FriendReqNotificationDto; import site.timecapsulearchive.core.infra.queue.data.dto.FriendsReqNotificationsDto; +import site.timecapsulearchive.core.infra.queue.data.dto.GroupAcceptNotificationDto; import site.timecapsulearchive.core.infra.queue.data.dto.GroupInviteNotificationDto; @Component @@ -71,9 +72,20 @@ public void sendGroupInviteMessage( final List targetIds ) { basicRabbitTemplate.convertAndSend( - RabbitmqComponentConstants.GROUP_INVITE_EXCHANGE.getSuccessComponent(), - RabbitmqComponentConstants.GROUP_INVITE_QUEUE.getSuccessComponent(), + RabbitmqComponentConstants.GROUP_INVITE_NOTIFICATION_EXCHANGE.getSuccessComponent(), + RabbitmqComponentConstants.GROUP_INVITE_NOTIFICATION_QUEUE.getSuccessComponent(), GroupInviteNotificationDto.createOf(ownerNickname, groupProfileUrl, targetIds) ); } + + public void sendGroupAcceptMessage( + final String groupMemberNickname, + final Long targetId + ) { + basicRabbitTemplate.convertAndSend( + RabbitmqComponentConstants.GROUP_ACCEPT_NOTIFICATION_EXCHANGE.getSuccessComponent(), + RabbitmqComponentConstants.GROUP_ACCEPT_NOTIFICATION_QUEUE.getSuccessComponent(), + GroupAcceptNotificationDto.createOf(targetId, groupMemberNickname) + ); + } } diff --git a/backend/core/src/main/resources/db/migration/V25__group_invite_update.sql b/backend/core/src/main/resources/db/migration/V25__group_invite_update.sql new file mode 100644 index 000000000..729e12a70 --- /dev/null +++ b/backend/core/src/main/resources/db/migration/V25__group_invite_update.sql @@ -0,0 +1,20 @@ +alter table group_invite + drop foreign key fk_group_invite_member_id; + +alter table group_invite + drop column member_id; + + +ALTER TABLE group_invite + ADD COLUMN group_owner_id BIGINT; +ALTER TABLE group_invite + ADD COLUMN group_member_id BIGINT; + +ALTER TABLE group_invite + ADD CONSTRAINT fk_group_invite_group_owner_id FOREIGN KEY (group_owner_id) REFERENCES member (member_id); + +ALTER TABLE group_invite + ADD CONSTRAINT fk_group_invite_group_member_id FOREIGN KEY (group_member_id) REFERENCES member (member_id); + +ALTER TABLE group_invite + ADD CONSTRAINT unique_owner_member_pair UNIQUE (group_owner_id, group_member_id); diff --git a/backend/core/src/main/resources/db/migration/V26__group_invite_add.sql b/backend/core/src/main/resources/db/migration/V26__group_invite_add.sql new file mode 100644 index 000000000..61254b875 --- /dev/null +++ b/backend/core/src/main/resources/db/migration/V26__group_invite_add.sql @@ -0,0 +1,3 @@ +alter table group_invite add column group_id BIGINT; +ALTER TABLE group_invite + ADD CONSTRAINT fk_group_invite_group_id FOREIGN KEY (group_id) REFERENCES `group` (group_id); \ No newline at end of file diff --git a/backend/core/src/test/java/site/timecapsulearchive/core/common/dependency/TestTransactionTemplate.java b/backend/core/src/test/java/site/timecapsulearchive/core/common/dependency/TestTransactionTemplate.java new file mode 100644 index 000000000..36a7970d4 --- /dev/null +++ b/backend/core/src/test/java/site/timecapsulearchive/core/common/dependency/TestTransactionTemplate.java @@ -0,0 +1,21 @@ +package site.timecapsulearchive.core.common.dependency; + +import static org.mockito.Mockito.spy; + +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.support.SimpleTransactionStatus; +import org.springframework.transaction.support.TransactionCallback; +import org.springframework.transaction.support.TransactionTemplate; + +public class TestTransactionTemplate extends TransactionTemplate { + + public static TestTransactionTemplate spied() { + return spy(new TestTransactionTemplate()); + } + + @Override + public T execute(TransactionCallback action) throws TransactionException { + return action.doInTransaction(new SimpleTransactionStatus()); + } + +} diff --git a/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/dto/GroupDtoFixture.java b/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/dto/GroupDtoFixture.java new file mode 100644 index 000000000..ca4196785 --- /dev/null +++ b/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/dto/GroupDtoFixture.java @@ -0,0 +1,23 @@ +package site.timecapsulearchive.core.common.fixture.dto; + +import java.util.List; +import site.timecapsulearchive.core.domain.group.data.dto.GroupCreateDto; +import site.timecapsulearchive.core.domain.group.data.dto.GroupOwnerSummaryDto; + +public class GroupDtoFixture { + + public static GroupCreateDto groupCreateDto(List targetIds) { + return GroupCreateDto.builder() + .groupName("testGroupName") + .groupProfileUrl("testGroupProfileUrl") + .groupImage("testGroupImage") + .description("testDescription") + .targetIds(targetIds) + .build(); + } + + public static GroupOwnerSummaryDto groupOwnerSummaryDto(Boolean isOwner) { + return new GroupOwnerSummaryDto("testNickname", isOwner, "testGroupProfileUrl"); + } + +} diff --git a/backend/core/src/test/java/site/timecapsulearchive/core/domain/group/repository/MemberGroupQueryRepositoryTest.java b/backend/core/src/test/java/site/timecapsulearchive/core/domain/group/repository/MemberGroupQueryRepositoryTest.java index bb5f408de..08f0234d4 100644 --- a/backend/core/src/test/java/site/timecapsulearchive/core/domain/group/repository/MemberGroupQueryRepositoryTest.java +++ b/backend/core/src/test/java/site/timecapsulearchive/core/domain/group/repository/MemberGroupQueryRepositoryTest.java @@ -26,6 +26,8 @@ import site.timecapsulearchive.core.domain.group.data.dto.GroupSummaryDto; import site.timecapsulearchive.core.domain.group.entity.Group; import site.timecapsulearchive.core.domain.group.entity.MemberGroup; +import site.timecapsulearchive.core.domain.group.repository.groupRepository.GroupQueryRepository; +import site.timecapsulearchive.core.domain.group.repository.groupRepository.GroupQueryRepositoryImpl; import site.timecapsulearchive.core.domain.member.entity.Member; @TestConstructor(autowireMode = AutowireMode.ALL) @@ -40,7 +42,7 @@ class MemberGroupQueryRepositoryTest extends RepositoryTest { private Long ownerGroupId; MemberGroupQueryRepositoryTest(JPAQueryFactory jpaQueryFactory) { - this.groupQueryRepository = new GroupQueryRepository(jpaQueryFactory); + this.groupQueryRepository = new GroupQueryRepositoryImpl(jpaQueryFactory); } @Transactional diff --git a/backend/core/src/test/java/site/timecapsulearchive/core/domain/group/service/GroupWriteServiceTest.java b/backend/core/src/test/java/site/timecapsulearchive/core/domain/group/service/GroupWriteServiceTest.java new file mode 100644 index 000000000..e3b158f37 --- /dev/null +++ b/backend/core/src/test/java/site/timecapsulearchive/core/domain/group/service/GroupWriteServiceTest.java @@ -0,0 +1,233 @@ +package site.timecapsulearchive.core.domain.group.service; + + +import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.Test; +import org.springframework.transaction.support.TransactionTemplate; +import site.timecapsulearchive.core.common.dependency.TestTransactionTemplate; +import site.timecapsulearchive.core.common.fixture.domain.GroupFixture; +import site.timecapsulearchive.core.common.fixture.domain.MemberFixture; +import site.timecapsulearchive.core.common.fixture.dto.GroupDtoFixture; +import site.timecapsulearchive.core.domain.group.data.dto.GroupCreateDto; +import site.timecapsulearchive.core.domain.group.data.dto.GroupOwnerSummaryDto; +import site.timecapsulearchive.core.domain.group.exception.GroupInviteNotFoundException; +import site.timecapsulearchive.core.domain.group.exception.GroupNotFoundException; +import site.timecapsulearchive.core.domain.group.exception.GroupOwnerAuthenticateException; +import site.timecapsulearchive.core.domain.group.repository.groupInviteRepository.GroupInviteRepository; +import site.timecapsulearchive.core.domain.group.repository.groupRepository.GroupRepository; +import site.timecapsulearchive.core.domain.group.repository.memberGroupRepository.MemberGroupRepository; +import site.timecapsulearchive.core.domain.group.service.write.GroupWriteService; +import site.timecapsulearchive.core.domain.group.service.write.GroupWriteServiceImpl; +import site.timecapsulearchive.core.domain.member.entity.Member; +import site.timecapsulearchive.core.domain.member.exception.MemberNotFoundException; +import site.timecapsulearchive.core.domain.member.repository.MemberRepository; +import site.timecapsulearchive.core.global.error.ErrorCode; +import site.timecapsulearchive.core.infra.queue.manager.SocialNotificationManager; + +class GroupWriteServiceTest { + + private final MemberRepository memberRepository = mock(MemberRepository.class); + private final GroupRepository groupRepository = mock(GroupRepository.class); + private final MemberGroupRepository memberGroupRepository = mock(MemberGroupRepository.class); + private final GroupInviteRepository groupInviteRepository = mock(GroupInviteRepository.class); + private final SocialNotificationManager socialNotificationManager = mock( + SocialNotificationManager.class); + private final TransactionTemplate transactionTemplate = TestTransactionTemplate.spied(); + + private final GroupWriteService groupWriteService = new GroupWriteServiceImpl( + memberRepository, + groupRepository, + memberGroupRepository, + groupInviteRepository, + transactionTemplate, + socialNotificationManager + ); + + @Test + void 그룹장이_그룹원들을_포함하여_그룹을_생성하면_그룹원들에게_그룹초대_알림이_요청된다() { + //given + Long memberId = 1L; + List targetIds = List.of(2L, 3L, 4L, 5L); + GroupCreateDto dto = GroupDtoFixture.groupCreateDto(targetIds); + given(memberRepository.findMemberById(memberId)).willReturn( + Optional.of(MemberFixture.member(1))); + + //when + groupWriteService.createGroup(memberId, dto); + + //then + verify(socialNotificationManager, times(1)).sendGroupInviteMessage( + anyString(), anyString(), anyList()); + } + + @Test + void 그룹장이_그룹초대를_할_때_존재하지_않는_그룹장_아이디면_예외가_발생한다() { + //given + Long memberId = -1L; + List targetIds = List.of(2L, 3L, 4L, 5L); + GroupCreateDto dto = GroupDtoFixture.groupCreateDto(targetIds); + given(memberRepository.findMemberById(memberId)).willReturn(Optional.empty()); + + //when + //then + assertThatThrownBy(() -> groupWriteService.createGroup(memberId, dto)) + .isInstanceOf(MemberNotFoundException.class) + .hasMessageContaining(ErrorCode.MEMBER_NOT_FOUND_ERROR.getMessage()); + } + + + @Test + void 그룹장이_그룹원에게_그룹초대를_하면_그룹초대_알림이_요청된다() { + //given + Long memberId = 1L; + Long groupId = 1L; + Long targetId = 2L; + Member groupOwner = MemberFixture.member(1); + GroupOwnerSummaryDto groupOwnerSummaryDto = GroupDtoFixture.groupOwnerSummaryDto(true); + + given(memberRepository.findMemberById(memberId)).willReturn(Optional.of(groupOwner)); + given(memberRepository.findMemberById(targetId)).willReturn( + Optional.of(MemberFixture.member(2))); + given(memberGroupRepository.findOwnerInMemberGroup(groupId, memberId)).willReturn( + Optional.of(groupOwnerSummaryDto)); + + //when + groupWriteService.inviteGroup(memberId, groupId, targetId); + + //then + verify(socialNotificationManager, times(1)).sendGroupInviteMessage(anyString(), anyString(), + anyList()); + } + + @Test + void 그룹장이_그룹초대를_할_때_존재하지_않은_그룹_아이디_이면_예외가_발생한다() { + //given + Long memberId = 1L; + Long groupId = 1L; + Long targetId = 2L; + Member groupOwner = MemberFixture.member(1); + + given(memberRepository.findMemberById(memberId)).willReturn(Optional.of(groupOwner)); + given(memberRepository.findMemberById(targetId)).willReturn( + Optional.of(MemberFixture.member(2))); + given(memberGroupRepository.findOwnerInMemberGroup(groupId, memberId)).willReturn( + Optional.empty()); + + //when + //then + assertThatThrownBy(() -> groupWriteService.inviteGroup(memberId, groupId, targetId)) + .isInstanceOf(GroupNotFoundException.class) + .hasMessageContaining(ErrorCode.GROUP_NOT_FOUND_ERROR.getMessage()); + } + + @Test + void 그룹장이_아닌_사용자가_그룹원에게_그룹초대를_하면_예외가_발생한다() { + //given + Long memberId = 1L; + Long groupId = 1L; + Long targetId = 2L; + Member groupOwner = MemberFixture.member(1); + GroupOwnerSummaryDto groupOwnerSummaryDto = GroupDtoFixture.groupOwnerSummaryDto(false); + + given(memberRepository.findMemberById(memberId)).willReturn(Optional.of(groupOwner)); + given(memberRepository.findMemberById(targetId)).willReturn( + Optional.of(MemberFixture.member(2))); + given(memberGroupRepository.findOwnerInMemberGroup(groupId, memberId)).willReturn( + Optional.of(groupOwnerSummaryDto)); + + //when + //then + assertThatThrownBy(() -> groupWriteService.inviteGroup(memberId, groupId, targetId)) + .isInstanceOf(GroupOwnerAuthenticateException.class) + .hasMessageContaining(ErrorCode.GROUP_OWNER_AUTHENTICATE_ERROR.getMessage()); + } + + @Test + void 그룹원은_그룹초대_삭제에서_1을_반환하면_거부할_수_있다() { + //given + Long memberId = 1L; + Long groupId = 1L; + Long targetId = 2L; + + given(groupInviteRepository.deleteGroupInviteByGroupIdAndGroupOwnerIdAndGroupMemberId( + groupId, targetId, memberId)).willReturn(1); + + //when + // then + assertThatCode(() -> groupWriteService.rejectRequestGroup(memberId, groupId, targetId)) + .doesNotThrowAnyException(); + } + + @Test + void 그룹원은_그룹초대_삭제에서_0을_반환하면_거부가_실패_한다() { + //given + Long memberId = 1L; + Long groupId = 1L; + Long targetId = 2L; + + given(groupInviteRepository.deleteGroupInviteByGroupIdAndGroupOwnerIdAndGroupMemberId( + groupId, targetId, memberId)).willReturn(0); + + //when + // then + assertThatThrownBy(() -> groupWriteService.rejectRequestGroup(memberId, groupId, targetId)) + .isInstanceOf(GroupInviteNotFoundException.class) + .hasMessageContaining(ErrorCode.GROUP_INVITATION_NOT_FOUND_ERROR.getMessage()); + } + + @Test + void 그룹원은_그룹초대를_수락하면_그룹장에게_알림이_전송된다() { + //given + Long memberId = 1L; + Long groupId = 1L; + Long targetId = 2L; + Member groupMember = MemberFixture.member(1); + + given(memberRepository.findMemberById(memberId)).willReturn(Optional.of(groupMember)); + given(groupRepository.findGroupById(groupId)).willReturn( + Optional.of(GroupFixture.group())); + given(groupInviteRepository.deleteGroupInviteByGroupIdAndGroupOwnerIdAndGroupMemberId( + groupId, targetId, memberId)).willReturn(1); + + //when + groupWriteService.acceptGroupInvite(memberId, groupId, targetId); + + //then + verify(socialNotificationManager, times(1)).sendGroupAcceptMessage(anyString(), anyLong()); + } + + @Test + void 그룹원은_그룹초대를_수락할_때_그룹초대가_존재하지_않으면_예외가_발생한다() { + //given + Long memberId = 1L; + Long groupId = 1L; + Long targetId = 2L; + Member groupMember = MemberFixture.member(1); + + given(memberRepository.findMemberById(memberId)).willReturn(Optional.of(groupMember)); + given(groupRepository.findGroupById(groupId)).willReturn( + Optional.of(GroupFixture.group())); + given(groupInviteRepository.deleteGroupInviteByGroupIdAndGroupOwnerIdAndGroupMemberId( + groupId, targetId, memberId)).willReturn(0); + + //when + //then + assertThatThrownBy(() -> groupWriteService.acceptGroupInvite(memberId, groupId, targetId)) + .isInstanceOf(GroupInviteNotFoundException.class) + .hasMessageContaining(ErrorCode.GROUP_INVITATION_NOT_FOUND_ERROR.getMessage()); + } + + +} +