Skip to content

Commit

Permalink
feat : 친구 초대 삭제 기능 추가 및 기존 API 동시성, 상태 검증 관리
Browse files Browse the repository at this point in the history
  • Loading branch information
seokho-1116 committed Jun 2, 2024
1 parent ab92eb8 commit ee49032
Show file tree
Hide file tree
Showing 9 changed files with 179 additions and 68 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
.idea

data
backend/data
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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<Long> friendIds);

List<FriendInviteMemberIdsDto> findFriendInviteMemberIdsDtoByMemberIdsAndFriendId(List<Long> memberIds, Long friendId);

Optional<FriendInviteMemberIdsDto> findFriendInviteMemberIdsDtoByMemberIdAndFriendId(Long memberId, Long friendId);
}
Original file line number Diff line number Diff line change
@@ -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<Long> friendIds) {
if (friendIds.isEmpty()) {
Expand Down Expand Up @@ -47,4 +55,49 @@ public int getBatchSize() {
}
);
}

@Override
public List<FriendInviteMemberIdsDto> findFriendInviteMemberIdsDtoByMemberIdsAndFriendId(
List<Long> 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<FriendInviteMemberIdsDto> 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()
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,25 @@ public interface FriendInviteRepository extends Repository<FriendInvite, Long>,

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<FriendInvite> findFriendInviteWithMembersByOwnerIdAndFriendId(
@Param(value = "memberId") Long memberId,
@Param(value = "friendId") Long friendId
);

Optional<FriendInvite> 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<FriendInvite> findFriendInviteForUpdateByOwnerIdAndFriendId(Long memberId, Long targetId);
@QueryHints({@QueryHint(name = "jakarta.persistence.lock.timeout", value = "3000")})
Optional<FriendInvite> 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<FriendInvite> findFriendReceptionInviteForUpdateByOwnerIdAndFriendId(
@Param(value = "memberId") Long memberId,
@Param(value = "friendId") Long friendId
);
}

Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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
Expand All @@ -34,6 +36,12 @@ public class FriendCommandService {
private final TransactionTemplate transactionTemplate;

public void requestFriends(Long memberId, List<Long> friendIds) {
List<Long> filteredFriendIds = filterTwoWayAndDuplicateInvite(memberId, friendIds);

if (filteredFriendIds.isEmpty()) {
return;
}

final Member[] owner = new Member[1];
final List<Long>[] foundFriendIds = new List[1];

Expand All @@ -42,7 +50,7 @@ public void requestFriends(Long memberId, List<Long> 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]);
}
Expand All @@ -55,9 +63,19 @@ protected void doInTransactionWithoutResult(TransactionStatus status) {
);
}

private List<Long> filterTwoWayAndDuplicateInvite(Long memberId, List<Long> friendIds) {
List<FriendInviteMemberIdsDto> 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);
Expand All @@ -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> friendInvite = friendInviteRepository.findFriendInviteByOwnerIdAndFriendId(
friendId, memberId);
private void validateTwoWayAndDuplicateInvite(final Long memberId, final Long friendId) {
final Optional<FriendInviteMemberIdsDto> 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<FriendInvite> 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<MemberFriend> memberFriends = memberFriendRepository
.findMemberFriendByOwnerIdAndFriendId(memberId, friendId);
Expand All @@ -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);
}
Expand Down
Loading

0 comments on commit ee49032

Please sign in to comment.