diff --git a/Projects/App/Project.swift b/Projects/App/Project.swift index 3ec2bb8a..c58e3a91 100644 --- a/Projects/App/Project.swift +++ b/Projects/App/Project.swift @@ -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") ] ) diff --git a/Projects/App/Sources/DependencyInjection.swift b/Projects/App/Sources/DependencyInjection.swift index ad2b5a19..f1770f5a 100644 --- a/Projects/App/Sources/DependencyInjection.swift +++ b/Projects/App/Sources/DependencyInjection.swift @@ -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() diff --git a/Projects/App/Sources/SceneDelegate.swift b/Projects/App/Sources/SceneDelegate.swift index 1d163d49..947a6019 100644 --- a/Projects/App/Sources/SceneDelegate.swift +++ b/Projects/App/Sources/SceneDelegate.swift @@ -5,6 +5,7 @@ // Created by 최정인 on 6/15/25. // +import Domain import KakaoSDKAuth import Presentation import Shared @@ -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) { diff --git a/Projects/DataSource/Sources/Common/DataSourceDependencyAssembler.swift b/Projects/DataSource/Sources/Common/DataSourceDependencyAssembler.swift index 95d2c27f..fcfb725a 100644 --- a/Projects/DataSource/Sources/Common/DataSourceDependencyAssembler.swift +++ b/Projects/DataSource/Sources/Common/DataSourceDependencyAssembler.swift @@ -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() } } } diff --git a/Projects/DataSource/Sources/Common/Enum/Endpoint.swift b/Projects/DataSource/Sources/Common/Enum/Endpoint.swift index 361e8ddb..cd47d6b5 100644 --- a/Projects/DataSource/Sources/Common/Enum/Endpoint.swift +++ b/Projects/DataSource/Sources/Common/Enum/Endpoint.swift @@ -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 } } diff --git a/Projects/DataSource/Sources/Common/Enum/UserDefaultsKey.swift b/Projects/DataSource/Sources/Common/Enum/UserDefaultsKey.swift index 4028411f..8e0eb971 100644 --- a/Projects/DataSource/Sources/Common/Enum/UserDefaultsKey.swift +++ b/Projects/DataSource/Sources/Common/Enum/UserDefaultsKey.swift @@ -7,4 +7,6 @@ enum UserDefaultsKey: String { case nickname + case socialLoginType + case profileImageUrl } diff --git a/Projects/DataSource/Sources/Common/Error/AuthError.swift b/Projects/DataSource/Sources/Common/Error/AuthError.swift index babca118..39a1fbd3 100644 --- a/Projects/DataSource/Sources/Common/Error/AuthError.swift +++ b/Projects/DataSource/Sources/Common/Error/AuthError.swift @@ -7,6 +7,7 @@ enum AuthError: Error, CustomStringConvertible { case kakaoTokenFetchFailed + case kakaoUserInformationFetchFailed case tokenSaveFailed case tokenLoadFailed case tokenRemoveFailed @@ -20,6 +21,8 @@ enum AuthError: Error, CustomStringConvertible { switch self { case .kakaoTokenFetchFailed: return "카카오 토큰을 가져오는데 실패했습니다." + case .kakaoUserInformationFetchFailed: + return "카카오 유저 정보를 가져오는데 실패했습니다." case .tokenSaveFailed: return "토큰 저장에 실패했습니다." case .tokenLoadFailed: diff --git a/Projects/DataSource/Sources/Common/Error/TokenError.swift b/Projects/DataSource/Sources/Common/Error/TokenError.swift new file mode 100644 index 00000000..38cc3920 --- /dev/null +++ b/Projects/DataSource/Sources/Common/Error/TokenError.swift @@ -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 "토큰 삭제에 실패했습니다." + } + } +} diff --git a/Projects/DataSource/Sources/Common/Error/UserError.swift b/Projects/DataSource/Sources/Common/Error/UserError.swift index c8c1e26d..964ca8c3 100644 --- a/Projects/DataSource/Sources/Common/Error/UserError.swift +++ b/Projects/DataSource/Sources/Common/Error/UserError.swift @@ -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)" } diff --git a/Projects/DataSource/Sources/Common/TokenManager.swift b/Projects/DataSource/Sources/Common/TokenManager.swift new file mode 100644 index 00000000..a8bc836c --- /dev/null +++ b/Projects/DataSource/Sources/Common/TokenManager.swift @@ -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 } + } +} diff --git a/Projects/DataSource/Sources/DTO/TokenResponseDTO.swift b/Projects/DataSource/Sources/DTO/TokenResponseDTO.swift new file mode 100644 index 00000000..df994f51 --- /dev/null +++ b/Projects/DataSource/Sources/DTO/TokenResponseDTO.swift @@ -0,0 +1,11 @@ +// +// TokenResponseDTO.swift +// DataSource +// +// Created by 최정인 on 7/26/25. +// + +struct TokenResponseDTO: Decodable { + let accessToken: String + let refreshToken: String +} diff --git a/Projects/DataSource/Sources/Endpoint/AuthEndpoint.swift b/Projects/DataSource/Sources/Endpoint/AuthEndpoint.swift index 0f7a1b61..b22943f2 100644 --- a/Projects/DataSource/Sources/Endpoint/AuthEndpoint.swift +++ b/Projects/DataSource/Sources/Endpoint/AuthEndpoint.swift @@ -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" @@ -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", @@ -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, _): @@ -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 @@ -79,6 +75,13 @@ extension AuthEndpoint: Endpoint { return [:] } } + + var isAuthorized: Bool { + switch self { + case .login, .reissue: false + case .logout, .withdraw, .agreements: true + } + } } extension TermsType { diff --git a/Projects/DataSource/Sources/Endpoint/OnboardingEndpoint.swift b/Projects/DataSource/Sources/Endpoint/OnboardingEndpoint.swift index e0be0991..554704da 100644 --- a/Projects/DataSource/Sources/Endpoint/OnboardingEndpoint.swift +++ b/Projects/DataSource/Sources/Endpoint/OnboardingEndpoint.swift @@ -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]) + case registerRecommendedRoutine(selectedRoutines: [Int]) } extension OnboardingEndpoint: Endpoint { @@ -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 } @@ -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 + } } diff --git a/Projects/NetworkService/Sources/BaseResponse.swift b/Projects/DataSource/Sources/NetworkService/BaseResponse.swift similarity index 100% rename from Projects/NetworkService/Sources/BaseResponse.swift rename to Projects/DataSource/Sources/NetworkService/BaseResponse.swift diff --git a/Projects/NetworkService/Sources/Extension/Endpoint+.swift b/Projects/DataSource/Sources/NetworkService/Extension/Endpoint+.swift similarity index 96% rename from Projects/NetworkService/Sources/Extension/Endpoint+.swift rename to Projects/DataSource/Sources/NetworkService/Extension/Endpoint+.swift index 4058a47d..83ab723f 100644 --- a/Projects/NetworkService/Sources/Extension/Endpoint+.swift +++ b/Projects/DataSource/Sources/NetworkService/Extension/Endpoint+.swift @@ -5,7 +5,6 @@ // Created by 최정인 on 6/21/25. // -import DataSource import Foundation extension Endpoint { diff --git a/Projects/NetworkService/Sources/Extension/URLRequest+.swift b/Projects/DataSource/Sources/NetworkService/Extension/URLRequest+.swift similarity index 100% rename from Projects/NetworkService/Sources/Extension/URLRequest+.swift rename to Projects/DataSource/Sources/NetworkService/Extension/URLRequest+.swift diff --git a/Projects/NetworkService/Sources/NetworkError.swift b/Projects/DataSource/Sources/NetworkService/NetworkError.swift similarity index 86% rename from Projects/NetworkService/Sources/NetworkError.swift rename to Projects/DataSource/Sources/NetworkService/NetworkError.swift index 0a9f02ea..73b823ef 100644 --- a/Projects/NetworkService/Sources/NetworkError.swift +++ b/Projects/DataSource/Sources/NetworkService/NetworkError.swift @@ -5,14 +5,14 @@ // Created by 최정인 on 6/23/25. // -public enum NetworkError: Error, CustomStringConvertible { +enum NetworkError: Error, CustomStringConvertible { case invalidURL case invalidResponse case invalidStatusCode(statusCode: Int) case emptyData case decodingError - public var description: String { + var description: String { switch self { case .invalidURL: return "invalidURL" diff --git a/Projects/NetworkService/Sources/NetworkService.swift b/Projects/DataSource/Sources/NetworkService/NetworkService.swift similarity index 66% rename from Projects/NetworkService/Sources/NetworkService.swift rename to Projects/DataSource/Sources/NetworkService/NetworkService.swift index 24f84568..55d846c5 100644 --- a/Projects/NetworkService/Sources/NetworkService.swift +++ b/Projects/DataSource/Sources/NetworkService/NetworkService.swift @@ -5,21 +5,22 @@ // Created by 최정인 on 6/19/25. // -import DataSource import Foundation import Shared -public final class NetworkService: NetworkServiceProtocol { - private let networkProvider: NetworkProviderProtocol +final class NetworkService { + static let shared = NetworkService() private let decoder = JSONDecoder() - public init(networkProvider: NetworkProviderProtocol = URLSession.shared) { - self.networkProvider = networkProvider - } + private init() { } - public func request(endpoint: Endpoint, type: T.Type) async throws -> T? { - let request = try endpoint.makeURLRequest() - let (data, response) = try await networkProvider.data(for: request) + func request(endpoint: Endpoint, type: T.Type) async throws -> T? { + var request = try endpoint.makeURLRequest() + if endpoint.isAuthorized { + let accessToken = try TokenManager.shared.loadToken(tokenType: .accessToken) + request.headers["Authorization"] = "Bearer \(accessToken)" + } + let (data, response) = try await URLSession.shared.data(for: request) guard let httpResponse = response as? HTTPURLResponse else { throw NetworkError.invalidResponse } diff --git a/Projects/Persistence/Sources/Keychain/KeychainStorage.swift b/Projects/DataSource/Sources/Persistence/KeychainStorage.swift similarity index 71% rename from Projects/Persistence/Sources/Keychain/KeychainStorage.swift rename to Projects/DataSource/Sources/Persistence/KeychainStorage.swift index 5e96be38..18c27ada 100644 --- a/Projects/Persistence/Sources/Keychain/KeychainStorage.swift +++ b/Projects/DataSource/Sources/Persistence/KeychainStorage.swift @@ -1,23 +1,19 @@ // // KeychainStorage.swift -// Persistence +// DataSource // -// Created by 반성준 on 6/21/25. +// Created by 최정인 on 7/26/25. // -import DataSource import Foundation -public final class KeychainStorage: KeychainStorageProtocol { - private let service: String - private let accessGroup: String? +final class KeychainStorage { + static let shared = KeychainStorage() + private let service: String = Bundle.main.bundleIdentifier ?? "DefaultService" - public init(service: String? = nil, accessGroup: String? = nil) { - self.service = service ?? Bundle.main.bundleIdentifier ?? "DefaultService" - self.accessGroup = accessGroup - } + private init() { } - public func save(_ value: String, forKey key: String) -> Bool { + func save(_ value: String, forKey key: String) -> Bool { guard let data = value.data(using: .utf8) else { return false } @@ -41,7 +37,7 @@ public final class KeychainStorage: KeychainStorageProtocol { return status == errSecSuccess } - public func load(forKey key: String) -> String? { + func load(forKey key: String) -> String? { var query = baseQuery(for: key) query[kSecReturnData as String] = kCFBooleanTrue query[kSecMatchLimit as String] = kSecMatchLimitOne @@ -57,7 +53,7 @@ public final class KeychainStorage: KeychainStorageProtocol { return nil } - public func remove(forKey key: String) -> Bool { + func remove(forKey key: String) -> Bool { let query = baseQuery(for: key) let status = SecItemDelete(query as CFDictionary) @@ -70,11 +66,6 @@ public final class KeychainStorage: KeychainStorageProtocol { kSecAttrService as String: service, kSecAttrAccount as String: key ] - - if let accessGroup = accessGroup { - query[kSecAttrAccessGroup as String] = accessGroup - } - return query } } diff --git a/Projects/DataSource/Sources/Persistence/UserDefaultsStorage.swift b/Projects/DataSource/Sources/Persistence/UserDefaultsStorage.swift new file mode 100644 index 00000000..4a71f985 --- /dev/null +++ b/Projects/DataSource/Sources/Persistence/UserDefaultsStorage.swift @@ -0,0 +1,29 @@ +// +// UserDefaultsStorage.swift +// DataSource +// +// Created by 최정인 on 7/26/25. +// + +import Foundation + +final class UserDefaultsStorage { + static let shared = UserDefaultsStorage() + private let userDefaults: UserDefaults = .standard + + private init() { } + + func save(_ value: Any?, forKey key: String) -> Bool { + userDefaults.set(value, forKey: key) + return userDefaults.object(forKey: key) != nil + } + + func load(forKey key: String) -> T? { + userDefaults.object(forKey: key) as? T + } + + func remove(forKey key: String) -> Bool { + userDefaults.removeObject(forKey: key) + return userDefaults.object(forKey: key) == nil + } +} diff --git a/Projects/DataSource/Sources/Protocol/KeychainStorageProtocol.swift b/Projects/DataSource/Sources/Protocol/KeychainStorageProtocol.swift deleted file mode 100644 index d95bf271..00000000 --- a/Projects/DataSource/Sources/Protocol/KeychainStorageProtocol.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// KeychainStorageProtocol.swift -// DataSource -// -// Created by 반성준 on 6/21/25. -// - -import Foundation - -public protocol KeychainStorageProtocol { - func save(_ value: String, forKey key: String) -> Bool - func load(forKey key: String) -> String? - func remove(forKey key: String) -> Bool -} diff --git a/Projects/DataSource/Sources/Protocol/NetworkServiceProtocol.swift b/Projects/DataSource/Sources/Protocol/NetworkServiceProtocol.swift deleted file mode 100644 index b4f07c58..00000000 --- a/Projects/DataSource/Sources/Protocol/NetworkServiceProtocol.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// NetworkServiceProtocol.swift -// DataSource -// -// Created by 최정인 on 6/20/25. -// - -import Foundation - -public protocol NetworkServiceProtocol { - /// Endpoint로 비동기 네트워크 요청을 수행하고, 응답 데이터를 디코딩하여 반환합니다. - /// 네트워크 오류 또는 디코딩 오류가 발생한 경우, NetworkError를 throw 합니다. - /// - /// - Parameters: - /// - endpoint: 요청을 보낼 API Endpoint 정보 - /// - type: 디코딩할 Response DTO 타입 - /// - Returns: 응답 데이터를 디코딩한 객체 - func request(endpoint: Endpoint, type: T.Type) async throws -> T? -} diff --git a/Projects/DataSource/Sources/Protocol/UserDefaultsStorageProtocol.swift b/Projects/DataSource/Sources/Protocol/UserDefaultsStorageProtocol.swift deleted file mode 100644 index c7c26cd5..00000000 --- a/Projects/DataSource/Sources/Protocol/UserDefaultsStorageProtocol.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// UserDefaultsStorageProtocol.swift -// DataSource -// -// Created by 반성준 on 6/25/25. -// - -import Foundation - -public protocol UserDefaultsStorageProtocol { - func save(_ value: Any?, forKey key: String) -> Bool - func load(forKey key: String) -> T? - func remove(forKey key: String) -> Bool -} diff --git a/Projects/DataSource/Sources/Repository/AuthRepository.swift b/Projects/DataSource/Sources/Repository/AuthRepository.swift index ea88542c..e55a64e3 100644 --- a/Projects/DataSource/Sources/Repository/AuthRepository.swift +++ b/Projects/DataSource/Sources/Repository/AuthRepository.swift @@ -12,29 +12,26 @@ import KakaoSDKUser import KakaoSDKAuth final class AuthRepository: AuthRepositoryProtocol { - private let networkService: NetworkServiceProtocol - private let keychainStorage: KeychainStorageProtocol - private let userDefaultsStorage: UserDefaultsStorageProtocol - - init( - networkService: NetworkServiceProtocol, - keychainStorage: KeychainStorageProtocol, - userDefaultsStorage: UserDefaultsStorageProtocol - ) { - self.networkService = networkService - self.keychainStorage = keychainStorage - self.userDefaultsStorage = userDefaultsStorage - } + private let networkService = NetworkService.shared + private let tokenManager = TokenManager.shared + private let userDefaultsStorage = UserDefaultsStorage.shared + // 카카오 로그인을 진행합니다. func kakaoLogin() async throws -> UserEntity { let accessToken = try await fetchKakaoToken() + let (nickname, profileImageUrl) = try await fetchKakaoUserInfo() let user = try await requestServerLogin( socialType: .kakao, nickname: nil, token: accessToken) + + try saveNickname(nickname: nickname) + try saveSocialLoginType(socialLoginType: .kakao) + try saveUserProfileImageUrl(profileImageUrl: profileImageUrl) return user } + // 애플 로그인을 진행합니다. func appleLogin(nickname: String?, authToken: String) async throws -> UserEntity { var savedNickname: String = "" if let nickname { @@ -47,48 +44,33 @@ final class AuthRepository: AuthRepositoryProtocol { socialType: .apple, nickname: savedNickname, token: authToken) + + try saveSocialLoginType(socialLoginType: .apple) return user } + // 이용 약관 동의를 진행합니다. func submitAgreement(agreements: [TermsType : Bool]) async throws { - let accessToken = try loadToken(tokenType: .accessToken) - let endpoint = AuthEndpoint.agreements(accessToken: accessToken, agreements: agreements) + let endpoint = AuthEndpoint.agreements(agreements: agreements) _ = try await networkService.request(endpoint: endpoint, type: EmptyResponseDTO.self) } + // 로그아웃을 진행합니다. func logout() async throws { - let accessToken = try loadToken(tokenType: .accessToken) - let endpoint = AuthEndpoint.logout(accessToken: accessToken) + let endpoint = AuthEndpoint.logout _ = try await networkService.request(endpoint: endpoint, type: String.self) - try removeToken() + try tokenManager.removeToken() } + // 탈퇴하기를 진행합니다. func withdraw() async throws { - let accessToken = try loadToken(tokenType: .accessToken) - let endpoint = AuthEndpoint.withdraw(accessToken: accessToken) + let endpoint = AuthEndpoint.withdraw _ = try await networkService.request(endpoint: endpoint, type: String.self) - try removeToken() - try removeNickname() - } - - func reissueToken() async throws { - let refreshToken = try loadToken(tokenType: .refreshToken) - let endpoint = AuthEndpoint.reissue(refreshToken: refreshToken) - - guard let userResponse = try await networkService.request(endpoint: endpoint, type: LoginResponseDTO.self) - else { return } - let userEntity = userResponse.toUserEntity() - - guard - saveToken(tokenType: .accessToken, token: userEntity.accessToken), - saveToken(tokenType: .refreshToken, token: userEntity.refreshToken) - else { throw AuthError.tokenSaveFailed } - - BitnagilLogger.log(logType: .debug, message: "User Logined: \(userEntity.userState)") - BitnagilLogger.log(logType: .debug, message: "AccessToken Saved: \(userEntity.accessToken)") - BitnagilLogger.log(logType: .debug, message: "RefreshToken Saved: \(userEntity.refreshToken)") + try tokenManager.removeToken() + try removeUserInfo() } + // 카카오 SDK를 통해 카카오 authToken을 받아옵니다. private func fetchKakaoToken() async throws -> String { try await withCheckedThrowingContinuation { continuation in let resultHandler: (OAuthToken?, Error?) -> Void = { oauthToken, error in @@ -111,6 +93,26 @@ final class AuthRepository: AuthRepositoryProtocol { } } + // 카카오 SDK를 통해 유저의 정보(카카오 닉네임, 프로필 이미지)를 받아옵니다. + private func fetchKakaoUserInfo() async throws -> (nickname: String, profileImageUrl: URL) { + try await withCheckedThrowingContinuation { continuation in + let resultHandler: (User?, Error?) -> Void = { user, error in + if let error { + continuation.resume(throwing: AuthError.unknown(error)) + } else if + let nickname = user?.kakaoAccount?.profile?.nickname, + let profileImageUrl = user?.kakaoAccount?.profile?.profileImageUrl { + continuation.resume(returning: (nickname, profileImageUrl)) + } else { + continuation.resume(throwing: AuthError.kakaoUserInformationFetchFailed) + } + } + + UserApi.shared.me(completion: resultHandler) + } + } + + // 서버 로그인을 진행합니다. private func requestServerLogin( socialType: SocialLoginType, nickname: String?, @@ -125,10 +127,8 @@ final class AuthRepository: AuthRepositoryProtocol { else { throw AuthError.invalidUserData } let userEntity = userResponse.toUserEntity() - guard - saveToken(tokenType: .accessToken, token: userEntity.accessToken), - saveToken(tokenType: .refreshToken, token: userEntity.refreshToken) - else { throw AuthError.tokenSaveFailed } + try tokenManager.saveToken(token: userEntity.accessToken, tokenType: .accessToken) + try tokenManager.saveToken(token: userEntity.refreshToken, tokenType: .refreshToken) BitnagilLogger.log(logType: .debug, message: "User Logined: \(userEntity.userState)") BitnagilLogger.log(logType: .debug, message: "AccessToken Saved: \(userEntity.accessToken)") @@ -137,41 +137,48 @@ final class AuthRepository: AuthRepositoryProtocol { return userEntity } - private func saveToken(tokenType: TokenType, token: String) -> Bool { - return keychainStorage.save(token, forKey: tokenType.rawValue) - } - - private func loadToken(tokenType: TokenType) throws -> String { - guard let token = keychainStorage.load(forKey: tokenType.rawValue) else { - throw AuthError.tokenLoadFailed - } - return token - } - - private func removeToken() throws { - guard - keychainStorage.remove(forKey: TokenType.accessToken.rawValue), - keychainStorage.remove(forKey: TokenType.refreshToken.rawValue) - else { throw AuthError.tokenRemoveFailed } - } - + // UserDefaults에 닉네임을 저장합니다. private func saveNickname(nickname: String) throws { guard userDefaultsStorage.save(nickname, forKey: UserDefaultsKey.nickname.rawValue) else { - throw AuthError.nicknameSaveFailed + throw UserError.nicknameSaveFailed } } + // UserDefaults에 저장된 닉네임을 불러옵니다. private func loadNickname() throws -> String { let nickname: String? = userDefaultsStorage.load(forKey: UserDefaultsKey.nickname.rawValue) guard let nickname else { - throw AuthError.nicknameLoadFailed + throw UserError.nicknameLoadFailed } return nickname } - private func removeNickname() throws { + // UserDefaults에 소셜 로그인 타입을 저장합니다. + private func saveSocialLoginType(socialLoginType: SocialLoginType) throws { + guard userDefaultsStorage.save(socialLoginType.rawValue, forKey: UserDefaultsKey.socialLoginType.rawValue) else { + throw UserError.socialLoginTypeSaveFailed + } + } + + // UserDefaults에 프로필 이미지를 저장합니다. + private func saveUserProfileImageUrl(profileImageUrl: URL) throws { + guard userDefaultsStorage.save(profileImageUrl.absoluteString, forKey: UserDefaultsKey.profileImageUrl.rawValue) else { + throw UserError.profileImageUrlSaveFailed + } + } + + // UserDefaults에 저장된 유저 정보(닉네임, 소셜 로그인 타입, 프로필 이미지)를 삭제합니다. + private func removeUserInfo() throws { guard userDefaultsStorage.remove(forKey: UserDefaultsKey.nickname.rawValue) else { - throw AuthError.nicknameRemoveFailed + throw UserError.nicknameRemoveFailed + } + + guard userDefaultsStorage.remove(forKey: UserDefaultsKey.socialLoginType.rawValue) else { + throw UserError.socialLoginTypeRemoveFailed + } + + guard userDefaultsStorage.remove(forKey: UserDefaultsKey.profileImageUrl.rawValue) else { + throw UserError.profileImageUrlRemoveFailed } } } diff --git a/Projects/DataSource/Sources/Repository/OnboardingRepository.swift b/Projects/DataSource/Sources/Repository/OnboardingRepository.swift index 1f1ebe03..b53a9c98 100644 --- a/Projects/DataSource/Sources/Repository/OnboardingRepository.swift +++ b/Projects/DataSource/Sources/Repository/OnboardingRepository.swift @@ -8,18 +8,10 @@ import Domain final class OnboardingRepository: OnboardingRepositoryProtocol { - private let networkService: NetworkServiceProtocol - private let keychainStorage: KeychainStorageProtocol - - init(networkService: NetworkServiceProtocol, keychainStorage: KeychainStorageProtocol) { - self.networkService = networkService - self.keychainStorage = keychainStorage - } + private let networkService = NetworkService.shared func registerOnboarding(onboardingChoices: [String : String]) async throws -> [RecommendedRoutineEntity] { - let accessToken = try loadToken(tokenType: .accessToken) - let endpoint = OnboardingEndpoint.registerOnboarding(accessToken: accessToken, choices: onboardingChoices) - + let endpoint = OnboardingEndpoint.registerOnboarding(choices: onboardingChoices) guard let response = try await networkService.request(endpoint: endpoint, type: RecommendedRoutineListResponseDTO.self) else { return [] } @@ -28,15 +20,7 @@ final class OnboardingRepository: OnboardingRepositoryProtocol { } func registerRecommendedRoutines(selectedRoutines: [Int]) async throws { - let accessToken = try loadToken(tokenType: .accessToken) - let endpoint = OnboardingEndpoint.registerRecommendedRoutine(accessToken: accessToken, selectedRoutines: selectedRoutines) + let endpoint = OnboardingEndpoint.registerRecommendedRoutine(selectedRoutines: selectedRoutines) _ = try await networkService.request(endpoint: endpoint, type: EmptyResponseDTO.self) } - - private func loadToken(tokenType: TokenType) throws -> String { - guard let token = keychainStorage.load(forKey: tokenType.rawValue) else { - throw AuthError.tokenLoadFailed - } - return token - } } diff --git a/Projects/DataSource/Sources/Repository/UserDataRepository.swift b/Projects/DataSource/Sources/Repository/UserDataRepository.swift index 1eb3d511..7d037b78 100644 --- a/Projects/DataSource/Sources/Repository/UserDataRepository.swift +++ b/Projects/DataSource/Sources/Repository/UserDataRepository.swift @@ -7,31 +7,41 @@ import Domain import Foundation +import Shared final class UserDataRepository: UserDataRepositoryProtocol { - private let keychainStorage: KeychainStorageProtocol - private let userDefaultsStorage: UserDefaultsStorageProtocol + private let networkService = NetworkService.shared + private let userDefaultsStorage = UserDefaultsStorage.shared + private let tokenManager = TokenManager.shared - init(keychainStorage: KeychainStorageProtocol, userDefaultsStorage: UserDefaultsStorageProtocol) { - self.keychainStorage = keychainStorage - self.userDefaultsStorage = userDefaultsStorage - } - - // TODO: - accessToken fetch 로직 상의 후 결정 - func loadAccessToken() throws -> String { - guard let token = keychainStorage.load(forKey: TokenType.accessToken.rawValue) else { - throw UserError.accessTokenLoadFailed - } - - return token - } - func loadNickname() throws -> String { + // TODO: 서버에서 닉넴 보내준대요 guard let nickname: String = userDefaultsStorage.load(forKey: UserDefaultsKey.nickname.rawValue) else { throw UserError.nicknameLoadFailed } return nickname } + + func reissueToken() async -> Bool { + do { + let refreshToken = try tokenManager.loadToken(tokenType: .refreshToken) + let endpoint = AuthEndpoint.reissue(refreshToken: refreshToken) + + guard let tokenResponse = try await networkService.request(endpoint: endpoint, type: TokenResponseDTO.self) + else { return false } + + try tokenManager.saveToken(token: tokenResponse.accessToken, tokenType: .accessToken) + try tokenManager.saveToken(token: tokenResponse.refreshToken, tokenType: .refreshToken) + + BitnagilLogger.log(logType: .debug, message: "AccessToken Saved: \(tokenResponse.accessToken)") + BitnagilLogger.log(logType: .debug, message: "RefreshToken Saved: \(tokenResponse.refreshToken)") + + return true + } catch { + BitnagilLogger.log(logType: .error, message: "\(error.localizedDescription)") + return false + } + } } diff --git a/Projects/Domain/Sources/Entity/Enum/OnboardingChoiceType.swift b/Projects/Domain/Sources/Entity/Enum/OnboardingChoiceType.swift index ca6f3185..1774043b 100644 --- a/Projects/Domain/Sources/Entity/Enum/OnboardingChoiceType.swift +++ b/Projects/Domain/Sources/Entity/Enum/OnboardingChoiceType.swift @@ -50,23 +50,23 @@ public enum OnboardingChoiceType: CaseIterable { var value: String { switch self { - case .morningTime: "MORNING" - case .eveningTime: "EVENING" - case .allTime: "NOTHING" + case .morningTime: "08:00:00" + case .eveningTime: "20:00:00" + case .allTime: "00:00:00" - case .never: "ZERO_PER_WEEK" - case .rarely: "ONE_TO_TWO_PER_WEEK" - case .sometimes: "THREE_TO_FOUR_PER_WEEK" - case .often: "MORE_THAN_FIVE_PER_WEEK" + case .never: "NEVER" + case .rarely: "SHORT" + case .sometimes: "SOMETIMES" + case .often: "OFTEN" case .stability: "STABILITY" case .connection: "CONNECTEDNESS" case .growth: "GROWTH" case .vitality: "VITALITY" - case .once: "ONE_TO_TWO_PER_WEEK" - case .twoToThree: "THREE_TO_FOUR_PER_WEEK" - case .fourOrMore: "MORE_THAN_FIVE_PER_WEEK" + case .once: "ONE_PER_WEEK" + case .twoToThree: "TWO_TO_THREE_PER_WEEK" + case .fourOrMore: "MORE_THAN_FOUR_PER_WEEK" case .notSure: "UNKNOWN" } } diff --git a/Projects/Domain/Sources/Protocol/Repository/AuthRepositoryProtocol.swift b/Projects/Domain/Sources/Protocol/Repository/AuthRepositoryProtocol.swift index f0eaba41..212044b2 100644 --- a/Projects/Domain/Sources/Protocol/Repository/AuthRepositoryProtocol.swift +++ b/Projects/Domain/Sources/Protocol/Repository/AuthRepositoryProtocol.swift @@ -25,7 +25,4 @@ public protocol AuthRepositoryProtocol { /// 계정 탈퇴를 진행합니다. func withdraw() async throws - - /// 토큰 재발급을 진행합니다. - func reissueToken() async throws } diff --git a/Projects/Domain/Sources/Protocol/Repository/UserDataRepositoryProtocol.swift b/Projects/Domain/Sources/Protocol/Repository/UserDataRepositoryProtocol.swift index 99f37db7..90229f6c 100644 --- a/Projects/Domain/Sources/Protocol/Repository/UserDataRepositoryProtocol.swift +++ b/Projects/Domain/Sources/Protocol/Repository/UserDataRepositoryProtocol.swift @@ -5,13 +5,12 @@ // Created by 이동현 on 7/20/25. // -/// 유저의 이름, 토큰 등 유저 정보와 관련된 데이터를 가져오는 Repository +/// 유저의 이름 등 유저 정보와 관련된 데이터를 가져오는 Repository public protocol UserDataRepositoryProtocol { - /// 저장한 accessToken을 가져옵니다. - /// - Returns: accessToken - func loadAccessToken() throws -> String - /// 저장한 닉네임을 가져옵니다. /// - Returns: 유저 닉네임 func loadNickname() throws -> String + + /// 토큰 재발급을 진행합니다. + func reissueToken() async -> Bool } diff --git a/Projects/Domain/Sources/Protocol/UseCase/LogoutUseCaseProtocol.swift b/Projects/Domain/Sources/Protocol/UseCase/LogoutUseCaseProtocol.swift index 31277830..2e655857 100644 --- a/Projects/Domain/Sources/Protocol/UseCase/LogoutUseCaseProtocol.swift +++ b/Projects/Domain/Sources/Protocol/UseCase/LogoutUseCaseProtocol.swift @@ -8,8 +8,4 @@ public protocol LogoutUseCaseProtocol { /// 로그아웃을 진행합니다. func logout() async throws - - // TODO: 추후 reissue 어디로 가야 하나 .. - /// 토큰 재발급을 진행합니다. - func reissueToken() async throws } diff --git a/Projects/Domain/Sources/UseCase/Auth/LogoutUseCase.swift b/Projects/Domain/Sources/UseCase/Auth/LogoutUseCase.swift index ca3b4b1b..d0429a40 100644 --- a/Projects/Domain/Sources/UseCase/Auth/LogoutUseCase.swift +++ b/Projects/Domain/Sources/UseCase/Auth/LogoutUseCase.swift @@ -15,8 +15,4 @@ public final class LogoutUseCase: LogoutUseCaseProtocol { public func logout() async throws { try await authRepository.logout() } - - public func reissueToken() async throws { - try await authRepository.reissueToken() - } } diff --git a/Projects/NetworkService/Project.swift b/Projects/NetworkService/Project.swift deleted file mode 100644 index 0d090980..00000000 --- a/Projects/NetworkService/Project.swift +++ /dev/null @@ -1,20 +0,0 @@ -import ProjectDescription - -let project = Project( - name: "NetworkService", - targets: [ - .target( - name: "NetworkService", - destinations: .iOS, - product: .staticFramework, - bundleId: "com.bitnagil.networkservice", - deploymentTargets: .iOS("15.0"), - sources: ["Sources/**"], - resources: [], - dependencies: [ - .project(target: "DataSource", path: "../DataSource"), - .project(target: "Shared", path: "../Shared") - ] - ) - ] -) diff --git a/Projects/NetworkService/Sources/Extension/URLSession+.swift b/Projects/NetworkService/Sources/Extension/URLSession+.swift deleted file mode 100644 index e1fea3c7..00000000 --- a/Projects/NetworkService/Sources/Extension/URLSession+.swift +++ /dev/null @@ -1,10 +0,0 @@ -// -// URLSession+.swift -// NetworkService -// -// Created by 최정인 on 6/27/25. -// - -import Foundation - -extension URLSession: NetworkProviderProtocol { } diff --git a/Projects/NetworkService/Sources/NetworkDependencyAssembler.swift b/Projects/NetworkService/Sources/NetworkDependencyAssembler.swift deleted file mode 100644 index cea229ad..00000000 --- a/Projects/NetworkService/Sources/NetworkDependencyAssembler.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// NetworkDependencyAssembler.swift -// NetworkService -// -// Created by 최정인 on 6/26/25. -// - -import DataSource -import Shared - -public struct NetworkDependencyAssembler: DependencyAssemblerProtocol { - - public init() { } - - public func assemble() { - DIContainer.shared.register(type: NetworkServiceProtocol.self) { _ in - return NetworkService() - } - } -} diff --git a/Projects/NetworkService/Sources/NetworkProviderProtocol.swift b/Projects/NetworkService/Sources/NetworkProviderProtocol.swift deleted file mode 100644 index a6b19542..00000000 --- a/Projects/NetworkService/Sources/NetworkProviderProtocol.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// NetworkProviderProtocol.swift -// NetworkService -// -// Created by 최정인 on 6/27/25. -// - -import Foundation - -public protocol NetworkProviderProtocol { - func data(for urlRequest: URLRequest) async throws -> (Data, URLResponse) -} diff --git a/Projects/Persistence/Project.swift b/Projects/Persistence/Project.swift deleted file mode 100644 index 6e48f674..00000000 --- a/Projects/Persistence/Project.swift +++ /dev/null @@ -1,19 +0,0 @@ -import ProjectDescription - -let project = Project( - name: "Persistence", - targets: [ - .target( - name: "Persistence", - destinations: .iOS, - product: .staticFramework, - bundleId: "com.bitnagil.persistence", - deploymentTargets: .iOS("15.0"), - sources: ["Sources/**"], - dependencies: [ - .project(target: "DataSource", path: "../DataSource"), - .project(target: "Shared", path: "../Shared") - ] - ) - ] -) diff --git a/Projects/Persistence/Sources/PersistenceDependencyAssembler.swift b/Projects/Persistence/Sources/PersistenceDependencyAssembler.swift deleted file mode 100644 index ac630835..00000000 --- a/Projects/Persistence/Sources/PersistenceDependencyAssembler.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// PersistenceDependencyAssembler.swift -// Persistence -// -// Created by 최정인 on 6/26/25. -// - -import DataSource -import Shared - -public struct PersistenceDependencyAssembler: DependencyAssemblerProtocol { - - public init() { } - - public func assemble() { - DIContainer.shared.register(type: KeychainStorageProtocol.self) { _ in - return KeychainStorage() - } - - DIContainer.shared.register(type: UserDefaultsStorageProtocol.self) { _ in - return UserDefaultsStorage() - } - } -} diff --git a/Projects/Persistence/Sources/UserDefaults/UserDefaultsStorage.swift b/Projects/Persistence/Sources/UserDefaults/UserDefaultsStorage.swift deleted file mode 100644 index 0b306071..00000000 --- a/Projects/Persistence/Sources/UserDefaults/UserDefaultsStorage.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// UserDefaultsStorage.swift -// Persistence -// -// Created by 반성준 on 6/23/25. -// - -import DataSource -import Foundation - -public final class UserDefaultsStorage: UserDefaultsStorageProtocol { - private let userDefaults: UserDefaults - - public init(userDefaults: UserDefaults = .standard) { - self.userDefaults = userDefaults - } - - public func save(_ value: Any?, forKey key: String) -> Bool { - userDefaults.set(value, forKey: key) - return userDefaults.object(forKey: key) != nil - } - - public func load(forKey key: String) -> T? { - userDefaults.object(forKey: key) as? T - } - - public func remove(forKey key: String) -> Bool { - userDefaults.removeObject(forKey: key) - return userDefaults.object(forKey: key) == nil - } -} diff --git a/Projects/Presentation/Project.swift b/Projects/Presentation/Project.swift index 587a5d61..c85074f1 100644 --- a/Projects/Presentation/Project.swift +++ b/Projects/Presentation/Project.swift @@ -13,7 +13,6 @@ let project = Project( resources: ["Resources/**"], dependencies: [ .external(name: "SnapKit"), - .external(name: "Then"), .project(target: "Domain", path: "../Domain"), .project(target: "Shared", path: "../Shared") ] diff --git a/Projects/Presentation/Resources/Images.xcassets/IntroGraphic.imageset/Contents.json b/Projects/Presentation/Resources/Images.xcassets/IntroGraphic.imageset/Contents.json new file mode 100644 index 00000000..b561e6e8 --- /dev/null +++ b/Projects/Presentation/Resources/Images.xcassets/IntroGraphic.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "IntroGraphic.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "IntroGraphic@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "IntroGraphic@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Presentation/Resources/Images.xcassets/IntroGraphic.imageset/IntroGraphic.png b/Projects/Presentation/Resources/Images.xcassets/IntroGraphic.imageset/IntroGraphic.png new file mode 100644 index 00000000..d44604fd Binary files /dev/null and b/Projects/Presentation/Resources/Images.xcassets/IntroGraphic.imageset/IntroGraphic.png differ diff --git a/Projects/Presentation/Resources/Images.xcassets/IntroGraphic.imageset/IntroGraphic@2x.png b/Projects/Presentation/Resources/Images.xcassets/IntroGraphic.imageset/IntroGraphic@2x.png new file mode 100644 index 00000000..44035d41 Binary files /dev/null and b/Projects/Presentation/Resources/Images.xcassets/IntroGraphic.imageset/IntroGraphic@2x.png differ diff --git a/Projects/Presentation/Resources/Images.xcassets/IntroGraphic.imageset/IntroGraphic@3x.png b/Projects/Presentation/Resources/Images.xcassets/IntroGraphic.imageset/IntroGraphic@3x.png new file mode 100644 index 00000000..ac0d6da8 Binary files /dev/null and b/Projects/Presentation/Resources/Images.xcassets/IntroGraphic.imageset/IntroGraphic@3x.png differ diff --git a/Projects/Presentation/Sources/Common/Component/CustomBottomSheet.swift b/Projects/Presentation/Sources/Common/Component/CustomBottomSheet.swift index 2ba4ade5..939d0610 100644 --- a/Projects/Presentation/Sources/Common/Component/CustomBottomSheet.swift +++ b/Projects/Presentation/Sources/Common/Component/CustomBottomSheet.swift @@ -54,25 +54,17 @@ final class CustomBottomSheet: UIViewController { } private func configureAttribute() { - dimmedView.do { - $0.backgroundColor = UIColor.black.withAlphaComponent(0.7) - $0.alpha = 0 - } + dimmedView.backgroundColor = UIColor.black.withAlphaComponent(0.7) + dimmedView.alpha = 0 - bottomSheetView.do { - $0.backgroundColor = UIColor.systemBackground - $0.layer.cornerRadius = Layout.bottomSheetCornerRadius - $0.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] - } + bottomSheetView.backgroundColor = UIColor.systemBackground + bottomSheetView.layer.cornerRadius = Layout.bottomSheetCornerRadius + bottomSheetView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] - dragHandle.do { - $0.backgroundColor = UIColor.systemGray4 - $0.layer.cornerRadius = Layout.dragHandleCornerRadius - } - - contentView.do { - $0.backgroundColor = .clear - } + dragHandle.backgroundColor = UIColor.systemGray4 + dragHandle.layer.cornerRadius = Layout.dragHandleCornerRadius + + contentView.backgroundColor = .clear let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissBottomSheet)) dimmedView.addGestureRecognizer(tapGesture) diff --git a/Projects/Presentation/Sources/Common/DesignSystem/BitnagilGraphic.swift b/Projects/Presentation/Sources/Common/DesignSystem/BitnagilGraphic.swift new file mode 100644 index 00000000..3f6e3ee4 --- /dev/null +++ b/Projects/Presentation/Sources/Common/DesignSystem/BitnagilGraphic.swift @@ -0,0 +1,16 @@ +// +// BitnagilGraphic.swift +// Presentation +// +// Created by 최정인 on 7/27/25. +// + +import UIKit + +enum BitnagilGraphic { + private static var bundle: Bundle { + return Bundle(for: IntroView.self) + } + + static let introGraphic = UIImage(named: "IntroGraphic", in: bundle, with: nil) +} diff --git a/Projects/Presentation/Sources/Common/SplashView.swift b/Projects/Presentation/Sources/Common/SplashView.swift new file mode 100644 index 00000000..2d4878fd --- /dev/null +++ b/Projects/Presentation/Sources/Common/SplashView.swift @@ -0,0 +1,33 @@ +// +// SplashView.swift +// Presentation +// +// Created by 최정인 on 7/27/25. +// + +import SnapKit +import UIKit + +public final class SplashView: UIViewController { + + private let splashGraphicView = UIImageView() + + override public func viewDidLoad() { + super.viewDidLoad() + configureAttribute() + configureLayout() + } + + private func configureAttribute() { + splashGraphicView.image = BitnagilGraphic.introGraphic + } + + private func configureLayout() { + view.backgroundColor = .systemBackground + view.addSubview(splashGraphicView) + + splashGraphicView.snp.makeConstraints { make in + make.center.equalToSuperview() + } + } +} diff --git a/Projects/Presentation/Sources/Home/View/HomeView.swift b/Projects/Presentation/Sources/Home/View/HomeView.swift index 90411d0f..3ff73a55 100644 --- a/Projects/Presentation/Sources/Home/View/HomeView.swift +++ b/Projects/Presentation/Sources/Home/View/HomeView.swift @@ -8,7 +8,6 @@ import Combine import Shared import SnapKit -import Then import UIKit final class HomeView: BaseViewController { diff --git a/Projects/Presentation/Sources/Login/View/Component/SocialLoginButton.swift b/Projects/Presentation/Sources/Login/View/Component/SocialLoginButton.swift index 826512b8..7febadbb 100644 --- a/Projects/Presentation/Sources/Login/View/Component/SocialLoginButton.swift +++ b/Projects/Presentation/Sources/Login/View/Component/SocialLoginButton.swift @@ -69,23 +69,17 @@ final class SocialLoginButton: UIButton { backgroundColor = socialType.buttonColor layer.cornerRadius = Layout.cornerRadius - stackView.do { - $0.axis = .horizontal - $0.alignment = .center - $0.spacing = Layout.stackViewSpacing - $0.isUserInteractionEnabled = false - } + stackView.axis = .horizontal + stackView.alignment = .center + stackView.spacing = Layout.stackViewSpacing + stackView.isUserInteractionEnabled = false - iconImageView.do { - $0.image = socialType.iconImage - $0.contentMode = .scaleAspectFit - } + iconImageView.image = socialType.iconImage + iconImageView.contentMode = .scaleAspectFit - buttonLabel.do { - $0.text = socialType.buttonTitle - $0.textColor = socialType.buttonTextColor - $0.font = BitnagilFont(style: .button2, weight: .semiBold).font - } + buttonLabel.text = socialType.buttonTitle + buttonLabel.textColor = socialType.buttonTextColor + buttonLabel.font = BitnagilFont(style: .button2, weight: .semiBold).font } private func configureLayout() { diff --git a/Projects/Presentation/Sources/Login/View/Component/TermsAgreementItemView.swift b/Projects/Presentation/Sources/Login/View/Component/TermsAgreementItemView.swift index e2d56814..da572b5a 100644 --- a/Projects/Presentation/Sources/Login/View/Component/TermsAgreementItemView.swift +++ b/Projects/Presentation/Sources/Login/View/Component/TermsAgreementItemView.swift @@ -47,31 +47,25 @@ final class TermsAgreementItemView: UIView { } private func configureAttribute() { - checkButton.do { - $0.setImage(BitnagilIcon.checkIcon, for: .normal) - $0.tintColor = BitnagilColor.navy100 - $0.addAction(UIAction { [weak self] _ in - guard let self else { return } - self.isAgreed.toggle() - self.delegate?.termsAgreementItemView(self, didToggleCheckFor: self.termType) - }, for: .touchUpInside) - } - - agreementLable.do { - $0.attributedText = BitnagilFont(style: .body2, weight: .medium).attributedString(text: termType.title) - $0.textColor = BitnagilColor.gray50 - } - - moreButton.do { - let title = BitnagilFont(style: .captionUnderline1, weight: .semiBold).attributedString(text: "더보기") - $0.setAttributedTitle(title, for: .normal) - $0.setTitleColor(BitnagilColor.gray50, for: .normal) - $0.isHidden = termType.link == nil - $0.addAction(UIAction { [weak self] _ in - guard let self else { return } - self.delegate?.termsAgreementItemView(self, didTapMoreButtonFor: self.termType) - }, for: .touchUpInside) - } + checkButton.setImage(BitnagilIcon.checkIcon, for: .normal) + checkButton.tintColor = BitnagilColor.navy100 + checkButton.addAction(UIAction { [weak self] _ in + guard let self else { return } + self.isAgreed.toggle() + self.delegate?.termsAgreementItemView(self, didToggleCheckFor: self.termType) + }, for: .touchUpInside) + + agreementLable.attributedText = BitnagilFont(style: .body2, weight: .medium).attributedString(text: termType.title) + agreementLable.textColor = BitnagilColor.gray50 + + let title = BitnagilFont(style: .captionUnderline1, weight: .semiBold).attributedString(text: "더보기") + moreButton.setAttributedTitle(title, for: .normal) + moreButton.setTitleColor(BitnagilColor.gray50, for: .normal) + moreButton.isHidden = termType.link == nil + moreButton.addAction(UIAction { [weak self] _ in + guard let self else { return } + self.delegate?.termsAgreementItemView(self, didTapMoreButtonFor: self.termType) + }, for: .touchUpInside) } private func configureLayout() { diff --git a/Projects/Presentation/Sources/Login/View/Component/TotalAgreementButton.swift b/Projects/Presentation/Sources/Login/View/Component/TotalAgreementButton.swift index d41a1c53..f00eed74 100644 --- a/Projects/Presentation/Sources/Login/View/Component/TotalAgreementButton.swift +++ b/Projects/Presentation/Sources/Login/View/Component/TotalAgreementButton.swift @@ -41,24 +41,18 @@ final class TotalAgreementButton: UIButton { backgroundColor = BitnagilColor.gray99 layer.cornerRadius = Layout.cornerRadius - stackView.do { - $0.axis = .horizontal - $0.alignment = .center - $0.spacing = Layout.stackViewSpacing - $0.isUserInteractionEnabled = false - } - - checkButton.do { - $0.image = BitnagilIcon.checkIcon - $0.tintColor = BitnagilColor.navy100 - $0.contentMode = .scaleAspectFit - } - - buttonLabel.do { - $0.text = "전체동의" - $0.textColor = BitnagilColor.gray50 - $0.font = BitnagilFont(style: .subtitle1, weight: .semiBold).font - } + stackView.axis = .horizontal + stackView.alignment = .center + stackView.spacing = Layout.stackViewSpacing + stackView.isUserInteractionEnabled = false + + checkButton.image = BitnagilIcon.checkIcon + checkButton.tintColor = BitnagilColor.navy100 + checkButton.contentMode = .scaleAspectFit + + buttonLabel.text = "전체동의" + buttonLabel.textColor = BitnagilColor.gray50 + buttonLabel.font = BitnagilFont(style: .subtitle1, weight: .semiBold).font } private func configureLayout() { diff --git a/Projects/Presentation/Sources/Login/View/IntroView.swift b/Projects/Presentation/Sources/Login/View/IntroView.swift index 1d54fe16..6ac31676 100644 --- a/Projects/Presentation/Sources/Login/View/IntroView.swift +++ b/Projects/Presentation/Sources/Login/View/IntroView.swift @@ -7,7 +7,6 @@ import Shared import SnapKit -import Then import UIKit public final class IntroView: UIViewController { @@ -15,14 +14,16 @@ public final class IntroView: UIViewController { private enum Layout { static let horizontalMargin: CGFloat = 20 static let labelTopSpacing: CGFloat = 54 - static let graphViewTopSpacing: CGFloat = 38 + static let graphViewTopSpacing: CGFloat = 118 static let graphViewBottomSpacing: CGFloat = 64 + static let graphViewHeight: CGFloat = 295 + static let graphViewWidth: CGFloat = 257 static let startButtonBottomSpacing: CGFloat = 20 static let startButtonHeight: CGFloat = 54 } private let introLabel = UILabel() - private let graphView = UIView() + private let graphView = UIImageView() private let startButton = PrimaryButton(buttonState: .default, buttonTitle: "시작하기") public override func viewDidLoad() { @@ -37,17 +38,13 @@ public final class IntroView: UIViewController { } private func configureAttribute() { - introLabel.do { - let text = "당신의 하루 리듬을 이해하고,\n작은 변화를 함께 시작해볼게요." - $0.attributedText = BitnagilFont(style: .title2, weight: .bold).attributedString(text: text) - $0.textAlignment = .center - $0.textColor = BitnagilColor.navy500 - $0.numberOfLines = 2 - } + let text = "당신의 하루 리듬을 이해하고,\n작은 변화를 함께 시작해볼게요." + introLabel.attributedText = BitnagilFont(style: .title2, weight: .bold).attributedString(text: text) + introLabel.textAlignment = .center + introLabel.textColor = BitnagilColor.navy500 + introLabel.numberOfLines = 2 - graphView.do { - $0.backgroundColor = BitnagilColor.gray90 - } + graphView.image = BitnagilGraphic.introGraphic startButton.addAction(UIAction { [weak self] _ in guard let loginViewModel = DIContainer.shared.resolve(type: LoginViewModel.self) else { @@ -70,13 +67,14 @@ public final class IntroView: UIViewController { make.leading.equalTo(safeArea).offset(Layout.horizontalMargin) make.trailing.equalTo(safeArea).inset(Layout.horizontalMargin) make.top.equalTo(safeArea).offset(Layout.labelTopSpacing) + make.height.equalTo(60) } graphView.snp.makeConstraints { make in - make.leading.equalTo(safeArea).offset(Layout.horizontalMargin) - make.trailing.equalTo(safeArea).inset(Layout.horizontalMargin) make.top.equalTo(introLabel.snp.bottom).offset(Layout.graphViewTopSpacing) - make.bottom.equalTo(startButton.snp.top).offset(-Layout.graphViewBottomSpacing) + make.bottom.equalTo(startButton.snp.top).offset(-133) + make.leading.equalTo(safeArea).offset(53) + make.trailing.equalTo(safeArea).inset(65) } startButton.snp.makeConstraints { make in diff --git a/Projects/Presentation/Sources/Login/View/LoginView.swift b/Projects/Presentation/Sources/Login/View/LoginView.swift index 56b0ad39..fbac8fdb 100644 --- a/Projects/Presentation/Sources/Login/View/LoginView.swift +++ b/Projects/Presentation/Sources/Login/View/LoginView.swift @@ -9,7 +9,6 @@ import AuthenticationServices import Combine import Shared import SnapKit -import Then import UIKit final class LoginView: BaseViewController { @@ -47,9 +46,7 @@ final class LoginView: BaseViewController { } override func configureAttribute() { - logoView.do { - $0.backgroundColor = BitnagilColor.gray90 - } + logoView.backgroundColor = BitnagilColor.gray90 kakaoLoginButton.addAction(UIAction { [weak self] _ in self?.viewModel.action(input: .kakaoLogin) diff --git a/Projects/Presentation/Sources/Login/View/TermsAgreementView.swift b/Projects/Presentation/Sources/Login/View/TermsAgreementView.swift index 9797981d..62bf0b77 100644 --- a/Projects/Presentation/Sources/Login/View/TermsAgreementView.swift +++ b/Projects/Presentation/Sources/Login/View/TermsAgreementView.swift @@ -10,7 +10,6 @@ import Domain import SafariServices import Shared import SnapKit -import Then import UIKit final class TermsAgreementView: BaseViewController { @@ -51,13 +50,11 @@ final class TermsAgreementView: BaseViewController { } override func configureAttribute() { - agreementLabel.do { - let text = "빛나길 이용을 위해\n필수 약관에 동의해 주세요." - $0.attributedText = BitnagilFont(style: .title2, weight: .bold).attributedString(text: text) - $0.textAlignment = .left - $0.numberOfLines = 2 - $0.textColor = BitnagilColor.navy500 - } + let text = "빛나길 이용을 위해\n필수 약관에 동의해 주세요." + agreementLabel.attributedText = BitnagilFont(style: .title2, weight: .bold).attributedString(text: text) + agreementLabel.textAlignment = .left + agreementLabel.numberOfLines = 2 + agreementLabel.textColor = BitnagilColor.navy500 totalAgreementButton.addAction(UIAction { [weak self] _ in self?.viewModel.action(input: .toggleTotalAgreement) diff --git a/Projects/Presentation/Sources/Onboarding/View/Component/OnboardingChoiceButton.swift b/Projects/Presentation/Sources/Onboarding/View/Component/OnboardingChoiceButton.swift index 4a8ce792..8eb411f3 100644 --- a/Projects/Presentation/Sources/Onboarding/View/Component/OnboardingChoiceButton.swift +++ b/Projects/Presentation/Sources/Onboarding/View/Component/OnboardingChoiceButton.swift @@ -45,32 +45,27 @@ final class OnboardingChoiceButton: UIButton { layer.borderWidth = 1 layer.cornerRadius = Layout.cornerRadius - stackView.do { - $0.axis = .vertical - $0.alignment = .leading - $0.spacing = Layout.stackViewSpacing - $0.isUserInteractionEnabled = false - } + stackView.axis = .vertical + stackView.alignment = .leading + stackView.spacing = Layout.stackViewSpacing + stackView.isUserInteractionEnabled = false guard let subTitle = onboardingChoice.subTitle else { - mainLabel.do { - $0.text = onboardingChoice.mainTitle - $0.font = BitnagilFont(style: .body1, weight: .regular).font - $0.textColor = BitnagilColor.gray50 - } + mainLabel.text = onboardingChoice.mainTitle + mainLabel.font = BitnagilFont(style: .body1, weight: .regular).font + mainLabel.textColor = BitnagilColor.gray50 return } - mainLabel.do { - $0.text = onboardingChoice.mainTitle - $0.font = BitnagilFont(style: .subtitle1, weight: .semiBold).font - $0.textColor = BitnagilColor.gray50 - } + mainLabel.text = onboardingChoice.mainTitle + mainLabel.font = BitnagilFont(style: .subtitle1, weight: .semiBold).font + mainLabel.textColor = BitnagilColor.gray50 - subLabel = UILabel().then { - $0.text = subTitle - $0.font = BitnagilFont(style: .body2, weight: .regular).font - $0.textColor = BitnagilColor.gray50 + subLabel = UILabel() + if let subLabel { + subLabel.text = subTitle + subLabel.font = BitnagilFont(style: .body2, weight: .regular).font + subLabel.textColor = BitnagilColor.gray50 } } diff --git a/Projects/Presentation/Sources/Onboarding/View/Component/ProgressBarView.swift b/Projects/Presentation/Sources/Onboarding/View/Component/ProgressBarView.swift index 74d1f655..4efb840a 100644 --- a/Projects/Presentation/Sources/Onboarding/View/Component/ProgressBarView.swift +++ b/Projects/Presentation/Sources/Onboarding/View/Component/ProgressBarView.swift @@ -32,17 +32,13 @@ final class ProgressBarView: UIView { } private func configureAttribute() { - backgroundView.do { - $0.frame = bounds - $0.backgroundColor = .white - $0.layer.cornerRadius = Layout.barHeight / 2 - $0.layer.masksToBounds = true - } + backgroundView.frame = bounds + backgroundView.backgroundColor = .white + backgroundView.layer.cornerRadius = Layout.barHeight / 2 + backgroundView.layer.masksToBounds = true - progressView.do { - $0.layer.cornerRadius = Layout.barHeight / 2 - $0.layer.masksToBounds = true - } + progressView.layer.cornerRadius = Layout.barHeight / 2 + progressView.layer.masksToBounds = true } private func configureLayout(step: Int, stepCount: Int) { diff --git a/Projects/Presentation/Sources/Onboarding/View/OnboardingRecommendedRoutineView.swift b/Projects/Presentation/Sources/Onboarding/View/OnboardingRecommendedRoutineView.swift index 0dab1d53..c6cb7e1b 100644 --- a/Projects/Presentation/Sources/Onboarding/View/OnboardingRecommendedRoutineView.swift +++ b/Projects/Presentation/Sources/Onboarding/View/OnboardingRecommendedRoutineView.swift @@ -22,6 +22,8 @@ final class OnboardingRecommendedRoutineView: BaseViewController { } override func configureAttribute() { - mainLabel.do { - let text = "이제 당신에게\n꼭 맞는 루틴을 제안해드릴게요." - $0.attributedText = BitnagilFont(style: .title2, weight: .bold).attributedString(text: text) - $0.textColor = BitnagilColor.navy500 - $0.numberOfLines = 2 - $0.textAlignment = .left - } - - subLabel.do { - $0.text = "당신은 지금" - $0.font = BitnagilFont(style: .body2, weight: .medium).font - $0.textColor = BitnagilColor.gray30 - } - - resultStackView.do { - $0.axis = .vertical - $0.spacing = Layout.resultStackViewSpacing - } + let text = "이제 당신에게\n꼭 맞는 루틴을 제안해드릴게요." + mainLabel.attributedText = BitnagilFont(style: .title2, weight: .bold).attributedString(text: text) + mainLabel.textColor = BitnagilColor.navy500 + mainLabel.numberOfLines = 2 + mainLabel.textAlignment = .left + + subLabel.text = "당신은 지금" + subLabel.font = BitnagilFont(style: .body2, weight: .medium).font + subLabel.textColor = BitnagilColor.gray30 + + resultStackView.axis = .vertical + resultStackView.spacing = Layout.resultStackViewSpacing [timeResultLabel, feelingResultLabel, outdoorResultLabel].forEach { label in - label.do { - $0.textColor = BitnagilColor.gray30 - } - } - graphicView.do { - $0.backgroundColor = BitnagilColor.gray90 + label.textColor = BitnagilColor.gray30 } + + graphicView.backgroundColor = BitnagilColor.gray90 } override func configureLayout() { @@ -180,22 +171,16 @@ final class OnboardingResultView: BaseViewController { baseText = "" } - timeResultLabel.do { - $0.attributedText = NSAttributedString.highlighted(text: baseText, highlightText: timeResult) - } + timeResultLabel.attributedText = NSAttributedString.highlighted(text: baseText, highlightText: timeResult) } private func updateFeelingResultLabel(feelingResult: String) { let baseText = "• \(feelingResult)을 원하는 중이에요" - feelingResultLabel.do { - $0.attributedText = NSAttributedString.highlighted(text: baseText, highlightText: feelingResult) - } + feelingResultLabel.attributedText = NSAttributedString.highlighted(text: baseText, highlightText: feelingResult) } private func updateOutdoorResultLabel(outdoorResult: String) { let baseText = "• \(outdoorResult)을 목표로 해볼게요!" - outdoorResultLabel.do { - $0.attributedText = NSAttributedString.highlighted(text: baseText, highlightText: outdoorResult) - } + outdoorResultLabel.attributedText = NSAttributedString.highlighted(text: baseText, highlightText: outdoorResult) } } diff --git a/Projects/Presentation/Sources/Onboarding/View/OnboardingView.swift b/Projects/Presentation/Sources/Onboarding/View/OnboardingView.swift index afc3c970..e801cf74 100644 --- a/Projects/Presentation/Sources/Onboarding/View/OnboardingView.swift +++ b/Projects/Presentation/Sources/Onboarding/View/OnboardingView.swift @@ -61,26 +61,23 @@ final class OnboardingView: BaseViewController { } override func configureAttribute() { - mainLabel.do { - $0.attributedText = BitnagilFont(style: .title2, weight: .bold).attributedString(text: onboarding.mainTitle) - $0.textColor = BitnagilColor.navy500 - $0.numberOfLines = 2 - $0.textAlignment = .left - } + mainLabel.attributedText = BitnagilFont(style: .title2, weight: .bold).attributedString(text: onboarding.mainTitle) + mainLabel.textColor = BitnagilColor.navy500 + mainLabel.numberOfLines = 2 + mainLabel.textAlignment = .left if let subTitle = onboarding.subTitle { - subLabel = UILabel().then { - $0.attributedText = BitnagilFont(style: .body2, weight: .medium).attributedString(text: subTitle) - $0.textColor = BitnagilColor.gray50 - $0.numberOfLines = 2 - $0.textAlignment = .left + subLabel = UILabel() + if let subLabel { + subLabel.attributedText = BitnagilFont(style: .body2, weight: .medium).attributedString(text: subTitle) + subLabel.textColor = BitnagilColor.gray50 + subLabel.numberOfLines = 2 + subLabel.textAlignment = .left } } - choiceStackView.do { - $0.axis = .vertical - $0.spacing = Layout.choiceStackViewSpacing - } + choiceStackView.axis = .vertical + choiceStackView.spacing = Layout.choiceStackViewSpacing for (index, choice) in onboarding.choices.enumerated() { let choiceButton = OnboardingChoiceButton(onboardingChoice: choice) diff --git a/Projects/Presentation/Sources/RecommendedRoutine/View/Component/RecommendedRoutineCardView.swift b/Projects/Presentation/Sources/RecommendedRoutine/View/Component/RecommendedRoutineCardView.swift index 786da57e..b2e569d2 100644 --- a/Projects/Presentation/Sources/RecommendedRoutine/View/Component/RecommendedRoutineCardView.swift +++ b/Projects/Presentation/Sources/RecommendedRoutine/View/Component/RecommendedRoutineCardView.swift @@ -46,33 +46,25 @@ final class RecommendedRoutineCardView: UIView { backgroundColor = BitnagilColor.lightBlue75 layer.cornerRadius = Layout.cornerRadius - labelStackView.do { - $0.axis = .vertical - $0.spacing = Layout.stackViewSpacing - } - - mainLabel.do { - $0.text = recommendedRoutine.mainTitle - $0.font = BitnagilFont(style: .body1, weight: .semiBold).font - $0.textColor = BitnagilColor.navy500 - } - - subLabel.do { - $0.text = recommendedRoutine.subTitle - $0.font = BitnagilFont(style: .body2, weight: .regular).font - $0.textColor = BitnagilColor.navy300 - } - - plusButton.do { - let plusIcon = BitnagilIcon.plusIcon? - .resizeAspectFit(to: CGSize(width: Layout.plusImageSize, height: Layout.plusImageSize)) - $0.setImage(plusIcon, for: .normal) - $0.tintColor = BitnagilColor.navy500 - $0.addAction(UIAction { [weak self] _ in - guard let self else { return } - self.delegate?.recommendedRoutineCardView(self, didTapRecommendedRoutine: recommendedRoutine) - }, for: .touchUpInside) - } + labelStackView.axis = .vertical + labelStackView.spacing = Layout.stackViewSpacing + + mainLabel.text = recommendedRoutine.mainTitle + mainLabel.font = BitnagilFont(style: .body1, weight: .semiBold).font + mainLabel.textColor = BitnagilColor.navy500 + + subLabel.text = recommendedRoutine.subTitle + subLabel.font = BitnagilFont(style: .body2, weight: .regular).font + subLabel.textColor = BitnagilColor.navy300 + + let plusIcon = BitnagilIcon.plusIcon? + .resizeAspectFit(to: CGSize(width: Layout.plusImageSize, height: Layout.plusImageSize)) + plusButton.setImage(plusIcon, for: .normal) + plusButton.tintColor = BitnagilColor.navy500 + plusButton.addAction(UIAction { [weak self] _ in + guard let self else { return } + self.delegate?.recommendedRoutineCardView(self, didTapRecommendedRoutine: recommendedRoutine) + }, for: .touchUpInside) } private func configureLayout() { diff --git a/Projects/Presentation/Sources/RecommendedRoutine/View/Component/RegisterEmotionButton.swift b/Projects/Presentation/Sources/RecommendedRoutine/View/Component/RegisterEmotionButton.swift index 41db9065..b0a7ea56 100644 --- a/Projects/Presentation/Sources/RecommendedRoutine/View/Component/RegisterEmotionButton.swift +++ b/Projects/Presentation/Sources/RecommendedRoutine/View/Component/RegisterEmotionButton.swift @@ -37,19 +37,15 @@ final class RegisterEmotionButton: UIButton { layer.cornerRadius = Layout.cornerRadius layer.borderColor = BitnagilColor.navy100?.cgColor - plusIcon.do { - $0.contentMode = .center - $0.image = BitnagilIcon.plusIcon? - .resizeAspectFit(to: CGSize(width: Layout.plusIconImageSize, height: Layout.plusIconImageSize))? - .withRenderingMode(.alwaysTemplate) - $0.tintColor = BitnagilColor.navy400 - } + plusIcon.contentMode = .center + plusIcon.image = BitnagilIcon.plusIcon? + .resizeAspectFit(to: CGSize(width: Layout.plusIconImageSize, height: Layout.plusIconImageSize))? + .withRenderingMode(.alwaysTemplate) + plusIcon.tintColor = BitnagilColor.navy400 - buttonLabel.do { - $0.text = "오늘의 감정 루틴 추천받기" - $0.font = BitnagilFont(style: .body2, weight: .medium).font - $0.textColor = BitnagilColor.navy400 - } + buttonLabel.text = "오늘의 감정 루틴 추천받기" + buttonLabel.font = BitnagilFont(style: .body2, weight: .medium).font + buttonLabel.textColor = BitnagilColor.navy400 } private func configureLayout() { diff --git a/Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineCategoryButton.swift b/Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineCategoryButton.swift index 8d09a4c1..9961c5f5 100644 --- a/Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineCategoryButton.swift +++ b/Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineCategoryButton.swift @@ -34,11 +34,9 @@ final class RoutineCategoryButton: UIButton { private func configureAttribute() { backgroundColor = BitnagilColor.gray99 - categoryLabel.do { - $0.text = routineCategory.title - $0.font = BitnagilFont(style: .caption1, weight: .regular).font - $0.textColor = BitnagilColor.navy100 - } + categoryLabel.text = routineCategory.title + categoryLabel.font = BitnagilFont(style: .caption1, weight: .regular).font + categoryLabel.textColor = BitnagilColor.navy100 } private func configureLayout() { diff --git a/Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineCategoryView.swift b/Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineCategoryView.swift index 3f745d36..56527e6f 100644 --- a/Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineCategoryView.swift +++ b/Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineCategoryView.swift @@ -38,10 +38,8 @@ final class RoutineCategoryView: UIView { private func configureAttribute() { scrollView.showsHorizontalScrollIndicator = false - buttonStackView.do { - $0.axis = .horizontal - $0.spacing = Layout.stackViewSpacing - } + buttonStackView.axis = .horizontal + buttonStackView.spacing = Layout.stackViewSpacing RoutineCategoryType.allCases.sorted(by: { $0.id < $1.id }).forEach { type in let button = RoutineCategoryButton(category: type) diff --git a/Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineLevelButton.swift b/Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineLevelButton.swift index 5375ec84..b11d05b0 100644 --- a/Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineLevelButton.swift +++ b/Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineLevelButton.swift @@ -37,26 +37,20 @@ final class RoutineLevelButton: UIButton { } private func configureAttribute() { - stackView.do { - $0.isUserInteractionEnabled = false - $0.axis = .horizontal - $0.spacing = Layout.stackViewSpacing - } + stackView.isUserInteractionEnabled = false + stackView.axis = .horizontal + stackView.spacing = Layout.stackViewSpacing - buttonLabel.do { - $0.text = level?.title ?? "난이도 선택" - $0.font = BitnagilFont(style: .body2, weight: .medium).font - $0.textColor = BitnagilColor.gray60 - } + buttonLabel.text = level?.title ?? "난이도 선택" + buttonLabel.font = BitnagilFont(style: .body2, weight: .medium).font + buttonLabel.textColor = BitnagilColor.gray60 - chevronIcon.do { - $0.image = BitnagilIcon - .chevronIcon(direction: .down)? - .resizeAspectFit(to: CGSize(width: Layout.chevronImageSize, height: Layout.chevronImageSize))? - .withRenderingMode(.alwaysTemplate) - $0.contentMode = .center - $0.tintColor = BitnagilColor.gray60 - } + chevronIcon.image = BitnagilIcon + .chevronIcon(direction: .down)? + .resizeAspectFit(to: CGSize(width: Layout.chevronImageSize, height: Layout.chevronImageSize))? + .withRenderingMode(.alwaysTemplate) + chevronIcon.contentMode = .center + chevronIcon.tintColor = BitnagilColor.gray60 } private func configureLayout() { diff --git a/Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineView.swift b/Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineView.swift index 530008a8..af0f2013 100644 --- a/Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineView.swift +++ b/Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineView.swift @@ -56,15 +56,11 @@ final class RecommendedRoutineView: BaseViewController