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
3 changes: 2 additions & 1 deletion Projects/App/Sources/AppContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,8 @@ final class AppContainer:
authRepository: authRepository,
loginRepository: logInRepository,
imageUploader: imageUploader,
myPageRepository: myPageRepository
myPageRepository: myPageRepository,
oauthRepository: oauthRepository
)
}()

Expand Down
9 changes: 9 additions & 0 deletions Projects/Cores/Core/Utils/ServiceConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,15 @@ public final class ServiceConfiguration {
public func setUserName(_ name: String) {
UserDefaults.standard.set(name, forKey: "userName")
}

/// 사용자의 `authProvider`를 리턴합니다.
public var authProvider: String {
return UserDefaults.standard.string(forKey: "authProvider") ?? "normal"
}

public func setAuthProvider(_ provider: String) {
UserDefaults.standard.set(provider, forKey: "authProvider")
}

// 개인정보 처리방침
public var privacyUrl: URL {
Expand Down
4 changes: 3 additions & 1 deletion Projects/Data/DTO/MyPage/UserProfileResponseDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public struct UserProfileResponseDTO: Decodable {
public let imageUrl: String?
public let username: String
public let email: String
public let provider: String?
}

public extension UserProfileResponseDTO {
Expand All @@ -20,7 +21,8 @@ public extension UserProfileResponseDTO {
"data": {
"imageUrl": "https://url.kr/5MhHhD",
"username": "photi",
"email": "photi@photi.com"
"email": "photi@photi.com",
"provider": null
}
}
"""
Expand Down
17 changes: 17 additions & 0 deletions Projects/Data/DTO/OAuth/OAuthWithdrawRequestDTO.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//
// OAuthWithdrawRequestDTO.swift
// Data
//
// Created by Claude on 5/10/26.
// Copyright © 2026 com.photi. All rights reserved.
//

public struct OAuthWithdrawRequestDTO: Encodable {
public let provider: String
public let sub: String

public init(provider: String, sub: String) {
self.provider = provider
self.sub = sub
}
}
8 changes: 7 additions & 1 deletion Projects/Data/DataMapper/MyPageDataMapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@ public extension MyPageDataMapperImpl {
return .init(
imageUrl: imageURL(from: dto.imageUrl),
name: dto.username,
email: dto.email
email: dto.email,
provider: authProvider(from: dto.provider)
)
}

Expand All @@ -108,4 +109,9 @@ private extension MyPageDataMapperImpl {
guard let strURL else { return nil }
return URL(string: strURL)
}

func authProvider(from provider: String?) -> AuthProvider {
guard let provider else { return .normal }
return AuthProvider(rawValue: provider) ?? .normal
}
}
3 changes: 2 additions & 1 deletion Projects/Data/Project.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ let project = Project.make(
.Project.Data.DTO,
.Project.Data.DataMapper,
.Project.Data.PhotiNetwork,
.Project.Domain.Repository
.Project.Domain.Repository,
.SPM.KakaoSDKUser
]
),
.make(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ import PhotiNetwork
public enum OAuthAPI {
case login(provider: String, idToken: String)
case setUsername(dto: OAuthUsernameRequestDTO)
/// 카카오/구글 탈퇴 - 클라이언트에서 unlink 후 호출
case withdrawKakaoGoogle(dto: OAuthWithdrawRequestDTO)
/// 애플 탈퇴 - 서버에서 revoke 처리
case withdrawApple(accessToken: String)
}

extension OAuthAPI: TargetType {
Expand All @@ -27,6 +31,10 @@ extension OAuthAPI: TargetType {
return "oauth/\(provider)/login"
case .setUsername:
return "oauth/username"
case .withdrawKakaoGoogle:
return "oauth/withdraw"
case .withdrawApple:
return "oauth/apple/withdraw"
}
}

Expand All @@ -36,6 +44,8 @@ extension OAuthAPI: TargetType {
return .get
case .setUsername:
return .post
case .withdrawKakaoGoogle, .withdrawApple:
return .patch
}
}

Expand All @@ -47,6 +57,13 @@ extension OAuthAPI: TargetType {

case let .setUsername(dto):
return .requestJSONEncodable(dto)

case let .withdrawKakaoGoogle(dto):
return .requestJSONEncodable(dto)

case let .withdrawApple(accessToken):
let parameters = ["access_token": accessToken]
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
}
}

Expand Down Expand Up @@ -75,6 +92,19 @@ extension OAuthAPI: TargetType {
"""
let jsonData = data.data(using: .utf8)
return .networkResponse(200, jsonData ?? Data(), "", "")

case .withdrawKakaoGoogle, .withdrawApple:
let data = """
{
"code": "200 OK",
"message": "성공",
"data": {
"successMessage": "string"
}
}
"""
let jsonData = data.data(using: .utf8)
return .networkResponse(200, jsonData ?? Data(), "", "")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import DTO
import Entity
import Repository
import PhotiNetwork
import KakaoSDKUser

public struct OAuthRepositoryImpl: OAuthRepository {
public init() { }
Expand All @@ -33,6 +34,71 @@ public extension OAuthRepositoryImpl {
responseType: SuccessResponseDTO.self
)
}

func withdrawKakao() async throws {
let sub = try await fetchKakaoUserId()
try await unlinkKakao()
try await requestWithdraw(provider: "KAKAO", sub: sub)
}

func withdrawGoogle() async throws {
// TODO: 구글 SDK 연동 후 구현
// 1. GIDSignIn.sharedInstance.currentUser?.userID로 sub 획득
// 2. GIDSignIn.sharedInstance.disconnect()로 연결 해제
// 3. requestWithdraw(provider: "GOOGLE", sub: sub) 호출
throw APIError.serverError
}

func withdrawApple() async throws {
// TODO: 애플 재인증 후 authorization code 획득하여 서버 전달
// ASAuthorizationAppleIDProvider를 사용하여 재인증
throw APIError.serverError
}
}

// MARK: - Kakao SDK Methods
private extension OAuthRepositoryImpl {
func fetchKakaoUserId() async throws -> String {
try await withCheckedThrowingContinuation { continuation in
UserApi.shared.me { user, error in
if let error = error {
continuation.resume(throwing: error)
return
}

guard let userId = user?.id else {
continuation.resume(throwing: APIError.serverError)
return
}

continuation.resume(returning: String(userId))
}
}
}

func unlinkKakao() async throws {
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
UserApi.shared.unlink { error in
if let error = error {
continuation.resume(throwing: error)
return
}
continuation.resume()
}
}
}
}

// MARK: - Server API Methods
private extension OAuthRepositoryImpl {
func requestWithdraw(provider: String, sub: String) async throws {
let requestDTO = OAuthWithdrawRequestDTO(provider: provider, sub: sub)

try await requestAuthorizableAPI(
api: OAuthAPI.withdrawKakaoGoogle(dto: requestDTO),
responseType: SuccessResponseDTO.self
)
}
}

// MARK: - Private Methods
Expand Down
19 changes: 17 additions & 2 deletions Projects/Domain/Entity/UserProfile.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,33 @@

import Foundation

public enum AuthProvider: String {
case normal
case kakao = "KAKAO"
case google = "GOOGLE"
case apple = "APPLE"
}

public enum WithdrawCredential {
case password(String)
case oauth(provider: AuthProvider)
}

public struct UserProfile {
public let imageUrl: URL?
public let name: String
public let email: String

public let provider: AuthProvider

public init(
imageUrl: URL?,
name: String,
email: String
email: String,
provider: AuthProvider
) {
self.imageUrl = imageUrl
self.name = name
self.email = email
self.provider = provider
}
}
9 changes: 9 additions & 0 deletions Projects/Domain/Repository/Interfaces/OAuthRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,13 @@ public protocol OAuthRepository {

/// OAuth 신규 사용자 username 설정
func setUsername(_ username: String) async throws

/// 카카오 회원 탈퇴 - SDK unlink + 서버 API 호출
func withdrawKakao() async throws

/// 구글 회원 탈퇴 - SDK disconnect + 서버 API 호출
func withdrawGoogle() async throws

/// 애플 회원 탈퇴 - 서버에서 revoke 처리
func withdrawApple() async throws
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
//

import Foundation
import Core
import Entity
import UseCase
import Repository
Expand All @@ -16,23 +17,28 @@ public final class ProfileEditUseCaseImpl: ProfileEditUseCase {
private let loginRepository: LogInRepository
private let imageUploader: PresignedImageUploader
private let myPageRepository: MyPageRepository

private let oauthRepository: OAuthRepository

public init(
authRepository: AuthRepository,
loginRepository: LogInRepository,
imageUploader: PresignedImageUploader,
myPageRepository: MyPageRepository
myPageRepository: MyPageRepository,
oauthRepository: OAuthRepository
) {
self.authRepository = authRepository
self.loginRepository = loginRepository
self.imageUploader = imageUploader
self.myPageRepository = myPageRepository
self.oauthRepository = oauthRepository
}
}

public extension ProfileEditUseCaseImpl {
func loadUserProfile() async throws -> UserProfile {
return try await myPageRepository.fetchUserProfile()
let profile = try await myPageRepository.fetchUserProfile()
ServiceConfiguration.shared.setAuthProvider(profile.provider.rawValue)
return profile
}

func updateProfileImage(_ imageData: Data, type: String) async throws -> URL? {
Expand All @@ -42,8 +48,22 @@ public extension ProfileEditUseCaseImpl {
return try await myPageRepository.uploadProfileImage(path: url)
}

func withdraw(with password: String) async throws {
try await myPageRepository.deleteUserAccount(password: password)
func withdraw(with credential: WithdrawCredential) async throws {
switch credential {
case let .password(password):
try await myPageRepository.deleteUserAccount(password: password)
case let .oauth(provider):
switch provider {
case .kakao:
try await oauthRepository.withdrawKakao()
case .google:
try await oauthRepository.withdrawGoogle()
case .apple:
try await oauthRepository.withdrawApple()
case .normal:
break
}
}
authRepository.removeToken()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import Entity
public protocol ProfileEditUseCase {
func loadUserProfile() async throws -> UserProfile
func updateProfileImage(_ imageData: Data, type: String) async throws -> URL?
func withdraw(with password: String) async throws
func withdraw(with credential: WithdrawCredential) async throws
func changePassword(from password: String, to newPassword: String) async throws
func sendTemporaryPassword(to email: String, userName: String) async throws
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ final class WithdrawAuthViewModel: WithdrawAuthViewModelType {
private extension WithdrawAuthViewModel {
@MainActor func withdraw(password: String) async {
do {
try await useCase.withdraw(with: password)
try await useCase.withdraw(with: .password(password))
coordinator?.withdrawalSucceed()
} catch {
requestFailed(with: error)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,20 @@ final class WithdrawContainer:
WithdrawContainable,
WithdrawAuthDependency {
var profileEditUsecase: ProfileEditUseCase { dependency.profileEditUseCase }

func coordinator(listener: WithdrawListener) -> ViewableCoordinating {
let viewModel = WithdrawViewModel()
let viewModel = WithdrawViewModel(useCase: dependency.profileEditUseCase)
let viewControllerable = WithdrawViewController(viewModel: viewModel)

let withdrawAuth = WithdrawAuthContainer(dependency: self)
let coordinator = WithdrawCoordinator(
viewControllerable: viewControllerable,
viewModel: viewModel,
withdrawAuthContainable: withdrawAuth
)

coordinator.listener = listener

return coordinator
}
}
Loading