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 @@ -4,10 +4,13 @@
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.runimo.runimo.user.controller.request.WithdrawRequest;
import org.runimo.runimo.user.service.WithdrawService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

Expand All @@ -26,9 +29,10 @@ public class UserWithdrawController {
})
@DeleteMapping()
public ResponseEntity<Void> deleteUser(
@UserId Long userId
@UserId Long userId,
@Valid @RequestBody WithdrawRequest request
) {
withdrawService.withdraw(userId);
withdrawService.withdraw(WithdrawRequest.toCommand(userId, request));
return ResponseEntity.noContent().build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package org.runimo.runimo.user.controller.request;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.validator.constraints.Length;
import org.runimo.runimo.user.domain.WithdrawalReason;
import org.runimo.runimo.user.service.dto.command.WithdrawCommand;

@Schema(description = "회원 탈퇴 DTO")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class WithdrawRequest {

@Schema(description = "탈퇴 이유", example = "FOUND_BETTER_SERVICE")
private String withdrawReason;
@Schema(description = "기타 항목에 적는 문자열", example = "기타 항목에 적는 문자열")
@Length(max = 255, message = "기타 항목에 적는 문자열은 255자 이하로 입력해주세요.")
private String reasonDetail;

public static WithdrawCommand toCommand(Long id, WithdrawRequest request) {
return new WithdrawCommand(
id,
WithdrawalReason.valueOf(request.getWithdrawReason()),
request.getReasonDetail()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package org.runimo.runimo.user.domain;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.NoArgsConstructor;

@Table(name = "user_withdrawal_reason")
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class UserWithdrawReason {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(name = "user_id", nullable = false)
private Long userId;

@Column(name = "reason", nullable = false)
@Enumerated(EnumType.STRING)
private WithdrawalReason reason;

@Column(name = "custom_reason")
private String customReason;

@Builder
public UserWithdrawReason(Long id, Long userId, WithdrawalReason reason, String customReason) {
this.id = id;
this.userId = userId;
this.reason = reason;
this.customReason = customReason;
validateReason();
}

private void validateReason() {
if (reason != WithdrawalReason.OTHER && (customReason != null && !customReason.isEmpty())) {
throw new IllegalArgumentException("customReason는 OTHER 타입일 때만 필요합니다.");
}
}

}
18 changes: 18 additions & 0 deletions src/main/java/org/runimo/runimo/user/domain/WithdrawalReason.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.runimo.runimo.user.domain;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum WithdrawalReason {
NOT_ENOUGH_CHARACTERS("수집할 캐릭터가 부족해요"),
NO_LONGER_NEEDED("러닝 목표에 도움이 되지 않아요"),
RECORD_NOT_EXPECTED("기록 기능이 기대와 달라요"),
COMPLEX_USAGE("앱 사용이 복잡해요"),
USING_OTHER_SERVICE("다른 러닝 앱을 사용하고 있어요"),
PRIVACY_CONCERN("개인정보가 걱정돼요"),
OTHER("기타 (직접 입력)");

private final String description;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.runimo.runimo.user.repository;

import org.runimo.runimo.user.domain.UserWithdrawReason;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserWithdrawReasonRepository extends JpaRepository<UserWithdrawReason, Long> {

}
18 changes: 16 additions & 2 deletions src/main/java/org/runimo/runimo/user/service/WithdrawService.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,12 @@
import org.runimo.runimo.user.domain.OAuthInfo;
import org.runimo.runimo.user.domain.SocialProvider;
import org.runimo.runimo.user.domain.User;
import org.runimo.runimo.user.domain.UserWithdrawReason;
import org.runimo.runimo.user.repository.AppleUserTokenRepository;
import org.runimo.runimo.user.repository.OAuthInfoRepository;
import org.runimo.runimo.user.repository.UserRepository;
import org.runimo.runimo.user.repository.UserWithdrawReasonRepository;
import org.runimo.runimo.user.service.dto.command.WithdrawCommand;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -25,15 +28,18 @@ public class WithdrawService {
private final AppleUserTokenRepository appleUserTokenRepository;
private final EncryptUtil encryptUtil;
private final TokenRefreshService tokenRefreshService;
private final UserWithdrawReasonRepository userWithdrawReasonRepository;

@Transactional
public void withdraw(Long userId) {
OAuthInfo oAuthInfo = oAuthInfoRepository.findByUserId(userId)
public void withdraw(WithdrawCommand command) {
OAuthInfo oAuthInfo = oAuthInfoRepository.findByUserId(command.userId())
.orElseThrow(NoSuchElementException::new);
User user = oAuthInfo.getUser();
if (oAuthInfo.getProvider() == SocialProvider.APPLE) {
withdrawAppleUser(user);
}
UserWithdrawReason reason = createUserWithdrawReason(command);
userWithdrawReasonRepository.save(reason);
oAuthInfoRepository.delete(oAuthInfo);
userRepository.delete(user);
tokenRefreshService.removeRefreshToken(user.getId());
Expand All @@ -55,4 +61,12 @@ private String getDecryptedToken(String encryptedRefreshToken) {
throw new RuntimeException("Failed to decrypt ID token", e);
}
}

private UserWithdrawReason createUserWithdrawReason(WithdrawCommand command) {
return UserWithdrawReason.builder()
.userId(command.userId())
.reason(command.reason())
.customReason(command.reasonDetail())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.runimo.runimo.user.service.dto.command;

import org.runimo.runimo.user.domain.WithdrawalReason;

public record WithdrawCommand(
Long userId,
WithdrawalReason reason,
String reasonDetail
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
create table user_withdrawal_reason
(
`id` BIGINT NOT NULL PRIMARY KEY AUTO_INCREMENT,
`user_id` BIGINT NOT NULL,
`reason` VARCHAR(32) NOT NULL,
`custom_reason` VARCHAR(255)
);
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@
import static io.restassured.RestAssured.given;
import static org.runimo.runimo.TestConsts.TEST_USER_UUID;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.runimo.runimo.CleanUpUtil;
import org.runimo.runimo.TokenUtils;
import org.runimo.runimo.user.controller.request.WithdrawRequest;
import org.runimo.runimo.user.domain.WithdrawalReason;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
Expand All @@ -27,6 +31,9 @@ class UserWithdrawAcceptanceTest {
@Autowired
private CleanUpUtil cleanUpUtil;

@Autowired
private ObjectMapper objectMapper;


@Autowired
private TokenUtils tokenUtils;
Expand All @@ -46,7 +53,12 @@ void tearDown() {

@Test
@Sql(scripts = "/sql/user_mypage_test_data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
void 회원_탈퇴_성공_시_유저_조회_불가() {
void 회원_탈퇴_성공_시_유저_조회_불가() throws JsonProcessingException {

WithdrawRequest request = new WithdrawRequest(
WithdrawalReason.NO_LONGER_NEEDED.name(),
""
);

given()
.header("Authorization", token)
Expand All @@ -58,10 +70,9 @@ void tearDown() {
given()
.header("Authorization", token)
.contentType(ContentType.JSON)

.body(objectMapper.writeValueAsString(request))
.when()
.delete("/api/v1/users")

.then()
.log().all()
.statusCode(204);
Expand All @@ -75,4 +86,43 @@ void tearDown() {

}

@Test
@Sql(scripts = "/sql/user_mypage_test_data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
void 탈퇴_사유가_기타가_아닌데_상세_사유가_있으면_에러() throws JsonProcessingException {

WithdrawRequest request = new WithdrawRequest(
"LACK_OF_IMPROVEMENT",
"예시 탈퇴 사유"
);

given()
.header("Authorization", token)
.contentType(ContentType.JSON)
.body(objectMapper.writeValueAsString(request))
.when()
.delete("/api/v1/users")
.then()
.log().all()
.statusCode(400);
}


@Test
@Sql(scripts = "/sql/user_mypage_test_data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
void 탈퇴_사유가_255자_이상이면_에러() throws JsonProcessingException {
WithdrawRequest request = new WithdrawRequest(
"OTHER",
"a".repeat(512)
);

given()
.header("Authorization", token)
.contentType(ContentType.JSON)
.body(objectMapper.writeValueAsString(request))
.when()
.delete("/api/v1/users")
.then()
.log().all()
.statusCode(400);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
create table user_withdrawal_reason
(
`id` BIGINT NOT NULL PRIMARY KEY AUTO_INCREMENT,
`user_id` BIGINT NOT NULL,
`reason` VARCHAR(32) NOT NULL,
`custom_reason` VARCHAR(255)
);