diff --git a/src/main/java/com/sb/web/auth/controller/AuthController.java b/src/main/java/com/sb/web/auth/controller/AuthController.java index 46cbffd..c527c9c 100644 --- a/src/main/java/com/sb/web/auth/controller/AuthController.java +++ b/src/main/java/com/sb/web/auth/controller/AuthController.java @@ -80,6 +80,15 @@ public class AuthController { return current; } + /** 회원 탈퇴 — 본인 계정과 모든 데이터 삭제 */ + @DeleteMapping("/me") + public ResponseEntity withdraw( + @RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current, + HttpServletRequest request) { + authService.withdraw(current.getId(), AuthInterceptor.resolveToken(request)); + return ResponseEntity.noContent().build(); + } + /** 현재 사용자 활동 포인트 (최신값 — 게시판 작성으로 수시 변동) */ @GetMapping("/points") public java.util.Map points( diff --git a/src/main/java/com/sb/web/auth/mapper/WithdrawMapper.java b/src/main/java/com/sb/web/auth/mapper/WithdrawMapper.java new file mode 100644 index 0000000..60e652a --- /dev/null +++ b/src/main/java/com/sb/web/auth/mapper/WithdrawMapper.java @@ -0,0 +1,34 @@ +package com.sb.web.auth.mapper; + +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +/** + * 회원 탈퇴 시 가계부(BackupMapper) 외의 사용자 데이터 정리 — 게시판/포인트/결제/세션. + */ +@Mapper +public interface WithdrawMapper { + + // 게시판 — 내가 누른 표 + int deletePostVotesByMember(@Param("memberId") Long memberId); + int deleteCommentVotesByMember(@Param("memberId") Long memberId); + + // 게시판 — 내 글에 달린 것들 (남이 단 댓글/표 포함) + int deleteCommentVotesOnMyPosts(@Param("memberId") Long memberId); + int deletePostVotesOnMyPosts(@Param("memberId") Long memberId); + int deletePostTagsOnMyPosts(@Param("memberId") Long memberId); + int deleteCommentsOnMyPosts(@Param("memberId") Long memberId); + + // 게시판 — 내 댓글(남의 글에 단 것)과 그 표 + int deleteCommentVotesOnMyComments(@Param("memberId") Long memberId); + int deleteCommentsByAuthor(@Param("memberId") Long memberId); + + // 게시판 — 내 글 + int deletePostsByAuthor(@Param("memberId") Long memberId); + int deleteBoardImagesByMember(@Param("memberId") Long memberId); + + // 포인트 / 결제 / 세션 + int deletePointHistory(@Param("memberId") Long memberId); + int deleteIapPurchases(@Param("memberId") Long memberId); + int deleteAuthSessions(@Param("memberId") Long memberId); +} diff --git a/src/main/java/com/sb/web/auth/service/AuthService.java b/src/main/java/com/sb/web/auth/service/AuthService.java index 97976e4..8eca1d9 100644 --- a/src/main/java/com/sb/web/auth/service/AuthService.java +++ b/src/main/java/com/sb/web/auth/service/AuthService.java @@ -38,6 +38,8 @@ public class AuthService { private final RedisTemplate redisTemplate; private final com.sb.web.admin.service.AppSettingService appSettingService; private final com.sb.web.auth.mapper.AuthSessionMapper authSessionMapper; + private final com.sb.web.auth.mapper.WithdrawMapper withdrawMapper; + private final com.sb.web.account.mapper.BackupMapper backupMapper; /** 구글 OAuth 클라이언트 ID (ID 토큰 aud 검증용). 미설정 시 구글 로그인 비활성. */ @org.springframework.beans.factory.annotation.Value("${app.google-client-id:}") @@ -289,6 +291,50 @@ public class AuthService { } } + /** + * 회원 탈퇴 — 사용자가 소유한 모든 데이터를 삭제하고 회원을 제거한다. + * (게시판 글/댓글/추천 · 가계부 전체 · 포인트 · 결제기록 · 세션 → 회원) + */ + @Transactional + public void withdraw(Long memberId, String token) { + Member member = memberMapper.findById(memberId); + if (member == null) { + throw new ApiException(HttpStatus.NOT_FOUND, "회원을 찾을 수 없습니다."); + } + // 1) 게시판 — 표/댓글/글/이미지 (참조 순서 고려) + withdrawMapper.deletePostVotesByMember(memberId); + withdrawMapper.deleteCommentVotesByMember(memberId); + withdrawMapper.deleteCommentVotesOnMyPosts(memberId); + withdrawMapper.deleteCommentVotesOnMyComments(memberId); + withdrawMapper.deleteCommentsOnMyPosts(memberId); + withdrawMapper.deleteCommentsByAuthor(memberId); + withdrawMapper.deletePostVotesOnMyPosts(memberId); + withdrawMapper.deletePostTagsOnMyPosts(memberId); + withdrawMapper.deletePostsByAuthor(memberId); + withdrawMapper.deleteBoardImagesByMember(memberId); + // 2) 가계부(계정) 데이터 (복구 삭제와 동일 순서) + backupMapper.deleteEntryTags(memberId); + backupMapper.deleteEntries(memberId); + backupMapper.deleteRecurrings(memberId); + backupMapper.deleteBudgets(memberId); + backupMapper.deleteBudgetIncome(memberId); + backupMapper.deleteQuickEntries(memberId); + backupMapper.deleteInvestTrades(memberId); + backupMapper.deleteInvestHoldings(memberId); + backupMapper.deleteCategories(memberId); + backupMapper.deleteTags(memberId); + backupMapper.deleteWallets(memberId); + // 3) 포인트 / 결제 / 세션 + withdrawMapper.deletePointHistory(memberId); + withdrawMapper.deleteIapPurchases(memberId); + withdrawMapper.deleteAuthSessions(memberId); + // 4) 회원 삭제 + memberMapper.deleteById(memberId); + // 5) 현재 세션(Redis) 정리 + logout(token); + log.info("[withdraw] member {} ({}) 및 데이터 전체 삭제", memberId, member.getLoginId()); + } + /** 만료된 백업 세션 정리 (매일 새벽 4시) */ @org.springframework.scheduling.annotation.Scheduled(cron = "0 0 4 * * *") public void cleanupExpiredSessions() { diff --git a/src/main/resources/mapper/WithdrawMapper.xml b/src/main/resources/mapper/WithdrawMapper.xml new file mode 100644 index 0000000..53ea42c --- /dev/null +++ b/src/main/resources/mapper/WithdrawMapper.xml @@ -0,0 +1,66 @@ + + + + + + + DELETE FROM post_vote WHERE member_id = #{memberId} + + + DELETE FROM comment_vote WHERE member_id = #{memberId} + + + + + DELETE cv FROM comment_vote cv + JOIN comment c ON c.id = cv.comment_id + JOIN post p ON p.id = c.post_id + WHERE p.author_id = #{memberId} + + + DELETE pv FROM post_vote pv + JOIN post p ON p.id = pv.post_id + WHERE p.author_id = #{memberId} + + + DELETE pt FROM post_tag pt + JOIN post p ON p.id = pt.post_id + WHERE p.author_id = #{memberId} + + + DELETE c FROM comment c + JOIN post p ON p.id = c.post_id + WHERE p.author_id = #{memberId} + + + + + DELETE cv FROM comment_vote cv + JOIN comment c ON c.id = cv.comment_id + WHERE c.author_id = #{memberId} + + + DELETE FROM comment WHERE author_id = #{memberId} + + + + + DELETE FROM post WHERE author_id = #{memberId} + + + DELETE FROM board_image WHERE member_id = #{memberId} + + + + + DELETE FROM point_history WHERE member_id = #{memberId} + + + DELETE FROM iap_purchase WHERE member_id = #{memberId} + + + DELETE FROM auth_session WHERE member_id = #{memberId} + + + diff --git a/src/test/java/com/sb/web/auth/service/AuthServiceTest.java b/src/test/java/com/sb/web/auth/service/AuthServiceTest.java index dcbfac8..f5ddff1 100644 --- a/src/test/java/com/sb/web/auth/service/AuthServiceTest.java +++ b/src/test/java/com/sb/web/auth/service/AuthServiceTest.java @@ -47,12 +47,15 @@ class AuthServiceTest { @Mock ValueOperations valueOps; @Mock AppSettingService appSettingService; @Mock AuthSessionMapper authSessionMapper; + @Mock com.sb.web.auth.mapper.WithdrawMapper withdrawMapper; + @Mock com.sb.web.account.mapper.BackupMapper backupMapper; AuthService service; @BeforeEach void setUp() { - service = new AuthService(memberMapper, passwordEncoder, redisTemplate, appSettingService, authSessionMapper); + service = new AuthService(memberMapper, passwordEncoder, redisTemplate, appSettingService, authSessionMapper, + withdrawMapper, backupMapper); } private Member localMember() {