diff --git a/.github/workflows/merge_to_develop_on_pr.yml b/.github/workflows/merge_to_develop_on_pr.yml index fb587674..764392c0 100644 --- a/.github/workflows/merge_to_develop_on_pr.yml +++ b/.github/workflows/merge_to_develop_on_pr.yml @@ -48,17 +48,17 @@ jobs: uses: actions/checkout@v4 #5. fetch XCConfig from repository - - name: fetch xcconfig + - name: fetch Secrets uses: actions/checkout@v4 with: repository: ${{ secrets.XCCONFIG_REPO }} token: ${{ secrets.XCCONFIG_REPO_TOKEN }} - path: '${{ github.workspace }}/project/XcodeConfiguration' + path: '${{ github.workspace }}/project/Secrets' - - name: check xcconfig files + - name: check secret files run: | echo "Checking configurations are loaded..." - tree ${{ github.workspace }}/project/XcodeConfiguration + tree ${{ github.workspace }}/project/Secrets #6. test tuist project - name: Test project diff --git a/.gitignore b/.gitignore index 9b94bb17..971e6246 100644 --- a/.gitignore +++ b/.gitignore @@ -5,8 +5,7 @@ **/*.xcworkspace # Ignore all xcconfig files in all directories -**/*.xcconfig -XcodeConfiguration/ +project/Secrets/ # Ignore Derived **/Derived/ diff --git a/project/Plugins/ConfigurationPlugin/ProjectDescriptionHelpers/BuildSettings.swift b/project/Plugins/ConfigurationPlugin/ProjectDescriptionHelpers/BuildSettings.swift index 2627ecd0..c09ca621 100644 --- a/project/Plugins/ConfigurationPlugin/ProjectDescriptionHelpers/BuildSettings.swift +++ b/project/Plugins/ConfigurationPlugin/ProjectDescriptionHelpers/BuildSettings.swift @@ -15,17 +15,17 @@ public enum IdleConfiguration { } private enum XcconfigFile { - static let appDebug: Path = .relativeToRoot("XcodeConfiguration/App/appDebug.xcconfig") - static let appRelease: Path = .relativeToRoot("XcodeConfiguration/App/appRelease.xcconfig") + static let appDebug: Path = .relativeToRoot("Secrets/XcodeConfiguration/App/appDebug.xcconfig") + static let appRelease: Path = .relativeToRoot("Secrets/XcodeConfiguration/App/appRelease.xcconfig") - static let domainDebug: Path = .relativeToRoot("XcodeConfiguration/Domain/domainDebug.xcconfig") - static let domainRelease: Path = .relativeToRoot("XcodeConfiguration/Domain/domainRelease.xcconfig") + static let domainDebug: Path = .relativeToRoot("Secrets/XcodeConfiguration/Domain/domainDebug.xcconfig") + static let domainRelease: Path = .relativeToRoot("Secrets/XcodeConfiguration/Domain/domainRelease.xcconfig") - static let dataDebug: Path = .relativeToRoot("XcodeConfiguration/Data/dataDebug.xcconfig") - static let dataRelease: Path = .relativeToRoot("XcodeConfiguration/Data/dataRelease.xcconfig") + static let dataDebug: Path = .relativeToRoot("Secrets/XcodeConfiguration/Data/dataDebug.xcconfig") + static let dataRelease: Path = .relativeToRoot("Secrets/XcodeConfiguration/Data/dataRelease.xcconfig") - static let presentationDebug: Path = .relativeToRoot("XcodeConfiguration/Presentation/presentationDebug.xcconfig") - static let presentationRelease: Path = .relativeToRoot("XcodeConfiguration/Presentation/presentationRelease.xcconfig") + static let presentationDebug: Path = .relativeToRoot("Secrets/XcodeConfiguration/Presentation/presentationDebug.xcconfig") + static let presentationRelease: Path = .relativeToRoot("Secrets/XcodeConfiguration/Presentation/presentationRelease.xcconfig") } public static let debugConfigName = Name.debug diff --git a/project/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Dependency.swift b/project/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Dependency.swift index b4f903a8..61608c1c 100644 --- a/project/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Dependency.swift +++ b/project/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Dependency.swift @@ -39,6 +39,8 @@ public extension ModuleDependency { public static let Swinject: TargetDependency = .external(name: "Swinject") public static let Alamofire: TargetDependency = .external(name: "Alamofire") public static let KeyChainAccess: TargetDependency = .external(name: "KeychainAccess") + public static let Moya: TargetDependency = .external(name: "Moya") + public static let RxMoya: TargetDependency = .external(name: "RxMoya") } } diff --git a/project/Plugins/DependencyPlugin/ProjectDescriptionHelpers/SecretSource.swift b/project/Plugins/DependencyPlugin/ProjectDescriptionHelpers/SecretSource.swift new file mode 100644 index 00000000..2923977e --- /dev/null +++ b/project/Plugins/DependencyPlugin/ProjectDescriptionHelpers/SecretSource.swift @@ -0,0 +1,14 @@ +// +// SecretSource.swift +// DependencyPlugin +// +// Created by choijunios on 7/3/24. +// + +import ProjectDescription + +// MARK: SecretSource +public enum SecretSource { + + public static let networkDataSource: SourceFileGlob = .glob(.relativeToRoot("Secrets/SwiftCode/NetworkDataSource/**")) +} diff --git a/project/Projects/Data/ConcretesTests/APITesting/TokenTesting.swift b/project/Projects/Data/ConcretesTests/APITesting/TokenTesting.swift index 0ad76d37..5d380e0a 100644 --- a/project/Projects/Data/ConcretesTests/APITesting/TokenTesting.swift +++ b/project/Projects/Data/ConcretesTests/APITesting/TokenTesting.swift @@ -41,51 +41,3 @@ class TestKeyValueStore: KeyValueStore { testStore.removeAll() } } - -// TestAPI -public enum TestAPI: BaseAPI { - - public static let apiType: APIType = .test - - case testEndPoint - - public var method: ReqeustConponents.HTTPMethod { .get } - - public var headers: [String: String] { - - var myHeader = defaultHeaders - - myHeader[ReqeustConponents.Header.authorization.key] = ReqeustConponents.Header.authorization.defaultValue - - return myHeader - } - - public var endPoint: String { - - switch self { - case .testEndPoint: - return "" - } - } -} - -protocol TestService { - - func testRequest() -> Single<[Person]> -} - -class DefaultTestService: BaseNetworkService { } - -extension DefaultTestService: TestService { - - func testRequest() -> Single<[Person]> { - - request(api: .testEndPoint) - } -} - -struct Person: Codable { - let name: String - let age: String - let id: String -} diff --git a/project/Projects/Data/NetworkDataSource/API/APIType.swift b/project/Projects/Data/NetworkDataSource/API/APIType.swift deleted file mode 100644 index 8c404577..00000000 --- a/project/Projects/Data/NetworkDataSource/API/APIType.swift +++ /dev/null @@ -1,72 +0,0 @@ -// -// APIType.swift -// ConcreteRepository -// -// Created by choijunios on 6/28/24. -// - -import Foundation - -enum APIType { - - case test - - var additionalPath: String { - - switch self { - case .test: - return "users" - } - } -} - -enum ReqeustConponents { - - enum Header { - - case authorization - case contentType - - var key: String { - - switch self { - case .authorization: - return "Authorization" - case .contentType: - return "Content-Type" - } - } - - var defaultValue: String { - - switch self { - case .authorization: - return "-" - case .contentType: - return "application/json" - } - } - } - - enum HTTPMethod { - - case get - case post - case put - case delete - - var value: String { - - switch self { - case .get: - return "GET" - case .post: - return "POST" - case .put: - return "PUT" - case .delete: - return "DELETE" - } - } - } -} diff --git a/project/Projects/Data/NetworkDataSource/API/BaseAPI.swift b/project/Projects/Data/NetworkDataSource/API/BaseAPI.swift index 07636d62..7a7722ab 100644 --- a/project/Projects/Data/NetworkDataSource/API/BaseAPI.swift +++ b/project/Projects/Data/NetworkDataSource/API/BaseAPI.swift @@ -6,49 +6,41 @@ // import Foundation +import Moya -protocol BaseAPI { +enum APIType { - static var apiType: APIType { get } - var headers: [String: String] { get } - var method: ReqeustConponents.HTTPMethod { get } - var queryItems: [URLQueryItem]? { get } - var endPoint: String { get } + case test +} + +// MARK: BaseAPI +protocol BaseAPI: TargetType { + + var apiType: APIType { get } } extension BaseAPI { - var baseUrl: URL { - - var baseUrlString = Config.baseUrlString - - [ - Self.apiType.additionalPath, - endPoint - ].forEach { path in - if !path.isEmpty { - baseUrlString.append("/\(path)") - } - } + var baseURL: URL { - return URL(string: baseUrlString)! - } - - - var headers: [String: String] { + let base = URL(string: NetworkConfig.baseUrl)! - return defaultHeaders + return base.appendingPathComponent(self.path) } - var defaultHeaders: [String: String] { + var path: String { - return [ - ReqeustConponents.Header.contentType.key: ReqeustConponents.Header.contentType.defaultValue - ] + switch apiType { + case .test: + "test" + default: + preconditionFailure("APIType is not defined") + } } - var queryItems: [URLQueryItem]? { + /// Default header + var headers: [String : String]? { - return nil + return ["Content-Type": "application/json"] } } diff --git a/project/Projects/Data/NetworkDataSource/API/Config.swift b/project/Projects/Data/NetworkDataSource/API/Config.swift deleted file mode 100644 index 30d47a6d..00000000 --- a/project/Projects/Data/NetworkDataSource/API/Config.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// Config.swift -// ConcreteRepository -// -// Created by choijunios on 6/28/24. -// - -import Foundation - -internal enum Config { - - static let baseUrlString = "https://667c2cf33c30891b865ba28e.mockapi.io" -} diff --git a/project/Projects/Data/NetworkDataSource/Service/BaseNetworkService.swift b/project/Projects/Data/NetworkDataSource/Service/BaseNetworkService.swift index 8a42ebf5..7aea3770 100644 --- a/project/Projects/Data/NetworkDataSource/Service/BaseNetworkService.swift +++ b/project/Projects/Data/NetworkDataSource/Service/BaseNetworkService.swift @@ -8,54 +8,62 @@ import Foundation import RxSwift import Alamofire +import Moya +import RxMoya -class BaseNetworkService { +class BaseNetworkService { - let keyValueStore: KeyValueStore + private let keyValueStore: KeyValueStore init(keyValueStore: KeyValueStore = KeyChainList.shared) { self.keyValueStore = keyValueStore } - lazy var tokenSession: Session = { + private lazy var provider = self.defaultProvider + + private lazy var defaultProvider: MoyaProvider = { + + let provider = MoyaProvider(session: sessionWithToken) + + return provider + }() + + lazy var sessionWithToken: Session = { let configuration = URLSessionConfiguration.default + // 단일 요청이 완료되는데 걸리는 최대 시간, 초과시 타임아웃 configuration.timeoutIntervalForRequest = 10 + // 하나의 리소스를 로드하는데 걸리는 시간, 재시도를 포함한다 초과시 타임아웃 + configuration.timeoutIntervalForResource = 10 + + // Cache policy: 로컬캐시를 무시하고 항상 새로운 데이터를 가져온다. + configuration.requestCachePolicy = .reloadIgnoringLocalCacheData + let tokenIntercepter = Interceptor.interceptor( adapter: tokenAdpater, retrier: tokenRetrier ) - let serverTrustPolicies: [String: ServerTrustEvaluating] = [ - "667c2cf33c30891b865ba28e.mockapi.io": DisabledTrustEvaluator() - ] - - let serverTrustManager = ServerTrustManager(evaluators: serverTrustPolicies) - - let tokenSession = Session( + return Session( configuration: configuration, - interceptor: tokenIntercepter, - serverTrustManager: serverTrustManager + interceptor: tokenIntercepter ) - - return tokenSession }() + + // MARK: Alamofire Interceptor lazy var tokenAdpater = Adapter { [weak self] request, session, completion in - guard let token = self?.keyValueStore.getAuthToken() else { - - // TODO: 에러처리 규칙 정해지면 수정예정 - precondition(false, "Token not found") - } - var adaptedRequest = request - let bearerToken = "Bearer \(token.accessToken)" - - adaptedRequest.setValue(bearerToken, forHTTPHeaderField: ReqeustConponents.Header.authorization.key) + if let token = self?.keyValueStore.getAuthToken() { + + let bearerToken = "Bearer \(token.accessToken)" + + adaptedRequest.setValue(bearerToken, forHTTPHeaderField: "Authorization") + } completion(.success(adaptedRequest)) } @@ -67,7 +75,6 @@ class BaseNetworkService { if httpResponse.statusCode == 401 { // TODO: 토큰 재발급후 요청 재시도 - } } @@ -76,33 +83,49 @@ class BaseNetworkService { } +// MARK: DataRequest extension BaseNetworkService { - func request(api: T) -> Single { + func requestDecodable(api: TagetAPI) -> Single { - return Single.create { [weak self] single in - - let urlRequest = URLRequest(url: api.baseUrl) - - print(urlRequest.url!.absoluteString) - - let requestSession = api.headers.keys.contains("Authorization") ? self?.tokenSession : AF + self.provider.rx.request(api) + .map(T.self) + } + + // MARK: Request with Progress + struct ProgressResponse { + + let progress: Double + let data: T? + } + + func requestDecodableWithProgress(api: TagetAPI) -> Single> { + + Single>.create { single in - let dataRequest = requestSession? - .request(urlRequest) - .validate(statusCode: 200..<300) - .responseDecodable(of: R.self) { response in - switch response.result { - case .success(let decoded): - single(.success(decoded)) - case .failure(let error): - single(.failure(error)) + self.provider.rx + .requestWithProgress(api) + .subscribe(onNext: { response in + + if let result = response.response { + + do { + + let decoded = try result.map(T.self) + + let item = ProgressResponse( + progress: response.progress, + data: decoded + ) + + single(.success(item)) + + } catch { + + single(.failure(error)) + } } - } - - return Disposables.create { - dataRequest?.cancel() - } + }) } } } diff --git a/project/Projects/Data/Project.swift b/project/Projects/Data/Project.swift index e93e9a4b..85c7d47f 100644 --- a/project/Projects/Data/Project.swift +++ b/project/Projects/Data/Project.swift @@ -28,6 +28,9 @@ let project = Project( dependencies: [ D.Domain.RepositoryInterface, D.Data.NetworkDataSource, + + // ThirdParty + D.ThirdParty.RxSwift ], settings: .settings( base: ["ENABLE_TESTABILITY": "YES"] @@ -58,13 +61,18 @@ let project = Project( product: .staticLibrary, bundleId: "$(PRODUCT_BUNDLE_IDENTIFIER)", deploymentTargets: DeploymentSettings.deployment_version, - sources: ["NetworkDataSource/**"], + sources: [ + "NetworkDataSource/**", + SecretSource.networkDataSource, + ], dependencies: [ // ThirdParty D.ThirdParty.Alamofire, D.ThirdParty.RxSwift, D.ThirdParty.KeyChainAccess, + D.ThirdParty.Moya, + D.ThirdParty.RxMoya, ], settings: .settings( base: ["ENABLE_TESTABILITY": "YES"] diff --git a/project/Projects/Domain/Project.swift b/project/Projects/Domain/Project.swift index 3593be76..070efb51 100644 --- a/project/Projects/Domain/Project.swift +++ b/project/Projects/Domain/Project.swift @@ -28,6 +28,9 @@ let project = Project( dependencies: [ D.Domain.UseCaseInterface, D.Domain.RepositoryInterface, + + // ThirdParty + D.ThirdParty.RxSwift, ], settings: .settings( base: ["ENABLE_TESTABILITY": "YES"] @@ -61,6 +64,9 @@ let project = Project( sources: ["UseCaseInterface/**"], dependencies: [ D.Domain.Entity, + + // ThirdParty + D.ThirdParty.RxSwift, ], settings: .settings( configurations: IdleConfiguration.domainConfigurations @@ -77,6 +83,9 @@ let project = Project( sources: ["RepositoryInterface/**"], dependencies: [ D.Domain.Entity, + + // ThirdParty + D.ThirdParty.RxSwift, ], settings: .settings( base: ["ENABLE_TESTABILITY": "YES"], diff --git a/project/Tuist/Package.swift b/project/Tuist/Package.swift index 7a38e4ac..28672185 100644 --- a/project/Tuist/Package.swift +++ b/project/Tuist/Package.swift @@ -33,6 +33,8 @@ let package = Package( // Swinject .package(url: "https://github.com/Swinject/Swinject.git", from: "2.9.1"), // KeyChainAccess - .package(url: "https://github.com/kishikawakatsumi/KeychainAccess.git", from: "4.2.2") + .package(url: "https://github.com/kishikawakatsumi/KeychainAccess.git", from: "4.2.2"), + // Moya + .package(url: "https://github.com/Moya/Moya.git", from: "15.0.3"), ] )