diff --git a/App/Sources/Application/DI/AppComponent.swift b/App/Sources/Application/DI/AppComponent.swift new file mode 100644 index 0000000..ea8d3a5 --- /dev/null +++ b/App/Sources/Application/DI/AppComponent.swift @@ -0,0 +1,29 @@ +import NeedleFoundation +import Service +import SwiftUI + +public final class AppComponent: BootstrapComponent { + public func makeRootView() -> some View { + rootComponent.makeView() + } + + var rootComponent: RootComponent { + RootComponent(parent: self) + } + + public var keychain: Keychain { + shared { + KeychainImpl() + } + } +} + +public extension AppComponent { + var signinFactory: any SigninFactory { + SigninComponent(parent: self) + } + + var mainFactory: any MainFactory { + MainComponent(parent: self) + } +} diff --git a/App/Sources/Application/DI/Auth/AppComponent+Auth.swift b/App/Sources/Application/DI/Auth/AppComponent+Auth.swift new file mode 100644 index 0000000..865f1ef --- /dev/null +++ b/App/Sources/Application/DI/Auth/AppComponent+Auth.swift @@ -0,0 +1,37 @@ +import NeedleFoundation +import Service + +public extension AppComponent { + var localAuthDataSource: any LocalAuthDataSource { + shared { + LocalAuthDataSourceImpl(keychain: keychain) + } + } + + var remoteAuthDataSource: any RemoteAuthDataSource { + shared { + RemoteAuthDataSourceImpl(keychain: keychain) + } + } + + var authRepository: any AuthRepository { + shared { + AuthRepositoryImpl( + remoteAuthDataSource: remoteAuthDataSource, + localAuthDataSource: localAuthDataSource + ) + } + } + + var loginUseCase: any LoginUseCase { + shared { + LoginUseCaseImpl(authRepository: authRepository) + } + } + + var logoutUseCase: any LogoutUseCase { + shared { + LogoutUseCaseImpl(authRepository: authRepository) + } + } +} diff --git a/App/Sources/Application/MindWayApp.swift b/App/Sources/Application/MindWayApp.swift index 5f219c3..bd462d5 100644 --- a/App/Sources/Application/MindWayApp.swift +++ b/App/Sources/Application/MindWayApp.swift @@ -4,11 +4,17 @@ import Service @main struct MindWayApp: App { @State private var showMainView = false + @StateObject private var sceneState = SceneState(sceneFlow: .login) + + init() { + registerProviderFactories() + } var body: some Scene { WindowGroup { if showMainView { - TabBarView(viewModel: MyPageViewModel()) + AppComponent().makeRootView() + .environmentObject(sceneState) } else { SplashView() .onAppear { diff --git a/App/Sources/Application/NeedleGenerated.swift b/App/Sources/Application/NeedleGenerated.swift new file mode 100644 index 0000000..e6e91be --- /dev/null +++ b/App/Sources/Application/NeedleGenerated.swift @@ -0,0 +1,118 @@ + + +import NeedleFoundation +import Service +import SwiftUI + +// swiftlint:disable unused_declaration +private let needleDependenciesHash : String? = nil + +// MARK: - Traversal Helpers + +private func parent1(_ component: NeedleFoundation.Scope) -> NeedleFoundation.Scope { + return component.parent +} + +// MARK: - Providers + +#if !NEEDLE_DYNAMIC + +private class MainDependency7c6a5b4738b211b8e155Provider: MainDependency { + + + init() { + + } +} +/// ^->AppComponent->MainComponent +private func factoryc9274e46e78e70f29c54e3b0c44298fc1c149afb(_ component: NeedleFoundation.Scope) -> AnyObject { + return MainDependency7c6a5b4738b211b8e155Provider() +} +private class RootDependency3944cc797a4a88956fb5Provider: RootDependency { + var signinFactory: any SigninFactory { + return appComponent.signinFactory + } + private let appComponent: AppComponent + init(appComponent: AppComponent) { + self.appComponent = appComponent + } +} +/// ^->AppComponent->RootComponent +private func factory264bfc4d4cb6b0629b40f47b58f8f304c97af4d5(_ component: NeedleFoundation.Scope) -> AnyObject { + return RootDependency3944cc797a4a88956fb5Provider(appComponent: parent1(component) as! AppComponent) +} +private class SigninDependencyde06a9d0b22764487733Provider: SigninDependency { + var loginUseCase: any LoginUseCase { + return appComponent.loginUseCase + } + var mainFactory: any MainFactory { + return appComponent.mainFactory + } + private let appComponent: AppComponent + init(appComponent: AppComponent) { + self.appComponent = appComponent + } +} +/// ^->AppComponent->SigninComponent +private func factory2882a056d84a613debccf47b58f8f304c97af4d5(_ component: NeedleFoundation.Scope) -> AnyObject { + return SigninDependencyde06a9d0b22764487733Provider(appComponent: parent1(component) as! AppComponent) +} + +#else +extension MainComponent: Registration { + public func registerItems() { + + } +} +extension RootComponent: Registration { + public func registerItems() { + keyPathToName[\RootDependency.signinFactory] = "signinFactory-any SigninFactory" + } +} +extension SigninComponent: Registration { + public func registerItems() { + keyPathToName[\SigninDependency.loginUseCase] = "loginUseCase-any LoginUseCase" + keyPathToName[\SigninDependency.mainFactory] = "mainFactory-any MainFactory" + } +} +extension AppComponent: Registration { + public func registerItems() { + + localTable["keychain-Keychain"] = { [unowned self] in self.keychain as Any } + localTable["localAuthDataSource-any LocalAuthDataSource"] = { [unowned self] in self.localAuthDataSource as Any } + localTable["remoteAuthDataSource-any RemoteAuthDataSource"] = { [unowned self] in self.remoteAuthDataSource as Any } + localTable["authRepository-any AuthRepository"] = { [unowned self] in self.authRepository as Any } + localTable["loginUseCase-any LoginUseCase"] = { [unowned self] in self.loginUseCase as Any } + localTable["logoutUseCase-any LogoutUseCase"] = { [unowned self] in self.logoutUseCase as Any } + localTable["signinFactory-any SigninFactory"] = { [unowned self] in self.signinFactory as Any } + localTable["mainFactory-any MainFactory"] = { [unowned self] in self.mainFactory as Any } + } +} + + +#endif + +private func factoryEmptyDependencyProvider(_ component: NeedleFoundation.Scope) -> AnyObject { + return EmptyDependencyProvider(component: component) +} + +// MARK: - Registration +private func registerProviderFactory(_ componentPath: String, _ factory: @escaping (NeedleFoundation.Scope) -> AnyObject) { + __DependencyProviderRegistry.instance.registerDependencyProviderFactory(for: componentPath, factory) +} + +#if !NEEDLE_DYNAMIC + +@inline(never) private func register1() { + registerProviderFactory("^->AppComponent->MainComponent", factoryc9274e46e78e70f29c54e3b0c44298fc1c149afb) + registerProviderFactory("^->AppComponent->RootComponent", factory264bfc4d4cb6b0629b40f47b58f8f304c97af4d5) + registerProviderFactory("^->AppComponent->SigninComponent", factory2882a056d84a613debccf47b58f8f304c97af4d5) + registerProviderFactory("^->AppComponent", factoryEmptyDependencyProvider) +} +#endif + +public func registerProviderFactories() { +#if !NEEDLE_DYNAMIC + register1() +#endif +} diff --git a/App/Sources/DesignSystem/Extensions/View+eraseToAnyView.swift b/App/Sources/DesignSystem/Extensions/View+eraseToAnyView.swift new file mode 100644 index 0000000..0921b34 --- /dev/null +++ b/App/Sources/DesignSystem/Extensions/View+eraseToAnyView.swift @@ -0,0 +1,7 @@ +import SwiftUI + +public extension View { + func eraseToAnyView() -> AnyView { + AnyView(self) + } +} diff --git a/App/Sources/Feature/BaseFeature/BaseViewModel.swift b/App/Sources/Feature/BaseFeature/BaseViewModel.swift new file mode 100644 index 0000000..0a2a673 --- /dev/null +++ b/App/Sources/Feature/BaseFeature/BaseViewModel.swift @@ -0,0 +1,31 @@ +import UIKit + +open class BaseViewModel: ObservableObject { + @Published public var isErrorOccurred = false + @Published public var isLoading = false + @Published public var errorMessage = "" + + public init() {} + + public func addCancellable( + _ task: @escaping @Sendable () async throws -> T, + onReceiveValue: @escaping (T) -> Void, + onReceiveError: ((Error) -> Void)? = nil + ) { + isLoading = true + Task { + do { + let value = try await task() + onReceiveValue(value) + } catch { + if let onReceiveError { + onReceiveError(error) + } + + errorMessage = error.localizedDescription + isErrorOccurred = true + } + isLoading = false + } + } +} diff --git a/App/Sources/Feature/BaseFeature/Flow/SceneFlow.swift b/App/Sources/Feature/BaseFeature/Flow/SceneFlow.swift new file mode 100644 index 0000000..052f09c --- /dev/null +++ b/App/Sources/Feature/BaseFeature/Flow/SceneFlow.swift @@ -0,0 +1,5 @@ +import Foundation + +public enum SceneFlow: String, RawRepresentable { + case login +} diff --git a/App/Sources/Feature/BaseFeature/Flow/TabFlow.swift b/App/Sources/Feature/BaseFeature/Flow/TabFlow.swift new file mode 100644 index 0000000..c5f7588 --- /dev/null +++ b/App/Sources/Feature/BaseFeature/Flow/TabFlow.swift @@ -0,0 +1,8 @@ +import Foundation + +public enum TabFlow: String, RawRepresentable { + case home + case event + case book + case my +} diff --git a/App/Sources/Feature/BaseFeature/Sources/SceneState.swift b/App/Sources/Feature/BaseFeature/Sources/SceneState.swift new file mode 100644 index 0000000..bbfe5ce --- /dev/null +++ b/App/Sources/Feature/BaseFeature/Sources/SceneState.swift @@ -0,0 +1,9 @@ +import Foundation + +public final class SceneState: ObservableObject { + @Published public var sceneFlow: SceneFlow + + init(sceneFlow: SceneFlow) { + self.sceneFlow = sceneFlow + } +} diff --git a/App/Sources/Feature/LoginFeature/LoginView.swift b/App/Sources/Feature/LoginFeature/LoginView.swift deleted file mode 100644 index cd7dee6..0000000 --- a/App/Sources/Feature/LoginFeature/LoginView.swift +++ /dev/null @@ -1,31 +0,0 @@ -import SwiftUI - -struct LoginView: View { - var body: some View { - VStack { - MindWayAsset.Images.mindWay.swiftUIImage - .padding(.bottom, 334) - - Button { - #warning("나중에 로그인 추가하기") - } label: { - HStack(spacing: 8) { - Spacer() - - MindWayAsset.Images.gauthLogo.swiftUIImage - - MindWayAsset.Images.signIn.swiftUIImage - - Spacer() - } - .padding(.horizontal, 10) - .padding(.vertical, 16) - .overlay { - RoundedRectangle(cornerRadius: 8) - .stroke(Color.mindway(.main(.main))) - } - } - .padding(.horizontal, 24) - } - } -} diff --git a/App/Sources/Feature/MainFeature/MainComponent.swift b/App/Sources/Feature/MainFeature/MainComponent.swift new file mode 100644 index 0000000..226d9ba --- /dev/null +++ b/App/Sources/Feature/MainFeature/MainComponent.swift @@ -0,0 +1,11 @@ +import NeedleFoundation +import Service +import SwiftUI + +public protocol MainDependency: Dependency {} + +public final class MainComponent: Component, MainFactory { + public func makeView() -> some View { + MainView() + } +} diff --git a/App/Sources/Feature/MainFeature/MainFactory.swift b/App/Sources/Feature/MainFeature/MainFactory.swift new file mode 100644 index 0000000..2574f6d --- /dev/null +++ b/App/Sources/Feature/MainFeature/MainFactory.swift @@ -0,0 +1,7 @@ +import SwiftUI + +public protocol MainFactory { + associatedtype SomeView: View + func makeView() -> SomeView +} + diff --git a/App/Sources/Feature/RootFeature/RootComponent.swift b/App/Sources/Feature/RootFeature/RootComponent.swift new file mode 100644 index 0000000..1fdd685 --- /dev/null +++ b/App/Sources/Feature/RootFeature/RootComponent.swift @@ -0,0 +1,14 @@ +import NeedleFoundation +import SwiftUI + +public protocol RootDependency: Dependency { + var signinFactory: any SigninFactory { get } +} + +public final class RootComponent: Component { + public func makeView() -> some View { + RootView( + signinFactory: self.dependency.signinFactory + ) + } +} diff --git a/App/Sources/Feature/RootFeature/RootView.swift b/App/Sources/Feature/RootFeature/RootView.swift new file mode 100644 index 0000000..4d56878 --- /dev/null +++ b/App/Sources/Feature/RootFeature/RootView.swift @@ -0,0 +1,24 @@ +import SwiftUI + +struct RootView: View { + @EnvironmentObject var sceneState: SceneState + private let signinFactory: any SigninFactory + + public init( + signinFactory: any SigninFactory + ) { + self.signinFactory = signinFactory + } + + var body: some View { + Group { + switch sceneState.sceneFlow { + case .login: + signinFactory.makeView() + .eraseToAnyView() + .environmentObject(sceneState) + } + } + .animation(.default, value: sceneState.sceneFlow) + } +} diff --git a/App/Sources/Feature/SigninFeature/Component/GAuthButtonView.swift b/App/Sources/Feature/SigninFeature/Component/GAuthButtonView.swift new file mode 100644 index 0000000..5e488d0 --- /dev/null +++ b/App/Sources/Feature/SigninFeature/Component/GAuthButtonView.swift @@ -0,0 +1,35 @@ +import GAuthSignin +import SwiftUI +import UIKit + +struct GAuthButtonView: UIViewRepresentable { + private let completion: (String) -> Void + + init( + completion: @escaping (String) -> Void + ) { + self.completion = completion + } + + func updateUIView(_ uiView: UIViewType, context: Context) { } + + func makeUIView(context: Context) -> some UIView { + let gauthButton = GAuthButton( + auth: .signup, + color: .outline, + rounded: .default + ) + guard let presentingViewController = (UIApplication.shared.connectedScenes.first as? UIWindowScene)? + .windows + .first? + .rootViewController + else { return gauthButton } + gauthButton.prepare( + clientID: Bundle.main.object(forInfoDictionaryKey: "CLIENT_ID") as? String ?? "", + redirectURI: Bundle.main.object(forInfoDictionaryKey: "REDIRECT_URI") as? String ?? "", + presenting: presentingViewController, + completion: completion + ) + return gauthButton + } +} diff --git a/App/Sources/Feature/SigninFeature/Interface/SigninFactory.swift b/App/Sources/Feature/SigninFeature/Interface/SigninFactory.swift new file mode 100644 index 0000000..b470b2f --- /dev/null +++ b/App/Sources/Feature/SigninFeature/Interface/SigninFactory.swift @@ -0,0 +1,6 @@ +import SwiftUI + +public protocol SigninFactory { + associatedtype SomeView: View + func makeView() -> SomeView +} diff --git a/App/Sources/Feature/SigninFeature/Sources/SigninComponent.swift b/App/Sources/Feature/SigninFeature/Sources/SigninComponent.swift new file mode 100644 index 0000000..a3fe6a1 --- /dev/null +++ b/App/Sources/Feature/SigninFeature/Sources/SigninComponent.swift @@ -0,0 +1,19 @@ +import NeedleFoundation +import Service +import SwiftUI + +public protocol SigninDependency: Dependency { + var loginUseCase: any LoginUseCase { get } + var mainFactory: any MainFactory { get } +} + +public final class SigninComponent: Component, SigninFactory { + public func makeView() -> some View { + SigninView( + viewModel: .init( + loginUseCase: dependency.loginUseCase + ), + mainFactory: dependency.mainFactory + ) + } +} diff --git a/App/Sources/Feature/SigninFeature/Sources/SigninView.swift b/App/Sources/Feature/SigninFeature/Sources/SigninView.swift new file mode 100644 index 0000000..8cd5238 --- /dev/null +++ b/App/Sources/Feature/SigninFeature/Sources/SigninView.swift @@ -0,0 +1,30 @@ +import SwiftUI +import Service + +struct SigninView: View { + @StateObject var viewModel: SigninViewModel + private let mainFactory: any MainFactory + @EnvironmentObject var sceneState: SceneState + + init( + viewModel: SigninViewModel, + mainFactory: any MainFactory + ) { + _viewModel = StateObject(wrappedValue: viewModel) + self.mainFactory = mainFactory + } + + var body: some View { + VStack { + MindWayAsset.Images.mindWay.swiftUIImage + .padding(.bottom, 334) + + GAuthButtonView { code in + viewModel.signin(code: code) + } + .padding(.horizontal, 24) + .frame(height: 50) + .padding(.bottom, 16) + } + } +} diff --git a/App/Sources/Feature/SigninFeature/Sources/SigninViewModel.swift b/App/Sources/Feature/SigninFeature/Sources/SigninViewModel.swift new file mode 100644 index 0000000..ffafadc --- /dev/null +++ b/App/Sources/Feature/SigninFeature/Sources/SigninViewModel.swift @@ -0,0 +1,24 @@ +import Foundation +import Service + +final class SigninViewModel: BaseViewModel { + private let loginUseCase: any LoginUseCase + + init( + loginUseCase: any LoginUseCase + ) { + self.loginUseCase = loginUseCase + } + + func signin(code: String) { + Task { + do { + let userSigninInfo = try await self.loginUseCase.execute(code: code) + print("로그인 성공 \(userSigninInfo.authority)") + } catch { + isErrorOccurred = true + print("로그인 실패: \(error)") + } + } + } +} diff --git a/App/Support/Info.plist b/App/Support/Info.plist index 60bfb44..9769cf5 100644 --- a/App/Support/Info.plist +++ b/App/Support/Info.plist @@ -2,6 +2,10 @@ + CLIENT_ID + $(CLIENT_ID) + REDIRECT_URI + $(REDIRECT_URI) CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable diff --git a/Scripts/NeedleRunScript.sh b/Scripts/NeedleRunScript.sh new file mode 100755 index 0000000..c15ff41 --- /dev/null +++ b/Scripts/NeedleRunScript.sh @@ -0,0 +1,11 @@ +if test -d "/opt/homebrew/bin/"; then + PATH="/opt/homebrew/bin/:${PATH}" +fi + +export PATH + +if which needle > /dev/null; then + needle generate Sources/Application/NeedleGenerated.swift ../ +else + echo "warning: Needle not installed, plz run 'brew install needle'" +fi diff --git a/Scripts/Setup.sh b/Scripts/Setup.sh new file mode 100644 index 0000000..3f10e31 --- /dev/null +++ b/Scripts/Setup.sh @@ -0,0 +1,15 @@ +if test -d "/opt/homebrew/bin/"; then + PATH="/opt/homebrew/bin/:${PATH}" +fi + +export PATH + +if which tuist > /dev/null; then + echo "✅ Tuist was installed" +else + echo "❌ Tuist was not installed" + curl -Ls https://install.tuist.io | bash +fi + +git config --local include.path ../.gitconfig +chmod 777 .githooks/* diff --git a/Service/Project.swift b/Service/Project.swift index 2395f85..2a7859c 100644 --- a/Service/Project.swift +++ b/Service/Project.swift @@ -4,5 +4,11 @@ import ProjectDescriptionHelpers let project = Project.dynamicFramwork( name: "Service", platform: .iOS, - deploymentTarget: .iOS(targetVersion: "16.0", devices: [.iphone]) + infoPlist: .extendingDefault( + with: [ + "BASE_URL" : "$(BASE_URL)" + ] + ), + deploymentTarget: .iOS(targetVersion: "16.0", devices: [.iphone]), + scripts: [.NeedleShell] ) diff --git a/Service/Sources/Base/API/RefreshAPI.swift b/Service/Sources/Base/API/RefreshAPI.swift index 5f503b8..10b954d 100644 --- a/Service/Sources/Base/API/RefreshAPI.swift +++ b/Service/Sources/Base/API/RefreshAPI.swift @@ -21,7 +21,7 @@ extension RefreshAPI: MindWayAPI { public var method: Moya.Method { switch self { case .reissueToken: - return .put + return .patch } } diff --git a/Service/Sources/Base/DataSoueces/BaseRemoteDataSource.swift b/Service/Sources/Base/DataSoueces/BaseRemoteDataSource.swift index 74f4399..76150f5 100644 --- a/Service/Sources/Base/DataSoueces/BaseRemoteDataSource.swift +++ b/Service/Sources/Base/DataSoueces/BaseRemoteDataSource.swift @@ -24,6 +24,10 @@ open class BaseRemoteDataSource { let response = try await retryingRequest(api) return try decoder.decode(dto, from: response.data) } + + public func request(_ api: API) async throws { + _ = try await retryingRequest(api) + } private func requestPublisher(_ api: API) async throws -> Response { try await withCheckedThrowingContinuation { continuation in diff --git a/Service/Sources/Base/Enum/UserAuthorityType.swift b/Service/Sources/Base/Enum/UserAuthorityType.swift new file mode 100644 index 0000000..cb67884 --- /dev/null +++ b/Service/Sources/Base/Enum/UserAuthorityType.swift @@ -0,0 +1,7 @@ +import Foundation + +public enum UserAuthorityType: String, CaseIterable, Decodable, Encodable { + case student = "ROLE_STUDENT" + case teacher = "ROLE_TEACHER" + case helper = "ROLE_HELPER" +} diff --git a/Service/Sources/Domain/AuthDomain/API/AuthAPI.swift b/Service/Sources/Domain/AuthDomain/API/AuthAPI.swift new file mode 100644 index 0000000..8f5520b --- /dev/null +++ b/Service/Sources/Domain/AuthDomain/API/AuthAPI.swift @@ -0,0 +1,89 @@ +import Foundation +import Moya + +public enum AuthAPI { + case login(code: String) + case reissueToken + case logout +} + +extension AuthAPI: MindWayAPI { + public typealias ErrorType = AuthDomainError + + public var domain: MindWayDomain { + .auth + } + + public var urlPath: String { + switch self { + case .login: + return "/login" + + case .reissueToken, .logout: + return "" + } + } + + public var method: Moya.Method { + switch self { + case .login: + return .post + + case .reissueToken: + return .patch + + case .logout: + return .delete + } + } + + public var task: Moya.Task { + switch self { + case let .login(code): + return .requestParameters(parameters: [ + "code" : code + ], + encoding: JSONEncoding.default) + + case .reissueToken: + return .requestPlain + + case .logout: + return .requestPlain + } + } + + public var jwtTokenType: JwtTokenType { + switch self { + case .reissueToken: + return .refreshToken + + case .logout: + return .accessToken + + default: + return .none + } + } + + public var errorMap: [Int: ErrorType] { + switch self { + case .login: + return [ + 500: .internalServerError + ] + + case .reissueToken: + return [ + 401: .unauthorized, + 404: .notFound + ] + + case .logout: + return [ + 401: .unauthorized, + 404: .notFound + ] + } + } +} diff --git a/Service/Sources/Domain/AuthDomain/DTO/Response/SigninResponseDTO.swift b/Service/Sources/Domain/AuthDomain/DTO/Response/SigninResponseDTO.swift new file mode 100644 index 0000000..81275f8 --- /dev/null +++ b/Service/Sources/Domain/AuthDomain/DTO/Response/SigninResponseDTO.swift @@ -0,0 +1,11 @@ +import Foundation + +struct SigninResponseDTO: Decodable { + let userAuthority: UserAuthorityType +} + +extension SigninResponseDTO { + func toDomain() -> UserSignupInfoEntity { + UserSignupInfoEntity(authority: userAuthority) + } +} diff --git a/Service/Sources/Domain/AuthDomain/DataSource/LocalAuthDataSourceImpl.swift b/Service/Sources/Domain/AuthDomain/DataSource/LocalAuthDataSourceImpl.swift new file mode 100644 index 0000000..c290704 --- /dev/null +++ b/Service/Sources/Domain/AuthDomain/DataSource/LocalAuthDataSourceImpl.swift @@ -0,0 +1,18 @@ +import Foundation + +public struct LocalAuthDataSourceImpl: LocalAuthDataSource { + private let keychain: Keychain + + public init( + keychain: Keychain + ) { + self.keychain = keychain + } + + public func logout() async throws { + keychain.delete(type: .accessToken) + keychain.delete(type: .accessExpiredAt) + keychain.delete(type: .refreshToken) + keychain.delete(type: .refreshExpiredAt) + } +} diff --git a/Service/Sources/Domain/AuthDomain/DataSource/RemoteAuthDataSourceImpl.swift b/Service/Sources/Domain/AuthDomain/DataSource/RemoteAuthDataSourceImpl.swift new file mode 100644 index 0000000..115e70d --- /dev/null +++ b/Service/Sources/Domain/AuthDomain/DataSource/RemoteAuthDataSourceImpl.swift @@ -0,0 +1,23 @@ +import Foundation + +public final class RemoteAuthDataSourceImpl: BaseRemoteDataSource, RemoteAuthDataSource { + public let keychain: Keychain + + public init(keychain: any Keychain) { + self.keychain = keychain + super.init(keychain: keychain) + } + + public func login(code: String) async throws -> UserSignupInfoEntity { + try await request(.login(code: code), dto: SigninResponseDTO.self) + .toDomain() + } + + public func logout() async throws { + try await request(.logout) + } + + public func refresh() async throws { + try await request(.reissueToken) + } +} diff --git a/Service/Sources/Domain/AuthDomain/Entity/UserSignupInfoEntity.swift b/Service/Sources/Domain/AuthDomain/Entity/UserSignupInfoEntity.swift new file mode 100644 index 0000000..1893bed --- /dev/null +++ b/Service/Sources/Domain/AuthDomain/Entity/UserSignupInfoEntity.swift @@ -0,0 +1,9 @@ +import Foundation + +public struct UserSignupInfoEntity { + public let authority: UserAuthorityType + + public init(authority: UserAuthorityType) { + self.authority = authority + } +} diff --git a/Service/Sources/Domain/AuthDomain/Error/AuthDomainError.swift b/Service/Sources/Domain/AuthDomain/Error/AuthDomainError.swift new file mode 100644 index 0000000..3842759 --- /dev/null +++ b/Service/Sources/Domain/AuthDomain/Error/AuthDomainError.swift @@ -0,0 +1,20 @@ +import Foundation + +public enum AuthDomainError: Error { + case unauthorized + case notFound + case internalServerError +} + +extension AuthDomainError: LocalizedError { + public var errorDescription: String? { + switch self { + case .unauthorized: + return "권한이 없습니다." + case .notFound: + return "존재하지 않는 계정입니다." + case .internalServerError: + return "알 수 없는 에러가 발생했습니다. 지속될 시 문의 주세요." + } + } +} diff --git a/Service/Sources/Domain/AuthDomain/Repository/AuthRepositoryImpl.swift b/Service/Sources/Domain/AuthDomain/Repository/AuthRepositoryImpl.swift new file mode 100644 index 0000000..34d6958 --- /dev/null +++ b/Service/Sources/Domain/AuthDomain/Repository/AuthRepositoryImpl.swift @@ -0,0 +1,27 @@ +import Foundation + +public struct AuthRepositoryImpl: AuthRepository { + private let remoteAuthDataSource: any RemoteAuthDataSource + private let localAuthDataSource: any LocalAuthDataSource + + public init( + remoteAuthDataSource: any RemoteAuthDataSource, + localAuthDataSource: any LocalAuthDataSource + ) { + self.remoteAuthDataSource = remoteAuthDataSource + self.localAuthDataSource = localAuthDataSource + } + + public func login(code: String) async throws -> UserSignupInfoEntity { + try await remoteAuthDataSource.login(code: code) + } + + public func logout() async throws { + try await remoteAuthDataSource.logout() + try await localAuthDataSource.logout() + } + + public func refresh() async throws { + try await remoteAuthDataSource.refresh() + } +} diff --git a/Service/Sources/Domain/AuthDomain/UseCase/LoginUseCaseImpl.swift b/Service/Sources/Domain/AuthDomain/UseCase/LoginUseCaseImpl.swift new file mode 100644 index 0000000..103ca22 --- /dev/null +++ b/Service/Sources/Domain/AuthDomain/UseCase/LoginUseCaseImpl.swift @@ -0,0 +1,13 @@ +import Foundation + +public struct LoginUseCaseImpl: LoginUseCase { + private let authRepository: any AuthRepository + + public init(authRepository: any AuthRepository) { + self.authRepository = authRepository + } + + public func execute(code: String) async throws -> UserSignupInfoEntity { + try await authRepository.login(code: code) + } +} diff --git a/Service/Sources/Domain/AuthDomain/UseCase/LogoutUseCaseImpl.swift b/Service/Sources/Domain/AuthDomain/UseCase/LogoutUseCaseImpl.swift new file mode 100644 index 0000000..c304559 --- /dev/null +++ b/Service/Sources/Domain/AuthDomain/UseCase/LogoutUseCaseImpl.swift @@ -0,0 +1,13 @@ +import Foundation + +public struct LogoutUseCaseImpl: LogoutUseCase { + private let authRepository: any AuthRepository + + public init(authRepository: any AuthRepository) { + self.authRepository = authRepository + } + + public func execute() async throws { + try await authRepository.logout() + } +} diff --git a/Service/Sources/Interface/DataSource/Auth/LocalAuthDataSource.swift b/Service/Sources/Interface/DataSource/Auth/LocalAuthDataSource.swift new file mode 100644 index 0000000..d6be07c --- /dev/null +++ b/Service/Sources/Interface/DataSource/Auth/LocalAuthDataSource.swift @@ -0,0 +1,5 @@ +import Foundation + +public protocol LocalAuthDataSource { + func logout() async throws +} diff --git a/Service/Sources/Interface/DataSource/Auth/RemoteAuthDateSource.swift b/Service/Sources/Interface/DataSource/Auth/RemoteAuthDateSource.swift new file mode 100644 index 0000000..1006fa6 --- /dev/null +++ b/Service/Sources/Interface/DataSource/Auth/RemoteAuthDateSource.swift @@ -0,0 +1,7 @@ +import Foundation + +public protocol RemoteAuthDataSource { + func login(code: String) async throws -> UserSignupInfoEntity + func logout() async throws + func refresh() async throws +} diff --git a/Service/Sources/Interface/Repository/Auth/AuthRepository.swift b/Service/Sources/Interface/Repository/Auth/AuthRepository.swift new file mode 100644 index 0000000..791f26b --- /dev/null +++ b/Service/Sources/Interface/Repository/Auth/AuthRepository.swift @@ -0,0 +1,7 @@ +import Foundation + +public protocol AuthRepository { + func login(code: String) async throws -> UserSignupInfoEntity + func logout() async throws + func refresh() async throws +} diff --git a/Service/Sources/Interface/UseCase/Auth/LoginUseCase.swift b/Service/Sources/Interface/UseCase/Auth/LoginUseCase.swift new file mode 100644 index 0000000..86615c2 --- /dev/null +++ b/Service/Sources/Interface/UseCase/Auth/LoginUseCase.swift @@ -0,0 +1,5 @@ +import Foundation + +public protocol LoginUseCase { + func execute(code: String) async throws -> UserSignupInfoEntity +} diff --git a/Service/Sources/Interface/UseCase/Auth/LogoutUseCase.swift b/Service/Sources/Interface/UseCase/Auth/LogoutUseCase.swift new file mode 100644 index 0000000..3204225 --- /dev/null +++ b/Service/Sources/Interface/UseCase/Auth/LogoutUseCase.swift @@ -0,0 +1,5 @@ +import Foundation + +public protocol LogoutUseCase { + func execute() async throws +} diff --git a/ThirdPartyLib/Project.swift b/ThirdPartyLib/Project.swift index d5ea910..bd23169 100644 --- a/ThirdPartyLib/Project.swift +++ b/ThirdPartyLib/Project.swift @@ -6,6 +6,8 @@ let project = Project.dynamicFramwork( packages: [], deploymentTarget: .iOS(targetVersion: "16.0", devices: [.iphone]), dependencies: [ - .SPM.Moya + .SPM.Moya, + .SPM.GAuthSignin, + .SPM.NeedleFoundation ] ) diff --git a/Tuist/Dependencies.swift b/Tuist/Dependencies.swift index 46ff8ca..6bb9ec7 100644 --- a/Tuist/Dependencies.swift +++ b/Tuist/Dependencies.swift @@ -7,8 +7,19 @@ let dependencies = Dependencies( .remote( url: "https://github.com/Moya/Moya.git", requirement: .upToNextMajor(from: "15.0.3") + ), + .remote( + url: "https://github.com/Team-MindWay/GAuthSignin-Swift.git", + requirement: .branch("master") + ), + .remote( + url: "https://github.com/uber/needle.git", + requirement: .upToNextMajor(from: "0.24.0") ) ], + productTypes: [ + "GAuthSignin": .framework + ], baseSettings: .settings( configurations: [ .debug(name: .debug), diff --git a/Tuist/ProjectDescriptionHelpers/DynamicFramwork.swift b/Tuist/ProjectDescriptionHelpers/DynamicFramwork.swift index d56eb37..0bbc71b 100644 --- a/Tuist/ProjectDescriptionHelpers/DynamicFramwork.swift +++ b/Tuist/ProjectDescriptionHelpers/DynamicFramwork.swift @@ -1,7 +1,10 @@ +import Foundation import ProjectDescription -extension Project{ - public static func dynamicFramwork( +let isCI = (ProcessInfo.processInfo.environment["TUIST_CI"] ?? "0") == "1" ? true : false + +public extension Project { + static func dynamicFramwork( name: String, platform: Platform = .iOS, packages: [Package] = [], @@ -9,12 +12,32 @@ extension Project{ deploymentTarget: DeploymentTarget, dependencies: [TargetDependency] = [ .project(target: "ThirdPartyLib", path: Path("../ThirdPartyLib")) - ] + ], + scripts: [TargetScript] = [] ) -> Project { return Project( name: name, packages: packages, - settings: nil, + settings: .settings( + base: .codeSign, + configurations: isCI ? + [ + .debug(name: .debug), + .release(name: .release) + ] : + [ + .debug( + name: .debug, + xcconfig: + .relativeToXCConfig(type: .debug, name: name) + ), + .release( + name: .release, + xcconfig: + .relativeToXCConfig(type: .release, name: name) + ) + ] + ), targets: [ Target( name: name, diff --git a/Tuist/ProjectDescriptionHelpers/Executable.swift b/Tuist/ProjectDescriptionHelpers/Executable.swift index b7eadec..93dd218 100644 --- a/Tuist/ProjectDescriptionHelpers/Executable.swift +++ b/Tuist/ProjectDescriptionHelpers/Executable.swift @@ -11,7 +11,25 @@ extension Project { return Project( name: name, organizationName: publicOrganizationName, - settings: nil, + settings: .settings( + configurations: isCI ? + [ + .debug(name: .debug), + .release(name: .release) + ] : + [ + .debug( + name: .debug, + xcconfig: + .relativeToXCConfig(type: .debug, name: name) + ), + .release( + name: .release, + xcconfig: + .relativeToXCConfig(type: .release, name: name) + ) + ] + ), targets: [ Target( name: name, @@ -22,6 +40,7 @@ extension Project { infoPlist: .file(path: Path("Support/Info.plist")), sources: ["Sources/**"], resources: ["Resources/**"], + scripts: [.NeedleShell], dependencies: [ .project(target: "ThirdPartyLib", path: Path("../ThirdPartyLib")), ] + dependencies diff --git a/Tuist/ProjectDescriptionHelpers/Scripts.swift b/Tuist/ProjectDescriptionHelpers/Scripts.swift new file mode 100644 index 0000000..e51519a --- /dev/null +++ b/Tuist/ProjectDescriptionHelpers/Scripts.swift @@ -0,0 +1,25 @@ +import ProjectDescription + +public extension TargetScript { + static let NeedleRunScript = TargetScript.pre( + script: """ + if test -d "/opt/homebrew/bin/"; then + PATH="/opt/homebrew/bin/:${PATH}" + fi + + export PATH + + if which needle > /dev/null; then + needle generate Sources/Application/NeedleGenerated.swift ../ + else + echo "warning: Needle not installed, plz run 'brew install needle'" + fi + """, + name: "NeedleRunScript" + ) + + static let NeedleShell = TargetScript.pre( + path: .relativeToRoot("Scripts/NeedleRunScript.sh"), + name: "NeedleShell" + ) +} diff --git a/Tuist/ProjectDescriptionHelpers/TargetDependency.swift b/Tuist/ProjectDescriptionHelpers/TargetDependency.swift index 14bfa60..8131cac 100644 --- a/Tuist/ProjectDescriptionHelpers/TargetDependency.swift +++ b/Tuist/ProjectDescriptionHelpers/TargetDependency.swift @@ -6,7 +6,8 @@ extension TargetDependency { public extension TargetDependency.SPM { static let Moya = TargetDependency.external(name: "Moya") + static let GAuthSignin = TargetDependency.external(name: "GAuthSignin") + static let NeedleFoundation = TargetDependency.external(name: "NeedleFoundation") } -public extension Package { -} +public extension Package {} diff --git a/XCConfig/Shared.xcconfig b/XCConfig/Shared.xcconfig new file mode 100644 index 0000000..c2409c8 --- /dev/null +++ b/XCConfig/Shared.xcconfig @@ -0,0 +1,4 @@ +OTHER_SWIFT_FLAGS[config=DEUBG][sdk=*] = $(inherited) -DDEUBG +OTHER_SWIFT_FLAGS[config=RELEASE][sdk=*] = $(inherited) - DRELEASE + +ENABLE_BITCODE = NO