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-142] 트레이니 초대 코드 화면 작성, DIContainer 추가 #40

Merged
merged 13 commits into from
Jan 27, 2025
8 changes: 0 additions & 8 deletions TnT/Projects/DI/Sources/DISoureDummy.swift

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,4 @@
@preconcurrency import ProjectDescription
@preconcurrency import ProjectDescriptionHelpers

//let project = Project.module(name: "DI", resources: false)


let project = Project.staticLibraryProejct(name: "DIContainer", resource: false)
33 changes: 33 additions & 0 deletions TnT/Projects/DIContainer/Sources/DIContainer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//
// DIContainer.swift
// DIContainer
//
// Created by 박민서 on 1/27/25.
//

import Dependencies

import Domain
import Data

// MARK: - Swift-Dependencies
private enum UserUseCaseKey: DependencyKey {
static let liveValue: UserUseCase = DefaultUserUseCase(userRepository: UserRepositoryImpl())
}

private enum TraineeUseCaseKey: DependencyKey {
static let liveValue: TraineeUseCase = DefaultTraineeUseCase(trainerRepository: TrainerRepositoryImpl())
}

// MARK: - DependencyValues
public extension DependencyValues {
var userUseCase: UserUseCase {
get { self[UserUseCaseKey.self] }
set { self[UserUseCaseKey.self] = newValue }
}

var traineeUseCase: TraineeUseCase {
get { self[TraineeUseCaseKey.self] }
set { self[TraineeUseCaseKey.self] = newValue }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
//
// SNSLoginManager.swift
// DomainAuthInterface
//
// Created by 박민서 on 9/19/24.
//

import AuthenticationServices
import KakaoSDKCommon
import KakaoSDKAuth
import KakaoSDKUser

import Domain

/// SNSLoginManager는 카카오, 애플과 같은 소셜 로그인 및 로그아웃을 위한 헬퍼 클래스입니다.
/// 소셜 로그인 관련 SDK, API 호출은 모두 이곳에서 관리됩니다.
public final class SNSLoginManager: NSObject, SocialLoginRepository {

override public init() {
KakaoSDK.initSDK(appKey: Config.kakaoNativeAppKey)
}

// 클로저 저장을 위한 프로퍼티
private var appleLoginCompletion: ((AppleLoginInfo?) -> Void)?

// 애플 로그인
public func appleLogin() async -> AppleLoginInfo? {

// 클로저 기반의 비동기 작업을 async/await 방식으로 변환
await withCheckedContinuation { continuation in
let appleIDProvider: ASAuthorizationAppleIDProvider = ASAuthorizationAppleIDProvider()
let request: ASAuthorizationAppleIDRequest = appleIDProvider.createRequest()
request.requestedScopes = [.fullName, .email]

let authorizationController: ASAuthorizationController = ASAuthorizationController(authorizationRequests: [request])

authorizationController.delegate = self
authorizationController.presentationContextProvider = self

self.appleLoginCompletion = { loginInfo in
// 비동기 작업이 끝나면 continuation.resume()으로 결과를 넘깁니다
continuation.resume(returning: loginInfo) // 로그인 성공 또는 실패 후 반환
}

authorizationController.performRequests()
}
}

// 카카오 로그인
public func kakaoLogin() async -> KakaoLoginInfo? {
return await loginWithKakao {
// 카카오 앱의 설치 유무에 따라 로그인 처리
return UserApi.isKakaoTalkLoginAvailable()
? await self.loginWithKakaoTalk()
: await self.loginWithKakaoWeb()
}
}

// 카카오 로그아웃
public func kakaoLogout() async {
if AuthApi.hasToken() {
await withCheckedContinuation { continuation in
UserApi.shared.logout() { error in
if let error {
print("Kakao Logout Failed: \(error.localizedDescription)")
continuation.resume()
} else {
print("Kakao Logout Success")
continuation.resume()
}
}
}
}
}
}

// MARK: 애플 로그인 Extension 구현
extension SNSLoginManager: ASAuthorizationControllerDelegate, ASWebAuthenticationPresentationContextProviding, ASAuthorizationControllerPresentationContextProviding {

// 애플 로그인 성공 시 필요 정보 클로저로 리턴
public func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {

guard let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential else {
self.appleLoginCompletion?(nil)
return
}

guard
let identityToken = appleIDCredential.identityToken,
let identityTokenString = String(data: identityToken, encoding: .utf8),
let authorizationCode = appleIDCredential.authorizationCode,
let authorizationCodeString = String(data: authorizationCode, encoding: .utf8)
else {
self.appleLoginCompletion?(nil)
return
}

self.appleLoginCompletion?(
AppleLoginInfo(
identityToken: identityTokenString,
authorizationCode: authorizationCodeString
)
)
}

// 애플 로그인 실패 시 nil 리턴
public func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
print("Apple login failed: \(error.localizedDescription)")
self.appleLoginCompletion?(nil)
}

public func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
return ASPresentationAnchor()
}

public func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
return ASPresentationAnchor()
}
}

