Skip to content

Commit 80300e9

Browse files
authored
feat: 카카오 회원탈퇴 기능 구현 완료 (#78)
* [BOOK-180] refactor: apis - 회원 탈퇴 요청에서 WithdrawRequest DTO 제거 및 메서드 시그니처 수정 * [BOOK-180] refactor: apis - WithdrawRequest DTO 제거로 인한 코드 정리 * [BOOK-180] feat: apis - 카카오 OAuth 속성 추가 및 AuthConfig 수정 * [BOOK-180] feat: apis - WithdrawStrategyRequest DTO에 유효성 검사 어노테이션 추가 * [BOOK-180] feat: apis, infra - 카카오 회원 탈퇴 기능 구현 및 예외 처리 추가 * [BOOK-180] feat: apis - 카카오 회원탈퇴 실패 및 응답 불일치에 대한 예외 코드 추가 * [BOOK-180] feat: apis - AppleWithdrawStrategy에서 WithdrawStrategyRequest에 유효성 검사 어노테이션 추가 * [BOOK-180] refactor: infra - 상수화 및 코드 정리, unlink 메서드의 요청 본문 구성 개선 * [BOOK-180] fix: apis - 테스트용 카카오 admin-key 추가 * [BOOK-180] refactor: apis - KakaoApiManager에서 adminKey를 KakaoOauthProperties로부터 가져오도록 수정 * [BOOK-180] refactor: infra - withdraw 메서드의 requestBody 인자를 MultiValueMap을 사용해 자동 인코딩 방식으로 변경 * [BOOK-180] refactor: apis - KakaoApiManager의 unlink 메서드에서 응답 검증 로직을 별도의 메서드로 분리
1 parent 6a15018 commit 80300e9

File tree

15 files changed

+131
-68
lines changed

15 files changed

+131
-68
lines changed

apis/src/main/kotlin/org/yapp/apis/auth/controller/AuthController.kt

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal
66
import org.springframework.web.bind.annotation.*
77
import org.yapp.apis.auth.dto.request.SocialLoginRequest
88
import org.yapp.apis.auth.dto.request.TokenRefreshRequest
9-
import org.yapp.apis.auth.dto.request.WithdrawRequest
109
import org.yapp.apis.auth.dto.response.AuthResponse
1110
import org.yapp.apis.auth.usecase.AuthUseCase
1211
import java.util.*
@@ -37,10 +36,9 @@ class AuthController(
3736

3837
@DeleteMapping("/withdraw")
3938
override fun withdraw(
40-
@AuthenticationPrincipal userId: UUID,
41-
@Valid @RequestBody request: WithdrawRequest
39+
@AuthenticationPrincipal userId: UUID
4240
): ResponseEntity<Unit> {
43-
authUseCase.withdraw(userId, request)
41+
authUseCase.withdraw(userId)
4442
return ResponseEntity.noContent().build()
4543
}
4644
}

apis/src/main/kotlin/org/yapp/apis/auth/controller/AuthControllerApi.kt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import org.springframework.web.bind.annotation.PostMapping
1414
import org.springframework.web.bind.annotation.RequestBody
1515
import org.yapp.apis.auth.dto.request.SocialLoginRequest
1616
import org.yapp.apis.auth.dto.request.TokenRefreshRequest
17-
import org.yapp.apis.auth.dto.request.WithdrawRequest
1817
import org.yapp.apis.auth.dto.response.AuthResponse
1918
import org.yapp.globalutils.exception.ErrorResponse
2019
import java.util.*
@@ -109,7 +108,6 @@ interface AuthControllerApi {
109108
)
110109
@DeleteMapping("/withdraw")
111110
fun withdraw(
112-
@AuthenticationPrincipal userId: UUID,
113-
@Valid @RequestBody request: WithdrawRequest
111+
@AuthenticationPrincipal userId: UUID
114112
): ResponseEntity<Unit>
115113
}

apis/src/main/kotlin/org/yapp/apis/auth/dto/request/WithdrawRequest.kt

