- DELETE /api/auth/me: 본인 계정과 모든 데이터 영구 삭제 - 게시판(글/댓글/추천/이미지)·가계부 전체·포인트·결제기록·세션 → 회원 순 삭제 - WithdrawMapper(게시판/포인트/결제/세션) + BackupMapper(가계부) 재사용 - AuthServiceTest 생성자 보강 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -80,6 +80,15 @@ public class AuthController {
|
|||||||
return current;
|
return current;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 회원 탈퇴 — 본인 계정과 모든 데이터 삭제 */
|
||||||
|
@DeleteMapping("/me")
|
||||||
|
public ResponseEntity<Void> withdraw(
|
||||||
|
@RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current,
|
||||||
|
HttpServletRequest request) {
|
||||||
|
authService.withdraw(current.getId(), AuthInterceptor.resolveToken(request));
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
/** 현재 사용자 활동 포인트 (최신값 — 게시판 작성으로 수시 변동) */
|
/** 현재 사용자 활동 포인트 (최신값 — 게시판 작성으로 수시 변동) */
|
||||||
@GetMapping("/points")
|
@GetMapping("/points")
|
||||||
public java.util.Map<String, Long> points(
|
public java.util.Map<String, Long> points(
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -38,6 +38,8 @@ public class AuthService {
|
|||||||
private final RedisTemplate<String, Object> redisTemplate;
|
private final RedisTemplate<String, Object> redisTemplate;
|
||||||
private final com.sb.web.admin.service.AppSettingService appSettingService;
|
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.AuthSessionMapper authSessionMapper;
|
||||||
|
private final com.sb.web.auth.mapper.WithdrawMapper withdrawMapper;
|
||||||
|
private final com.sb.web.account.mapper.BackupMapper backupMapper;
|
||||||
|
|
||||||
/** 구글 OAuth 클라이언트 ID (ID 토큰 aud 검증용). 미설정 시 구글 로그인 비활성. */
|
/** 구글 OAuth 클라이언트 ID (ID 토큰 aud 검증용). 미설정 시 구글 로그인 비활성. */
|
||||||
@org.springframework.beans.factory.annotation.Value("${app.google-client-id:}")
|
@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시) */
|
/** 만료된 백업 세션 정리 (매일 새벽 4시) */
|
||||||
@org.springframework.scheduling.annotation.Scheduled(cron = "0 0 4 * * *")
|
@org.springframework.scheduling.annotation.Scheduled(cron = "0 0 4 * * *")
|
||||||
public void cleanupExpiredSessions() {
|
public void cleanupExpiredSessions() {
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
|
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.sb.web.auth.mapper.WithdrawMapper">
|
||||||
|
|
||||||
|
<!-- 내가 누른 표 -->
|
||||||
|
<delete id="deletePostVotesByMember">
|
||||||
|
DELETE FROM post_vote WHERE member_id = #{memberId}
|
||||||
|
</delete>
|
||||||
|
<delete id="deleteCommentVotesByMember">
|
||||||
|
DELETE FROM comment_vote WHERE member_id = #{memberId}
|
||||||
|
</delete>
|
||||||
|
|
||||||
|
<!-- 내 글에 달린 것들 -->
|
||||||
|
<delete id="deleteCommentVotesOnMyPosts">
|
||||||
|
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>
|
||||||
|
<delete id="deletePostVotesOnMyPosts">
|
||||||
|
DELETE pv FROM post_vote pv
|
||||||
|
JOIN post p ON p.id = pv.post_id
|
||||||
|
WHERE p.author_id = #{memberId}
|
||||||
|
</delete>
|
||||||
|
<delete id="deletePostTagsOnMyPosts">
|
||||||
|
DELETE pt FROM post_tag pt
|
||||||
|
JOIN post p ON p.id = pt.post_id
|
||||||
|
WHERE p.author_id = #{memberId}
|
||||||
|
</delete>
|
||||||
|
<delete id="deleteCommentsOnMyPosts">
|
||||||
|
DELETE c FROM comment c
|
||||||
|
JOIN post p ON p.id = c.post_id
|
||||||
|
WHERE p.author_id = #{memberId}
|
||||||
|
</delete>
|
||||||
|
|
||||||
|
<!-- 내 댓글과 그 표 -->
|
||||||
|
<delete id="deleteCommentVotesOnMyComments">
|
||||||
|
DELETE cv FROM comment_vote cv
|
||||||
|
JOIN comment c ON c.id = cv.comment_id
|
||||||
|
WHERE c.author_id = #{memberId}
|
||||||
|
</delete>
|
||||||
|
<delete id="deleteCommentsByAuthor">
|
||||||
|
DELETE FROM comment WHERE author_id = #{memberId}
|
||||||
|
</delete>
|
||||||
|
|
||||||
|
<!-- 내 글 / 업로드 이미지 -->
|
||||||
|
<delete id="deletePostsByAuthor">
|
||||||
|
DELETE FROM post WHERE author_id = #{memberId}
|
||||||
|
</delete>
|
||||||
|
<delete id="deleteBoardImagesByMember">
|
||||||
|
DELETE FROM board_image WHERE member_id = #{memberId}
|
||||||
|
</delete>
|
||||||
|
|
||||||
|
<!-- 포인트 / 결제 / 세션 -->
|
||||||
|
<delete id="deletePointHistory">
|
||||||
|
DELETE FROM point_history WHERE member_id = #{memberId}
|
||||||
|
</delete>
|
||||||
|
<delete id="deleteIapPurchases">
|
||||||
|
DELETE FROM iap_purchase WHERE member_id = #{memberId}
|
||||||
|
</delete>
|
||||||
|
<delete id="deleteAuthSessions">
|
||||||
|
DELETE FROM auth_session WHERE member_id = #{memberId}
|
||||||
|
</delete>
|
||||||
|
|
||||||
|
</mapper>
|
||||||
@@ -47,12 +47,15 @@ class AuthServiceTest {
|
|||||||
@Mock ValueOperations<String, Object> valueOps;
|
@Mock ValueOperations<String, Object> valueOps;
|
||||||
@Mock AppSettingService appSettingService;
|
@Mock AppSettingService appSettingService;
|
||||||
@Mock AuthSessionMapper authSessionMapper;
|
@Mock AuthSessionMapper authSessionMapper;
|
||||||
|
@Mock com.sb.web.auth.mapper.WithdrawMapper withdrawMapper;
|
||||||
|
@Mock com.sb.web.account.mapper.BackupMapper backupMapper;
|
||||||
|
|
||||||
AuthService service;
|
AuthService service;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
service = new AuthService(memberMapper, passwordEncoder, redisTemplate, appSettingService, authSessionMapper);
|
service = new AuthService(memberMapper, passwordEncoder, redisTemplate, appSettingService, authSessionMapper,
|
||||||
|
withdrawMapper, backupMapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Member localMember() {
|
private Member localMember() {
|
||||||
|
|||||||
Reference in New Issue
Block a user