Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TNT-221] 로그인/회원가입/로그아웃/회원탈퇴 간 인증 정보 처리 로직 작성 #64

Merged
merged 8 commits into from
Feb 10, 2025
9 changes: 9 additions & 0 deletions TnT/Projects/DIContainer/Sources/DIContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,17 @@ private enum SocialUseCaseKey: DependencyKey {
static let liveValue: SocialLoginUseCase = SocialLoginUseCase(socialLoginRepository: SocialLogInRepositoryImpl(loginManager: SNSLoginManager()))
}

private enum KeyChainManagerKey: DependencyKey {
static let liveValue: KeyChainManager = keyChainManager
}

// MARK: - DependencyValues
public extension DependencyValues {
var keyChainManager: KeyChainManager {
get { self[KeyChainManagerKey.self] }
set { self[KeyChainManagerKey.self] = newValue }
}

var userUseCase: UserUseCase {
get { self[UserUseCaseKey.self] }
set { self[UserUseCaseKey.self] = newValue }
Expand Down
14 changes: 8 additions & 6 deletions TnT/Projects/Data/Sources/LocalStorage/KeyChainManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

import Foundation

public let keyChainManager = KeyChainManager()

/// KeyChainManager
/// - 키체인을 통해 데이터를 CRUD 하기 위한 유틸리티입니다.
public struct KeyChainManager {
Expand All @@ -18,7 +20,7 @@ public struct KeyChainManager {
/// - value: 저장할 데이터 (Generic 타입)
/// - key: Key 열거형으로 정의된 키
/// - Throws: 타입 불일치, 데이터 변환 실패, 키체인 저장 실패 에러
public static func save<T>(_ value: T, for key: Key) throws {
public func save<T>(_ value: T, for key: Key) throws {

guard type(of: value) == key.converter.type else {
throw KeyChainError.typeMismatch(
Expand Down Expand Up @@ -50,7 +52,7 @@ public struct KeyChainManager {
/// - Parameter key: Key 열거형으로 정의된 키
/// - Returns: Generic 타입으로 변환된 값 (데이터가 없으면 nil)
/// - Throws: 데이터 변환 실패, 읽기 실패, 타입 불일치 에러
public static func read<T>(for key: Key) throws -> T? {
public func read<T>(for key: Key) throws -> T? {
let keyString: String = key.keyString

let query: [String: Any] = [
Expand Down Expand Up @@ -89,7 +91,7 @@ public struct KeyChainManager {
/// 키체인에서 데이터를 삭제합니다.
/// - Parameter key: Key 열거형으로 정의된 키
/// - Throws: 삭제 실패 에러
public static func delete(_ key: Key) throws {
public func delete(_ key: Key) throws {
let keyString: String = key.keyString

let query: [String: Any] = [
Expand All @@ -107,21 +109,21 @@ public struct KeyChainManager {
public extension KeyChainManager {
/// Key 열거형: 키체인에 저장할 데이터를 정의
enum Key {
case token
case sessionId
case userId

/// 키 고유 문자열
var keyString: String {
switch self {
case .token: return "com.TnT.token"
case .sessionId: return "com.TnT.sessionId"
case .userId: return "com.TnT.userId"
}
}

/// 각 데이터 별 타입 & 변환 로직 정의
var converter: KeyConverter {
switch self {
case .token: return KeyConverter(type: String.self, convert: { $0 })
case .sessionId: return KeyConverter(type: String.self, convert: { $0 })
case .userId: return KeyConverter(type: Int.self, convert: { Int($0) })
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,18 @@
//

import Foundation
import Dependencies

/// 네트워크 요청에 인증 정보를 추가하는 인터셉터
struct AuthTokenInterceptor: Interceptor {
let priority: InterceptorPriority = .highest

func adapt(request: URLRequest) async throws -> URLRequest {
var request: URLRequest = request
guard let token: String = try KeyChainManager.read(for: .token) else {
guard let sessionId: String = try keyChainManager.read(for: .sessionId) else {
return request
}
request.setValue(token, forHTTPHeaderField: "Authorization")
request.setValue("SESSION-ID \(sessionId)", forHTTPHeaderField: "Authorization")
return request
}
}
5 changes: 5 additions & 0 deletions TnT/Projects/Data/Sources/Network/NetworkService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

import Foundation

import Domain

/// 네트워크 요청을 처리하는 서비스 클래스
public final class NetworkService {

Expand Down Expand Up @@ -85,6 +87,9 @@ private extension NetworkService {

/// JSON 데이터 디코딩
func decodeData<T: Decodable>(_ data: Data, as type: T.Type) throws -> T {
// 빈 데이터인 경우 EmptyResponse 타입으로 처리
if data.isEmpty, let emptyValue = EmptyResponse() as? T { return emptyValue }

do {
return try JSONDecoder(setting: .defaultSetting).decode(T.self, from: data)
} catch let decodingError as DecodingError {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ public struct UserRepositoryImpl: UserRepository {

public init() {}

/// 로그인 세션 유효 체크 요청을 수행
public func getSessionCheck() async throws -> GetSessionCheckResDTO {
return try await networkService.request(UserTargetType.getSessionCheck, decodingType: GetSessionCheckResDTO.self)
}

/// 소셜 로그인 요청을 수행
public func postSocialLogin(_ reqDTO: PostSocialLoginReqDTO) async throws -> PostSocialLoginResDTO {
return try await networkService.request(
Expand All @@ -35,4 +40,14 @@ public struct UserRepositoryImpl: UserRepository {
decodingType: PostSignUpResDTO.self
)
}

/// 로그아웃 요청을 수행
public func postLogout() async throws -> PostLogoutResDTO {
return try await networkService.request(UserTargetType.postLogout, decodingType: PostLogoutResDTO.self)
}

/// 회원탈퇴 요청을 수행
public func postWithdrawal() async throws -> PostWithdrawalResDTO {
return try await networkService.request(UserTargetType.postWithdrawal, decodingType: PostWithdrawalResDTO.self)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,16 @@ import Domain

/// 사용자 관련 API 요청 타입 정의
public enum UserTargetType {
/// 로그인 세션 유효 확인
case getSessionCheck
/// 소셜 로그인 요청
case postSocialLogin(reqDTO: PostSocialLoginReqDTO)
/// 회원가입 요청
case postSignUp(reqDTO: PostSignUpReqDTO, imgData: Data?)
/// 로그아웃 요청
case postLogout
/// 회원 탈퇴 요청
case postWithdrawal
}

extension UserTargetType: TargetType {
Expand All @@ -25,23 +31,38 @@ extension UserTargetType: TargetType {

var path: String {
switch self {
case .getSessionCheck:
return "/check-session"

case .postSocialLogin:
return "/login"

case .postSignUp:
return "/members/sign-up"

case .postLogout:
return "/logout"

case .postWithdrawal:
return "/members/withdraw"
}
}

var method: HTTPMethod {
switch self {
case .postSocialLogin, .postSignUp:
case .getSessionCheck:
return .get

case .postSocialLogin, .postSignUp, .postLogout, .postWithdrawal:
return .post
}
}

var task: RequestTask {
switch self {
case .getSessionCheck, .postLogout, .postWithdrawal:
return .requestPlain

case .postSocialLogin(let reqDto):
return .requestJSONEncodable(encodable: reqDto)

Expand All @@ -59,6 +80,9 @@ extension UserTargetType: TargetType {

var headers: [String: String]? {
switch self {
case .getSessionCheck, .postLogout, .postWithdrawal:
return nil

case .postSocialLogin:
return ["Content-Type": "application/json"]

Expand All @@ -71,10 +95,20 @@ extension UserTargetType: TargetType {
}

var interceptors: [any Interceptor] {
return [
LoggingInterceptor(),
ResponseValidatorInterceptor(),
RetryInterceptor(maxRetryCount: 2)
]
switch self {
case .getSessionCheck, .postLogout, .postWithdrawal:
return [
LoggingInterceptor(),
AuthTokenInterceptor(),
ResponseValidatorInterceptor(),
RetryInterceptor(maxRetryCount: 2)
]
default:
return [
LoggingInterceptor(),
ResponseValidatorInterceptor(),
RetryInterceptor(maxRetryCount: 2)
]
}
}
}
14 changes: 14 additions & 0 deletions TnT/Projects/Domain/Sources/DTO/EmptyResponse.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//
// EmptyResponse.swift
// Data
//
// Created by 박민서 on 2/9/25.
// Copyright © 2025 yapp25thTeamTnT. All rights reserved.
//

import Foundation

/// 비어 있는 응답을 처리할 빈 구조체
public struct EmptyResponse: Decodable {
public init() {}
}
13 changes: 13 additions & 0 deletions TnT/Projects/Domain/Sources/DTO/User/UserResponseDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@

import Foundation

/// 로그인 세션 유효 확인 응답 DTO
public struct GetSessionCheckResDTO: Decodable {
public let memberType: MemberTypeResDTO
}

/// 소셜 로그인 응답 DTO
public struct PostSocialLoginResDTO: Decodable {
/// 세션 ID
Expand Down Expand Up @@ -49,3 +54,11 @@ public struct PostSignUpResDTO: Decodable {
/// 프로필 이미지 URL
let profileImageUrl: String?
}

/// 로그아웃 응답 DTO
public struct PostLogoutResDTO: Decodable {
let sessionId: String
}

/// 회원탈퇴 응답 DTO
public typealias PostWithdrawalResDTO = EmptyResponse
15 changes: 15 additions & 0 deletions TnT/Projects/Domain/Sources/Repository/UserRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ import Foundation
/// 사용자 관련 데이터를 관리하는 UserRepository 프로토콜
/// - 실제 네트워크 요청은 이 인터페이스를 구현한 `UserRepositoryImpl`에서 수행됩니다.
public protocol UserRepository {
/// 로그인 세션 유효 확인
/// - Returns: 세션 유효 시, 멤버 타입 정보를 포함한 응답 DTO (`GetSessionCheckResDTO`)
/// - Throws: 네트워크 오류 또는 서버에서 반환한 오류를 발생시킬 수 있음
func getSessionCheck() async throws -> GetSessionCheckResDTO

/// 소셜 로그인 요청
/// - Parameter reqDTO: 소셜 로그인 요청에 필요한 데이터 (액세스 토큰 등)
/// - Returns: 로그인 성공 시, 사용자 정보를 포함한 응답 DTO (`PostSocialLoginResDTO`)
Expand All @@ -24,4 +29,14 @@ public protocol UserRepository {
/// - Returns: 회원가입 성공 시, 사용자 정보를 포함한 응답 DTO (`PostSignUpResDTO`)
/// - Throws: 네트워크 오류 또는 서버에서 반환한 오류를 발생시킬 수 있음
func postSignUp(_ reqDTO: PostSignUpReqDTO, profileImage: Data?) async throws -> PostSignUpResDTO

/// 로그아웃 요청
/// - Returns: 로그아웃 완료시 SessionID 포함한 응답 DTO (`PostLogoutResDTO`)
/// - Throws: 네트워크 오류 또는 서버에서 반환한 오류를 발생시킬 수 있음
func postLogout() async throws -> PostLogoutResDTO

/// 회원탈퇴 요청
/// - Returns: 회원 탈퇴 완료 시 응답 DTO (`PostWithdrawalResDTO`)
/// - Throws: 네트워크 오류 또는 서버에서 반환한 오류를 발생시킬 수 있음
func postWithdrawal() async throws -> PostWithdrawalResDTO
}
13 changes: 12 additions & 1 deletion TnT/Projects/Domain/Sources/UseCase/UserUseCase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,23 @@ public struct DefaultUserUseCase: UserRepository, UserUseCase {

// MARK: - Repository
extension DefaultUserUseCase {
public func getSessionCheck() async throws -> GetSessionCheckResDTO {
return try await userRepostiory.getSessionCheck()
}

public func postSocialLogin(_ reqDTO: PostSocialLoginReqDTO) async throws -> PostSocialLoginResDTO {
return try await userRepostiory.postSocialLogin(reqDTO)
}

public func postSignUp(_ reqDTO: PostSignUpReqDTO, profileImage: Data?) async throws -> PostSignUpResDTO {
return try await userRepostiory.postSignUp(reqDTO, profileImage: profileImage)
}

public func postLogout() async throws -> PostLogoutResDTO {
return try await userRepostiory.postLogout()
}

public func postWithdrawal() async throws -> PostWithdrawalResDTO {
return try await userRepostiory.postWithdrawal()
}
}

Loading