Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public enum ErrorStatus implements BaseErrorCode {
MEMBER_EXIST_IN_OTHER_SOCIAL(HttpStatus.BAD_REQUEST, "MEMBER4006", "다른 소셜로 가입한 회원입니다."),
MEMBER_ALREADY_EXIST(HttpStatus.BAD_REQUEST, "MEMBER4007", "이미 가입한 회원입니다."),
MEMBER_CONFLICT(HttpStatus.BAD_REQUEST, "MEMBER4008", "이메일과 멤버ID가 일치하지 않습니다."),
WITHDRAWAL_REASON_EMPTY(HttpStatus.BAD_REQUEST, "MEMBER4009", "기타 사유 선택 시 상세 내용을 입력해주세요."),

//Record 에러
RECORD_NOT_FOUND(HttpStatus.BAD_REQUEST, "RECORD4000", "해당하는 일지를 찾을 수 없습니다."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import io.micrometer.common.lang.Nullable;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.util.StringUtils;
Expand All @@ -12,6 +13,7 @@
import ttakkeun.ttakkeun_server.apiPayLoad.exception.ExceptionHandler;
import ttakkeun.ttakkeun_server.apiPayLoad.exception.OAuthHandler;
import ttakkeun.ttakkeun_server.dto.auth.LoginResponseDto;
import ttakkeun.ttakkeun_server.dto.auth.WithdrawalRequestDto;
import ttakkeun.ttakkeun_server.dto.auth.apple.AppleLoginRequestDto;
import ttakkeun.ttakkeun_server.dto.auth.apple.AppleSignUpRequestDto;
import ttakkeun.ttakkeun_server.dto.auth.kakao.KakaoLoginRequestDTO;
Expand Down Expand Up @@ -68,8 +70,9 @@ public ApiResponse<LoginResponseDto> appleSignUp(@RequestBody @Validated AppleSi
@Operation(summary = "애플 탈퇴 API")
@DeleteMapping("/delete/apple")
public ApiResponse<String> appleWithdraw(@AuthenticationPrincipal Member member,
@Nullable @RequestHeader("authorization-code") final String code){
oAuthService.appleDelete(member, code);
@Nullable @RequestHeader("authorization-code") final String code,
@Valid @RequestBody WithdrawalRequestDto withdrawalRequestDto){
oAuthService.appleDelete(member, code, withdrawalRequestDto);

return ApiResponse.onSuccess("apple delete success");
}
Expand All @@ -92,8 +95,9 @@ public ApiResponse<LoginResponseDto> kakaoSignUp(@RequestBody @Validated KakaoSi

@Operation(summary = "카카오 탈퇴 API")
@DeleteMapping("/delete/kakao")
public ApiResponse<String> kakaoWithdraw(@AuthenticationPrincipal Member member){
oAuthService.kakaoDelete(member);
public ApiResponse<String> kakaoWithdraw(@AuthenticationPrincipal Member member,
@Valid @RequestBody WithdrawalRequestDto withdrawalDto){
oAuthService.kakaoDelete(member, withdrawalDto);
return ApiResponse.onSuccess("kakao delete success");
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package ttakkeun.ttakkeun_server.dto.auth;

import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import ttakkeun.ttakkeun_server.entity.enums.WithdrawalReasonType;

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class WithdrawalRequestDto {

@NotNull(message = "탈퇴 사유는 필수입니다")
private WithdrawalReasonType reasonType;

@Size(max = 500, message = "기타 사유는 500자 이내로 입력해주세요")
private String customReason;

// 기타 사유 유효성 검증
public boolean isValid() {
if (reasonType == WithdrawalReasonType.OTHER) {
return customReason != null && !customReason.trim().isEmpty();
}
return true;
}
}
43 changes: 43 additions & 0 deletions src/main/java/ttakkeun/ttakkeun_server/entity/Withdrawal.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package ttakkeun.ttakkeun_server.entity;

import jakarta.persistence.*;
import lombok.*;
import ttakkeun.ttakkeun_server.entity.enums.WithdrawalReasonType;

@Entity
@Getter
@Builder
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class Withdrawal {

@Id
@Column(name = "withdrawal_id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long withdrawalId;

@Column(name = "user_id", nullable = false)
private Long userId; // 탈퇴한 사용자 ID

@Enumerated(EnumType.STRING)
@Column(name = "reason_type", nullable = false)
private WithdrawalReasonType reasonType;

@Column(name = "custom_reason", length = 500)
private String customReason; // reasonType이 OTHER일 때만 사용

// 기타 사유인지 확인하는 헬퍼 메소드
public boolean isOtherReason() {
return reasonType == WithdrawalReasonType.OTHER;
}

// 표시용 사유 텍스트 반환
public String getDisplayReason() {
if (isOtherReason() && customReason != null && !customReason.trim().isEmpty()) {
return customReason;
}
return reasonType.getDescription();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package ttakkeun.ttakkeun_server.entity.enums;

public enum WithdrawalReasonType {
FEATURE_DISSATISFACTION("기능 불만족"),
LACK_OF_CONTENT("앱 내 콘텐츠 부족"),
TECHNICAL_ISSUES("기술적 문제 (버그, 오류)"),
COMPATIBILITY_ISSUES("기기 호환성 문제"),
UX_INCONVENIENCE("사용자 경험 불편"),
OTHER("기타 사유");

private final String description;

WithdrawalReasonType(String description) {
this.description = description;
}

public String getDescription() {
return description;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package ttakkeun.ttakkeun_server.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import ttakkeun.ttakkeun_server.entity.Withdrawal;

public interface WithdrawalRepository extends JpaRepository<Withdrawal, Long> {
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import ttakkeun.ttakkeun_server.client.DiscordMessageProvider;
import ttakkeun.ttakkeun_server.converter.MemberConverter;
import ttakkeun.ttakkeun_server.dto.auth.LoginResponseDto;
import ttakkeun.ttakkeun_server.dto.auth.WithdrawalRequestDto;
import ttakkeun.ttakkeun_server.dto.auth.apple.AppleAuthClient;
import ttakkeun.ttakkeun_server.dto.auth.apple.AppleLoginRequestDto;
import ttakkeun.ttakkeun_server.dto.auth.apple.AppleRevokeRequest;
Expand All @@ -21,9 +22,11 @@
import ttakkeun.ttakkeun_server.dto.auth.kakao.KakaoSignUpRequestDTO;
import ttakkeun.ttakkeun_server.dto.auth.kakao.KakaoUserDTO;
import ttakkeun.ttakkeun_server.entity.Member;
import ttakkeun.ttakkeun_server.entity.Withdrawal;
import ttakkeun.ttakkeun_server.entity.enums.EventMessage;
import ttakkeun.ttakkeun_server.entity.enums.LoginType;
import ttakkeun.ttakkeun_server.repository.MemberRepository;
import ttakkeun.ttakkeun_server.repository.WithdrawalRepository;
import ttakkeun.ttakkeun_server.service.MemberService;
import ttakkeun.ttakkeun_server.utils.AppleClientSecretGenerator;
import ttakkeun.ttakkeun_server.utils.AppleOAuthProvider;
Expand All @@ -50,6 +53,7 @@ public class OAuthService {
private final KakaoService kakaoService;
private final DiscordMessageProvider discordMessageProvider;
private final KakaoUnlinkClient kakaoUnlinkClient;
private final WithdrawalRepository withdrawalRepository;

@Value("${spring.social-login.provider.apple.client-id}")
private String clientId;
Expand Down Expand Up @@ -164,7 +168,7 @@ public LoginResponseDto appleSignUp(AppleSignUpRequestDto appleSignUpRequestDto)
}

@Transactional
public void appleDelete(Member member, String code) {
public void appleDelete(Member member, String code, WithdrawalRequestDto withdrawalDto) {
try {
String clientSecret = appleClientSecretGenerator.createClientSecret();
String refreshToken = appleOAuthProvider.getAppleRefreshToken(code, clientSecret);
Expand All @@ -184,6 +188,9 @@ public void appleDelete(Member member, String code) {
log.info("애플 탈퇴 성공");
log.info("member id :: " + member.getMemberId());

// 탈퇴 사유 저장
saveWithdrawalReason(member.getMemberId(), withdrawalDto);

memberService.deleteMember(member);
}

Expand Down Expand Up @@ -235,8 +242,12 @@ public LoginResponseDto kakaoSignUp(KakaoSignUpRequestDTO kakaoSignUpReqDto) {
return createToken(signUpMember);
}

public void kakaoDelete(Member member) {
public void kakaoDelete(Member member, WithdrawalRequestDto withdrawalDto) {
kakaoUnlinkClient.unlinkUser("KakaoAK " + kakaoAdminKey, "user_id", member.getKakaoUserId());

// 탈퇴 사유 저장
saveWithdrawalReason(member.getMemberId(), withdrawalDto);

memberService.deleteMember(member);
}

Expand All @@ -251,4 +262,20 @@ public void logout(String refreshToken) {
member.refreshTokenExpires();
memberRepository.save(member);
}

// 탈퇴 사유 저장
private void saveWithdrawalReason(Long memberId, WithdrawalRequestDto withdrawalDto) {
// 유효성 검증
if (!withdrawalDto.isValid()) {
throw new ExceptionHandler(WITHDRAWAL_REASON_EMPTY);
}

Withdrawal withdrawal = Withdrawal.builder()
.userId(memberId)
.reasonType(withdrawalDto.getReasonType())
.customReason(withdrawalDto.getCustomReason())
.build();

withdrawalRepository.save(withdrawal);
}
}