diff --git a/src/main/java/org/sopt/app/application/fortune/FortuneService.java b/src/main/java/org/sopt/app/application/fortune/FortuneService.java index a2e6847b..42f16853 100644 --- a/src/main/java/org/sopt/app/application/fortune/FortuneService.java +++ b/src/main/java/org/sopt/app/application/fortune/FortuneService.java @@ -2,12 +2,14 @@ import java.time.LocalDate; import lombok.RequiredArgsConstructor; +import org.sopt.app.application.user.UserWithdrawEvent; import org.sopt.app.common.exception.BadRequestException; import org.sopt.app.common.response.ErrorCode; import org.sopt.app.domain.entity.fortune.UserFortune; import org.sopt.app.interfaces.postgres.fortune.FortuneCardRepository; import org.sopt.app.interfaces.postgres.fortune.FortuneWordRepository; import org.sopt.app.interfaces.postgres.fortune.UserFortuneRepository; +import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -57,4 +59,9 @@ public FortuneCardInfo getTodayFortuneCardByUserId(final Long userId) { .map(FortuneCardInfo::of) .orElseThrow(() -> new BadRequestException(ErrorCode.FORTUNE_NOT_FOUND_FROM_USER)); } + + @EventListener(UserWithdrawEvent.class) + public void handleUserWithdrawEvent(final UserWithdrawEvent event) { + userFortuneRepository.deleteAllByUserIdInQuery(event.getUserId()); + } } diff --git a/src/main/java/org/sopt/app/application/friend/FriendService.java b/src/main/java/org/sopt/app/application/friend/FriendService.java index ff653735..968c074d 100755 --- a/src/main/java/org/sopt/app/application/friend/FriendService.java +++ b/src/main/java/org/sopt/app/application/friend/FriendService.java @@ -5,12 +5,14 @@ import java.util.Map.Entry; import org.jetbrains.annotations.NotNull; import org.sopt.app.application.poke.PokeInfo.Relationship; +import org.sopt.app.application.user.UserWithdrawEvent; import org.sopt.app.common.exception.NotFoundException; import org.sopt.app.common.response.ErrorCode; import org.sopt.app.common.utils.AnonymousNameGenerator; import org.sopt.app.domain.entity.Friend; import org.sopt.app.domain.enums.Friendship; import org.sopt.app.interfaces.postgres.FriendRepository; +import org.springframework.context.event.EventListener; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; @@ -169,4 +171,10 @@ public boolean getIsNewUser(Long userId) { public Set findAllFriendIdsByUserId(Long userId) { return friendRepository.findAllOfFriendIdsByUserId(userId); } + + @EventListener(UserWithdrawEvent.class) + public void handleUserWithdrawEvent(final UserWithdrawEvent event) { + friendRepository.deleteAllByFriendUserIdInQuery(event.getUserId()); + friendRepository.deleteAllByUserIdInQuery(event.getUserId()); + } } diff --git a/src/main/java/org/sopt/app/application/notification/NotificationService.java b/src/main/java/org/sopt/app/application/notification/NotificationService.java index 5c435f68..6b7e9cdf 100755 --- a/src/main/java/org/sopt/app/application/notification/NotificationService.java +++ b/src/main/java/org/sopt/app/application/notification/NotificationService.java @@ -5,6 +5,7 @@ import lombok.RequiredArgsConstructor; import lombok.val; +import org.sopt.app.application.user.UserWithdrawEvent; import org.sopt.app.common.exception.BadRequestException; import org.sopt.app.common.response.ErrorCode; import org.sopt.app.domain.entity.Notification; @@ -13,6 +14,7 @@ import org.sopt.app.interfaces.postgres.NotificationRepository; import org.sopt.app.interfaces.postgres.UserRepository; import org.sopt.app.presentation.notification.NotificationRequest.RegisterNotificationRequest; +import org.springframework.context.event.EventListener; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -83,4 +85,9 @@ public boolean getNotificationConfirmStatus(Long userId) { .toList(); return unreadNotificationList.isEmpty(); } + + @EventListener(UserWithdrawEvent.class) + public void handleUserWithdrawEvent(final UserWithdrawEvent event) { + notificationRepository.deleteByUserIdInQuery(event.getUserId()); + } } diff --git a/src/main/java/org/sopt/app/application/notification/PushTokenService.java b/src/main/java/org/sopt/app/application/notification/PushTokenService.java index 70a365e4..5438ee93 100755 --- a/src/main/java/org/sopt/app/application/notification/PushTokenService.java +++ b/src/main/java/org/sopt/app/application/notification/PushTokenService.java @@ -2,15 +2,17 @@ import lombok.RequiredArgsConstructor; import lombok.val; +import org.sopt.app.application.user.UserWithdrawEvent; import org.sopt.app.common.exception.BadRequestException; import org.sopt.app.common.response.ErrorCode; import org.sopt.app.domain.entity.PushToken; import org.sopt.app.domain.entity.User; import org.sopt.app.domain.enums.PushTokenPlatform; import org.sopt.app.interfaces.postgres.PushTokenRepository; -import org.sopt.app.presentation.notification.PushTokenRequest; +import org.sopt.app.presentation.notification.PushTokenRequest.PushTokenManageRequest; import org.sopt.app.presentation.notification.PushTokenResponse; import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.event.EventListener; import org.springframework.http.*; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -59,7 +61,7 @@ public PushTokenResponse.StatusResponse registerDeviceToken(User user, String pu .platform(PushTokenPlatform.valueOf(platform)) .build(); try { - val entity = new HttpEntity( + val entity = new HttpEntity<>( createBodyFor(registerToken), createHeadersFor(ACTION_REGISTER, platform) ); @@ -80,21 +82,20 @@ public PushTokenResponse.StatusResponse registerDeviceToken(User user, String pu .message("토큰 등록 성공") .build(); } - - + // 유효하지 않은 토큰으로 인해 BadRequest가 발생하더라도 넘어가야함.(Local 에는 모든 토큰을 쌓아놓기 때문에) @Transactional - public PushTokenResponse.StatusResponse deleteDeviceToken(PushToken pushToken) { + public void deleteDeviceToken(PushToken pushToken) { + pushTokenRepository.delete(pushToken); try { - val entity = new HttpEntity( + val entity = new HttpEntity<>( createBodyFor(pushToken), createHeadersFor(ACTION_DELETE, pushToken.getPlatform().name()) ); val response = sendRequestToPushServer(entity); - pushTokenRepository.delete(pushToken); - return response.getBody(); + response.getBody(); } catch (BadRequestException e) { - return PushTokenResponse.StatusResponse.builder() + PushTokenResponse.StatusResponse.builder() .status(e.getErrorCode().getHttpStatus().value()) .success(false) .message(e.getErrorCode().getMessage()) @@ -102,15 +103,24 @@ public PushTokenResponse.StatusResponse deleteDeviceToken(PushToken pushToken) { } } - @Transactional - public void deleteAllDeviceTokenOf(User user) { - // 기존에 저장되어 있던 Tokens -> 알림 서버에 삭제 요청 - List userTokens = pushTokenRepository.findAllByUserId(user.getId()); + private ResponseEntity sendRequestToPushServer( + HttpEntity requestEntity + ) { + return restTemplate.exchange( + baseURI, + HttpMethod.POST, + requestEntity, + PushTokenResponse.StatusResponse.class + ); + } + + @EventListener(UserWithdrawEvent.class) + public void deleteAllDeviceTokenOf(final UserWithdrawEvent event) { + List userTokens = pushTokenRepository.findAllByUserId(event.getUserId()); if (!userTokens.isEmpty()) { for (PushToken token : userTokens) { - deleteDeviceToken(token); + this.deleteDeviceToken(token); } - // 우선 서비스 DB에 있는 모든 토큰 지워버리기 pushTokenRepository.deleteAll(userTokens); } } @@ -126,18 +136,10 @@ private HttpHeaders createHeadersFor(String action, String platform) { return headers; } - private PushTokenRequest.PushTokenManageRequest createBodyFor(PushToken pushToken) { - return new PushTokenRequest.PushTokenManageRequest( + private PushTokenManageRequest createBodyFor(PushToken pushToken) { + return new PushTokenManageRequest( List.of(String.valueOf(pushToken.getPlaygroundId())), pushToken.getToken() ); } - private ResponseEntity sendRequestToPushServer(HttpEntity requestEntity) { - return restTemplate.exchange( - baseURI, - HttpMethod.POST, - requestEntity, - PushTokenResponse.StatusResponse.class - ); - } } diff --git a/src/main/java/org/sopt/app/application/poke/PokeHistoryService.java b/src/main/java/org/sopt/app/application/poke/PokeHistoryService.java index 24bebe8c..872c1431 100755 --- a/src/main/java/org/sopt/app/application/poke/PokeHistoryService.java +++ b/src/main/java/org/sopt/app/application/poke/PokeHistoryService.java @@ -1,16 +1,14 @@ package org.sopt.app.application.poke; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import lombok.RequiredArgsConstructor; -import lombok.val; +import java.util.*; +import lombok.*; import org.sopt.app.application.poke.PokeInfo.PokeHistoryInfo; +import org.sopt.app.application.user.UserWithdrawEvent; import org.sopt.app.common.exception.BadRequestException; import org.sopt.app.common.response.ErrorCode; import org.sopt.app.domain.entity.poke.PokeHistory; import org.sopt.app.interfaces.postgres.PokeHistoryRepository; +import org.springframework.context.event.EventListener; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; @@ -80,4 +78,10 @@ public List getAllPokeHistoryByUsers(Long userId, Long friendUs val pokeHistories = pokeHistoryRepository.findAllPokeHistoryByUsers(userId, friendUserId); return pokeHistories.stream().map(PokeHistoryInfo::from).toList(); } + + @EventListener(UserWithdrawEvent.class) + public void handleUserWithdrawEvent(final UserWithdrawEvent event) { + pokeHistoryRepository.deleteAllByPokerIdInQuery(event.getUserId()); + pokeHistoryRepository.deleteAllByPokedIdInQuery(event.getUserId()); + } } diff --git a/src/main/java/org/sopt/app/application/poke/PokeService.java b/src/main/java/org/sopt/app/application/poke/PokeService.java index 58518bd4..6acdfd57 100755 --- a/src/main/java/org/sopt/app/application/poke/PokeService.java +++ b/src/main/java/org/sopt/app/application/poke/PokeService.java @@ -2,7 +2,7 @@ import java.util.List; import lombok.RequiredArgsConstructor; -import org.sopt.app.common.event.Events; +import org.sopt.app.common.event.EventPublisher; import org.sopt.app.common.exception.NotFoundException; import org.sopt.app.common.response.ErrorCode; import org.sopt.app.domain.entity.poke.PokeHistory; @@ -19,6 +19,7 @@ public class PokeService { private final UserRepository userRepository; private final PokeHistoryRepository historyRepository; + private final EventPublisher eventPublisher; @Transactional(readOnly = true) public PokeInfo.PokeDetail getPokeDetail(Long pokeHistoryId) { @@ -39,7 +40,7 @@ public PokeHistory poke(Long pokerUserId, Long pokedUserId, String pokeMessage, PokeHistory pokeByApplyingReply = createPokeByApplyingReply(pokerUserId, pokedUserId, pokeMessage, isAnonymous); - Events.raise(PokeEvent.of(pokedUser.getPlaygroundId())); + eventPublisher.raise(PokeEvent.of(pokedUser.getPlaygroundId())); return pokeByApplyingReply; } diff --git a/src/main/java/org/sopt/app/application/s3/S3Service.java b/src/main/java/org/sopt/app/application/s3/S3Service.java index 31744ef8..f560a9bb 100755 --- a/src/main/java/org/sopt/app/application/s3/S3Service.java +++ b/src/main/java/org/sopt/app/application/s3/S3Service.java @@ -8,10 +8,11 @@ import lombok.*; import org.joda.time.LocalDateTime; import org.slf4j.LoggerFactory; +import org.sopt.app.application.stamp.StampDeletedEvent; import org.sopt.app.common.exception.BadRequestException; import org.sopt.app.common.response.ErrorCode; import org.springframework.beans.factory.annotation.Value; -import org.springframework.scheduling.annotation.Async; +import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; @Service @@ -49,8 +50,12 @@ public S3Info.PreSignedUrl getPreSignedUrl(String folderName) { .build(); } - @Async - public void deleteFiles(List fileUrls, String folderName) { + @EventListener(StampDeletedEvent.class) + public void handleStampDeletedEvent(StampDeletedEvent event) { + this.deleteFiles(event.getFileUrls(), "stamp"); + } + + private void deleteFiles(List fileUrls, String folderName) { val folderURI = bucket + "/mainpage/makers-app-img/" + folderName; val fileNameList = getFileNameList(fileUrls); fileNameList.forEach(file -> deleteFile(folderURI, file)); diff --git a/src/main/java/org/sopt/app/application/soptamp/SoptampUserService.java b/src/main/java/org/sopt/app/application/soptamp/SoptampUserService.java index d58be7f1..77337994 100755 --- a/src/main/java/org/sopt/app/application/soptamp/SoptampUserService.java +++ b/src/main/java/org/sopt/app/application/soptamp/SoptampUserService.java @@ -7,11 +7,13 @@ import lombok.*; import org.sopt.app.application.playground.dto.PlaygroundProfileInfo.ActivityCardinalInfo; import org.sopt.app.application.playground.dto.PlaygroundProfileInfo.PlaygroundProfile; +import org.sopt.app.application.user.UserWithdrawEvent; import org.sopt.app.common.exception.BadRequestException; import org.sopt.app.common.response.ErrorCode; import org.sopt.app.domain.entity.soptamp.SoptampUser; import org.sopt.app.domain.enums.PlaygroundPart; import org.sopt.app.interfaces.postgres.SoptampUserRepository; +import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -122,4 +124,9 @@ public void initAllSoptampUserPoints() { soptampUserList.forEach(SoptampUser::initTotalPoints); soptampUserRepository.saveAll(soptampUserList); } + + @EventListener(UserWithdrawEvent.class) + public void handleUserWithdrawEvent(final UserWithdrawEvent event) { + soptampUserRepository.deleteByUserId(event.getUserId()); + } } \ No newline at end of file diff --git a/src/main/java/org/sopt/app/application/stamp/StampDeletedEvent.java b/src/main/java/org/sopt/app/application/stamp/StampDeletedEvent.java index 3442832d..786a4b9e 100755 --- a/src/main/java/org/sopt/app/application/stamp/StampDeletedEvent.java +++ b/src/main/java/org/sopt/app/application/stamp/StampDeletedEvent.java @@ -2,15 +2,12 @@ import java.util.List; import lombok.Getter; +import lombok.RequiredArgsConstructor; import org.sopt.app.common.event.Event; @Getter +@RequiredArgsConstructor public class StampDeletedEvent extends Event { - private List fileUrls; - - public StampDeletedEvent(List fileUrls) { - super(); - this.fileUrls = fileUrls; - } + private final List fileUrls; } diff --git a/src/main/java/org/sopt/app/application/stamp/StampDeletedEventHandler.java b/src/main/java/org/sopt/app/application/stamp/StampDeletedEventHandler.java deleted file mode 100755 index 0a312173..00000000 --- a/src/main/java/org/sopt/app/application/stamp/StampDeletedEventHandler.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.sopt.app.application.stamp; - -import lombok.RequiredArgsConstructor; -import org.sopt.app.application.s3.S3Service; -import org.springframework.context.event.EventListener; -import org.springframework.stereotype.Component; - -@Component -@RequiredArgsConstructor -public class StampDeletedEventHandler { - - private final S3Service s3Service; - - @EventListener(StampDeletedEvent.class) - public void handle(StampDeletedEvent event) { - s3Service.deleteFiles(event.getFileUrls(), "stamp"); - } -} \ No newline at end of file diff --git a/src/main/java/org/sopt/app/application/stamp/StampService.java b/src/main/java/org/sopt/app/application/stamp/StampService.java index b930fe9f..ff95a355 100755 --- a/src/main/java/org/sopt/app/application/stamp/StampService.java +++ b/src/main/java/org/sopt/app/application/stamp/StampService.java @@ -6,13 +6,15 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.val; -import org.sopt.app.common.event.Events; +import org.sopt.app.application.user.UserWithdrawEvent; +import org.sopt.app.common.event.EventPublisher; import org.sopt.app.common.exception.BadRequestException; import org.sopt.app.common.response.ErrorCode; import org.sopt.app.domain.entity.soptamp.Stamp; import org.sopt.app.interfaces.postgres.StampRepository; import org.sopt.app.presentation.stamp.StampRequest; import org.sopt.app.presentation.stamp.StampRequest.RegisterStampRequest; +import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; @@ -22,6 +24,7 @@ public class StampService { private final StampRepository stampRepository; + private final EventPublisher eventPublisher; @Transactional(readOnly = true) public StampInfo.Stamp findStamp(Long missionId, Long userId) { @@ -143,7 +146,7 @@ public void deleteStampById(Long stampId) { .orElseThrow(() -> new BadRequestException(ErrorCode.STAMP_NOT_FOUND)); stampRepository.deleteById(stampId); - Events.raise(new StampDeletedEvent(stamp.getImages())); + eventPublisher.raise(new StampDeletedEvent(stamp.getImages())); } @Transactional @@ -152,7 +155,12 @@ public void deleteAllStamps(Long userId) { val imageUrls = stampRepository.findAllByUserId(userId).stream().map(Stamp::getImages) .flatMap(Collection::stream).toList(); - Events.raise(new StampDeletedEvent(imageUrls)); + eventPublisher.raise(new StampDeletedEvent(imageUrls)); + } + + @EventListener(UserWithdrawEvent.class) + public void handleUserWithdrawEvent(final UserWithdrawEvent event) { + this.deleteAllStamps(event.getUserId()); } private void validateStampInfo(Stamp entity) { diff --git a/src/main/java/org/sopt/app/application/user/UserService.java b/src/main/java/org/sopt/app/application/user/UserService.java index 256cc077..9acb92af 100755 --- a/src/main/java/org/sopt/app/application/user/UserService.java +++ b/src/main/java/org/sopt/app/application/user/UserService.java @@ -16,6 +16,7 @@ @Service +@Transactional(readOnly = true) @RequiredArgsConstructor public class UserService { @@ -45,12 +46,6 @@ private User registerNewUser(LoginInfo loginInfo) { return userRepository.save(newUser); } - @Transactional - public void deleteUser(User user) { - userRepository.delete(user); - } - - @Transactional(readOnly = true) public AccessTokenRequest getPlaygroundToken(Long userId) { val user = userRepository.findUserById(userId) .orElseThrow(() -> new UnauthorizedException(ErrorCode.INVALID_REFRESH_TOKEN)); diff --git a/src/main/java/org/sopt/app/application/user/UserWithdrawEvent.java b/src/main/java/org/sopt/app/application/user/UserWithdrawEvent.java new file mode 100644 index 00000000..9a9a3911 --- /dev/null +++ b/src/main/java/org/sopt/app/application/user/UserWithdrawEvent.java @@ -0,0 +1,12 @@ +package org.sopt.app.application.user; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.sopt.app.common.event.Event; + +@Getter +@RequiredArgsConstructor +public class UserWithdrawEvent extends Event { + + private final Long userId; +} diff --git a/src/main/java/org/sopt/app/application/user/UserWithdrawService.java b/src/main/java/org/sopt/app/application/user/UserWithdrawService.java new file mode 100644 index 00000000..eba9addb --- /dev/null +++ b/src/main/java/org/sopt/app/application/user/UserWithdrawService.java @@ -0,0 +1,21 @@ +package org.sopt.app.application.user; + +import lombok.RequiredArgsConstructor; +import org.sopt.app.common.event.EventPublisher; +import org.sopt.app.interfaces.postgres.UserRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class UserWithdrawService { + + private final UserRepository userRepository; + private final EventPublisher eventPublisher; + + @Transactional + public void withdrawUser(Long userId) { + eventPublisher.raise(new UserWithdrawEvent(userId)); + userRepository.deleteById(userId); + } +} diff --git a/src/main/java/org/sopt/app/common/config/EventsConfig.java b/src/main/java/org/sopt/app/common/config/EventsConfig.java deleted file mode 100755 index 052bd172..00000000 --- a/src/main/java/org/sopt/app/common/config/EventsConfig.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.sopt.app.common.config; - -import lombok.RequiredArgsConstructor; -import org.sopt.app.common.event.Events; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.*; - -@Configuration -@RequiredArgsConstructor -public class EventsConfig { - - private final ApplicationContext applicationContext; - - @Bean - public InitializingBean eventsInitializer() { - return () -> Events.setPublisher(applicationContext); - } -} \ No newline at end of file diff --git a/src/main/java/org/sopt/app/common/event/EventPublisher.java b/src/main/java/org/sopt/app/common/event/EventPublisher.java new file mode 100755 index 00000000..9b43e570 --- /dev/null +++ b/src/main/java/org/sopt/app/common/event/EventPublisher.java @@ -0,0 +1,16 @@ +package org.sopt.app.common.event; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class EventPublisher { + + private final ApplicationEventPublisher publisher; + + public void raise(Event event) { + publisher.publishEvent(event); + } +} diff --git a/src/main/java/org/sopt/app/common/event/Events.java b/src/main/java/org/sopt/app/common/event/Events.java deleted file mode 100755 index 0a812951..00000000 --- a/src/main/java/org/sopt/app/common/event/Events.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.sopt.app.common.event; - -import static java.util.Objects.nonNull; - -import lombok.NoArgsConstructor; -import org.springframework.context.ApplicationEventPublisher; - -@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE) -public class Events { - - private static ApplicationEventPublisher publisher; - - public static void setPublisher(ApplicationEventPublisher publisher) { - Events.publisher = publisher; - } - - public static void raise(Object event) { - if (nonNull(publisher)) { - publisher.publishEvent(event); - } - } -} diff --git a/src/main/java/org/sopt/app/facade/SoptampFacade.java b/src/main/java/org/sopt/app/facade/SoptampFacade.java index a1cbba70..d8637613 100755 --- a/src/main/java/org/sopt/app/facade/SoptampFacade.java +++ b/src/main/java/org/sopt/app/facade/SoptampFacade.java @@ -17,6 +17,7 @@ import org.springframework.transaction.annotation.Transactional; @Service +@Transactional(readOnly = true) @RequiredArgsConstructor public class SoptampFacade { @@ -57,7 +58,6 @@ public SoptampUserInfo editSoptampUserProfileMessage(Long userId, String newProf return soptampUserService.editProfileMessage(userId, newProfileMessage); } - @Transactional public Stamp getStampInfo(Long missionId, String nickname){ val userId = soptampUserFinder.findByNickname(nickname).getUserId(); return stampService.findStamp(missionId, userId); diff --git a/src/main/java/org/sopt/app/interfaces/postgres/FriendRepository.java b/src/main/java/org/sopt/app/interfaces/postgres/FriendRepository.java index 7f234b24..6b441d92 100755 --- a/src/main/java/org/sopt/app/interfaces/postgres/FriendRepository.java +++ b/src/main/java/org/sopt/app/interfaces/postgres/FriendRepository.java @@ -24,4 +24,10 @@ public interface FriendRepository extends JpaRepository { Page findAllByUserIdAndFriendUserIdInOrderByPokeCount(Long userId, List friendIdsPokeMe, Pageable pageable); + + @Query("DELETE From Friend f WHERE f.friendUserId = :friendUserId") + void deleteAllByFriendUserIdInQuery(@Param("friendUserId") Long friendUserId); + + @Query("DELETE From Friend f WHERE f.userId = :userId") + void deleteAllByUserIdInQuery(@Param("userId") Long userId); } diff --git a/src/main/java/org/sopt/app/interfaces/postgres/NotificationRepository.java b/src/main/java/org/sopt/app/interfaces/postgres/NotificationRepository.java index 4c755036..18cea697 100755 --- a/src/main/java/org/sopt/app/interfaces/postgres/NotificationRepository.java +++ b/src/main/java/org/sopt/app/interfaces/postgres/NotificationRepository.java @@ -5,6 +5,8 @@ import org.sopt.app.domain.entity.Notification; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface NotificationRepository extends JpaRepository { @@ -12,6 +14,8 @@ public interface NotificationRepository extends JpaRepository findAllByUserId(Long userId, Pageable pageable); - Optional findByIdAndUserId(Long id, Long userId); Optional findByNotificationIdAndUserId(String notificationId, Long userId); + + @Query("DELETE FROM Notification n WHERE n.userId = :userId") + void deleteByUserIdInQuery(@Param("userId") Long userId); } diff --git a/src/main/java/org/sopt/app/interfaces/postgres/PokeHistoryRepository.java b/src/main/java/org/sopt/app/interfaces/postgres/PokeHistoryRepository.java index fd7dd96f..b9ecd6b8 100755 --- a/src/main/java/org/sopt/app/interfaces/postgres/PokeHistoryRepository.java +++ b/src/main/java/org/sopt/app/interfaces/postgres/PokeHistoryRepository.java @@ -22,6 +22,12 @@ public interface PokeHistoryRepository extends JpaRepository Page findAllByIdIsInOrderByCreatedAtDesc(List historyIds, Pageable pageable); + @Query("DELETE From PokeHistory ph WHERE ph.pokedId = :userId") + void deleteAllByPokedIdInQuery(@Param("userId") Long userId); + + @Query("DELETE From PokeHistory ph WHERE ph.pokerId = :userId") + void deleteAllByPokerIdInQuery(@Param("userId") Long userId); + @Query("SELECT ph FROM PokeHistory ph WHERE ((ph.pokerId = :userId AND ph.pokedId = :friendId) OR (ph.pokerId = :friendId AND ph.pokedId = :userId)) AND ph.isReply = false ORDER BY ph.createdAt DESC ") List findAllWithFriendOrderByCreatedAtDescIsReplyFalse(@Param("userId") Long userId, @Param("friendId") Long friendId); diff --git a/src/main/java/org/sopt/app/interfaces/postgres/SoptampUserRepository.java b/src/main/java/org/sopt/app/interfaces/postgres/SoptampUserRepository.java index 2f90ca0a..08b25eb9 100755 --- a/src/main/java/org/sopt/app/interfaces/postgres/SoptampUserRepository.java +++ b/src/main/java/org/sopt/app/interfaces/postgres/SoptampUserRepository.java @@ -16,4 +16,6 @@ public interface SoptampUserRepository extends JpaRepository List findAllByGeneration(Long generation); boolean existsByNickname(String nickname); + + void deleteByUserId(Long userId); } diff --git a/src/main/java/org/sopt/app/interfaces/postgres/fortune/UserFortuneRepository.java b/src/main/java/org/sopt/app/interfaces/postgres/fortune/UserFortuneRepository.java index a47af526..ca631378 100644 --- a/src/main/java/org/sopt/app/interfaces/postgres/fortune/UserFortuneRepository.java +++ b/src/main/java/org/sopt/app/interfaces/postgres/fortune/UserFortuneRepository.java @@ -3,7 +3,12 @@ import java.util.Optional; import org.sopt.app.domain.entity.fortune.UserFortune; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface UserFortuneRepository extends JpaRepository { Optional findByUserId(final Long userId); + + @Query("DELETE FROM UserFortune u WHERE u.userId = :userId") + void deleteAllByUserIdInQuery(@Param("userId") Long userId); } diff --git a/src/main/java/org/sopt/app/presentation/user/UserWithdrawController.java b/src/main/java/org/sopt/app/presentation/user/UserWithdrawController.java index d2afbd8b..09e09687 100755 --- a/src/main/java/org/sopt/app/presentation/user/UserWithdrawController.java +++ b/src/main/java/org/sopt/app/presentation/user/UserWithdrawController.java @@ -7,12 +7,10 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirement; import lombok.RequiredArgsConstructor; import org.sopt.app.application.notification.PushTokenService; -import org.sopt.app.application.stamp.StampService; -import org.sopt.app.application.user.UserService; +import org.sopt.app.application.user.UserWithdrawService; import org.sopt.app.domain.entity.PushToken; import org.sopt.app.domain.entity.User; import org.sopt.app.presentation.notification.PushTokenRequest; -import org.sopt.app.presentation.notification.PushTokenResponse; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; @@ -25,8 +23,7 @@ @SecurityRequirement(name = "Authorization") public class UserWithdrawController { - private final UserService userService; - private final StampService stampService; + private final UserWithdrawService userWithdrawService; private final PushTokenService pushTokenService; @@ -36,7 +33,7 @@ public class UserWithdrawController { @ApiResponse(responseCode = "500", description = "server error", content = @Content) }) @DeleteMapping(value = "/logout") - public ResponseEntity logout( + public ResponseEntity logout( @AuthenticationPrincipal User user, @Valid @RequestBody PushTokenRequest.DeleteRequest deleteRequest ) { @@ -55,15 +52,8 @@ public ResponseEntity logout( @ApiResponse(responseCode = "500", description = "server error", content = @Content) }) @DeleteMapping(value = "") - public ResponseEntity withdraw(@AuthenticationPrincipal User user) { - // TODO: S3 이미지 삭제 - // TODO: 알림 서버 FCM Token 삭제 요청 => pushTokenService#deleteAllDeviceTokenOf 구현이 완료되어야함. - // 스탬프 정보 삭제 - stampService.deleteAllStamps(user.getId()); - // 푸시 토큰 일괄 삭제 - pushTokenService.deleteAllDeviceTokenOf(user); - // 유저 정보 삭제 - userService.deleteUser(user); + public ResponseEntity withdraw(@AuthenticationPrincipal User user) { + userWithdrawService.withdrawUser(user.getId()); return ResponseEntity.ok().build(); } } \ No newline at end of file diff --git a/src/test/java/org/sopt/app/application/EventPublisherTest.java b/src/test/java/org/sopt/app/application/EventPublisherTest.java new file mode 100644 index 00000000..947a8de3 --- /dev/null +++ b/src/test/java/org/sopt/app/application/EventPublisherTest.java @@ -0,0 +1,132 @@ +package org.sopt.app.application; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; + +import java.util.List; +import org.junit.jupiter.api.Test; +import org.sopt.app.application.fortune.FortuneService; +import org.sopt.app.application.friend.FriendService; +import org.sopt.app.application.notification.NotificationService; +import org.sopt.app.application.notification.PushTokenService; +import org.sopt.app.application.poke.PokeHistoryService; +import org.sopt.app.application.s3.S3Service; +import org.sopt.app.application.soptamp.SoptampUserService; +import org.sopt.app.application.stamp.StampDeletedEvent; +import org.sopt.app.application.stamp.StampService; +import org.sopt.app.application.user.UserWithdrawEvent; +import org.sopt.app.common.event.EventPublisher; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +@SpringBootTest +class EventPublisherTest { + + @MockBean private PokeHistoryService pokeHistoryService; + @MockBean private PushTokenService pushTokenService; + @MockBean private FriendService friendService; + @MockBean private NotificationService notificationService; + @MockBean private StampService stampService; + @MockBean private FortuneService fortuneService; + @MockBean private SoptampUserService soptampUserService; + @MockBean private S3Service s3Service; + + @Autowired private EventPublisher eventPublisher; + + @Test + void SUCCESS_유저_탈퇴_이벤트_발행시_SoptampUser_삭제_메서드_실행() { + //given + final UserWithdrawEvent event = new UserWithdrawEvent(1L); + + // when + eventPublisher.raise(event); + + //then + verify(soptampUserService).handleUserWithdrawEvent(any()); + } + + @Test + void SUCCESS_유저_탈퇴_이벤트_발행시_Stamp_삭제_메서드_실행() { + //given + final UserWithdrawEvent event = new UserWithdrawEvent(1L); + + // when + eventPublisher.raise(event); + + //then + verify(stampService).handleUserWithdrawEvent(any()); + } + + @Test + void SUCCESS_유저_탈퇴_이벤트_발행시_UserFortune_삭제_메서드_실행() { + //given + final UserWithdrawEvent event = new UserWithdrawEvent(1L); + + // when + eventPublisher.raise(event); + + //then + verify(fortuneService).handleUserWithdrawEvent(any()); + } + + @Test + void SUCCESS_유저_탈퇴_이벤트_발행시_Notification_삭제_메서드_실행() { + //given + final UserWithdrawEvent event = new UserWithdrawEvent(1L); + + // when + eventPublisher.raise(event); + + //then + verify(notificationService).handleUserWithdrawEvent(any()); + } + + @Test + void SUCCESS_유저_탈퇴_이벤트_발행시_Friend_삭제_메서드_실행() { + //given + final UserWithdrawEvent event = new UserWithdrawEvent(1L); + + // when + eventPublisher.raise(event); + + //then + verify(friendService).handleUserWithdrawEvent(any()); + } + + @Test + void SUCCESS_유저_탈퇴_이벤트_발행시_PushToken_삭제_메서드_실행() { + //given + final UserWithdrawEvent event = new UserWithdrawEvent(1L); + + // when + eventPublisher.raise(event); + + //then + verify(pushTokenService).deleteAllDeviceTokenOf(any()); + } + + @Test + void SUCCESS_유저_탈퇴_이벤트_발행시_PokeHistory_삭제_메서드_실행() { + //given + final UserWithdrawEvent event = new UserWithdrawEvent(1L); + + // when + eventPublisher.raise(event); + + //then + verify(pokeHistoryService).handleUserWithdrawEvent(any()); + } + + @Test + void SUCCESS_스탬프_삭제_이벤트_발행시_스탬프_삭제_및_S3_이미지_삭제_메서드_실행() { + //given + final StampDeletedEvent event = new StampDeletedEvent(List.of("fileUrl")); + + // when + eventPublisher.raise(event); + + //then + verify(s3Service).handleStampDeletedEvent(any()); + } +} diff --git a/src/test/java/org/sopt/app/application/PokeServiceTest.java b/src/test/java/org/sopt/app/application/PokeServiceTest.java index f1c5926a..2dc17de4 100755 --- a/src/test/java/org/sopt/app/application/PokeServiceTest.java +++ b/src/test/java/org/sopt/app/application/PokeServiceTest.java @@ -4,6 +4,9 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.util.List; @@ -16,6 +19,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.sopt.app.application.poke.PokeInfo.PokeDetail; import org.sopt.app.application.poke.PokeService; +import org.sopt.app.common.event.EventPublisher; import org.sopt.app.common.exception.NotFoundException; import org.sopt.app.domain.entity.poke.PokeHistory; import org.sopt.app.domain.entity.User; @@ -31,6 +35,9 @@ class PokeServiceTest { @Mock private PokeHistoryRepository pokeHistoryRepository; + @Mock + private EventPublisher eventPublisher; + @InjectMocks private PokeService pokeService; @@ -83,6 +90,20 @@ void SUCCESS_pokeWithUserEmpty() { assertNull(result); } + @Test + void SUCCESS_찌르기_실행시_PokeEvent발생() { + // given + User user = User.builder().id(1L).build(); + given(userRepository.findUserById(any())).willReturn(Optional.of(user)); + given(pokeHistoryRepository.findAllByPokerIdAndPokedIdAndIsReplyIsFalse(anyLong(), anyLong())).willReturn(List.of()); + + // when + pokeService.poke(2L, 1L, "message", false); + + // then + verify(eventPublisher).raise(any()); + } + @Test @DisplayName("FAIL_찌르기 유저 없을 때 NotFoundException 발생") void FAIL_pokeWithoutUserNotFoundException() { diff --git a/src/test/java/org/sopt/app/application/S3ServiceTest.java b/src/test/java/org/sopt/app/application/S3ServiceTest.java index 71264b62..4abe68d8 100755 --- a/src/test/java/org/sopt/app/application/S3ServiceTest.java +++ b/src/test/java/org/sopt/app/application/S3ServiceTest.java @@ -1,7 +1,9 @@ package org.sopt.app.application; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doNothing; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.amazonaws.services.s3.AmazonS3; @@ -17,6 +19,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.sopt.app.application.s3.S3Info; import org.sopt.app.application.s3.S3Service; +import org.sopt.app.application.stamp.StampDeletedEvent; import org.sopt.app.common.exception.BadRequestException; @ExtendWith(MockitoExtension.class) @@ -51,12 +54,14 @@ void FAIL_getPreSignedUrlNullPointerException() { } @Test - @DisplayName("SUCCESS_파일 삭제") + @DisplayName("SUCCESS_유저 탈퇴시 파일 삭제 이벤트 발생") void SUCCESS_deleteFiles() { - doNothing().when(s3Client).deleteObject(any(), any()); + // given + final List fileUrls = List.of("fileUrl1", "fileUrl2"); + // when + s3Service.handleStampDeletedEvent(new StampDeletedEvent(fileUrls)); - Assertions.assertDoesNotThrow(() -> - s3Service.deleteFiles(List.of("https://url.com"), "folderName") - ); + // then + verify(s3Client, times(fileUrls.size())).deleteObject(anyString(), anyString()); } } \ No newline at end of file diff --git a/src/test/java/org/sopt/app/application/SoptampUserFinderTest.java b/src/test/java/org/sopt/app/application/SoptampUserFinderTest.java index a4c4c847..4bfaa444 100644 --- a/src/test/java/org/sopt/app/application/SoptampUserFinderTest.java +++ b/src/test/java/org/sopt/app/application/SoptampUserFinderTest.java @@ -68,10 +68,6 @@ void FAIL_findByNickname() { Mockito.when(soptampUserRepository.findUserByNickname(anyNickname)).thenReturn(Optional.empty()); //then - Assertions.assertThrows(BadRequestException.class, () -> { - soptampUserFinder.findByNickname(anyNickname); - }); + Assertions.assertThrows(BadRequestException.class, () -> soptampUserFinder.findByNickname(anyNickname)); } - - } diff --git a/src/test/java/org/sopt/app/application/StampServiceTest.java b/src/test/java/org/sopt/app/application/StampServiceTest.java index 5b0f4f83..d1ac0f3b 100755 --- a/src/test/java/org/sopt/app/application/StampServiceTest.java +++ b/src/test/java/org/sopt/app/application/StampServiceTest.java @@ -3,11 +3,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; -import static org.sopt.app.common.fixtures.SoptampFixture.MISSION_ID; -import static org.sopt.app.common.fixtures.SoptampFixture.STAMP_CONTENTS; -import static org.sopt.app.common.fixtures.SoptampFixture.STAMP_IMG_PATHS; -import static org.sopt.app.common.fixtures.SoptampFixture.USER_ID; -import static org.sopt.app.common.fixtures.SoptampFixture.getEditStampRequest; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; +import static org.sopt.app.common.fixtures.SoptampFixture.*; import java.time.LocalDateTime; import java.util.List; @@ -22,6 +20,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.sopt.app.application.stamp.StampInfo; import org.sopt.app.application.stamp.StampService; +import org.sopt.app.common.event.EventPublisher; import org.sopt.app.common.exception.BadRequestException; import org.sopt.app.common.fixtures.SoptampFixture; import org.sopt.app.domain.entity.soptamp.Stamp; @@ -36,6 +35,9 @@ class StampServiceTest { @Mock private StampRepository stampRepository; + @Mock + private EventPublisher eventPublisher; + @InjectMocks private StampService stampService; @@ -405,6 +407,18 @@ void SUCCESS_deleteStampById() { }); } + @Test + void SUCCESS_모든_스탬프_삭제시_스탬프_삭제_이벤트_발생(){ + // given + final Long userId = anyLong(); + + // when + stampService.deleteAllStamps(userId); + + // then + verify(eventPublisher).raise(any()); + } + @Test @DisplayName("FAIL_스탬프를 찾지 못하면 BadRequestException 발생") void FAIL_deleteStampById() { @@ -420,6 +434,20 @@ void FAIL_deleteStampById() { }); } + @Test + void SUCCESS_스탬프_삭제시_스탬프_삭제_이벤트_발생(){ + // given + final Stamp stamp = Stamp.builder().build(); + final Long stampId = anyLong(); + given(stampRepository.findById(stampId)).willReturn(Optional.of(stamp)); + + // when + stampService.deleteStampById(stampId); + + // then + verify(eventPublisher).raise(any()); + } + @Test @DisplayName("SUCCESS_모든 스탬프 삭제") void SUCCESS_deleteAllStamps() { @@ -427,9 +455,7 @@ void SUCCESS_deleteAllStamps() { final Long anyUserId = anyLong(); //then - Assertions.assertDoesNotThrow(() -> { - stampService.deleteAllStamps(anyUserId); - }); + Assertions.assertDoesNotThrow(() -> stampService.deleteAllStamps(anyUserId)); } @Test diff --git a/src/test/java/org/sopt/app/application/UserServiceTest.java b/src/test/java/org/sopt/app/application/UserServiceTest.java index 61b784a6..ab971d5d 100755 --- a/src/test/java/org/sopt/app/application/UserServiceTest.java +++ b/src/test/java/org/sopt/app/application/UserServiceTest.java @@ -65,16 +65,6 @@ void SUCCESS_upsertRegisteredUser() { Assertions.assertEquals(anyUserId, result); } - @Test - @DisplayName("SUCCESS_유저 삭제") - void SUCCESS_deleteUser() { - //given - User user = new User(); - - //then - Assertions.assertDoesNotThrow(() -> userService.deleteUser(user)); - } - @Test @DisplayName("SUCCESS_플레이그라운드 토큰 조회") void SUCCESS_getPlaygroundToken() { diff --git a/src/test/java/org/sopt/app/application/UserWithdrawTest.java b/src/test/java/org/sopt/app/application/UserWithdrawTest.java new file mode 100644 index 00000000..62454495 --- /dev/null +++ b/src/test/java/org/sopt/app/application/UserWithdrawTest.java @@ -0,0 +1,50 @@ +package org.sopt.app.application; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.sopt.app.application.user.UserWithdrawService; +import org.sopt.app.common.event.EventPublisher; +import org.sopt.app.interfaces.postgres.UserRepository; + +@ExtendWith(MockitoExtension.class) +class UserWithdrawTest { + + @Mock + private EventPublisher eventPublisher; + + @Mock + private UserRepository userRepository; + + @InjectMocks + private UserWithdrawService userWithdrawService; + + @Test + void SUCCESS_유저_탈퇴시_유저_삭제() { + //given + final Long userId = 1L; + + // when + userWithdrawService.withdrawUser(userId); + + //then + verify(userRepository).deleteById(userId); + } + + @Test + void SUCCESS_유저_탈퇴시_스탬프_삭제_이벤트_발생() { + //given + final Long userId = 1L; + + // when + userWithdrawService.withdrawUser(userId); + + //then + verify(eventPublisher).raise(any()); + } +} diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index ac60be24..8c38d0d0 100755 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -1,9 +1,15 @@ spring: - application: - name: app-server + config: + import: optional:configserver:http://127.0.0.1:8087 + cloud: + config: + uri: http://127.0.0.1:8087 + name: application profiles: active: test -springdoc: - swagger-ui: - path: /swagger-ui.html \ No newline at end of file +management: + endpoints: + web: + exposure: + include: refresh \ No newline at end of file