Lines changed: 0 additions & 22 deletions
This file was deleted.

apis/src/main/kotlin/org/yapp/apis/auth/dto/request/WithdrawStrategyRequest.kt

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,32 @@
11
package org.yapp.apis.auth.dto.request
22

33
import io.swagger.v3.oas.annotations.media.Schema
4+
import jakarta.validation.constraints.NotBlank
5+
import jakarta.validation.constraints.NotNull
46
import org.yapp.apis.user.dto.response.WithdrawTargetUserResponse
57
import org.yapp.domain.user.ProviderType
68
import java.util.*
79

810
@Schema(description = "회원 탈퇴 처리 시 내부적으로 사용되는 요청 DTO")
911
data class WithdrawStrategyRequest private constructor(
12+
@field:NotNull(message = "사용자 ID는 필수 값입니다.")
1013
@Schema(
1114
description = "사용자 고유 ID",
12-
example = "123e4567-e89b-12d3-a456-426614174000",
15+
example = "123e4567-e89b-12d3-a456-426614174000"
1316
)
1417
val userId: UUID,
1518

19+
@field:NotNull(message = "소셜 로그인 제공자 타입은 필수 값입니다.")
1620
@Schema(
1721
description = "소셜 로그인 제공자 타입",
18-
example = "KAKAO",
22+
example = "KAKAO"
1923
)
2024
val providerType: ProviderType,
2125

26+
@field:NotBlank(message = "소셜 로그인 제공자로부터 발급받은 고유 ID는 필수 값입니다.")
2227
@Schema(
2328
description = "소셜 로그인 제공자로부터 발급받은 고유 ID",
24-
example = "21412412412",
29+
example = "21412412412"
2530
)
2631
val providerId: String,
2732

apis/src/main/kotlin/org/yapp/apis/auth/exception/AuthErrorCode.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ enum class AuthErrorCode(
2222
INVALID_APPLE_ID_TOKEN(HttpStatus.BAD_REQUEST, "AUTH_400_10", "유효하지 않은 Apple ID 토큰입니다."),
2323
PROVIDER_TYPE_MISMATCH(HttpStatus.BAD_REQUEST, "AUTH_400_11", "요청된 공급자 타입이 실제 사용자의 공급자 타입과 일치하지 않습니다."),
2424
APPLE_REFRESH_TOKEN_MISSING(HttpStatus.BAD_REQUEST, "AUTH_400_12", "Apple 사용자 탈퇴 시 리프레시 토큰이 누락되었습니다."),
25+
KAKAO_UNLINK_FAILED(HttpStatus.BAD_REQUEST, "AUTH_400_15", "카카오 회원탈퇴 처리에 실패했습니다."),
2526

2627
/* 401 UNAUTHORIZED */
2728
INVALID_OAUTH_TOKEN(HttpStatus.UNAUTHORIZED, "AUTH_401_01", "잘못된 소셜 OAuth 토큰입니다."),
@@ -49,7 +50,8 @@ enum class AuthErrorCode(
4950
HttpStatus.INTERNAL_SERVER_ERROR,
5051
"AUTH_500_06",
5152
"Apple에서 초기 로그인 시 리프레시 토큰을 제공하지 않았습니다."
52-
);
53+
),
54+
KAKAO_UNLINK_RESPONSE_MISMATCH(HttpStatus.INTERNAL_SERVER_ERROR, "AUTH_500_08", "카카오 회원탈퇴 응답이 요청과 일치하지 않습니다.");
5355

5456
override fun getHttpStatus(): HttpStatus = httpStatus
5557
override fun getCode(): String = code

apis/src/main/kotlin/org/yapp/apis/auth/manager/KakaoApiManager.kt

Lines changed: 45 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,17 @@ package org.yapp.apis.auth.manager
33
import mu.KotlinLogging
44
import org.springframework.stereotype.Component
55
import org.springframework.web.client.HttpClientErrorException
6-
import org.springframework.web.client.HttpServerErrorException
76
import org.yapp.apis.auth.exception.AuthErrorCode
87
import org.yapp.apis.auth.exception.AuthException
8+
import org.yapp.apis.config.KakaoOauthProperties
99
import org.yapp.infra.external.oauth.kakao.KakaoApi
10+
import org.yapp.infra.external.oauth.kakao.response.KakaoUnlinkResponse
1011
import org.yapp.infra.external.oauth.kakao.response.KakaoUserInfo
1112

1213
@Component
1314
class KakaoApiManager(
14-
private val kakaoApi: KakaoApi
15+
private val kakaoApi: KakaoApi,
16+
private val kakaoOauthProperties: KakaoOauthProperties
1517
) {
1618
private val log = KotlinLogging.logger {}
1719

@@ -24,22 +26,48 @@ class KakaoApiManager(
2426
log.error("Failed to fetch Kakao user info", exception)
2527

2628
when (exception) {
27-
is HttpClientErrorException -> {
28-
throw AuthException(
29-
AuthErrorCode.INVALID_OAUTH_TOKEN,
30-
"Invalid Kakao OAuth token."
31-
)
32-
}
33-
34-
is HttpServerErrorException, is Exception -> {
35-
throw AuthException(
36-
AuthErrorCode.OAUTH_SERVER_ERROR,
37-
"Failed to communicate with Kakao server."
38-
)
39-
}
40-
41-
else -> throw exception
29+
is HttpClientErrorException -> throw AuthException(
30+
AuthErrorCode.INVALID_OAUTH_TOKEN,
31+
"Invalid Kakao Access Token."
32+
)
33+
34+
else -> throw AuthException(
35+
AuthErrorCode.OAUTH_SERVER_ERROR,
36+
"Failed to communicate with Kakao server."
37+
)
38+
}
39+
}
40+
}
41+
42+
fun unlink(targetId: String): KakaoUnlinkResponse {
43+
return kakaoApi.unlink(kakaoOauthProperties.adminKey, targetId)
44+
.onSuccess { response ->
45+
log.info("Successfully unlinked Kakao user with targetId: $targetId, responseId: ${response.id}")
46+
validateUnlinkResponse(response, targetId)
47+
}
48+
.getOrElse { exception ->
49+
log.error("Failed to unlink Kakao user with targetId: $targetId", exception)
50+
51+
when (exception) {
52+
is HttpClientErrorException -> throw AuthException(
53+
AuthErrorCode.KAKAO_UNLINK_FAILED,
54+
"Failed to unlink Kakao user due to client error: ${exception.message}"
55+
)
56+
57+
else -> throw AuthException(
58+
AuthErrorCode.OAUTH_SERVER_ERROR,
59+
"Failed to unlink Kakao user due to server error: ${exception.message}"
60+
)
4261
}
4362
}
4463
}
64+
65+
private fun validateUnlinkResponse(response: KakaoUnlinkResponse, expectedTargetId: String) {
66+
if (response.id != expectedTargetId) {
67+
throw AuthException(
68+
AuthErrorCode.KAKAO_UNLINK_RESPONSE_MISMATCH,
69+
"Kakao unlink response ID does not match target ID"
70+
)
71+
}
72+
}
4573
}

