diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index e0dd576c8..53c94f7c6 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -3,12 +3,13 @@ name: Bibbi on: push: branches: - - feat/* - - fix/* + - release pull_request: branches: - - release/** + - release - develop + types: + - closed jobs: build: @@ -61,7 +62,7 @@ jobs: run: tuist generate - name: fastlane upload_prd_testflight - if: github.event.pull_request.base.ref == 'release' && github.head_ref == 'develop' + if: ${{ github.base_ref == 'release' && github.event_name == 'push' }} env: APP_STORE_CONNECT_API_KEY_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY_ID }} APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }} @@ -82,7 +83,7 @@ jobs: - name: fastlane upload_stg_testflight - if: github.event.pull_request.base.ref == 'develop' && startsWith(github.head_ref, 'feat/') + if: ${{ github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'develop' }} env: APP_STORE_CONNECT_API_KEY_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY_ID }} APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }} diff --git a/14th-team5-iOS/App/Project.swift b/14th-team5-iOS/App/Project.swift index 32d643c8f..cb83f420b 100644 --- a/14th-team5-iOS/App/Project.swift +++ b/14th-team5-iOS/App/Project.swift @@ -19,8 +19,8 @@ private let targets: [Target] = [ "CFBundleDisplayName": .string("Bibbi"), "CFBundleVersion": .string("1"), "CFBuildVersion": .string("0"), - "CFBundleShortVersionString": .string("1.2.2"), - "UILaunchStoryboardName": .string("LaunchScreen.storyboard"), + "CFBundleShortVersionString": .string("1.2.3"), + "UILaunchStoryboardName": .string("LaunchScreen"), "UISupportedInterfaceOrientations": .array([.string("UIInterfaceOrientationPortrait")]), "UIUserInterfaceStyle": .string("Dark"), "NSPhotoLibraryAddUsageDescription" : .string("프로필 사진, 피드 업로드를 위한 사진 촬영을 위해 Bibbi가 앨범에 접근할 수 있도록 허용해 주세요"), diff --git a/14th-team5-iOS/App/Sources/Application/AppDelegate.swift b/14th-team5-iOS/App/Sources/Application/AppDelegate.swift index b9a606ad1..a5e7b4d4f 100644 --- a/14th-team5-iOS/App/Sources/Application/AppDelegate.swift +++ b/14th-team5-iOS/App/Sources/Application/AppDelegate.swift @@ -108,6 +108,7 @@ extension AppDelegate { return } App.Repository.token.clearAccessToken() + KeychainWrapper.standard.removeAllKeys() } } diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/AppDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/AppDIContainer.swift index 8718a1fa1..565818384 100644 --- a/14th-team5-iOS/App/Sources/Application/DIContainer/AppDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/AppDIContainer.swift @@ -19,12 +19,20 @@ final class AppDIContainer: BaseContainer { ) } - private func makeFetchFamilyManagementUseCase() -> FetchIsFirstFamilyManagementUseCaseProtocol { - FetchFamilyManagementUseCase(repository: makeAppRepository()) + private func makeCheckIsFirstWidgetAlertUseCase() -> IsFirstWidgetAlertUseCaseProtocol { + IsFirstWidgetAlertUseCase(repository: makeAppRepository()) } - private func makeSaveFamilyManagementUseCase() -> UpdateFamilyManagementUseCaseProtocol { - UpdateFamilyManagementUseCase(repository: makeAppRepository()) + private func makeSaveWidgetAlertUseCase() -> SaveIsFirstWidgetAlertUseCaseProtocol { + SaveIsFirstWidgetAlertUseCase(repository: makeAppRepository()) + } + + private func makeFetchFamilyManagementUseCase() -> IsFirstFamilyManagementUseCaseProtocol { + IsFirstFamilyManagementUseCase(repository: makeAppRepository()) + } + + private func makeSaveFamilyManagementUseCase() -> SaveIsFirstFamilyManagementUseCaseProtocol { + SaveIsFirstFamilyManagementUseCase(repository: makeAppRepository()) } @@ -38,11 +46,15 @@ final class AppDIContainer: BaseContainer { // MARK: - Register func registerDependencies() { - container.register(type: FetchIsFirstFamilyManagementUseCaseProtocol.self) { _ in + container.register(type: IsFirstWidgetAlertUseCaseProtocol.self) { _ in + self.makeCheckIsFirstWidgetAlertUseCase() + } + + container.register(type: IsFirstFamilyManagementUseCaseProtocol.self) { _ in self.makeFetchFamilyManagementUseCase() } - container.register(type: UpdateFamilyManagementUseCaseProtocol.self) { _ in + container.register(type: SaveIsFirstFamilyManagementUseCaseProtocol.self) { _ in self.makeSaveFamilyManagementUseCase() } @@ -58,6 +70,10 @@ final class AppDIContainer: BaseContainer { container.register(type: AppUserDefaultsType.self) { _ in return AppUserDefaults() } + + container.register(type: SaveIsFirstWidgetAlertUseCaseProtocol.self) { _ in + return self.makeSaveWidgetAlertUseCase() + } } } diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/NavigatorDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/NavigatorDIContainer.swift index 455e810bc..daef02ea9 100644 --- a/14th-team5-iOS/App/Sources/Application/DIContainer/NavigatorDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/NavigatorDIContainer.swift @@ -38,7 +38,7 @@ final class NavigatorDIContainer: BaseContainer { } container.register(type: MainNavigatorProtocol.self) { _ in - MainNavigator(navigationController: makeUINavigationController()) + MainNavigator(navigationController: makeUINavigationController()) } container.register(type: SplashNavigatorProtocol.self) { _ in @@ -90,7 +90,7 @@ final class NavigatorDIContainer: BaseContainer { } container.register(type: FamilyEntranceNavigatorProtocol.self) { _ in - FamilyEntranceNavigator(navigationController: makeUINavigationController()) + FamilyEntranceNavigator(navigationController: makeUINavigationController()) } container.register(type: JoinFamilyNavigatorProtocol.self) { _ in @@ -116,6 +116,10 @@ final class NavigatorDIContainer: BaseContainer { navigationController: makeUINavigationController() ) } + + container.register(type: InputFamilyLinkNavigatorProtocol.self) { _ in + InputFamilyLInkNavigator(navigationController: makeUINavigationController()) + } } } diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/InputFamilyLinkNavigator.swift b/14th-team5-iOS/App/Sources/Application/Navigator/InputFamilyLinkNavigator.swift new file mode 100644 index 000000000..6e935d2d2 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/Navigator/InputFamilyLinkNavigator.swift @@ -0,0 +1,38 @@ +// +// InputFamilyLinkNavigator.swift +// App +// +// Created by 마경미 on 30.09.24. +// + +import Core +import UIKit + +protocol InputFamilyLinkNavigatorProtocol: BaseNavigator { + func toHome() + func pop() +} + +final class InputFamilyLInkNavigator: InputFamilyLinkNavigatorProtocol { + + // MARK: - Properties + + var navigationController: UINavigationController + + // MARK: - Intializer + + init(navigationController: UINavigationController) { + self.navigationController = navigationController + } + + // MARK: - To + + func pop() { + navigationController.popViewController(animated: true) + } + + func toHome() { + let vc = MainViewControllerWrapper().viewController + navigationController.setViewControllers([vc], animated: true) + } +} diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/MainNavigator.swift b/14th-team5-iOS/App/Sources/Application/Navigator/MainNavigator.swift index d045efa81..2b261fcf5 100644 --- a/14th-team5-iOS/App/Sources/Application/Navigator/MainNavigator.swift +++ b/14th-team5-iOS/App/Sources/Application/Navigator/MainNavigator.swift @@ -15,6 +15,7 @@ protocol MainNavigatorProtocol: BaseNavigator { // alert func showSurvivalAlert() func pickAlert(_ name: String) + func showWidgetAlert() func missionUnlockedAlert() @@ -51,7 +52,14 @@ final class MainNavigator: MainNavigatorProtocol { } func missionUnlockedAlert() { - BBAlert.style(.mission).show() + let handler: BBAlertActionHandler = { [weak self] alert in + self?.toCamera(.survival) + } + BBAlert.style(.takePhoto, primaryAction: handler).show() + } + + func showWidgetAlert() { + BBAlert.style(.widget).show() } func showToast(_ image: UIImage?, _ message: String) { diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/JoinFamily/FamilyEntranceControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/FamilyEntrance/FamilyEntranceControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/JoinFamily/FamilyEntranceControllerWrapper.swift rename to 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/FamilyEntrance/FamilyEntranceControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/JoinFamily/InputFamilyLinkViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/FamilyEntrance/InputFamilyLinkViewControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/JoinFamily/InputFamilyLinkViewControllerWrapper.swift rename to 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/FamilyEntrance/InputFamilyLinkViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/JoinFamily/JoinFamilyViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/FamilyEntrance/JoinFamilyViewControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/JoinFamily/JoinFamilyViewControllerWrapper.swift rename to 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/FamilyEntrance/JoinFamilyViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignIn/AccountSignInViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignIn/AccountSignInViewController.swift index e63cdf766..b4a153d8d 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignIn/AccountSignInViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignIn/AccountSignInViewController.swift @@ -113,7 +113,7 @@ public final class AccountSignInViewController: BaseViewController.just(.setEnableCommentTextField(false)), fetchCommentUseCase.execute(postId: postId, query: query) - .withUnretained(self) .concatMap { - // 통신에 실패한다면 - guard let comments = $0.1 else { - Haptic.notification(type: .error) - $0.0.navigator.showFetchFailureToast() - return Observable.concat( - Observable.just(.setComments([])), - Observable.just(.setHiddenTablePrgressHud(true)), - Observable.just(.setHiddenNoneCommentView(true)), - Observable.just(.setHiddenFetchFailureView(false)) - ) - } - // 댓글이 없다면 - if comments.results.isEmpty { + if $0.results.isEmpty { return Observable.concat( Observable.just(.setComments([])), Observable.just(.setBecomeFirstResponder(true)), @@ -136,7 +124,7 @@ final public class CommentViewReactor: Reactor { ) } - let cells = comments.results + let cells = $0.results .map { CommentCellReactor($0) } return Observable.concat( @@ -150,6 +138,21 @@ final public class CommentViewReactor: Reactor { Observable.just(.scrollTableToLast(true)) ) } + .catchError(with: self, of: APIWorkerError.self) { + switch $1 { + case .networkFailure: + Haptic.notification(type: .error) + $0.navigator.showFetchFailureToast() + return Observable.concat( + Observable.just(.setComments([])), + Observable.just(.setHiddenTablePrgressHud(true)), + Observable.just(.setHiddenNoneCommentView(true)), + Observable.just(.setHiddenFetchFailureView(false)) + ) + + default: return Observable.empty() + } + } ) case let .createComment(content): diff --git a/14th-team5-iOS/App/Sources/Presentation/Comment/ViewController/CommentViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Comment/ViewController/CommentViewController.swift index 03852e043..85230ce4c 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Comment/ViewController/CommentViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Comment/ViewController/CommentViewController.swift @@ -216,14 +216,12 @@ extension CommentViewController { cell.reactor = reactor return cell } - dataSource.canEditRowAtIndexPath = { let myMemberId = App.Repository.member.memberID.value let commentMemberId = $0[$1].currentState.comment.memberId return myMemberId == commentMemberId } - - return dataSource + return dataSource } } diff --git a/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Reactor/InputFamilyLinkReactor.swift b/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Reactor/InputFamilyLinkReactor.swift index 926b638ee..16a3d32b7 100644 --- a/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Reactor/InputFamilyLinkReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Reactor/InputFamilyLinkReactor.swift @@ -23,28 +23,20 @@ public final class InputFamilyLinkReactor: Reactor { // MARK: - Mutate public enum Mutation { case setLinkString(String) - case setShowHome(Bool) case setToastMessage(String) - case setPoped(Bool) } // MARK: - State public struct State { var linkString: String = "" - var isShowHome: Bool = false @Pulse var showToastMessage: String = "" - var isPoped: Bool = false } // MARK: - Properties - public let initialState: State + public let initialState: State = State() @Injected var familyUseCase: FamilyUseCaseProtocol - @Injected var joinFamilyUseCase: JoinFamilyUseCaseProtocol - - init() { - self.initialState = State() - } + @Navigator var navigator: InputFamilyLinkNavigatorProtocol } extension InputFamilyLinkReactor { @@ -65,7 +57,9 @@ extension InputFamilyLinkReactor { // Repository에서 이미 UserDefaults와 App.Repository에 저장하고 있음 App.Repository.member.familyId.accept(joinFamilyData.familyId) App.Repository.member.familyCreatedAt.accept(joinFamilyData.createdAt) - return Observable.just(Mutation.setShowHome(true)) + + self.navigator.toHome() + return .empty() } if App.Repository.member.familyId.value != nil { @@ -93,7 +87,8 @@ extension InputFamilyLinkReactor { } } case .tapPopButton: - return Observable.just(Mutation.setPoped(true)) + navigator.pop() + return .empty() } } @@ -103,12 +98,8 @@ extension InputFamilyLinkReactor { switch mutation { case let .setLinkString(link): newState.linkString = link - case let .setShowHome(isShow): - newState.isShowHome = isShow case let .setToastMessage(message): newState.showToastMessage = message - case let .setPoped(isPop): - newState.isPoped = isPop } return newState } diff --git a/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/ViewController/InputFamilyLinkViewController.swift b/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/ViewController/InputFamilyLinkViewController.swift index da6131e45..004ef822b 100644 --- a/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/ViewController/InputFamilyLinkViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/ViewController/InputFamilyLinkViewController.swift @@ -22,37 +22,13 @@ final class InputFamilyLinkViewController: BaseViewController.just(.setFamilyManagement(false)) } case .contributorNextButtonTap: @@ -247,11 +252,20 @@ extension MainViewReactor { self.pushViewController(type: .missionUnlockedAlert) return .empty() } - case .checkFamilyManagement: - return checkFamilyManagementUseCase.execute() + case .checkIsFirstFamilyManagement: + return isFirstFamilyManagementUseCase.execute() .flatMap { return Observable.just(.setFamilyManagement($0)) } + case .checkIsFirstWidgetAlert: + return isFirstWidgetAlertUseCase.execute() + .filter { $0 } + .withUnretained(self) + .flatMap { _ -> Observable in + self.pushViewController(type: .widgetAlert) + self.saveIsFirstWidgetAlertUseCase.execute(false) + return .empty() + } } } @@ -309,6 +323,8 @@ extension MainViewReactor { navigator.showToast(image, message) case .showErrorToast: navigator.showToast(DesignSystemAsset.warning.image, "에러가 발생했습니다") + case .widgetAlert: + navigator.showWidgetAlert() } } } diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift index 1ee4c2b1d..7f9fc2b1d 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift @@ -129,7 +129,12 @@ extension MainViewController { .disposed(by: disposeBag) Observable.just(()) - .map { Reactor.Action.checkFamilyManagement } + .map { Reactor.Action.checkIsFirstFamilyManagement } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + Observable.just(()) + .map { Reactor.Action.checkIsFirstWidgetAlert } .bind(to: reactor.action) .disposed(by: disposeBag) diff --git a/14th-team5-iOS/App/Sources/Presentation/Management/Reactor/FamilyNameSettingViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Management/Reactor/FamilyNameSettingViewReactor.swift index c76ed8e32..21191a200 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Management/Reactor/FamilyNameSettingViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Management/Reactor/FamilyNameSettingViewReactor.swift @@ -69,14 +69,14 @@ public final class FamilyNameSettingViewReactor: Reactor { .compactMap { $0 } .withUnretained(self) .flatMap { owner, familyGroupInfo -> Observable in - if familyGroupInfo.familyNameEditorId.isEmpty { + if familyGroupInfo.familyNameEditorId == nil { return .concat( .just(.setFamilyNickNameVaildation(false)), .just(.setFamilyGroupEditValidation(true)), .just(.setFamilyGroupInfoItem(familyGroupInfo)) ) } - let editorId = familyGroupInfo.familyNameEditorId + let editorId = familyGroupInfo.familyNameEditorId ?? "" return owner.fetchFamilyEditerUseCase.execute(memberId: editorId) .asObservable() .compactMap { $0 } diff --git a/14th-team5-iOS/App/Sources/Presentation/Management/Reactor/ManagementReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Management/Reactor/ManagementReactor.swift index 79f4ac348..e9b5226b3 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Management/Reactor/ManagementReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Management/Reactor/ManagementReactor.swift @@ -137,12 +137,11 @@ public final class ManagementReactor: Reactor { case .fetchFamilyGroupInfo: return fetchFamilyGroupInfoUseCase.execute() - .withUnretained(self) .flatMap { - guard let familyInfo = $0.1 else { + guard let familyName = $0?.familyName else { return Observable.just(.setFamilyName("나의 가족")) } - return Observable.just(.setFamilyName(familyInfo.familyName)) + return Observable.just(.setFamilyName(familyName)) } case let .fetchPaginationFamilyMemeber(refresh): diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/AlertViews/DefaultToastView/DefaultAlertView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/AlertViews/DefaultToastView/DefaultAlertView.swift index 692d3a3d8..cbff6d1d0 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/AlertViews/DefaultToastView/DefaultAlertView.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/AlertViews/DefaultToastView/DefaultAlertView.swift @@ -95,9 +95,9 @@ public class DefaultAlertView: UIView, BBAlertView { private func setupSubviewConstraints() { buttonStack.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - buttonStack.bottomAnchor.constraint(equalTo: self.layoutMarginsGuide.bottomAnchor), - buttonStack.leadingAnchor.constraint(equalTo: self.layoutMarginsGuide.leadingAnchor), - buttonStack.trailingAnchor.constraint(equalTo: self.layoutMarginsGuide.trailingAnchor), + buttonStack.bottomAnchor.constraint(equalTo: self.layoutMarginsGuide.bottomAnchor, constant: -4), + buttonStack.leadingAnchor.constraint(equalTo: self.layoutMarginsGuide.leadingAnchor, constant: 8), + buttonStack.trailingAnchor.constraint(equalTo: self.layoutMarginsGuide.trailingAnchor, constant: -8), ]) if case .vertical = viewConfig.buttonAxis { diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBErrorLogger.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBErrorLogger.swift new file mode 100644 index 000000000..54246ab93 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBErrorLogger.swift @@ -0,0 +1,22 @@ +// +// BBAPIErrorLogger.swift +// Core +// +// Created by 김건우 on 10/5/24. +// + +import Foundation + +// MARK: - Erorr Logger + +public protocol BBErrorLogger { + func log(error: any Error) + func log(localizedError error: E) where E: LocalizedError + func log(data: Data, response: URLResponse) +} + +extension BBErrorLogger { + public func log(error: any Error) { } + public func log(localizedError error: E) where E: LocalizedError { } + public func log(data: Data, response: URLResponse) { } +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/BBAPIErrorLogger.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/BBAPIErrorLogger.swift new file mode 100644 index 000000000..6369b8522 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/BBAPIErrorLogger.swift @@ -0,0 +1,23 @@ +// +// BBAPIErrorLogger.swift +// Core +// +// Created by 김건우 on 10/9/24. +// + +import Foundation + +public struct APIWorkerErrorLogger: BBErrorLogger { + + public init() { } + + /// 매개변수로 주어진 `Error`의 로그를 출력합니다. + /// - Parameter error: `Error` 프로토콜을 준수하는 에러입니다. + public func log(localizedError error: E) where E: LocalizedError { + var errorLog: String = "-- [APIWorker Error Log] ----------------------------------\n" + let description = " - DESCRIPTION: \(error.localizedDescription)\n" + errorLog.append(description) + print(errorLog + "----------------------------------------------------------\n") + } + +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/BBAPIErrorMapper.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/BBAPIErrorMapper.swift new file mode 100644 index 000000000..401c9b789 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/BBAPIErrorMapper.swift @@ -0,0 +1,36 @@ +// +// BBAPIErrorResolver.swift +// Core +// +// Created by 김건우 on 10/5/24. +// + +import Foundation + +// MARK: - API Error Mapper + +public protocol APIErrorMapper { + func map(networkError error: any Error) -> APIWorkerError +} + + +// MARK: - Default API Error Mapper + +public struct APIDefaultErrorMapper: APIErrorMapper { + + public init() { } + + /// `BBNetworkError` 타입의 에러를 `APIWorkerError` 타입의 에러로 변환합니다. + /// + /// 적합한 케이스로 변환이 어렵다면 `.unknown` 에러로 변환합니다. + /// + /// - Parameter error: `Error` 프로토콜을 준수하는 에러입니다. + /// - Returns: `APIWorkerError` + public func map(networkError error: any Error) -> APIWorkerError { + if let error = error as? BBNetworkError { + return .networkFailure(reason: error) + } + return .unknown(error) + } + +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/BBAPISpec.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/BBAPISpec.swift new file mode 100644 index 000000000..28ced21c4 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/BBAPISpec.swift @@ -0,0 +1,219 @@ +// +// APISpec.swift +// BBNetwork +// +// Created by 김건우 on 9/25/24. +// + +import Foundation + +import RxSwift + +// MARK: - BBAPI + +public protocol BBAPI { + var spec: Spec { get } +} + + +// MARK: - URL Generation Error + +/// URL 및 URLRequest 생성 중 발생하는 에러입니다. +public enum RequestGenerationError: Error { + + /// 잘못된 URL이 생성됨을 의미합니다. + case components + +} + +extension RequestGenerationError: CustomStringConvertible { + + public var description: String { + switch self { + case .components: return "Invalid Components" + } + } + +} + + +// MARK: - Requestable + +public protocol Requestable { + var method: BBNetworkMethod { get } + var path: String { get } + var queryParameters: BBNetworkParameters? { get } + var queryParametersEncodable: (any Encodable)? { get } + var bodyParameters: BBNetworkParameters? { get } + var bodyParametersEncodable: (any Encodable)? { get } + var headers: BBNetworkHeaders { get } + var bodyEncoder: any BBBodyEncoder { get } + + func urlRequest(_ config: any BBNetworkConfigurable) throws -> URLRequest +} + +extension Requestable { + + /// 전달된 구성 요소를 바탕으로 URLRequest를 생성합니다. + /// - Parameter config: HTTP 통신에 필요한 설정값입니다. 기본값은 `BBNetworkDefaultConfiguration()`입니다. + /// - Returns: URLRequest + public func urlRequest(_ config: any BBNetworkConfigurable = BBNetworkDefaultConfiguration()) throws -> URLRequest { + let url = try self.url(config) + var urlRequest = URLRequest(url: url) + + guard + let bodyParamters = try? bodyParametersEncodable?.toDictionary() + ?? self.bodyParameters?.toDictionary() ?? [:] + else { throw RequestGenerationError.components } + if !bodyParamters.isEmpty { + urlRequest.httpBody = bodyEncoder.encode(bodyParamters) + } + + urlRequest.headers = headers.asHTTPHeaders + urlRequest.httpMethod = method.asHTTPMethod.rawValue + urlRequest.timeoutInterval = 10 + return urlRequest + } + + private func url(_ config: any BBNetworkConfigurable) throws -> URL { + let baseUrl = config.baseUrl + + var urlString: String = path.hasPrefix(baseUrl) + ? path + : baseUrl + path + + urlString = replaceRegex(":/{3,}", "://", urlString) + urlString = replaceRegex("(? String { + guard + let regex = try? NSRegularExpression(pattern: pattern, options: []) + else { return string } + let range = NSMakeRange(0, string.count) + return regex.stringByReplacingMatches(in: string, options: [], range: range, withTemplate: replacement) + } + +} + +// MARK: - ResponseRequestable + +public protocol ResponseRequestable: Requestable { + var responseDecoder: any BBResponseDecoder { get } +} + + +// MARK: - Spec + +public struct Spec: ResponseRequestable { + + /// HTTP 호출 메서드입니다. + public let method: BBNetworkMethod + + /// 호출하고자 하는 URL의 경로입니다. + /// + /// 베이스 URL을 제외한 나머지 URL만 작성해야 합니다. + /// 예를 들어, 전체 URL이 **https://api.oing.kr/v1/families**라면 베이스 URL을 제외한 **/families**만 작성해야 합니다. + public let path: String + + /// 호출하고자 하는 API의 쿼리 파라미터입니다. + public let queryParameters: BBNetworkParameters? + + /// 호출하고자 하는 API의 쿼리 파라미터입니다. + /// + /// - Warning: 이 프로퍼티에 값이 있다면 `bodyParametersEncodable` 프로퍼티는 무시됩니다. + public let queryParametersEncodable: (any Encodable)? + + /// 호출하고자 하는 API의 요청 바디입니다. + /// + /// - Warning: 이 프로퍼티에 값이 있다면 `bodyParametersEncodable` 프로퍼티는 무시됩니다. + public let bodyParameters: BBNetworkParameters? + + /// 호출하고자 하는 API의 요청 바디입니다. Encodable 프로토콜을 준수하는 객체여야 합니다. + public let bodyParametersEncodable: (any Encodable)? + + /// 요청 헤더입니다. + public let headers: BBNetworkHeaders + + /// 요청 바디를 인코딩하는 인코더입니다. + public let bodyEncoder: any BBBodyEncoder + + /// HTTP 통신 결과로 받은 Data를 디코딩하는 디코더입니다. + public let responseDecoder: any BBResponseDecoder + + /// HTTP 통신에 필요한 재료 보따리를 만듭니다. + /// - Parameters: + /// - method: HTTP 메서드입니다. + /// - path: 베이스 URL을 제외한 나머지 경로입니다. + /// - queryParameters: 쿼리 파라미터입니다. 기본값은 `nil`입니다. + /// - queryParametersEncodable: 쿼리 파라미터입니다. `Encodable` 프로토콜을 준수해야 합니다. 기본값은 `nil`입니다. + /// - bodyParameters: 요청 바디입니다. 기본값은 `nil`입니다. + /// - bodyParametersEncodable: 요청 바디입니다. `Encodable` 프로토콜을 준수해야 합니다. 기본값은 `nil`입니다. + /// - headers: HTTP 요청 헤더입니다. 기본값은 `BBNetworkHeaders.default`입니다. + /// - bodyEncoder: 요청 바디 인코딩을 위한 인코더입니다. 기본값은 `BBDefaultBodyEncoder()`입니다. + /// - responseDecoder: HTTP 통신 결과로 받은 Data를 디코딩하는 디코더입니다. 기본값은 `BBDefaultResponderDecoder()`입니다. + public init( + method: BBNetworkMethod, + path: String, + queryParameters: BBNetworkParameters? = nil, + queryParametersEncodable: (any Encodable)? = nil, + bodyParameters: BBNetworkParameters? = nil, + bodyParametersEncodable: (any Encodable)? = nil, + headers: BBNetworkHeaders = BBNetworkHeaders.default, + bodyEncoder: any BBBodyEncoder = BBDefaultBodyEncoder(), + responseDecoder: any BBResponseDecoder = BBDefaultResponderDecoder() + ) { + self.method = method + self.path = path + self.queryParameters = queryParameters + self.queryParametersEncodable = queryParametersEncodable + self.bodyParameters = bodyParameters + self.bodyParametersEncodable = bodyParametersEncodable + self.headers = headers + self.bodyEncoder = bodyEncoder + self.responseDecoder = responseDecoder + } + +} + + + +// MARK: - Extensions + +private extension Dictionary where Key == BBNetworkParameterKey, Value == BBNetworkParameterValue { + + func toDictionary() -> [String: Any] { + var dict = [String: Any]() + self.forEach { key, value in + dict.updateValue(value.rawValue as Any, forKey: "\(key.rawValue)") + } + return dict + } + +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/BBAPIWorker.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/BBAPIWorker.swift new file mode 100644 index 000000000..9e83fbebd --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/BBAPIWorker.swift @@ -0,0 +1,195 @@ +// +// BBAPIWorker.swift +// BBNetwork +// +// Created by 김건우 on 9/25/24. +// + +import Foundation + +import Alamofire +import Combine +import RxAlamofire +import RxSwift + +// MARK: - Error + +/// HTTP 통신 및 디코딩, 쓰래드 전환 등 부가 기능 수행 중 발생하는 에러입니다. +public enum APIWorkerError: Error { + + /// 받아온 데이터가 없음을 의미합니다. + case noResponse + + /// 알 수 없는 에러가 발생했음을 의미합니다. + case unknown(Error) + + /// 파싱에 실패했음을 의미합니다. + case parsing(Error) + + /// 네트워크 통신 중 문제가 발생했음을 의미합니다. + case networkFailure(reason: BBNetworkError) + +} + +extension APIWorkerError { + + /// 발생한 에러가 네트워크 오류인 경우 해당 오류 원인을 반환합니다. + /// + /// - Returns: `BBNetworkError` 타입의 네트워크 에러 또는 `nil` + var underlyingError: BBNetworkError? { + if case let .networkFailure(reason) = self { + return reason + } + return nil + } + +} + +extension APIWorkerError: LocalizedError { + + public var errorDescription: String? { + switch self { + case .noResponse: + return "서버로부터 받아온 데이터가 없습니다." + case .unknown(let error): + return "알 수 없는 오류가 발생했습니다 [이유: \(error.localizedDescription)]" + case .parsing: + return "데이터를 처리하는 중에 문제가 발생했습니다. 서버에서 반환된 데이터가 예상한 형식과 맞지 않습니다." + case .networkFailure(let reason): + return "네트워크 통신 중 오류가 발생했습니다. [이유: \(reason.localizedDescription)]" + } + } +} + + +// MARK: - Workable + +public protocol Workable: AnyObject { + @discardableResult + func request( + _ spec: any ResponseRequestable, + on queue: any SchedulerType + ) -> Observable where D: Decodable + + @discardableResult + func request( + _ spec: any ResponseRequestable + ) -> Observable where D: Decodable +} + +// MARK: - Default API Worker + + +// MARK: - Combine API Worker + + +// MARK: - Rx API Worker + +open class BBRxAPIWorker { + + private let service: any BBNetworkService + private let errorMapper: any APIErrorMapper + private let errorLogger: any BBErrorLogger + + /// APIWorker 인스턴스를 생성합니다. + /// + /// - Parameters: + /// - service: 이 인스턴스가 사용하기를 원하는 `NetworkService`입니다. 기본값은 `BBNetworkDefaultService()`입니다. + /// - errorResolver: 이 인스턴스가 사용하기를 원하는 `APIErrorMapper`입니다. 기본값은 `APIDefaultErrorMapper()`입니다. + /// - errorLogger: 이 인스턴스가 사용하기를 원하는 `BBErrorLogger`입니다. 기본값은 `APIWorkerErrorLogger()`입니다. + public init( + with service: any BBNetworkService = BBNetworkDefaultService(), + errorMapper: any APIErrorMapper = APIDefaultErrorMapper(), + errorLogger: any BBErrorLogger = APIWorkerErrorLogger() + ) { + self.service = service + self.errorMapper = errorMapper + self.errorLogger = errorLogger + } + +} + +extension BBRxAPIWorker: Workable { + + /// 매개변수로 주어진 스펙(spec) 정보를 바탕으로 HTTP 통신을 수행합니다. + /// + /// HTTP 통신에 성공하면 디코딩된 값을 next 항목으로 방출하고, 실패한다면 `APIWorkerError` 타입의 에러가 담긴 error 항목을 방출합니다. + /// HTTP 통신 결과를 방출 할 때 스트림은 `queue` 매개변수로 주어진 쓰레드로 바뀝니다. + /// + /// - Parameters: + /// - spec: `ResponseRequestable` 프로토콜을 준수하는 스펙(spec)입니다. + /// - queue: HTTP 통신이 끝나면 흐르게 하는 쓰레드를 지정합니다. 기본값은 `RxScheduler.main`입니다. + /// - Returns: Observable\ + public func request( + _ spec: any ResponseRequestable, + on queue: any SchedulerType = RxScheduler.main + ) -> Observable where D: Decodable { + + Observable.create { [unowned self] observer in + let dataRequest = self.service.request(with: spec) { result in + switch result { + case let .success(data): + do { + let decoded: D = try self.decode(data, using: spec.responseDecoder) + observer.onNext(decoded) + observer.onCompleted() + } catch { + let apiError = self.map(error: error) + self.errorLogger.log(localizedError: apiError) + observer.onError(error) + } + + case let .failure(error): + let mappedError = self.errorMapper.map(networkError: error) + self.errorLogger.log(localizedError: mappedError) + observer.onError(mappedError) + } + } + + return Disposables.create { + let _ = dataRequest?.cancel() + } + } + .observe(on: queue) + + } + + /// 매개변수로 주어진 스펙(spec) 정보를 바탕으로 HTTP 통신을 수행합니다. + /// + /// HTTP 통신에 성공하면 디코딩된 값을 next 항목으로 방출하고, 실패한다면 `APIWorkerError` 타입의 에러가 담긴 error 항목을 방출합니다. + /// HTTP 통신 결과를 방출 할 때 스트림은 메인 쓰레드로 바뀝니다. + /// + /// - Parameters: + /// - spec: `ResponseRequestable` 프로토콜을 준수하는 스펙(spec)입니다. + /// - Returns: Observable\ + @discardableResult + public func request( + _ spec: any ResponseRequestable + ) -> Observable where D: Decodable { + + request(spec, on: RxScheduler.main) + + } + +} + +extension BBRxAPIWorker { + + private func map(error: any Error) -> APIWorkerError { + (error as? APIWorkerError) ?? APIWorkerError.unknown(error) + } + + private func decode( + _ data: Data?, + using decoder: any BBResponseDecoder + ) throws -> T where T: Decodable { + do { + guard let data = data else { throw APIWorkerError.noResponse } + let decodedData: T = try decoder.decode(from: data) + return decodedData + } catch { + throw APIWorkerError.parsing(error) + } + } + +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/BBNetworkConfiguration.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/BBNetworkConfiguration.swift new file mode 100644 index 000000000..4a2e1edd7 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/BBNetworkConfiguration.swift @@ -0,0 +1,37 @@ +// +// BBAPIConfiguration.swift +// BBNetwork +// +// Created by 김건우 on 9/25/24. +// + +import Foundation + +import Alamofire + + +// MARK: - Configrable + +public protocol BBNetworkConfigurable { + + var baseUrl: String { get } + +} + + +// MARK: - Default Configuration + +public struct BBNetworkDefaultConfiguration: BBNetworkConfigurable { + + public init() { } + + /// 베이스URL을 반환합니다. 빌드 환경에 따라 반환되는 URL이 달라집니다. + public var baseUrl: String = { + #if PRD + return "https://api.no5ing.kr/v1" + #else + return "https://dev.api.no5ing.kr/v1" + #endif + }() + +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/BBNetworkError.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/BBNetworkError.swift new file mode 100644 index 000000000..a39b72e8e --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/BBNetworkError.swift @@ -0,0 +1,112 @@ +// +// BBNetworkErrorLogger.swift +// Core +// +// Created by 김건우 on 10/5/24. +// + +import Foundation + +// MARK: - Error + +/// 네트워크 통신 중 발생하는 예외입니다. +public enum BBNetworkError: Error { + + /// 네트워크에 연결할 수 없음을 의미합니다. (오프라인 상태) + case notConnected + + /// 사용자 또는 시스템에 의해 통신이 취소되었음을 의미합니다. + case cancelled + + /// 요청 시간이 초과되었음을 의미합니다. + case timeout + + /// 잘못된 요청을 보냈음을 의미합니다. (상태 코드 400) + case badRequest + + /// 인증되지 않은 요청임을 의미합니다. (상태 코드 401) + case unauthorized + + /// 접근이 금지되었음을 의미합니다. (상태 코드 403) + case forbidden + + /// 요청한 리소스를 찾을 수 없음을 의미합니다. (상태 코드 404) + case notFound + + /// 허용되지 않은 메소드 요청을 의미합니다. (상태 코드 405) + case methodNotAllowed + + /// 요청이 갈등을 일으켰음을 의미합니다. (상태 코드 409) + case conflict + + /// 요청한 미디어 형식을 지원하지 않음을 의미합니다. (상태 코드 415) + case unsupportedMediaType + + /// 서버에서 내부 오류가 발생했음을 의미합니다. (상태 코드 500) + case internalServerError + + /// 서버의 기능이 구현되지 않았음을 의미합니다. (상태 코드 501) + case notImplemented + + /// 게이트웨이에서 잘못된 응답을 받았음을 의미합니다. (상태 코드 502) + case badGateway + + /// 서비스가 일시적으로 사용 불가능함을 의미합니다. (상태 코드 503) + case serviceUnavailable + + /// 게이트웨이 타임아웃을 의미합니다. (상태 코드 504) + case gatewayTimeout + + /// 기타 네트워크 에러가 발생했음을 의미합니다. 원본 에러와 함께 처리합니다. + case generic(Error) + + /// URL을 생성할 수 없음을 의미합니다. EndPoint에 잘못 기재된 요소는 없는지 확인하세요. + case urlGeneration + + /// 기타 네트워크 오류가 발생했음을 의미합니다. + case error(statusCode: Int) +} + +extension BBNetworkError: LocalizedError { + + public var errorDescription: String? { + switch self { + case .notConnected: + return "네트워크 연결이 되어 있지 않습니다. 인터넷 연결을 확인하세요. 오프라인 상태에서는 일부 기능이 제한될 수 있습니다." + case .cancelled: + return "요청이 취소되었습니다. 사용자가 요청을 중단했거나, 네트워크 연결이 끊어졌을 수 있습니다." + case .timeout: + return "요청 시간이 초과되었습니다. 서버 응답이 너무 느리거나 네트워크가 불안정할 수 있습니다. 다시 시도해 주세요." + case .badRequest: + return "잘못된 요청입니다. 서버에 유효하지 않은 데이터를 보냈습니다. 입력값을 다시 확인하고 요청을 시도하세요. (상태 코드 400)" + case .unauthorized: + return "인증되지 않은 요청입니다. 로그인 상태가 유효하지 않거나 인증 토큰이 만료되었을 수 있습니다. 다시 로그인한 후 요청을 시도하세요. (상태 코드 401)" + case .forbidden: + return "접근이 금지되었습니다. 이 리소스에 대한 접근 권한이 없으므로 요청이 거부되었습니다. 권한이 있는지 확인해 주세요. (상태 코드 403)" + case .notFound: + return "요청한 리소스를 찾을 수 없습니다. 요청 URL이 올바른지, 또는 리소스가 존재하는지 확인하세요. (상태 코드 404)" + case .methodNotAllowed: + return "허용되지 않은 HTTP 메소드로 요청했습니다. 해당 리소스에서 지원하는 HTTP 메소드를 확인하세요. (상태 코드 405)" + case .conflict: + return "요청이 서버의 현재 상태와 충돌을 일으켰습니다. 리소스의 상태를 확인하고 다시 요청하세요. (상태 코드 409)" + case .unsupportedMediaType: + return "서버에서 지원하지 않는 미디어 형식의 요청입니다. 지원되는 형식을 확인하고 다시 시도하세요. (상태 코드 415)" + case .internalServerError: + return "서버 내부 오류가 발생했습니다. 서버 측에서 문제가 발생했을 수 있습니다. 잠시 후 다시 시도하세요. (상태 코드 500)" + case .notImplemented: + return "서버에서 아직 구현되지 않은 기능을 요청했습니다. (상태 코드 501)" + case .badGateway: + return "게이트웨이 또는 프록시 서버에서 잘못된 응답을 받았습니다. 잠시 후 다시 시도하세요. (상태 코드 502)" + case .serviceUnavailable: + return "서버가 과부하 상태이거나 유지보수 중입니다. 잠시 후 다시 시도하세요. (상태 코드 503)" + case .gatewayTimeout: + return "게이트웨이 서버가 응답하지 않아서 요청이 시간 초과되었습니다. 잠시 후 다시 시도하세요. (상태 코드 504)" + case .generic(let error): + return "알 수 없는 오류가 발생했습니다: \(error.localizedDescription)" + case .urlGeneration: + return "유효하지 않은 URL이 생성되었습니다. 요청 URL을 확인하세요." + case .error(let statusCode): + return "알 수 없는 HTTP 오류가 발생했습니다. (상태 코드: \(statusCode))" + } + } +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/BBNetworkErrorLogger.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/BBNetworkErrorLogger.swift new file mode 100644 index 000000000..78afb070d --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/BBNetworkErrorLogger.swift @@ -0,0 +1,16 @@ +// +// BBNetworkErrorLogger.swift +// Core +// +// Created by 김건우 on 10/9/24. +// + +import Foundation + +public struct BBNetworkErrorLogger: BBErrorLogger { + + public init() { } + + public func log(localizedError error: E) where E : LocalizedError { } + +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/BBNetworkEventMonitor.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/BBNetworkEventMonitor.swift new file mode 100644 index 000000000..02a4b26cd --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/BBNetworkEventMonitor.swift @@ -0,0 +1,95 @@ +// +// BBNetworkInterceptor.swift +// Core +// +// Created by 김건우 on 10/5/24. +// + +import Foundation + +import Alamofire + +// MARK: - Event Monitor + +public protocol BBNetworkEventMonitor: EventMonitor { } + +extension BBNetworkEventMonitor { + func isSuccessfulStatusCode(_ dataRespnse: DataResponse) -> Bool { + guard + let statusCode = dataRespnse.response?.statusCode, + (200..<300) ~= statusCode else { + return false + } + return true + } +} + + +// MARK: - Default Logger + +public final class BBNetworkDefaultLogger { + public init() { } + public var queue = DispatchQueue(label: "com.bibbi.logger.queue") +} + +extension BBNetworkDefaultLogger: BBNetworkEventMonitor { + + public func requestDidFinish(_ request: Request) { + var httpLog = "-- [BBNetwork Request Log] ----------------------------\n" + + let urlString = request.request?.url?.absoluteString ?? "(unknown)" + let httpMethod = request.request?.httpMethod ?? "(unknown)" + + var allHeadersString = "[\n" + request.request?.allHTTPHeaderFields? + .forEach { allHeadersString.append("\t ・ \($0.key): \($0.value)\n") } + allHeadersString.append("]") + + let httpBody = request.request?.httpBody?.toPrettyPrintedString + + httpLog.append("- URL: \(urlString)\n") + httpLog.append("- METHOD: \(httpMethod)\n") + httpLog.append("- HEADERS: \(allHeadersString)\n") + if let httpBody = httpBody { + httpLog.append("- HTTP BODY: \(httpBody)\n") + } + + BBLogger.logInfo(category: "Network", message: httpLog) + } + + public func request( + _ request: DataRequest, + didParseResponse response: DataResponse + ) { + guard isSuccessfulStatusCode(response) else { return } + + let urlString = request.request?.url?.absoluteString ?? "(unknown)" + let statusCode = response.response?.statusCode.description ?? "(unknown)" + let httpMethod = request.request?.httpMethod ?? "(unknown)" + + var httpLog = "-- [BBNetwork Response Log] ----------------------------\n" + + var allHeadersString = "[\n" + request.request?.allHTTPHeaderFields? + .forEach { allHeadersString.append("\t ・ \($0.key): \($0.value)\n") } + allHeadersString.append("]") + + var responseDataString = "" + responseDataString.append(response.data?.toPrettyPrintedString ?? "(unknown)") + + httpLog.append("- URL: \(urlString)\n") + httpLog.append("- METHOD: \(httpMethod)\n") + httpLog.append("- HEADERS: \(allHeadersString)\n") + httpLog.append("- STATUS CODE: \(statusCode)\n") + httpLog.append("- RESONSE DATA: \(responseDataString)\n") + + BBLogger.logInfo(category: "Network", message: httpLog) + } + + public func request( + _ request: Request, + didFailTask task: URLSessionTask, + earlyWithError error: AFError + ) { } + +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/BBNetworkInterceptor.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/BBNetworkInterceptor.swift new file mode 100644 index 000000000..1991888ed --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/BBNetworkInterceptor.swift @@ -0,0 +1,85 @@ +// +// BBIntercepter.swift +// BBNetwork +// +// Created by 김건우 on 9/25/24. +// + +import Foundation + +import Alamofire +import RxSwift + +// MARK: - Default Interceptor + +public final class BBNetworkDefaultInterceptor { + public init() { } + private let session: BBNetworkSession = .refresh +} + +extension BBNetworkDefaultInterceptor: RequestInterceptor { + + public func adapt( + _ urlRequest: URLRequest, + for session: Alamofire.Session, + completion: @escaping (Result + ) -> Void) { + completion(.success(urlRequest)) + } + + public func retry( + _ request: Request, + for session: Session, + dueTo error: any Error, + completion: @escaping (RetryResult) -> Void + ) { + + if let response = request.response, response.statusCode != 401 { + completion(.doNotRetry) + return + } + + guard let authToken: AuthToken = KeychainWrapper.standard.object(forKey: .accessToken) else { + completion(.doNotRetry) + return + } + + var refreshedAuthToken: AuthToken? = nil + refreshAuthToken(authToken.refreshToken) { dataResponse in + + switch dataResponse.result { + case let .success(data): + refreshedAuthToken = data?.decode(AuthToken.self) + KeychainWrapper.standard.set(refreshedAuthToken, forKey: "accessToken") + completion(.retry) + + case let .failure(error): + // KeychainWrapper.standard.removeAllKeys() + completion(.doNotRetryWithError(error)) + } + } + + } + +} + +extension BBNetworkDefaultInterceptor { + + private func refreshAuthToken( + _ refreshToken: String, + completion: @escaping (AFDataResponse) -> Void + ) { + let endpoint = Spec( + method: .post, + path: "/auth/refresh", + bodyParameters: ["refreshToken": "\(refreshToken)"], + headers: .unAuthorized + ) + + guard let urlRequest = try? endpoint.urlRequest() else { + return + } + let _ = session.request(with: urlRequest, completion: completion) + } + +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/BBNetworkService.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/BBNetworkService.swift new file mode 100644 index 000000000..f2988d02e --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/BBNetworkService.swift @@ -0,0 +1,137 @@ +// +// BBNetworkService.swift +// Core +// +// Created by 김건우 on 10/5/24. +// + +import Foundation + +import Alamofire + +// MARK: - Cancellable + +public protocol BBNetworkCancellable { + func cancel() -> Self +} + +extension Alamofire.Request: BBNetworkCancellable { } + + +// MARK: - Network Service + +public protocol BBNetworkService { + typealias CompletionHandler = (Result) -> Void + + func request( + with spec: any Requestable, + completion: @escaping CompletionHandler + ) -> (any BBNetworkCancellable)? +} + + +// MARK: - Default Network Service + +public final class BBNetworkDefaultService { + + private let config: any BBNetworkConfigurable + private let sessionManager: any BBNetworkSessionManager + private let errorLogger: any BBErrorLogger + + + /// 네트워크 통신을 위한 Network 서비스를 만듭니다. + /// - Parameters: + /// - config: HTTP 통신에 필요한 설정값입니다. 기본값은 `BBNetworkDefaultConfigraion()`입니다. + /// - sessionManager: HTTP 통신에 쓰이는 세션입니다. 기본값은 `BBNetworkDefaultSession()`입니다. + /// - logger: HTTP 통신 시 쓰이는 로거입니다. 기본값은 `BBNetwortErrorLogger()`입니다. + public init( + config: any BBNetworkConfigurable = BBNetworkDefaultConfiguration(), + sessionManager: any BBNetworkSessionManager = BBNetworkSession.default, + errorLogger: any BBErrorLogger = BBNetworkErrorLogger() + ) { + self.config = config + self.sessionManager = sessionManager + self.errorLogger = errorLogger + } + + private func request( + request: URLRequest, + completion: @escaping CompletionHandler + ) -> any BBNetworkCancellable { + + let dataRequest = sessionManager.request(with: request) { dataResponse in + + if let statusCode = dataResponse.response?.statusCode { + guard (200..<300) ~= statusCode else { + let networkError = self.map(statusCode: statusCode) + completion(.failure(networkError)) + return + } + completion(.success(dataResponse.data)) + } + + } + + return dataRequest + + } + + private func map(statusCode code: Int) -> BBNetworkError { + switch code { + case 400: + return .badRequest + case 401: + return .unauthorized + case 403: + return .forbidden + case 404: + return .notFound + case 405: + return .methodNotAllowed + case 409: + return .conflict + case 415: + return .unsupportedMediaType + case 500: + return .internalServerError + case 501: + return .notImplemented + case 502: + return .badGateway + case 503: + return .serviceUnavailable + case 504: + return .gatewayTimeout + default: + return .error(statusCode: code) + } + } + +} + +extension BBNetworkDefaultService: BBNetworkService { + + /// 매개변수로 전달된 스펙(spec)을 바탕으로 HTTP 통신을 수행합니다. + /// + /// URLReqeust 생성에 실패한다면 `urlGeneration` 에러를 던집니다. + /// 시간 초과, 타임아웃, 잘못된 요청 등 네트워크 에러가 발생한다면 그에 맞는 에러를 던집니다. 자세한 정보는 `BBNetworkError`를 참조하세요. + /// - Parameters: + /// - endpoint: 통신에 사용할 스펙입니다. + /// - completion: 통신이 완료되면 처리할 핸들러입니다. + /// - Returns: `(any BBNetworkCancellable)?` + /// + /// - seealso: ``BBNetworkError`` + public func request( + with spec: any Requestable, + completion: @escaping CompletionHandler + ) -> (any BBNetworkCancellable)? { + do { + let urlRequest = try spec.urlRequest(config) + return request(request: urlRequest, completion: completion) + } catch { + completion(.failure(.urlGeneration)) + return nil + } + } + +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/BBNetworkSessionManager.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/BBNetworkSessionManager.swift new file mode 100644 index 000000000..e7014afc6 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/BBNetworkSessionManager.swift @@ -0,0 +1,112 @@ +// +// File.swift +// BBNetwork +// +// Created by 김건우 on 9/25/24. +// + +import Foundation + +import Alamofire + +// MARK: - Session Manager + +public protocol BBNetworkSessionManager { + typealias CompletionHandler = (AFDataResponse) -> Void + + func request( + with request: URLRequest, + completion: @escaping CompletionHandler + ) -> BBNetworkCancellable + +} + + +// MARK: - Network Session + +public class BBNetworkSession { + + /// 가장 기본적인 네트워크 세션입니다. + public static let `default`: BBNetworkSession = BBNetworkSession() + + /// 토큰 리프레시용 네트워크 세션입니다. + public static let refresh: BBNetworkSession = BBNetworkSession(interceptor: nil) + + /// 세션은 생명 주기 동안 Alamofire의 `Request` 타입을 생성하고 관리합니다. + /// 또한, 세션은 큐잉(queuing), 인터셉터, 신뢰 관리, 리다이렉트와 캐시 응답 처리를 포함한 모든 요청에 대한 보편적인 기능을 제공합니다. + private var session: Session = .default + + + /// 네트워크 세션을 만듭니다. + /// + /// - Parameters: + /// - configuration: 내부 `URLSession`을 생성할 때 사용할 `URLSessionConfiguration`입니다. 이니셜라이저를 거친 다음 해당 값에 대한 변경사항은 반영되지 않습니다. 기본값은 `URLSessionConfiguration.af.default`입니다. + /// + /// - delegate: `session`의 델리게이트 콜백과 `request` 상호작용을 처리할 `SessionDelegate`입니다. 기본값은 `SessionDelegate()`입니다. + /// + /// - rootQueue: 모든 내부 콜백과 상태 업데이트를 처리하는 기본 `DispatchQueue`입니다. **반드시** 직렬 큐여야 합니다. 기본값은 `DispatchQueue(label: "bibbi.com.rootQueue")`입니다. + /// + /// - startRequestsImmediately: 모든 `Request`를 자동으로 시작할 지 결정합니다. 기본값은 `true`입니다. 만약 `false`로 설정한다면, 모든 `Request`는 `resume()` 메서드를 호출해 시작해야 합니다. + /// + /// - requestQueue: `URLRequest` 생성을 수행하는 `DispatchQueue`입니다. 기본적으로 rootQueue를 타겟으로 사용합니다. 요청 생성이 병목을 유발하는 경우 신중한 테스트과 프로파일링 후 별도 큐를 사용할 수 있습니다. 기본값은 `nil`입니다. + /// + /// - serializationQueue:ㅡ모든 응답 직렬화를 수행할 `DispatchQueue`입니다. 기본적으로 rootQueue를 타겟으로 사용합니다. 요청 직렬이 병복을 유발하는 경우 신중한 테스트와 프로파일링 후 별도 큐를 사용할 수 있습니다. 기본값은 `nil`입니다. + /// + /// - interceptor: 이 인스턴스에 의해 생성된 `Request`가 사용할 `RequestInterceptor`입니다. 기본값은 `nil`입니다. + /// + /// - serverTrustManager: 이 인스턴스에서 신뢰 평가에 사용될 `ServerTrustManager`입니다. 기본값은 `nil`입니다. + /// + /// - redirectHandler: 이 인스턴스에 의해 생성된 `Request`가 사용할 `RedirectHandler`입니다. 기본값은 `nil`입니다. + /// + /// - cachedResponseHandler: 이 인스턴스에 의해 생성된 `Request`가 사용할 `CachedResponseHandler`입니다. 기본값은 `nil`입니다. + /// + /// - eventMonitors: 이 인스턴스에 의해 사용될 추가적인 `EventMonitor`입니다. Alamofire는 항상 기본값으로 `AlamofireNotification`과 `EventMonitor`를 추가합니다. 기본값은 `[]`입니다. + /// + /// - seealso: `Alamofire.Session` + public init( + configuration: URLSessionConfiguration = URLSessionConfiguration.af.default, + delegate: SessionDelegate = SessionDelegate(), + rootQueue: DispatchQueue = DispatchQueue(label: "bibbi.com.rootQueue"), + startRequestsImmediately: Bool = true, + requestQueue: DispatchQueue? = nil, + serializtionQueue: DispatchQueue? = nil, + interceptor: (any RequestInterceptor)? = BBNetworkDefaultInterceptor(), + serverTrustManger: ServerTrustManager? = nil, + redirectHandler: (any RedirectHandler)? = nil, + cachedResponseHandler: (any CachedResponseHandler)? = nil, + eventMonitors: [any BBNetworkEventMonitor] = [BBNetworkDefaultLogger()] + ) { + self.session = Session( + configuration: configuration, + delegate: delegate, + rootQueue: rootQueue, + startRequestsImmediately: startRequestsImmediately, + requestQueue: requestQueue, + serializationQueue: serializtionQueue, + interceptor: interceptor, + serverTrustManager: serverTrustManger, + redirectHandler: redirectHandler, + cachedResponseHandler: cachedResponseHandler, + eventMonitors: eventMonitors + ) + } + +} + +extension BBNetworkSession: BBNetworkSessionManager { + + /// 전달된 URLRequest를 바탕으로 HTTP 통신을 수행합니다. + /// - Parameters: + /// - request: 통신에 사용할 `URLRequset`입니다. + /// - completion: 통신이 완료되면 처리할 핸들러입니다. + /// - Returns: `any BBNetworkCancellable` + public func request( + with request: URLRequest, + completion: @escaping CompletionHandler + ) -> any BBNetworkCancellable { + let dataRequest = session.request(request).response(completionHandler: completion) + dataRequest.resume() + return dataRequest + } + +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/Components/BBNetworkHeader.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/Components/BBNetworkHeader.swift new file mode 100644 index 000000000..ecd264ae3 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/Components/BBNetworkHeader.swift @@ -0,0 +1,119 @@ +// +// BBNetworkHeader.swift +// BBNetwork +// +// Created by 김건우 on 9/25/24. +// + +import Foundation + +import Alamofire + +// MARK: - Typealias + +public typealias BBNetworkHeaders = [BBNetworkHeader] + + +// MARK: - Header + +/// 서버에 전달하는 부가적인 정보입니다. +public enum BBNetworkHeader { + + case xAppKey + case xAuthToken + case xUserPlatform + case xUserId + case contentType + +} + + +// MARK: - Extensions + +public extension BBNetworkHeader { + + /// 헤더의 키입니다. + var key: String { + switch self { + case .xAppKey: return "X-APP-KEY" + case .xAuthToken: return "X-AUTH-TOKEN" + case .xUserPlatform: return "X-USER-PLATFORM" + case .xUserId: return "X-USER-ID" + case .contentType: return "Content-Type" + } + } + + /// 헤더가 가지는 실질적인 값입니다. + var value: String { + switch self { + case .xAppKey: return fetchXAppKey() + case .xAuthToken: return fetchXAuthTokenValue() + case .xUserPlatform: return fetchXUserPlatform() + case .xUserId: return fetchXuserId() + case .contentType: return fetchContentType() + } + } + + /// `BBNetworkHeader`를 `HTTPHeader` 타입으로 변환합니다. + var asHTTPHeader: HTTPHeader { + HTTPHeader(name: key, value: value) + } + +} + +public extension BBNetworkHeaders { + + /// 가장 일반적인 헤더 모음입니다. + static var `default`: [BBNetworkHeader] { + [.xAppKey, .xAuthToken, .xUserPlatform, .xUserId, .contentType] + } + + /// 인증이 필요없는 API 요청에 사용되는 헤더 모음입니다. + static var unAuthorized: [BBNetworkHeader] { + [.xAppKey, .xUserPlatform, .contentType] + } + +} + + + + + +private extension BBNetworkHeader { + + func fetchXAppKey() -> String { + Bundle.main.xAppKey + } + + func fetchXAuthTokenValue() -> String { + if let authToken: AuthToken = KeychainWrapper.standard[.accessToken] { + return authToken.accessToken + } + return "" + } + + func fetchXuserId() -> String { + if let memberId: String = UserDefaultsWrapper.standard[.memberId] { + return memberId + } + return "" + } + + func fetchXUserPlatform() -> String { + return "iOS" + } + + func fetchContentType() -> String { + return "application/json" + } + +} + +public extension Array where Element == BBNetworkHeader { + + /// `[BBNetworkHeader]`를 `HTTPHeaders` 타입으로 변환합니다. + var asHTTPHeaders: HTTPHeaders { + HTTPHeaders(self.map { $0.asHTTPHeader }) + } + +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/Components/BBNetworkMethod.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/Components/BBNetworkMethod.swift new file mode 100644 index 000000000..bcf7832f7 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/Components/BBNetworkMethod.swift @@ -0,0 +1,41 @@ +// +// BBAPIMethod.swift +// BBNetwork +// +// Created by 김건우 on 9/25/24. +// + +import Foundation + +import Alamofire + +/// 서버가 수행해야 할 동작입니다. +public enum BBNetworkMethod { + + /// 데이터 조회 + case get + + /// 데이터 대체 및 수정 + case put + + /// 데이터 추가 및 등록 + case post + + /// 데이터 삭제 + case delete + +} + +public extension BBNetworkMethod { + + /// `BBAPIMethod`를 `HTTPMethod` 타입으로 변환합니다. + var asHTTPMethod: HTTPMethod { + switch self { + case .get: return HTTPMethod.get + case .put: return HTTPMethod.put + case .post: return HTTPMethod.post + case .delete: return HTTPMethod.delete + } + } + +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/Components/BBNetworkParameter.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/Components/BBNetworkParameter.swift new file mode 100644 index 000000000..d3ee6e6d4 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/Components/BBNetworkParameter.swift @@ -0,0 +1,114 @@ +// +// BBParameter.swift +// Data +// +// Created by 김건우 on 9/28/24. +// + +import Foundation + +// MARK: - Typealias + +/// 서버에 전달하는 파라미터 입니다. +/// +/// `BBNetworkParameter`는 서버에 전달하는 쿼리 및 바디 파라미터를 손쉽게 정의하도록 도와줍니다. +/// +/// 파라미터 키(Key)는 문자열로만 입력할 수 있습니다. 파라미터 키에는 `page`, `size`, `sort`, `date`와 같이 자주 사용하는 키가 미리 정의되어 있습니다. +/// +/// 파라미터 값(Value)는 정수(Int), 부동소수점(Float), 부울(Bool), 문자열 및 문자열 보간(String)과 nil이 들어갈 수 있습니다. 파라미터 값에 nil을 넣으면 빈 문자열이 삽입됩니다. +/// +/// 아래 코드는 파라미터를 만드는 기본적인 방법을 보여줍니다. +/// ```swift +/// let pagingParameters: BBNetworkParameters = [.page: 3, "size": "10", .sort: "ASC"] +/// let commentParameters: BBNetworkParameters = ["content": "\(content)", "mention": nil] +/// ``` +public typealias BBNetworkParameters = [BBNetworkParameter.Key: BBNetworkParameter.Value] + +public typealias BBNetworkParameterKey = BBNetworkParameter.Key +public typealias BBNetworkParameterValue = BBNetworkParameter.Value + + +// MARK: - BBParameter + +public struct BBNetworkParameter { + + + // MARK: - Key + + /// 파라미터 키입니다. + public struct Key: RawRepresentable, ExpressibleByStringLiteral { + + public let rawValue: String + + public init?(rawValue: String) { + self.rawValue = rawValue + } + + public init(stringLiteral value: StringLiteralType) { + self.rawValue = value + } + + } + + + // MARK: - Value + + /// 파라미터 값입니다. + public struct Value: RawRepresentable, ExpressibleByIntegerLiteral, ExpressibleByFloatLiteral, ExpressibleByBooleanLiteral, ExpressibleByStringInterpolation, ExpressibleByNilLiteral { + + public let rawValue: String + + public init?(rawValue: String) { + self.rawValue = rawValue + } + + public init(integerLiteral value: IntegerLiteralType) { + self.rawValue = "\(value)" + } + + public init(floatLiteral value: FloatLiteralType) { + self.rawValue = "\(value)" + } + + public init(booleanLiteral value: BooleanLiteralType) { + self.rawValue = "\(value)" + } + + public init(nilLiteral: ()) { + self.rawValue = "" + } + + public init(stringLiteral value: StringLiteralType) { + self.rawValue = value + } + + public init(stringInterpolation: DefaultStringInterpolation) { + self.rawValue = stringInterpolation.description + } + + } + +} + +// MARK: - Extensions + +extension BBNetworkParameterKey: Hashable { } + + + +public extension BBNetworkParameterKey { + + static var page: Self = "page" + static var size: Self = "size" + static var sort: Self = "sort" + static var date: Self = "date" + static var type: Self = "type" + static var memberId: Self = "memberId" + static var provider: Self = "provider" + + static var content: Self = "content" + static var refreshToken: Self = "refreshToken" + +} + +public extension BBNetworkParameterValue { } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Utils/BBBodyEncoder.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Utils/BBBodyEncoder.swift new file mode 100644 index 000000000..aecdf9443 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Utils/BBBodyEncoder.swift @@ -0,0 +1,24 @@ +// +// BBBodyEncoder.swift +// Data +// +// Created by 김건우 on 10/3/24. +// + +import Foundation + +// MARK: - Body Encoder + +public protocol BBBodyEncoder { + func encode(_ parameters: [String: Any]) -> Data? +} + + +// MARK: - Default Body Encoder + +public struct BBDefaultBodyEncoder: BBBodyEncoder { + public func encode(_ parameters: [String : Any]) -> Data? { + return try? JSONSerialization.data(withJSONObject: parameters) + } + public init() { } +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Utils/BBResponseDecoder.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Utils/BBResponseDecoder.swift new file mode 100644 index 000000000..7275f7833 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Utils/BBResponseDecoder.swift @@ -0,0 +1,40 @@ +// +// BBResponseDecoder.swift +// Data +// +// Created by 김건우 on 10/2/24. +// + +import Foundation + +// MARK: - Response Decoder + +public protocol BBResponseDecoder { + func decode(from data: Data) throws -> T where T: Decodable +} + + +// MARK: - JSON Default Response Decoder + +public struct BBDefaultResponderDecoder: BBResponseDecoder { + private let decoder = JSONDecoder() + public init() { } + public func decode(from data: Data) throws -> T where T : Decodable { + return try decoder.decode(T.self, from: data) + } +} + + +// MARK: - JSON Iso8601 Response Decoder + +public struct BBIso8601ResponderDecoder: BBResponseDecoder { + private let decoder = { + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 + return decoder + } + public init() { } + public func decode(from data: Data) throws -> T where T : Decodable { + return try decoder().decode(T.self, from: data) + } +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/KeychainWrapper/KeychainWrapper.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/KeychainWrapper/KeychainWrapper.swift index c76c65355..e9477540e 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/KeychainWrapper/KeychainWrapper.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/KeychainWrapper/KeychainWrapper.swift @@ -10,6 +10,7 @@ import Foundation final public class KeychainWrapper { // MARK: - Properties + public static let standard = KeychainWrapper() private let SecMatchLimit: String! = kSecMatchLimit as String @@ -30,7 +31,9 @@ final public class KeychainWrapper { return Bundle.main.bundleIdentifier ?? "KeychainWrapper" }() + // MARK: - Intializer + public init( serviceName: String, accessGroup: String? = nil @@ -131,7 +134,7 @@ final public class KeychainWrapper { forKey key: String, withAccessibility accessibility: KeychainItemAccessibility? = nil, isSynchronizable: Bool = false - ) -> NSCoding? { + ) -> (any NSCoding)? { guard let keychainData = data( forKey: key, withAccessibility: accessibility, @@ -143,6 +146,22 @@ final public class KeychainWrapper { return try? NSKeyedUnarchiver.unarchivedObject(ofClass: NSNumber.self, from: keychainData) } + public func object( + forKey key: String, + withAccessibility accessibility: KeychainItemAccessibility? = nil, + isSynchronizable: Bool = false + ) -> T? where T: Decodable { + guard let keychainData = data( + forKey: key, + withAccessibility: accessibility, + isSynchronizable: isSynchronizable + ) else { + return nil + } + + return try? JSONDecoder().decode(T.self, from: keychainData) + } + public func data( forKey key: String, withAccessibility accessibility: KeychainItemAccessibility? = nil, @@ -251,7 +270,7 @@ final public class KeychainWrapper { @discardableResult public func set( - _ value: NSCoding, + _ value: any NSCoding, forKey key: String, withAccessibility accessibiilty: KeychainItemAccessibility? = nil, isSynchronizable: Bool = false @@ -271,6 +290,25 @@ final public class KeychainWrapper { } } + @discardableResult + public func set( + _ value: any Encodable, + forKey key: String, + withAccessibility accessibiilty: KeychainItemAccessibility? = nil, + isSynchronizable: Bool = false + ) -> Bool { + if let data = try? JSONEncoder().encode(value) { + return set( + data, + forKey: key, + withAccessibility: accessibiilty, + isSynchronizable: isSynchronizable + ) + } else { + return false + } + } + @discardableResult public func set( _ value: Data, @@ -384,7 +422,8 @@ final public class KeychainWrapper { } - // MARK: - KeychainQueryDictionary + // MARK: - Keychain Query Dictionary + private func setupKeychainQueryDictionary( forkey key: String, withAccessibility accessibility: KeychainItemAccessibility? = nil, diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/KeychainWrapper/KeychainWrapperSubscript.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/KeychainWrapper/KeychainWrapperSubscript.swift index c63af1c1b..d41ceb7bf 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/KeychainWrapper/KeychainWrapperSubscript.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/KeychainWrapper/KeychainWrapperSubscript.swift @@ -49,7 +49,15 @@ public extension KeychainWrapper { } } - subscript(key: Key) -> NSCoding? { + subscript(key: Key) -> (any NSCoding)? { + get { object(forKey: key) } + set { + guard let value = newValue else { return } + set(value, forKey: key.rawValue) + } + } + + subscript(key: Key) -> T? where T: Codable { get { object(forKey: key) } set { guard let value = newValue else { return } @@ -89,7 +97,11 @@ public extension KeychainWrapper { string(forKey: key.rawValue) } - func object(forKey key: Key) -> NSCoding? { + func object(forKey key: Key) -> (any NSCoding)? { + object(forKey: key.rawValue) + } + + func object(forKey key: Key) -> T? where T: Decodable { object(forKey: key.rawValue) } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/UserDefaultsWrapper/Models/AuthToken.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/UserDefaultsWrapper/Models/AuthToken.swift new file mode 100644 index 000000000..4196385ae --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/UserDefaultsWrapper/Models/AuthToken.swift @@ -0,0 +1,24 @@ +// +// AuthToken.swift +// Core +// +// Created by 김건우 on 10/3/24. +// + +import Foundation + +public struct AuthToken: Codable { + + public let accessToken: String + public let refreshToken: String + public let isTemporaryToken: Bool + + public init(accessToken: String, refreshToken: String, isTemporaryToken: Bool) { + self.accessToken = accessToken + self.refreshToken = refreshToken + self.isTemporaryToken = isTemporaryToken + } + +} + +extension AuthToken: Equatable { } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/UserDefaultsWrapper/UserDefaultsWrapper.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/UserDefaultsWrapper/UserDefaultsWrapper.swift index fffd9640b..30a91d43d 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/UserDefaultsWrapper/UserDefaultsWrapper.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/UserDefaultsWrapper/UserDefaultsWrapper.swift @@ -10,6 +10,7 @@ import Foundation final public class UserDefaultsWrapper { // MARK: - Properties + public static let standard = UserDefaultsWrapper() private let userDefaults: UserDefaults! @@ -20,7 +21,9 @@ final public class UserDefaultsWrapper { "UserDefaultsWrapper" }() + // MARK: - Intializer + convenience init() { self.init(suitName: UserDefaultsWrapper.defaultSuitName) } @@ -166,4 +169,25 @@ final public class UserDefaultsWrapper { userDefaults.removePersistentDomain(forName: suitName) } + + // MARK: - Register + + /// 해당 키에 값이 비어있다면 넣을 기본 값을 등록합니다. + /// + /// 앱 델리게이트의 `application(_ application:didFinishLaunchingWithOptions:)` 메서드에서 등록해야 합니다. + /// - Parameter values: [UserDefaultsWrapper.Key: Any] + public func register(_ values: [Key: Any]) { + var newValues = [String: Any]() + values.forEach { newValues.updateValue($0.value, forKey: $0.key.rawValue) } + register(newValues) + } + + /// 해당 키에 값이 비어있다면 넣을 기본 값을 등록합니다. + /// + /// 앱 델리게이트의 `application(_ application:didFinishLaunchingWithOptions:)` 메서드에서 등록해야 합니다. + /// - Parameter values: [String: Any] + public func register(_ values: [String: Any]) { + userDefaults.register(defaults: values) + } + } diff --git a/14th-team5-iOS/Core/Sources/Extensions/Bundle+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/Bundle+Ext.swift index 6dc871d5b..a814974a3 100644 --- a/14th-team5-iOS/Core/Sources/Extensions/Bundle+Ext.swift +++ b/14th-team5-iOS/Core/Sources/Extensions/Bundle+Ext.swift @@ -63,4 +63,8 @@ extension Bundle { return 0 } } + + public var xAppKey: String { + "db3ca026-0f9c-415a-a250-c97807f54add" + } } diff --git a/14th-team5-iOS/Core/Sources/Extensions/Data+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/Data+Ext.swift new file mode 100644 index 000000000..68a565eb1 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Extensions/Data+Ext.swift @@ -0,0 +1,24 @@ +// +// Decodable.swift +// Core +// +// Created by 김건우 on 10/6/24. +// + +import Foundation + +public extension Data { + + func decode( + using decoder: JSONDecoder = JSONDecoder() + ) -> T? where T: Decodable { + try? decoder.decode(T.self, from: self) + } + + func decode( + using decoder: any BBResponseDecoder = BBDefaultResponderDecoder() + ) -> T? where T: Decodable { + try? decoder.decode(from: self) + } + +} diff --git a/14th-team5-iOS/Core/Sources/Extensions/Encodable+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/Encodable+Ext.swift new file mode 100644 index 000000000..ed50a40f6 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Extensions/Encodable+Ext.swift @@ -0,0 +1,31 @@ +// +// Encodable+Ext.swift +// Core +// +// Created by 김건우 on 9/25/24. +// + +import Foundation + +public extension Encodable { + + /// 인코딩이 가능한 객체를 Data로 변환합니다. + /// - Parameter encoder: JSONEncoder 객체 + /// - Returns: Data? + /// + /// - Authors: 김소월 + func toData(using encoder: JSONEncoder = JSONEncoder()) throws -> Data { + return try encoder.encode(self) + } + + /// 인코딩이 가능한 객체를 딕셔너리로 변환합니다. + /// - Returns: [String: Any]? + /// + /// - Authors: 김소월 + func toDictionary(using encoder: JSONEncoder = JSONEncoder()) throws -> [String: Any] { + let data = try encoder.encode(self) + let jsonData = try JSONSerialization.jsonObject(with: data) + return jsonData as? [String: Any] ?? [:] + } + +} diff --git a/14th-team5-iOS/Core/Sources/Utilities/Modifiers/Shimmer.swift b/14th-team5-iOS/Core/Sources/Extensions/Modifiers/Shimmer.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Utilities/Modifiers/Shimmer.swift rename to 14th-team5-iOS/Core/Sources/Extensions/Modifiers/Shimmer.swift diff --git a/14th-team5-iOS/Core/Sources/Extensions/ObservableType+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/ObservableType+Ext.swift new file mode 100644 index 000000000..4b57268ca --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Extensions/ObservableType+Ext.swift @@ -0,0 +1,86 @@ +// +// ObservableType+Ext.swift +// Core +// +// Created by 김건우 on 10/3/24. +// + +import Foundation + +import RxSwift + +public extension ObservableType { + + /// 기존 `flatMap` 연산자에 with 매개변수를 붙인 새로운 연산자입니다. + /// - Parameters: + /// - object: 약한 참조하고자 하는 객체 + /// - handler: flatMap 연산 핸들러 + /// - Returns: Observable\ + /// + /// - Authors: 김소월 + func flatMap( + with object: O, + _ handler: @escaping (O, Element) -> Observable + ) -> Observable where O: AnyObject { + flatMap { [weak object] element in + guard let object else { return Observable.empty() } + return handler(object, element) + } + } + +} + +public extension ObservableType { + + /// 스트림에서 받은 error 항목이 매개변수로 주어진 `type`으로 변환이 가능하다면, 에러 처리를 합니다. + /// + /// - Parameters: + /// - object: 약한 참조를 하고자 하는 객체입니다. + /// - type: 변환하고자 하는 에러 타입입니다. `Error` 프로토콜을 준수하는 타입이어야 합니다. 기본값은 `(any Error).self`입니다. + /// - handler: 에러 처리를 하는 핸들러입니다. + /// - Returns: Observable\ + /// + /// - Authors: 김소월 + func catchError( + with object: O, + of type: E.Type = (any Error).self, + handler: @escaping (O, E) -> Observable + ) -> Observable where O: AnyObject, E: Error { + + return `catch` { [weak object] error in + guard let object else { return .empty() } + if let castedError = error as? E { + return handler(object, castedError) + } + return .empty() + } + + } + + /// 스트림에서 `APIWorker` 타입의 error 항목을 받으면 에러 처리를 합니다. + /// + /// - Parameters: + /// - object: 약한 참조를 하고자 하는 객체입니다. + /// - handler: 에러 처리를 하는 핸들러입니다. + /// - Returns: Observable\ + /// + /// - Authors: 김소월 + func catchAPIWorkerError( + with object: O, + handler: @escaping (O, APIWorkerError) -> Observable + ) -> Observable where O: AnyObject { + + return catchError(with: object, of: APIWorkerError.self, handler: handler) + + } + + + /// 스트림에서 error 항목을 받으면 스트림을 종료합니다. + /// - Authors: 김소월 + func catchErrorJustComplete() -> Observable { + return `catch` { _ in + Observable.empty() + } + } + +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/API.swift b/14th-team5-iOS/Core/Sources/Trash/BBNetwork/API.swift similarity index 94% rename from 14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/API.swift rename to 14th-team5-iOS/Core/Sources/Trash/BBNetwork/API.swift index da83f68b9..26bc95141 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/API.swift +++ b/14th-team5-iOS/Core/Sources/Trash/BBNetwork/API.swift @@ -15,12 +15,14 @@ public typealias BibbiNoResponse = BibbiAPI.NoResponse public typealias BibbiBoolResponse = BibbiAPI.BoolResponse public typealias BibbiCodableResponse = BibbiAPI.CodableResponse - +@available(*, deprecated, renamed: "BBAPI") public enum BibbiAPI { private static let _config: BibbiAPIConfigType = BibbiAPIConfig() public static let hostApi: String = _config.hostApi // MARK: Common Headers + + @available(*, deprecated, renamed: "BBAPIHeader") public enum Header: APIHeader { case auth(String) case xAppKey @@ -51,7 +53,7 @@ public enum BibbiAPI { public var value: String { switch self { case let .auth(token): return "Bearer \(token)" - case .xAppKey: return "7b159d28-b106-4b6d-a490-1fd654ce40c2" // TODO: - 번들에서 가져오기 + case .xAppKey: return "db3ca026-0f9c-415a-a250-c97807f54add" // TODO: - 번들에서 가져오기 case let .xAuthToken(token): return "\(token)" case .contentForm: return "application/x-www-form-urlencoded" case .contentJson: return "application/json" diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/APIConfig.swift b/14th-team5-iOS/Core/Sources/Trash/BBNetwork/APIConfig.swift similarity index 86% rename from 14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/APIConfig.swift rename to 14th-team5-iOS/Core/Sources/Trash/BBNetwork/APIConfig.swift index 6cd6a0cc3..c4d78d32e 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/APIConfig.swift +++ b/14th-team5-iOS/Core/Sources/Trash/BBNetwork/APIConfig.swift @@ -11,6 +11,7 @@ public protocol BibbiAPIConfigType { var hostApi: String { get } } +@available(*, deprecated, renamed: "BBAPIConfiguration") public struct BibbiAPIConfig: BibbiAPIConfigType { #if PRD public var hostApi: String = "https://api.no5ing.kr/v1" diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/APISpec.swift b/14th-team5-iOS/Core/Sources/Trash/BBNetwork/APISpec.swift similarity index 86% rename from 14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/APISpec.swift rename to 14th-team5-iOS/Core/Sources/Trash/BBNetwork/APISpec.swift index ae37c5b39..b28e68899 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/APISpec.swift +++ b/14th-team5-iOS/Core/Sources/Trash/BBNetwork/APISpec.swift @@ -10,7 +10,10 @@ import Alamofire import RxSwift // MARK: API Protocol +@available(*, deprecated, renamed: "BBAPI") public typealias BaseAPI = API + +@available(*, deprecated, renamed: "BBAPI") public protocol API { var spec: APISpec { get } } @@ -50,9 +53,13 @@ public enum APIResult { } // MARK: API Error Protocol + +@available(*, deprecated, renamed: "BBAPIError") public protocol APIError: CustomNSError, Equatable {} // MARK: API Specification + +@available(*, deprecated, renamed: "BBAPISpec") public struct APISpec { public let method: HTTPMethod public let url: String diff --git a/14th-team5-iOS/Core/Sources/Trash/BBRx/Repositories/Token/TokenRepository.swift b/14th-team5-iOS/Core/Sources/Trash/BBRx/Repositories/Token/TokenRepository.swift index 41116db69..01c6b7657 100644 --- a/14th-team5-iOS/Core/Sources/Trash/BBRx/Repositories/Token/TokenRepository.swift +++ b/14th-team5-iOS/Core/Sources/Trash/BBRx/Repositories/Token/TokenRepository.swift @@ -10,6 +10,7 @@ import Foundation import RxCocoa import RxSwift +@available(*, deprecated, renamed: "AuthToken") public struct AccessToken: Codable, Equatable { public var accessToken: String? public var refreshToken: String? @@ -22,6 +23,7 @@ public struct AccessToken: Codable, Equatable { } } +@available(*, deprecated, renamed: "TokenKeychain") public class TokenRepository: RxObject { public lazy var keychain = KeychainWrapper(serviceName: "Bibbi", accessGroup: "P9P4WJ623F.com.5ing.bibbi") diff --git a/14th-team5-iOS/Core/Sources/Utilities/Haptic.swift b/14th-team5-iOS/Core/Sources/Utils/Haptic.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Utilities/Haptic.swift rename to 14th-team5-iOS/Core/Sources/Utils/Haptic.swift diff --git a/14th-team5-iOS/Core/Sources/Utils/Logger/BBLogger.swift b/14th-team5-iOS/Core/Sources/Utils/Logger/BBLogger.swift new file mode 100644 index 000000000..af480d9c6 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Utils/Logger/BBLogger.swift @@ -0,0 +1,187 @@ +// +// BBLogger.swift +// Core +// +// Created by 김도현 on 10/16/24. +// + +import Foundation +import os + +import RxSwift + +/// LogLevel은 **OSLogType** 기준으로 정의를 했습니다. +/// 해당 **LogLevel** 을 통해서 BBLogger의 출력 메세지를 지정 할 수 있습니다. +private enum LogLevel { + /// 일반적인 정보를 조회할 때 사용하는 Type + /// ex) 네트워크 요청 시 Response 값 조회 + case info + /// 코드 디버깅할 때 사용하는 Type + case debug + /// 경고 오류가 발생했을 때 사용하는 Type + case error + /// 크래시를 유발하는 오류 나타낼 때 사용하는 Type + case fault + + + var label: String { + switch self { + case .info: return "[INFO ⚪]" + case .debug: return "[DEBUG 🟢]" + case .error: return "[ERROR 🟠]" + case .fault: return "[FAULT 🔴]" + } + } +} + + +public struct BBLogger { + /// 일반적인 정보를 조회할때 사용하는 메서드 입니다. + /// - Parameters: + /// - function: logInfo 메서드를 호출한 함수명 + /// - fileName: logInfo 메서드를 호출한 파일명 + /// - category: 로그의 카테고리 예) "Network", "UI", "UseCase", + /// - message: 로그 메세지 + public static func logInfo( + function: String = #function, + fileName: String = #file, + category: String = "", + message: String = "" + ) { + guard let bundleId = Bundle.main.bundleIdentifier else { + fatalError("Bundle ID value not found") + } + + let infotMessage = createLoggerMessage( + level: LogLevel.info, + message: message, + function: function, + fileName: fileName + ) + + Logger(subsystem: bundleId, category: category) + .info("\(infotMessage)") + } + + /// 코드 디버깅할때 사용하는 메서드 입니다.. + /// - Parameters: + /// - function: logInfo 메서드를 호출한 함수명 + /// - fileName: logInfo 메서드를 호출한 파일명 + /// - category: 로그의 카테고리 예) "Network", "UI", "UseCase", + /// - message: 로그 메세지 + public static func logDebug( + function: String = #function, + fileName: String = #file, + category: String = "", + message: String = "" + ) { + + guard let bundleId = Bundle.main.bundleIdentifier else { + fatalError("Bundle ID value not found") + } + + let debugMessage = createLoggerMessage( + level: LogLevel.debug, + message: message, + function: function, + fileName: fileName + ) + + Logger(subsystem: bundleId, category: category) + .debug("\(debugMessage)") + + } + + /// 경고 오류를 기록 및 추적할 때 사용하는 메서드입니다. + /// - Parameters: + /// - function: logInfo 메서드를 호출한 함수명 + /// - fileName: logInfo 메서드를 호출한 파일명 + /// - category: 로그의 카테고리 예) "Network", "UI", "UseCase", + /// - message: 로그 메세지 + public static func logError( + function: String = #function, + fileName: String = #file, + category: String = "", + message: String = "" + ) { + guard let bundleId = Bundle.main.bundleIdentifier else { + fatalError("Bundle ID value not found") + } + + let errorMessage = createLoggerMessage( + level: LogLevel.error, + message: message, + function: function, + fileName: fileName + ) + + Logger(subsystem: bundleId, category: category) + .error("\(errorMessage)") + } + + /// 크래시 오류를 추적 및 기록할 때 사용하는 메서드입니다. + /// - Parameters: + /// - function: logInfo 메서드를 호출한 함수명 + /// - fileName: logInfo 메서드를 호출한 파일명 + /// - category: 로그의 카테고리 예) "Network", "UI", "UseCase", + /// - message: 로그 메세지 + public static func logFault( + function: String = #function, + fileName: String = #file, + category: String = "", + message: String = "" + ) { + guard let bundleId = Bundle.main.bundleIdentifier else { + fatalError("Bundle ID value not found") + } + + let faultMessage = createLoggerMessage( + level: LogLevel.fault, + message: message, + function: function, + fileName: fileName + ) + + Logger(subsystem: bundleId, category: category) + .fault("\(faultMessage)") + } +} + + +extension BBLogger { + + /// 로그 메서드 내부 출력 구문을 생성하기 위한 메서드입니다. + /// - Parameters: + /// - level: LogLevel Type + /// - message: 로그 메세지 + /// - function: logInfo 메서드를 호출한 함수명 + /// - fileName: logInfo 메서드를 호출한 파일명 + private static func createLoggerMessage( + level: LogLevel, + message: String, + function: String = #function, + fileName: String = #file + ) -> String { + + let timestamp: String = "🕖 \(Date().toFormatString(with: .ahhmmss))" + let functionName: String = "#️⃣ \(function)" + let filename: String = URL(fileURLWithPath: fileName).lastPathComponent + let message: String = "\n\(message)" + + return Array(arrayLiteral: level.label, timestamp, filename, functionName, message).joined(separator: "|") + } +} + + + +extension ObservableType { + /// RxSwift에서 제공되는. debug 연산자와 동일하게 element를 로깅 하는 연산자입니다. + /// 해당 연산자를 통해서 BBLogger를 사용하여 로깅을 할 수 있습니다. + /// - Parameters: + /// - category: 로그의 카테고리 예) "Network", "UI", "UseCase", + public func log(_ category: String) -> Observable { + return self.do(onNext: { element in + BBLogger.logDebug(category: category, message: "\(element)") + }) + } +} diff --git a/14th-team5-iOS/Core/Sources/Utilities/Analytics/Mixpanel/MixPanelService+Account.swift b/14th-team5-iOS/Core/Sources/Utils/Mixpanel/MixPanelService+Account.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Utilities/Analytics/Mixpanel/MixPanelService+Account.swift rename to 14th-team5-iOS/Core/Sources/Utils/Mixpanel/MixPanelService+Account.swift diff --git a/14th-team5-iOS/Core/Sources/Utilities/Analytics/Mixpanel/MixPanelService+Camera.swift b/14th-team5-iOS/Core/Sources/Utils/Mixpanel/MixPanelService+Camera.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Utilities/Analytics/Mixpanel/MixPanelService+Camera.swift rename to 14th-team5-iOS/Core/Sources/Utils/Mixpanel/MixPanelService+Camera.swift diff --git a/14th-team5-iOS/Core/Sources/Utilities/Analytics/Mixpanel/MixPanelService+Family.swift b/14th-team5-iOS/Core/Sources/Utils/Mixpanel/MixPanelService+Family.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Utilities/Analytics/Mixpanel/MixPanelService+Family.swift rename to 14th-team5-iOS/Core/Sources/Utils/Mixpanel/MixPanelService+Family.swift diff --git a/14th-team5-iOS/Core/Sources/Utilities/Analytics/Mixpanel/MixPanelService+Home.swift b/14th-team5-iOS/Core/Sources/Utils/Mixpanel/MixPanelService+Home.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Utilities/Analytics/Mixpanel/MixPanelService+Home.swift rename to 14th-team5-iOS/Core/Sources/Utils/Mixpanel/MixPanelService+Home.swift diff --git a/14th-team5-iOS/Core/Sources/Utilities/Analytics/Mixpanel/MixpanelService.swift b/14th-team5-iOS/Core/Sources/Utils/Mixpanel/MixpanelService.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Utilities/Analytics/Mixpanel/MixpanelService.swift rename to 14th-team5-iOS/Core/Sources/Utils/Mixpanel/MixpanelService.swift diff --git a/14th-team5-iOS/Core/Sources/Utilities/MulticastDelegate.swift b/14th-team5-iOS/Core/Sources/Utils/MulticastDelegate.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Utilities/MulticastDelegate.swift rename to 14th-team5-iOS/Core/Sources/Utils/MulticastDelegate.swift diff --git a/14th-team5-iOS/Core/Sources/Utilities/Rx/RxInterval.swift b/14th-team5-iOS/Core/Sources/Utils/Reactive/RxInterval.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Utilities/Rx/RxInterval.swift rename to 14th-team5-iOS/Core/Sources/Utils/Reactive/RxInterval.swift diff --git a/14th-team5-iOS/Core/Sources/Utilities/Rx/RxObject.swift b/14th-team5-iOS/Core/Sources/Utils/Reactive/RxObject.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Utilities/Rx/RxObject.swift rename to 14th-team5-iOS/Core/Sources/Utils/Reactive/RxObject.swift diff --git a/14th-team5-iOS/Core/Sources/Utilities/Rx/RxScheduler.swift b/14th-team5-iOS/Core/Sources/Utils/Reactive/RxScheduler.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Utilities/Rx/RxScheduler.swift rename to 14th-team5-iOS/Core/Sources/Utils/Reactive/RxScheduler.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/App/Repository/AppRepository.swift b/14th-team5-iOS/Data/Sources/APIs/App/Repository/AppRepository.swift index 0d374e76c..2708349de 100644 --- a/14th-team5-iOS/Data/Sources/APIs/App/Repository/AppRepository.swift +++ b/14th-team5-iOS/Data/Sources/APIs/App/Repository/AppRepository.swift @@ -40,7 +40,7 @@ extension AppRepository { .asObservable() } - public func fetchIsFirstFamilyManagement() -> RxSwift.Observable { + public func loadIsFirstFamilyManagement() -> RxSwift.Observable { let isFirstFamily = appUserDefaults.loadIsFirstFamilyManagement() return .just(isFirstFamily) } @@ -48,4 +48,13 @@ extension AppRepository { public func saveIsFirstFamilyManagement(isFirst: Bool) { appUserDefaults.saveIsFirstFamilyManagement(isFirst) } + + public func loadIsFirstWidgetAlert() -> Observable { + let isFirstWidgetAlert = appUserDefaults.loadIsFirstShowWidgetAlert() + return .just(isFirstWidgetAlert) + } + + public func saveIsFirstWidgetAlert(isFirst: Bool) { + appUserDefaults.saveIsFirstShowWidgetAlert(isFirst) + } } diff --git a/14th-team5-iOS/Data/Sources/APIs/BibbiNetworkMonitor.swift b/14th-team5-iOS/Data/Sources/APIs/BibbiNetworkMonitor.swift deleted file mode 100644 index f2e587b9a..000000000 --- a/14th-team5-iOS/Data/Sources/APIs/BibbiNetworkMonitor.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// BibbiNetworkMonitor.swift -// Data -// -// Created by Kim dohyun on 7/10/24. -// - -import Foundation - -import Alamofire -import Core - - - -final class BibbiNetworkMonitor: EventMonitor { - - var queue: DispatchQueue = { - guard let bundleId = Bundle.main.bundleIdentifier else { - fatalError("올바르지 않는 식별ID값 입니다.") - } - return DispatchQueue(label: bundleId) - }() - - - func requestDidFinish(_ request: Request) { - print("[Reqeust BibbiNetwork LOG]") - print("- URL : \((request.request?.url?.absoluteString ?? ""))") - print(" - Method : \((request.request?.httpMethod ?? ""))") - print(" - Headers \((request.request?.headers) ?? .default):") - } - - public func request(_ request: DataRequest, didParseResponse response: DataResponse) { - print("[Response BibbiNetwork LOG]") - print("- URL : \((request.request?.url?.absoluteString ?? ""))") - print(" - Results : \((response.result))") - print(" - StatusCode : \(response.response?.statusCode ?? 0)") - print(" - Data : \(response.data?.toPrettyPrintedString ?? "")") - } -} diff --git a/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/CommentAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/CommentAPIWorker.swift index 92c8f18c5..2fc752db8 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/CommentAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/CommentAPIWorker.swift @@ -13,15 +13,8 @@ import RxSwift typealias CommentAPIWorker = CommentAPIs.Worker extension CommentAPIs { - final class Worker: APIWorker { - static let queue = { - ConcurrentDispatchQueueScheduler(queue: DispatchQueue(label: "PostCommentAPIWorker", qos: .utility)) - }() - - override init() { - super.init() - self.id = "PostCommentAPIWorker" - } + public final class Worker: BBRxAPIWorker { + public init() { super.init() } } } @@ -33,76 +26,53 @@ extension CommentAPIWorker { // MARK: - Fetch Comment - public func fetchComment( + func fetchComment( postId: String, query: PostCommentPaginationQuery - ) -> Single { + ) -> Observable { let page = query.page let size = query.size let sort = query.sort.rawValue - let spec = CommentAPIs.fetchPostComment(postId, page, size, sort).spec + let spec = CommentAPIs.fetchPostComment(postId: postId, page: page, size: size, sort: sort).spec - return request(spec: spec) - .subscribe(on: Self.queue) - .map(PaginationResponsePostCommentResponseDTO.self) - .catchAndReturn(nil) - .asSingle() + return request(spec) } // MARK: - Create Comment - public func createComment( + func createComment( postId: String, body: CreatePostCommentReqeustDTO - ) -> Single { - let spec = CommentAPIs.createPostComment(postId).spec - let headers = { - let accessToken = App.Repository.token.accessToken.value?.accessToken - var apiHeaders: [APIHeader] = [ - BibbiAPI.Header.xAppKey, - BibbiAPI.Header.xAuthToken(accessToken!) - ] - return apiHeaders - }() // TODO: - APIWorker 리팩토링되는 대로 코드 삭제하기 - return request(spec: spec, headers: headers, jsonEncodable: body) - .subscribe(on: Self.queue) - .map(PostCommentResponseDTO.self) - .catchAndReturn(nil) - .asSingle() + ) -> Observable { + let spec = CommentAPIs.createPostComment(postId: postId, body: body).spec + + return request(spec) } // MARK: - Update Comment - public func updateComment( + func updateComment( postId: String, commentId: String, body: UpdatePostCommentReqeustDTO - ) -> Single { - let spec = CommentAPIs.updatePostComment(postId, commentId).spec + ) -> Observable { + let spec = CommentAPIs.updatePostComment(postId: postId, commentId: commentId).spec - return request(spec: spec, jsonEncodable: body) - .subscribe(on: Self.queue) - .map(PostCommentResponseDTO.self) - .catchAndReturn(nil) - .asSingle() + return request(spec) } // MARK: - Delete Comment - public func deleteComment( + func deleteComment( postId: String, commentId: String - ) -> Single { - let spec = CommentAPIs.deletePostComment(postId, commentId).spec + ) -> Observable { + let spec = CommentAPIs.deletePostComment(postId: postId, commentId: commentId).spec - return request(spec: spec) - .subscribe(on: Self.queue) - .map(PostCommentDeleteResponseDTO.self) - .catchAndReturn(nil) - .asSingle() + return request(spec) } } diff --git a/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/CommentAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/CommentAPIs.swift index 7d039f5d2..5f25ffdb1 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/CommentAPIs.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/CommentAPIs.swift @@ -8,22 +8,43 @@ import Core import Foundation -enum CommentAPIs: API { - case fetchPostComment(String, Int, Int, String) - case createPostComment(String) - case updatePostComment(String, String) - case deletePostComment(String, String) +enum CommentAPIs: BBAPI { + case fetchPostComment(postId: String, page: Int, size: Int, sort: String) + case createPostComment(postId: String, body: CreatePostCommentReqeustDTO) + case updatePostComment(postId: String, commentId: String) + case deletePostComment(postId: String, commentId: String) - var spec: APISpec { + var spec: Spec { switch self { case let .fetchPostComment(postId, page, size, sort): - return APISpec(method: .get, url: "\(BibbiAPI.hostApi)/posts/\(postId)/comments?page=\(page)&size=\(size)&sort=\(sort)") - case let .createPostComment(postId): - return APISpec(method: .post, url: "\(BibbiAPI.hostApi)/posts/\(postId)/comments") + return Spec( + method: .get, + path: "/posts/\(postId)/comments", + queryParameters: [ + .page: "\(page)", + .size: "\(size)", + .sort: "\(sort)" + ] + ) + + case let .createPostComment(postId, body): + return Spec( + method: .post, + path: "/posts/\(postId)/comments", + bodyParameters: ["content": "\(body.content)"] + ) + case let .updatePostComment(postId, commentId): - return APISpec(method: .put, url: "\(BibbiAPI.hostApi)/posts/\(postId)/comments/\(commentId)") + return Spec( + method: .put, + path: "/posts/\(postId)/comments/\(commentId)" + ) + case let .deletePostComment(postId, commentId): - return APISpec(method: .delete, url: "\(BibbiAPI.hostApi)/posts/\(postId)/comments/\(commentId)") + return Spec( + method: .delete, + path: "/posts/\(postId)/comments/\(commentId)" + ) } } } diff --git a/14th-team5-iOS/Data/Sources/APIs/Comment/Repository/CommentRepository.swift b/14th-team5-iOS/Data/Sources/APIs/Comment/Repository/CommentRepository.swift index 5275cd123..26fb6a41c 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Comment/Repository/CommentRepository.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Comment/Repository/CommentRepository.swift @@ -22,10 +22,9 @@ extension CommentRepository { // MARK: - Fetch Comment - public func fetchPostComment(postId: String, query: PostCommentPaginationQuery) -> Observable { + public func fetchPostComment(postId: String, query: PostCommentPaginationQuery) -> Observable { return commentApiWorker.fetchComment(postId: postId, query: query) - .map { $0?.toDomain() } - .asObservable() + .map { $0.toDomain() } } @@ -34,8 +33,7 @@ extension CommentRepository { public func createPostComment(postId: String, body: CreatePostCommentRequest) -> Observable { let body = CreatePostCommentReqeustDTO(content: body.content) return commentApiWorker.createComment(postId: postId, body: body) - .map { $0?.toDomain() } - .asObservable() + .map { $0.toDomain() } } @@ -44,8 +42,7 @@ extension CommentRepository { public func updatePostComment(postId: String, commentId: String, body: UpdatePostCommentRequest) -> Observable { let body = UpdatePostCommentReqeustDTO(content: body.content) return commentApiWorker.updateComment(postId: postId, commentId: commentId, body: body) - .map { $0?.toDomain() } - .asObservable() + .map { $0.toDomain() } } @@ -53,7 +50,6 @@ extension CommentRepository { public func deletePostComment(postId: String, commentId: String) -> Observable { return commentApiWorker.deleteComment(postId: postId, commentId: commentId) - .map { $0?.toDomain() } - .asObservable() + .map { $0.toDomain() } } } diff --git a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/FamilyGroupInfoResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/FamilyGroupInfoResponseDTO.swift index 3ce302eea..fee984d19 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/FamilyGroupInfoResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/FamilyGroupInfoResponseDTO.swift @@ -20,8 +20,8 @@ extension FamilyGroupInfoResponseDTO { func toDomain() -> FamilyGroupInfoEntity { return .init( familyId: familyId, - familyName: familyName ?? "", - familyNameEditorId: familyNameEditorId ?? "", + familyName: familyName, + familyNameEditorId: familyNameEditorId, createdAt: createdAt.iso8601ToDate() ) } diff --git a/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/Repository/OAuthRepository.swift b/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/Repository/OAuthRepository.swift index e703d2512..98a960184 100644 --- a/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/Repository/OAuthRepository.swift +++ b/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/Repository/OAuthRepository.swift @@ -33,7 +33,7 @@ extension OAuthRepository { refreshToken: body.refreshToken ) return oAuthApiWorker.refreshAccessToken(body: body) - .observe(on: RxSchedulers.main) + .observe(on: RxScheduler.main) .map { $0?.toDomain() } .do(onSuccess: { [weak self] in guard @@ -44,7 +44,7 @@ extension OAuthRepository { refreshToken: $0?.refreshToken, isTemporaryToken: $0?.isTemporaryToken ) - keychain.saveOldAccessToken(accessToken) + keychain.saveAccessToken(accessToken) }) .asObservable() } @@ -59,7 +59,7 @@ extension OAuthRepository { profileImageUrl: body.profileImageUrl ) return oAuthApiWorker.registerNewMember(body: body) - .observe(on: RxSchedulers.main) + .observe(on: RxScheduler.main) .map { $0?.toDomain() } .do(onSuccess: { [weak self] in guard @@ -70,7 +70,7 @@ extension OAuthRepository { refreshToken: $0?.refreshToken, isTemporaryToken: $0?.isTemporaryToken ) - keychain.saveOldAccessToken(accessToken) + keychain.saveAccessToken(accessToken) }) .asObservable() } @@ -83,7 +83,7 @@ extension OAuthRepository { accessToken: body.accessToken ) return oAuthApiWorker.signIn(type, body: body) - .observe(on: RxSchedulers.main) + .observe(on: RxScheduler.main) .map { $0?.toDomain() } .do(onSuccess: { [weak self] in guard @@ -94,7 +94,7 @@ extension OAuthRepository { refreshToken: $0?.refreshToken, isTemporaryToken: $0?.isTemporaryToken ) - keychain.saveOldAccessToken(accessToken) + keychain.saveAccessToken(accessToken) }) .asObservable() } @@ -107,7 +107,7 @@ extension OAuthRepository { fcmToken: body.fcmToken ) return oAuthApiWorker.registerNewFCMToken(body: body) - .observe(on: RxSchedulers.main) + .observe(on: RxScheduler.main) .map { $0?.toDomain() } .asObservable() } @@ -124,7 +124,7 @@ extension OAuthRepository { } return oAuthApiWorker.deleteFCMToken(fcmToken: fcmToken) - .observe(on: RxSchedulers.main) + .observe(on: RxScheduler.main) .map { $0?.toDomain() } .asObservable() diff --git a/14th-team5-iOS/Data/Sources/Storages/Keychain/TokenKeychain/Models/OldAccessToken.swift b/14th-team5-iOS/Data/Sources/Storages/Keychain/TokenKeychain/Models/OldAccessToken.swift deleted file mode 100644 index 4d00cada3..000000000 --- a/14th-team5-iOS/Data/Sources/Storages/Keychain/TokenKeychain/Models/OldAccessToken.swift +++ /dev/null @@ -1,8 +0,0 @@ -// -// AccessToken.swift -// Data -// -// Created by 김건우 on 8/24/24. -// - -import Foundation diff --git a/14th-team5-iOS/Data/Sources/Storages/Keychain/TokenKeychain/TokenKeychain.swift b/14th-team5-iOS/Data/Sources/Storages/Keychain/TokenKeychain/TokenKeychain.swift index d2a46b3b4..1f3112ed9 100644 --- a/14th-team5-iOS/Data/Sources/Storages/Keychain/TokenKeychain/TokenKeychain.swift +++ b/14th-team5-iOS/Data/Sources/Storages/Keychain/TokenKeychain/TokenKeychain.swift @@ -15,18 +15,12 @@ public protocol TokenKeychainType: KeychainType { func saveSignInType(_ type: SignInType?) func loadSignInType() -> SignInType? + + func saveAuthToken(_ authToken: AuthToken?) + func loadAuthToken() -> AuthToken? - func saveAccessToken(_ accessToken: String?) - func loadAccessToken() -> String? - - func saveIsTemporaryToken(_ isTemporary: Bool?) - func loadIsTemporaryToken() -> Bool? - - func saveOldAccessToken(_ tokenResult: AccessToken?) - func loadOldAccessToken() -> AccessToken? - - func saveRefreshToken(_ refreshToken: String?) - func loadRefreshToken() -> String? + func saveAccessToken(_ accessToken: AccessToken?) + func loadAccessToken() -> AccessToken? func saveFCMToken(_ fcmToken: String?) func loadFCMToken() -> String? @@ -68,43 +62,6 @@ final public class TokenKeychain: TokenKeychainType { } - // MARK: - AccessToken - - /// 삐삐 서버로부터 발급받은 접근 토큰을 저장합니다. - public func saveAccessToken(_ accessToken: String?) { - keychain[.newAccessToken] = accessToken - } - - /// 삐삐 서버로부터 발급받은 접근 토큰을 불러옵니다. - public func loadAccessToken() -> String? { - keychain[.newAccessToken] - } - - - // MARK: - RefreshToken - - /// 삐삐 서버로부터 발급받은 리프레시 토큰을 저장합니다. - public func saveRefreshToken(_ refreshToken: String?) { - keychain[.newRefreshToken] = refreshToken - } - - /// 삐삐 서버로부터 발급받은 리프레시 토큰을 불러옵니다. - public func loadRefreshToken() -> String? { - keychain[.newRefreshToken] - } - - - // MARK: - Is Temporary Token - - public func saveIsTemporaryToken(_ isTemporary: Bool?) { - keychain[.newIsTemporaryToken] = isTemporary - } - - public func loadIsTemporaryToken() -> Bool? { - keychain[.newIsTemporaryToken] - } - - // MARK: - FCM Token /// FCM 서버로부터 발급받은 FCM 토큰을 저장합니다. @@ -119,26 +76,28 @@ final public class TokenKeychain: TokenKeychainType { + // MARK: - Auth AccessToken + + public func saveAuthToken(_ authToken: AuthToken?) { + keychain[.accessToken] = authToken + } + + public func loadAuthToken() -> AuthToken? { + keychain[.accessToken] + } + + + // MARK: - Old AccessToken - @available(*, deprecated) - public func saveOldAccessToken(_ tokenResult: AccessToken?) { - // 🔵Info: AccessToken은 Core 모듈의 TokenRepository.swift에 정의되어 있음 - guard - let data = try? JSONEncoder().encode(tokenResult), - let str = String(data: data, encoding: .utf8) - else { return } - keychain[.accessToken] = str + @available(*, deprecated, renamed: "saveAuthToken") + public func saveAccessToken(_ accessToken: AccessToken?) { + keychain[.accessToken] = accessToken } - @available(*, deprecated) - public func loadOldAccessToken() -> AccessToken? { - guard - let str: String = keychain[.accessToken], - let data = str.data(using: .utf8), - let tokenResult = try? JSONDecoder().decode(AccessToken.self, from: data) - else { return nil } - return tokenResult + @available(*, deprecated, renamed: "loadAuthToken") + public func loadAccessToken() -> AccessToken? { + keychain[.accessToken] } } diff --git a/14th-team5-iOS/Data/Sources/Storages/UserDefaults/AppUserDefaults/AppUserDefaults.swift b/14th-team5-iOS/Data/Sources/Storages/UserDefaults/AppUserDefaults/AppUserDefaults.swift index 71f290718..a026b825e 100644 --- a/14th-team5-iOS/Data/Sources/Storages/UserDefaults/AppUserDefaults/AppUserDefaults.swift +++ b/14th-team5-iOS/Data/Sources/Storages/UserDefaults/AppUserDefaults/AppUserDefaults.swift @@ -42,10 +42,7 @@ final public class AppUserDefaults: AppUserDefaultsType { } public func loadIsFirstLaunchApp() -> Bool? { - guard - let value: Bool? = userDefaults[.isFirstLaunchApp] - else { return nil } - return value + return userDefaults[.isFirstLaunchApp] } @@ -56,10 +53,7 @@ final public class AppUserDefaults: AppUserDefaultsType { } public func loadIsFirstChangeFamilyName() -> Bool? { - guard - let value: Bool? = userDefaults[.isFirstChangeFamilyName] - else { return nil } - return value + return userDefaults[.isFirstChangeFamilyName] } @@ -70,10 +64,7 @@ final public class AppUserDefaults: AppUserDefaultsType { } public func loadIsFirstShowWidgetAlert() -> Bool? { - guard - let value: Bool? = userDefaults[.isFirstShowWidgetAlert] - else { return nil } - return value + return userDefaults[.isFirstShowWidgetAlert] } @@ -84,10 +75,7 @@ final public class AppUserDefaults: AppUserDefaultsType { } public func loadInviteCode() -> String? { - guard - let inviteCode: String? = userDefaults[.inviteCode] - else { return nil } - return inviteCode + return userDefaults[.inviteCode] } diff --git a/14th-team5-iOS/Data/Sources/APIs/APIWorker.swift b/14th-team5-iOS/Data/Sources/Trash/APIWorker.swift similarity index 97% rename from 14th-team5-iOS/Data/Sources/APIs/APIWorker.swift rename to 14th-team5-iOS/Data/Sources/Trash/APIWorker.swift index ddf34b3c2..3fa32bcd8 100644 --- a/14th-team5-iOS/Data/Sources/APIs/APIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Trash/APIWorker.swift @@ -16,13 +16,14 @@ import RxSwift // MARK: - Base API Worker +@available(*, deprecated, renamed: "BBAPIWorker") public class APIWorker: NSObject { // MARK: - Identifier var id: String = "APIWorker" private static let session: Session = { - let networkMonitor: BibbiNetworkMonitor = BibbiNetworkMonitor() + let networkMonitor: BBNetworkEventMonitor = BBNetworkDefaultLogger() let networkConfiguration: URLSessionConfiguration = AF.session.configuration let networkInterceptor: RequestInterceptor = NetworkInterceptor() let networkSession: Session = Session( @@ -132,7 +133,7 @@ public class APIWorker: NSObject { headers: [APIHeader]? = nil, jsonEncodable: Encodable ) -> Observable<(HTTPURLResponse, Data)> { - guard let jsonData = jsonEncodable.encodeData() else { + guard let jsonData = try? jsonEncodable.toData() else { return Observable.error(AFError.explicitlyCancelled) } diff --git a/14th-team5-iOS/Data/Sources/APIs/NetworkIntercepter.swift b/14th-team5-iOS/Data/Sources/Trash/NetworkIntercepter.swift similarity index 98% rename from 14th-team5-iOS/Data/Sources/APIs/NetworkIntercepter.swift rename to 14th-team5-iOS/Data/Sources/Trash/NetworkIntercepter.swift index f3b06e425..1d119d495 100644 --- a/14th-team5-iOS/Data/Sources/APIs/NetworkIntercepter.swift +++ b/14th-team5-iOS/Data/Sources/Trash/NetworkIntercepter.swift @@ -13,6 +13,7 @@ import Alamofire import RxCocoa import RxSwift +@available(*, deprecated, renamed: "BBInterceptor") public final class NetworkInterceptor: RequestInterceptor { // MARK: - Properties diff --git a/14th-team5-iOS/Domain/Sources/Entities/Family/FamilyGroupInfoEntity.swift b/14th-team5-iOS/Domain/Sources/Entities/Family/FamilyGroupInfoEntity.swift index 12c26e081..df1058e92 100644 --- a/14th-team5-iOS/Domain/Sources/Entities/Family/FamilyGroupInfoEntity.swift +++ b/14th-team5-iOS/Domain/Sources/Entities/Family/FamilyGroupInfoEntity.swift @@ -9,15 +9,15 @@ import Foundation public struct FamilyGroupInfoEntity { public let familyId: String - public let familyName: String - public let familyNameEditorId: String + public let familyName: String? + public let familyNameEditorId: String? public let createdAt: Date public init( familyId: String, - familyName: String, - familyNameEditorId: String, + familyName: String?, + familyNameEditorId: String?, createdAt: Date ) { self.familyId = familyId diff --git a/14th-team5-iOS/Domain/Sources/Repositories/AppRepository.swift b/14th-team5-iOS/Domain/Sources/Repositories/AppRepository.swift index 5dcdb49cb..85d11b1f4 100644 --- a/14th-team5-iOS/Domain/Sources/Repositories/AppRepository.swift +++ b/14th-team5-iOS/Domain/Sources/Repositories/AppRepository.swift @@ -11,7 +11,10 @@ import RxSwift public protocol AppRepositoryProtocol { func fetchAppVersion() -> Observable - func fetchIsFirstFamilyManagement() -> Observable + func loadIsFirstFamilyManagement() -> Observable func saveIsFirstFamilyManagement(isFirst: Bool) -> Void + + func loadIsFirstWidgetAlert() -> Observable + func saveIsFirstWidgetAlert(isFirst: Bool) -> Void } diff --git a/14th-team5-iOS/Domain/Sources/Repositories/CommentRepository.swift b/14th-team5-iOS/Domain/Sources/Repositories/CommentRepository.swift index 72d38153d..85f815097 100644 --- a/14th-team5-iOS/Domain/Sources/Repositories/CommentRepository.swift +++ b/14th-team5-iOS/Domain/Sources/Repositories/CommentRepository.swift @@ -10,7 +10,7 @@ import Foundation import RxSwift public protocol CommentRepositoryProtocol { - func fetchPostComment(postId: String, query: PostCommentPaginationQuery) -> Observable + func fetchPostComment(postId: String, query: PostCommentPaginationQuery) -> Observable func createPostComment(postId: String, body: CreatePostCommentRequest) -> Observable func updatePostComment(postId: String, commentId: String, body: UpdatePostCommentRequest) -> Observable func deletePostComment(postId: String, commentId: String) -> Observable diff --git a/14th-team5-iOS/Domain/Sources/Trash/PostComment/UseCase/PostCommentUseCase.swift b/14th-team5-iOS/Domain/Sources/Trash/PostComment/UseCase/PostCommentUseCase.swift index 9c06dc7c4..a7bece531 100644 --- a/14th-team5-iOS/Domain/Sources/Trash/PostComment/UseCase/PostCommentUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/Trash/PostComment/UseCase/PostCommentUseCase.swift @@ -12,7 +12,7 @@ import RxSwift @available(*, deprecated) public protocol PostCommentUseCaseProtocol { - func executeFetchPostComment(postId: String, query: PostCommentPaginationQuery) -> Observable + func executeFetchPostComment(postId: String, query: PostCommentPaginationQuery) -> Observable func executeCreatePostComment(postId: String, body: CreatePostCommentRequest) -> Observable func executeDeletePostComment(postId: String, commentId: String) -> Observable } @@ -27,7 +27,7 @@ public final class PostCommentUseCase: PostCommentUseCaseProtocol { self.commentRepository = commentRepository } - public func executeFetchPostComment(postId: String, query: PostCommentPaginationQuery) -> Observable { + public func executeFetchPostComment(postId: String, query: PostCommentPaginationQuery) -> Observable { return commentRepository.fetchPostComment(postId: postId, query: query) } diff --git a/14th-team5-iOS/Domain/Sources/UseCases/App/FetchFamilyManagementUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/App/FetchFamilyManagementUseCase.swift index 8c226dab1..7d04f2f49 100644 --- a/14th-team5-iOS/Domain/Sources/UseCases/App/FetchFamilyManagementUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/App/FetchFamilyManagementUseCase.swift @@ -9,11 +9,11 @@ import Foundation import RxSwift -public protocol FetchIsFirstFamilyManagementUseCaseProtocol { +public protocol IsFirstFamilyManagementUseCaseProtocol { func execute() -> Observable } -public class FetchFamilyManagementUseCase: FetchIsFirstFamilyManagementUseCaseProtocol { +public class IsFirstFamilyManagementUseCase: IsFirstFamilyManagementUseCaseProtocol { private let repository: AppRepositoryProtocol @@ -22,10 +22,10 @@ public class FetchFamilyManagementUseCase: FetchIsFirstFamilyManagementUseCasePr } public func execute() -> Observable { - repository.fetchIsFirstFamilyManagement() + repository.loadIsFirstFamilyManagement() .map { isFirst in guard let isFirst else { - return false + return true } return isFirst } diff --git a/14th-team5-iOS/Domain/Sources/UseCases/App/IsFirstWidgetAlertUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/App/IsFirstWidgetAlertUseCase.swift new file mode 100644 index 000000000..d1db62e58 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/UseCases/App/IsFirstWidgetAlertUseCase.swift @@ -0,0 +1,27 @@ +// +// CheckWidgetAlertUseCaseprotocol.swift +// Domain +// +// Created by 마경미 on 15.10.24. +// + +import RxSwift + +public protocol IsFirstWidgetAlertUseCaseProtocol { + func execute() -> Observable +} + +public class IsFirstWidgetAlertUseCase: IsFirstWidgetAlertUseCaseProtocol { + + private let repository: AppRepositoryProtocol + + public init(repository: AppRepositoryProtocol) { + self.repository = repository + } + + public func execute() -> Observable { + repository.loadIsFirstWidgetAlert() + .map { ($0 ?? true) } + .asObservable() + } +} diff --git a/14th-team5-iOS/Domain/Sources/UseCases/App/UpdateFamilyManagementUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/App/SaveIsFirstFamilyManagementUseCase.swift similarity index 74% rename from 14th-team5-iOS/Domain/Sources/UseCases/App/UpdateFamilyManagementUseCase.swift rename to 14th-team5-iOS/Domain/Sources/UseCases/App/SaveIsFirstFamilyManagementUseCase.swift index cf0334ed0..82ee08ccc 100644 --- a/14th-team5-iOS/Domain/Sources/UseCases/App/UpdateFamilyManagementUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/App/SaveIsFirstFamilyManagementUseCase.swift @@ -9,11 +9,11 @@ import Foundation import RxSwift -public protocol UpdateFamilyManagementUseCaseProtocol { +public protocol SaveIsFirstFamilyManagementUseCaseProtocol { func execute(_ isFirst: Bool) } -public class UpdateFamilyManagementUseCase: UpdateFamilyManagementUseCaseProtocol { +public class SaveIsFirstFamilyManagementUseCase: SaveIsFirstFamilyManagementUseCaseProtocol { private let repository: AppRepositoryProtocol diff --git a/14th-team5-iOS/Domain/Sources/UseCases/App/SaveIsFirstWidgetAlertUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/App/SaveIsFirstWidgetAlertUseCase.swift new file mode 100644 index 000000000..749aca15d --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/UseCases/App/SaveIsFirstWidgetAlertUseCase.swift @@ -0,0 +1,27 @@ +// +// UpdateWidgetAlertUseCase.swift +// Domain +// +// Created by 마경미 on 16.10.24. +// + +import Foundation + +import RxSwift + +public protocol SaveIsFirstWidgetAlertUseCaseProtocol { + func execute(_ isFirst: Bool) +} + +public class SaveIsFirstWidgetAlertUseCase: SaveIsFirstWidgetAlertUseCaseProtocol { + + private let repository: AppRepositoryProtocol + + public init(repository: AppRepositoryProtocol) { + self.repository = repository + } + + public func execute(_ isFirst: Bool) { + repository.saveIsFirstWidgetAlert(isFirst: isFirst) + } +} diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Comment/FetchCommentUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Comment/FetchCommentUseCase.swift index f98f8a3b6..f7b9d2570 100644 --- a/14th-team5-iOS/Domain/Sources/UseCases/Comment/FetchCommentUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/Comment/FetchCommentUseCase.swift @@ -10,7 +10,7 @@ import Foundation import RxSwift public protocol FetchCommentUseCaseProtocol { - func execute(postId: String, query: PostCommentPaginationQuery) -> Observable + func execute(postId: String, query: PostCommentPaginationQuery) -> Observable } public final class FetchCommentUseCase: FetchCommentUseCaseProtocol { @@ -27,7 +27,7 @@ public final class FetchCommentUseCase: FetchCommentUseCaseProtocol { public func execute( postId: String, query: PostCommentPaginationQuery - ) -> Observable { + ) -> Observable { return commentRepository.fetchPostComment(postId: postId, query: query) } diff --git a/14th-team5-iOS/Domain/Sources/UseCases/MainView/CheckSurvivalTimeUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/MainView/CheckSurvivalTimeUseCase.swift new file mode 100644 index 000000000..e5b4a1da1 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/UseCases/MainView/CheckSurvivalTimeUseCase.swift @@ -0,0 +1,35 @@ +// +// CheckSurvivalTimeUseCase.swift +// Domain +// +// Created by 마경미 on 30.09.24. +// + +import Foundation + +public protocol CheckSurvivalTimeUseCaseProtocol { + func execute() -> (isIntime: Bool, remainTIme: Int) +} + +public final class CheckSurvivalTimeUseCase: CheckSurvivalTimeUseCaseProtocol { + public func execute() -> (isIntime: Bool, remainTIme: Int) { + let calendar = Calendar.current + let currentTime = Date() + + let currentHour = calendar.component(.hour, from: currentTime) + + if currentHour >= 10 { + if let nextMidnight = calendar.date(bySettingHour: 0, minute: 0, second: 0, of: currentTime.addingTimeInterval(24 * 60 * 60)) { + let timeDifference = calendar.dateComponents([.second], from: currentTime, to: nextMidnight) + return (true, max(0, timeDifference.second ?? 0)) + } + } else { + if let nextMidnight = calendar.date(bySettingHour: 12, minute: 0, second: 0, of: currentTime) { + let timeDifference = calendar.dateComponents([.second], from: currentTime, to: nextMidnight) + return (false, max(0, timeDifference.second ?? 0)) + } + } + + return (false, 1000) + } +} diff --git a/Tuist/ProjectDescriptionHelpers/Modular+Templates.swift b/Tuist/ProjectDescriptionHelpers/Modular+Templates.swift index 4a4f80d76..ef25909d5 100644 --- a/Tuist/ProjectDescriptionHelpers/Modular+Templates.swift +++ b/Tuist/ProjectDescriptionHelpers/Modular+Templates.swift @@ -79,7 +79,7 @@ extension Target { case .App: return .target( name: layer.rawValue, - destinations: .iOS, + destinations: [.iPhone], product: factory.products.isApp ? .app : .staticFramework, bundleId: factory.bundleId.lowercased(), deploymentTargets: factory.deploymentTargets, diff --git a/Tuist/ProjectDescriptionHelpers/Project+Templates.swift b/Tuist/ProjectDescriptionHelpers/Project+Templates.swift index f78e441a1..1c68726be 100644 --- a/Tuist/ProjectDescriptionHelpers/Project+Templates.swift +++ b/Tuist/ProjectDescriptionHelpers/Project+Templates.swift @@ -59,7 +59,7 @@ extension Project { settings: .settings( base: [ "OTHER_LDFLAGS": ["-ObjC"], - "MARKETING_VERSION": "1.0", + "MARKETING_VERSION": "1.2.3", "CURRENT_PROJECT_VERSION": "1", "VERSIONING_SYSTEM": "apple-generic" ], diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 560736ae3..48f224b90 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -43,7 +43,7 @@ platform :ios do lane :github_action_stg_upload_testflight do |options| - app_store_connect_api_key( + api_key = app_store_connect_api_key( key_id: "#{APP_STORE_CONNECT_API_KEY_KEY_ID}", issuer_id: "#{APP_STORE_CONNECT_API_KEY_ISSUER_ID}", key_content: "#{APP_STORE_CONNECT_API_KEY_KEY}", @@ -62,7 +62,12 @@ platform :ios do generate_apple_certs: false ) - new_build_number = latest_testflight_build_number() + 1 + build_num = app_store_build_number( + api_key: api_key, + live: false + ) + + new_build_number = build_num + 1 increment_build_number( @@ -125,7 +130,7 @@ platform :ios do lane :github_action_prd_upload_testflight do |options| - app_store_connect_api_key( + api_key = app_store_connect_api_key( key_id: "#{APP_STORE_CONNECT_API_KEY_KEY_ID}", issuer_id: "#{APP_STORE_CONNECT_API_KEY_ISSUER_ID}", key_content: "#{APP_STORE_CONNECT_API_KEY_KEY}", @@ -144,7 +149,12 @@ platform :ios do generate_apple_certs: false ) - new_build_number = latest_testflight_build_number() + 1 + build_num = app_store_build_number( + api_key: api_key, + live: false + ) + + new_build_number = build_num + 1 increment_build_number(