From ab92eb867e626294fd8d3a2a0412c0d77379a134 Mon Sep 17 00:00:00 2001 From: hong seokho Date: Sat, 1 Jun 2024 18:48:29 +0900 Subject: [PATCH 01/10] =?UTF-8?q?feat=20:=20=EC=B9=9C=EA=B5=AC=20=EC=B4=88?= =?UTF-8?q?=EB=8C=80=20=EC=82=AD=EC=A0=9C=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../friend/api/command/FriendCommandApi.java | 26 ++++++++++++++++ .../command/FriendCommandApiController.java | 20 +++++++++++-- .../friend_invite/FriendInviteRepository.java | 8 +++++ .../service/command/FriendCommandService.java | 13 ++++++++ .../global/error/GlobalExceptionHandler.java | 30 +++++++++++++------ 5 files changed, 86 insertions(+), 11 deletions(-) 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/repository/friend_invite/FriendInviteRepository.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/repository/friend_invite/FriendInviteRepository.java index 944e7bedf..562710440 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,8 +1,12 @@ package site.timecapsulearchive.core.domain.friend.repository.friend_invite; +import jakarta.persistence.LockModeType; +import jakarta.persistence.QueryHint; import java.util.List; import java.util.Optional; +import org.springframework.data.jpa.repository.Lock; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.jpa.repository.QueryHints; import org.springframework.data.repository.Repository; import org.springframework.data.repository.query.Param; import site.timecapsulearchive.core.domain.friend.entity.FriendInvite; @@ -28,5 +32,9 @@ List findFriendInviteWithMembersByOwnerIdAndFriendId( int deleteFriendInviteByOwnerIdAndFriendId(Long memberId, Long targetId); void delete(FriendInvite friendInvite); + + @Lock(LockModeType.READ) + @QueryHints({@QueryHint(name = "javax.persistence.lock.timeout", value = "3000")}) + Optional findFriendInviteForUpdateByOwnerIdAndFriendId(Long memberId, Long targetId); } 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 00ada5929..8d153cce5 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 @@ -1,8 +1,10 @@ package site.timecapsulearchive.core.domain.friend.service.command; +import jakarta.persistence.PersistenceException; import java.util.List; import java.util.Optional; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.annotation.Transactional; @@ -20,6 +22,7 @@ import site.timecapsulearchive.core.domain.member.repository.MemberRepository; import site.timecapsulearchive.core.infra.queue.manager.SocialNotificationManager; +@Slf4j @Service @RequiredArgsConstructor public class FriendCommandService { @@ -142,4 +145,14 @@ public void deleteFriend(final Long memberId, final Long friendId) { memberFriends.forEach(memberFriendRepository::delete); } + @Transactional + public void deleteSendingFriendInvite(final Long memberId, final Long friendId) { + validateFriendDuplicateId(memberId, friendId); + + FriendInvite friendInvite = friendInviteRepository.findFriendInviteForUpdateByOwnerIdAndFriendId( + memberId, friendId) + .orElseThrow(FriendInviteNotFoundException::new); + + friendInviteRepository.delete(friendInvite); + } } 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..459f36da8 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; @@ -50,7 +51,7 @@ protected ResponseEntity handleBusinessException(final BusinessEx 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 +63,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 +74,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 +84,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 +97,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 +111,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 +127,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 +138,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 +150,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 +159,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); + } } From ee4903276608985f1a1f7e4c097dda55a61a6ad4 Mon Sep 17 00:00:00 2001 From: hong seokho Date: Sun, 2 Jun 2024 14:46:10 +0900 Subject: [PATCH 02/10] =?UTF-8?q?feat=20:=20=EC=B9=9C=EA=B5=AC=20=EC=B4=88?= =?UTF-8?q?=EB=8C=80=20=EC=82=AD=EC=A0=9C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20=EA=B8=B0=EC=A1=B4=20API=20=EB=8F=99?= =?UTF-8?q?=EC=8B=9C=EC=84=B1,=20=EC=83=81=ED=83=9C=20=EA=B2=80=EC=A6=9D?= =?UTF-8?q?=20=EA=B4=80=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 +- .../data/dto/FriendInviteMemberIdsDto.java | 28 +++++ .../FriendInviteDuplicateException.java | 11 ++ ...java => SelfFriendOperationException.java} | 6 +- .../FriendInviteQueryRepository.java | 6 ++ .../FriendInviteQueryRepositoryImpl.java | 53 +++++++++ .../friend_invite/FriendInviteRepository.java | 34 +++--- .../service/command/FriendCommandService.java | 102 ++++++++++-------- .../core/global/error/ErrorCode.java | 5 +- 9 files changed, 179 insertions(+), 68 deletions(-) create mode 100644 backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/data/dto/FriendInviteMemberIdsDto.java create mode 100644 backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/exception/FriendInviteDuplicateException.java rename backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/exception/{FriendDuplicateIdException.java => SelfFriendOperationException.java} (55%) diff --git a/.gitignore b/.gitignore index 91cd78597..51e94bb56 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ .idea -data \ No newline at end of file +backend/data \ No newline at end of file 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..65f6132f1 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/data/dto/FriendInviteMemberIdsDto.java @@ -0,0 +1,28 @@ +package site.timecapsulearchive.core.domain.friend.data.dto; + +import java.util.Objects; + +public record FriendInviteMemberIdsDto( + Long ownerId, + Long friendId +) { + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + FriendInviteMemberIdsDto that = (FriendInviteMemberIdsDto) o; + return (Objects.equals(ownerId, that.ownerId) && Objects.equals(friendId, that.friendId)) + || (Objects.equals(ownerId, that.friendId) && Objects.equals(friendId, that.ownerId)); + } + + @Override + public int hashCode() { + return Objects.hash(ownerId, friendId); + } +} 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 d8887fb0d..aec48ac1d 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 @@ -1,8 +1,14 @@ package site.timecapsulearchive.core.domain.friend.repository.friend_invite; import java.util.List; +import java.util.Optional; +import site.timecapsulearchive.core.domain.friend.data.dto.FriendInviteMemberIdsDto; public interface FriendInviteQueryRepository { void bulkSave(final Long ownerId, final List friendIds); + + 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 136441e2d..8c156acf0 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 @@ -1,21 +1,29 @@ package site.timecapsulearchive.core.domain.friend.repository.friend_invite; +import static site.timecapsulearchive.core.domain.friend.entity.QFriendInvite.friendInvite; + +import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.types.Projections; +import com.querydsl.jpa.impl.JPAQueryFactory; 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 java.util.Optional; import lombok.RequiredArgsConstructor; 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; @Repository @RequiredArgsConstructor public class FriendInviteQueryRepositoryImpl implements FriendInviteQueryRepository { private final JdbcTemplate jdbcTemplate; + private final JPAQueryFactory jpaQueryFactory; public void bulkSave(final Long ownerId, final List friendIds) { if (friendIds.isEmpty()) { @@ -47,4 +55,49 @@ public int getBatchSize() { } ); } + + @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 562710440..e37380046 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 @@ -16,25 +16,25 @@ 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, - @Param(value = "friendId") Long friendId - ); - - Optional findFriendInviteByOwnerIdAndFriendId(Long memberId, Long targetId); - - int deleteFriendInviteByOwnerIdAndFriendId(Long memberId, Long targetId); - void delete(FriendInvite friendInvite); @Lock(LockModeType.READ) - @QueryHints({@QueryHint(name = "javax.persistence.lock.timeout", value = "3000")}) - Optional findFriendInviteForUpdateByOwnerIdAndFriendId(Long memberId, Long targetId); + @QueryHints({@QueryHint(name = "jakarta.persistence.lock.timeout", value = "3000")}) + 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 =:friendId and fi.friend.id =:memberId + """) + @Lock(LockModeType.READ) + @QueryHints({@QueryHint(name = "jakarta.persistence.lock.timeout", value = "3000")}) + Optional findFriendReceptionInviteForUpdateByOwnerIdAndFriendId( + @Param(value = "memberId") Long memberId, + @Param(value = "friendId") Long friendId + ); } 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 8d153cce5..8c4d885a4 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 @@ -1,6 +1,5 @@ package site.timecapsulearchive.core.domain.friend.service.command; -import jakarta.persistence.PersistenceException; import java.util.List; import java.util.Optional; import lombok.RequiredArgsConstructor; @@ -10,16 +9,19 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionCallbackWithoutResult; import org.springframework.transaction.support.TransactionTemplate; +import site.timecapsulearchive.core.domain.friend.data.dto.FriendInviteMemberIdsDto; 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; 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; @Slf4j @@ -34,6 +36,12 @@ public class FriendCommandService { private final TransactionTemplate transactionTemplate; public void requestFriends(Long memberId, List friendIds) { + List filteredFriendIds = filterTwoWayAndDuplicateInvite(memberId, friendIds); + + if (filteredFriendIds.isEmpty()) { + return; + } + final Member[] owner = new Member[1]; final List[] foundFriendIds = new List[1]; @@ -42,7 +50,7 @@ public void requestFriends(Long memberId, List friendIds) { protected void doInTransactionWithoutResult(TransactionStatus status) { owner[0] = memberRepository.findMemberById(memberId) .orElseThrow(MemberNotFoundException::new); - foundFriendIds[0] = memberRepository.findMemberIdsByIds(friendIds); + foundFriendIds[0] = memberRepository.findMemberIdsByIds(filteredFriendIds); friendInviteRepository.bulkSave(owner[0].getId(), foundFriendIds[0]); } @@ -55,9 +63,19 @@ protected void doInTransactionWithoutResult(TransactionStatus status) { ); } + private List filterTwoWayAndDuplicateInvite(Long memberId, List friendIds) { + List twoWayIds = friendInviteRepository.findFriendInviteMemberIdsDtoByMemberIdsAndFriendId( + friendIds, memberId); + + return friendIds.stream() + .filter(id -> !memberId.equals(id)) + .filter(id -> !twoWayIds.contains(new FriendInviteMemberIdsDto(id, memberId))) + .toList(); + } + 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); @@ -77,67 +95,61 @@ protected void doInTransactionWithoutResult(TransactionStatus status) { 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); + validateSelfFriendOperation(memberId, friendId); - final String[] ownerNickname = new String[1]; - transactionTemplate.execute(new TransactionCallbackWithoutResult() { - @Override - protected void doInTransactionWithoutResult(TransactionStatus status) { - final List friendInvites = friendInviteRepository - .findFriendInviteWithMembersByOwnerIdAndFriendId(memberId, friendId); - - if (friendInvites.isEmpty()) { - throw new FriendInviteNotFoundException(); - } - - final FriendInvite friendInvite = friendInvites.get(0); + final String ownerNickname = transactionTemplate.execute(status -> { + FriendInvite friendInvite = friendInviteRepository.findFriendReceptionInviteForUpdateByOwnerIdAndFriendId( + memberId, friendId) + .orElseThrow(FriendInviteNotFoundException::new); - final MemberFriend ownerRelation = friendInvite.ownerRelation(); - ownerNickname[0] = ownerRelation.getOwnerNickname(); + final MemberFriend ownerRelation = friendInvite.ownerRelation(); + final MemberFriend friendRelation = friendInvite.friendRelation(); - final MemberFriend friendRelation = friendInvite.friendRelation(); + memberFriendRepository.save(ownerRelation); + memberFriendRepository.save(friendRelation); + friendInviteRepository.delete(friendInvite); - memberFriendRepository.save(ownerRelation); - memberFriendRepository.save(friendRelation); - friendInvites.forEach(friendInviteRepository::delete); - } + return ownerRelation.getOwnerNickname(); }); - socialNotificationManager.sendFriendAcceptMessage(ownerNickname[0], friendId); + socialNotificationManager.sendFriendAcceptMessage(ownerNickname, friendId); } @Transactional public void denyRequestFriend(final Long memberId, final Long friendId) { - validateFriendDuplicateId(memberId, friendId); + validateSelfFriendOperation(memberId, friendId); - int isDenyRequest = friendInviteRepository.deleteFriendInviteByOwnerIdAndFriendId(friendId, - memberId); + FriendInvite friendInvite = friendInviteRepository.findFriendReceptionInviteForUpdateByOwnerIdAndFriendId( + memberId, friendId) + .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); @@ -147,11 +159,11 @@ public void deleteFriend(final Long memberId, final Long friendId) { @Transactional public void deleteSendingFriendInvite(final Long memberId, final Long friendId) { - validateFriendDuplicateId(memberId, friendId); + validateSelfFriendOperation(memberId, friendId); - FriendInvite friendInvite = friendInviteRepository.findFriendInviteForUpdateByOwnerIdAndFriendId( - memberId, friendId) - .orElseThrow(FriendInviteNotFoundException::new); + FriendInvite friendInvite = friendInviteRepository.findFriendSendingInviteForUpdateByOwnerIdAndFriendId( + memberId, friendId) + .orElseThrow(FriendInviteNotFoundException::new); friendInviteRepository.delete(friendInvite); } 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 2f97178f9..1f3485e2e 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 @@ -56,7 +56,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", "그룹 생성에 실패하였습니다."), @@ -72,7 +72,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; From 0207f90b806b34fbd7adb9e3607215498bb33cb2 Mon Sep 17 00:00:00 2001 From: hong seokho Date: Sun, 2 Jun 2024 15:31:41 +0900 Subject: [PATCH 03/10] =?UTF-8?q?test=20:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/FriendInviteMemberIdsDtoFixture.java | 41 ++++ .../FriendInviteQueryRepositoryTest.java | 35 ++- .../MemberFriendQueryRepositoryTest.java | 4 +- .../MemberFriendRepositoryTest.java | 3 +- .../command/FriendCommandServiceTest.java | 200 ++++++++++++++++++ .../{ => query}/FriendQueryServiceTest.java | 4 +- 6 files changed, 274 insertions(+), 13 deletions(-) create mode 100644 backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/dto/FriendInviteMemberIdsDtoFixture.java rename backend/core/src/test/java/site/timecapsulearchive/core/domain/friend/repository/{ => friend_invite}/FriendInviteQueryRepositoryTest.java (64%) rename backend/core/src/test/java/site/timecapsulearchive/core/domain/friend/repository/{ => member_friend}/MemberFriendQueryRepositoryTest.java (98%) rename backend/core/src/test/java/site/timecapsulearchive/core/domain/friend/repository/{ => member_friend}/MemberFriendRepositoryTest.java (94%) create mode 100644 backend/core/src/test/java/site/timecapsulearchive/core/domain/friend/service/command/FriendCommandServiceTest.java rename backend/core/src/test/java/site/timecapsulearchive/core/domain/friend/service/{ => query}/FriendQueryServiceTest.java (94%) 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 64% 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 c4c63562f..fd7a21c05 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,7 +1,8 @@ -package site.timecapsulearchive.core.domain.friend.repository; +package site.timecapsulearchive.core.domain.friend.repository.friend_invite; import static org.assertj.core.api.Assertions.assertThat; +import com.querydsl.jpa.impl.JPAQueryFactory; import jakarta.persistence.EntityManager; import jakarta.persistence.Query; import java.util.ArrayList; @@ -12,10 +13,10 @@ import org.springframework.test.context.TestConstructor; import org.springframework.test.context.TestConstructor.AutowireMode; 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.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) @@ -27,9 +28,11 @@ class FriendInviteQueryRepositoryTest extends RepositoryTest { private Member owner; - FriendInviteQueryRepositoryTest(EntityManager entityManager, JdbcTemplate jdbcTemplate) { + FriendInviteQueryRepositoryTest(EntityManager entityManager, JdbcTemplate jdbcTemplate, + JPAQueryFactory jpaQueryFactory) { this.entityManager = entityManager; - this.friendInviteQueryRepository = new FriendInviteQueryRepositoryImpl(jdbcTemplate); + this.friendInviteQueryRepository = new FriendInviteQueryRepositoryImpl(jdbcTemplate, + jpaQueryFactory); } @BeforeEach @@ -39,6 +42,11 @@ void setup() { friends.addAll(MemberFixture.members(1, 11)); friends.forEach(entityManager::persist); + + for (Member friend : friends) { + FriendInvite friendInvite = FriendInviteFixture.friendInvite(friend, owner); + entityManager.persist(friendInvite); + } } @Test @@ -63,4 +71,21 @@ private List getFriendInvites(EntityManager entityManager, Long ow query.setParameter("ownerId", ownerId); return query.getResultList(); } + + //Friend -> Owner + @Test + void 친구가_사용자에게_요청을_보낸_경우_사용자_아이디와_친구_아이디_목록으로_모든_요청_방향의_친구_초대를_조회하면_존재하는_단방향_친구_초대가_나온다() { + //given + List friendIds = friends.stream() + .map(Member::getId) + .toList(); + + //when + List friendInviteMemberIdsDtos = friendInviteQueryRepository.findFriendInviteMemberIdsDtoByMemberIdsAndFriendId( + friendIds, + owner.getId() + ); + + assertThat(friendInviteMemberIdsDtos).hasSize(friendIds.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 09ae4cf3d..5dec0970d 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..e1ff42ee8 --- /dev/null +++ b/backend/core/src/test/java/site/timecapsulearchive/core/domain/friend/service/command/FriendCommandServiceTest.java @@ -0,0 +1,200 @@ +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.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.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; + + //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 94% 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 6c940d657..db106d8c9 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; @@ -18,10 +18,8 @@ import site.timecapsulearchive.core.common.fixture.dto.FriendDtoFixture; 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.SearchTagFriendSummaryResponse; import site.timecapsulearchive.core.domain.friend.exception.FriendNotFoundException; import site.timecapsulearchive.core.domain.friend.repository.member_friend.MemberFriendRepository; -import site.timecapsulearchive.core.domain.friend.service.query.FriendQueryService; import site.timecapsulearchive.core.global.common.wrapper.ByteArrayWrapper; class FriendQueryServiceTest { From 2c7119d7c60129fe1a2a4dbffb45653db6a9baff Mon Sep 17 00:00:00 2001 From: hong seokho Date: Mon, 3 Jun 2024 00:01:29 +0900 Subject: [PATCH 04/10] =?UTF-8?q?fix=20:=20=EB=9D=BD=20=EB=AA=A8=EB=93=9C?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 순차적인 쓰기를 위한 락 모드 변경 (읽기락 -> 쓰기락) --- .../repository/friend_invite/FriendInviteRepository.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 e37380046..18f7bcabe 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 @@ -18,7 +18,7 @@ public interface FriendInviteRepository extends Repository, void delete(FriendInvite friendInvite); - @Lock(LockModeType.READ) + @Lock(LockModeType.PESSIMISTIC_WRITE) @QueryHints({@QueryHint(name = "jakarta.persistence.lock.timeout", value = "3000")}) Optional findFriendSendingInviteForUpdateByOwnerIdAndFriendId(Long memberId, Long friendId); @@ -30,7 +30,7 @@ Optional findFriendSendingInviteForUpdateByOwnerIdAndFriendId(Long join fetch fi.friend where fi.owner.id =:friendId and fi.friend.id =:memberId """) - @Lock(LockModeType.READ) + @Lock(LockModeType.PESSIMISTIC_WRITE) @QueryHints({@QueryHint(name = "jakarta.persistence.lock.timeout", value = "3000")}) Optional findFriendReceptionInviteForUpdateByOwnerIdAndFriendId( @Param(value = "memberId") Long memberId, From 041cac83270eb9922c1e399a2fe4f07a0a541d4e Mon Sep 17 00:00:00 2001 From: hong seokho Date: Tue, 4 Jun 2024 23:25:15 +0900 Subject: [PATCH 05/10] =?UTF-8?q?fix=20:=20=EB=9D=BD=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 현재 상황에선 오버 엔지니어링인 것으로 예상되어 락 제거 --- .../domain/friend/entity/FriendInvite.java | 5 ++-- .../friend_invite/FriendInviteRepository.java | 15 +++-------- .../service/command/FriendCommandService.java | 22 +++++++++------- .../core/domain/member/entity/Member.java | 26 ------------------- .../global/error/GlobalExceptionHandler.java | 12 +++++++++ .../exception/InternalServerException.java | 10 +++++++ 6 files changed, 40 insertions(+), 50 deletions(-) create mode 100644 backend/core/src/main/java/site/timecapsulearchive/core/global/error/exception/InternalServerException.java 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..a31f7b4ae 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; @@ -26,11 +27,11 @@ public class FriendInvite extends BaseEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @ManyToOne(fetch = FetchType.LAZY) + @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST) @JoinColumn(name = "owner_id", nullable = false) private Member owner; - @ManyToOne(fetch = FetchType.LAZY) + @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST) @JoinColumn(name = "friend_id", nullable = false) private Member friend; 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 18f7bcabe..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,12 +1,7 @@ package site.timecapsulearchive.core.domain.friend.repository.friend_invite; -import jakarta.persistence.LockModeType; -import jakarta.persistence.QueryHint; -import java.util.List; import java.util.Optional; -import org.springframework.data.jpa.repository.Lock; import org.springframework.data.jpa.repository.Query; -import org.springframework.data.jpa.repository.QueryHints; import org.springframework.data.repository.Repository; import org.springframework.data.repository.query.Param; import site.timecapsulearchive.core.domain.friend.entity.FriendInvite; @@ -18,8 +13,6 @@ public interface FriendInviteRepository extends Repository, void delete(FriendInvite friendInvite); - @Lock(LockModeType.PESSIMISTIC_WRITE) - @QueryHints({@QueryHint(name = "jakarta.persistence.lock.timeout", value = "3000")}) Optional findFriendSendingInviteForUpdateByOwnerIdAndFriendId(Long memberId, Long friendId); @@ -28,12 +21,10 @@ Optional findFriendSendingInviteForUpdateByOwnerIdAndFriendId(Long from FriendInvite fi join fetch fi.owner join fetch fi.friend - where fi.owner.id =:friendId and fi.friend.id =:memberId + where fi.owner.id =:ownerId and fi.friend.id =:friendId """) - @Lock(LockModeType.PESSIMISTIC_WRITE) - @QueryHints({@QueryHint(name = "jakarta.persistence.lock.timeout", value = "3000")}) - Optional findFriendReceptionInviteForUpdateByOwnerIdAndFriendId( - @Param(value = "memberId") Long memberId, + Optional findFriendReceivingInviteForUpdateByOwnerIdAndFriendId( + @Param(value = "ownerId") Long ownerId, @Param(value = "friendId") Long friendId ); } 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 8c4d885a4..15a0455ad 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 @@ -1,5 +1,6 @@ package site.timecapsulearchive.core.domain.friend.service.command; +import jakarta.persistence.OptimisticLockException; import java.util.List; import java.util.Optional; import lombok.RequiredArgsConstructor; @@ -21,7 +22,7 @@ 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.global.error.exception.InternalServerException; import site.timecapsulearchive.core.infra.queue.manager.SocialNotificationManager; @Slf4j @@ -115,12 +116,12 @@ private void validateTwoWayAndDuplicateInvite(final Long memberId, final Long fr } - public void acceptFriend(final Long memberId, final Long friendId) { - validateSelfFriendOperation(memberId, friendId); + public void acceptFriend(final Long memberId, final Long ownerId) { + validateSelfFriendOperation(memberId, ownerId); final String ownerNickname = transactionTemplate.execute(status -> { - FriendInvite friendInvite = friendInviteRepository.findFriendReceptionInviteForUpdateByOwnerIdAndFriendId( - memberId, friendId) + FriendInvite friendInvite = friendInviteRepository.findFriendReceivingInviteForUpdateByOwnerIdAndFriendId( + ownerId, memberId) .orElseThrow(FriendInviteNotFoundException::new); final MemberFriend ownerRelation = friendInvite.ownerRelation(); @@ -128,20 +129,21 @@ public void acceptFriend(final Long memberId, final Long friendId) { memberFriendRepository.save(ownerRelation); memberFriendRepository.save(friendRelation); + friendInviteRepository.delete(friendInvite); return ownerRelation.getOwnerNickname(); }); - socialNotificationManager.sendFriendAcceptMessage(ownerNickname, friendId); + socialNotificationManager.sendFriendAcceptMessage(ownerNickname, ownerId); } @Transactional - public void denyRequestFriend(final Long memberId, final Long friendId) { - validateSelfFriendOperation(memberId, friendId); + public void denyRequestFriend(final Long memberId, final Long ownerId) { + validateSelfFriendOperation(memberId, ownerId); - FriendInvite friendInvite = friendInviteRepository.findFriendReceptionInviteForUpdateByOwnerIdAndFriendId( - memberId, friendId) + FriendInvite friendInvite = friendInviteRepository.findFriendReceivingInviteForUpdateByOwnerIdAndFriendId( + ownerId, memberId) .orElseThrow(FriendInviteNotFoundException::new); friendInviteRepository.delete(friendInvite); 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/global/error/GlobalExceptionHandler.java b/backend/core/src/main/java/site/timecapsulearchive/core/global/error/GlobalExceptionHandler.java index 459f36da8..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 @@ -19,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; @@ -47,6 +48,17 @@ 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 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); + } +} From 981056a1dba1fc84048b6b6325e88d56d387ac49 Mon Sep 17 00:00:00 2001 From: hong seokho Date: Wed, 5 Jun 2024 00:03:46 +0900 Subject: [PATCH 06/10] =?UTF-8?q?fix=20:=20=EB=A8=B8=EC=A7=80=20=EC=B6=A9?= =?UTF-8?q?=EB=8F=8C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../friend_invite/FriendInviteQueryRepositoryImpl.java | 2 ++ 1 file changed, 2 insertions(+) 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 c686722cd..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 @@ -12,11 +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.Slice; 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; From b9260bedaa4df7b0a551e6629b45f8562883f276 Mon Sep 17 00:00:00 2001 From: hong seokho Date: Wed, 5 Jun 2024 00:04:05 +0900 Subject: [PATCH 07/10] =?UTF-8?q?fix=20:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=A9=EB=8F=8C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - bulk 저장 테스트와 일반 테스트 분리 --- .../FriendInviteQueryRepositoryTest.java | 49 +++++++++++++------ 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/backend/core/src/test/java/site/timecapsulearchive/core/domain/friend/repository/friend_invite/FriendInviteQueryRepositoryTest.java b/backend/core/src/test/java/site/timecapsulearchive/core/domain/friend/repository/friend_invite/FriendInviteQueryRepositoryTest.java index f22eb76ee..c65fb10c0 100644 --- a/backend/core/src/test/java/site/timecapsulearchive/core/domain/friend/repository/friend_invite/FriendInviteQueryRepositoryTest.java +++ b/backend/core/src/test/java/site/timecapsulearchive/core/domain/friend/repository/friend_invite/FriendInviteQueryRepositoryTest.java @@ -5,6 +5,8 @@ import com.querydsl.jpa.impl.JPAQueryFactory; import jakarta.persistence.EntityManager; import jakarta.persistence.Query; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.BeforeEach; @@ -28,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 = @@ -37,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); @@ -59,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(); @@ -72,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); @@ -83,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); @@ -96,7 +108,7 @@ void setup() { @Test void 대량의_친구_초대를_저장하면_조회하면_친구_초대를_볼_수_있다() { //given - List friendIds = friends.stream() + List friendIds = bulkFriends.stream() .map(Member::getId) .toList(); @@ -254,16 +266,25 @@ private List getFriendInvites(EntityManager entityManager, Long ow @Test void 친구가_사용자에게_요청을_보낸_경우_사용자_아이디와_친구_아이디_목록으로_모든_요청_방향의_친구_초대를_조회하면_존재하는_단방향_친구_초대가_나온다() { //given - List friendIds = friends.stream() - .map(Member::getId) - .toList(); + //when + List friendInviteMemberIdsDtos = friendInviteQueryRepository.findFriendInviteMemberIdsDtoByMemberIdsAndFriendId( + sendingInvitesFriendIds, + ownerId + ); + + assertThat(friendInviteMemberIdsDtos).hasSize(sendingInvitesFriendIds.size()); + } + //Owner -> Friend + @Test + void 사용자가_친구에게_요청을_보낸_경우_사용자_아이디와_친구_아이디_목록으로_모든_요청_방향의_친구_초대를_조회하면_존재하는_단방향_친구_초대가_나온다() { + //given //when List friendInviteMemberIdsDtos = friendInviteQueryRepository.findFriendInviteMemberIdsDtoByMemberIdsAndFriendId( - friendIds, - owner.getId() + receivingInviteFriendIds, + ownerId ); - assertThat(friendInviteMemberIdsDtos).hasSize(friendIds.size()); + assertThat(friendInviteMemberIdsDtos).hasSize(receivingInviteFriendIds.size()); } } \ No newline at end of file From db383c773e2170cab062ddc57f54947ecbfb49b7 Mon Sep 17 00:00:00 2001 From: hong seokho Date: Wed, 5 Jun 2024 18:17:30 +0900 Subject: [PATCH 08/10] =?UTF-8?q?fix=20:=20=EC=96=91=EB=B0=A9=ED=96=A5=20?= =?UTF-8?q?=EB=B0=8F=20=EC=A4=91=EB=B3=B5=20=EC=9A=94=EC=B2=AD=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - equals & hash 코드를 제거하고 스트림 predicate로 검증 - transaction template dto return하도록 변경 --- .../data/dto/FriendInviteMemberIdsDto.java | 20 ----------- .../data/dto/FriendInviteNotificationDto.java | 18 ++++++++++ .../service/command/FriendCommandService.java | 33 ++++++++++++------- 3 files changed, 40 insertions(+), 31 deletions(-) create mode 100644 backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/data/dto/FriendInviteNotificationDto.java 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 index 65f6132f1..02882eafb 100644 --- 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 @@ -1,28 +1,8 @@ package site.timecapsulearchive.core.domain.friend.data.dto; -import java.util.Objects; - public record FriendInviteMemberIdsDto( Long ownerId, Long friendId ) { - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - - FriendInviteMemberIdsDto that = (FriendInviteMemberIdsDto) o; - return (Objects.equals(ownerId, that.ownerId) && Objects.equals(friendId, that.friendId)) - || (Objects.equals(ownerId, that.friendId) && Objects.equals(friendId, that.ownerId)); - } - - @Override - public int hashCode() { - return Objects.hash(ownerId, 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/service/command/FriendCommandService.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/service/command/FriendCommandService.java index c02a3c5e9..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,11 +2,13 @@ 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.FriendInviteDuplicateException; @@ -37,21 +39,21 @@ public void requestFriends(Long memberId, List friendIds) { return; } - final Member[] owner = new Member[1]; - final List[] foundFriendIds = new List[1]; - - transactionTemplate.executeWithoutResult(status -> { - owner[0] = memberRepository.findMemberById(memberId) + 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() ); } @@ -61,10 +63,19 @@ private List filterTwoWayAndDuplicateInvite(Long memberId, List frie return friendIds.stream() .filter(id -> !memberId.equals(id)) - .filter(id -> !twoWayIds.contains(new FriendInviteMemberIdsDto(id, memberId))) + .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) { validateSelfFriendOperation(memberId, friendId); validateTwoWayAndDuplicateInvite(memberId, friendId); From 67ab632b712c4c86748edef90d51ab5c39cc9337 Mon Sep 17 00:00:00 2001 From: hong seokho Date: Wed, 5 Jun 2024 18:17:40 +0900 Subject: [PATCH 09/10] =?UTF-8?q?fix=20:=20persist=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/domain/friend/entity/FriendInvite.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 a31f7b4ae..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 @@ -27,11 +27,11 @@ public class FriendInvite extends BaseEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST) + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "owner_id", nullable = false) private Member owner; - @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST) + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "friend_id", nullable = false) private Member friend; From 859698982c6538da96f78068f06e865caf01cb6e Mon Sep 17 00:00:00 2001 From: hong seokho Date: Wed, 5 Jun 2024 18:17:48 +0900 Subject: [PATCH 10/10] =?UTF-8?q?test=20:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BC=80=EC=9D=B4=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../command/FriendCommandServiceTest.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) 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 index e1ff42ee8..63523ef95 100644 --- 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 @@ -7,6 +7,7 @@ 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; @@ -15,6 +16,7 @@ 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; @@ -93,6 +95,25 @@ class FriendCommandServiceTest { 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