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
2 changes: 0 additions & 2 deletions Projects/App/Project.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ let project = Project(
.project(target: "Presentation", path: "../Presentation"),
.project(target: "Domain", path: "../Domain"),
.project(target: "DataSource", path: "../DataSource"),
.project(target: "NetworkService", path: "../NetworkService"),
.project(target: "Persistence", path: "../Persistence"),
.project(target: "Shared", path: "../Shared")
]
)
Expand Down
6 changes: 1 addition & 5 deletions Projects/App/Sources/DependencyInjection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,12 @@
import DataSource
import Domain
import Foundation
import NetworkService
import Persistence
import Presentation
import Shared

extension DIContainer {
func dependencyInjection() {
let networkAssembler = NetworkDependencyAssembler()
let persistenceAssembler = PersistenceDependencyAssembler()
let dataSourceAssembler = DataSourceDependencyAssembler(preAssemblers: [networkAssembler, persistenceAssembler])
let dataSourceAssembler = DataSourceDependencyAssembler()
let domainAssembler = DomainDependencyAssembler(preAssembler: dataSourceAssembler)
let presentationAssembler = PresentationDependencyAssembler(preAssembler: domainAssembler)
presentationAssembler.assemble()
Expand Down
19 changes: 15 additions & 4 deletions Projects/App/Sources/SceneDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// Created by 최정인 on 6/15/25.
//

import Domain
import KakaoSDKAuth
import Presentation
import Shared
Expand All @@ -19,13 +20,23 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {

DIContainer.shared.dependencyInjection()

let introView = IntroView()
let navigationController = UINavigationController(rootViewController: introView)
guard let userDataRepository = DIContainer.shared.resolve(type: UserDataRepositoryProtocol.self)
else { fatalError("userDataRepository 의존성이 등록되지 않았습니다.") }

window.rootViewController = navigationController
window.rootViewController = SplashView()
window.makeKeyAndVisible()

self.window = window

Task { @MainActor in
let isLogined = await userDataRepository.reissueToken()
if isLogined {
window.rootViewController = TabBarView()
} else {
let introView = IntroView()
let navigationController = UINavigationController(rootViewController: introView)
window.rootViewController = navigationController
}
}
}

func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,41 +5,24 @@
// Created by 최정인 on 6/26/25.
//

import Foundation
import Domain
import Foundation
import Shared

public struct DataSourceDependencyAssembler: DependencyAssemblerProtocol {
private let preAssemblers: [DependencyAssemblerProtocol]

public init(preAssemblers: [DependencyAssemblerProtocol]) {
self.preAssemblers = preAssemblers
}
public init() { }

public func assemble() {
preAssemblers.forEach { assembler in
assembler.assemble()
}

guard
let networkService = DIContainer.shared.resolve(type: NetworkServiceProtocol.self),
let keychainStorage = DIContainer.shared.resolve(type: KeychainStorageProtocol.self),
let userDefaultsStorage = DIContainer.shared.resolve(type: UserDefaultsStorageProtocol.self)
else { fatalError("networkService, keychainStorage, userDefaultsStorage 의존성이 등록되지 않았습니다.") }

DIContainer.shared.register(type: AuthRepositoryProtocol.self) { _ in
return AuthRepository(
networkService: networkService,
keychainStorage: keychainStorage,
userDefaultsStorage: userDefaultsStorage)
return AuthRepository()
}

DIContainer.shared.register(type: OnboardingRepositoryProtocol.self) { _ in
return OnboardingRepository(networkService: networkService, keychainStorage: keychainStorage)
return OnboardingRepository()
}

DIContainer.shared.register(type: UserDataRepositoryProtocol.self) { _ in
return UserDataRepository(keychainStorage: keychainStorage, userDefaultsStorage: userDefaultsStorage)
return UserDataRepository()
}
}
}
1 change: 1 addition & 0 deletions Projects/DataSource/Sources/Common/Enum/Endpoint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ public protocol Endpoint {
var headers: [String: String] { get }
var queryParameters: [String: String] { get }
var bodyParameters: [String: Any] { get }
var isAuthorized: Bool { get }
}
2 changes: 2 additions & 0 deletions Projects/DataSource/Sources/Common/Enum/UserDefaultsKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,6 @@

enum UserDefaultsKey: String {
case nickname
case socialLoginType
case profileImageUrl
}
3 changes: 3 additions & 0 deletions Projects/DataSource/Sources/Common/Error/AuthError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

enum AuthError: Error, CustomStringConvertible {
case kakaoTokenFetchFailed
case kakaoUserInformationFetchFailed
case tokenSaveFailed
case tokenLoadFailed
case tokenRemoveFailed
Expand All @@ -20,6 +21,8 @@ enum AuthError: Error, CustomStringConvertible {
switch self {
case .kakaoTokenFetchFailed:
return "카카오 토큰을 가져오는데 실패했습니다."
case .kakaoUserInformationFetchFailed:
return "카카오 유저 정보를 가져오는데 실패했습니다."
case .tokenSaveFailed:
return "토큰 저장에 실패했습니다."
case .tokenLoadFailed:
Expand Down
23 changes: 23 additions & 0 deletions Projects/DataSource/Sources/Common/Error/TokenError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//
// TokenError.swift
// DataSource
//
// Created by 최정인 on 7/26/25.
//

enum TokenError: Error, CustomStringConvertible {
case tokenSaveFailed
case tokenLoadFailed
case tokenRemoveFailed

public var description: String {
switch self {
case .tokenSaveFailed:
return "토큰 저장에 실패했습니다."
case .tokenLoadFailed:
return "토큰 불러오기에 실패했습니다."
case .tokenRemoveFailed:
return "토큰 삭제에 실패했습니다."
}
}
}
28 changes: 24 additions & 4 deletions Projects/DataSource/Sources/Common/Error/UserError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,37 @@
//

enum UserError: Error, CustomStringConvertible {
case accessTokenLoadFailed
case nicknameSaveFailed
case nicknameLoadFailed
case nicknameRemoveFailed
case socialLoginTypeSaveFailed
case socialLoginTypeLoadFailed
case socialLoginTypeRemoveFailed
case profileImageUrlSaveFailed
case profileImageUrlLoadFailed
case profileImageUrlRemoveFailed
case unknown(error: Error)


var description: String {
switch self {
case .accessTokenLoadFailed:
return "토큰 불러오기에 실패했습니다."
case .nicknameSaveFailed:
return "닉네임 저장 실패했습니다."
case .nicknameLoadFailed:
return "닉네임 불러오기에 실패했습니다."
case .nicknameRemoveFailed:
return "닉네임 삭제 실패했습니다."
case .socialLoginTypeSaveFailed:
return "소셜 로그인 타입 저장 실패했습니다."
case .socialLoginTypeLoadFailed:
return "소셜 로그인 타입 불러오기에 실패했습니다."
case .socialLoginTypeRemoveFailed:
return "소셜 로그인 타입 삭제 실패했습니다."
case .profileImageUrlSaveFailed:
return "유저 프로필 저장 실패했습니다."
case .profileImageUrlLoadFailed:
return "유저 프로필 불러오기에 실패했습니다."
case .profileImageUrlRemoveFailed:
return "유저 프로필 삭제 실패했습니다."
case .unknown(let error):
return "알 수 없는 에러가 발생했습니다. \(error.localizedDescription)"
}
Expand Down
31 changes: 31 additions & 0 deletions Projects/DataSource/Sources/Common/TokenManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//
// TokenManager.swift
// DataSource
//
// Created by 최정인 on 7/26/25.
//

final class TokenManager {
static let shared = TokenManager()
private let keychainStorage = KeychainStorage.shared

private init() { }

func loadToken(tokenType: TokenType) throws -> String {
guard let token: String = keychainStorage.load(forKey: tokenType.rawValue)
else { throw TokenError.tokenLoadFailed }
return token
}

func saveToken(token: String, tokenType: TokenType) throws {
guard keychainStorage.save(token, forKey: tokenType.rawValue)
else { throw TokenError.tokenSaveFailed }
}

func removeToken() throws {
guard
keychainStorage.remove(forKey: TokenType.accessToken.rawValue),
keychainStorage.remove(forKey: TokenType.refreshToken.rawValue)
else { throw TokenError.tokenRemoveFailed }
}
}
11 changes: 11 additions & 0 deletions Projects/DataSource/Sources/DTO/TokenResponseDTO.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//
// TokenResponseDTO.swift
// DataSource
//
// Created by 최정인 on 7/26/25.
//

struct TokenResponseDTO: Decodable {
let accessToken: String
let refreshToken: String
}
33 changes: 18 additions & 15 deletions Projects/DataSource/Sources/Endpoint/AuthEndpoint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,17 @@ import Domain

enum AuthEndpoint {
case login(socialLoginType: SocialLoginType, nickname: String?, token: String)
case logout(accessToken: String)
case withdraw(accessToken: String)
case logout
case withdraw
case reissue(refreshToken: String)
case agreements(accessToken: String, agreements: [TermsType: Bool])
case agreements(agreements: [TermsType: Bool])
}

extension AuthEndpoint: Endpoint {
var baseURL: String {
return AppProperties.baseURL + "/api/v1/auth"
}

var path: String {
switch self {
case .login: baseURL + "/login"
Expand All @@ -30,11 +30,11 @@ extension AuthEndpoint: Endpoint {
case .agreements: baseURL + "/agreements"
}
}

var method: HTTPMethod {
return .post
}

var headers: [String : String] {
var headers: [String: String] = [
"Content-Type": "application/json",
Expand All @@ -44,23 +44,19 @@ extension AuthEndpoint: Endpoint {
switch self {
case .login(_, _, let token):
headers["SocialAccessToken"] = token
case .logout(let accessToken):
headers["Authorization"] = "Bearer \(accessToken)"
case .withdraw(let accessToken):
headers["Authorization"] = "Bearer \(accessToken)"
case .reissue(let refreshToken):
headers["Refresh-Token"] = refreshToken
case .agreements(let accessToken, _):
headers["Authorization"] = "Bearer \(accessToken)"
default:
break
}

return headers
}

var queryParameters: [String : String] {
return [:]
}

var bodyParameters: [String : Any] {
switch self {
case .login(let socialLoginType, let nickname, _):
Expand All @@ -69,7 +65,7 @@ extension AuthEndpoint: Endpoint {
parameters["nickname"] = nickname
}
return parameters
case .agreements(_, let agreements):
case .agreements(let agreements):
var parameters: [String: Any] = [:]
for agreement in agreements {
parameters[agreement.key.termKey] = agreement.value
Expand All @@ -79,6 +75,13 @@ extension AuthEndpoint: Endpoint {
return [:]
}
}

var isAuthorized: Bool {
switch self {
case .login, .reissue: false
case .logout, .withdraw, .agreements: true
}
}
}

extension TermsType {
Expand Down
22 changes: 9 additions & 13 deletions Projects/DataSource/Sources/Endpoint/OnboardingEndpoint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
import Foundation

enum OnboardingEndpoint {
case registerOnboarding(accessToken: String, choices: [String: String])
case registerRecommendedRoutine(accessToken: String, selectedRoutines: [Int])
case registerOnboarding(choices: [String: String])

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

온보딩 등록 요청 시, 키 값으로 어떤 값들이 들어갈까요?.? 만약에 만약에 enum 과 같은 타입으로 범위를 정해둘 수 있다면, 타입을 강제하는 방법은 어떨까요??

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

엇 그 OnboardingUseCase에서 enum으로 받은 값들을 dictionary로 바꿔주는 로직을 수행하고 있습니다 !!

// OnboardingUseCase.swift
public func registerOnboarding(onboardingChoices: [OnboardingChoiceType]) async throws -> [RecommendedRoutineEntity] {
    let choices = convertToDictionary(onboardingChoices: onboardingChoices)
    let recommendedRoutines = try await onboardingRepository.registerOnboarding(onboardingChoices: choices)
    return recommendedRoutines
}

private func convertToDictionary(onboardingChoices: [OnboardingChoiceType]) -> [String: String] {
    var result: [String: String] = [:]
    let onboardingTypes: [OnboardingType] = [.time, .frequency, .feeling, .outdoor]
    for type in onboardingTypes {
        guard let choice = onboardingChoices.first(where: { $0.onboardingType == type })
        else { break }
        result[choice.onboardingType.key] = choice.value
    }
    return result
}

호옥시 이 부분을 말씀하신게 맞을까요 ?? 그렇다면 UseCase가 아닌 Repository에서 변경하는게 맞다고 생각하시나욤 ... ??

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

근데 또 이용약관 로직보면 ...

UseCase에서는 다음과 같이 repository 호출하고, DataSource 단에서 바꿔주고 있긴 하네요 ....

// LoginUseCase.swift
public func sumbitAgreement(agreements: [TermsType: Bool]) async throws {
    try await authRepository.submitAgreement(agreements: agreements)
}


// DataSource .... LoginEndpoint.swift
var bodyParameters: [String : Any] {
    switch self {
    case .login(let socialLoginType, let nickname, _):
        // ···
    case .agreements(let agreements):
        var parameters: [String: Any] = [:]
        for agreement in agreements {
            parameters[agreement.key.termKey] = agreement.value
        }
        return parameters
    default:
        return [:]
    }
}

extension TermsType {
    var termKey: String {
        switch self {
        case .service: "agreedToTermsOfService"
        case .privacy: "agreedToPrivacyPolicy"
        case .age: "isOverFourteen"
        }
    }
}

근데 그렇다 보니 TermsType의 위치가 애매하긴 해서 LoginEndpoint 파일 밑에 그냥 넣어둔 ㄹㅈㄷ 멋대로 짠 코드였습니다 ......

해당 로직 어디서 바꿔줄지 결정하면 하나로 통일하면서 수정하겠습니다 !!

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

말씀해주신 상황은 진짜 개발을 진행하면 많이 발생하는 문제인거 같아요.. 저도 하다보면 애매한 겅우가 많아서 일단 진행하는 경우가 많아서 왕 공감합니다 🥹

말씀해주신 내용이 맞아요!! 괜찮으시다면 변환은 repository에서 하는건 어떤가요? 가능하면 데이터 변환 로직은 reposiorty에서 진행하면 좋을 것 같다고 생각합니다. domain에 있는 usecase들은 데이터를 요청할 때나/데이터를 받을 때나 해당 데이터가 어떻게 가공해야할지 상관 쓰지 않고, 핵심 비즈니스 로직을 실행하기 위해 필요한 데이터 타입을 repository에게 주거나/받거나 할 수 있으면 좋을것 같다 생각하기 때문입니다!! 조이 의견은 어떠신가요?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

하아 ......... 그것은 너무 고민되는 사항 .......
띵의 의견 뭔말인지 너무 이해가고 ... 동의하지만 그럼 UseCase에서 어떤 것을 할 수 있을까요 ......

넘 passthrough 되는 기분이 들기도 합니다 ㅠ !!!!!!!!!!!!!

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

참고할 수 있을만한 다른 레포나 다른 분들의 조언도 정말 절실하네요 🥹
이후 시니어 개발자 분의 피드백도 한 번 듣고 고민해볼까요.?.?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오키 조아요 !!!!!!! 그럼 일단 현상 유지 하고 돌아오는 토요일에 조언을 구해볼까요 ??

그 외의 코드 리뷰 사항은 37bef52에서 반영했습니다 !!!

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 그래주시면 압도적 감사입니다!!!!

case registerRecommendedRoutine(selectedRoutines: [Int])
}

extension OnboardingEndpoint: Endpoint {
Expand All @@ -29,18 +29,10 @@ extension OnboardingEndpoint: Endpoint {
}

var headers: [String : String] {
var headers: [String: String] = [
let headers: [String: String] = [
"Content-Type": "application/json",
"accept": "*/*"
]

switch self {
case .registerOnboarding(let accessToken, _):
headers["Authorization"] = "Bearer \(accessToken)"
case .registerRecommendedRoutine(let accessToken, _):
headers["Authorization"] = "Bearer \(accessToken)"
}

return headers
}

Expand All @@ -50,10 +42,14 @@ extension OnboardingEndpoint: Endpoint {

var bodyParameters: [String : Any] {
switch self {
case .registerOnboarding(_, let choices):
case .registerOnboarding(let choices):
return choices
case .registerRecommendedRoutine(_, let selectedRoutines):
case .registerRecommendedRoutine(let selectedRoutines):
return ["recommendedRoutineIds": selectedRoutines]
}
}

var isAuthorized: Bool {
return true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
// Created by 최정인 on 6/21/25.
//

import DataSource
import Foundation

extension Endpoint {
Expand Down
Loading
Loading