Skip to content

Commit 1dea59a

Browse files
authored
Merge pull request #216 from Clokey-dev/develop
[DEPLOY] 데이터 적재 스크립트 추가
2 parents b4328af + b62674e commit 1dea59a

17 files changed

Lines changed: 1277 additions & 0 deletions

File tree

clokey-api/src/main/java/org/clokey/ClokeyApiApplication.java

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

33
import org.springframework.boot.SpringApplication;
44
import org.springframework.boot.autoconfigure.SpringBootApplication;
5+
import org.springframework.scheduling.annotation.EnableScheduling;
56

67
@SpringBootApplication
8+
@EnableScheduling
79
public class ClokeyApiApplication {
810

911
public static void main(String[] args) {

clokey-api/src/main/java/org/clokey/domain/auth/controller/AuthController.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import org.clokey.code.GlobalBaseSuccessCode;
99
import org.clokey.domain.auth.dto.request.DeviceTokenRenewRequest;
1010
import org.clokey.domain.auth.dto.request.TokenReissueRequest;
11+
import org.clokey.domain.auth.dto.request.UserStatusUpdateRequest;
1112
import org.clokey.domain.auth.dto.response.TokenResponse;
1213
import org.clokey.domain.auth.dto.response.UserStatusResponse;
1314
import org.clokey.domain.auth.service.AuthService;
@@ -64,4 +65,25 @@ public BaseResponse<Void> logoutUser() {
6465
authService.logoutUser();
6566
return BaseResponse.onSuccess(GlobalBaseSuccessCode.NO_CONTENT, null);
6667
}
68+
69+
@PatchMapping("/user-status")
70+
@Operation(
71+
operationId = "Auth_updateUserStatus",
72+
summary = "회원 활성화/비활성화",
73+
description = "true: 활성화, false: 비활성화 (15일 뒤 회원 탈퇴)")
74+
public BaseResponse<Void> updateUserStatus(
75+
@Valid @RequestBody UserStatusUpdateRequest request) {
76+
authService.updateUserStatus(request);
77+
return BaseResponse.onSuccess(GlobalBaseSuccessCode.NO_CONTENT, null);
78+
}
79+
80+
@DeleteMapping
81+
@Operation(
82+
operationId = "Auth_withdrawMember",
83+
summary = "회원 탈퇴",
84+
description = "(개발용) 회원과 관련된 모든 데이터를 즉시 삭제합니다.")
85+
public BaseResponse<Void> withdrawMember() {
86+
authService.withdrawMember();
87+
return BaseResponse.onSuccess(GlobalBaseSuccessCode.NO_CONTENT, null);
88+
}
6789
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package org.clokey.domain.auth.dto.request;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
import jakarta.validation.constraints.NotNull;
5+
6+
public record UserStatusUpdateRequest(
7+
@NotNull(message = "활성화 여부는 필수입니다.") @Schema(description = "true: 활성화, false: 비활성화")
8+
Boolean active) {}

clokey-api/src/main/java/org/clokey/domain/auth/service/AuthService.java

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

33
import org.clokey.domain.auth.dto.request.DeviceTokenRenewRequest;
44
import org.clokey.domain.auth.dto.request.TokenReissueRequest;
5+
import org.clokey.domain.auth.dto.request.UserStatusUpdateRequest;
56
import org.clokey.domain.auth.dto.response.TokenResponse;
67
import org.clokey.domain.auth.dto.response.UserStatusResponse;
78

@@ -14,4 +15,10 @@ public interface AuthService {
1415
TokenResponse reissueTokens(TokenReissueRequest request);
1516

1617
void logoutUser();
18+
19+
void updateUserStatus(UserStatusUpdateRequest request);
20+
21+
void withdrawMember();
22+
23+
void withdrawMemberById(Long memberId);
1724
}

clokey-api/src/main/java/org/clokey/domain/auth/service/AuthServiceImpl.java

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,49 @@
11
package org.clokey.domain.auth.service;
22

3+
import java.util.ArrayList;
4+
import java.util.List;
5+
import java.util.stream.Collectors;
36
import lombok.RequiredArgsConstructor;
7+
import org.clokey.cloth.entity.Cloth;
8+
import org.clokey.coordinate.entity.Coordinate;
49
import org.clokey.domain.auth.dto.AccessTokenDto;
510
import org.clokey.domain.auth.dto.RefreshTokenDto;
611
import org.clokey.domain.auth.dto.request.DeviceTokenRenewRequest;
712
import org.clokey.domain.auth.dto.request.TokenReissueRequest;
13+
import org.clokey.domain.auth.dto.request.UserStatusUpdateRequest;
814
import org.clokey.domain.auth.dto.response.TokenResponse;
915
import org.clokey.domain.auth.dto.response.UserStatusResponse;
1016
import org.clokey.domain.auth.enums.RegisterStatus;
1117
import org.clokey.domain.auth.exception.AuthErrorCode;
1218
import org.clokey.domain.auth.repository.RefreshTokenRepository;
19+
import org.clokey.domain.cloth.repository.ClothRepository;
20+
import org.clokey.domain.comment.repository.CommentRepository;
21+
import org.clokey.domain.coordinate.repository.CoordinateClothRepository;
22+
import org.clokey.domain.coordinate.repository.CoordinateRepository;
23+
import org.clokey.domain.folder.repository.ClothFolderRepository;
24+
import org.clokey.domain.folder.repository.FolderRepository;
25+
import org.clokey.domain.history.repository.HistoryClothTagRepository;
26+
import org.clokey.domain.history.repository.HistoryHashtagRepository;
27+
import org.clokey.domain.history.repository.HistoryImageRepository;
28+
import org.clokey.domain.history.repository.HistoryRepository;
29+
import org.clokey.domain.history.repository.HistoryStyleRepository;
30+
import org.clokey.domain.image.event.ImagesDeleteEvent;
31+
import org.clokey.domain.like.repository.MemberLikeRepository;
32+
import org.clokey.domain.lookbook.repository.LookBookRepository;
1333
import org.clokey.domain.member.exception.MemberErrorCode;
34+
import org.clokey.domain.member.repository.BlockRepository;
35+
import org.clokey.domain.member.repository.FollowRepository;
1436
import org.clokey.domain.member.repository.MemberRepository;
37+
import org.clokey.domain.member.repository.PendingFollowRepository;
38+
import org.clokey.domain.notification.repository.CodiveNotificationRepository;
39+
import org.clokey.domain.report.repository.ReportRepository;
1540
import org.clokey.domain.term.repository.MemberTermRepository;
1641
import org.clokey.exception.BaseCustomException;
1742
import org.clokey.global.util.MemberUtil;
43+
import org.clokey.history.entity.History;
44+
import org.clokey.history.entity.HistoryImage;
1845
import org.clokey.member.entity.Member;
46+
import org.springframework.context.ApplicationEventPublisher;
1947
import org.springframework.stereotype.Service;
2048
import org.springframework.transaction.annotation.Transactional;
2149

@@ -30,6 +58,27 @@ public class AuthServiceImpl implements AuthService {
3058
private final MemberRepository memberRepository;
3159
private final JwtTokenService jwtTokenService;
3260
private final RefreshTokenRepository refreshTokenRepository;
61+
private final ApplicationEventPublisher eventPublisher;
62+
63+
// 회원 탈퇴 관련 Repository
64+
private final HistoryRepository historyRepository;
65+
private final HistoryImageRepository historyImageRepository;
66+
private final HistoryClothTagRepository historyClothTagRepository;
67+
private final HistoryStyleRepository historyStyleRepository;
68+
private final HistoryHashtagRepository historyHashtagRepository;
69+
private final MemberLikeRepository memberLikeRepository;
70+
private final CommentRepository commentRepository;
71+
private final CoordinateRepository coordinateRepository;
72+
private final CoordinateClothRepository coordinateClothRepository;
73+
private final LookBookRepository lookBookRepository;
74+
private final ClothRepository clothRepository;
75+
private final ClothFolderRepository clothFolderRepository;
76+
private final FolderRepository folderRepository;
77+
private final CodiveNotificationRepository codiveNotificationRepository;
78+
private final FollowRepository followRepository;
79+
private final PendingFollowRepository pendingFollowRepository;
80+
private final BlockRepository blockRepository;
81+
private final ReportRepository reportRepository;
3382

3483
@Override
3584
public UserStatusResponse getUserStatus() {
@@ -76,6 +125,217 @@ public void logoutUser() {
76125
.ifPresent(refreshTokenRepository::delete);
77126
}
78127

128+
@Override
129+
@Transactional
130+
public void updateUserStatus(UserStatusUpdateRequest request) {
131+
final Member currentMember = memberUtil.getCurrentMember();
132+
if (request.active()) {
133+
currentMember.activate();
134+
} else {
135+
currentMember.deactivate();
136+
}
137+
}
138+
139+
@Override
140+
@Transactional
141+
public void withdrawMember() {
142+
final Member currentMember = memberUtil.getCurrentMember();
143+
withdrawMemberById(currentMember.getId());
144+
}
145+
146+
// TODO: (중요 메모) 절대로 - 카테고리, 상황, 약관, 스타일은 삭제하면 안됩니다!! 미리 적재된 데이터입니다.
147+
@Override
148+
@Transactional
149+
public void withdrawMemberById(Long memberId) {
150+
final Member currentMember =
151+
memberRepository
152+
.findById(memberId)
153+
.orElseThrow(
154+
() -> new BaseCustomException(MemberErrorCode.MEMBER_NOT_FOUND));
155+
156+
List<String> imageUrlsToDelete = new ArrayList<>();
157+
158+
// 1. History 관련 데이터 삭제
159+
List<History> histories =
160+
historyRepository.findAll().stream()
161+
.filter(h -> h.getMember().getId().equals(memberId))
162+
.collect(Collectors.toList());
163+
164+
List<Long> historyIds = histories.stream().map(History::getId).collect(Collectors.toList());
165+
166+
if (!historyIds.isEmpty()) {
167+
// HistoryClothTag 삭제
168+
List<Long> historyImageIds =
169+
historyImageRepository.findAll().stream()
170+
.filter(hi -> historyIds.contains(hi.getHistory().getId()))
171+
.map(HistoryImage::getId)
172+
.collect(Collectors.toList());
173+
174+
historyImageIds.forEach(
175+
historyImageId ->
176+
historyClothTagRepository
177+
.findByHistoryImageId(historyImageId)
178+
.forEach(historyClothTagRepository::delete));
179+
180+
// HistoryImage 삭제 및 이미지 URL 수집
181+
List<HistoryImage> historyImages =
182+
historyImageRepository.findAll().stream()
183+
.filter(hi -> historyIds.contains(hi.getHistory().getId()))
184+
.collect(Collectors.toList());
185+
186+
historyImages.forEach(
187+
hi -> {
188+
if (hi.getImageUrl() != null) {
189+
imageUrlsToDelete.add(hi.getImageUrl());
190+
}
191+
});
192+
historyImageRepository.deleteAll(historyImages);
193+
194+
// HistoryHashtag 삭제
195+
historyIds.forEach(
196+
historyId ->
197+
historyHashtagRepository
198+
.findByHistoryId(historyId)
199+
.forEach(historyHashtagRepository::delete));
200+
201+
// HistoryStyle 삭제
202+
historyIds.forEach(
203+
historyId ->
204+
historyStyleRepository
205+
.findByHistoryId(historyId)
206+
.forEach(historyStyleRepository::delete));
207+
208+
// MemberLike 삭제 (해당 회원이 좋아요한 것)
209+
memberLikeRepository.findAll().stream()
210+
.filter(ml -> ml.getMember().getId().equals(memberId))
211+
.forEach(memberLikeRepository::delete);
212+
213+
// Comment 삭제
214+
commentRepository.findAll().stream()
215+
.filter(c -> c.getMember().getId().equals(memberId))
216+
.forEach(commentRepository::delete);
217+
218+
// History 삭제
219+
historyRepository.deleteAll(histories);
220+
}
221+
222+
// 2. Coordinate 관련 데이터 삭제
223+
List<Coordinate> coordinates =
224+
coordinateRepository.findAll().stream()
225+
.filter(c -> c.getMember().getId().equals(memberId))
226+
.collect(Collectors.toList());
227+
228+
List<Long> coordinateIds =
229+
coordinates.stream().map(Coordinate::getId).collect(Collectors.toList());
230+
231+
if (!coordinateIds.isEmpty()) {
232+
// CoordinateCloth 삭제
233+
coordinateIds.forEach(coordinateClothRepository::deleteAllByCoordinateId);
234+
235+
// Coordinate 이미지 URL 수집 및 삭제
236+
coordinates.forEach(
237+
c -> {
238+
if (c.getImageUrl() != null) {
239+
imageUrlsToDelete.add(c.getImageUrl());
240+
}
241+
});
242+
coordinateRepository.deleteAll(coordinates);
243+
}
244+
245+
// 3. LookBook 삭제
246+
lookBookRepository.findAll().stream()
247+
.filter(lb -> lb.getMember().getId().equals(memberId))
248+
.forEach(lookBookRepository::delete);
249+
250+
// 4. Cloth 관련 데이터 삭제
251+
List<Cloth> clothes =
252+
clothRepository.findAll().stream()
253+
.filter(c -> c.getMember().getId().equals(memberId))
254+
.collect(Collectors.toList());
255+
256+
List<Long> clothIds = clothes.stream().map(Cloth::getId).collect(Collectors.toList());
257+
258+
if (!clothIds.isEmpty()) {
259+
// ClothFolder 삭제
260+
clothIds.forEach(clothFolderRepository::deleteAllByClothId);
261+
262+
// Cloth 이미지 URL 수집 및 삭제
263+
clothes.forEach(
264+
c -> {
265+
if (c.getClothImageUrl() != null) {
266+
imageUrlsToDelete.add(c.getClothImageUrl());
267+
}
268+
});
269+
clothRepository.deleteAll(clothes);
270+
}
271+
272+
// 5. Folder 삭제
273+
folderRepository.findAll().stream()
274+
.filter(f -> f.getMember().getId().equals(memberId))
275+
.forEach(folderRepository::delete);
276+
277+
// 6. MemberTerm 삭제
278+
memberTermRepository.findAll().stream()
279+
.filter(mt -> mt.getMember().getId().equals(memberId))
280+
.forEach(memberTermRepository::delete);
281+
282+
// 7. CodiveNotification 삭제
283+
codiveNotificationRepository.findAll().stream()
284+
.filter(cn -> cn.getMember().getId().equals(memberId))
285+
.forEach(codiveNotificationRepository::delete);
286+
287+
// 8. Follow 삭제 (followFrom, followTo 모두)
288+
followRepository.findAll().stream()
289+
.filter(
290+
f ->
291+
f.getFollowFrom().getId().equals(memberId)
292+
|| f.getFollowTo().getId().equals(memberId))
293+
.forEach(followRepository::delete);
294+
295+
// 9. PendingFollow 삭제
296+
pendingFollowRepository.findAll().stream()
297+
.filter(
298+
pf ->
299+
pf.getFollowFrom().getId().equals(memberId)
300+
|| pf.getFollowTo().getId().equals(memberId))
301+
.forEach(pendingFollowRepository::delete);
302+
303+
// 10. Block 삭제 (blocker, blocked 모두)
304+
blockRepository.findAll().stream()
305+
.filter(
306+
b ->
307+
b.getBlocker().getId().equals(memberId)
308+
|| b.getBlocked().getId().equals(memberId))
309+
.forEach(blockRepository::delete);
310+
311+
// 11. Report 삭제 (reporter, reported 모두)
312+
reportRepository.findAll().stream()
313+
.filter(
314+
r ->
315+
r.getReporter().getId().equals(memberId)
316+
|| r.getReported().getId().equals(memberId))
317+
.forEach(reportRepository::delete);
318+
319+
// 12. RefreshToken 삭제
320+
refreshTokenRepository.findById(memberId).ifPresent(refreshTokenRepository::delete);
321+
322+
// 13. Member 프로필 이미지 URL 수집
323+
if (currentMember.getProfileImageUrl() != null) {
324+
imageUrlsToDelete.add(currentMember.getProfileImageUrl());
325+
}
326+
if (currentMember.getProfileBackImageUrl() != null) {
327+
imageUrlsToDelete.add(currentMember.getProfileBackImageUrl());
328+
}
329+
330+
// 14. 이미지 삭제 이벤트 발행 (비동기 처리)
331+
if (!imageUrlsToDelete.isEmpty()) {
332+
eventPublisher.publishEvent(ImagesDeleteEvent.of(imageUrlsToDelete));
333+
}
334+
335+
// 15. Member 삭제
336+
memberRepository.delete(currentMember);
337+
}
338+
79339
private Member getMember(RefreshTokenDto refreshTokenDto) {
80340
return memberRepository
81341
.findById(refreshTokenDto.memberId())

0 commit comments

Comments
 (0)