// MARK: 카카오 로그인 Extension 구현
extension SNSLoginManager {

/// 카카오톡(App) 로그인 처리를 async await로 래핑하는 함수입니다
@MainActor
private func loginWithKakaoTalk() async -> (OAuthToken?, Error?) {
return await withCheckedContinuation { continuation in
UserApi.shared.loginWithKakaoTalk { (oauthToken, error) in
continuation.resume(returning: (oauthToken, error))
}
}
}

/// 카카오톡(Web) 로그인 처리를 async await로 래핑하는 함수입니다
@MainActor
private func loginWithKakaoWeb() async -> (OAuthToken?, Error?) {
return await withCheckedContinuation { continuation in
UserApi.shared.loginWithKakaoAccount { (oauthToken, error) in
continuation.resume(returning: (oauthToken, error))
}
}
}

/// 공통된 카카오 로그인 로직을 async/await 방식으로 처리
/// - Parameters:
/// - loginMethod: 로그인 방식 (카카오톡 또는 웹 로그인을 선택적으로 처리)
/// - Returns: 성공 시 `KakaoLoginInfo`를 반환하고, 실패(에러, 토큰x) 시 nil을 반환합니다.
private func loginWithKakao(
loginMethod: @escaping () async -> (OAuthToken?, Error?)
) async -> KakaoLoginInfo? {
let (oauthToken, error): (OAuthToken?, Error?) = await loginMethod()

if let error = error {
print("kakao login failed: \(error.localizedDescription)")
return nil // 에러 발생 시 nil 반환
}

guard
let kakaoAccessToken = oauthToken?.accessToken,
let kakaoRefreshToken = oauthToken?.refreshToken
else {
return nil // 토큰이 없을 시 nil 반환
}

return KakaoLoginInfo(
accessToken: kakaoAccessToken,
socialRefreshToken: kakaoRefreshToken
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//
// TrainerRepositoryImpl.swift
// Data
//
// Created by 박민서 on 1/26/25.
// Copyright © 2025 yapp25thTeamTnT. All rights reserved.
//

import Foundation
import Dependencies

import Domain

/// 트레이너 관련 네트워크 요청을 처리하는 TrainerRepository 구현체
public struct TrainerRepositoryImpl: TrainerRepository {
private let networkService: NetworkService = .shared

public init() {}

public func getVerifyInvitationCode(code: String) async throws -> GetVerifyInvitationCodeResDTO {
return try await networkService.request(
TrainerTargetType.getVerifyInvitationCode(code: code),
decodingType: GetVerifyInvitationCodeResDTO.self
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//
// TrainerTargetType.swift
// Data
//
// Created by 박민서 on 1/26/25.
// Copyright © 2025 yapp25thTeamTnT. All rights reserved.
//

import Foundation

import Domain

/// 사용자 관련 API 요청 타입 정의
public enum TrainerTargetType {
/// 트레이너 초대코드 인증
case getVerifyInvitationCode(code: String)
}

extension TrainerTargetType: TargetType {
var baseURL: URL {
let url: String = Config.apiBaseUrlDev + "/trainers"
return URL(string: url)!
}

var path: String {
switch self {
case .getVerifyInvitationCode(let code):
return "/invitation-code/verify/\(code)"
}
}

var method: HTTPMethod {
switch self {
case .getVerifyInvitationCode:
return .get
}
}

var task: RequestTask {
switch self {
case .getVerifyInvitationCode:
return .requestPlain
}
}

var headers: [String: String]? {
switch self {
case .getVerifyInvitationCode:
return ["Content-Type": "application/json"]
}
}

var interceptors: [any Interceptor] {
return [
LoggingInterceptor(),
AuthTokenInterceptor(),
ResponseValidatorInterceptor(),
RetryInterceptor(maxRetryCount: 0)
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,3 @@ public struct UserRepositoryImpl: UserRepository {
)
}
}

// MARK: - Swift-Dependencies
private enum UserRepositoryKey: DependencyKey {
static let liveValue: UserRepository = UserRepositoryImpl()
}

// MARK: - DependencyValues
public extension DependencyValues {
var userRepository: UserRepository {
get { self[UserRepositoryKey.self] }
set { self[UserRepositoryKey.self] = newValue }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ public struct TTextField: View {
public extension TTextField.RightView {
enum Style {
case unit(text: String, status: TTextField.Status)
case button(title: String, tapAction: () -> Void)
case button(title: String, state: TButton.ButtonState, tapAction: () -> Void)
}
}

Expand All @@ -96,17 +96,15 @@ public extension TTextField {
.padding(.horizontal, 12)
.padding(.vertical, 3)

case let .button(title, tapAction):
case let .button(title, state, tapAction):
// TODO: 추후 버튼 컴포넌트 나오면 대체
Button(action: tapAction) {
Text(title)
.typographyStyle(.label2Medium, with: .neutral50)
.padding(.horizontal, 12)
.padding(.vertical, 7)
.background(Color.neutral900)
.clipShape(.rect(cornerRadius: 8))
}
.padding(.vertical, 5)
TButton(
title: title,
config: .small,
state: state,
action: tapAction
)
.frame(width: 66)
}
}
}
Expand Down
31 changes: 31 additions & 0 deletions TnT/Projects/Domain/Sources/DTO/SocialLogin/OAuthDTO.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//
// OAuthDTO.swift
// Domain
//
// Created by 박민서 on 1/26/25.
// Copyright © 2025 yapp25thTeamTnT. All rights reserved.
//

import Foundation

/// 카카오 로그인 요청 DTO
public struct KakaoLoginInfo: Codable {
public let accessToken: String
public let socialRefreshToken: String

public init(accessToken: String, socialRefreshToken: String) {
self.accessToken = accessToken
self.socialRefreshToken = socialRefreshToken
}
}

/// 애플 로그인 요청 DTO
public struct AppleLoginInfo: Codable {
public let identityToken: String
public let authorizationCode: String

public init(identityToken: String, authorizationCode: String) {
self.identityToken = identityToken
self.authorizationCode = authorizationCode
}
}
Loading