Skip to content

Commit 1359104

Browse files
committed
feat: 회원 탈퇴 시 사용자 연관 데이터 정리 범위 확장
1 parent 0283365 commit 1359104

7 files changed

Lines changed: 155 additions & 0 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.semosan.api.domain.hiking.repository;
2+
3+
import com.semosan.api.domain.hiking.entity.HikingMember;
4+
import org.springframework.data.jpa.repository.JpaRepository;
5+
import org.springframework.data.jpa.repository.Modifying;
6+
import org.springframework.data.jpa.repository.Query;
7+
import org.springframework.data.repository.query.Param;
8+
9+
public interface HikingMemberRepository extends JpaRepository<HikingMember, Long> {
10+
11+
@Modifying(clearAutomatically = true)
12+
@Query("DELETE FROM HikingMember hm WHERE hm.user.id = :userId")
13+
void deleteByUser_Id(@Param("userId") Long userId);
14+
}

src/main/java/com/semosan/api/domain/hiking/repository/HikingRecordRepository.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,26 @@
1010
import org.springframework.data.jpa.repository.Query;
1111
import org.springframework.data.repository.query.Param;
1212

13+
import java.util.List;
14+
1315
public interface HikingRecordRepository extends JpaRepository<HikingRecord, Long> {
1416

17+
@Query(
18+
value = """
19+
SELECT hm.hiking_record_id
20+
FROM hiking_members hm
21+
WHERE hm.user_id = :userId
22+
AND NOT EXISTS (
23+
SELECT 1
24+
FROM hiking_members other_hm
25+
WHERE other_hm.hiking_record_id = hm.hiking_record_id
26+
AND other_hm.user_id <> :userId
27+
)
28+
""",
29+
nativeQuery = true
30+
)
31+
List<Long> findRecordIdsOnlyParticipatedByUser(@Param("userId") Long userId);
32+
1533
@Query(
1634
value = """
1735
SELECT

src/main/java/com/semosan/api/domain/mountain/repository/MountainLikeRepository.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import org.springframework.data.domain.Pageable;
66
import org.springframework.data.jpa.repository.EntityGraph;
77
import org.springframework.data.jpa.repository.JpaRepository;
8+
import org.springframework.data.jpa.repository.Modifying;
89
import org.springframework.data.jpa.repository.Query;
910
import org.springframework.data.repository.query.Param;
1011

@@ -16,6 +17,10 @@ public interface MountainLikeRepository extends JpaRepository<MountainLike, Long
1617

1718
Optional<MountainLike> findByUser_IdAndMountain_Id(Long userId, Long mountainId);
1819

20+
@Modifying(clearAutomatically = true)
21+
@Query("DELETE FROM MountainLike ml WHERE ml.user.id = :userId")
22+
void deleteByUser_Id(@Param("userId") Long userId);
23+
1924
@EntityGraph(attributePaths = "mountain")
2025
@Query(
2126
value = "SELECT ml FROM MountainLike ml WHERE ml.user.id = :userId",

src/main/java/com/semosan/api/domain/notification/repository/NotificationRepository.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,17 @@
22

33
import com.semosan.api.domain.notification.entity.Notification;
44
import org.springframework.data.jpa.repository.JpaRepository;
5+
import org.springframework.data.jpa.repository.Modifying;
6+
import org.springframework.data.jpa.repository.Query;
7+
import org.springframework.data.repository.query.Param;
58

69
import java.util.List;
710

811
// TODO: 알림판 구현할 때 쓸 예정
912
public interface NotificationRepository extends JpaRepository<Notification, Long> {
1013
List<Notification> findAllByUserIdOrderByCreatedAtDesc(Long userId);
14+
15+
@Modifying(clearAutomatically = true)
16+
@Query("DELETE FROM Notification n WHERE n.userId = :userId")
17+
void deleteAllByUserId(@Param("userId") Long userId);
1118
}

src/main/java/com/semosan/api/domain/review/repository/ReviewRepository.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,18 @@
33
import com.semosan.api.domain.review.entity.Review;
44
import org.springframework.data.jpa.repository.EntityGraph;
55
import org.springframework.data.jpa.repository.JpaRepository;
6+
import org.springframework.data.jpa.repository.Modifying;
67
import org.springframework.data.jpa.repository.Query;
78
import org.springframework.data.repository.query.Param;
89

910
import java.util.List;
1011

1112
public interface ReviewRepository extends JpaRepository<Review, Long> {
1213

14+
@Modifying(clearAutomatically = true)
15+
@Query("DELETE FROM Review r WHERE r.user.id = :userId")
16+
void deleteByUser_Id(@Param("userId") Long userId);
17+
1318
@EntityGraph(attributePaths = {"user", "course"})
1419
@Query("SELECT r FROM Review r WHERE r.mountain.id = :mountainId ORDER BY r.createdAt DESC LIMIT :limit")
1520
List<Review> findRecentByMountainId(@Param("mountainId") Long mountainId, @Param("limit") int limit);

src/main/java/com/semosan/api/domain/user/service/UserService.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
import com.semosan.api.common.exception.GeneralException;
44
import com.semosan.api.common.status.ErrorStatus;
5+
import com.semosan.api.domain.hiking.repository.HikingMemberRepository;
6+
import com.semosan.api.domain.hiking.repository.HikingRecordRepository;
7+
import com.semosan.api.domain.mountain.repository.MountainLikeRepository;
8+
import com.semosan.api.domain.notification.repository.NotificationRepository;
9+
import com.semosan.api.domain.review.repository.ReviewRepository;
510
import com.semosan.api.domain.user.dto.command.UpdateUserProfileCommand;
611
import com.semosan.api.domain.user.dto.request.UpdateUserProfileRequest;
712
import com.semosan.api.domain.user.entity.User;
@@ -17,13 +22,20 @@
1722
import org.springframework.stereotype.Service;
1823
import org.springframework.transaction.annotation.Transactional;
1924

25+
import java.util.List;
26+
2027
@Service
2128
@RequiredArgsConstructor
2229
public class UserService {
2330

2431
private final UserRepository userRepository;
2532
private final UserNotificationSettingRepository userNotificationSettingRepository;
2633
private final UserOnboardingRepository userOnboardingRepository;
34+
private final MountainLikeRepository mountainLikeRepository;
35+
private final ReviewRepository reviewRepository;
36+
private final HikingMemberRepository hikingMemberRepository;
37+
private final HikingRecordRepository hikingRecordRepository;
38+
private final NotificationRepository notificationRepository;
2739
private final NicknamePolicy nicknamePolicy;
2840
private final UserReader userReader;
2941

@@ -148,6 +160,14 @@ public void withdrawUser(User user) {
148160
}
149161

150162
private void deleteUserChildRecords(Long userId) {
163+
mountainLikeRepository.deleteByUser_Id(userId);
164+
reviewRepository.deleteByUser_Id(userId);
165+
List<Long> recordIdsToDelete = hikingRecordRepository.findRecordIdsOnlyParticipatedByUser(userId);
166+
hikingMemberRepository.deleteByUser_Id(userId);
167+
if (!recordIdsToDelete.isEmpty()) {
168+
hikingRecordRepository.deleteAllByIdInBatch(recordIdsToDelete);
169+
}
170+
notificationRepository.deleteAllByUserId(userId);
151171
userOnboardingRepository.deleteByUser_Id(userId);
152172
userNotificationSettingRepository.deleteByUser_Id(userId);
153173
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package com.semosan.api.domain.user.service;
2+
3+
import com.semosan.api.domain.hiking.repository.HikingMemberRepository;
4+
import com.semosan.api.domain.hiking.repository.HikingRecordRepository;
5+
import com.semosan.api.domain.mountain.repository.MountainLikeRepository;
6+
import com.semosan.api.domain.notification.repository.NotificationRepository;
7+
import com.semosan.api.domain.review.repository.ReviewRepository;
8+
import com.semosan.api.domain.user.entity.User;
9+
import com.semosan.api.domain.user.enums.user.DeviceType;
10+
import com.semosan.api.domain.user.enums.user.OnboardingStatus;
11+
import com.semosan.api.domain.user.policy.NicknamePolicy;
12+
import com.semosan.api.domain.user.repository.UserNotificationSettingRepository;
13+
import com.semosan.api.domain.user.repository.UserOnboardingRepository;
14+
import com.semosan.api.domain.user.repository.UserRepository;
15+
import org.junit.jupiter.api.Test;
16+
import org.junit.jupiter.api.extension.ExtendWith;
17+
import org.mockito.InjectMocks;
18+
import org.mockito.Mock;
19+
import org.mockito.junit.jupiter.MockitoExtension;
20+
import org.springframework.test.util.ReflectionTestUtils;
21+
22+
import java.util.List;
23+
24+
import static org.assertj.core.api.Assertions.assertThat;
25+
import static org.mockito.Mockito.verify;
26+
import static org.mockito.Mockito.when;
27+
28+
@ExtendWith(MockitoExtension.class)
29+
class UserServiceTest {
30+
31+
@Mock
32+
private UserRepository userRepository;
33+
34+
@Mock
35+
private UserNotificationSettingRepository userNotificationSettingRepository;
36+
37+
@Mock
38+
private UserOnboardingRepository userOnboardingRepository;
39+
40+
@Mock
41+
private MountainLikeRepository mountainLikeRepository;
42+
43+
@Mock
44+
private ReviewRepository reviewRepository;
45+
46+
@Mock
47+
private HikingMemberRepository hikingMemberRepository;
48+
49+
@Mock
50+
private HikingRecordRepository hikingRecordRepository;
51+
52+
@Mock
53+
private NotificationRepository notificationRepository;
54+
55+
@Mock
56+
private NicknamePolicy nicknamePolicy;
57+
58+
@Mock
59+
private UserReader userReader;
60+
61+
@InjectMocks
62+
private UserService userService;
63+
64+
@Test
65+
void withdrawUserDeletesUserChildRecordsAndSoftDeletesUser() {
66+
User user = User.createTestUser("withdraw-test-user", DeviceType.IOS);
67+
ReflectionTestUtils.setField(user, "id", 1L);
68+
when(hikingRecordRepository.findRecordIdsOnlyParticipatedByUser(1L)).thenReturn(List.of(10L, 11L));
69+
70+
userService.withdrawUser(user);
71+
72+
verify(mountainLikeRepository).deleteByUser_Id(1L);
73+
verify(reviewRepository).deleteByUser_Id(1L);
74+
verify(hikingRecordRepository).findRecordIdsOnlyParticipatedByUser(1L);
75+
verify(hikingMemberRepository).deleteByUser_Id(1L);
76+
verify(hikingRecordRepository).deleteAllByIdInBatch(List.of(10L, 11L));
77+
verify(notificationRepository).deleteAllByUserId(1L);
78+
verify(userOnboardingRepository).deleteByUser_Id(1L);
79+
verify(userNotificationSettingRepository).deleteByUser_Id(1L);
80+
81+
assertThat(user.isDeleted()).isTrue();
82+
assertThat(user.getEmail()).isNull();
83+
assertThat(user.getName()).isNull();
84+
assertThat(user.getOnboardingStatus()).isEqualTo(OnboardingStatus.INCOMPLETE);
85+
}
86+
}

0 commit comments

Comments
 (0)