diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/api/command/FriendCommandApi.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/api/command/FriendCommandApi.java index 83f1351fe..ff56bce53 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/api/command/FriendCommandApi.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/api/command/FriendCommandApi.java @@ -42,6 +42,32 @@ ResponseEntity> acceptFriendRequest( Long friendId ); + @Operation( + summary = "소셜 친구 요청 삭제", + description = "사용자가 보낸 소셜 친구 요청을 삭제한다.", + security = {@SecurityRequirement(name = "user_token")}, + tags = {"friend"} + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "처리 완료" + ), + @ApiResponse( + responseCode = "400", + description = """ + 잘못된 요청 파라미터 또는 존재하지 않는 사용자로 요청하거나 존재하지 않는 친구에게 친구 요청을 보낼 경우 발생 + """, + content = @Content(schema = @Schema(implementation = ErrorResponse.class)) + ) + }) + ResponseEntity> deleteSendingFriendInvite( + Long memberId, + + @Parameter(in = ParameterIn.PATH, required = true, schema = @Schema()) + Long friendId + ); + @Operation( summary = "소셜 친구 삭제", description = "사용자의 소셜 친구를 삭제한다.", diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/api/command/FriendCommandApiController.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/api/command/FriendCommandApiController.java index cf33ef264..5ccc2e626 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/api/command/FriendCommandApiController.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/api/command/FriendCommandApiController.java @@ -11,6 +11,7 @@ import org.springframework.web.bind.annotation.RestController; import site.timecapsulearchive.core.domain.friend.data.request.SendFriendRequest; import site.timecapsulearchive.core.domain.friend.service.command.FriendCommandService; +import site.timecapsulearchive.core.domain.member.service.MemberService; import site.timecapsulearchive.core.global.common.response.ApiSpec; import site.timecapsulearchive.core.global.common.response.SuccessCode; @@ -20,6 +21,7 @@ public class FriendCommandApiController implements FriendCommandApi { private final FriendCommandService friendCommandService; + private final MemberService memberService; @DeleteMapping(value = "/{friend_id}") @Override @@ -36,6 +38,22 @@ public ResponseEntity> deleteFriend( ); } + @DeleteMapping("/{friend_id}/sending-invites") + @Override + public ResponseEntity> deleteSendingFriendInvite( + @AuthenticationPrincipal final Long memberId, + @PathVariable("friend_id") final Long friendId + ) { + friendCommandService.deleteSendingFriendInvite(memberId, friendId); + + return ResponseEntity.ok( + ApiSpec.empty( + SuccessCode.SUCCESS + ) + ); + } + + @DeleteMapping("/{friend_id}/deny-request") @Override public ResponseEntity> denyFriendRequest( @@ -95,6 +113,4 @@ public ResponseEntity> acceptFriendRequest( ) ); } - - } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/api/query/FriendQueryApi.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/api/query/FriendQueryApi.java index 7fe063959..345a98c22 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/api/query/FriendQueryApi.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/api/query/FriendQueryApi.java @@ -66,10 +66,10 @@ ResponseEntity> findFriendsBeforeGroupInvite( @Operation( summary = "소셜 친구 요청 받은 목록 조회", description = """ - 사용자가 소셜 친구 요청을 받은 목록을 보여준다. -
- 수락 대기 중인 요청만 해당한다. - """, + 사용자가 소셜 친구 요청을 받은 목록을 보여준다. +
+ 수락 대기 중인 요청만 해당한다. + """, security = {@SecurityRequirement(name = "user_token")}, tags = {"friend"} ) diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/data/dto/FriendInviteMemberIdsDto.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/data/dto/FriendInviteMemberIdsDto.java new file mode 100644 index 000000000..02882eafb --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/data/dto/FriendInviteMemberIdsDto.java @@ -0,0 +1,8 @@ +package site.timecapsulearchive.core.domain.friend.data.dto; + +public record FriendInviteMemberIdsDto( + Long ownerId, + Long friendId +) { + +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/data/dto/FriendInviteNotificationDto.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/data/dto/FriendInviteNotificationDto.java new file mode 100644 index 000000000..55e84fb58 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/data/dto/FriendInviteNotificationDto.java @@ -0,0 +1,18 @@ +package site.timecapsulearchive.core.domain.friend.data.dto; + +import java.util.List; + +public record FriendInviteNotificationDto( + String nickname, + String profileUrl, + List foundFriendIds +) { + + public static FriendInviteNotificationDto create( + final String nickname, + final String profileUrl, + final List foundFriendIds + ) { + return new FriendInviteNotificationDto(nickname, profileUrl, foundFriendIds); + } +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/entity/FriendInvite.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/entity/FriendInvite.java index 9a2ce2f19..e1247de04 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/entity/FriendInvite.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/entity/FriendInvite.java @@ -1,5 +1,6 @@ package site.timecapsulearchive.core.domain.friend.entity; +import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/exception/FriendInviteDuplicateException.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/exception/FriendInviteDuplicateException.java new file mode 100644 index 000000000..e9963080b --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/exception/FriendInviteDuplicateException.java @@ -0,0 +1,11 @@ +package site.timecapsulearchive.core.domain.friend.exception; + +import site.timecapsulearchive.core.global.error.ErrorCode; +import site.timecapsulearchive.core.global.error.exception.BusinessException; + +public class FriendInviteDuplicateException extends BusinessException { + + public FriendInviteDuplicateException() { + super(ErrorCode.FRIEND_INVITE_DUPLICATE_ERROR); + } +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/exception/FriendDuplicateIdException.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/exception/SelfFriendOperationException.java similarity index 55% rename from backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/exception/FriendDuplicateIdException.java rename to backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/exception/SelfFriendOperationException.java index d221257d9..eba202929 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/exception/FriendDuplicateIdException.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/exception/SelfFriendOperationException.java @@ -3,9 +3,9 @@ import site.timecapsulearchive.core.global.error.ErrorCode; import site.timecapsulearchive.core.global.error.exception.BusinessException; -public class FriendDuplicateIdException extends BusinessException { +public class SelfFriendOperationException extends BusinessException { - public FriendDuplicateIdException() { - super(ErrorCode.FRIEND_DUPLICATE_ID_ERROR); + public SelfFriendOperationException() { + super(ErrorCode.SELF_FRIEND_OPERATION_ERROR); } } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/repository/friend_invite/FriendInviteQueryRepository.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/repository/friend_invite/FriendInviteQueryRepository.java index 40ba418cc..05b827ce9 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/repository/friend_invite/FriendInviteQueryRepository.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/repository/friend_invite/FriendInviteQueryRepository.java @@ -2,7 +2,9 @@ import java.time.ZonedDateTime; import java.util.List; +import java.util.Optional; import org.springframework.data.domain.Slice; +import site.timecapsulearchive.core.domain.friend.data.dto.FriendInviteMemberIdsDto; import site.timecapsulearchive.core.domain.friend.data.dto.FriendSummaryDto; public interface FriendInviteQueryRepository { @@ -21,4 +23,10 @@ Slice findFriendSendingInvitesSlice( final int size, final ZonedDateTime createdAt ); + + List findFriendInviteMemberIdsDtoByMemberIdsAndFriendId( + List memberIds, Long friendId); + + Optional findFriendInviteMemberIdsDtoByMemberIdAndFriendId( + Long memberId, Long friendId); } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/repository/friend_invite/FriendInviteQueryRepositoryImpl.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/repository/friend_invite/FriendInviteQueryRepositoryImpl.java index ae6aeceef..33e02694d 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/repository/friend_invite/FriendInviteQueryRepositoryImpl.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/repository/friend_invite/FriendInviteQueryRepositoryImpl.java @@ -3,6 +3,7 @@ import static site.timecapsulearchive.core.domain.friend.entity.QFriendInvite.friendInvite; import static site.timecapsulearchive.core.domain.member.entity.QMember.member; +import com.querydsl.core.BooleanBuilder; import com.querydsl.core.types.Projections; import com.querydsl.jpa.impl.JPAQueryFactory; import java.sql.PreparedStatement; @@ -11,13 +12,13 @@ import java.sql.Types; import java.time.ZonedDateTime; import java.util.List; +import java.util.Optional; import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; -import org.springframework.data.domain.SliceImpl; import org.springframework.jdbc.core.BatchPreparedStatementSetter; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; +import site.timecapsulearchive.core.domain.friend.data.dto.FriendInviteMemberIdsDto; import site.timecapsulearchive.core.domain.friend.data.dto.FriendSummaryDto; import site.timecapsulearchive.core.global.util.SliceUtil; @@ -107,4 +108,49 @@ public Slice findFriendSendingInvitesSlice( return SliceUtil.makeSlice(size, friends); } + + @Override + public List findFriendInviteMemberIdsDtoByMemberIdsAndFriendId( + List memberIds, Long friendId) { + BooleanBuilder multipleColumnsInCondition = new BooleanBuilder(); + for (Long memberId : memberIds) { + multipleColumnsInCondition.or(friendInvite.owner.id.eq(memberId) + .and(friendInvite.friend.id.eq(friendId))); + + multipleColumnsInCondition.or(friendInvite.owner.id.eq(friendId) + .and(friendInvite.friend.id.eq(memberId))); + } + + return jpaQueryFactory + .select( + Projections.constructor( + FriendInviteMemberIdsDto.class, + friendInvite.owner.id, + friendInvite.friend.id + ) + ) + .from(friendInvite) + .where(multipleColumnsInCondition) + .fetch(); + } + + @Override + public Optional findFriendInviteMemberIdsDtoByMemberIdAndFriendId( + Long memberId, Long friendId) { + return Optional.ofNullable( + jpaQueryFactory + .select( + Projections.constructor( + FriendInviteMemberIdsDto.class, + friendInvite.owner.id, + friendInvite.friend.id + ) + ) + .from(friendInvite) + .where(friendInvite.owner.id.eq(memberId).and(friendInvite.friend.id.eq(friendId)) + .or(friendInvite.owner.id.eq(friendId) + .and(friendInvite.friend.id.eq(memberId)))) + .fetchOne() + ); + } } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/repository/friend_invite/FriendInviteRepository.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/repository/friend_invite/FriendInviteRepository.java index 944e7bedf..bc9340551 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/repository/friend_invite/FriendInviteRepository.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/repository/friend_invite/FriendInviteRepository.java @@ -1,6 +1,5 @@ package site.timecapsulearchive.core.domain.friend.repository.friend_invite; -import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.Repository; @@ -12,21 +11,21 @@ public interface FriendInviteRepository extends Repository, void save(FriendInvite friendInvite); - @Query(value = "select fi " - + "from FriendInvite fi " - + "join fetch fi.owner " - + "join fetch fi.friend " - + "where (fi.owner.id =:friendId and fi.friend.id =:memberId) " - + "or (fi.owner.id =: memberId and fi.friend.id =: friendId)") - List findFriendInviteWithMembersByOwnerIdAndFriendId( - @Param(value = "memberId") Long memberId, + void delete(FriendInvite friendInvite); + + Optional findFriendSendingInviteForUpdateByOwnerIdAndFriendId(Long memberId, + Long friendId); + + @Query(value = """ + select fi + from FriendInvite fi + join fetch fi.owner + join fetch fi.friend + where fi.owner.id =:ownerId and fi.friend.id =:friendId + """) + Optional findFriendReceivingInviteForUpdateByOwnerIdAndFriendId( + @Param(value = "ownerId") Long ownerId, @Param(value = "friendId") Long friendId ); - - Optional findFriendInviteByOwnerIdAndFriendId(Long memberId, Long targetId); - - int deleteFriendInviteByOwnerIdAndFriendId(Long memberId, Long targetId); - - void delete(FriendInvite friendInvite); } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/repository/member_friend/MemberFriendQueryRepository.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/repository/member_friend/MemberFriendQueryRepository.java index de7021d81..73a9bb6d4 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/repository/member_friend/MemberFriendQueryRepository.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/repository/member_friend/MemberFriendQueryRepository.java @@ -30,7 +30,7 @@ Optional findFriendsByTag( List findFriendIdsByOwnerId(final Long memberId); - Slice findFriendsBeforeGroupInvite( + Slice findFriends( final FriendBeforeGroupInviteRequest request); List findFriendIds(final List groupMemberIds, final Long memberId); diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/repository/member_friend/MemberFriendQueryRepositoryImpl.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/repository/member_friend/MemberFriendQueryRepositoryImpl.java index 63cb8d93f..f4d70e495 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/repository/member_friend/MemberFriendQueryRepositoryImpl.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/repository/member_friend/MemberFriendQueryRepositoryImpl.java @@ -1,10 +1,7 @@ package site.timecapsulearchive.core.domain.friend.repository.member_friend; -import static com.querydsl.jpa.JPAExpressions.select; -import static site.timecapsulearchive.core.domain.friend.entity.QFriendInvite.friendInvite; import static site.timecapsulearchive.core.domain.friend.entity.QMemberFriend.memberFriend; import static site.timecapsulearchive.core.domain.member.entity.QMember.member; -import static site.timecapsulearchive.core.domain.member_group.entity.QMemberGroup.memberGroup; import com.querydsl.core.types.OrderSpecifier; import com.querydsl.core.types.Projections; @@ -73,8 +70,9 @@ private Slice getFriendSummaryDtos(final int size, return new SliceImpl<>(friendSummaryDtos, Pageable.ofSize(size), hasNext); } - public Slice findFriendsBeforeGroupInvite( - final FriendBeforeGroupInviteRequest request) { + public Slice findFriends( + final FriendBeforeGroupInviteRequest request + ) { final List friends = jpaQueryFactory .select( Projections.constructor( @@ -86,15 +84,8 @@ public Slice findFriendsBeforeGroupInvite( ) ) .from(memberFriend) - .innerJoin(member).on(memberFriend.owner.id.eq(member.id)) - .innerJoin(member).on(memberFriend.friend.id.eq(member.id)) .where(memberFriend.owner.id.eq(request.memberId()) .and(memberFriend.createdAt.lt(request.createdAt())) - .and(memberFriend.friend.id.notIn( - select(memberGroup.member.id) - .from(memberGroup) - .where(memberGroup.group.id.eq(request.groupId())) - )) ) .limit(request.size() + 1) .fetch(); diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/service/command/FriendCommandService.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/service/command/FriendCommandService.java index 255fe7a62..ffd05fbf0 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/service/command/FriendCommandService.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/service/command/FriendCommandService.java @@ -2,15 +2,19 @@ import java.util.List; import java.util.Optional; +import java.util.function.Predicate; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionTemplate; +import site.timecapsulearchive.core.domain.friend.data.dto.FriendInviteMemberIdsDto; +import site.timecapsulearchive.core.domain.friend.data.dto.FriendInviteNotificationDto; import site.timecapsulearchive.core.domain.friend.entity.FriendInvite; import site.timecapsulearchive.core.domain.friend.entity.MemberFriend; -import site.timecapsulearchive.core.domain.friend.exception.FriendDuplicateIdException; +import site.timecapsulearchive.core.domain.friend.exception.FriendInviteDuplicateException; import site.timecapsulearchive.core.domain.friend.exception.FriendInviteNotFoundException; import site.timecapsulearchive.core.domain.friend.exception.FriendTwoWayInviteException; +import site.timecapsulearchive.core.domain.friend.exception.SelfFriendOperationException; import site.timecapsulearchive.core.domain.friend.repository.friend_invite.FriendInviteRepository; import site.timecapsulearchive.core.domain.friend.repository.member_friend.MemberFriendRepository; import site.timecapsulearchive.core.domain.member.entity.Member; @@ -29,27 +33,52 @@ public class FriendCommandService { private final TransactionTemplate transactionTemplate; public void requestFriends(Long memberId, List friendIds) { - final Member[] owner = new Member[1]; - final List[] foundFriendIds = new List[1]; + List filteredFriendIds = filterTwoWayAndDuplicateInvite(memberId, friendIds); - transactionTemplate.executeWithoutResult(status -> { - owner[0] = memberRepository.findMemberById(memberId) + if (filteredFriendIds.isEmpty()) { + return; + } + + final FriendInviteNotificationDto dto = transactionTemplate.execute(status -> { + Member owner = memberRepository.findMemberById(memberId) .orElseThrow(MemberNotFoundException::new); - foundFriendIds[0] = memberRepository.findMemberIdsByIds(friendIds); + List foundFriendIds = memberRepository.findMemberIdsByIds(friendIds); + + friendInviteRepository.bulkSave(owner.getId(), foundFriendIds); - friendInviteRepository.bulkSave(owner[0].getId(), foundFriendIds[0]); + return FriendInviteNotificationDto.create(owner.getNickname(), owner.getProfileUrl(), + foundFriendIds); }); socialNotificationManager.sendFriendRequestMessages( - owner[0].getNickname(), - owner[0].getProfileUrl(), - foundFriendIds[0] + dto.nickname(), + dto.profileUrl(), + dto.foundFriendIds() ); } + private List filterTwoWayAndDuplicateInvite(Long memberId, List friendIds) { + List twoWayIds = friendInviteRepository.findFriendInviteMemberIdsDtoByMemberIdsAndFriendId( + friendIds, memberId); + + return friendIds.stream() + .filter(id -> !memberId.equals(id)) + .filter(isNotTwoWayInvite(memberId, twoWayIds)) + .toList(); + } + + private Predicate isNotTwoWayInvite(Long memberId, + List twoWayIds) { + return id -> twoWayIds.stream() + .noneMatch(twoWayId -> + (twoWayId.ownerId().equals(id) && twoWayId.friendId().equals(memberId)) || + (twoWayId.ownerId().equals(memberId) && twoWayId.friendId().equals(id)) + ); + } + public void requestFriend(final Long memberId, final Long friendId) { - validateFriendDuplicateId(memberId, friendId); - validateTwoWayInvite(memberId, friendId); + validateSelfFriendOperation(memberId, friendId); + validateTwoWayAndDuplicateInvite(memberId, friendId); final Member owner = memberRepository.findMemberById(memberId).orElseThrow( MemberNotFoundException::new); @@ -66,64 +95,62 @@ public void requestFriend(final Long memberId, final Long friendId) { socialNotificationManager.sendFriendReqMessage(owner.getNickname(), friendId); } - private void validateFriendDuplicateId(final Long memberId, final Long friendId) { + private void validateSelfFriendOperation(final Long memberId, final Long friendId) { if (memberId.equals(friendId)) { - throw new FriendDuplicateIdException(); + throw new SelfFriendOperationException(); } } - private void validateTwoWayInvite(final Long memberId, final Long friendId) { - final Optional friendInvite = friendInviteRepository.findFriendInviteByOwnerIdAndFriendId( - friendId, memberId); + private void validateTwoWayAndDuplicateInvite(final Long memberId, final Long friendId) { + final Optional friendInvite = friendInviteRepository.findFriendInviteMemberIdsDtoByMemberIdAndFriendId( + memberId, friendId); - if (friendInvite.isPresent()) { - throw new FriendTwoWayInviteException(); - } + friendInvite.ifPresent(dto -> { + if (dto.ownerId().equals(friendId) && dto.friendId().equals(memberId)) { + throw new FriendTwoWayInviteException(); + } else { + throw new FriendInviteDuplicateException(); + } + }); } - public void acceptFriend(final Long memberId, final Long friendId) { - validateFriendDuplicateId(memberId, friendId); + public void acceptFriend(final Long memberId, final Long ownerId) { + validateSelfFriendOperation(memberId, ownerId); final String ownerNickname = transactionTemplate.execute(status -> { - final List friendInvites = friendInviteRepository - .findFriendInviteWithMembersByOwnerIdAndFriendId(memberId, friendId); - - if (friendInvites.isEmpty()) { - throw new FriendInviteNotFoundException(); - } - - final FriendInvite friendInvite = friendInvites.get(0); + final FriendInvite friendInvite = friendInviteRepository.findFriendReceivingInviteForUpdateByOwnerIdAndFriendId( + ownerId, memberId) + .orElseThrow(FriendInviteNotFoundException::new); final MemberFriend ownerRelation = friendInvite.ownerRelation(); - final MemberFriend friendRelation = friendInvite.friendRelation(); memberFriendRepository.save(ownerRelation); memberFriendRepository.save(friendRelation); - friendInvites.forEach(friendInviteRepository::delete); + + friendInviteRepository.delete(friendInvite); return ownerRelation.getOwnerNickname(); }); - socialNotificationManager.sendFriendAcceptMessage(ownerNickname, friendId); + socialNotificationManager.sendFriendAcceptMessage(ownerNickname, ownerId); } @Transactional - public void denyRequestFriend(final Long memberId, final Long friendId) { - validateFriendDuplicateId(memberId, friendId); + public void denyRequestFriend(final Long memberId, final Long ownerId) { + validateSelfFriendOperation(memberId, ownerId); - int isDenyRequest = friendInviteRepository.deleteFriendInviteByOwnerIdAndFriendId(friendId, - memberId); + FriendInvite friendInvite = friendInviteRepository.findFriendReceivingInviteForUpdateByOwnerIdAndFriendId( + ownerId, memberId) + .orElseThrow(FriendInviteNotFoundException::new); - if (isDenyRequest != 1) { - throw new FriendTwoWayInviteException(); - } + friendInviteRepository.delete(friendInvite); } @Transactional public void deleteFriend(final Long memberId, final Long friendId) { - validateFriendDuplicateId(memberId, friendId); + validateSelfFriendOperation(memberId, friendId); final List memberFriends = memberFriendRepository .findMemberFriendByOwnerIdAndFriendId(memberId, friendId); @@ -131,4 +158,14 @@ public void deleteFriend(final Long memberId, final Long friendId) { memberFriends.forEach(memberFriendRepository::delete); } + @Transactional + public void deleteSendingFriendInvite(final Long memberId, final Long friendId) { + validateSelfFriendOperation(memberId, friendId); + + FriendInvite friendInvite = friendInviteRepository.findFriendSendingInviteForUpdateByOwnerIdAndFriendId( + memberId, friendId) + .orElseThrow(FriendInviteNotFoundException::new); + + friendInviteRepository.delete(friendInvite); + } } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/service/query/FriendQueryService.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/service/query/FriendQueryService.java index 2e0ecc5e8..15e711f91 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/service/query/FriendQueryService.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/service/query/FriendQueryService.java @@ -2,8 +2,10 @@ import java.time.ZonedDateTime; import java.util.List; +import java.util.stream.Stream; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import site.timecapsulearchive.core.domain.friend.data.dto.FriendSummaryDto; @@ -13,6 +15,8 @@ import site.timecapsulearchive.core.domain.friend.exception.FriendNotFoundException; import site.timecapsulearchive.core.domain.friend.repository.friend_invite.FriendInviteRepository; import site.timecapsulearchive.core.domain.friend.repository.member_friend.MemberFriendRepository; +import site.timecapsulearchive.core.domain.member_group.repository.group_invite_repository.GroupInviteRepository; +import site.timecapsulearchive.core.domain.member_group.repository.member_group_repository.MemberGroupRepository; import site.timecapsulearchive.core.global.common.wrapper.ByteArrayWrapper; @Service @@ -21,6 +25,8 @@ public class FriendQueryService { private final MemberFriendRepository memberFriendRepository; + private final MemberGroupRepository memberGroupRepository; + private final GroupInviteRepository groupInviteRepository; private final FriendInviteRepository friendInviteRepository; public Slice findFriendsSlice( @@ -33,7 +39,36 @@ public Slice findFriendsSlice( public Slice findFriendsBeforeGroupInviteSlice( final FriendBeforeGroupInviteRequest request) { - return memberFriendRepository.findFriendsBeforeGroupInvite(request); + final Slice friendSummaryDtos = memberFriendRepository.findFriends( + request); + + final List groupMemberIdsToExcludeBeforeGroupInvite = getGroupMemberIdsToExcludeBeforeGroupInvite( + request); + + final List friendSummaryBeforeGroupInvitedDtos = getFriendSummaryBeforeGroupInvitedDtos( + friendSummaryDtos, groupMemberIdsToExcludeBeforeGroupInvite); + + return new SliceImpl<>(friendSummaryBeforeGroupInvitedDtos, friendSummaryDtos.getPageable(), + friendSummaryDtos.hasNext()); + } + + private List getGroupMemberIdsToExcludeBeforeGroupInvite( + final FriendBeforeGroupInviteRequest request) { + return Stream.concat( + memberGroupRepository.findGroupMemberIdsByGroupId(request.groupId()).stream(), + groupInviteRepository.findGroupMemberIdsByGroupIdAndGroupOwnerId(request.groupId(), + request.memberId()).stream()) + .distinct() + .toList(); + } + + private List getFriendSummaryBeforeGroupInvitedDtos( + final Slice friendSummaryDtos, + final List groupMemberIdsToExcludeBeforeGroupInvite + ) { + return friendSummaryDtos.getContent() + .stream() + .filter(dto -> !groupMemberIdsToExcludeBeforeGroupInvite.contains(dto.id())).toList(); } public Slice findFriendReceivingInvitesSlice( diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/api/query/GroupQueryApi.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/api/query/GroupQueryApi.java index 42695d39b..8cf12d1d5 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/api/query/GroupQueryApi.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/api/query/GroupQueryApi.java @@ -9,7 +9,6 @@ import java.time.ZonedDateTime; import org.springframework.http.ResponseEntity; import site.timecapsulearchive.core.domain.group.data.response.GroupDetailResponse; -import site.timecapsulearchive.core.domain.group.data.response.GroupMemberInfosResponse; import site.timecapsulearchive.core.domain.group.data.response.GroupsSliceResponse; import site.timecapsulearchive.core.global.common.response.ApiSpec; diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/api/query/GroupQueryApiController.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/api/query/GroupQueryApiController.java index 4eb6a53ce..22349e302 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/api/query/GroupQueryApiController.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/api/query/GroupQueryApiController.java @@ -1,7 +1,6 @@ package site.timecapsulearchive.core.domain.group.api.query; import java.time.ZonedDateTime; -import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Slice; import org.springframework.http.ResponseEntity; @@ -13,9 +12,7 @@ import org.springframework.web.bind.annotation.RestController; import site.timecapsulearchive.core.domain.group.data.dto.CompleteGroupSummaryDto; import site.timecapsulearchive.core.domain.group.data.dto.GroupDetailTotalDto; -import site.timecapsulearchive.core.domain.group.data.dto.GroupMemberDto; import site.timecapsulearchive.core.domain.group.data.response.GroupDetailResponse; -import site.timecapsulearchive.core.domain.group.data.response.GroupMemberInfosResponse; import site.timecapsulearchive.core.domain.group.data.response.GroupsSliceResponse; import site.timecapsulearchive.core.domain.group.service.query.GroupQueryService; import site.timecapsulearchive.core.global.common.response.ApiSpec; @@ -60,7 +57,8 @@ public ResponseEntity> findGroups( @RequestParam(defaultValue = "20", value = "size") final int size, @RequestParam(value = "created_at") final ZonedDateTime createdAt ) { - final Slice groupsSlice = groupQueryService.findGroupsSlice(memberId, + final Slice groupsSlice = groupQueryService.findGroupsSlice( + memberId, size, createdAt); 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/GroupQueryRepository.java index b13299a40..d9d6b63cf 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/GroupQueryRepository.java @@ -5,7 +5,6 @@ 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.GroupMemberDto; import site.timecapsulearchive.core.domain.group.data.dto.GroupSummaryDto; 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 e94e51515..0b179f065 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 @@ -1,6 +1,5 @@ package site.timecapsulearchive.core.domain.member.entity; -import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; @@ -8,19 +7,12 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; -import jakarta.persistence.OneToMany; import jakarta.persistence.Table; import jakarta.validation.constraints.Email; -import java.util.List; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -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.history.entity.History; -import site.timecapsulearchive.core.domain.member_group.entity.MemberGroup; import site.timecapsulearchive.core.global.entity.BaseEntity; import site.timecapsulearchive.core.global.util.NullCheck; import site.timecapsulearchive.core.global.util.TagGenerator; @@ -74,24 +66,6 @@ public class Member extends BaseEntity { @Column(name = "tag", nullable = false, unique = true) private String tag; - @OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true) - private List capsules; - - @OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true) - private List groups; - - @OneToMany(mappedBy = "friend", cascade = CascadeType.ALL, orphanRemoval = true) - private List friends; - - @OneToMany(mappedBy = "owner", cascade = CascadeType.ALL, orphanRemoval = true) - private List friendsRequests; - - @OneToMany(mappedBy = "friend", cascade = CascadeType.ALL, orphanRemoval = true) - private List notifications; - - @OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true) - private List histories; - @Builder private Member(String profileUrl, String nickname, SocialType socialType, String email, String authId, String password, String tag, byte[] phone, byte[] phone_hash) { 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 8efa30cea..02c68c260 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 @@ -1,6 +1,5 @@ package site.timecapsulearchive.core.domain.member.repository; -import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/member_group/data/response/GroupSendingInviteMemberResponse.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/member_group/data/response/GroupSendingInviteMemberResponse.java index cee057b8a..f93d7a2fc 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/member_group/data/response/GroupSendingInviteMemberResponse.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/member_group/data/response/GroupSendingInviteMemberResponse.java @@ -12,9 +12,11 @@ public record GroupSendingInviteMemberResponse( String profileUrl, ZonedDateTime sendingInvitesCreatedAt ) { + public GroupSendingInviteMemberResponse { if (sendingInvitesCreatedAt != null) { - sendingInvitesCreatedAt = sendingInvitesCreatedAt.withZoneSameInstant(ResponseMappingConstant.ZONE_ID); + sendingInvitesCreatedAt = sendingInvitesCreatedAt.withZoneSameInstant( + ResponseMappingConstant.ZONE_ID); } } } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/member_group/repository/group_invite_repository/GroupInviteQueryRepository.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/member_group/repository/group_invite_repository/GroupInviteQueryRepository.java index 64e878abc..2d2008a15 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/member_group/repository/group_invite_repository/GroupInviteQueryRepository.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/member_group/repository/group_invite_repository/GroupInviteQueryRepository.java @@ -19,6 +19,8 @@ Slice findGroupReceivingInvitesSlice( final ZonedDateTime createdAt ); + List findGroupMemberIdsByGroupIdAndGroupOwnerId(final Long groupId, final Long memberId); + Slice findGroupSendingInvites( final GroupSendingInvitesSliceRequestDto dto ); diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/member_group/repository/group_invite_repository/GroupInviteQueryRepositoryImpl.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/member_group/repository/group_invite_repository/GroupInviteQueryRepositoryImpl.java index 2f1ff36aa..be6975dfd 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/member_group/repository/group_invite_repository/GroupInviteQueryRepositoryImpl.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/member_group/repository/group_invite_repository/GroupInviteQueryRepositoryImpl.java @@ -7,6 +7,7 @@ import com.querydsl.core.types.Projections; import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.jpa.impl.JPAQueryFactory; import java.sql.PreparedStatement; import java.sql.SQLException; @@ -22,6 +23,7 @@ import site.timecapsulearchive.core.domain.member_group.data.dto.GroupInviteSummaryDto; import site.timecapsulearchive.core.domain.member_group.data.dto.GroupSendingInviteMemberDto; import site.timecapsulearchive.core.domain.member_group.data.dto.GroupSendingInvitesSliceRequestDto; +import site.timecapsulearchive.core.domain.member_group.data.dto.GroupSendingInvitesSliceRequestDto; import site.timecapsulearchive.core.global.util.SliceUtil; @Repository @@ -32,8 +34,11 @@ public class GroupInviteQueryRepositoryImpl implements GroupInviteQueryRepositor private final JPAQueryFactory jpaQueryFactory; @Override - public void bulkSave(final Long groupOwnerId, final Long groupId, - final List groupMemberIds) { + public void bulkSave( + final Long groupOwnerId, + final Long groupId, + final List groupMemberIds + ) { jdbcTemplate.batchUpdate( """ INSERT INTO group_invite ( @@ -102,6 +107,19 @@ public Slice findGroupReceivingInvitesSlice( return SliceUtil.makeSlice(size, groupInviteSummaryDtos); } + @Override + public List findGroupMemberIdsByGroupIdAndGroupOwnerId( + final Long groupId, + final Long groupOwnerId + ) { + return jpaQueryFactory + .select(groupInvite.groupMember.id) + .from(groupInvite) + .where(groupInvite.groupOwner.id.eq(groupOwnerId).and(groupInvite.group.id.eq(groupId))) + .fetch(); + } + + @Override public Slice findGroupSendingInvites( final GroupSendingInvitesSliceRequestDto dto ) { diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/member_group/repository/member_group_repository/MemberGroupQueryRepository.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/member_group/repository/member_group_repository/MemberGroupQueryRepository.java index f664cc0cc..b11a3eade 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/member_group/repository/member_group_repository/MemberGroupQueryRepository.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/member_group/repository/member_group_repository/MemberGroupQueryRepository.java @@ -18,4 +18,6 @@ public interface MemberGroupQueryRepository { List findGroupMemberInfos(Long memberId, Long groupId); Optional findGroupMembersCount(Long groupId); + + List findGroupMemberIdsByGroupId(final Long groupId); } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/member_group/repository/member_group_repository/MemberGroupQueryRepositoryImpl.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/member_group/repository/member_group_repository/MemberGroupQueryRepositoryImpl.java index 9eafc40a2..84eb64bfe 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/member_group/repository/member_group_repository/MemberGroupQueryRepositoryImpl.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/member_group/repository/member_group_repository/MemberGroupQueryRepositoryImpl.java @@ -102,4 +102,12 @@ public Optional findGroupMembersCount(final Long groupId) { .fetchOne() ); } + + public List findGroupMemberIdsByGroupId(final Long groupId) { + return jpaQueryFactory + .select(memberGroup.member.id) + .from(memberGroup) + .where(memberGroup.group.id.eq(groupId)) + .fetch(); + } } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/member_group/repository/member_group_repository/MemberGroupRepository.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/member_group/repository/member_group_repository/MemberGroupRepository.java index 39393cb7b..6f2a9cf28 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/member_group/repository/member_group_repository/MemberGroupRepository.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/member_group/repository/member_group_repository/MemberGroupRepository.java @@ -3,7 +3,6 @@ import java.util.List; import java.util.Optional; import org.springframework.data.repository.Repository; -import site.timecapsulearchive.core.domain.group.data.dto.GroupMemberDto; import site.timecapsulearchive.core.domain.member_group.entity.MemberGroup; public interface MemberGroupRepository extends Repository, diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/global/config/redis/RedissonLockAspect.java b/backend/core/src/main/java/site/timecapsulearchive/core/global/config/redis/RedissonLockAspect.java index f4444c523..5430e1499 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/global/config/redis/RedissonLockAspect.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/global/config/redis/RedissonLockAspect.java @@ -32,7 +32,8 @@ public Object redissonLock(ProceedingJoinPoint joinPoint) throws Throwable { RedissonLock redissonLock = method.getAnnotation(RedissonLock.class); String lockKey = - method.getName() + DIVISION + RedisLockSpELParser.getLockKey(signature.getParameterNames(), + method.getName() + DIVISION + RedisLockSpELParser.getLockKey( + signature.getParameterNames(), joinPoint.getArgs(), redissonLock.value()); long waitTime = redissonLock.waitTime(); 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 f29833617..71f58e810 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,7 +60,7 @@ public enum ErrorCode { //friend FRIEND_NOT_FOUND_ERROR(404, "FRIEND-001", "친구를 찾지 못하였습니다"), - FRIEND_DUPLICATE_ID_ERROR(404, "FRIEND-002", "친구 아이디가 중복되었습니다."), + SELF_FRIEND_OPERATION_ERROR(400, "FRIEND-002", "자기 자신을 향해 친구와 관련된 작업을 수행할 수 없습니다."), //group GROUP_CREATE_ERROR(400, "GROUP-001", "그룹 생성에 실패하였습니다."), @@ -76,7 +76,8 @@ public enum ErrorCode { //friend invite FRIEND_INVITE_NOT_FOUND_ERROR(404, "FRIEND-INVITE-001", "친구 요청을 찾지 못하였습니다."), - FRIEND_TWO_WAY_INVITE_ERROR(400, "FRIEND-INVITE-002", "친구 요청을 받은 상태입니다."); + FRIEND_TWO_WAY_INVITE_ERROR(400, "FRIEND-INVITE-002", "친구 요청을 받은 상태입니다."), + FRIEND_INVITE_DUPLICATE_ERROR(400, "FRIEND-INVITE-003", "이미 친구 요청된 상태입니다."); private final int status; private final String code; diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/global/error/GlobalExceptionHandler.java b/backend/core/src/main/java/site/timecapsulearchive/core/global/error/GlobalExceptionHandler.java index e7ef5add0..d45f86f38 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/global/error/GlobalExceptionHandler.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/global/error/GlobalExceptionHandler.java @@ -6,6 +6,7 @@ import static site.timecapsulearchive.core.global.error.ErrorCode.REQUEST_PARAMETER_NOT_FOUND_ERROR; import static site.timecapsulearchive.core.global.error.ErrorCode.REQUEST_PARAMETER_TYPE_NOT_MATCH_ERROR; +import jakarta.persistence.PersistenceException; import jakarta.transaction.TransactionalException; import jakarta.validation.ConstraintViolationException; import lombok.extern.slf4j.Slf4j; @@ -18,6 +19,7 @@ import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; import site.timecapsulearchive.core.global.error.exception.BusinessException; +import site.timecapsulearchive.core.global.error.exception.InternalServerException; import site.timecapsulearchive.core.global.error.exception.NullCheckValidateException; import site.timecapsulearchive.core.infra.sms.exception.ExternalApiException; @@ -46,11 +48,22 @@ protected ResponseEntity handleBusinessException(final BusinessEx .body(errorResponse); } + @ExceptionHandler(InternalServerException.class) + protected ResponseEntity handleInternalServerException(final InternalServerException e) { + log.error(e.getMessage(), e); + + ErrorCode errorCode = INTERNAL_SERVER_ERROR; + final ErrorResponse errorResponse = ErrorResponse.fromErrorCode(errorCode); + + return ResponseEntity.status(errorCode.getStatus()) + .body(errorResponse); + } + @ExceptionHandler(MethodArgumentNotValidException.class) protected ResponseEntity handleRequestArgumentNotValidException( MethodArgumentNotValidException e ) { - log.warn(e.getMessage(), e); + log.error(e.getMessage(), e); final ErrorResponse response = ErrorResponse.ofBindingResult(INPUT_INVALID_VALUE_ERROR, e.getBindingResult()); @@ -62,7 +75,7 @@ protected ResponseEntity handleRequestArgumentNotValidException( protected ResponseEntity handleRequestTypeNotValidException( HttpMessageNotReadableException e ) { - log.warn(e.getMessage(), e); + log.error(e.getMessage(), e); final ErrorResponse response = ErrorResponse.fromErrorCode(INPUT_INVALID_TYPE_ERROR); return ResponseEntity.status(INPUT_INVALID_VALUE_ERROR.getStatus()) @@ -73,7 +86,7 @@ protected ResponseEntity handleRequestTypeNotValidException( protected ResponseEntity handleNullCheckValidException( NullCheckValidateException e ) { - log.warn(e.getMessage(), e); + log.error(e.getMessage(), e); final ErrorResponse response = ErrorResponse.fromParameter( REQUEST_PARAMETER_NOT_FOUND_ERROR, e.getMessage()); @@ -83,7 +96,7 @@ protected ResponseEntity handleNullCheckValidException( @ExceptionHandler(ExternalApiException.class) protected ResponseEntity handleExternalApiException(ExternalApiException e) { - log.warn(e.getMessage(), e); + log.error(e.getMessage(), e); final ErrorCode errorCode = e.getErrorCode(); final ErrorResponse response = ErrorResponse.fromErrorCode(errorCode); @@ -96,7 +109,7 @@ protected ResponseEntity handleExternalApiException(ExternalApiEx protected ResponseEntity handleMissingServletRequestParameterException( MissingServletRequestParameterException e ) { - log.warn(e.getMessage(), e); + log.error(e.getMessage(), e); final ErrorResponse errorResponse = ErrorResponse.fromParameter( REQUEST_PARAMETER_NOT_FOUND_ERROR, @@ -110,7 +123,7 @@ protected ResponseEntity handleMissingServletRequestParameterExce protected ResponseEntity handleMethodArgumentTypeMismatchException( MethodArgumentTypeMismatchException e ) { - log.warn(e.getMessage(), e); + log.error(e.getMessage(), e); final ErrorResponse errorResponse = ErrorResponse.fromType( REQUEST_PARAMETER_TYPE_NOT_MATCH_ERROR, @@ -126,7 +139,7 @@ protected ResponseEntity handleMethodArgumentTypeMismatchExceptio protected ResponseEntity handleDataIntegrityViolationException( DataIntegrityViolationException e ) { - log.warn(e.getMessage(), e); + log.error(e.getMessage(), e); ErrorCode errorCode = INPUT_INVALID_VALUE_ERROR; final ErrorResponse errorResponse = ErrorResponse.fromErrorCode(errorCode); @@ -137,7 +150,7 @@ protected ResponseEntity handleDataIntegrityViolationException( @ExceptionHandler(TransactionalException.class) protected ResponseEntity handleTransactionException(TransactionalException e) { - log.warn(e.getMessage(), e); + log.error(e.getMessage(), e); ErrorCode errorCode = INTERNAL_SERVER_ERROR; final ErrorResponse errorResponse = ErrorResponse.fromErrorCode(errorCode); @@ -149,7 +162,7 @@ protected ResponseEntity handleTransactionException(Transactional @ExceptionHandler(ConstraintViolationException.class) protected ResponseEntity handleConstraintViolationException( ConstraintViolationException e) { - log.warn(e.getMessage(), e); + log.error(e.getMessage(), e); ErrorCode errorCode = INPUT_INVALID_VALUE_ERROR; final ErrorResponse errorResponse = ErrorResponse.ofConstraints(errorCode, @@ -158,4 +171,15 @@ protected ResponseEntity handleConstraintViolationException( return ResponseEntity.status(errorCode.getStatus()) .body(errorResponse); } + + @ExceptionHandler(PersistenceException.class) + protected ResponseEntity handlePersistenceException(PersistenceException e) { + log.error(e.getMessage(), e); + + ErrorCode errorCode = INTERNAL_SERVER_ERROR; + final ErrorResponse errorResponse = ErrorResponse.fromErrorCode(errorCode); + + return ResponseEntity.status(errorCode.getStatus()) + .body(errorResponse); + } } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/global/error/exception/InternalServerException.java b/backend/core/src/main/java/site/timecapsulearchive/core/global/error/exception/InternalServerException.java new file mode 100644 index 000000000..8f8909972 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/global/error/exception/InternalServerException.java @@ -0,0 +1,10 @@ +package site.timecapsulearchive.core.global.error.exception; + +import site.timecapsulearchive.core.global.error.ErrorCode; + +public class InternalServerException extends RuntimeException { + + public InternalServerException(Throwable e) { + super(ErrorCode.INTERNAL_SERVER_ERROR.getMessage(), e); + } +} diff --git a/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/dto/FriendDtoFixture.java b/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/dto/FriendDtoFixture.java index 5cf59d72a..4ee3f18b8 100644 --- a/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/dto/FriendDtoFixture.java +++ b/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/dto/FriendDtoFixture.java @@ -1,9 +1,16 @@ package site.timecapsulearchive.core.common.fixture.dto; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.util.List; import java.util.Optional; +import java.util.stream.IntStream; import java.util.stream.LongStream; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; import site.timecapsulearchive.core.common.fixture.domain.MemberFixture; +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.global.common.wrapper.ByteArrayWrapper; @@ -35,4 +42,18 @@ public static Optional getFriendSummaryDtoByTag() { .build()); } + + public static Slice getFriendSummaryDtoSlice(int count, boolean hasNextPage) { + List dtos = IntStream.range(0, count) + .mapToObj(i -> new FriendSummaryDto( + (long) i, + i + "testProfileUrl", + i + "testNickname", + ZonedDateTime.now(ZoneId.of("UTC")).plusDays(i) + ) + ) + .toList(); + + return new SliceImpl<>(dtos, Pageable.ofSize(count), hasNextPage); + } } diff --git a/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/dto/FriendInviteMemberIdsDtoFixture.java b/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/dto/FriendInviteMemberIdsDtoFixture.java new file mode 100644 index 000000000..beed3025d --- /dev/null +++ b/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/dto/FriendInviteMemberIdsDtoFixture.java @@ -0,0 +1,41 @@ +package site.timecapsulearchive.core.common.fixture.dto; + +import java.util.List; +import java.util.Optional; +import site.timecapsulearchive.core.domain.friend.data.dto.FriendInviteMemberIdsDto; + +public class FriendInviteMemberIdsDtoFixture { + + public static List duplicates( + final Long memberId, + final List friendIds + ) { + return friendIds.stream() + .map(friendId -> new FriendInviteMemberIdsDto(memberId, friendId)) + .toList(); + } + + public static List twoWays( + final Long memberId, + final List friendIds + ) { + return friendIds.stream() + .map(friendId -> new FriendInviteMemberIdsDto(friendId, memberId)) + .toList(); + } + + public static Optional duplicate( + final Long memberId, + final Long friendId + ) { + return Optional.of( + new FriendInviteMemberIdsDto(memberId, friendId) + ); + } + + public static Optional twoWay(Long memberId, Long friendId) { + return Optional.of( + new FriendInviteMemberIdsDto(friendId, memberId) + ); + } +} diff --git a/backend/core/src/test/java/site/timecapsulearchive/core/domain/friend/repository/FriendInviteQueryRepositoryTest.java b/backend/core/src/test/java/site/timecapsulearchive/core/domain/friend/repository/friend_invite/FriendInviteQueryRepositoryTest.java similarity index 80% rename from backend/core/src/test/java/site/timecapsulearchive/core/domain/friend/repository/FriendInviteQueryRepositoryTest.java rename to backend/core/src/test/java/site/timecapsulearchive/core/domain/friend/repository/friend_invite/FriendInviteQueryRepositoryTest.java index 8233b0761..c65fb10c0 100644 --- a/backend/core/src/test/java/site/timecapsulearchive/core/domain/friend/repository/FriendInviteQueryRepositoryTest.java +++ b/backend/core/src/test/java/site/timecapsulearchive/core/domain/friend/repository/friend_invite/FriendInviteQueryRepositoryTest.java @@ -1,4 +1,4 @@ -package site.timecapsulearchive.core.domain.friend.repository; +package site.timecapsulearchive.core.domain.friend.repository.friend_invite; import static org.assertj.core.api.Assertions.assertThat; @@ -20,10 +20,9 @@ import site.timecapsulearchive.core.common.RepositoryTest; import site.timecapsulearchive.core.common.fixture.domain.FriendInviteFixture; import site.timecapsulearchive.core.common.fixture.domain.MemberFixture; +import site.timecapsulearchive.core.domain.friend.data.dto.FriendInviteMemberIdsDto; import site.timecapsulearchive.core.domain.friend.data.dto.FriendSummaryDto; import site.timecapsulearchive.core.domain.friend.entity.FriendInvite; -import site.timecapsulearchive.core.domain.friend.repository.friend_invite.FriendInviteQueryRepository; -import site.timecapsulearchive.core.domain.friend.repository.friend_invite.FriendInviteQueryRepositoryImpl; import site.timecapsulearchive.core.domain.member.entity.Member; @TestConstructor(autowireMode = AutowireMode.ALL) @@ -31,8 +30,9 @@ class FriendInviteQueryRepositoryTest extends RepositoryTest { private static final int MAX_COUNT = 40; private static final Long BULK_FRIEND_INVITE_MEMBER_START_ID = 2L; + private static final Long OWNER_START_ID = BULK_FRIEND_INVITE_MEMBER_START_ID + MAX_COUNT; private static final Long FRIEND_RECEIVING_INVITE_MEMBER_START_ID = - BULK_FRIEND_INVITE_MEMBER_START_ID + MAX_COUNT; + OWNER_START_ID + MAX_COUNT; private static final Long FRIEND_SENDING_INVITE_MEMBER_START_ID = FRIEND_RECEIVING_INVITE_MEMBER_START_ID + MAX_COUNT; private static final Long NOT_FRIEND_INVITE_START_ID = @@ -40,15 +40,21 @@ class FriendInviteQueryRepositoryTest extends RepositoryTest { private final FriendInviteQueryRepository friendInviteQueryRepository; private final EntityManager entityManager; - private final List friends = new ArrayList<>(); + + private final List bulkFriends = new ArrayList<>(); + private final List receivingInviteFriendIds = new ArrayList<>(); + private final List sendingInvitesFriendIds = new ArrayList<>(); private Long bulkOwnerId; private Long ownerId; private Long ownerInviteReceivingStartId; private Long ownerInviteSendingStartId; - FriendInviteQueryRepositoryTest(EntityManager entityManager, JdbcTemplate jdbcTemplate, - JPAQueryFactory jpaQueryFactory) { + FriendInviteQueryRepositoryTest( + EntityManager entityManager, + JdbcTemplate jdbcTemplate, + JPAQueryFactory jpaQueryFactory + ) { this.entityManager = entityManager; this.friendInviteQueryRepository = new FriendInviteQueryRepositoryImpl(jdbcTemplate, jpaQueryFactory); @@ -62,11 +68,12 @@ void setup() { bulkOwnerId = bulkOwner.getId(); // 벌크 저장 시 owner 친구 데이터 - friends.addAll(MemberFixture.members(2, BULK_FRIEND_INVITE_MEMBER_START_ID.intValue())); - friends.forEach(entityManager::persist); + bulkFriends.addAll(MemberFixture.members(BULK_FRIEND_INVITE_MEMBER_START_ID.intValue(), + BULK_FRIEND_INVITE_MEMBER_START_ID.intValue())); + bulkFriends.forEach(entityManager::persist); // 친구 초대 owner 멤버 데이터 - Member owner = MemberFixture.member(1); + Member owner = MemberFixture.member(OWNER_START_ID.intValue()); entityManager.persist(owner); ownerId = owner.getId(); @@ -75,6 +82,7 @@ void setup() { FRIEND_RECEIVING_INVITE_MEMBER_START_ID.intValue(), MAX_COUNT); for (Member member : receivingInviteToOwnerMembers) { entityManager.persist(member); + receivingInviteFriendIds.add(member.getId()); FriendInvite receivingInvite = FriendInviteFixture.friendInvite(owner, member); entityManager.persist(receivingInvite); @@ -86,6 +94,7 @@ void setup() { FRIEND_SENDING_INVITE_MEMBER_START_ID.intValue(), MAX_COUNT); for (Member member : sendingInviteToOwnerMembers) { entityManager.persist(member); + sendingInvitesFriendIds.add(member.getId()); FriendInvite sendingInvite = FriendInviteFixture.friendInvite(member, owner); entityManager.persist(sendingInvite); @@ -99,7 +108,7 @@ void setup() { @Test void 대량의_친구_초대를_저장하면_조회하면_친구_초대를_볼_수_있다() { //given - List friendIds = friends.stream() + List friendIds = bulkFriends.stream() .map(Member::getId) .toList(); @@ -252,4 +261,30 @@ private List getFriendInvites(EntityManager entityManager, Long ow //then assertThat(slice).isEmpty(); } + + //Friend -> Owner + @Test + void 친구가_사용자에게_요청을_보낸_경우_사용자_아이디와_친구_아이디_목록으로_모든_요청_방향의_친구_초대를_조회하면_존재하는_단방향_친구_초대가_나온다() { + //given + //when + List friendInviteMemberIdsDtos = friendInviteQueryRepository.findFriendInviteMemberIdsDtoByMemberIdsAndFriendId( + sendingInvitesFriendIds, + ownerId + ); + + assertThat(friendInviteMemberIdsDtos).hasSize(sendingInvitesFriendIds.size()); + } + + //Owner -> Friend + @Test + void 사용자가_친구에게_요청을_보낸_경우_사용자_아이디와_친구_아이디_목록으로_모든_요청_방향의_친구_초대를_조회하면_존재하는_단방향_친구_초대가_나온다() { + //given + //when + List friendInviteMemberIdsDtos = friendInviteQueryRepository.findFriendInviteMemberIdsDtoByMemberIdsAndFriendId( + receivingInviteFriendIds, + ownerId + ); + + assertThat(friendInviteMemberIdsDtos).hasSize(receivingInviteFriendIds.size()); + } } \ No newline at end of file diff --git a/backend/core/src/test/java/site/timecapsulearchive/core/domain/friend/repository/MemberFriendQueryRepositoryTest.java b/backend/core/src/test/java/site/timecapsulearchive/core/domain/friend/repository/member_friend/MemberFriendQueryRepositoryTest.java similarity index 98% rename from backend/core/src/test/java/site/timecapsulearchive/core/domain/friend/repository/MemberFriendQueryRepositoryTest.java rename to backend/core/src/test/java/site/timecapsulearchive/core/domain/friend/repository/member_friend/MemberFriendQueryRepositoryTest.java index 86842d9c4..14af65234 100644 --- a/backend/core/src/test/java/site/timecapsulearchive/core/domain/friend/repository/MemberFriendQueryRepositoryTest.java +++ b/backend/core/src/test/java/site/timecapsulearchive/core/domain/friend/repository/member_friend/MemberFriendQueryRepositoryTest.java @@ -1,4 +1,4 @@ -package site.timecapsulearchive.core.domain.friend.repository; +package site.timecapsulearchive.core.domain.friend.repository.member_friend; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -33,8 +33,6 @@ import site.timecapsulearchive.core.domain.friend.data.dto.SearchFriendSummaryDtoByTag; import site.timecapsulearchive.core.domain.friend.entity.FriendInvite; import site.timecapsulearchive.core.domain.friend.entity.MemberFriend; -import site.timecapsulearchive.core.domain.friend.repository.member_friend.MemberFriendQueryRepository; -import site.timecapsulearchive.core.domain.friend.repository.member_friend.MemberFriendQueryRepositoryImpl; import site.timecapsulearchive.core.domain.member.entity.Member; @TestConstructor(autowireMode = AutowireMode.ALL) diff --git a/backend/core/src/test/java/site/timecapsulearchive/core/domain/friend/repository/MemberFriendRepositoryTest.java b/backend/core/src/test/java/site/timecapsulearchive/core/domain/friend/repository/member_friend/MemberFriendRepositoryTest.java similarity index 94% rename from backend/core/src/test/java/site/timecapsulearchive/core/domain/friend/repository/MemberFriendRepositoryTest.java rename to backend/core/src/test/java/site/timecapsulearchive/core/domain/friend/repository/member_friend/MemberFriendRepositoryTest.java index 2a449daab..33a226749 100644 --- a/backend/core/src/test/java/site/timecapsulearchive/core/domain/friend/repository/MemberFriendRepositoryTest.java +++ b/backend/core/src/test/java/site/timecapsulearchive/core/domain/friend/repository/member_friend/MemberFriendRepositoryTest.java @@ -1,4 +1,4 @@ -package site.timecapsulearchive.core.domain.friend.repository; +package site.timecapsulearchive.core.domain.friend.repository.member_friend; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.SoftAssertions.assertSoftly; @@ -15,7 +15,6 @@ import site.timecapsulearchive.core.common.fixture.domain.MemberFixture; import site.timecapsulearchive.core.common.fixture.domain.MemberFriendFixture; import site.timecapsulearchive.core.domain.friend.entity.MemberFriend; -import site.timecapsulearchive.core.domain.friend.repository.member_friend.MemberFriendRepository; import site.timecapsulearchive.core.domain.member.entity.Member; @TestConstructor(autowireMode = AutowireMode.ALL) diff --git a/backend/core/src/test/java/site/timecapsulearchive/core/domain/friend/service/command/FriendCommandServiceTest.java b/backend/core/src/test/java/site/timecapsulearchive/core/domain/friend/service/command/FriendCommandServiceTest.java new file mode 100644 index 000000000..63523ef95 --- /dev/null +++ b/backend/core/src/test/java/site/timecapsulearchive/core/domain/friend/service/command/FriendCommandServiceTest.java @@ -0,0 +1,221 @@ +package site.timecapsulearchive.core.domain.friend.service.command; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import java.util.Collections; +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.MemberFixture; +import site.timecapsulearchive.core.common.fixture.dto.FriendInviteMemberIdsDtoFixture; +import site.timecapsulearchive.core.domain.friend.exception.FriendInviteDuplicateException; +import site.timecapsulearchive.core.domain.friend.exception.FriendInviteNotFoundException; +import site.timecapsulearchive.core.domain.friend.exception.FriendTwoWayInviteException; +import site.timecapsulearchive.core.domain.friend.exception.SelfFriendOperationException; +import site.timecapsulearchive.core.domain.friend.repository.friend_invite.FriendInviteRepository; +import site.timecapsulearchive.core.domain.friend.repository.member_friend.MemberFriendRepository; +import site.timecapsulearchive.core.domain.member.repository.MemberRepository; +import site.timecapsulearchive.core.global.error.ErrorCode; +import site.timecapsulearchive.core.infra.queue.manager.SocialNotificationManager; + +class FriendCommandServiceTest { + + private final MemberFriendRepository memberFriendRepository = mock( + MemberFriendRepository.class); + private final MemberRepository memberRepository = mock(MemberRepository.class); + private final FriendInviteRepository friendInviteRepository = mock( + FriendInviteRepository.class); + private final SocialNotificationManager socialNotificationManager = mock( + SocialNotificationManager.class); + private final TransactionTemplate transactionTemplate = TestTransactionTemplate.spied(); + + private final FriendCommandService friendCommandService = new FriendCommandService( + memberFriendRepository, + memberRepository, + friendInviteRepository, + socialNotificationManager, + transactionTemplate + ); + + @Test + void 사용자_본인한테_다건_친구_요청을_보낸_경우_어떠한_동작도_수행하지_않는다() { + //given + Long memberId = 1L; + List friendIds = List.of(1L, 1L, 1L, 1L); + given(friendInviteRepository.findFriendInviteMemberIdsDtoByMemberIdsAndFriendId(anyList(), + any())) + .willReturn(Collections.emptyList()); + + //when + friendCommandService.requestFriends(memberId, friendIds); + + //then + verify(transactionTemplate, never()).execute(any()); + } + + @Test + void 이미_친구요청된_친구에게_다건_친구_요청을_보낸_경우_어떠한_동작도_수행하지_않는다() { + //given + Long memberId = 1L; + List friendIds = List.of(2L, 3L, 4L, 5L, 6L); + given(friendInviteRepository.findFriendInviteMemberIdsDtoByMemberIdsAndFriendId(anyList(), + any())) + .willReturn(FriendInviteMemberIdsDtoFixture.duplicates(memberId, friendIds)); + + //when + friendCommandService.requestFriends(memberId, friendIds); + + //then + verify(transactionTemplate, never()).execute(any()); + } + + @Test + void 양방향_다건_친구_요청을_보낸_경우_어떠한_동작도_수행하지_않는다() { + //given + Long memberId = 1L; + List friendIds = List.of(2L, 3L, 4L, 5L, 6L); + given(friendInviteRepository.findFriendInviteMemberIdsDtoByMemberIdsAndFriendId(anyList(), + any())) + .willReturn(FriendInviteMemberIdsDtoFixture.twoWays(memberId, friendIds)); + + //when + friendCommandService.requestFriends(memberId, friendIds); + + //then + verify(transactionTemplate, never()).execute(any()); + } + + @Test + void 친구_요청을_보낸_경우_트랜잭션이_동작된다() { + //given + Long memberId = 1L; + List friendIds = List.of(2L, 3L, 4L, 5L, 6L); + List subFriendIds = friendIds.subList(0, 3); + given(friendInviteRepository.findFriendInviteMemberIdsDtoByMemberIdsAndFriendId(anyList(), + any())) + .willReturn(FriendInviteMemberIdsDtoFixture.twoWays(memberId, subFriendIds)); + given(memberRepository.findMemberById(anyLong())).willReturn( + Optional.ofNullable(MemberFixture.memberWithMemberId(0L))); + + //when + friendCommandService.requestFriends(memberId, friendIds); + + //then + verify(transactionTemplate, times(1)).execute(any()); + } + + @Test + void 사용자_본인한테_단건_친구_요청을_보낸_경우_예외가_발생한다() { + //given + Long memberId = 1L; + + //when + //then + assertThatThrownBy(() -> friendCommandService.requestFriend(memberId, memberId)) + .isInstanceOf(SelfFriendOperationException.class) + .hasMessageContaining(ErrorCode.SELF_FRIEND_OPERATION_ERROR.getMessage()); + } + + @Test + void 이미_친구요청된_친구에게_단건_친구_요청을_보낸_경우_예외가_발생한다() { + //given + Long memberId = 1L; + Long friendId = 2L; + given(friendInviteRepository.findFriendInviteMemberIdsDtoByMemberIdAndFriendId(anyLong(), anyLong())) + .willReturn(FriendInviteMemberIdsDtoFixture.duplicate(memberId, friendId)); + + //when + //then + assertThatThrownBy(() -> friendCommandService.requestFriend(memberId, friendId)) + .isInstanceOf(FriendInviteDuplicateException.class) + .hasMessageContaining(ErrorCode.FRIEND_INVITE_DUPLICATE_ERROR.getMessage()); + } + + @Test + void 양방향_단건_친구_요청을_보낸_경우_예외가_발생한다() { + //given + Long memberId = 1L; + Long friendId = 2L; + given(friendInviteRepository.findFriendInviteMemberIdsDtoByMemberIdAndFriendId(anyLong(), anyLong())) + .willReturn(FriendInviteMemberIdsDtoFixture.twoWay(memberId, friendId)); + + //when + //then + assertThatThrownBy(() -> friendCommandService.requestFriend(memberId, friendId)) + .isInstanceOf(FriendTwoWayInviteException.class) + .hasMessageContaining(ErrorCode.FRIEND_TWO_WAY_INVITE_ERROR.getMessage()); + } + + @Test + void 사용자_본인한테_친구_요청을_수락한_경우_예외가_발생한다() { + //given + Long memberId = 1L; + + //when + //then + assertThatThrownBy(() -> friendCommandService.acceptFriend(memberId, memberId)) + .isInstanceOf(SelfFriendOperationException.class) + .hasMessageContaining(ErrorCode.SELF_FRIEND_OPERATION_ERROR.getMessage()); + } + + @Test + void 사용자_본인한테_친구_요청을_거부한_경우_예외가_발생한다() { + //given + Long memberId = 1L; + + //when + //then + assertThatThrownBy(() -> friendCommandService.denyRequestFriend(memberId, memberId)) + .isInstanceOf(SelfFriendOperationException.class) + .hasMessageContaining(ErrorCode.SELF_FRIEND_OPERATION_ERROR.getMessage()); + } + + @Test + void 사용자_본인한테_친구_삭제를_요청한_경우_예외가_발생한다() { + //given + Long memberId = 1L; + + //when + //then + assertThatThrownBy(() -> friendCommandService.deleteFriend(memberId, memberId)) + .isInstanceOf(SelfFriendOperationException.class) + .hasMessageContaining(ErrorCode.SELF_FRIEND_OPERATION_ERROR.getMessage()); + } + + @Test + void 친구_요청을_보낸_사용자가_본인의_아이디로_친구_요청을_삭제하면_예외가_발생한다() { + //given + Long memberId = 1L; + + //when + //then + assertThatThrownBy(() -> friendCommandService.deleteSendingFriendInvite(memberId, memberId)) + .isInstanceOf(SelfFriendOperationException.class) + .hasMessageContaining(ErrorCode.SELF_FRIEND_OPERATION_ERROR.getMessage()); + } + + @Test + void 친구_요청을_보내지_않은_사용자가_친구_요청을_삭제하면_예외가_발생한다() { + //given + Long memberId = 1L; + Long friendId = 2L; + given(friendInviteRepository.findFriendSendingInviteForUpdateByOwnerIdAndFriendId(anyLong(), + anyLong())).willReturn(Optional.empty()); + + //when + //then + assertThatThrownBy(() -> friendCommandService.deleteSendingFriendInvite(memberId, friendId)) + .isInstanceOf(FriendInviteNotFoundException.class) + .hasMessageContaining(ErrorCode.FRIEND_INVITE_NOT_FOUND_ERROR.getMessage()); + } +} \ No newline at end of file diff --git a/backend/core/src/test/java/site/timecapsulearchive/core/domain/friend/service/FriendQueryServiceTest.java b/backend/core/src/test/java/site/timecapsulearchive/core/domain/friend/service/query/FriendQueryServiceTest.java similarity index 52% rename from backend/core/src/test/java/site/timecapsulearchive/core/domain/friend/service/FriendQueryServiceTest.java rename to backend/core/src/test/java/site/timecapsulearchive/core/domain/friend/service/query/FriendQueryServiceTest.java index 97e83bb9f..4596edcc3 100644 --- a/backend/core/src/test/java/site/timecapsulearchive/core/domain/friend/service/FriendQueryServiceTest.java +++ b/backend/core/src/test/java/site/timecapsulearchive/core/domain/friend/service/query/FriendQueryServiceTest.java @@ -1,4 +1,4 @@ -package site.timecapsulearchive.core.domain.friend.service; +package site.timecapsulearchive.core.domain.friend.service.query; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -9,30 +9,43 @@ import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.Optional; import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.Test; +import org.springframework.data.domain.Slice; import site.timecapsulearchive.core.common.fixture.domain.MemberFixture; import site.timecapsulearchive.core.common.fixture.dto.FriendDtoFixture; +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.request.FriendBeforeGroupInviteRequest; import site.timecapsulearchive.core.domain.friend.exception.FriendNotFoundException; import site.timecapsulearchive.core.domain.friend.repository.friend_invite.FriendInviteRepository; import site.timecapsulearchive.core.domain.friend.repository.member_friend.MemberFriendRepository; -import site.timecapsulearchive.core.domain.friend.service.query.FriendQueryService; +import site.timecapsulearchive.core.domain.member_group.repository.group_invite_repository.GroupInviteRepository; +import site.timecapsulearchive.core.domain.member_group.repository.member_group_repository.MemberGroupRepository; import site.timecapsulearchive.core.global.common.wrapper.ByteArrayWrapper; class FriendQueryServiceTest { private final MemberFriendRepository memberFriendRepository = mock( MemberFriendRepository.class); + private final MemberGroupRepository memberGroupRepository = mock(MemberGroupRepository.class); + private final GroupInviteRepository groupInviteRepository = mock(GroupInviteRepository.class); private final FriendInviteRepository friendInviteRepository = mock( FriendInviteRepository.class); private final FriendQueryService friendQueryService = new FriendQueryService( - memberFriendRepository, friendInviteRepository); + memberFriendRepository, + memberGroupRepository, + groupInviteRepository, + friendInviteRepository + ); @Test void 사용자는_주소록_기반_핸드폰_번호로_Ahchive_사용자_리스트를_조회_할_수_있다() { @@ -104,4 +117,66 @@ class FriendQueryServiceTest { assertThatThrownBy(() -> friendQueryService.searchFriend(memberId, tag)) .isInstanceOf(FriendNotFoundException.class); } + + @Test + void 그룹장은_그룹_초대_전_초대_가능한_친구_목록을_조회할_수_있다() { + //given + Long memberId = 1L; + Long groupId = 1L; + int size = 20; + ZonedDateTime now = ZonedDateTime.now(ZoneId.of("UTC")).plusDays(1); + + FriendBeforeGroupInviteRequest request = FriendBeforeGroupInviteRequest.of(memberId, + groupId, + size, now); + given(memberFriendRepository.findFriends(request)).willReturn( + FriendDtoFixture.getFriendSummaryDtoSlice(5, true)); + given(memberGroupRepository.findGroupMemberIdsByGroupId(request.groupId())).willReturn( + List.of(3L)); + given(groupInviteRepository.findGroupMemberIdsByGroupIdAndGroupOwnerId(request.groupId(), + request.memberId())).willReturn(List.of(4L)); + + Slice friendsBeforeGroupInviteSlice = friendQueryService.findFriendsBeforeGroupInviteSlice( + request); + + SoftAssertions.assertSoftly( + softly -> { + assertThat(friendsBeforeGroupInviteSlice.getContent()).isNotEmpty(); + assertThat(friendsBeforeGroupInviteSlice.getContent()).allMatch( + dto -> !dto.profileUrl().isBlank()); + assertThat(friendsBeforeGroupInviteSlice.getContent()).allMatch( + dto -> !dto.nickname().isBlank()); + assertThat(friendsBeforeGroupInviteSlice.getContent()).allMatch( + dto -> Objects.nonNull(dto.id())); + assertThat(friendsBeforeGroupInviteSlice.getContent()).allMatch( + dto -> Objects.nonNull(dto.createdAt())); + assertThat(friendsBeforeGroupInviteSlice.hasNext()).isTrue(); + assertThat(friendsBeforeGroupInviteSlice.getSize()).isEqualTo(5); + } + ); + } + + @Test + void 그룹장은_그룹_초대_전_이미_그룹멤버_혹은_그룹_요청을_보낸_사용자를_제외하고_초대_가능한_사용자를_조회한다() { + //given + Long memberId = 1L; + Long groupId = 1L; + int size = 20; + ZonedDateTime now = ZonedDateTime.now(ZoneId.of("UTC")).plusDays(1); + + FriendBeforeGroupInviteRequest request = FriendBeforeGroupInviteRequest.of(memberId, + groupId, + size, now); + given(memberFriendRepository.findFriends(request)).willReturn( + FriendDtoFixture.getFriendSummaryDtoSlice(5, true)); + given(memberGroupRepository.findGroupMemberIdsByGroupId(request.groupId())).willReturn( + List.of(3L)); + given(groupInviteRepository.findGroupMemberIdsByGroupIdAndGroupOwnerId(request.groupId(), + request.memberId())).willReturn(List.of(4L)); + + Slice friendsBeforeGroupInviteSlice = friendQueryService.findFriendsBeforeGroupInviteSlice( + request); + + assertThat(friendsBeforeGroupInviteSlice.getContent()).isNotIn(3L, 4L); + } } \ No newline at end of file