apis/src/main/kotlin/org/yapp/apis/auth/strategy/withdraw/AppleWithdrawStrategy.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
11
package org.yapp.apis.auth.strategy.withdraw
22

3+
import jakarta.validation.Valid
34
import org.springframework.stereotype.Component
5+
import org.springframework.validation.annotation.Validated
46
import org.yapp.apis.auth.dto.request.WithdrawStrategyRequest
57
import org.yapp.apis.auth.exception.AuthErrorCode
68
import org.yapp.apis.auth.exception.AuthException
79
import org.yapp.apis.auth.manager.AppleApiManager
810
import org.yapp.domain.user.ProviderType
911

1012
@Component
13+
@Validated
1114
class AppleWithdrawStrategy(
1215
private val appleApiManager: AppleApiManager
1316
) : WithdrawStrategy {
1417
override fun getProviderType(): ProviderType = ProviderType.APPLE
1518

16-
override fun withdraw(request: WithdrawStrategyRequest) {
19+
override fun withdraw(@Valid request: WithdrawStrategyRequest) {
1720
val appleRefreshToken = request.appleRefreshToken
1821
?: throw AuthException(AuthErrorCode.APPLE_REFRESH_TOKEN_MISSING)
1922

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,27 @@
11
package org.yapp.apis.auth.strategy.withdraw
22

3+
import jakarta.validation.Valid
4+
import mu.KotlinLogging
35
import org.springframework.stereotype.Component
6+
import org.springframework.validation.annotation.Validated
47
import org.yapp.apis.auth.dto.request.WithdrawStrategyRequest
58
import org.yapp.apis.auth.manager.KakaoApiManager
69
import org.yapp.domain.user.ProviderType
710

811
@Component
12+
@Validated
913
class KakaoWithdrawStrategy(
1014
private val kakaoApiManager: KakaoApiManager
1115
) : WithdrawStrategy {
16+
private val log = KotlinLogging.logger {}
17+
1218
override fun getProviderType() = ProviderType.KAKAO
1319

14-
override fun withdraw(request: WithdrawStrategyRequest) {
15-
// kakaoApiManager.unlink(request.providerId)
20+
override fun withdraw(@Valid request: WithdrawStrategyRequest) {
21+
log.info("Starting Kakao withdrawal for user: ${request.userId}, providerId: ${request.providerId}")
22+
23+
val unlinkResponse = kakaoApiManager.unlink(request.providerId)
24+
25+
log.info("Successfully unlinked Kakao user. Response ID: ${unlinkResponse.id}")
1626
}
1727
}

apis/src/main/kotlin/org/yapp/apis/auth/usecase/AuthUseCase.kt

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ package org.yapp.apis.auth.usecase
33
import org.springframework.transaction.annotation.Transactional
44
import org.yapp.apis.auth.dto.request.*
55
import org.yapp.apis.auth.dto.response.TokenPairResponse
6-
import org.yapp.apis.auth.exception.AuthErrorCode
7-
import org.yapp.apis.auth.exception.AuthException
86
import org.yapp.apis.auth.service.*
97
import org.yapp.apis.auth.strategy.signin.AppleAuthCredentials
108
import org.yapp.apis.auth.strategy.signin.SignInCredentials
@@ -63,17 +61,9 @@ class AuthUseCase(
6361
authTokenService.deleteRefreshTokenForSignOutOrWithdraw(DeleteTokenRequest.from(refreshTokenResponse))
6462
}
6563

66-
fun withdraw(userId: UUID, withdrawRequest: WithdrawRequest) {
64+
fun withdraw(userId: UUID) {
6765
val withdrawTargetUserResponse = userAccountService.findWithdrawUserById(userId)
68-
69-
if (withdrawTargetUserResponse.providerType != withdrawRequest.validProviderType()) {
70-
throw AuthException(
71-
AuthErrorCode.PROVIDER_TYPE_MISMATCH,
72-
"The provider type in the request does not match the user's actual provider type."
73-
)
74-
}
75-
76-
val strategy = withdrawStrategyResolver.resolve(withdrawRequest.validProviderType())
66+
val strategy = withdrawStrategyResolver.resolve(withdrawTargetUserResponse.providerType)
7767
strategy.withdraw(WithdrawStrategyRequest.from(withdrawTargetUserResponse))
7868

7969
userWithdrawalService.processWithdrawal(userId)

apis/src/main/kotlin/org/yapp/apis/config/AuthConfig.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
44
import org.springframework.context.annotation.Configuration
55

66
@Configuration
7-
@EnableConfigurationProperties(AppleOauthProperties::class)
7+
@EnableConfigurationProperties(AppleOauthProperties::class, KakaoOauthProperties::class)
88
class AuthConfig

0 commit comments

Comments
 (0)