From 3ee83db009957edac6b2ae2dc1bf786eb5892392 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Thu, 2 May 2024 16:33:53 +0900 Subject: [PATCH 001/263] =?UTF-8?q?feat:=20Home=20=EC=9E=AC=EC=B4=89?= =?UTF-8?q?=ED=95=98=EA=B8=B0=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?(#488)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Home/Dependency/MainViewDIContainer.swift | 15 +++- .../Home/Model/FamilySection.swift | 36 +++------- .../Reactor/Cell/MainFamilyCellReactor.swift | 71 ++++++++++++++++--- .../Home/Reactor/MainViewReactor.swift | 67 ++++++++++++++--- .../MainFamilyViewController.swift | 16 ++--- .../ViewControllers/MainViewController.swift | 63 ++++++++++++---- .../Views/MainFamilyCollectionViewCell.swift | 6 ++ .../GlobalState/GlobalStateProvider.swift | 3 + .../Sources/GlobalState/HomeService.swift | 41 +++++++++++ .../yellowPaperPlane.imageset/Contents.json | 12 ++++ .../icon-paper-plane.svg | 3 + .../Sources/Main/Entities/FamilySection.swift | 54 +++++++------- .../Sources/Main/Entities/MainData.swift | 4 +- .../xcschemes/Bibbi-Workspace.xcscheme | 56 --------------- 14 files changed, 293 insertions(+), 154 deletions(-) create mode 100644 14th-team5-iOS/Core/Sources/GlobalState/HomeService.swift create mode 100644 14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/yellowPaperPlane.imageset/Contents.json create mode 100644 14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/yellowPaperPlane.imageset/icon-paper-plane.svg diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Dependency/MainViewDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Dependency/MainViewDIContainer.swift index 0632bd8a0..5d63b82e5 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Dependency/MainViewDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Dependency/MainViewDIContainer.swift @@ -28,7 +28,20 @@ final class MainViewDIContainer { extension MainViewDIContainer { private func makeReactor() -> MainViewReactor { - return MainViewReactor(initialState: .init(isInTime: false), fetchMainUseCase: makeFetchMainUseCase(), provider: globalState) + return MainViewReactor( + initialState: .init(isInTime: false), + fetchMainUseCase: makeFetchMainUseCase(), + pickUseCase: makePickUseCase(), + provider: globalState + ) + } + + private func makePickReposiotry() -> PickRepositoryProtocol { + return PickRepository() + } + + private func makePickUseCase() -> PickUseCaseProtocol { + return PickUseCase(pickRepository: makePickReposiotry()) } private func makeMainRepository() -> MainRepository { diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Model/FamilySection.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Model/FamilySection.swift index ed1342053..1894d8174 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Model/FamilySection.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Model/FamilySection.swift @@ -8,30 +8,12 @@ import RxDataSources import Domain -//struct FamilySection { -// typealias Model = SectionModel -// -// enum Item { -// case main(ProfileData) -// } -//} -// -//extension FamilySection.Item: Equatable, Hashable { -// public static func == (lhs: FamilySection.Item, rhs: FamilySection.Item) -> Bool { -// switch (lhs, rhs) { -// case (.main(let leftFamily), .main(let rightFamily)): -// return leftFamily == rightFamily -// default: -// return false -// } -// } -// -// public func hash(into hasher: inout Hasher) { -// switch self { -// case .main(let family): -// hasher.combine(family) -// default: -// break -// } -// } -//} +struct FamilySection { + typealias Model = SectionModel + + enum Item { + case main(MainFamilyCellReactor) + } +} + +extension FamilySection.Item: Equatable { } diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/Cell/MainFamilyCellReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/Cell/MainFamilyCellReactor.swift index 13b3cd556..2187d866e 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/Cell/MainFamilyCellReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/Cell/MainFamilyCellReactor.swift @@ -14,42 +14,78 @@ import ReactorKit enum RankBadge: Int { case one = 1 - case two - case three + case two = 2 + case three = 3 } final class MainFamilyCellReactor: Reactor { + + // MARK: - Action enum Action { case fetchData + case pickButtonTapped } + // MARK: - Mutation enum Mutation { case setData + case setPickButtonAppearent(Bool) } + // MARK: - State struct State { let profileData: ProfileData - var profile: (String?, String) = (nil,"") + var profile: (imageUrl: String?, name: String) = (nil, .none) var rank: Int? = nil var isShowBirthdayBadge: Bool = false var isShowPickButton: Bool = false } + // MARK: - Properties let initialState: State + let provider: GlobalStateProviderProtocol - init(initialState: State) { - self.initialState = initialState + // MARK: - Intializer + init(_ profileData: ProfileData, service provider: GlobalStateProviderProtocol) { + self.initialState = State(profileData: profileData) + self.provider = provider } -} - -extension MainFamilyCellReactor { + + // MARK: - Transofrm + func transform(mutation: Observable) -> Observable { + let homeMutation = provider.homeService.event + .flatMap { event in + switch event { + case let .showPickButton(show, id): + guard id == self.currentState.profileData.memberId else { + return Observable.empty() + } + return Observable.just(.setPickButtonAppearent(show)) + + default: + return Observable.empty() + } + } + + return Observable.merge(mutation, homeMutation) + } + + // MARK: - Mutate func mutate(action: Action) -> Observable { switch action { case .fetchData: return Observable.just(.setData) + + case .pickButtonTapped: + provider.homeService.pickButtonTapped( + name: currentState.profileData.name, + memberId: currentState.profileData.memberId + ) + return Observable.empty() } } - + + // MARK: - Reduce func reduce(state: State, mutation: Mutation) -> State { var newState = state @@ -59,8 +95,25 @@ extension MainFamilyCellReactor { newState.rank = currentState.profileData.postRank newState.isShowBirthdayBadge = currentState.profileData.isShowBirthdayMark newState.isShowPickButton = currentState.profileData.isShowPickIcon + + case let .setPickButtonAppearent(show): + newState.isShowPickButton = show } return newState } + +} + +// MARK: - Extensions +extension MainFamilyCellReactor: Equatable { + + static func == (lhs: MainFamilyCellReactor, rhs: MainFamilyCellReactor) -> Bool { + lhs.initialState.id == rhs.initialState.id + } + +} + +extension MainFamilyCellReactor.State: Identifiable { + var id: UUID { UUID() } } diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift index a23febc63..7435d7e52 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift @@ -20,6 +20,7 @@ final class MainViewReactor: Reactor { case fetchMainUseCase case didTapSegmentControl(PostType) + case pickConfirmButtonTapped(String, String) case pushWidgetPostDeepLink(WidgetDeepLink) case pushNotificationPostDeepLink(NotificationDeepLink) @@ -32,7 +33,12 @@ final class MainViewReactor: Reactor { case setInTime(Bool) case setPageIndex(Int) - case setCopySuccessToastMessageView + + case setPickSuccessToastMessage(String) + case setCopySuccessToastMessage + case setFailureToastMessage + + case setPickAlertView(String, String) case setWidgetPostDeepLink(WidgetDeepLink) case setNotificationPostDeepLink(NotificationDeepLink) @@ -46,38 +52,59 @@ final class MainViewReactor: Reactor { @Pulse var isMeSurvivalUploadedToday: Bool = false @Pulse var isMissionUnlocked: Bool = false @Pulse var familySection: [FamilySection.Item] = [] + @Pulse var widgetPostDeepLink: WidgetDeepLink? @Pulse var notificationPostDeepLink: NotificationDeepLink? @Pulse var notificationCommentDeepLink: NotificationDeepLink? - - @Pulse var shouldPresentCopySuccessToastMessageView: Bool = false + + @Pulse var shouldPresentPickAlert: (String, String)? + @Pulse var shouldPresentPickSuccessToastMessage: String? + @Pulse var shouldPresentCopySuccessToastMessage: Bool = false + @Pulse var shouldPresentFailureToastMessage: Bool = false } let initialState: State let fetchMainUseCase: FetchMainUseCaseProtocol + let pickUseCase: PickUseCaseProtocol let provider: GlobalStateProviderProtocol - init(initialState: State, fetchMainUseCase: FetchMainUseCaseProtocol, provider: GlobalStateProviderProtocol) { + init( + initialState: State, + fetchMainUseCase: FetchMainUseCaseProtocol, + pickUseCase: PickUseCaseProtocol, + provider: GlobalStateProviderProtocol + ) { self.initialState = initialState self.fetchMainUseCase = fetchMainUseCase + self.pickUseCase = pickUseCase self.provider = provider } } extension MainViewReactor { func transform(mutation: Observable) -> Observable { + let homeMutation = provider.homeService.event + .flatMap { event in + switch event { + case let .presentPickAlert(name, id): + return Observable.just(.setPickAlertView(name, id)) + default: + return Observable.empty() + } + } + let eventMutation = provider.activityGlobalState.event .flatMap { event -> Observable in switch event { case .didTapCopyInvitationUrlAction: - return Observable.just(.setCopySuccessToastMessageView) + return Observable.just(.setCopySuccessToastMessage) default: return Observable.empty() } } - return Observable.merge(mutation, eventMutation) + return Observable.merge(mutation, eventMutation, homeMutation) } func mutate(action: Action) -> Observable { @@ -123,6 +150,17 @@ extension MainViewReactor { ) case .didTapSegmentControl(let type): return Observable.just(.setPageIndex(type.getIndex())) + + case let .pickConfirmButtonTapped(name, id): + return pickUseCase.executePickMember(memberId: id) + .flatMap { response in + guard let response = response, + response.success else { + return Observable.just(.setFailureToastMessage) + } + self.provider.homeService.showPickButton(false, memberId: id) + return Observable.just(.setPickSuccessToastMessage(name)) + } } } @@ -138,14 +176,25 @@ extension MainViewReactor { newState.notificationPostDeepLink = deepLink case let .setNotificationCommentDeepLink(deepLink): newState.notificationCommentDeepLink = deepLink - case .setCopySuccessToastMessageView: - newState.shouldPresentCopySuccessToastMessageView = true + case .setCopySuccessToastMessage: + newState.shouldPresentCopySuccessToastMessage = true + case let .setPickSuccessToastMessage(name): + newState.shouldPresentPickSuccessToastMessage = name + case .setFailureToastMessage: + newState.shouldPresentFailureToastMessage = true case .setPageIndex(let index): newState.pageIndex = index case .updateMainData(let data): newState.isMissionUnlocked = data.isMissionUnlocked newState.isMeSurvivalUploadedToday = data.isMeSurvivalUploadedToday - newState.familySection = data.mainFamilyProfileDatas + newState.familySection = FamilySection.Model( + model: 0, + items: data.mainFamilyProfileDatas.map { + .main(MainFamilyCellReactor($0, service: provider)) + } + ).items + case let .setPickAlertView(name, id): + newState.shouldPresentPickAlert = (name, id) } return newState diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainFamilyViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainFamilyViewController.swift index c8ec7bcf2..0cf4f4b2c 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainFamilyViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainFamilyViewController.swift @@ -88,7 +88,7 @@ extension MainFamilyViewController { .throttle(RxConst.throttleInterval, scheduler: MainScheduler.instance) .compactMap { item -> ProfileData? in switch item { - case .main(let profileData): return profileData + case let .main(reactor): return reactor.currentState.profileData } } .withUnretained(self) @@ -132,13 +132,9 @@ extension MainFamilyViewController { .disposed(by: disposeBag) reactor.pulse(\.$shouldPresentFetchFailureToastMessageView) - .skip(1) - .withUnretained(self) - .subscribe { - $0.0.makeBibbiToastView( - text: "잠시 후에 다시 시도해주세요", - image: DesignSystemAsset.warning.image - ) + .filter { $0 } + .bind(with: self) { owner, _ in + owner.makeErrorBibbiToastView() } .disposed(by: disposeBag) } @@ -160,11 +156,11 @@ extension MainFamilyViewController { return RxCollectionViewSectionedReloadDataSource( configureCell: { (_, collectionView, indexPath, item) in switch item { - case .main(let data): + case let .main(reactor): guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: MainFamilyCollectionViewCell.id, for: indexPath) as? MainFamilyCollectionViewCell else { return UICollectionViewCell() } - cell.reactor = MainFamilyCellReactor(initialState: .init(profileData: data)) + cell.reactor = reactor return cell } }) 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 d37cc0536..b56c759f8 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift @@ -30,6 +30,8 @@ final class MainViewController: BaseViewController, UICollectio private let memberRepo = App.Repository.member private let deepLinkRepo = App.Repository.deepLink + private let alertConfirmRelay = PublishRelay<(String, String)>() + override func viewDidLoad() { super.viewDidLoad() @@ -166,7 +168,7 @@ extension MainViewController { $0.0.navigationController?.pushViewController(cameraViewController, animated: true) }) .disposed(by: disposeBag) - + // 위젯 딥링크 코드 App.Repository.deepLink.widget .compactMap { $0 } @@ -197,6 +199,7 @@ extension MainViewController { .observe(on: MainScheduler.instance) .bind(to: familyViewController.familySectionRelay) .disposed(by: disposeBag) + // reactor.pulse(\.$isSelfUploaded) // .distinctUntilChanged() // .observe(on: MainScheduler.instance) @@ -214,7 +217,52 @@ extension MainViewController { $0.0.descriptionView.postTypeRelay.accept(($0.1 == 0) ? .survival : .mission) }) .disposed(by: disposeBag) -// + + reactor.pulse(\.$shouldPresentPickAlert) + .compactMap { $0 } + .bind(with: self) { owner, profile in + BibbiAlertBuilder(owner) + .alertStyle(.pickMember(profile.0)) + .setConfirmAction { + // (name, memberId) + owner.alertConfirmRelay.accept((profile.0, profile.1)) + } + .present() + } + .disposed(by: disposeBag) + + alertConfirmRelay + .map { Reactor.Action.pickConfirmButtonTapped($0.0, $0.1) } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + reactor.pulse(\.$shouldPresentPickSuccessToastMessage) + .compactMap { $0 } + .bind(with: self) { owner, name in + owner.makeBibbiToastView( + text: "\(name)님에게 생존신고 알림을 보냈어요", + image: DesignSystemAsset.yellowPaperPlane.image + ) + } + .disposed(by: disposeBag) + + reactor.pulse(\.$shouldPresentCopySuccessToastMessage) + .filter { $0 } + .bind(with: self) { owner, _ in + owner.makeBibbiToastView( + text: "링크가 복사되었어요", + image: DesignSystemAsset.link.image + ) + } + .disposed(by: disposeBag) + + reactor.pulse(\.$shouldPresentFailureToastMessage) + .filter { $0 } + .bind(with: self) { owner, _ in + owner.makeErrorBibbiToastView() + } + .disposed(by: disposeBag) + // // 위젯 딥링크 코드 // reactor.pulse(\.$widgetPostDeepLink) // .delay(RxConst.smallDelayInterval, scheduler: Schedulers.main) @@ -241,17 +289,6 @@ extension MainViewController { // owner.handleCommentNotificationDeepLink(deepLink) // } // .disposed(by: disposeBag) - - reactor.pulse(\.$shouldPresentCopySuccessToastMessageView) - .skip(1) - .withUnretained(self) - .subscribe { - $0.0.makeBibbiToastView( - text: "링크가 복사되었어요", - image: DesignSystemAsset.link.image - ) - } - .disposed(by: disposeBag) } } diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Views/MainFamilyCollectionViewCell.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Views/MainFamilyCollectionViewCell.swift index 078fcf456..4a95eef87 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Views/MainFamilyCollectionViewCell.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Views/MainFamilyCollectionViewCell.swift @@ -121,6 +121,12 @@ extension MainFamilyCollectionViewCell { .map { Reactor.Action.fetchData} .bind(to: reactor.action) .disposed(by: disposeBag) + + pickButton.rx.tap + .throttle(RxConst.throttleInterval, scheduler: Schedulers.main) + .map { Reactor.Action.pickButtonTapped } + .bind(to: reactor.action) + .disposed(by: disposeBag) } private func bindOutput(reactor: MainFamilyCellReactor) { diff --git a/14th-team5-iOS/Core/Sources/GlobalState/GlobalStateProvider.swift b/14th-team5-iOS/Core/Sources/GlobalState/GlobalStateProvider.swift index abde23304..d99a4d6c0 100644 --- a/14th-team5-iOS/Core/Sources/GlobalState/GlobalStateProvider.swift +++ b/14th-team5-iOS/Core/Sources/GlobalState/GlobalStateProvider.swift @@ -15,6 +15,7 @@ public protocol GlobalStateProviderProtocol: AnyObject { var profileGlobalState: ProfileGlobalStateType { get } var timerGlobalState: TimerGlobalStateType { get } var realEmojiGlobalState: RealEmojiGlobalStateType { get } + var homeService: HomeServiceType { get } } final public class GlobalStateProvider: GlobalStateProviderProtocol { @@ -27,5 +28,7 @@ final public class GlobalStateProvider: GlobalStateProviderProtocol { public lazy var timerGlobalState: TimerGlobalStateType = TimerGlobalState(provider: self) public lazy var realEmojiGlobalState: RealEmojiGlobalStateType = RealEmojiGlobalState(provider: self) + public lazy var homeService: HomeServiceType = HomeService(provider: self) + public init() { } } diff --git a/14th-team5-iOS/Core/Sources/GlobalState/HomeService.swift b/14th-team5-iOS/Core/Sources/GlobalState/HomeService.swift new file mode 100644 index 000000000..c9c565cf3 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/GlobalState/HomeService.swift @@ -0,0 +1,41 @@ +// +// HomeService.swift +// Core +// +// Created by 김건우 on 5/2/24. +// + +import Foundation + +import RxSwift + +public enum HomeEvent { + case presentPickAlert(String, String) + case showPickButton(Bool, String) +} + +public protocol HomeServiceType { + var event: PublishSubject { get } + + @discardableResult + func pickButtonTapped(name: String, memberId id: String) -> Observable + @discardableResult + func showPickButton(_ show: Bool, memberId id: String) -> Observable +} + +final public class HomeService: BaseGlobalState, HomeServiceType { + public var event = PublishSubject() + + @discardableResult + public func pickButtonTapped(name: String, memberId id: String) -> Observable { + event.onNext(.presentPickAlert(name, id)) + return Observable.empty() + } + + @discardableResult + public func showPickButton(_ show: Bool, memberId id: String) -> Observable { + event.onNext(.showPickButton(show, id)) + return Observable.just(show) + } + +} diff --git a/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/yellowPaperPlane.imageset/Contents.json b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/yellowPaperPlane.imageset/Contents.json new file mode 100644 index 000000000..9197bed96 --- /dev/null +++ b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/yellowPaperPlane.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "icon-paper-plane.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/yellowPaperPlane.imageset/icon-paper-plane.svg b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/yellowPaperPlane.imageset/icon-paper-plane.svg new file mode 100644 index 000000000..9077e80b8 --- /dev/null +++ b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/yellowPaperPlane.imageset/icon-paper-plane.svg @@ -0,0 +1,3 @@ + + + diff --git a/14th-team5-iOS/Domain/Sources/Main/Entities/FamilySection.swift b/14th-team5-iOS/Domain/Sources/Main/Entities/FamilySection.swift index cc8e9fa18..1ddc6d1e5 100644 --- a/14th-team5-iOS/Domain/Sources/Main/Entities/FamilySection.swift +++ b/14th-team5-iOS/Domain/Sources/Main/Entities/FamilySection.swift @@ -8,30 +8,30 @@ import Foundation import RxDataSources -public struct FamilySection { - public typealias Model = SectionModel - - public enum Item { - case main(ProfileData) - } -} - -extension FamilySection.Item: Equatable, Hashable { - public static func == (lhs: FamilySection.Item, rhs: FamilySection.Item) -> Bool { - switch (lhs, rhs) { - case (.main(let leftFamily), .main(let rightFamily)): - return leftFamily == rightFamily - default: - return false - } - } - - public func hash(into hasher: inout Hasher) { - switch self { - case .main(let family): - hasher.combine(family) - default: - break - } - } -} +//public struct FamilySection { +// public typealias Model = SectionModel +// +// public enum Item { +// case main(ProfileData) +// } +//} +// +//extension FamilySection.Item: Equatable, Hashable { +// public static func == (lhs: FamilySection.Item, rhs: FamilySection.Item) -> Bool { +// switch (lhs, rhs) { +// case (.main(let leftFamily), .main(let rightFamily)): +// return leftFamily == rightFamily +// default: +// return false +// } +// } +// +// public func hash(into hasher: inout Hasher) { +// switch self { +// case .main(let family): +// hasher.combine(family) +// default: +// break +// } +// } +//} diff --git a/14th-team5-iOS/Domain/Sources/Main/Entities/MainData.swift b/14th-team5-iOS/Domain/Sources/Main/Entities/MainData.swift index 565042a1a..993b0628a 100644 --- a/14th-team5-iOS/Domain/Sources/Main/Entities/MainData.swift +++ b/14th-team5-iOS/Domain/Sources/Main/Entities/MainData.swift @@ -20,7 +20,7 @@ public struct Picker { } public struct MainData { - public let mainFamilyProfileDatas: [FamilySection.Item] + public let mainFamilyProfileDatas: [ProfileData] public let leftUploadCountUntilMissionUnlock: Int public let isMissionUnlocked: Bool public let isMeSurvivalUploadedToday: Bool @@ -28,7 +28,7 @@ public struct MainData { public let pickers: [Picker] public init(mainFamilyProfileDatas: [ProfileData], leftUploadCountUntilMissionUnlock: Int, isMissionUnlocked: Bool, isMeSurvivalUploadedToday: Bool, isMeMissionUploadedToday: Bool, pickers: [Picker]) { - self.mainFamilyProfileDatas = mainFamilyProfileDatas.map(FamilySection.Item.main) + self.mainFamilyProfileDatas = mainFamilyProfileDatas self.leftUploadCountUntilMissionUnlock = leftUploadCountUntilMissionUnlock self.isMissionUnlocked = isMissionUnlocked self.isMeSurvivalUploadedToday = isMeSurvivalUploadedToday diff --git a/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme b/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme index 9f49bc0db..72399b415 100644 --- a/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme +++ b/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme @@ -300,20 +300,6 @@ ReferencedContainer = "container:Tuist/Dependencies/SwiftPackageManager/.build/checkouts/GoogleDataTransport/GoogleDataTransport.xcodeproj"> - - - - - - - - - - - - - - - - Date: Thu, 2 May 2024 16:46:00 +0900 Subject: [PATCH 002/263] =?UTF-8?q?add:=20FamilySection=EC=97=90=20NOTE=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#488)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Home/Model/FamilySection.swift | 11 ++++ .../DataSource/CommentSectionModel.swift | 13 +++++ .../DataSource/PostCommentSectionModel.swift | 52 ------------------- .../Dependency/PostCommentDIContainer.swift | 4 +- ...Reactor.swift => CommentViewReactor.swift} | 4 +- .../PostCommentViewController.swift | 14 ++--- 6 files changed, 35 insertions(+), 63 deletions(-) create mode 100644 14th-team5-iOS/App/Sources/Presentation/PostComment/DataSource/CommentSectionModel.swift delete mode 100644 14th-team5-iOS/App/Sources/Presentation/PostComment/DataSource/PostCommentSectionModel.swift rename 14th-team5-iOS/App/Sources/Presentation/PostComment/Reactor/{PostCommentViewReactor.swift => CommentViewReactor.swift} (99%) diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Model/FamilySection.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Model/FamilySection.swift index 1894d8174..9c6e8dcea 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Model/FamilySection.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Model/FamilySection.swift @@ -11,6 +11,17 @@ import Domain struct FamilySection { typealias Model = SectionModel + /// NOTE: - FamilySection의 Item 타입으로 `MainFamilyReactor`로 가져야 + /// `MainViewReactor`에서 `MainFamilyCellReactor`를 생성할 때, `GlobalState(Service)`를 주입시킬 수 있습니다. + /// 하지만, `FamilySection`이 `Domain` 모듈에 위치할 경우, `App` 모듈에 접근할 수 없어 + /// `MainFamilyCellReactor`를 Item 타입으로 지정할 수 없는 건 물론, 뷰 컨트롤러에게 Item을 넘겨주고 cell의 Reactor를 주입하는 과정이 무척이나 번거로워집니다. + /// + /// 아울러, `Entity`는 비즈니스 로직의 한 축을 이루는 원천 데이터인 반면, + /// `RxDatasource` 테이블을 구성하는 데 필요한 `ModelType`은 UI 구성에 조금 더 가깝지 않나라는 개인적인 견해도 가지고 있습니다. + /// + /// 이와 관련된 코드는 `PostSectionModel`과 `CommentViewReactor`를 참조하시면 됩니다. + /// 추가 자료는 [여기](https://ios-development.tistory.com/796)에서 확인하실 수 있습니다. 감사합니다. + enum Item { case main(MainFamilyCellReactor) } diff --git a/14th-team5-iOS/App/Sources/Presentation/PostComment/DataSource/CommentSectionModel.swift b/14th-team5-iOS/App/Sources/Presentation/PostComment/DataSource/CommentSectionModel.swift new file mode 100644 index 000000000..776477395 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/PostComment/DataSource/CommentSectionModel.swift @@ -0,0 +1,13 @@ +// +// CommentAnimatableSectionModel.swift +// App +// +// Created by 김건우 on 1/18/24. +// + +import Domain +import Foundation + +import Differentiator + +typealias CommentSectionModel = AnimatableSectionModel diff --git a/14th-team5-iOS/App/Sources/Presentation/PostComment/DataSource/PostCommentSectionModel.swift b/14th-team5-iOS/App/Sources/Presentation/PostComment/DataSource/PostCommentSectionModel.swift deleted file mode 100644 index de69ea69c..000000000 --- a/14th-team5-iOS/App/Sources/Presentation/PostComment/DataSource/PostCommentSectionModel.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// CommentAnimatableSectionModel.swift -// App -// -// Created by 김건우 on 1/18/24. -// - -import Domain -import Foundation - -import Differentiator - -typealias PostCommentSectionModel = AnimatableSectionModel - -extension PostCommentSectionModel { - static func generateTestData() -> [PostCommentSectionModel] { - let comment1: PostCommentResponse = PostCommentResponse( - commentId: .none, - postId: .none, - memberId: "01HM0SHPB8663AATDN1YZCEB7E", - comment: "Hello, Swift!", - createdAt: Date().addingTimeInterval(-300) - ) - - let comment2: PostCommentResponse = PostCommentResponse( - commentId: .none, - postId: .none, - memberId: "01HM4CZD12Y04FDEBG0WGST7KN", - comment: "Hello, UIKit!", - createdAt: Date().addingTimeInterval(-3_000) - ) - - let comment3: PostCommentResponse = PostCommentResponse( - commentId: .none, - postId: .none, - memberId: "01HM4CZD12Y04FDEBG0WGST7KN", - comment: "Hello, SwiftUI!", - createdAt: Date().addingTimeInterval(-30_000) - ) - - return [ - PostCommentSectionModel( - model: .none, - items: [ -// CommentCellReactor(comment1), -// CommentCellReactor(comment2), -// CommentCellReactor(comment3) - ] - ) - ] - } -} diff --git a/14th-team5-iOS/App/Sources/Presentation/PostComment/Dependency/PostCommentDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/PostComment/Dependency/PostCommentDIContainer.swift index bc79eb31c..c3468f02d 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostComment/Dependency/PostCommentDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostComment/Dependency/PostCommentDIContainer.swift @@ -47,8 +47,8 @@ public final class PostCommentDIContainer { return PostCommentUseCase(postCommentRepository: makePostCommentRespository()) } - public func makeReactor() -> PostCommentViewReactor { - return PostCommentViewReactor( + public func makeReactor() -> CommentViewReactor { + return CommentViewReactor( postId: postId, memberUseCase: makeMemberUseCase(), postCommentUseCase: makePostCommentUseCase(), diff --git a/14th-team5-iOS/App/Sources/Presentation/PostComment/Reactor/PostCommentViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/PostComment/Reactor/CommentViewReactor.swift similarity index 99% rename from 14th-team5-iOS/App/Sources/Presentation/PostComment/Reactor/PostCommentViewReactor.swift rename to 14th-team5-iOS/App/Sources/Presentation/PostComment/Reactor/CommentViewReactor.swift index 056d70ae0..d06f221c9 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostComment/Reactor/PostCommentViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostComment/Reactor/CommentViewReactor.swift @@ -12,7 +12,7 @@ import Foundation import ReactorKit import RxSwift -final public class PostCommentViewReactor: Reactor { +final public class CommentViewReactor: Reactor { // MARK: - Action public enum Action { case inputComment(String) @@ -51,7 +51,7 @@ final public class PostCommentViewReactor: Reactor { @Pulse var commentCount: Int var inputComment: String - @Pulse var displayComment: [PostCommentSectionModel] + @Pulse var displayComment: [CommentSectionModel] @Pulse var shouldScrollToLast: Int @Pulse var shouldClearCommentTextField: Bool @Pulse var shouldPresentUploadCommentFailureTaostMessageView: Bool diff --git a/14th-team5-iOS/App/Sources/Presentation/PostComment/ViewController/PostCommentViewController.swift b/14th-team5-iOS/App/Sources/Presentation/PostComment/ViewController/PostCommentViewController.swift index 6d1276326..7555cbe02 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostComment/ViewController/PostCommentViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostComment/ViewController/PostCommentViewController.swift @@ -16,7 +16,7 @@ import Then import SnapKit fileprivate typealias _Str = PostCommentStrings -final public class PostCommentViewController: BaseViewController { +final public class PostCommentViewController: BaseViewController { // MARK: - Views private let commentNavigationBarView: PostCommentTopBarView = PostCommentTopBarView() @@ -31,7 +31,7 @@ final public class PostCommentViewController: BaseViewController = prepareDatasource() + private lazy var dataSource: RxTableViewSectionedAnimatedDataSource = prepareDatasource() // MARK: - LifeCycles public override func viewDidLoad() { @@ -44,14 +44,14 @@ final public class PostCommentViewController: BaseViewController.just(()) .map { Reactor.Action.fetchPostComment } .bind(to: reactor.action) @@ -111,7 +111,7 @@ final public class PostCommentViewController: BaseViewController RxTableViewSectionedAnimatedDataSource { + private func prepareDatasource() -> RxTableViewSectionedAnimatedDataSource { return RxTableViewSectionedAnimatedDataSource { dataSource, tableView, indexPath, reactor in let cell = tableView.dequeueReusableCell(withIdentifier: CommentCell.id) as! CommentCell cell.reactor = reactor @@ -360,7 +360,7 @@ extension PostCommentViewController { } } - private func bindDatasource(reactor: PostCommentViewReactor) { + private func bindDatasource(reactor: CommentViewReactor) { dataSource.canEditRowAtIndexPath = { dataSource, indexPath in let myMemberId = App.Repository.member.memberID.value let cellMemberId = dataSource[indexPath].currentState.memberId From 5fc47f71b1fccbfe0ca7ee44e8bdb03c96c77698 Mon Sep 17 00:00:00 2001 From: Do-hyun-Kim Date: Thu, 2 May 2024 19:12:48 +0900 Subject: [PATCH 003/263] =?UTF-8?q?feat:=20CameraViewController,=20CameraD?= =?UTF-8?q?isplayViewController=20BibbiMissionView=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?-=20CameraAPIWorker=20=EC=98=A4=EB=8A=98=20=EB=AF=B8=EC=85=98?= =?UTF-8?q?=20API=20=EC=B6=94=EA=B0=80=20-=20CameraUseCase=20=EC=98=A4?= =?UTF-8?q?=EB=8A=98=20=EB=AF=B8=EC=85=98=20=EB=B9=84=EC=A6=88=EB=8B=88?= =?UTF-8?q?=EC=8A=A4=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80=20-=20Camer?= =?UTF-8?q?a=20Domain=20=EB=AA=A8=EB=93=88=20TodayMissionResponse=20Entity?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=20-=20CameraAPIWorker=20=EC=97=85?= =?UTF-8?q?=EB=A1=9C=EB=93=9C=20Query=20Param=20=EC=B6=94=EA=B0=80=20-=20C?= =?UTF-8?q?ameraDisplayViewReactor=20PostType=20=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B6=84=EA=B8=B0=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Camera/CameraDisplayViewController.swift | 18 ++++- .../Camera/CameraViewController.swift | 68 +++++++++++++------ .../Dependency/CameraDisplayDIContainer.swift | 10 ++- .../Reactor/CameraDisplayViewReactor.swift | 44 ++++++++---- .../Camera/Reactor/CameraViewReactor.swift | 39 ++++++++--- .../ViewControllers/MainViewController.swift | 2 +- .../ViewControllers/PostViewController.swift | 1 + .../Profile/ProfileDetailViewController.swift | 1 - .../Profile/ProfileViewController.swift | 8 +-- .../Sources/ShareView/BibbiMissionView.swift | 65 ++++++++++++++++++ .../Camera/CameraAPI/CameraAPIWorker.swift | 19 +++++- .../Sources/Camera/CameraAPI/CameraAPIs.swift | 9 ++- .../DataMapping/CameraDisplayPostDTO.swift | 4 ++ .../DataMapping/CameraTodayMissionDTO.swift | 36 ++++++++++ .../CameraDisplayViewRepository.swift | 7 +- .../Repositories/CameraViewRepository.swift | 6 ++ .../missionTitleBadge.imageset/Contents.json | 12 ++++ .../missionTitleBadge.svg | 19 ++++++ .../Entity/CameraDisplayPostResponse.swift | 6 ++ .../Entity/CameraMissionFeedQuery.swift | 20 ++++++ .../Entity/CameraTodayMissionResponse.swift | 22 ++++++ .../CameraDisplayViewInterface.swift | 40 ++++++++++- .../Interfaces/CameraViewInterface.swift | 1 + .../UseCases/CameraDisplayViewUseCase.swift | 6 +- .../Camera/UseCases/CameraViewUseCase.swift | 5 ++ 25 files changed, 398 insertions(+), 70 deletions(-) create mode 100644 14th-team5-iOS/Core/Sources/ShareView/BibbiMissionView.swift create mode 100644 14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraTodayMissionDTO.swift create mode 100644 14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/missionTitleBadge.imageset/Contents.json create mode 100644 14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/missionTitleBadge.imageset/missionTitleBadge.svg create mode 100644 14th-team5-iOS/Domain/Sources/Camera/Entity/CameraMissionFeedQuery.swift create mode 100644 14th-team5-iOS/Domain/Sources/Camera/Entity/CameraTodayMissionResponse.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Camera/CameraDisplayViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Camera/CameraDisplayViewController.swift index 15e90b055..939a5a71f 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Camera/CameraDisplayViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Camera/CameraDisplayViewController.swift @@ -20,6 +20,7 @@ import SnapKit public final class CameraDisplayViewController: BaseViewController { //MARK: Views private let displayView: UIImageView = UIImageView() + private let missionDisplayView: BibbiMissionView = BibbiMissionView() private let confirmButton: UIButton = UIButton(configuration: .plain()) private let displayIndicatorView: BibbiLoadingView = BibbiLoadingView() private let backButton: UIButton = UIButton(frame: CGRect(origin: .zero, size: CGSize(width: 52, height: 52))) @@ -52,7 +53,7 @@ public final class CameraDisplayViewController: BaseViewController { fileprivate var captureOutputStream: AVCapturePhotoOutput! //MARK: Views + private let missionView: BibbiMissionView = BibbiMissionView() private let cameraView: UIView = UIView() private let shutterButton: UIButton = UIButton() private let flashButton: UIButton = UIButton.createCircleButton(radius: 24) @@ -56,7 +57,7 @@ public final class CameraViewController: BaseViewController { private var isToggle: Bool = false //MARK: LifeCylce - + public override func viewDidLoad() { super.viewDidLoad() setupCameraPermission() @@ -66,7 +67,7 @@ public final class CameraViewController: BaseViewController { public override func setupUI() { super.setupUI() realEmojiFaceView.addSubview(realEmojiFaceImageView) - view.addSubviews(cameraView, shutterButton, flashButton, toggleButton, realEmojiFaceView, realEmojiHorizontalStakView, realEmojiCollectionView ,cameraIndicatorView) + view.addSubviews(cameraView, missionView ,shutterButton, flashButton, toggleButton, realEmojiFaceView, realEmojiHorizontalStakView, realEmojiCollectionView ,cameraIndicatorView) } public override func setupAttributes() { @@ -122,7 +123,7 @@ public final class CameraViewController: BaseViewController { $0.contentMode = .scaleAspectFill $0.image = DesignSystemAsset.filter.image } - + cameraView.do { $0.layer.cornerRadius = 40 $0.clipsToBounds = true @@ -146,6 +147,12 @@ public final class CameraViewController: BaseViewController { public override func setupAutoLayout() { super.setupAutoLayout() + missionView.snp.makeConstraints { + $0.top.equalTo(navigationBarView.snp.bottom).offset(26) + $0.left.right.equalToSuperview() + $0.height.equalTo(46) + } + realEmojiHorizontalStakView.snp.makeConstraints { $0.height.equalTo(34) $0.centerX.equalTo(cameraView) @@ -162,7 +169,7 @@ public final class CameraViewController: BaseViewController { $0.width.height.equalTo(26) $0.center.equalToSuperview() } - + cameraView.snp.makeConstraints { $0.width.equalToSuperview() @@ -206,7 +213,7 @@ public final class CameraViewController: BaseViewController { .map { Reactor.Action.viewDidLoad} .bind(to: reactor.action) .disposed(by: disposeBag) - + reactor.pulse(\.$isLoading) .distinctUntilChanged() @@ -232,9 +239,7 @@ public final class CameraViewController: BaseViewController { }.disposed(by: disposeBag) - reactor.pulse(\.$feedImageData) - .distinctUntilChanged() .compactMap { $0 } .withUnretained(self) .bind { @@ -242,20 +247,42 @@ public final class CameraViewController: BaseViewController { $0.0.navigationController?.pushViewController(cameraDisplayViewController, animated: true) }.disposed(by: disposeBag) + + Observable + .zip( + reactor.state.compactMap { $0.feedImageData }.distinctUntilChanged(), + reactor.state.compactMap { $0.missionEnttiy?.missionContent}.distinctUntilChanged(), + reactor.state.map { $0.cameraType.asPostType } + ) + .withUnretained(self) + .bind { + let cameraDisplayViewController = CameraDisplayDIContainer(displayData: $0.1.0, missionTitle: $0.1.1, cameraDisplayType: $0.1.2).makeViewController() + $0.0.navigationController?.pushViewController(cameraDisplayViewController, animated: true) + }.disposed(by: disposeBag) + reactor.state - .map { $0.cameraType == .realEmoji ? false : true } + .map { $0.cameraType.isRealEmojiType } .distinctUntilChanged() .withUnretained(self) - .bind(onNext: {$0.0.setupRealEmojiLayoutContent(isShow: $0.1)}) + .bind(onNext: {$0.0.setupRealEmojiLayoutContent(isShow: !$0.1)}) .disposed(by: disposeBag) + reactor.pulse(\.$missionEnttiy) + .map { $0?.missionContent } + .bind(to: missionView.missionTitleView.rx.text) + .disposed(by: disposeBag) - reactor.state - .map { $0.cameraType } - .map { $0 == .realEmoji ? "셀피 이미지" : "카메라" } - .distinctUntilChanged() - .withUnretained(self) - .bind(onNext: { $0.0.navigationBarView.setNavigationTitle(title: $0.1) }) + //TODO: Navigation Bar 제약 조건 이슈 + reactor.pulse(\.$cameraType) + .map { $0.setTitle() } + .observe(on: MainScheduler.instance) + .bind(with: self, onNext: { owner, title in + owner.navigationBarView.setNavigationTitle(title: title) + }).disposed(by: disposeBag) + + reactor.pulse(\.$cameraType) + .map { $0 == .mission ? false : true } + .bind(to: missionView.rx.isHidden) .disposed(by: disposeBag) reactor.pulse(\.$isError) @@ -317,6 +344,7 @@ public final class CameraViewController: BaseViewController { .bind(onNext: { $0.0.zoomView.setBackgroundImage($0.1, for: .normal) }) .disposed(by: disposeBag) + reactor.state .map { ($0.accountImage, $0.profileImageURLEntity, $0.memberId)} .filter { $0.0 != nil } @@ -326,7 +354,7 @@ public final class CameraViewController: BaseViewController { NotificationCenter.default.post(name: .AccountViewPresignURLDismissNotification, object: nil, userInfo: userInfo) owner.dismissCameraViewController() }).disposed(by: disposeBag) - + realEmojiCollectionView .rx.itemSelected @@ -358,14 +386,14 @@ public final class CameraViewController: BaseViewController { .map { Reactor.Action.didTapFlashButton } .bind(to: reactor.action) .disposed(by: disposeBag) - + cameraView.rx .pinchGesture .map { $0.scale } .map { Reactor.Action.dragPreviewLayer($0)} .bind(to: reactor.action) .disposed(by: disposeBag) - + toggleButton .rx.tap @@ -396,7 +424,7 @@ extension CameraViewController { private func setupCamera() { captureSession = AVCaptureSession() captureSession.beginConfiguration() - + if captureSession.canSetSessionPreset(.photo) { captureSession.sessionPreset = .photo } @@ -472,7 +500,7 @@ extension CameraViewController { private func setupCameraPermission() { switch AVCaptureDevice.authorizationStatus(for: .video) { - + case .authorized: setupCamera() setupCameraOuputStream() diff --git a/14th-team5-iOS/App/Sources/Presentation/Camera/Dependency/CameraDisplayDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/Camera/Dependency/CameraDisplayDIContainer.swift index 64608eeda..6ac21df60 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Camera/Dependency/CameraDisplayDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Camera/Dependency/CameraDisplayDIContainer.swift @@ -19,9 +19,13 @@ public final class CameraDisplayDIContainer: BaseDIContainer { public typealias UseCase = CameraDisplayViewUseCaseProtocol fileprivate var displayData: Data - - public init(displayData: Data) { + fileprivate var missionTitle: String + fileprivate var cameraDisplayType: PostType + + public init(displayData: Data, missionTitle: String = "", cameraDisplayType: PostType = .survival) { self.displayData = displayData + self.missionTitle = missionTitle + self.cameraDisplayType = cameraDisplayType } public func makeViewController() -> ViewContrller { @@ -37,7 +41,7 @@ public final class CameraDisplayDIContainer: BaseDIContainer { } public func makeReactor() -> Reactor { - return CameraDisplayViewReactor(cameraDisplayUseCase: makeUseCase(), displayData: displayData) + return CameraDisplayViewReactor(cameraDisplayUseCase: makeUseCase(), displayData: displayData, missionTitle: missionTitle, cameraType: cameraDisplayType) } } diff --git a/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraDisplayViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraDisplayViewReactor.swift index 9e00cf94f..fde374d82 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraDisplayViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraDisplayViewReactor.swift @@ -40,8 +40,10 @@ public final class CameraDisplayViewReactor: Reactor { public struct State { var isLoading: Bool var displayDescrption: String + var cameraType: PostType @Pulse var isError: Bool @Pulse var displayData: Data + @Pulse var missionTitle: String @Pulse var displaySection: [DisplayEditSectionModel] @Pulse var displayEntity: CameraDisplayImageResponse? @Pulse var displayOringalEntity: Bool @@ -50,13 +52,20 @@ public final class CameraDisplayViewReactor: Reactor { - init(cameraDisplayUseCase: CameraDisplayViewUseCaseProtocol, displayData: Data) { + init( + cameraDisplayUseCase: CameraDisplayViewUseCaseProtocol, + displayData: Data, + missionTitle: String, + cameraType: PostType = .survival + ) { self.cameraDisplayUseCase = cameraDisplayUseCase self.initialState = State( isLoading: true, displayDescrption: "", + cameraType: cameraType, isError: false, displayData: displayData, + missionTitle: missionTitle, displaySection: [.displayKeyword([])], displayEntity: nil, displayOringalEntity: false, @@ -67,13 +76,13 @@ public final class CameraDisplayViewReactor: Reactor { public func mutate(action: Action) -> Observable { switch action { case .viewDidLoad: - let fileName = "\(self.currentState.displayData.hashValue).jpg" + let fileName = "\(currentState.displayData.hashValue).jpg" let parameters: CameraDisplayImageParameters = CameraDisplayImageParameters(imageName: "\(fileName)") return .concat( .just(.setLoading(false)), .just(.setError(false)), - .just(.setRenderImage(self.currentState.displayData)), + .just(.setRenderImage(currentState.displayData)), cameraDisplayUseCase.executeDisplayImageURL(parameters: parameters) .withUnretained(self) .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) @@ -124,7 +133,7 @@ public final class CameraDisplayViewReactor: Reactor { case .didTapArchiveButton: return .concat( .just(.setLoading(false)), - .just(.saveDeviceimage(self.currentState.displayData)), + .just(.saveDeviceimage(currentState.displayData)), .just(.setLoading(true)) ) @@ -132,25 +141,30 @@ public final class CameraDisplayViewReactor: Reactor { MPEvent.Camera.uploadPhoto.track(with: nil) - guard let presingedURL = self.currentState.displayEntity?.imageURL else { return .just(.setError(true)) } + guard let presingedURL = currentState.displayEntity?.imageURL else { return .just(.setError(true)) } let originURL = configureOriginalS3URL(url: presingedURL) + let cameraQuery = CameraMissionFeedQuery(type: currentState.cameraType.rawValue, isUploded: true) let parameters: CameraDisplayPostParameters = CameraDisplayPostParameters( imageUrl: originURL, - content: self.currentState.displayDescrption, + content: currentState.displayDescrption, uploadTime: DateFormatter.yyyyMMddTHHmmssXXX.string(from: Date()) ) - return cameraDisplayUseCase.executeCombineWithTextImage(parameters: parameters) + return cameraDisplayUseCase.executeCombineWithTextImage(parameters: parameters, query: cameraQuery) .asObservable() + .catchAndReturn(nil) .flatMap { entity -> Observable in - return .concat( - .just(.setLoading(false)), - .just(.setPostEntity(entity)), - .just(.setLoading(true)), - .just(.setError(false)) - ) - + if entity == nil { + return .just(.setError(true)) + } else { + return .concat( + .just(.setLoading(false)), + .just(.setPostEntity(entity)), + .just(.setLoading(true)), + .just(.setError(false)) + ) + } } case .hideDisplayEditCell: return .concat( @@ -195,7 +209,7 @@ extension CameraDisplayViewReactor { func getSection(_ section: DisplayEditSectionModel) -> Int { var index: Int = 0 - for i in 0 ..< self.currentState.displaySection.count where self.currentState.displaySection[i].getSectionType() == section.getSectionType() { + for i in 0 ..< currentState.displaySection.count where currentState.displaySection[i].getSectionType() == section.getSectionType() { index = i } diff --git a/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraViewReactor.swift index b4e984262..b25aee7d0 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraViewReactor.swift @@ -46,6 +46,7 @@ public final class CameraViewReactor: Reactor { case setRealEmojiImageCreateResponse(CameraCreateRealEmojiResponse?) case setRealEmojiItems([CameraRealEmojiImageItemResponse?]) case setRealEmojiSection([EmojiSectionItem]) + case setMissionResponse(CameraTodayMissionResponse?) case setErrorAlert(Bool) case setRealEmojiType(Emojis) case setFeedImageData(Data) @@ -60,13 +61,14 @@ public final class CameraViewReactor: Reactor { @Pulse var realEmojiURLEntity: CameraRealEmojiPreSignedResponse? @Pulse var realEmojiCreateEntity: CameraCreateRealEmojiResponse? @Pulse var realEmojiEntity: [CameraRealEmojiImageItemResponse?] + @Pulse var missionEnttiy: CameraTodayMissionResponse? @Pulse var realEmojiSection: [EmojiSectionModel] @Pulse var zoomScale: CGFloat @Pulse var pinchZoomScale: CGFloat @Pulse var feedImageData: Data? var updateEmojiImage: URL? var emojiType: Emojis = .emoji(forIndex: 1) - var cameraType: UploadLocation = .feed + @Pulse var cameraType: UploadLocation = .survival var accountImage: Data? var memberId: String var isUpload: Bool @@ -92,6 +94,7 @@ public final class CameraViewReactor: Reactor { realEmojiURLEntity: nil, realEmojiCreateEntity: nil, realEmojiEntity: [], + missionEnttiy: nil, realEmojiSection: [.realEmoji([])], zoomScale: 1.0, pinchZoomScale: 1.0, @@ -179,6 +182,8 @@ public final class CameraViewReactor: Reactor { newState.pinchZoomScale = pinchZoomScale case let .setFeedImageData(feedImage): newState.feedImageData = feedImage + case let .setMissionResponse(missionEntity): + newState.missionEnttiy = missionEntity } return newState @@ -204,7 +209,8 @@ extension CameraViewReactor { } private func viewDidLoadMutation() -> Observable { - if cameraType == .realEmoji { + switch cameraType { + case .realEmoji: return .concat( cameraUseCase.executeRealEmojiItems(memberId: memberId) .withUnretained(self) @@ -224,7 +230,7 @@ extension CameraViewReactor { realEmojiType: $0.element?.realEmojiType ?? "" ) ) - + ) } return .concat( @@ -237,15 +243,28 @@ extension CameraViewReactor { } ) - } else { + case .mission: + return cameraUseCase.executeTodayMission() + .withUnretained(self) + .flatMap { owner, entity -> Observable in + + return .concat( + .just(.setLoading(false)), + .just(.setMissionResponse(entity)), + .just(.setLoading(true)) + ) + } + + + default: return .empty() } } private func didTapShutterButtonMutation(imageData: Data) -> Observable { - + switch cameraType { - case .feed: + case .survival, .mission: return .concat( .just(.setLoading(false)), .just(.setFeedImageData(imageData)), @@ -318,7 +337,7 @@ extension CameraViewReactor { case .realEmoji: let realEmojiImage = "\(imageData.hashValue).jpg" let realEmojiParameter = CameraRealEmojiParameters(imageName: realEmojiImage) - + if currentState.realEmojiEntity[currentState.emojiType.rawValue - 1] == nil { return .concat( .just(.setLoading(false)), @@ -363,7 +382,7 @@ extension CameraViewReactor { } } - + ) } else { let realEmojiImage = "\(imageData.hashValue).jpg" @@ -400,12 +419,12 @@ extension CameraViewReactor { } } - + ) } } - + } 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 e3fc6adbe..bdfe9b42e 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift @@ -222,7 +222,7 @@ extension MainViewController { .withUnretained(self) .bind(onNext: { MPEvent.Home.cameraTapped.track(with: nil) - let cameraViewController = CameraDIContainer(cameraType: .feed).makeViewController() + let cameraViewController = CameraDIContainer(cameraType: .survival).makeViewController() $0.0.navigationController?.pushViewController(cameraViewController, animated: true) }) .disposed(by: disposeBag) diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/PostViewController.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/PostViewController.swift index 5ab02efda..73c6f07a6 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/PostViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/PostViewController.swift @@ -54,6 +54,7 @@ final class PostViewController: BaseViewController { return type } .withUnretained(self) + .observe(on: MainScheduler.asyncInstance) .bind { owner, entity in let cameraViewController = CameraDIContainer(cameraType: .realEmoji, realEmojiType: entity).makeViewController() owner.navigationController?.pushViewController(cameraViewController, animated: true) diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileDetailViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileDetailViewController.swift index 930a1cef2..abfe0e2dc 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileDetailViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileDetailViewController.swift @@ -24,7 +24,6 @@ final class ProfileDetailViewController: BaseViewController .rx.tap .throttle(.milliseconds(300), scheduler: MainScheduler.instance) .withUnretained(self) - .bind(onNext: {$0.0.createAlertController(owner: $0.0)}) + .bind(onNext: {$0.0.createAlertController()}) .disposed(by: disposeBag) @@ -400,14 +400,14 @@ extension ProfileViewController { self.navigationController?.pushViewController(accountNickNameViewController, animated: false) } - private func createAlertController(owner: ProfileViewController) { + private func createAlertController() { let alertController: UIAlertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) let presentCameraAction: UIAlertAction = UIAlertAction(title: "카메라", style: .default) { _ in guard let profileMemberId = self.reactor?.currentState.profileMemberEntity?.memberId else { return } let cameraViewController = CameraDIContainer(cameraType: .profile, memberId: profileMemberId).makeViewController() - owner.navigationController?.pushViewController(cameraViewController, animated: true) + self.navigationController?.pushViewController(cameraViewController, animated: true) } let presentAlbumAction: UIAlertAction = UIAlertAction(title: "앨범", style: .default) { _ in @@ -428,7 +428,7 @@ extension ProfileViewController { alertController.addAction($0) } alertController.overrideUserInterfaceStyle = .dark - owner.present(alertController, animated: true) + self.present(alertController, animated: true) } } diff --git a/14th-team5-iOS/Core/Sources/ShareView/BibbiMissionView.swift b/14th-team5-iOS/Core/Sources/ShareView/BibbiMissionView.swift new file mode 100644 index 000000000..a033e4218 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/ShareView/BibbiMissionView.swift @@ -0,0 +1,65 @@ +// +// BibbiMissionView.swift +// Core +// +// Created by Kim dohyun on 5/1/24. +// + +import UIKit + +import DesignSystem +import SnapKit + + +public final class BibbiMissionView: UIView { + + public let missionBadgeView: UIImageView = UIImageView() + public let missionTitleView: BibbiLabel = BibbiLabel(.body2Bold, textAlignment: .center, textColor: .mainYellow) + + + + + override init(frame: CGRect) { + super.init(frame: .zero) + setupUI() + setupAttributes() + setupAutoLayout() + } + + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + private func setupUI() { + addSubviews(missionBadgeView, missionTitleView) + } + + private func setupAttributes() { + missionBadgeView.do { + $0.image = DesignSystemAsset.missionTitleBadge.image + } + missionTitleView.do { + $0.text = "tesetstest" + } + } + + private func setupAutoLayout() { + missionBadgeView.snp.makeConstraints { + $0.top.centerX.equalToSuperview() + $0.width.equalTo(40) + $0.height.equalTo(18) + } + + missionTitleView.snp.makeConstraints { + $0.top.equalTo(missionBadgeView.snp.bottom).offset(8) + $0.height.equalTo(20) + $0.centerX.equalTo(missionBadgeView) + } + + } + + +} + diff --git a/14th-team5-iOS/Data/Sources/Camera/CameraAPI/CameraAPIWorker.swift b/14th-team5-iOS/Data/Sources/Camera/CameraAPI/CameraAPIWorker.swift index f3f335ce6..d8e4b3e4d 100644 --- a/14th-team5-iOS/Data/Sources/Camera/CameraAPI/CameraAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Camera/CameraAPI/CameraAPIWorker.swift @@ -32,7 +32,6 @@ extension CameraAPIs { extension CameraAPIWorker { - //TODO: 나중에 Prameters memberId 추가하고 type에 따라 realEmoji, feed, profile, PresignedURL 호출 API 분기 작성하면 하나로 통합 할 수 있을듯 public func createProfilePresignedURL(accessToken: String, parameters: Encodable) -> Single { let spec = CameraAPIs.uploadProfileImageURL.spec @@ -90,8 +89,8 @@ extension CameraAPIWorker { .map { $0 } } - public func combineWithTextImageUpload(accessToken: String, parameters: Encodable) -> Single { - let spec = CameraAPIs.updateImage.spec + public func combineWithTextImageUpload(accessToken: String, parameters: Encodable, query: CameraMissionFeedQuery) -> Single { + let spec = CameraAPIs.updateImage(query).spec return request(spec: spec, headers: [BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.xAuthToken(accessToken)], jsonEncodable: parameters) .subscribe(on: Self.queue) @@ -173,5 +172,19 @@ extension CameraAPIWorker { .catchAndReturn(nil) .asSingle() } + + public func fetchMissionItems(accessToken: String) -> Single { + let spec = CameraAPIs.fetchMissionToday.spec + return request(spec: spec, headers: [BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.xAuthToken(accessToken)]) + .subscribe(on: Self.queue) + .do { + if let str = String(data: $0.1, encoding: .utf8) { + debugPrint("Fetch Today Mission Items : \(str)") + } + } + .map(CameraTodayMissionDTO.self) + .catchAndReturn(nil) + .asSingle() + } } diff --git a/14th-team5-iOS/Data/Sources/Camera/CameraAPI/CameraAPIs.swift b/14th-team5-iOS/Data/Sources/Camera/CameraAPI/CameraAPIs.swift index 56d4f75d8..2f1336f70 100644 --- a/14th-team5-iOS/Data/Sources/Camera/CameraAPI/CameraAPIs.swift +++ b/14th-team5-iOS/Data/Sources/Camera/CameraAPI/CameraAPIs.swift @@ -12,13 +12,14 @@ import Domain public enum CameraAPIs: API { case uploadImageURL case presignedURL(String) - case updateImage + case updateImage(CameraMissionFeedQuery) case uploadProfileImageURL case editProfileImage(String) case uploadRealEmojiURL(String) case updateRealEmojiImage(String) case reloadRealEmoji(String) case modifyRealEmojiImage(String, String) + case fetchMissionToday var spec: APISpec { switch self { @@ -30,8 +31,8 @@ public enum CameraAPIs: API { return APISpec(method: .post, url: "\(BibbiAPI.hostApi)/posts/image-upload-request") case let .presignedURL(url): return APISpec(method: .put, url: url) - case .updateImage: - return APISpec(method: .post, url: "\(BibbiAPI.hostApi)/posts") + case let .updateImage(query): + return APISpec(method: .post, url: "\(BibbiAPI.hostApi)/posts?type=\(query.type)&available=\(query.isUploded)") case let .uploadRealEmojiURL(memberId): return APISpec(method: .post, url: "\(BibbiAPI.hostApi)/members/\(memberId)/real-emoji/image-upload-request") case let .updateRealEmojiImage(memberId): @@ -40,6 +41,8 @@ public enum CameraAPIs: API { return APISpec(method: .get, url: "\(BibbiAPI.hostApi)/members/\(memberId)/real-emoji") case let .modifyRealEmojiImage(memberId, realEmojiId): return APISpec(method: .put, url: "\(BibbiAPI.hostApi)/members/\(memberId)/real-emoji/\(realEmojiId)") + case .fetchMissionToday: + return APISpec(method: .get, url: "\(BibbiAPI.hostApi)/missions/today") } } diff --git a/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraDisplayPostDTO.swift b/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraDisplayPostDTO.swift index 63a98149b..305e03ba3 100644 --- a/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraDisplayPostDTO.swift +++ b/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraDisplayPostDTO.swift @@ -13,6 +13,8 @@ public struct CameraDisplayPostDTO: Decodable { public var postId: String? public var authorId: String? + public var type: String? + public var missionId: String? public var commentCount: Int? public var emojiCount: Int? public var imageUrl: String? @@ -30,6 +32,8 @@ extension CameraDisplayPostDTO { postId: postId ?? "", authorId: authorId ?? "", commentCount: commentCount ?? 0, + missionType: type ?? "SURVIVAL", + missionId: missionId ?? "", emojiCount: emojiCount ?? 0, imageURL: imageUrl ?? "", content: content ?? "", diff --git a/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraTodayMissionDTO.swift b/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraTodayMissionDTO.swift new file mode 100644 index 000000000..464c27fb8 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraTodayMissionDTO.swift @@ -0,0 +1,36 @@ +// +// CameraTodayMissionDTO.swift +// Data +// +// Created by Kim dohyun on 4/30/24. +// + +import Foundation +import Domain + + +public struct CameraTodayMissionDTO: Decodable { + + public var missionDate: String + public var missionId: String + public var missionContent: String + + public enum CodingKeys: String, CodingKey { + case missionDate = "date" + case missionId = "id" + case missionContent = "content" + + } +} + + +extension CameraTodayMissionDTO { + func toDomain() -> CameraTodayMissionResponse { + return .init( + missionDate: missionDate.toDate(with: "yyyy-MM-dd"), + missionId: missionId, + missionContent: missionContent + ) + } + +} diff --git a/14th-team5-iOS/Data/Sources/Camera/Repositories/CameraDisplayViewRepository.swift b/14th-team5-iOS/Data/Sources/Camera/Repositories/CameraDisplayViewRepository.swift index df50c6ef2..c7b44bec5 100644 --- a/14th-team5-iOS/Data/Sources/Camera/Repositories/CameraDisplayViewRepository.swift +++ b/14th-team5-iOS/Data/Sources/Camera/Repositories/CameraDisplayViewRepository.swift @@ -51,9 +51,10 @@ extension CameraDisplayViewRepository: CameraDisplayViewInterface { } - public func combineWithTextImage(parameters: CameraDisplayPostParameters) -> Observable { - return cameraDisplayAPIWorker.combineWithTextImageUpload(accessToken: accessToken, parameters: parameters) - .compactMap { $0?.toDomain() } + public func combineWithTextImage(parameters: CameraDisplayPostParameters, query:CameraMissionFeedQuery) -> Observable { + return cameraDisplayAPIWorker.combineWithTextImageUpload(accessToken: accessToken, parameters: parameters, query: query) + .map { $0?.toDomain() } + .catchAndReturn(nil) .asObservable() } diff --git a/14th-team5-iOS/Data/Sources/Camera/Repositories/CameraViewRepository.swift b/14th-team5-iOS/Data/Sources/Camera/Repositories/CameraViewRepository.swift index e49fd7c65..2eb3d112b 100644 --- a/14th-team5-iOS/Data/Sources/Camera/Repositories/CameraViewRepository.swift +++ b/14th-team5-iOS/Data/Sources/Camera/Repositories/CameraViewRepository.swift @@ -93,4 +93,10 @@ extension CameraViewRepository: CameraViewInterface { .compactMap { $0?.toDomain() } .asObservable() } + + public func fetchTodayMissionItem() -> Observable { + return cameraAPIWorker.fetchMissionItems(accessToken: accessToken) + .compactMap { $0?.toDomain() } + .asObservable() + } } diff --git a/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/missionTitleBadge.imageset/Contents.json b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/missionTitleBadge.imageset/Contents.json new file mode 100644 index 000000000..4cb8d3213 --- /dev/null +++ b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/missionTitleBadge.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "missionTitleBadge.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/missionTitleBadge.imageset/missionTitleBadge.svg b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/missionTitleBadge.imageset/missionTitleBadge.svg new file mode 100644 index 000000000..67d4d7c94 --- /dev/null +++ b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/missionTitleBadge.imageset/missionTitleBadge.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/14th-team5-iOS/Domain/Sources/Camera/Entity/CameraDisplayPostResponse.swift b/14th-team5-iOS/Domain/Sources/Camera/Entity/CameraDisplayPostResponse.swift index b97074bee..e4e1b6fcc 100644 --- a/14th-team5-iOS/Domain/Sources/Camera/Entity/CameraDisplayPostResponse.swift +++ b/14th-team5-iOS/Domain/Sources/Camera/Entity/CameraDisplayPostResponse.swift @@ -12,6 +12,8 @@ public struct CameraDisplayPostResponse { public var postId: String public var authorId: String public var commentCount: Int + public var missionType: String + public var missionId: String public var emojiCount: Int public var imageURL: String public var content: String @@ -21,6 +23,8 @@ public struct CameraDisplayPostResponse { postId: String, authorId: String, commentCount: Int, + missionType: String, + missionId: String, emojiCount: Int, imageURL: String, content: String, @@ -29,6 +33,8 @@ public struct CameraDisplayPostResponse { self.postId = postId self.authorId = authorId self.commentCount = commentCount + self.missionType = missionType + self.missionId = missionId self.emojiCount = emojiCount self.imageURL = imageURL self.content = content diff --git a/14th-team5-iOS/Domain/Sources/Camera/Entity/CameraMissionFeedQuery.swift b/14th-team5-iOS/Domain/Sources/Camera/Entity/CameraMissionFeedQuery.swift new file mode 100644 index 000000000..f3328c49d --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Camera/Entity/CameraMissionFeedQuery.swift @@ -0,0 +1,20 @@ +// +// CameraMissionFeedQuery.swift +// Domain +// +// Created by Kim dohyun on 5/2/24. +// + +import Foundation + + +public struct CameraMissionFeedQuery { + public var type: String + public var isUploded: Bool + + + public init(type: String, isUploded: Bool) { + self.type = type + self.isUploded = isUploded + } +} diff --git a/14th-team5-iOS/Domain/Sources/Camera/Entity/CameraTodayMissionResponse.swift b/14th-team5-iOS/Domain/Sources/Camera/Entity/CameraTodayMissionResponse.swift new file mode 100644 index 000000000..8df01d15b --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Camera/Entity/CameraTodayMissionResponse.swift @@ -0,0 +1,22 @@ +// +// CameraTodayMissionResponse.swift +// Domain +// +// Created by Kim dohyun on 4/30/24. +// + +import Foundation + + +public struct CameraTodayMissionResponse { + public var missionDate: Date + public var missionId: String + public var missionContent: String + + public init(missionDate: Date, missionId: String, missionContent: String) { + self.missionDate = missionDate + self.missionId = missionId + self.missionContent = missionContent + } + +} diff --git a/14th-team5-iOS/Domain/Sources/Camera/Interfaces/CameraDisplayViewInterface.swift b/14th-team5-iOS/Domain/Sources/Camera/Interfaces/CameraDisplayViewInterface.swift index f4e4f0802..f3178222a 100644 --- a/14th-team5-iOS/Domain/Sources/Camera/Interfaces/CameraDisplayViewInterface.swift +++ b/14th-team5-iOS/Domain/Sources/Camera/Interfaces/CameraDisplayViewInterface.swift @@ -11,14 +11,24 @@ import RxCocoa import RxSwift public enum UploadLocation { - case feed + case survival + case mission case profile case realEmoji + + public var isRealEmojiType: Bool { + switch self { + case .realEmoji: + return true + default: + return false + } + } public var location: String { switch self { - case .feed: + case .survival, .mission: return "images/feed/" case .profile: return "images/profile/" @@ -26,6 +36,30 @@ public enum UploadLocation { return "images/real-emoji/" } } + + public var asPostType: PostType { + switch self { + case .survival: + return .survival + case .mission: + return .mission + default: + return .survival + } + } + + public func setTitle() -> String { + switch self { + case .survival: + return "생존 카메라" + case .mission: + return "미션 카메라" + case .profile: + return "카메라" + case .realEmoji: + return "셀피 이미지" + } + } } @@ -34,6 +68,6 @@ public protocol CameraDisplayViewInterface: AnyObject { func generateDescrption(with keyword: String) -> Observable> func fetchFeedImageURL(parameters: CameraDisplayImageParameters) -> Observable func uploadImageToS3(toURL url: String, imageData: Data) -> Observable - func combineWithTextImage(parameters: CameraDisplayPostParameters) -> Observable + func combineWithTextImage(parameters: CameraDisplayPostParameters, query: CameraMissionFeedQuery) -> Observable } diff --git a/14th-team5-iOS/Domain/Sources/Camera/Interfaces/CameraViewInterface.swift b/14th-team5-iOS/Domain/Sources/Camera/Interfaces/CameraViewInterface.swift index b195bfbd0..8a424ac8f 100644 --- a/14th-team5-iOS/Domain/Sources/Camera/Interfaces/CameraViewInterface.swift +++ b/14th-team5-iOS/Domain/Sources/Camera/Interfaces/CameraViewInterface.swift @@ -24,4 +24,5 @@ public protocol CameraViewInterface: AnyObject { func uploadRealEmojiImageToS3(memberId: String, parameters: CameraCreateRealEmojiParameters) -> Observable func fetchRealEmojiItems(memberId: String) -> Observable<[CameraRealEmojiImageItemResponse?]> func updateRealEmojiImage(memberId: String, realEmojiId: String, parameters: CameraUpdateRealEmojiParameters) -> Observable + func fetchTodayMissionItem() -> Observable } diff --git a/14th-team5-iOS/Domain/Sources/Camera/UseCases/CameraDisplayViewUseCase.swift b/14th-team5-iOS/Domain/Sources/Camera/UseCases/CameraDisplayViewUseCase.swift index ff7ead071..99adb216c 100644 --- a/14th-team5-iOS/Domain/Sources/Camera/UseCases/CameraDisplayViewUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/Camera/UseCases/CameraDisplayViewUseCase.swift @@ -15,7 +15,7 @@ public protocol CameraDisplayViewUseCaseProtocol { func executeDescrptionItems(with keyword: String) -> Observable> func executeDisplayImageURL(parameters: CameraDisplayImageParameters) -> Observable func executeUploadToS3(toURL url: String, imageData: Data) -> Observable - func executeCombineWithTextImage(parameters: CameraDisplayPostParameters) -> Observable + func executeCombineWithTextImage(parameters: CameraDisplayPostParameters, query: CameraMissionFeedQuery) -> Observable } @@ -40,8 +40,8 @@ public final class CameraDisplayViewUseCase: CameraDisplayViewUseCaseProtocol { return cameraDisplayViewRepository.uploadImageToS3(toURL: url, imageData: imageData) } - public func executeCombineWithTextImage(parameters: CameraDisplayPostParameters) -> Observable { - return cameraDisplayViewRepository.combineWithTextImage(parameters: parameters) + public func executeCombineWithTextImage(parameters: CameraDisplayPostParameters, query: CameraMissionFeedQuery) -> Observable { + return cameraDisplayViewRepository.combineWithTextImage(parameters: parameters, query: query) } diff --git a/14th-team5-iOS/Domain/Sources/Camera/UseCases/CameraViewUseCase.swift b/14th-team5-iOS/Domain/Sources/Camera/UseCases/CameraViewUseCase.swift index 2ff4c74df..622507f2e 100644 --- a/14th-team5-iOS/Domain/Sources/Camera/UseCases/CameraViewUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/Camera/UseCases/CameraViewUseCase.swift @@ -21,6 +21,7 @@ public protocol CameraViewUseCaseProtocol { func executeRealEmojiUploadToS3(memberId: String, parameter: CameraCreateRealEmojiParameters) -> Observable func executeRealEmojiItems(memberId: String) -> Observable<[CameraRealEmojiImageItemResponse?]> func executeUpdateRealEmojiImage(memberId: String, realEmojiId: String ,parameter: CameraUpdateRealEmojiParameters) -> Observable + func executeTodayMission() -> Observable } @@ -69,5 +70,9 @@ public final class CameraViewUseCase: CameraViewUseCaseProtocol { public func executeUpdateRealEmojiImage(memberId: String, realEmojiId: String, parameter: CameraUpdateRealEmojiParameters) -> Observable { return cameraViewRepository.updateRealEmojiImage(memberId: memberId, realEmojiId: realEmojiId, parameters: parameter) } + + public func executeTodayMission() -> Observable { + return cameraViewRepository.fetchTodayMissionItem() + } } From 7395f6207e27cc3390de5c483c8426822fd289bf Mon Sep 17 00:00:00 2001 From: Do-hyun-Kim Date: Thu, 2 May 2024 19:55:15 +0900 Subject: [PATCH 004/263] =?UTF-8?q?fix:=20ci/cd=20Branch=20=EB=84=A4?= =?UTF-8?q?=EC=9D=B4=EB=B0=8D=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20STG,PRD?= =?UTF-8?q?=20=EB=B0=B0=ED=8F=AC=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/swift.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index e197266dc..f5065fab5 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -78,11 +78,11 @@ jobs: APPLE_ID: ${{secrets.APPLE_ID}} TEAM_ID: ${{secrets.TEAM_ID}} WIDGET_NAME: ${{secrets.WIDGET_NAME}} - run: fastlane github_action_prd_upload_testflight + run: fastlane github_action_stg_upload_testflight - name: fastlane upload_prd_testflight - if: github.event.pull_request.base.ref == 'develop' && github.head_ref == 'feature' + if: github.event.pull_request.base.ref == 'develop' && github.head_ref == 'feat' 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 }} @@ -99,7 +99,7 @@ jobs: APPLE_ID: ${{secrets.APPLE_ID}} TEAM_ID: ${{secrets.TEAM_ID}} WIDGET_NAME: ${{secrets.WIDGET_NAME}} - run: fastlane github_action_stg_upload_testflight + run: fastlane github_action_prd_upload_testflight - name: Upload coverage to Codecov uses: codecov/codecov-action@v1.2.1 From 43135b5bed5de9643c4259ec9731411c34a96879 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Thu, 2 May 2024 20:17:23 +0900 Subject: [PATCH 005/263] =?UTF-8?q?feat:=20BBAlert=20Style=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#403)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BibbiAlertBuilder/BibbiAlertStyle.swift | 11 ++++ .../BibbiAlertViewController.swift | 5 -- .../widgetGraphic.imageset/Contents.json | 12 ++++ .../widgetGraphic.imageset/WidgetGraphic.svg | 30 ++++++++++ .../xcschemes/Bibbi-Workspace.xcscheme | 56 ------------------- 5 files changed, 53 insertions(+), 61 deletions(-) create mode 100644 14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/Character/widgetGraphic.imageset/Contents.json create mode 100644 14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/Character/widgetGraphic.imageset/WidgetGraphic.svg diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BibbiAlertBuilder/BibbiAlertStyle.swift b/14th-team5-iOS/Core/Sources/Bibbi/BibbiAlertBuilder/BibbiAlertStyle.swift index 3d6ce4cb6..4fee47a49 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BibbiAlertBuilder/BibbiAlertStyle.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BibbiAlertBuilder/BibbiAlertStyle.swift @@ -12,6 +12,7 @@ public enum BibbiAlertStyle { case missionKey case takeSurvival case pickMember(String) + case widget var mainTitle: String { switch self { @@ -21,6 +22,8 @@ public enum BibbiAlertStyle { return "생존신고 사진을 먼저 찍으세요!" case .pickMember: return "생존 확인하기" + case .widget: + return "위젯 추가하셨나요?" } } @@ -32,6 +35,8 @@ public enum BibbiAlertStyle { return "미션 사진을 올리려면\n생존신고 사진을 먼저 업로드해야해요." case let .pickMember(name): return "\(name)님의 생존 여부를 물어볼까요?\n지금 알림이 전송됩니다." + case .widget: + return "홈 화면에서 위젯으로\n가족의 소식을 한눈에 파악할 수 있어요" } } @@ -43,6 +48,8 @@ public enum BibbiAlertStyle { return DesignSystemAsset.takeSurvivalGraphic.image case .pickMember: return DesignSystemAsset.exhaustedBibbiGraphic.image + case .widget: + return DesignSystemAsset.widgetGraphic.image } } @@ -54,6 +61,8 @@ public enum BibbiAlertStyle { return "생존신고 먼저 하기" case .pickMember: return "지금 하기" + case .widget: + return "확인하기" } } @@ -65,6 +74,8 @@ public enum BibbiAlertStyle { return "다음에 하기" case .pickMember: return "다음에 하기" + case .widget: + return "닫기" } } } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BibbiAlertBuilder/BibbiAlertViewController.swift b/14th-team5-iOS/Core/Sources/Bibbi/BibbiAlertBuilder/BibbiAlertViewController.swift index ac41db8e9..874f8fbbc 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BibbiAlertBuilder/BibbiAlertViewController.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BibbiAlertBuilder/BibbiAlertViewController.swift @@ -178,12 +178,7 @@ public final class BibbiAlertViewController: UIViewController { $0.distribution = .fillProportionally } -// mainTitleLabel.do { -// $0.textAlignment = .center -// } - subTitleLabel.do { -// $0.textAlignment = .center $0.numberOfLines = 0 } diff --git a/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/Character/widgetGraphic.imageset/Contents.json b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/Character/widgetGraphic.imageset/Contents.json new file mode 100644 index 000000000..85fe01265 --- /dev/null +++ b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/Character/widgetGraphic.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "WidgetGraphic.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/Character/widgetGraphic.imageset/WidgetGraphic.svg b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/Character/widgetGraphic.imageset/WidgetGraphic.svg new file mode 100644 index 000000000..b8035184e --- /dev/null +++ b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/Character/widgetGraphic.imageset/WidgetGraphic.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme b/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme index 9f49bc0db..72399b415 100644 --- a/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme +++ b/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme @@ -300,20 +300,6 @@ ReferencedContainer = "container:Tuist/Dependencies/SwiftPackageManager/.build/checkouts/GoogleDataTransport/GoogleDataTransport.xcodeproj"> - - - - - - - - - - - - - - - - Date: Thu, 2 May 2024 22:16:36 +0900 Subject: [PATCH 006/263] =?UTF-8?q?fix:=20=EC=98=A4=ED=83=80=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#493)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../App/Sources/Presentation/Home/Model/FamilySection.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Model/FamilySection.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Model/FamilySection.swift index 9c6e8dcea..251dc3f96 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Model/FamilySection.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Model/FamilySection.swift @@ -20,7 +20,9 @@ struct FamilySection { /// `RxDatasource` 테이블을 구성하는 데 필요한 `ModelType`은 UI 구성에 조금 더 가깝지 않나라는 개인적인 견해도 가지고 있습니다. /// /// 이와 관련된 코드는 `PostSectionModel`과 `CommentViewReactor`를 참조하시면 됩니다. - /// 추가 자료는 [여기](https://ios-development.tistory.com/796)에서 확인하실 수 있습니다. 감사합니다. + /// 추가 자료는 [여기](https://ios-development.tistory.com/796)에서 확인하실 수 있습니다. + /// + /// 회의 때 더 자세히 설명드릴게요. enum Item { case main(MainFamilyCellReactor) From 0f05a9d490490402c0024edafe2e69a33dabedd4 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Fri, 3 May 2024 11:17:33 +0900 Subject: [PATCH 007/263] =?UTF-8?q?feat:=20CalendarAPI=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20Deprecated=20=EC=B2=98=EB=A6=AC=20(#494?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MonthlyCalendarSectionModel.swift | 104 +----------------- .../ImageCalendarCellDIContainer.swift | 4 +- .../Reactor/CalendarPageViewCellReactor.swift | 8 +- .../Reactor/CalendarPostViewReactor.swift | 4 +- .../Reactor/ImageCalendarCellReactor.swift | 2 +- .../Calendar/View/Cell/CalendarPageCell.swift | 2 +- .../CalendarPostViewController.swift | 2 +- .../Data/Sources/API/APIWorker.swift | 9 ++ .../Account/AccountAPI/AccountAPIWorker.swift | 9 -- .../Sources/Account/MeAPI/MeAPIWorker.swift | 9 -- .../CalendarAPI/CalendarAPIWorker.swift | 82 +++++++++++--- .../Calendar/CalendarAPI/CalendarAPIs.swift | 21 +++- .../ArrayResponseCalendarResponseDTO.swift | 13 +-- ...rrayResponseDailyCalendarResponseDTO.swift | 73 ++++++++++++ ...ayResponseMonthlyCalendarResponseDTO.swift | 50 +++++++++ .../DataMapping/BannerResponseDTO.swift | 2 +- .../FamilyMonthlyStatisticsResponseDTO.swift | 2 +- .../Repository/CalendarRepository.swift | 20 +++- .../Emoji/EmojiAPI/EmojiAPIWorker.swift | 8 -- .../Family/FamilyAPI/FamilyAPIWorker.swift | 8 -- .../Mission/MissionAPI/MissionAPIWorker.swift | 8 -- .../Sources/Pick/PickAPI/PickAPIWorker.swift | 13 --- .../PostListAPI/PostListAPIWorker.swift | 8 -- .../PostCommentAPI/PostCommentAPIWorker.swift | 9 -- .../RealEmojiAPI/RealEmojiAPIWorker.swift | 8 -- .../View/Main/Worker/MainAPIWorker.swift | 9 -- .../ArrayResponseCalendarEntity.swift | 37 +++++++ .../ArrayResponseDailyCalendarEntity.swift | 57 ++++++++++ ... ArrayResponseMonthlyCalendarEntity.swift} | 9 +- ...annerResponse.swift => BannerEntity.swift} | 2 +- ...ft => FamilyMonthlyStatisticsEntity.swift} | 2 +- .../Repositories/CalendarRepository.swift | 9 +- .../Calendar/UseCases/CalendarUseCase.swift | 12 +- 33 files changed, 361 insertions(+), 254 deletions(-) create mode 100644 14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/DataMapping/ArrayResponseDailyCalendarResponseDTO.swift create mode 100644 14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/DataMapping/ArrayResponseMonthlyCalendarResponseDTO.swift create mode 100644 14th-team5-iOS/Domain/Sources/Calendar/Entities/ArrayResponseCalendarEntity.swift create mode 100644 14th-team5-iOS/Domain/Sources/Calendar/Entities/ArrayResponseDailyCalendarEntity.swift rename 14th-team5-iOS/Domain/Sources/Calendar/Entities/{ArrayResponseCalendarResponse.swift => ArrayResponseMonthlyCalendarEntity.swift} (78%) rename 14th-team5-iOS/Domain/Sources/Calendar/Entities/{BannerResponse.swift => BannerEntity.swift} (96%) rename 14th-team5-iOS/Domain/Sources/Calendar/Entities/{FamilyMonthlyStatisticsResponse.swift => FamilyMonthlyStatisticsEntity.swift} (83%) diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/DataSource/MonthlyCalendarSectionModel.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/DataSource/MonthlyCalendarSectionModel.swift index edaba862d..5a91ada45 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/DataSource/MonthlyCalendarSectionModel.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/DataSource/MonthlyCalendarSectionModel.swift @@ -5,13 +5,11 @@ // Created by 김건우 on 12/22/23. // +import Domain import Foundation -import Domain import RxDataSources -typealias CalendarTestData = SectionOfMonthlyCalendar.TestData - public struct SectionOfMonthlyCalendar { public var items: [Item] } @@ -24,103 +22,3 @@ extension SectionOfMonthlyCalendar: SectionModelType { self.items = items } } - -extension SectionOfMonthlyCalendar { - enum TestData { - static let calendar = Calendar.current - static let components1: DateComponents = DateComponents(year: 2023, month: 12, day: 1) - static let components2: DateComponents = DateComponents(year: 2023, month: 12, day: 3) - static let components3: DateComponents = DateComponents(year: 2023, month: 12, day: 5) - - static let components4: DateComponents = DateComponents(year: 2024, month: 1, day: 7) - static let components5: DateComponents = DateComponents(year: 2024, month: 1, day: 9) - static let components6: DateComponents = DateComponents(year: 2024, month: 1, day: 11) - - static let date1: Date = calendar.date(from: components1)! - static let date2: Date = calendar.date(from: components2)! - static let date3: Date = calendar.date(from: components3)! - - static let date4: Date = calendar.date(from: components4)! - static let date5: Date = calendar.date(from: components5)! - static let date6: Date = calendar.date(from: components6)! - - static let months: [Date] = [ - "2023-12".toDate(with: "yyyy-MM"), "2024-01".toDate(with: "yyyy-MM") - ] - static let dates: [[Date]] = [ - [date1, date2, date3], - [date4, date5, date6] - ] - static let representativePostIds: [[String]] = [ - ["1", "2", "3"], - ["4", "5", "6"], - ] - static let representativeThumbnailUrls: [[String]] = [ - [ - "https://cdn.pixabay.com/photo/2023/11/20/13/48/butterfly-8401173_1280.jpg", - "https://cdn.pixabay.com/photo/2023/11/10/02/30/woman-8378634_1280.jpg", - "https://cdn.pixabay.com/photo/2023/11/26/08/27/leaves-8413064_1280.jpg" - ], - [ - "https://cdn.pixabay.com/photo/2023/11/20/13/48/butterfly-8401173_1280.jpg", - "https://cdn.pixabay.com/photo/2023/11/10/02/30/woman-8378634_1280.jpg", - "https://cdn.pixabay.com/photo/2023/11/26/08/27/leaves-8413064_1280.jpg" - ], - ] - static let allFamilyMemebersUploadeds: [[Bool]] = [ - [false, true, false], - [true, false, true] - ] - - static let dayInfo202312: [CalendarResponse] = (0...2).map { - return CalendarResponse( - date: dates[0][$0], - representativePostId: representativePostIds[0][$0], - representativeThumbnailUrl: representativeThumbnailUrls[0][$0], - allFamilyMemebersUploaded: allFamilyMemebersUploadeds[0][$0] - ) - } - - static let dayInfo202401: [CalendarResponse] = (0...2).map { - return CalendarResponse( - date: dates[1][$0], - representativePostId: representativePostIds[1][$0], - representativeThumbnailUrl: representativeThumbnailUrls[1][$0], - allFamilyMemebersUploaded: allFamilyMemebersUploadeds[1][$0] - ) - } - } - - static func generateTestData() -> ArrayResponseCalendarResponse { - var arrayCalendarResponse: ArrayResponseCalendarResponse = .init(results: []) - - (0...1).forEach { outerIdx in - (0...2).forEach { innerIdx in - let dayResponse = CalendarResponse( - date: CalendarTestData.dates[outerIdx][innerIdx], - representativePostId: CalendarTestData.representativePostIds[outerIdx][innerIdx], - representativeThumbnailUrl: CalendarTestData.representativeThumbnailUrls[outerIdx][innerIdx], - allFamilyMemebersUploaded: CalendarTestData.allFamilyMemebersUploadeds[outerIdx][innerIdx] - ) - arrayCalendarResponse.results.append(dayResponse) - } - } - - return arrayCalendarResponse - } - - static func generateTestData(_ yearMonth: String) -> ArrayResponseCalendarResponse { - - var arrayCalendarResponse: ArrayResponseCalendarResponse = .init(results: []) - switch yearMonth { - case "2023-12": - arrayCalendarResponse.results.append(contentsOf: TestData.dayInfo202312) - case "2024-01": - fallthrough - default: - arrayCalendarResponse.results.append(contentsOf: TestData.dayInfo202401) - } - - return arrayCalendarResponse - } -} diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/ImageCalendarCellDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/ImageCalendarCellDIContainer.swift index 385a6b2c7..9cd7ec593 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/ImageCalendarCellDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/ImageCalendarCellDIContainer.swift @@ -15,7 +15,7 @@ final public class ImageCalendarCellDIContainer { // MARK: - Properties public let type: ImageCalendarCellReactor.CalendarType public let isSelected: Bool - public let dayResponse: CalendarResponse + public let dayResponse: CalendarEntity private var globalState: GlobalStateProviderProtocol { guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { @@ -28,7 +28,7 @@ final public class ImageCalendarCellDIContainer { public init( _ type: ImageCalendarCellReactor.CalendarType, isSelected: Bool = false, - dayResponse: CalendarResponse + dayResponse: CalendarEntity ) { self.type = type self.isSelected = isSelected diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPageViewCellReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPageViewCellReactor.swift index e1f510ad4..207e09891 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPageViewCellReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPageViewCellReactor.swift @@ -25,9 +25,9 @@ public final class CalendarPageCellReactor: Reactor { // MARK: - Mutation public enum Mutation { - case injectCalendarBanner(BannerResponse) - case injectStatisticsSummary(FamilyMonthlyStatisticsResponse) - case injectCalendarResponse(ArrayResponseCalendarResponse) + case injectCalendarBanner(BannerEntity) + case injectStatisticsSummary(FamilyMonthlyStatisticsEntity) + case injectCalendarResponse(ArrayResponseCalendarEntity) } // MARK: - State @@ -35,7 +35,7 @@ public final class CalendarPageCellReactor: Reactor { var yearMonthDate: Date var displayCalendarBanner: BannerViewModel.State? var displayMemoryCount: Int - var displayCalendarResponse: ArrayResponseCalendarResponse? + var displayCalendarResponse: ArrayResponseCalendarEntity? } // MARK: - Properties diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPostViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPostViewReactor.swift index 42838ca95..6d537c07b 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPostViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPostViewReactor.swift @@ -29,7 +29,7 @@ public final class CalendarPostViewReactor: Reactor { // MARK: - Mutation public enum Mutation { case setAllUploadedToastMessageView(Bool) - case injectCalendarResponse(String, ArrayResponseCalendarResponse) + case injectCalendarResponse(String, ArrayResponseCalendarEntity) case injectPostResponse([PostListData]) case injectBlurImageIndex(Int) case injectVisiblePost(PostListData) @@ -48,7 +48,7 @@ public final class CalendarPostViewReactor: Reactor { var blurImageUrlString: String? var visiblePost: PostListData? @Pulse var displayPostResponse: [PostListSectionModel] - @Pulse var displayCalendarResponse: [String: [CalendarResponse]] + @Pulse var displayCalendarResponse: [String: [CalendarEntity]] @Pulse var shouldPresentAllUploadedToastMessageView: Bool @Pulse var shouldGenerateSelectionHaptic: Bool @Pulse var shouldPushProfileViewController: String? diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/ImageCalendarCellReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/ImageCalendarCellReactor.swift index 19d71e520..3796fcdbd 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/ImageCalendarCellReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/ImageCalendarCellReactor.swift @@ -49,7 +49,7 @@ final public class ImageCalendarCellReactor: Reactor { init( _ type: CalendarType, isSelected: Bool, - dayResponse: CalendarResponse, + dayResponse: CalendarEntity, calendarUseCase: CalendarUseCaseProtocol, provider: GlobalStateProviderProtocol ) { diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarPageCell.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarPageCell.swift index aabd45644..4401967f6 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarPageCell.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarPageCell.swift @@ -238,7 +238,7 @@ extension CalendarPageCell: FSCalendarDataSource { // 해당 일자에 데이터가 존재하지 않는다면 guard let dayResponse = reactor?.currentState.displayCalendarResponse?.results.filter({ $0.date == date }).first else { - let emptyResponse = CalendarResponse( + let emptyResponse = CalendarEntity( date: date, representativePostId: .none, representativeThumbnailUrl: .none, diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/CalendarPostViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/CalendarPostViewController.swift index e15c450d7..ea692db67 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/CalendarPostViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/CalendarPostViewController.swift @@ -486,7 +486,7 @@ extension CalendarPostViewController: FSCalendarDataSource { guard let currentState = reactor?.currentState, let dayResponse = currentState.displayCalendarResponse[yyyyMM]?.filter({ $0.date.isEqual(with: date) }).first else { - let emptyResponse = CalendarResponse( + let emptyResponse = CalendarEntity( date: date, representativePostId: .none, representativeThumbnailUrl: .none, diff --git a/14th-team5-iOS/Data/Sources/API/APIWorker.swift b/14th-team5-iOS/Data/Sources/API/APIWorker.swift index 3cd2bff08..c60ba0de1 100644 --- a/14th-team5-iOS/Data/Sources/API/APIWorker.swift +++ b/14th-team5-iOS/Data/Sources/API/APIWorker.swift @@ -85,6 +85,15 @@ public final class BibbiRequestInterceptor: RequestInterceptor, BibbiRouterInter // MARK: API Worker public class APIWorker: NSObject, BibbiRouterInterface { + // MARK: - Headers + var _headers: Observable<[APIHeader]?> { + return App.Repository.token.accessToken + .map { + guard let token = $0, let accessToken = token.accessToken, !accessToken.isEmpty else { return [] } + return [BibbiAPI.Header.xAppKey, BibbiAPI.Header.xAuthToken(accessToken)] + } + } + private func appendCommonHeaders(to headers: [APIHeader]?) -> [APIHeader] { var result: [APIHeader] = BibbiAPI.Header.baseHeaders guard let headers = headers else { return result } diff --git a/14th-team5-iOS/Data/Sources/Account/AccountAPI/AccountAPIWorker.swift b/14th-team5-iOS/Data/Sources/Account/AccountAPI/AccountAPIWorker.swift index 14d73f190..7336b928e 100644 --- a/14th-team5-iOS/Data/Sources/Account/AccountAPI/AccountAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Account/AccountAPI/AccountAPIWorker.swift @@ -26,15 +26,6 @@ extension AccountAPIs { super.init() self.id = "AccountAPIWorker" } - - // MARK: Values - private var _headers: Observable<[APIHeader]?> { - return App.Repository.token.accessToken - .map { - guard let token = $0, let accessToken = token.accessToken, !accessToken.isEmpty else { return [] } - return [BibbiAPI.Header.xAppKey, BibbiAPI.Header.xAuthToken(accessToken)] - } - } } } diff --git a/14th-team5-iOS/Data/Sources/Account/MeAPI/MeAPIWorker.swift b/14th-team5-iOS/Data/Sources/Account/MeAPI/MeAPIWorker.swift index b1065735d..25b30a0ad 100644 --- a/14th-team5-iOS/Data/Sources/Account/MeAPI/MeAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Account/MeAPI/MeAPIWorker.swift @@ -26,15 +26,6 @@ extension MeAPIs { super.init() self.id = "MeAPIWorker" } - - // MARK: Values - private var _headers: Observable<[APIHeader]?> { - return App.Repository.token.accessToken - .map { - guard let token = $0, let accessToken = token.accessToken, !accessToken.isEmpty else { return [] } - return [BibbiAPI.Header.xAuthToken(accessToken), BibbiAPI.Header.acceptJson] - } - } } } diff --git a/14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/CalendarAPIWorker.swift b/14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/CalendarAPIWorker.swift index e7bc8506a..feecbc434 100644 --- a/14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/CalendarAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/CalendarAPIWorker.swift @@ -22,21 +22,14 @@ extension CalendarAPIs { super.init() self.id = "CalendarAPIWorker" } - - // MARK: - Headers - private var _headers: Observable<[APIHeader]?> { - return App.Repository.token.accessToken - .map { - guard let token = $0, let accessToken = token.accessToken, !accessToken.isEmpty else { return [] } - return [BibbiAPI.Header.xAppKey, BibbiAPI.Header.xAuthToken(accessToken)] - } - } } } // MARK: - Extensions extension CalendarAPIWorker { - private func fetchCalendarResponse(spec: APISpec, headers: [APIHeader]?) -> Single { + + @available(*, deprecated) + private func fetchCalendarResponse(spec: APISpec, headers: [APIHeader]?) -> Single { return request(spec: spec, headers: headers) .subscribe(on: Self.queue) .do { @@ -50,8 +43,9 @@ extension CalendarAPIWorker { .asSingle() } - public func fetchCalendarResponse(yearMonth: String) -> Single { - let spec = CalendarAPIs.fetchCalendarResponse(yearMonth).spec + @available(*, deprecated) + public func fetchCalendarResponse(yearMonth: String) -> Single { + let spec = CalendarAPIs.calendarResponse(yearMonth).spec return Observable.just(()) .withLatestFrom(self._headers) @@ -61,7 +55,7 @@ extension CalendarAPIWorker { .asSingle() } - private func fetchStatisticsSummary(spec: APISpec, headers: [APIHeader]?) -> Single { + private func fetchStatisticsSummary(spec: APISpec, headers: [APIHeader]?) -> Single { return request(spec: spec, headers: headers) .subscribe(on: Self.queue) .do { @@ -75,8 +69,60 @@ extension CalendarAPIWorker { .asSingle() } - public func fetchStatisticsSummary(yearMonth: String) -> Single { - let spec = CalendarAPIs.fetchStatisticsSummary(yearMonth).spec + public func fetchMonthlyCalendar(yearMonth: String) -> Single { + let spec = CalendarAPIs.monthlyCalendar(yearMonth).spec + + return Observable.just(()) + .withLatestFrom(self._headers) + .observe(on: Self.queue) + .withUnretained(self) + .flatMap { $0.0.fetchMonthlyCalendar(spec: spec, headers: $0.1) } + .asSingle() + } + + + private func fetchMonthlyCalendar(spec: APISpec, headers: [APIHeader]?) -> Single { + return request(spec: spec, headers: headers) + .subscribe(on: Self.queue) + .do { + if let str = String(data: $0.1, encoding: .utf8) { + debugPrint("MonthlyCalendar Fetch Result: \(str)") + } + } + .map(ArrayResponseMonthlyCalendarResponseDTO.self) + .catchAndReturn(nil) + .map { $0?.toDomain() } + .asSingle() + } + + public func fetchDailyCalendar(yearMonth: String) -> Single { + let spec = CalendarAPIs.monthlyCalendar(yearMonth).spec + + return Observable.just(()) + .withLatestFrom(self._headers) + .observe(on: Self.queue) + .withUnretained(self) + .flatMap { $0.0.fetchDailyCalendar(spec: spec, headers: $0.1) } + .asSingle() + } + + + private func fetchDailyCalendar(spec: APISpec, headers: [APIHeader]?) -> Single { + return request(spec: spec, headers: headers) + .subscribe(on: Self.queue) + .do { + if let str = String(data: $0.1, encoding: .utf8) { + debugPrint("DailyCalendar Fetch Result: \(str)") + } + } + .map(ArrayResponseDailyCalendarResponseDTO.self) + .catchAndReturn(nil) + .map { $0?.toDomain() } + .asSingle() + } + + public func fetchStatisticsSummary(yearMonth: String) -> Single { + let spec = CalendarAPIs.statisticsSummary(yearMonth).spec return Observable.just(()) .withLatestFrom(self._headers) @@ -86,7 +132,7 @@ extension CalendarAPIWorker { .asSingle() } - private func fetchCalendarBanner(spec: APISpec, headers: [APIHeader]?) -> Single { + private func fetchCalendarBanner(spec: APISpec, headers: [APIHeader]?) -> Single { return request(spec: spec, headers: headers) .subscribe(on: Self.queue) .do { @@ -100,8 +146,8 @@ extension CalendarAPIWorker { .asSingle() } - public func fetchCalendarBanner(yearMonth: String) -> Single { - let spec = CalendarAPIs.fetchCalendarBenner(yearMonth).spec + public func fetchCalendarBanner(yearMonth: String) -> Single { + let spec = CalendarAPIs.calendarBenner(yearMonth).spec return Observable.just(()) .withLatestFrom(self._headers) diff --git a/14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/CalendarAPIs.swift b/14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/CalendarAPIs.swift index b2a52183f..289e1d6cf 100644 --- a/14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/CalendarAPIs.swift +++ b/14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/CalendarAPIs.swift @@ -10,17 +10,26 @@ import Foundation import Domain enum CalendarAPIs: API { - case fetchCalendarResponse(String) - case fetchStatisticsSummary(String) - case fetchCalendarBenner(String) + @available(*, deprecated) + case calendarResponse(String) + + case monthlyCalendar(String) + case dailyCalendar(String) + case statisticsSummary(String) + case calendarBenner(String) var spec: APISpec { switch self { - case let .fetchCalendarResponse(yearMonth): + case let .calendarResponse(yearMonth): return APISpec(method: .get, url: "\(BibbiAPI.hostApi)/calendar?type=MONTHLY&yearMonth=\(yearMonth)") - case let .fetchStatisticsSummary(yearMonth): + + case let .monthlyCalendar(yearMonth): + return APISpec(method: .get, url: "\(BibbiAPI.hostApi)/calendar/monthly?yearMonth=\(yearMonth)") + case let .dailyCalendar(yearMonth): + return APISpec(method: .get, url: "\(BibbiAPI.hostApi)/calendar/daily?yearMonthDay=\(yearMonth)") + case let .statisticsSummary(yearMonth): return APISpec(method: .get, url: "\(BibbiAPI.hostApi)/calendar/summary?yearMonth=\(yearMonth)") - case let .fetchCalendarBenner(yearMonth): + case let .calendarBenner(yearMonth): return APISpec(method: .get, url: "\(BibbiAPI.hostApi)/calendar/banner?yearMonth=\(yearMonth)") } } diff --git a/14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/DataMapping/ArrayResponseCalendarResponseDTO.swift b/14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/DataMapping/ArrayResponseCalendarResponseDTO.swift index 508588ae6..00c5af3eb 100644 --- a/14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/DataMapping/ArrayResponseCalendarResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/DataMapping/ArrayResponseCalendarResponseDTO.swift @@ -5,11 +5,10 @@ // Created by 김건우 on 12/21/23. // -import Foundation - import Domain +import Foundation -// MARK: - Data Transfer Object (DTO) +@available(*, deprecated) public struct ArrayResponseCalendarResponseDTO: Decodable { private enum CodingKeys: String, CodingKey { case results @@ -33,16 +32,16 @@ extension ArrayResponseCalendarResponseDTO { } extension ArrayResponseCalendarResponseDTO { - func toDomain() -> ArrayResponseCalendarResponse { - return ArrayResponseCalendarResponse( + func toDomain() -> ArrayResponseCalendarEntity { + return ArrayResponseCalendarEntity( results: results.map { $0.toDomain() } ) } } extension ArrayResponseCalendarResponseDTO.CalendarResponseDTO { - func toDomain() -> CalendarResponse { - return CalendarResponse( + func toDomain() -> CalendarEntity { + return CalendarEntity( date: date.toDate(), representativePostId: representativePostId, representativeThumbnailUrl: representativeThumbnailUrl, diff --git a/14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/DataMapping/ArrayResponseDailyCalendarResponseDTO.swift b/14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/DataMapping/ArrayResponseDailyCalendarResponseDTO.swift new file mode 100644 index 000000000..094b7684e --- /dev/null +++ b/14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/DataMapping/ArrayResponseDailyCalendarResponseDTO.swift @@ -0,0 +1,73 @@ +// +// ArrayResponseDailyCalendarResponseDTO.swift +// Data +// +// Created by 김건우 on 5/3/24. +// + +import Domain +import Foundation + +struct ArrayResponseDailyCalendarResponseDTO: Decodable { + enum CodingKeys: String, CodingKey { + case results + } + var results: [DailyCalendarResponseDTO] +} + +extension ArrayResponseDailyCalendarResponseDTO { + struct DailyCalendarResponseDTO: Decodable { + enum CodingKeys: String, CodingKey { + case date + case type + case postId + case postImageUrl = "postImgUrl" + case postContent + case missionContent + case authorId + case commentCount + case emojiCount + case allFamilyMembersUploaded + case createdAt + } + var date: String + var type: String + var postId: String + var postImageUrl: String + var postContent: String + var missionContent: String + var authorId: String + var commentCount: Int + var emojiCount: Int + var allFamilyMembersUploaded: Bool + var createdAt: String + } +} + +extension ArrayResponseDailyCalendarResponseDTO { + func toDomain() -> ArrayResponseDailyCalendarEntity { + return ArrayResponseDailyCalendarEntity( + results: results.map { $0.toDomain() } + ) + } +} + +extension ArrayResponseDailyCalendarResponseDTO.DailyCalendarResponseDTO { + func toDomain() -> DailyCalendarEntity { + return DailyCalendarEntity( + date: date.toDate(with: .dashYyyyMMdd), + type: .init(rawValue: type) ?? .survival, + postId: postId, + postImageUrl: postImageUrl, + postContent: postContent, + missionContent: missionContent, + authorId: authorId, + commentCount: commentCount, + emojiCount: emojiCount, + allFamilyMembersUploaded: allFamilyMembersUploaded, + createdAt: createdAt.iso8601ToDate() + ) + } +} + + diff --git a/14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/DataMapping/ArrayResponseMonthlyCalendarResponseDTO.swift b/14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/DataMapping/ArrayResponseMonthlyCalendarResponseDTO.swift new file mode 100644 index 000000000..da33b2e19 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/DataMapping/ArrayResponseMonthlyCalendarResponseDTO.swift @@ -0,0 +1,50 @@ +// +// ArrayResponseMonthlyCalendarResponse.swift +// Data +// +// Created by 김건우 on 5/3/24. +// + +import Domain +import Foundation + +public struct ArrayResponseMonthlyCalendarResponseDTO: Decodable { + private enum CodingKeys: String, CodingKey { + case results + } + var results: [MonthlyCalendarResponseDTO] +} + +extension ArrayResponseMonthlyCalendarResponseDTO { + public struct MonthlyCalendarResponseDTO: Decodable { + private enum CodingKeys: String, CodingKey { + case date + case representativePostId + case representativeThumbnailUrl + case allFamilyMembersUploaded + } + var date: String + var representativePostId: String + var representativeThumbnailUrl: String + var allFamilyMembersUploaded: Bool + } +} + +extension ArrayResponseMonthlyCalendarResponseDTO { + func toDomain() -> ArrayResponseMonthlyCalendarEntity { + return ArrayResponseMonthlyCalendarEntity( + results: results.map { $0.toDomain() } + ) + } +} + +extension ArrayResponseMonthlyCalendarResponseDTO.MonthlyCalendarResponseDTO { + func toDomain() -> MonthlyCalendarEntity { + return MonthlyCalendarEntity( + date: date.toDate(with: .dashYyyyMMdd), + representativePostId: representativePostId, + representativeThumbnailUrl: representativeThumbnailUrl, + allFamilyMemebersUploaded: allFamilyMembersUploaded + ) + } +} diff --git a/14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/DataMapping/BannerResponseDTO.swift b/14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/DataMapping/BannerResponseDTO.swift index 5b4f54212..27f945a25 100644 --- a/14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/DataMapping/BannerResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/DataMapping/BannerResponseDTO.swift @@ -71,7 +71,7 @@ extension BannerResponseDTO { } extension BannerResponseDTO { - func toDomain() -> BannerResponse { + func toDomain() -> BannerEntity { return .init( familyTopPercentage: familyTopPercentage, allFamilyMembersUploadedDays: allFamilyMembersUploadedDays, diff --git a/14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/DataMapping/FamilyMonthlyStatisticsResponseDTO.swift b/14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/DataMapping/FamilyMonthlyStatisticsResponseDTO.swift index 8b6eae8b9..dc665f3e4 100644 --- a/14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/DataMapping/FamilyMonthlyStatisticsResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/DataMapping/FamilyMonthlyStatisticsResponseDTO.swift @@ -18,7 +18,7 @@ public struct FamilyMonthlyStatisticsResponseDTO: Decodable { } extension FamilyMonthlyStatisticsResponseDTO { - func toDomain() -> FamilyMonthlyStatisticsResponse { + func toDomain() -> FamilyMonthlyStatisticsEntity { return .init(totalImageCnt: self.totalImageCnt) } } diff --git a/14th-team5-iOS/Data/Sources/Calendar/Repository/CalendarRepository.swift b/14th-team5-iOS/Data/Sources/Calendar/Repository/CalendarRepository.swift index 5460b6abb..df5c38be5 100644 --- a/14th-team5-iOS/Data/Sources/Calendar/Repository/CalendarRepository.swift +++ b/14th-team5-iOS/Data/Sources/Calendar/Repository/CalendarRepository.swift @@ -23,18 +23,32 @@ public final class CalendarRepository: CalendarRepositoryProtocol { // MARK: - Extensions extension CalendarRepository { - public func fetchCalendarResponse(yearMonth: String) -> Observable { + + @available(*, deprecated) + public func fetchCalendarResponse(yearMonth: String) -> Observable { return calendarApiWorker.fetchCalendarResponse(yearMonth: yearMonth) .asObservable() } - public func fetchStatisticsSummary(yearMonth: String) -> Observable { + + public func fetchMonthyCalendarResponse(yearMonth: String) -> Observable { + return calendarApiWorker.fetchMonthlyCalendar(yearMonth: yearMonth) + .asObservable() + } + + public func fetchDailyCalendarResponse(yearMonth: String) -> Observable { + return calendarApiWorker.fetchDailyCalendar(yearMonth: yearMonth) + .asObservable() + } + + public func fetchStatisticsSummary(yearMonth: String) -> Observable { return calendarApiWorker.fetchStatisticsSummary(yearMonth: yearMonth) .asObservable() } - public func fetchCalendarBanner(yearMonth: String) -> Observable { + public func fetchCalendarBanner(yearMonth: String) -> Observable { return calendarApiWorker.fetchCalendarBanner(yearMonth: yearMonth) .asObservable() } + } diff --git a/14th-team5-iOS/Data/Sources/Emoji/EmojiAPI/EmojiAPIWorker.swift b/14th-team5-iOS/Data/Sources/Emoji/EmojiAPI/EmojiAPIWorker.swift index bafbebe20..ff6f26136 100644 --- a/14th-team5-iOS/Data/Sources/Emoji/EmojiAPI/EmojiAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Emoji/EmojiAPI/EmojiAPIWorker.swift @@ -23,14 +23,6 @@ extension EmojiAPIs { super.init() self.id = "EmojiAPIWorker" } - - private var _headers: Observable<[APIHeader]?> { - return App.Repository.token.accessToken - .map { - guard let token = $0, let accessToken = token.accessToken, !accessToken.isEmpty else { return [] } - return [BibbiAPI.Header.xAppKey, BibbiAPI.Header.xAuthToken(accessToken), BibbiAPI.Header.acceptJson] - } - } } } diff --git a/14th-team5-iOS/Data/Sources/Family/FamilyAPI/FamilyAPIWorker.swift b/14th-team5-iOS/Data/Sources/Family/FamilyAPI/FamilyAPIWorker.swift index ca0a80d95..15882c48d 100644 --- a/14th-team5-iOS/Data/Sources/Family/FamilyAPI/FamilyAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Family/FamilyAPI/FamilyAPIWorker.swift @@ -23,14 +23,6 @@ extension FamilyAPIs { super.init() self.id = "FamilyAPIWorker" } - - private var _headers: Observable<[APIHeader]?> { - return App.Repository.token.accessToken - .map { - guard let token = $0, let accessToken = token.accessToken, !accessToken.isEmpty else { return [] } - return [BibbiAPI.Header.xAppKey, BibbiAPI.Header.xAuthToken(accessToken), BibbiAPI.Header.acceptJson] - } - } } } diff --git a/14th-team5-iOS/Data/Sources/Mission/MissionAPI/MissionAPIWorker.swift b/14th-team5-iOS/Data/Sources/Mission/MissionAPI/MissionAPIWorker.swift index 86d60fa1c..a058b1210 100644 --- a/14th-team5-iOS/Data/Sources/Mission/MissionAPI/MissionAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Mission/MissionAPI/MissionAPIWorker.swift @@ -23,14 +23,6 @@ extension MissionAPIs { super.init() self.id = "MissionAPIWorker" } - - private var _headers: Observable<[APIHeader]?> { - return App.Repository.token.accessToken - .map { - guard let token = $0, let accessToken = token.accessToken, !accessToken.isEmpty else { return [] } - return [BibbiAPI.Header.xAppKey, BibbiAPI.Header.xAuthToken(accessToken), BibbiAPI.Header.acceptJson] - } - } } } diff --git a/14th-team5-iOS/Data/Sources/Pick/PickAPI/PickAPIWorker.swift b/14th-team5-iOS/Data/Sources/Pick/PickAPI/PickAPIWorker.swift index 9119958b7..52da97469 100644 --- a/14th-team5-iOS/Data/Sources/Pick/PickAPI/PickAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Pick/PickAPI/PickAPIWorker.swift @@ -22,19 +22,6 @@ extension PickAPIs { super.init() self.id = "PickAPIWorker" } - - // MARK: - Headers - private var _headers: Observable<[APIHeader]?> { - return App.Repository.token.accessToken - .map { - guard let token = $0, - let accessToken = token.accessToken, - !accessToken.isEmpty else { - return [] - } - return [BibbiAPI.Header.xAppKey, BibbiAPI.Header.xAuthToken(accessToken)] - } - } } } diff --git a/14th-team5-iOS/Data/Sources/Post/PostList/PostListAPI/PostListAPIWorker.swift b/14th-team5-iOS/Data/Sources/Post/PostList/PostListAPI/PostListAPIWorker.swift index 1d8537aa3..05941f2b9 100644 --- a/14th-team5-iOS/Data/Sources/Post/PostList/PostListAPI/PostListAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Post/PostList/PostListAPI/PostListAPIWorker.swift @@ -24,14 +24,6 @@ extension PostListAPIs { super.init() self.id = "PostListAPIWorker" } - - private var _headers: Observable<[APIHeader]?> { - return App.Repository.token.accessToken - .map { - guard let token = $0, let accessToken = token.accessToken, !accessToken.isEmpty else { return [] } - return [BibbiAPI.Header.xAppKey, BibbiAPI.Header.xAuthToken(accessToken), BibbiAPI.Header.acceptJson] - } - } } } diff --git a/14th-team5-iOS/Data/Sources/PostComment/PostCommentAPI/PostCommentAPIWorker.swift b/14th-team5-iOS/Data/Sources/PostComment/PostCommentAPI/PostCommentAPIWorker.swift index 9f224da78..31376f2eb 100644 --- a/14th-team5-iOS/Data/Sources/PostComment/PostCommentAPI/PostCommentAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/PostComment/PostCommentAPI/PostCommentAPIWorker.swift @@ -22,15 +22,6 @@ extension PostCommentAPIs { super.init() self.id = "PostCommentAPIWorker" } - - // MARK: - Headers - private var _headers: Observable<[APIHeader]?> { - return App.Repository.token.accessToken - .map { - guard let token = $0, let accessToken = token.accessToken, !accessToken.isEmpty else { return [] } - return [BibbiAPI.Header.xAppKey, BibbiAPI.Header.xAuthToken(accessToken)] - } - } } } diff --git a/14th-team5-iOS/Data/Sources/RealEmoji/RealEmojiAPI/RealEmojiAPIWorker.swift b/14th-team5-iOS/Data/Sources/RealEmoji/RealEmojiAPI/RealEmojiAPIWorker.swift index 303ba850c..715c1e23c 100644 --- a/14th-team5-iOS/Data/Sources/RealEmoji/RealEmojiAPI/RealEmojiAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/RealEmoji/RealEmojiAPI/RealEmojiAPIWorker.swift @@ -24,14 +24,6 @@ extension RealEmojiAPIS { super.init() self.id = "RealEmojiAPIWorker" } - - private var _headers: Observable<[APIHeader]?> { - return App.Repository.token.accessToken - .map { - guard let token = $0, let accessToken = token.accessToken, !accessToken.isEmpty else { return [] } - return [BibbiAPI.Header.xAppKey, BibbiAPI.Header.xAuthToken(accessToken), BibbiAPI.Header.acceptJson] - } - } } } diff --git a/14th-team5-iOS/Data/Sources/View/Main/Worker/MainAPIWorker.swift b/14th-team5-iOS/Data/Sources/View/Main/Worker/MainAPIWorker.swift index fd09c31ea..91264a22a 100644 --- a/14th-team5-iOS/Data/Sources/View/Main/Worker/MainAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/View/Main/Worker/MainAPIWorker.swift @@ -23,15 +23,6 @@ extension MainAPIs { super.init() self.id = "MainAPIWorker" } - - // MARK: - Headers - private var _headers: Observable<[APIHeader]?> { - return App.Repository.token.accessToken - .map { - guard let token = $0, let accessToken = token.accessToken, !accessToken.isEmpty else { return [] } - return [BibbiAPI.Header.xAppKey, BibbiAPI.Header.xAuthToken(accessToken)] - } - } } } diff --git a/14th-team5-iOS/Domain/Sources/Calendar/Entities/ArrayResponseCalendarEntity.swift b/14th-team5-iOS/Domain/Sources/Calendar/Entities/ArrayResponseCalendarEntity.swift new file mode 100644 index 000000000..55242e1a0 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Calendar/Entities/ArrayResponseCalendarEntity.swift @@ -0,0 +1,37 @@ +// +// dddd.swift +// Domain +// +// Created by 김건우 on 5/3/24. +// + +import Foundation + +@available(*, deprecated) +public struct ArrayResponseCalendarEntity { + public var results: [CalendarEntity] + + public init(results: [CalendarEntity]) { + self.results = results + } +} + +public struct CalendarEntity { + public var date: Date + public var representativePostId: String + public var representativeThumbnailUrl: String + public var allFamilyMemebersUploaded: Bool + + public init( + date: Date, + representativePostId: String, + representativeThumbnailUrl: String, + allFamilyMemebersUploaded: Bool + ) { + self.date = date + self.representativePostId = representativePostId + self.representativeThumbnailUrl = representativeThumbnailUrl + self.allFamilyMemebersUploaded = allFamilyMemebersUploaded + } +} + diff --git a/14th-team5-iOS/Domain/Sources/Calendar/Entities/ArrayResponseDailyCalendarEntity.swift b/14th-team5-iOS/Domain/Sources/Calendar/Entities/ArrayResponseDailyCalendarEntity.swift new file mode 100644 index 000000000..7c414ec3c --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Calendar/Entities/ArrayResponseDailyCalendarEntity.swift @@ -0,0 +1,57 @@ +// +// ArrayResponseDailyCalendarEntity.swift +// Domain +// +// Created by 김건우 on 5/3/24. +// + +import Foundation + +public struct ArrayResponseDailyCalendarEntity { + public var results: [DailyCalendarEntity] + + public init(results: [DailyCalendarEntity]) { + self.results = results + } +} + +public struct DailyCalendarEntity { + public var date: Date + public var type: PostType + public var postId: String + public var postImageUrl: String + public var postContent: String + public var missionContent: String + public var authorId: String + public var commentCount: Int + public var emojiCount: Int + public var allFamilyMembersUploaded: Bool + public var createdAt: Date + + public init( + date: Date, + type: PostType, + postId: String, + postImageUrl: String, + postContent: String, + missionContent: String, + authorId: String, + commentCount: Int, + emojiCount: Int, + allFamilyMembersUploaded: Bool, + createdAt: Date + ) { + self.date = date + self.type = type + self.postId = postId + self.postImageUrl = postImageUrl + self.postContent = postContent + self.missionContent = missionContent + self.authorId = authorId + self.commentCount = commentCount + self.emojiCount = emojiCount + self.allFamilyMembersUploaded = allFamilyMembersUploaded + self.createdAt = createdAt + } +} + diff --git a/14th-team5-iOS/Domain/Sources/Calendar/Entities/ArrayResponseCalendarResponse.swift b/14th-team5-iOS/Domain/Sources/Calendar/Entities/ArrayResponseMonthlyCalendarEntity.swift similarity index 78% rename from 14th-team5-iOS/Domain/Sources/Calendar/Entities/ArrayResponseCalendarResponse.swift rename to 14th-team5-iOS/Domain/Sources/Calendar/Entities/ArrayResponseMonthlyCalendarEntity.swift index c77c7939e..aa1c200a8 100644 --- a/14th-team5-iOS/Domain/Sources/Calendar/Entities/ArrayResponseCalendarResponse.swift +++ b/14th-team5-iOS/Domain/Sources/Calendar/Entities/ArrayResponseMonthlyCalendarEntity.swift @@ -7,15 +7,15 @@ import Foundation -public struct ArrayResponseCalendarResponse { - public var results: [CalendarResponse] +public struct ArrayResponseMonthlyCalendarEntity { + public var results: [MonthlyCalendarEntity] - public init(results: [CalendarResponse]) { + public init(results: [MonthlyCalendarEntity]) { self.results = results } } -public struct CalendarResponse { +public struct MonthlyCalendarEntity { public var date: Date public var representativePostId: String public var representativeThumbnailUrl: String @@ -32,4 +32,5 @@ public struct CalendarResponse { self.representativeThumbnailUrl = representativeThumbnailUrl self.allFamilyMemebersUploaded = allFamilyMemebersUploaded } + } diff --git a/14th-team5-iOS/Domain/Sources/Calendar/Entities/BannerResponse.swift b/14th-team5-iOS/Domain/Sources/Calendar/Entities/BannerEntity.swift similarity index 96% rename from 14th-team5-iOS/Domain/Sources/Calendar/Entities/BannerResponse.swift rename to 14th-team5-iOS/Domain/Sources/Calendar/Entities/BannerEntity.swift index 854b86c4c..8c6b18137 100644 --- a/14th-team5-iOS/Domain/Sources/Calendar/Entities/BannerResponse.swift +++ b/14th-team5-iOS/Domain/Sources/Calendar/Entities/BannerEntity.swift @@ -7,7 +7,7 @@ import UIKit -public struct BannerResponse { +public struct BannerEntity { public var familyTopPercentage: Int public var allFammilyMembersUploadedDays: Int public var familyLevel: Int diff --git a/14th-team5-iOS/Domain/Sources/Calendar/Entities/FamilyMonthlyStatisticsResponse.swift b/14th-team5-iOS/Domain/Sources/Calendar/Entities/FamilyMonthlyStatisticsEntity.swift similarity index 83% rename from 14th-team5-iOS/Domain/Sources/Calendar/Entities/FamilyMonthlyStatisticsResponse.swift rename to 14th-team5-iOS/Domain/Sources/Calendar/Entities/FamilyMonthlyStatisticsEntity.swift index d35eb65a2..a1b19c72c 100644 --- a/14th-team5-iOS/Domain/Sources/Calendar/Entities/FamilyMonthlyStatisticsResponse.swift +++ b/14th-team5-iOS/Domain/Sources/Calendar/Entities/FamilyMonthlyStatisticsEntity.swift @@ -7,7 +7,7 @@ import Foundation -public struct FamilyMonthlyStatisticsResponse { +public struct FamilyMonthlyStatisticsEntity { public var totalImageCnt: Int public init(totalImageCnt: Int) { diff --git a/14th-team5-iOS/Domain/Sources/Calendar/Interfaces/Repositories/CalendarRepository.swift b/14th-team5-iOS/Domain/Sources/Calendar/Interfaces/Repositories/CalendarRepository.swift index 31b71bf73..1f7f84169 100644 --- a/14th-team5-iOS/Domain/Sources/Calendar/Interfaces/Repositories/CalendarRepository.swift +++ b/14th-team5-iOS/Domain/Sources/Calendar/Interfaces/Repositories/CalendarRepository.swift @@ -12,7 +12,10 @@ import RxSwift public protocol CalendarRepositoryProtocol { var disposeBag: DisposeBag { get } - func fetchCalendarResponse(yearMonth: String) -> Observable - func fetchStatisticsSummary(yearMonth: String) -> Observable - func fetchCalendarBanner(yearMonth: String) -> Observable + func fetchCalendarResponse(yearMonth: String) -> Observable + + func fetchMonthyCalendarResponse(yearMonth: String) -> Observable + func fetchDailyCalendarResponse(yearMonth: String) -> Observable + func fetchStatisticsSummary(yearMonth: String) -> Observable + func fetchCalendarBanner(yearMonth: String) -> Observable } diff --git a/14th-team5-iOS/Domain/Sources/Calendar/UseCases/CalendarUseCase.swift b/14th-team5-iOS/Domain/Sources/Calendar/UseCases/CalendarUseCase.swift index d8a4995cb..042f97e1f 100644 --- a/14th-team5-iOS/Domain/Sources/Calendar/UseCases/CalendarUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/Calendar/UseCases/CalendarUseCase.swift @@ -10,9 +10,9 @@ import Foundation import RxSwift public protocol CalendarUseCaseProtocol { - func executeFetchCalednarResponse(yearMonth: String) -> Observable - func executeFetchStatisticsSummary(yearMonth: String) -> Observable - func executeFetchCalendarBenner(yearMonth: String) -> Observable + func executeFetchCalednarResponse(yearMonth: String) -> Observable + func executeFetchStatisticsSummary(yearMonth: String) -> Observable + func executeFetchCalendarBenner(yearMonth: String) -> Observable } public final class CalendarUseCase: CalendarUseCaseProtocol { @@ -22,15 +22,15 @@ public final class CalendarUseCase: CalendarUseCaseProtocol { self.calendarRepository = calendarRepository } - public func executeFetchCalednarResponse(yearMonth: String) -> Observable { + public func executeFetchCalednarResponse(yearMonth: String) -> Observable { return calendarRepository.fetchCalendarResponse(yearMonth: yearMonth) } - public func executeFetchStatisticsSummary(yearMonth: String) -> Observable { + public func executeFetchStatisticsSummary(yearMonth: String) -> Observable { return calendarRepository.fetchStatisticsSummary(yearMonth: yearMonth) } - public func executeFetchCalendarBenner(yearMonth: String) -> Observable { + public func executeFetchCalendarBenner(yearMonth: String) -> Observable { return calendarRepository.fetchCalendarBanner(yearMonth: yearMonth) } } From eea98b191e6c5a00c886b2e2e2d754570658e2ef Mon Sep 17 00:00:00 2001 From: Do-hyun-Kim Date: Mon, 6 May 2024 01:01:04 +0900 Subject: [PATCH 008/263] =?UTF-8?q?ProfileViewController=20ProfileFeedView?= =?UTF-8?q?Controller=EB=A1=9C=20=EB=B6=84=EA=B8=B0=20=EC=B2=98=EB=A6=AC?= =?UTF-8?q?=20-=20ProfileViewController=20FeedAPI=20or=20=EB=B9=84?= =?UTF-8?q?=EC=A6=88=EB=8B=88=EC=8A=A4=20=EB=A1=9C=EC=A7=81=20ProfileFeedV?= =?UTF-8?q?iewUseCase=EB=A1=9C=20=EC=B1=85=EC=9E=84=20=EC=A0=84=ED=99=98?= =?UTF-8?q?=20-=20ProfilePostDTO,=20Query,Parameter=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=20=EC=A0=9C=EA=B1=B0=20-=20ProfileFeedViewController=20Prefetc?= =?UTF-8?q?h=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CellReactor/ProfileFeedCellReactor.swift | 7 +- .../Dependency/ProfileFeedDIContainer.swift | 48 +++++ .../ProfileFeedPageViewController.swift | 64 +++++++ .../Profile/ProfileFeedViewController.swift | 132 +++++++++++++ .../Profile/ProfileViewController.swift | 137 ++------------ .../Reactor/ProfileFeedViewReactor.swift | 179 ++++++++++++++++++ .../Profile/Reactor/ProfileViewReactor.swift | 177 ++--------------- .../Post/PostList/DTO/PostListDTO.swift | 1 + .../PostListAPI/PostListAPIWorker.swift | 2 +- .../Profile/DataMapping/ProfilePostDTO.swift | 80 -------- .../Profile/ProfileAPI/ProfileAPIWorker.swift | 17 -- .../Repositories/ProfileViewRepository.swift | 18 -- .../Colors/Black.colorset/Contents.json | 12 +- .../Sources/Post/Entities/PostListData.swift | 4 +- .../Sources/Post/Entities/PostListQuery.swift | 6 +- .../Profile/Entity/ProfilePostResponse.swift | 51 ----- .../Interfaces/ProfileViewInterface.swift | 1 - .../Parameters/ProfilePostParameter.swift | 58 ------ .../Profile/UseCases/ProfileFeedUseCase.swift | 35 ++++ .../Profile/UseCases/ProfileViewUseCase.swift | 5 - 20 files changed, 515 insertions(+), 519 deletions(-) create mode 100644 14th-team5-iOS/App/Sources/Presentation/Profile/Dependency/ProfileFeedDIContainer.swift create mode 100644 14th-team5-iOS/App/Sources/Presentation/Profile/ProfileFeedPageViewController.swift create mode 100644 14th-team5-iOS/App/Sources/Presentation/Profile/ProfileFeedViewController.swift create mode 100644 14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileFeedViewReactor.swift delete mode 100644 14th-team5-iOS/Data/Sources/Profile/DataMapping/ProfilePostDTO.swift delete mode 100644 14th-team5-iOS/Domain/Sources/Profile/Entity/ProfilePostResponse.swift delete mode 100644 14th-team5-iOS/Domain/Sources/Profile/Parameters/ProfilePostParameter.swift create mode 100644 14th-team5-iOS/Domain/Sources/Profile/UseCases/ProfileFeedUseCase.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/Cell/CellReactor/ProfileFeedCellReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/Cell/CellReactor/ProfileFeedCellReactor.swift index 16718d2de..8dc919c72 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/Cell/CellReactor/ProfileFeedCellReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/Cell/CellReactor/ProfileFeedCellReactor.swift @@ -20,7 +20,7 @@ public final class ProfileFeedCellReactor: Reactor { var date: String var commentCount: String var content: [String] - var feedType: String? + var feedType: String @Pulse var descrptionSection: [ProfileFeedDescrptionSectionModel] = [.feedDescrption([])] } @@ -37,7 +37,8 @@ public final class ProfileFeedCellReactor: Reactor { emojiCount: String, date: String, commentCount: String, - content: [String] + content: [String], + feedType: String ) { self.initialState = State( imageURL: imageURL, @@ -45,7 +46,7 @@ public final class ProfileFeedCellReactor: Reactor { date: date, commentCount: commentCount, content: content, - feedType: nil + feedType: feedType ) } diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/Dependency/ProfileFeedDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/Dependency/ProfileFeedDIContainer.swift new file mode 100644 index 000000000..2a70f168e --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/Dependency/ProfileFeedDIContainer.swift @@ -0,0 +1,48 @@ +// +// ProfileFeedDIContainer.swift +// App +// +// Created by Kim dohyun on 5/4/24. +// + +import UIKit + +import Core +import Data +import Domain + + +final class ProfileFeedDIContainer { + typealias ViewController = ProfileFeedViewController + typealias Reactor = ProfileFeedViewReactor + typealias Repository = PostListRepositoryProtocol + typealias UseCase = ProfileFeedUseCaseProtocol + + private let postType: PostType + private let memberId: String + + + init(postType: PostType, memberId: String) { + self.postType = postType + self.memberId = memberId + } + + func makeViewController() -> ViewController { + return ProfileFeedViewController(reactor: makeReactor()) + } + + func makeReactor() -> Reactor { + return ProfileFeedViewReactor(feedUseCase: makeUseCase(), type: postType, memberId: memberId) + } + + func makeUseCase() -> UseCase { + return ProfileFeedUseCase(missionFeedRepository: makeRepository()) + } + + func makeRepository() -> Repository { + return PostListAPIs.Worker() + } + + + +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileFeedPageViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileFeedPageViewController.swift new file mode 100644 index 000000000..f8a695125 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileFeedPageViewController.swift @@ -0,0 +1,64 @@ +// +// ProfileFeedPageViewController.swift +// App +// +// Created by Kim dohyun on 5/4/24. +// + +import UIKit + +import Core +import RxSwift +import RxCocoa + +final class ProfileFeedPageViewController: UIPageViewController { + + + public var currentPage: Int = 0 { + didSet { + setViewController(index: currentPage) + } + } + + public var memberId: String = "" + + private lazy var profileFeedSurivalViewController: ProfileFeedViewController = ProfileFeedDIContainer(postType: .survival, memberId: memberId).makeViewController() + private lazy var profileFeedMissionViewController: ProfileFeedViewController = ProfileFeedDIContainer(postType: .mission, memberId: memberId).makeViewController() + + + + override init(transitionStyle style: UIPageViewController.TransitionStyle, navigationOrientation: UIPageViewController.NavigationOrientation, options: [UIPageViewController.OptionsKey : Any]? = nil) { + super.init(transitionStyle: .scroll, navigationOrientation: .horizontal) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + override func viewDidLoad() { + super.viewDidLoad() + + setupUI() + } + + private func setupUI() { + setViewController(index: currentPage) + } + + + private func setViewController(index: Int) { + switch index { + case 0: + setViewControllers([profileFeedSurivalViewController], direction: .reverse, animated: true) + case 1: + setViewControllers([profileFeedMissionViewController], direction: .forward, animated: true) + default: + break + } + } +} + + + + diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileFeedViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileFeedViewController.swift new file mode 100644 index 000000000..41a33a010 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileFeedViewController.swift @@ -0,0 +1,132 @@ +// +// ProfileFeedViewController.swift +// App +// +// Created by Kim dohyun on 5/4/24. +// + +import UIKit + +import Core +import ReactorKit +import RxDataSources +import RxCocoa + +final class ProfileFeedViewController: BaseViewController { + + private let profileFeedCollectionViewLayout: UICollectionViewFlowLayout = UICollectionViewFlowLayout() + private lazy var profileFeedCollectionView: UICollectionView = UICollectionView(frame: .zero, collectionViewLayout: profileFeedCollectionViewLayout) + + private let profileFeedDataSources: RxCollectionViewSectionedReloadDataSource = .init { dataSources, collectionView, indexPath, sectionItem in + switch sectionItem { + case let .feedCategoryItem(cellReactor): + guard let profileFeedCell = collectionView.dequeueReusableCell(withReuseIdentifier: "ProfileFeedCollectionViewCell", for: indexPath) as? ProfileFeedCollectionViewCell else { return UICollectionViewCell() } + profileFeedCell.reactor = cellReactor + return profileFeedCell + + case let .feedCateogryEmptyItem(cellReactor): + guard let profileFeedEmptyCell = collectionView.dequeueReusableCell(withReuseIdentifier: "ProfileFeedEmptyCollectionViewCell", for: indexPath) as? ProfileFeedEmptyCollectionViewCell else { return UICollectionViewCell() } + profileFeedEmptyCell.reactor = cellReactor + return profileFeedEmptyCell + } + } + + override func viewDidLoad() { + super.viewDidLoad() + } + + override func setupUI() { + super.setupUI() + view.addSubview(profileFeedCollectionView) + } + + override func setupAttributes() { + super.setupAttributes() + profileFeedCollectionViewLayout.do { + $0.scrollDirection = .vertical + } + + profileFeedCollectionView.do { + $0.register(ProfileFeedCollectionViewCell.self, forCellWithReuseIdentifier: "ProfileFeedCollectionViewCell") + $0.register(ProfileFeedEmptyCollectionViewCell.self, forCellWithReuseIdentifier: "ProfileFeedEmptyCollectionViewCell") + $0.showsVerticalScrollIndicator = true + $0.showsHorizontalScrollIndicator = false + $0.backgroundColor = .clear + } + } + + override func setupAutoLayout() { + super.setupAutoLayout() + + profileFeedCollectionView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + + } + + + override func bind(reactor: ProfileFeedViewReactor) { + + + Observable.just(()) + .map { Reactor.Action.reloadFeedItems } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + + profileFeedCollectionView.rx + .setDelegate(self) + .disposed(by: disposeBag) + + reactor.pulse(\.$feedSection) + .asDriver(onErrorJustReturn: []) + .drive(profileFeedCollectionView.rx.items(dataSource: profileFeedDataSources)) + .disposed(by: disposeBag) + + profileFeedCollectionView.rx + .prefetchItems + .distinctUntilChanged() + .compactMap(\.last?.item) + .withLatestFrom(reactor.state.compactMap { $0.feedItems}) + .filter { !$0.isLast } + .map { _ in Reactor.Action.fetchMoreFeedItems} + .bind(to: reactor.action) + .disposed(by: disposeBag) + + reactor.state + .compactMap { $0.feedItems?.postLists.isEmpty } + .map { !$0 } + .bind(to: profileFeedCollectionView.rx.isScrollEnabled) + .disposed(by: disposeBag) + + + } +} + +extension ProfileFeedViewController: UICollectionViewDelegateFlowLayout { + + public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + + switch profileFeedDataSources[indexPath] { + case .feedCategoryItem: + return CGSize(width: (collectionView.frame.size.width / 2) - 4, height: 243) + case .feedCateogryEmptyItem: + return CGSize(width: collectionView.frame.size.width, height: collectionView.frame.size.height) + } + + } + + public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { + return UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) + } + + public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { + return 3 + } + + public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { + return 16 + } + +} + diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileViewController.swift index 03c345668..f1892ad48 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileViewController.swift @@ -35,22 +35,9 @@ public final class ProfileViewController: BaseViewController private lazy var profileView: BibbiProfileView = BibbiProfileView(cornerRadius: 50) private let profileLineView: UIView = UIView() private lazy var profilePickerController: PHPickerViewController = PHPickerViewController(configuration: pickerConfiguration) - private let profileFeedCollectionViewLayout: UICollectionViewFlowLayout = UICollectionViewFlowLayout() - private lazy var profileFeedCollectionView: UICollectionView = UICollectionView(frame: .zero, collectionViewLayout: profileFeedCollectionViewLayout) - private let profileFeedDataSources: RxCollectionViewSectionedReloadDataSource = .init { dataSources, collectionView, indexPath, sectionItem in - switch sectionItem { - case let .feedCategoryItem(cellReactor): - guard let profileFeedCell = collectionView.dequeueReusableCell(withReuseIdentifier: "ProfileFeedCollectionViewCell", for: indexPath) as? ProfileFeedCollectionViewCell else { return UICollectionViewCell() } - profileFeedCell.reactor = cellReactor - return profileFeedCell - - case let .feedCateogryEmptyItem(cellReactor): - guard let profileFeedEmptyCell = collectionView.dequeueReusableCell(withReuseIdentifier: "ProfileFeedEmptyCollectionViewCell", for: indexPath) as? ProfileFeedEmptyCollectionViewCell else { return UICollectionViewCell() } - profileFeedEmptyCell.reactor = cellReactor - return profileFeedEmptyCell - } - } + private lazy var profileFeedViewController: ProfileFeedPageViewController = ProfileFeedPageViewController() + public override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) @@ -68,16 +55,12 @@ public final class ProfileViewController: BaseViewController public override func setupUI() { super.setupUI() - view.addSubviews(profileView, profileLineView, profileSegementControl, profileFeedCollectionView, profileIndicatorView) + view.addSubviews(profileView, profileLineView, profileFeedViewController.view, profileSegementControl, profileIndicatorView) } public override func setupAttributes() { super.setupAttributes() - profileFeedCollectionViewLayout.do { - $0.scrollDirection = .vertical - } - profilePickerController.do { $0.delegate = self } @@ -90,13 +73,6 @@ public final class ProfileViewController: BaseViewController $0.setNavigationView(leftItem: .arrowLeft, centerItem: .label("활동"), rightItem: .setting) } - profileFeedCollectionView.do { - $0.register(ProfileFeedCollectionViewCell.self, forCellWithReuseIdentifier: "ProfileFeedCollectionViewCell") - $0.register(ProfileFeedEmptyCollectionViewCell.self, forCellWithReuseIdentifier: "ProfileFeedEmptyCollectionViewCell") - $0.showsVerticalScrollIndicator = true - $0.showsHorizontalScrollIndicator = false - $0.backgroundColor = .clear - } } public override func setupAutoLayout() { @@ -120,12 +96,11 @@ public final class ProfileViewController: BaseViewController $0.width.equalTo(140) $0.centerX.equalTo(profileView) } - - profileFeedCollectionView.snp.makeConstraints { - $0.top.equalTo(profileSegementControl.snp.bottom).offset(20) - $0.left.right.bottom.equalToSuperview() - } - + + profileFeedViewController.view.snp.makeConstraints { + $0.top.equalTo(profileSegementControl.snp.bottom).offset(20) + $0.left.right.bottom.equalToSuperview() + } profileIndicatorView.snp.makeConstraints { $0.center.equalToSuperview() @@ -148,11 +123,6 @@ public final class ProfileViewController: BaseViewController .disposed(by: disposeBag) - profileFeedCollectionView.rx - .setDelegate(self) - .disposed(by: disposeBag) - - profileView.circleButton .rx.tap .throttle(.milliseconds(300), scheduler: MainScheduler.instance) @@ -185,11 +155,6 @@ public final class ProfileViewController: BaseViewController .drive(profileIndicatorView.rx.isHidden) .disposed(by: disposeBag) - reactor.pulse(\.$feedSection) - .asDriver(onErrorJustReturn: []) - .drive(profileFeedCollectionView.rx.items(dataSource: profileFeedDataSources)) - .disposed(by: disposeBag) - reactor.pulse(\.$profileMemberEntity) .compactMap { $0 } .map { $0.memberName } @@ -226,8 +191,17 @@ public final class ProfileViewController: BaseViewController .map { Reactor.Action.didTapSegementControl(.mission) } .bind(to: reactor.action) .disposed(by: disposeBag) - - + + reactor.state + .map { $0.memberId } + .bind(to: profileFeedViewController.rx.memberId) + .disposed(by: disposeBag) + + reactor.state + .map { $0.feedType.rawValue } + .distinctUntilChanged() + .bind(to: profileFeedViewController.rx.currentPage) + .disposed(by: disposeBag) reactor @@ -262,29 +236,6 @@ public final class ProfileViewController: BaseViewController .bind(to: profileView.profileDefaultLabel.rx.text) .disposed(by: disposeBag) - profileFeedCollectionView.rx.itemSelected - .withLatestFrom(reactor.pulse(\.$feedResultItem)) { indexPath, feedResultItem in - return (indexPath, feedResultItem) - } - .throttle(.milliseconds(300), scheduler: MainScheduler.instance) - .map { Reactor.Action.didTapProfilePost($0.0, $0.1) } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - - Observable - .combineLatest( - reactor.pulse(\.$profileData), - reactor.pulse(\.$selectedIndexPath) - ) - .withUnretained(self) - .filter { !$0.1.0.items.isEmpty } - .debounce(.milliseconds(300), scheduler: MainScheduler.instance) - .subscribe { - guard let indexPath = $0.1.1 else { return } - let postListViewController = PostListsDIContainer().makeViewController(postLists: $0.1.0, selectedIndex: indexPath) - $0.0.navigationController?.pushViewController(postListViewController, animated: true) - }.disposed(by: disposeBag) reactor.state .compactMap { $0.profileMemberEntity?.dayOfBirth } @@ -294,12 +245,6 @@ public final class ProfileViewController: BaseViewController .disposed(by: disposeBag) - reactor.state - .compactMap { $0.profilePostEntity?.results.isEmpty } - .map { !$0 } - .bind(to: profileFeedCollectionView.rx.isScrollEnabled) - .disposed(by: disposeBag) - reactor.state .compactMap { $0.profileMemberEntity} .map { "\($0.familyJoinAt) 가입" } @@ -316,22 +261,6 @@ public final class ProfileViewController: BaseViewController owner.navigationController?.pushViewController(profileDetailViewController, animated: false) }.disposed(by: disposeBag) - profileFeedCollectionView.rx - .didScroll - .withLatestFrom(profileFeedCollectionView.rx.contentOffset) - .withUnretained(self) - .map { - let contentPadding = $0.0.profileFeedCollectionView.contentSize.height - $0.1.y - if contentPadding < UIScreen.main.bounds.height { - return true - } else { - return false - } - } - .distinctUntilChanged() - .map { Reactor.Action.fetchMorePostItems($0) } - .bind(to: reactor.action) - .disposed(by: disposeBag) navigationBarView.rx.rightButtonTap .withLatestFrom(reactor.state.map { $0.memberId }) @@ -343,34 +272,6 @@ public final class ProfileViewController: BaseViewController } } - -extension ProfileViewController: UICollectionViewDelegateFlowLayout { - - public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { - - switch profileFeedDataSources[indexPath] { - case .feedCategoryItem: - return CGSize(width: (collectionView.frame.size.width / 2) - 4, height: 243) - case .feedCateogryEmptyItem: - return CGSize(width: collectionView.frame.size.width, height: collectionView.frame.size.height) - } - - } - - public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { - return UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) - } - - public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { - return 3 - } - - public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { - return 16 - } - -} - // 기본 이미지가 true 이고 닉네임 변경 할 경우 redraw extension ProfileViewController { private func setupProfileImage(url: URL) { diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileFeedViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileFeedViewReactor.swift new file mode 100644 index 000000000..9e5f50b6a --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileFeedViewReactor.swift @@ -0,0 +1,179 @@ +// +// ProfileFeedViewReactor.swift +// App +// +// Created by Kim dohyun on 5/4/24. +// + +import Foundation + +import Core +import Domain +import RxSwift +import RxCocoa +import ReactorKit + +final class ProfileFeedViewReactor: Reactor { + var initialState: State + private let feedUseCase: ProfileFeedUseCaseProtocol + + enum Action { + case reloadFeedItems + case fetchMoreFeedItems + } + + enum Mutation { + case setLoading(Bool) + case setFeedSectionItems([ProfileFeedSectionItem]) + case setFeedItemPage(Int) + case setFeedPaginationItems([PostListData]) + case setFeedItems(PostListPage) + } + + struct State { + var isLoading: Bool + var memberId: String + @Pulse var feedPaginationItems: [PostListData] + @Pulse var feedPage: Int + @Pulse var type: PostType + @Pulse var feedItems: PostListPage? + @Pulse var feedSection: [ProfileFeedSectionModel] + } + + init(feedUseCase: ProfileFeedUseCaseProtocol, type: PostType, memberId: String) { + self.feedUseCase = feedUseCase + self.initialState = State( + isLoading: false, + memberId: memberId, + feedPaginationItems: [], + feedPage: 1, + type: type, + feedItems: nil, + feedSection: [.feedCategory([])] + ) + } + + + func mutate(action: Action) -> Observable { + + var query: PostListQuery = PostListQuery(page: currentState.feedPage, size: 10, date: "", memberId: currentState.memberId, type: currentState.type, sort: .desc) + + switch action { + case .reloadFeedItems: + return feedUseCase.execute(query: query) + .asObservable() + .flatMap { entity -> Observable in + var sectionItem: [ProfileFeedSectionItem] = [] + guard let entity = entity else { return .empty() } + + if entity.postLists.isEmpty { + sectionItem.append( + .feedCateogryEmptyItem( + ProfileFeedEmptyCellReactor( + descrption: "아직 업로드한 사진이 없어요", + resource: "profileEmpty" + ) + ) + ) + } else { + entity.postLists.forEach { + sectionItem.append( + .feedCategoryItem( + ProfileFeedCellReactor( + imageURL: URL(string: $0.imageURL) ?? URL(fileReferenceLiteralResourceName: ""), + emojiCount: "\($0.emojiCount)", + date: $0.time, + commentCount: "\($0.commentCount)", + content: $0.content?.map { "\($0)"} ?? [], + feedType: $0.missionType ?? .none + ) + ) + ) + } + } + return .concat( + .just(.setLoading(false)), + .just(.setFeedPaginationItems(entity.postLists)), + .just(.setFeedItems(entity)), + .just(.setFeedSectionItems(sectionItem)), + .just(.setLoading(true)) + ) + } + case .fetchMoreFeedItems: + print("query page: \(query.page)") + let updatePage = currentState.feedPage + 1 + query.page = updatePage + print("fetch query value: \(query.page)") + return feedUseCase.execute(query: query) + .asObservable() + .withUnretained(self) + .flatMap { owner, entity -> Observable in + var sectionItem: [ProfileFeedSectionItem] = [] + guard let entity = entity else { return .empty() } + + var feedItems: [PostListData] = owner.currentState.feedPaginationItems + feedItems.append(contentsOf: entity.postLists) + + feedItems.forEach { + sectionItem.append( + .feedCategoryItem( + ProfileFeedCellReactor( + imageURL: URL(string: $0.imageURL) ?? URL(fileReferenceLiteralResourceName: ""), + emojiCount: "\($0.emojiCount)", + date: $0.time, + commentCount: "\($0.commentCount)", + content: $0.content?.map { "\($0)"} ?? [], + feedType: $0.missionType ?? .none + ) + ) + ) + } + + return .concat( + .just(.setLoading(false)), + .just(.setFeedItemPage(updatePage)), + .just(.setFeedPaginationItems(feedItems)), + .just(.setFeedSectionItems(sectionItem)), + .just(.setLoading(true)) + ) + } + } + } + + + + + func reduce(state: State, mutation: Mutation) -> State { + var newState = state + + switch mutation { + case let .setLoading(isLoading): + newState.isLoading = isLoading + case let .setFeedItems(feedItems): + newState.feedItems = feedItems + case let .setFeedSectionItems(section): + let sectionIndex = getSection(.feedCategory([])) + newState.feedSection[sectionIndex] = .feedCategory(section) + case let .setFeedItemPage(feedPage): + newState.feedPage = feedPage + case let .setFeedPaginationItems(PaginationItems): + newState.feedPaginationItems = PaginationItems + } + + return newState + } + +} + + +extension ProfileFeedViewReactor { + func getSection(_ section: ProfileFeedSectionModel) -> Int { + var index: Int = 0 + + for i in 0 ..< self.currentState.feedSection.count where self.currentState.feedSection[i].getSectionType() == section.getSectionType() { + index = i + } + + return index + } +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift index f5c812261..e49381d65 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift @@ -11,6 +11,10 @@ import Core import Domain import ReactorKit +public enum BibbiFeedType: Int { + case survival = 0 + case mission = 1 +} public final class ProfileViewReactor: Reactor { public var initialState: State @@ -22,22 +26,16 @@ public final class ProfileViewReactor: Reactor { case viewDidLoad case viewWillAppear case updateNickNameProfile(Data) - case fetchMorePostItems(Bool) case didSelectPHAssetsImage(Data) case didTapInitProfile case didTapSegementControl(BibbiFeedType) - case didTapProfilePost(IndexPath, [ProfilePostResultResponse]) } public enum Mutation { case setLoading(Bool) case setProfilePresingedURL(CameraDisplayImageResponse?) - case setFeedCategroySection([ProfileFeedSectionItem]) - case setFeedResultItems([ProfilePostResultResponse]) case setProfileMemberItems(ProfileMemberResponse?) case setProfileFeedType(BibbiFeedType) - case setProfilePostItems(ProfilePostResponse) - case setProfileData(PostSection.Model, IndexPath) } public struct State { @@ -45,12 +43,7 @@ public final class ProfileViewReactor: Reactor { var memberId: String var isUser: Bool var feedType: BibbiFeedType - @Pulse var profileData: PostSection.Model - @Pulse var selectedIndexPath: IndexPath? - @Pulse var feedResultItem: [ProfilePostResultResponse] - @Pulse var feedSection: [ProfileFeedSectionModel] @Pulse var profileMemberEntity: ProfileMemberResponse? - @Pulse var profilePostEntity: ProfilePostResponse? @Pulse var profilePresingedURLEntity: CameraDisplayImageResponse? } @@ -62,15 +55,7 @@ public final class ProfileViewReactor: Reactor { isLoading: false, memberId: memberId, isUser: isUser, - feedType: .survival, - profileData: PostSection.Model( - model: 0, items: [] - ), - selectedIndexPath: nil, - feedResultItem: [], - feedSection: [.feedCategory([])], - profileMemberEntity: nil, - profilePresingedURLEntity: nil + feedType: .survival ) self.provider = provider @@ -80,8 +65,6 @@ public final class ProfileViewReactor: Reactor { public func mutate(action: Action) -> Observable { //TODO: Keychain, UserDefaults 추가 - var query: ProfilePostQuery = ProfilePostQuery(page: 1, size: 10) - let parameters: ProfilePostDefaultValue = ProfilePostDefaultValue(date: "", memberId: currentState.memberId, type: currentState.feedType.rawValue, sort: "DESC") switch action { case .viewDidLoad: return .concat( @@ -89,28 +72,11 @@ public final class ProfileViewReactor: Reactor { profileUseCase.executeProfileMemberItems(memberId: currentState.memberId) .asObservable() .flatMap { entity -> Observable in - .just(.setProfileMemberItems(entity)) - }, - - profileUseCase.executeProfilePostItems(query: query, parameters: parameters) - .asObservable() - .flatMap { entity -> Observable in - var sectionItem: [ProfileFeedSectionItem] = [] - if entity.results.isEmpty { - sectionItem.append(.feedCateogryEmptyItem(ProfileFeedEmptyCellReactor(descrption: "아직 업로드한 사진이 없어요", resource: "profileEmpty"))) - } else { - entity.results.forEach { - sectionItem.append(.feedCategoryItem(ProfileFeedCellReactor(imageURL: $0.imageUrl, emojiCount: $0.emojiCount, date: $0.createdAt.toDate(with: "yyyy-MM-dd'T'HH:mm:ssZ").relativeFormatter(), commentCount: $0.commentCount, content: $0.content.map { "\($0)"}))) - } - } return .concat( - .just(.setProfilePostItems(entity)), - .just(.setFeedResultItems(entity.results)), - .just(.setFeedCategroySection(sectionItem)), + .just(.setProfileMemberItems(entity)), .just(.setLoading(true)) ) - - } + } ) case let .updateNickNameProfile(nickNameFileData): let nickNameProfileImage: String = "\(nickNameFileData.hashValue).jpg" @@ -149,19 +115,16 @@ public final class ProfileViewReactor: Reactor { }) case .viewWillAppear: - return .concat( - profileUseCase.executeProfileMemberItems(memberId: currentState.memberId) - .asObservable() - .withUnretained(self) - .flatMap { owner ,entity -> Observable in - .concat( - .just(.setLoading(false)), - .just(.setProfileMemberItems(entity)), - .just(.setLoading(true)) - ) - } - - ) + return profileUseCase.executeProfileMemberItems(memberId: currentState.memberId) + .asObservable() + .withUnretained(self) + .flatMap { owner ,entity -> Observable in + .concat( + .just(.setLoading(false)), + .just(.setProfileMemberItems(entity)), + .just(.setLoading(true)) + ) + } case let .didSelectPHAssetsImage(fileData): let profileImage: String = "\(fileData.hashValue).jpg" @@ -198,34 +161,9 @@ public final class ProfileViewReactor: Reactor { } } - - } ) - case let .fetchMorePostItems(isPagination): - query.page += 1 - guard self.currentState.profilePostEntity?.hasNext == true && isPagination else { return .empty() } - return profileUseCase.executeProfilePostItems(query: query, parameters: parameters) - .asObservable() - .flatMap { entity -> Observable in - guard let originalItems = self.currentState.profilePostEntity?.results else { return .empty() } - var paginationItems: [ProfilePostResultResponse] = originalItems - - var sectionItem: [ProfileFeedSectionItem] = [] - paginationItems.append(contentsOf: entity.results) - - paginationItems.forEach { - sectionItem.append(.feedCategoryItem(ProfileFeedCellReactor(imageURL: $0.imageUrl, emojiCount: $0.emojiCount, date: $0.createdAt.toDate(with: "yyyy-MM-dd'T'HH:mm:ssZ").relativeFormatter(), commentCount: $0.commentCount, content: $0.content.map { "\($0)"}))) - } - return .concat( - .just(.setLoading(false)), - .just(.setProfilePostItems(entity)), - .just(.setFeedResultItems(paginationItems)), - .just(.setFeedCategroySection(sectionItem)), - .just(.setLoading(true)) - ) - } case .didTapInitProfile: return profileUseCase.executeDeleteProfileImage(memberId: memberId) .asObservable() @@ -238,64 +176,8 @@ public final class ProfileViewReactor: Reactor { } - - case let .didTapProfilePost(indexPath, postEntity): - guard let profleEntity = currentState.profileMemberEntity else { return .empty() } - var postSection: PostSection.Model = .init(model: 0, items: []) - postEntity.forEach { - postSection.items.append( - .main( - PostListData( - postId: $0.postId, - author: ProfileData(memberId: memberId, profileImageURL: profleEntity.memberImage.absoluteString, name: profleEntity.memberName, dayOfBirth: profleEntity.dayOfBirth), - commentCount: Int($0.commentCount) ?? 0, - emojiCount: Int($0.emojiCount) ?? 0, - imageURL: $0.imageUrl.absoluteString, - content: $0.content, - time: $0.createdAt - ) - ) - ) - } - return .just(.setProfileData(postSection, indexPath)) - - case let .didTapSegementControl(feedType): - let feedQuery: ProfilePostQuery = ProfilePostQuery(page: 1, size: 10) - let feedParameters: ProfilePostDefaultValue = ProfilePostDefaultValue(date: "", memberId: currentState.memberId, type: feedType.rawValue, sort: "DESC") - - return .concat( - .just(.setLoading(false)), - .just(.setProfileFeedType(feedType)), - profileUseCase.executeProfilePostItems(query: feedQuery, parameters: feedParameters) - .asObservable() - .flatMap { entity -> Observable in - var sectionItem: [ProfileFeedSectionItem] = [] - if entity.results.isEmpty { - sectionItem.append(.feedCateogryEmptyItem(ProfileFeedEmptyCellReactor(descrption: "아직 업로드한 사진이 없어요", resource: "profileEmpty"))) - } else { - entity.results.forEach { - sectionItem.append( - .feedCategoryItem( - ProfileFeedCellReactor( - imageURL: $0.imageUrl, - emojiCount: $0.emojiCount, - date: $0.createdAt.toDate(with: "yyyy-MM-dd'T'HH:mm:ssZ").relativeFormatter(), - commentCount: $0.commentCount, - content: $0.content.map {"\($0)"} - ) - ) - ) - } - } - return .concat( - .just(.setProfilePostItems(entity)), - .just(.setFeedResultItems(entity.results)), - .just(.setFeedCategroySection(sectionItem)), - .just(.setLoading(true)) - ) - } - ) + return .just(.setProfileFeedType(feedType)) } } @@ -306,24 +188,14 @@ public final class ProfileViewReactor: Reactor { switch mutation { case let .setLoading(isLoading): newState.isLoading = isLoading - case let .setFeedCategroySection(section): - let sectionIndex = getSection(.feedCategory([])) - newState.feedSection[sectionIndex] = .feedCategory(section) - case let .setProfilePostItems(entity): - newState.profilePostEntity = entity + case let .setProfileMemberItems(entity): provider.profileGlobalState.refreshFamilyMembers() newState.profileMemberEntity = entity case let .setProfilePresingedURL(entity): newState.profilePresingedURLEntity = entity - - case let .setProfileData(profileData, indexPath): - newState.profileData = profileData - newState.selectedIndexPath = indexPath - - case let .setFeedResultItems(feedResultItem): - newState.feedResultItem = feedResultItem + case let .setProfileFeedType(feedType): newState.feedType = feedType } @@ -336,15 +208,6 @@ public final class ProfileViewReactor: Reactor { extension ProfileViewReactor { - func getSection(_ section: ProfileFeedSectionModel) -> Int { - var index: Int = 0 - - for i in 0 ..< self.currentState.feedSection.count where self.currentState.feedSection[i].getSectionType() == section.getSectionType() { - index = i - } - - return index - } func configureProfileOriginalS3URL(url: String) -> String { guard let range = url.range(of: #"[^&?]+"#, options: .regularExpression) else { return "" } diff --git a/14th-team5-iOS/Data/Sources/Post/PostList/DTO/PostListDTO.swift b/14th-team5-iOS/Data/Sources/Post/PostList/DTO/PostListDTO.swift index eab1ddd23..33887f27c 100644 --- a/14th-team5-iOS/Data/Sources/Post/PostList/DTO/PostListDTO.swift +++ b/14th-team5-iOS/Data/Sources/Post/PostList/DTO/PostListDTO.swift @@ -35,6 +35,7 @@ extension PostListDTO { return .init( postId: postId, missionId: missionId, + missionType: type, author: author, commentCount: commentCount, emojiCount: emojiCount, diff --git a/14th-team5-iOS/Data/Sources/Post/PostList/PostListAPI/PostListAPIWorker.swift b/14th-team5-iOS/Data/Sources/Post/PostList/PostListAPI/PostListAPIWorker.swift index 1d8537aa3..7d09acfdb 100644 --- a/14th-team5-iOS/Data/Sources/Post/PostList/PostListAPI/PostListAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Post/PostList/PostListAPI/PostListAPIWorker.swift @@ -69,7 +69,7 @@ extension PostListAPIWorker: PostListRepositoryProtocol { } private func fetchTodayPostList(headers: [APIHeader]?, query: Domain.PostListQuery) -> RxSwift.Single { - let requestDTO = PostListRequestDTO(page: query.page, size: query.size, date: query.date, memberId: nil, sort: query.sort, type: query.type.rawValue) + let requestDTO = PostListRequestDTO(page: query.page, size: query.size, date: query.date, memberId: query.memberId, sort: query.sort, type: query.type.rawValue) let spec = PostListAPIs.fetchPostList.spec return request(spec: spec, headers: headers, parameters: requestDTO) .subscribe(on: Self.queue) diff --git a/14th-team5-iOS/Data/Sources/Profile/DataMapping/ProfilePostDTO.swift b/14th-team5-iOS/Data/Sources/Profile/DataMapping/ProfilePostDTO.swift deleted file mode 100644 index d9f7dcb30..000000000 --- a/14th-team5-iOS/Data/Sources/Profile/DataMapping/ProfilePostDTO.swift +++ /dev/null @@ -1,80 +0,0 @@ -// -// ProfilePostDTO.swift -// Domain -// -// Created by Kim dohyun on 12/26/23. -// - -import Foundation - -import Core -import Domain - - -public struct ProfilePostDTO: Decodable { - public var currentPage: Int - public var totalPage: Int - public var itemPerPage: Int - public var hasNext: Bool - public var results: [ProfilePostResponseDTO] - - - public enum CodingKeys: String, CodingKey { - case currentPage, totalPage, itemPerPage - case hasNext - case results - } - -} - -extension ProfilePostDTO { - - public struct ProfilePostResponseDTO: Decodable { - public var postId: String - public var authorId: String - public var commentCount: Int - public var emojiCount: Int - public var imageUrl: String - public var content: String - public var createdAt: String - public var missionId: String? - public var missionType: String? - - - public enum CodingKeys: String, CodingKey { - case postId, authorId, content, imageUrl, createdAt, missionId - case commentCount, emojiCount - case missionType = "type" - } - } - -} - - -extension ProfilePostDTO { - public func toDomain() -> ProfilePostResponse { - return .init( - currentPage: currentPage, - totalPage: totalPage, - itemPerPage: itemPerPage, - hasNext: hasNext, - results: results.map { $0.toDomain() } - ) - } -} - -extension ProfilePostDTO.ProfilePostResponseDTO { - public func toDomain() -> ProfilePostResultResponse { - return .init( - postId: postId, - authorId: authorId, - commentCount: "\(commentCount)", - emojiCount: "\(emojiCount)", - imageUrl: URL(string: imageUrl) ?? URL(fileURLWithPath: ""), - content: content, - createdAt: createdAt, - missionId: missionId ?? "", - missionType: missionType ?? "" - ) - } -} diff --git a/14th-team5-iOS/Data/Sources/Profile/ProfileAPI/ProfileAPIWorker.swift b/14th-team5-iOS/Data/Sources/Profile/ProfileAPI/ProfileAPIWorker.swift index c93d2bc8b..25d5a2f93 100644 --- a/14th-team5-iOS/Data/Sources/Profile/ProfileAPI/ProfileAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Profile/ProfileAPI/ProfileAPIWorker.swift @@ -49,23 +49,6 @@ extension ProfileAPIWorker { } - public func fetchProfilePost(accessToken: String, parameter: ProfilePostParameter) -> Single { - let spec = ProfileAPIs.profilePost.spec - print("check FetchProfile post and url \(spec.url)") - - return request(spec: spec, headers: [BibbiAPI.Header.xAppVersion, BibbiAPI.Header.xUserPlatform ,BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.xAuthToken(accessToken)], parameters: parameter) - .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("fetch Profile Post Result: \(str)") - } - } - .map(ProfilePostDTO.self) - .catchAndReturn(nil) - .asSingle() - - } - public func createProfileImagePresingedURL(accessToken: String, parameters: Encodable) -> Single { let spec = ProfileAPIs.profileAlbumUploadImageURL.spec diff --git a/14th-team5-iOS/Data/Sources/Profile/Repositories/ProfileViewRepository.swift b/14th-team5-iOS/Data/Sources/Profile/Repositories/ProfileViewRepository.swift index 699e703b8..29f745cfe 100644 --- a/14th-team5-iOS/Data/Sources/Profile/Repositories/ProfileViewRepository.swift +++ b/14th-team5-iOS/Data/Sources/Profile/Repositories/ProfileViewRepository.swift @@ -34,24 +34,6 @@ extension ProfileViewRepository: ProfileViewInterface { .asObservable() } - public func fetchProfilePostItems(query: ProfilePostQuery, parameter: ProfilePostDefaultValue) -> Observable { - - - let parameters: ProfilePostParameter = ProfilePostParameter( - page: query.page, - size: query.size, - date: parameter.date, - type: parameter.type, - memberId: parameter.memberId, - sort: parameter.sort - ) - - - return profileAPIWorker.fetchProfilePost(accessToken: accessToken, parameter: parameters) - .compactMap { $0?.toDomain() } - .asObservable() - } - public func fetchProfileAlbumImageURL(parameter: CameraDisplayImageParameters) -> Observable { return profileAPIWorker.createProfileImagePresingedURL(accessToken: accessToken, parameters: parameter) diff --git a/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/Colors/Black.colorset/Contents.json b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/Colors/Black.colorset/Contents.json index 8035c3349..198980a10 100644 --- a/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/Colors/Black.colorset/Contents.json +++ b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/Colors/Black.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "39", - "green" : "36", - "red" : "36" + "blue" : "27", + "green" : "24", + "red" : "24" } }, "idiom" : "universal" @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "39", - "green" : "36", - "red" : "36" + "blue" : "27", + "green" : "24", + "red" : "24" } }, "idiom" : "universal" diff --git a/14th-team5-iOS/Domain/Sources/Post/Entities/PostListData.swift b/14th-team5-iOS/Domain/Sources/Post/Entities/PostListData.swift index 0bf0296c8..f7acad4c7 100644 --- a/14th-team5-iOS/Domain/Sources/Post/Entities/PostListData.swift +++ b/14th-team5-iOS/Domain/Sources/Post/Entities/PostListData.swift @@ -12,14 +12,16 @@ public struct PostListData: Equatable, Hashable { public let author: ProfileData? public var commentCount: Int public let missionId: String? + public let missionType: String? public let emojiCount: Int public let imageURL: String public let content: String? public let time: String - public init(postId: String, missionId: String? = nil, author: ProfileData?, commentCount: Int, emojiCount: Int, imageURL: String, content: String?, time: String) { + public init(postId: String, missionId: String? = nil, missionType: String? = nil, author: ProfileData?, commentCount: Int, emojiCount: Int, imageURL: String, content: String?, time: String) { self.postId = postId self.missionId = missionId + self.missionType = missionType self.author = author self.commentCount = commentCount self.emojiCount = emojiCount diff --git a/14th-team5-iOS/Domain/Sources/Post/Entities/PostListQuery.swift b/14th-team5-iOS/Domain/Sources/Post/Entities/PostListQuery.swift index a71c9c085..1196057e7 100644 --- a/14th-team5-iOS/Domain/Sources/Post/Entities/PostListQuery.swift +++ b/14th-team5-iOS/Domain/Sources/Post/Entities/PostListQuery.swift @@ -38,10 +38,10 @@ public enum PostType: String { } public struct PostListQuery { - public let page: Int + public var page: Int public let size: Int public let date: String - public let memberId: String + public let memberId: String? public let type: PostType public let sort: String @@ -49,7 +49,7 @@ public struct PostListQuery { page: Int = 1, size: Int = 256, date: String, - memberId: String = "", + memberId: String? = nil, type: PostType = .survival, sort: Sort = .desc ) { diff --git a/14th-team5-iOS/Domain/Sources/Profile/Entity/ProfilePostResponse.swift b/14th-team5-iOS/Domain/Sources/Profile/Entity/ProfilePostResponse.swift deleted file mode 100644 index 74d1fcdfa..000000000 --- a/14th-team5-iOS/Domain/Sources/Profile/Entity/ProfilePostResponse.swift +++ /dev/null @@ -1,51 +0,0 @@ -// -// ProfilePostResponse.swift -// Domain -// -// Created by Kim dohyun on 12/26/23. -// - -import Foundation - - -public struct ProfilePostResponse { - - public var currentPage: Int - public var totalPage: Int - public var itemPerPage: Int - public var hasNext: Bool - public var results: [ProfilePostResultResponse] - - public init(currentPage: Int, totalPage: Int, itemPerPage: Int, hasNext: Bool, results: [ProfilePostResultResponse]) { - self.currentPage = currentPage - self.totalPage = totalPage - self.itemPerPage = itemPerPage - self.hasNext = hasNext - self.results = results - } - -} - -public struct ProfilePostResultResponse { - public var postId: String - public var authorId: String - public var commentCount: String - public var emojiCount: String - public var imageUrl: URL - public var content: String - public var createdAt: String - public var missionId: String - public var missionType: String - - public init(postId: String, authorId: String, commentCount: String, emojiCount: String, imageUrl: URL, content: String, createdAt: String, missionId: String, missionType: String) { - self.postId = postId - self.authorId = authorId - self.commentCount = commentCount - self.emojiCount = emojiCount - self.imageUrl = imageUrl - self.content = content - self.createdAt = createdAt - self.missionId = missionId - self.missionType = missionType - } -} diff --git a/14th-team5-iOS/Domain/Sources/Profile/Interfaces/ProfileViewInterface.swift b/14th-team5-iOS/Domain/Sources/Profile/Interfaces/ProfileViewInterface.swift index 37945a5d4..ee7b0608a 100644 --- a/14th-team5-iOS/Domain/Sources/Profile/Interfaces/ProfileViewInterface.swift +++ b/14th-team5-iOS/Domain/Sources/Profile/Interfaces/ProfileViewInterface.swift @@ -28,7 +28,6 @@ public protocol ProfileViewInterface: AnyObject { var disposeBag: DisposeBag { get } func fetchProfileMemberItems(memberId: String) -> Observable - func fetchProfilePostItems(query: ProfilePostQuery, parameter: ProfilePostDefaultValue) -> Observable func fetchProfileAlbumImageURL(parameter: CameraDisplayImageParameters) -> Observable func uploadProfileImageToPresingedURL(to url: String, imageData: Data) -> Observable func updataProfileImageToS3(memberId: String, parameter: ProfileImageEditParameter) -> Observable diff --git a/14th-team5-iOS/Domain/Sources/Profile/Parameters/ProfilePostParameter.swift b/14th-team5-iOS/Domain/Sources/Profile/Parameters/ProfilePostParameter.swift deleted file mode 100644 index d428e9e05..000000000 --- a/14th-team5-iOS/Domain/Sources/Profile/Parameters/ProfilePostParameter.swift +++ /dev/null @@ -1,58 +0,0 @@ -// -// ProfilePostParameter.swift -// Domain -// -// Created by Kim dohyun on 12/26/23. -// - -import Foundation - -//TODO: BibbiFeedType (임시로 넣어둠 홈이랑 공유 해서 사용하기에 논의후 위치 변경) -public enum BibbiFeedType: String { - case survival = "SURVIVAL" - case mission = "MISSION" -} - -public struct ProfilePostParameter: Encodable { - public var page: Int - public var size: Int - public var date: String - public var memberId: String - public var type: String - public var sort: String - - public init(page: Int, size: Int, date: String, type: String, memberId: String, sort: String) { - self.page = page - self.size = size - self.date = date - self.type = type - self.memberId = memberId - self.sort = sort - } -} - -public struct ProfilePostDefaultValue: Encodable { - public var date: String - public var memberId: String - public var type: String - public var sort: String - - public init(date: String, memberId: String, type: String, sort: String) { - self.date = date - self.memberId = memberId - self.type = type - self.sort = sort - } - -} - -public struct ProfilePostQuery: Encodable { - public var page: Int - public var size: Int - - public init(page: Int, size: Int) { - self.page = page - self.size = size - } -} - diff --git a/14th-team5-iOS/Domain/Sources/Profile/UseCases/ProfileFeedUseCase.swift b/14th-team5-iOS/Domain/Sources/Profile/UseCases/ProfileFeedUseCase.swift new file mode 100644 index 000000000..819867687 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Profile/UseCases/ProfileFeedUseCase.swift @@ -0,0 +1,35 @@ +// +// ProfileFeedUseCase.swift +// Domain +// +// Created by Kim dohyun on 5/4/24. +// + +import Foundation + +import RxSwift +import RxCocoa + + +public protocol ProfileFeedUseCaseProtocol { + func execute(query: PostListQuery) -> Observable +} + + + +public final class ProfileFeedUseCase: ProfileFeedUseCaseProtocol { + + private let missionFeedRepository: PostListRepositoryProtocol + + public init(missionFeedRepository: PostListRepositoryProtocol) { + self.missionFeedRepository = missionFeedRepository + } + + public func execute(query: PostListQuery) -> RxSwift.Observable { + return missionFeedRepository.fetchTodayPostList(query: query) + .asObservable() + } + + + +} diff --git a/14th-team5-iOS/Domain/Sources/Profile/UseCases/ProfileViewUseCase.swift b/14th-team5-iOS/Domain/Sources/Profile/UseCases/ProfileViewUseCase.swift index fc0341af7..34e3aba83 100644 --- a/14th-team5-iOS/Domain/Sources/Profile/UseCases/ProfileViewUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/Profile/UseCases/ProfileViewUseCase.swift @@ -12,7 +12,6 @@ import RxSwift public protocol ProfileViewUsecaseProtocol { func executeProfileMemberItems(memberId: String) -> Observable - func executeProfilePostItems(query: ProfilePostQuery, parameters: ProfilePostDefaultValue) -> Observable func executeProfileImageURLCreate(parameter: CameraDisplayImageParameters) -> Observable func executeProfileImageToPresingedUpload(to url: String, data: Data) -> Observable func executeReloadProfileImage(memberId: String, parameter: ProfileImageEditParameter) -> Observable @@ -31,10 +30,6 @@ public final class ProfileViewUseCase: ProfileViewUsecaseProtocol { return profileViewRepository.fetchProfileMemberItems(memberId: memberId) } - public func executeProfilePostItems(query: ProfilePostQuery, parameters: ProfilePostDefaultValue) -> Observable { - return profileViewRepository.fetchProfilePostItems(query: query, parameter: parameters) - } - public func executeProfileImageURLCreate(parameter: CameraDisplayImageParameters) -> Observable { return profileViewRepository.fetchProfileAlbumImageURL(parameter: parameter) } From c81090494d69322a57971432010ce4f5488471c3 Mon Sep 17 00:00:00 2001 From: Do-hyun-Kim Date: Mon, 6 May 2024 15:31:44 +0900 Subject: [PATCH 009/263] =?UTF-8?q?feat:=20ProfileFeedViewReactor=20PostDe?= =?UTF-8?q?tail=20=EC=83=81=EC=84=B8=20=ED=99=94=EB=A9=B4=20=EC=A0=84?= =?UTF-8?q?=ED=99=98=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80=20-=20Profi?= =?UTF-8?q?leFeedViewController=20PreFetch=20=EA=B3=BC=EC=A0=95=20?= =?UTF-8?q?=EC=A4=91=EB=B3=B5=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EB=B0=9C?= =?UTF-8?q?=EC=83=9D=20=EC=9D=B4=EC=8A=88=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Profile/ProfileFeedViewController.swift | 26 +++++++++++- .../Profile/ProfileViewController.swift | 2 + .../Reactor/ProfileFeedViewReactor.swift | 42 ++++++++++++++++--- 3 files changed, 64 insertions(+), 6 deletions(-) diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileFeedViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileFeedViewController.swift index 41a33a010..78f54b770 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileFeedViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileFeedViewController.swift @@ -85,7 +85,7 @@ final class ProfileFeedViewController: BaseViewController public override func setupUI() { super.setupUI() + + addChild(profileFeedViewController) view.addSubviews(profileView, profileLineView, profileFeedViewController.view, profileSegementControl, profileIndicatorView) } diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileFeedViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileFeedViewReactor.swift index 9e5f50b6a..23ce97bfe 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileFeedViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileFeedViewReactor.swift @@ -20,6 +20,7 @@ final class ProfileFeedViewReactor: Reactor { enum Action { case reloadFeedItems case fetchMoreFeedItems + case didTapProfileFeedItem(IndexPath, [PostListData]) } enum Mutation { @@ -28,11 +29,14 @@ final class ProfileFeedViewReactor: Reactor { case setFeedItemPage(Int) case setFeedPaginationItems([PostListData]) case setFeedItems(PostListPage) + case setFeedDetailItem(PostSection.Model, IndexPath) } struct State { var isLoading: Bool var memberId: String + @Pulse var selectedIndex: IndexPath? + @Pulse var feedDetailItem: PostSection.Model @Pulse var feedPaginationItems: [PostListData] @Pulse var feedPage: Int @Pulse var type: PostType @@ -45,6 +49,8 @@ final class ProfileFeedViewReactor: Reactor { self.initialState = State( isLoading: false, memberId: memberId, + selectedIndex: nil, + feedDetailItem: .init(model: 0, items: []), feedPaginationItems: [], feedPage: 1, type: type, @@ -82,7 +88,7 @@ final class ProfileFeedViewReactor: Reactor { ProfileFeedCellReactor( imageURL: URL(string: $0.imageURL) ?? URL(fileReferenceLiteralResourceName: ""), emojiCount: "\($0.emojiCount)", - date: $0.time, + date: $0.time.toDate(with: "yyyy-MM-dd'T'HH:mm:ssZ").relativeFormatter(), commentCount: "\($0.commentCount)", content: $0.content?.map { "\($0)"} ?? [], feedType: $0.missionType ?? .none @@ -100,10 +106,8 @@ final class ProfileFeedViewReactor: Reactor { ) } case .fetchMoreFeedItems: - print("query page: \(query.page)") let updatePage = currentState.feedPage + 1 query.page = updatePage - print("fetch query value: \(query.page)") return feedUseCase.execute(query: query) .asObservable() .withUnretained(self) @@ -120,7 +124,7 @@ final class ProfileFeedViewReactor: Reactor { ProfileFeedCellReactor( imageURL: URL(string: $0.imageURL) ?? URL(fileReferenceLiteralResourceName: ""), emojiCount: "\($0.emojiCount)", - date: $0.time, + date: $0.time.toDate(with: "yyyy-MM-dd'T'HH:mm:ssZ").relativeFormatter(), commentCount: "\($0.commentCount)", content: $0.content?.map { "\($0)"} ?? [], feedType: $0.missionType ?? .none @@ -131,12 +135,37 @@ final class ProfileFeedViewReactor: Reactor { return .concat( .just(.setLoading(false)), - .just(.setFeedItemPage(updatePage)), .just(.setFeedPaginationItems(feedItems)), + .just(.setFeedItemPage(updatePage)), .just(.setFeedSectionItems(sectionItem)), .just(.setLoading(true)) ) } + case let .didTapProfileFeedItem(indexPath, feedItems): + var feedDetailSection: PostSection.Model = .init(model: 0, items: []) + + feedItems.forEach { + feedDetailSection.items.append( + .main(PostListData( + postId: $0.postId, + missionId: $0.missionId, + missionType: $0.missionType, + author: ProfileData( + memberId: currentState.memberId, + profileImageURL: $0.author?.profileImageURL, + name: $0.author?.name ?? ""), + commentCount: $0.commentCount, + emojiCount: $0.emojiCount, + imageURL: $0.imageURL, + content: $0.content, + time: $0.time + ) + ) + ) + } + + + return .just(.setFeedDetailItem(feedDetailSection, indexPath)) } } @@ -158,6 +187,9 @@ final class ProfileFeedViewReactor: Reactor { newState.feedPage = feedPage case let .setFeedPaginationItems(PaginationItems): newState.feedPaginationItems = PaginationItems + case let .setFeedDetailItem(feedDetailItem, selectedIndex): + newState.feedDetailItem = feedDetailItem + newState.selectedIndex = selectedIndex } return newState From 35ce05b09da72edc98a6d81058a8a2e770bafad2 Mon Sep 17 00:00:00 2001 From: Kyoungmi <1mmgm.dev@gmail.com> Date: Sat, 4 May 2024 16:16:51 +0900 Subject: [PATCH 010/263] delete file --- ...354\202\254\353\263\270 2024-03-01).swift" | 68 ------------------- 1 file changed, 68 deletions(-) delete mode 100644 "14th-team5-iOS/App/Project (\352\271\200\352\261\264\354\232\260\354\235\230 \354\266\251\353\217\214\353\220\234 \354\202\254\353\263\270 2024-03-01).swift" diff --git "a/14th-team5-iOS/App/Project (\352\271\200\352\261\264\354\232\260\354\235\230 \354\266\251\353\217\214\353\220\234 \354\202\254\353\263\270 2024-03-01).swift" "b/14th-team5-iOS/App/Project (\352\271\200\352\261\264\354\232\260\354\235\230 \354\266\251\353\217\214\353\220\234 \354\202\254\353\263\270 2024-03-01).swift" deleted file mode 100644 index c59af0867..000000000 --- "a/14th-team5-iOS/App/Project (\352\271\200\352\261\264\354\232\260\354\235\230 \354\266\251\353\217\214\353\220\234 \354\202\254\353\263\270 2024-03-01).swift" +++ /dev/null @@ -1,68 +0,0 @@ -// -// Project.swift -// ProjectDescriptionHelpers -// -// Created by Kim dohyun on 2023/11/14. -// - -import ProjectDescription -import ProjectDescriptionHelpers - -private let targets: [Target] = [ - .makeModular( - layer: .App, - factory: .init( - products: .app, - dependencies: ModuleLayer.App.dependencies, - bundleId: "com.5ing.bibbi", - infoPlist: .extendingDefault(with: [ - "CFBundleDisplayName": .string("Bibbi"), - "CFBundleVersion": .string("1"), - "CFBuildVersion": .string("0"), - "CFBundleShortVersionString": .string("1.1.7"), - "UILaunchStoryboardName": .string("LaunchScreen.storyboard"), - "UISupportedInterfaceOrientations": .array([.string("UIInterfaceOrientationPortrait")]), - "UIUserInterfaceStyle": .string("Light"), - "NSPhotoLibraryAddUsageDescription" : .string("프로필 사진, 피드 업로드를 위한 사진 촬영을 위해 Bibbi가 앨범에 접근할 수 있도록 허용해 주세요"), - "NSCameraUsageDescription": .string("프로필 사진, 피드 업로드를 위한 사진 촬영을 위해 Bibbi가 카메라에 접근할 수 있도록 허용해 주세요"), - "UIApplicationSceneManifest" : .dictionary([ - "UIApplicationSupportsMultipleScenes" : .boolean(false), - "UISceneConfigurations" : .dictionary([ - "UIWindowSceneSessionRoleApplication" : .array([ - .dictionary([ - "UISceneConfigurationName" : .string("Default Configuration"), - "UISceneDelegateClassName" : .string("$(PRODUCT_MODULE_NAME).SceneDelegate") - ]) - ]) - ]) - ]), - "LSApplicationQueriesSchemes": .array([.string("kakaokompassauth"), .string("kakaolink")]), - "CFBundleURLTypes": .array([ - .dictionary([ - "CFBundleURLSchemes": .array([.string("$(KAKAO_API_KEY)")]), - ]), - ]), - "KAKAO_LOGIN_API_KEY": .string("$(KAKAO_LOGIN_API_KEY)"), - "MIXPANEL_API_KEY": .string("$(MIXPANEL_API_KEY)"), - "TEAM_ID": .string("$(TEAM_ID)"), - ]), - entitlements: .relativeToRoot("App.entitlements") - ) - ), - .makeModular(extenions: .Widget, factory: .init( - products: .appExtension, - dependencies: ExtensionsLayer.Widget.dependencies, - bundleId: "com.5ing.bibbi.widget", - infoPlist: .extendingDefault(with: [ - "CFBundleDisplayName": .string("Bibbi"), - "NSExtension" : .dictionary([ - "NSExtensionPointIdentifier": .string("com.apple.widgetkit-extension") - ]) - ]), - entitlements: .relativeToRoot("WidgetExtension.entitlements") - ) -) -] - - -private let app = Project.makeApp(name: ModuleLayer.App.rawValue, target: targets) From de04951fa52c4d7c96980104b13506bc71cc4485 Mon Sep 17 00:00:00 2001 From: Kyoungmi <1mmgm.dev@gmail.com> Date: Sun, 5 May 2024 14:17:54 +0900 Subject: [PATCH 011/263] [feat]: set text(#501) --- .../Dependency/DescriptionDIContainer.swift | 30 ---- .../Home/Reactor/DescriptionReactor.swift | 103 ------------ .../Home/Reactor/MainCameraReactor.swift | 28 ++-- .../Home/Reactor/MainViewReactor.swift | 108 +++++++++++-- .../ViewControllers/MainViewController.swift | 152 +++++------------- .../Home/Views/DescriptionView.swift | 97 ----------- .../Home/Views/MainCameraButtonView.swift | 6 +- .../Core/Sources/Popover/BalloonView.swift | 3 +- .../Post/PostList/DTO/PostListDTO.swift | 4 +- .../PostListAPI/PostListAPIWorker.swift | 13 +- .../View/Main/DTO/MainResponseDTO.swift | 5 +- .../Sources/Main/Entities/MainData.swift | 17 +- .../Sources/Post/Entities/PostListData.swift | 6 +- .../Post/UseCases/PostListUseCase.swift | 4 +- 14 files changed, 177 insertions(+), 399 deletions(-) delete mode 100644 14th-team5-iOS/App/Sources/Presentation/Home/Dependency/DescriptionDIContainer.swift delete mode 100644 14th-team5-iOS/App/Sources/Presentation/Home/Reactor/DescriptionReactor.swift delete mode 100644 14th-team5-iOS/App/Sources/Presentation/Home/Views/DescriptionView.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Dependency/DescriptionDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Dependency/DescriptionDIContainer.swift deleted file mode 100644 index 6d93dc1a0..000000000 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Dependency/DescriptionDIContainer.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// DescriptionDIContainer.swift -// App -// -// Created by 마경미 on 21.04.24. -// - -import Foundation - -import Data -import Domain - -final class DescriptionDIContainer { - func makeView() -> DescriptionView { - return DescriptionView(reactor: makeReactor()) - } - - func makeReactor() -> DescriptionReactor { - return DescriptionReactor(missionUseCase: makeUseCase()) - } -} - -extension DescriptionDIContainer { - private func makeRepository() -> MissionRepositoryProtocol { - return MissionRepository() - } - private func makeUseCase() -> GetTodayMissionUseCaseProtocol { - return GetTodayMissionUseCase(missionRepository: makeRepository()) - } -} diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/DescriptionReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/DescriptionReactor.swift deleted file mode 100644 index 6b411e994..000000000 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/DescriptionReactor.swift +++ /dev/null @@ -1,103 +0,0 @@ -// -// DescriptionReacto.swift -// App -// -// Created by 마경미 on 21.04.24. -// - -import Foundation - -import Core -import Domain - -import ReactorKit - -enum SurvivalDescription: String { - case none = "매일 12-24시에 사진 한 장을 올려요" - case full = "우리 가족 모두가 사진을 올린 날" -} - -enum MissionDescription { - case none(Int) - case some(String) - case full - - var description: String { - switch self { - case .none(let count): - return "가족 중 \(count)명만 더 올리면 미션 열쇠를 받아요!" - case .some(let string): - return string - case .full: - return "우리 가족 모두가 미션을 성공한 날" - } - } -} - -final class DescriptionReactor: Reactor { - enum Action { - case setPostType(PostType) - case getMission - } - - enum Mutation { - case setDescriptionText(String) - case setMission(String?) - case setPostType(PostType) - } - - struct State { - var postType: PostType = .survival - var description: String = "매일 12-24시에 사진 한 장을 올려요" - var mission: String = "가족 중 2명만 더 올리면 미션 열쇠를 받아요!" - } - - let initialState: State = State() - let missionUseCase: GetTodayMissionUseCaseProtocol - - init(missionUseCase: GetTodayMissionUseCaseProtocol) { - self.missionUseCase = missionUseCase - } -} - -extension DescriptionReactor { - func mutate(action: Action) -> Observable { - switch action { - case .setPostType(let type): - if type == .mission { - return Observable.from([ - Observable.just(Mutation.setPostType(type)), - self.mutate(action: .getMission) - ]).flatMap { $0 } - } else { - return Observable.just(.setPostType(type)) - } - case .getMission: - return missionUseCase.execute() - .asObservable() - .flatMap { - return Observable.from([ - .setDescriptionText($0?.content ?? "미션을 불러오는데 실패하였습니다."), - .setMission($0?.content) - ]) - } - - } - } - - func reduce(state: State, mutation: Mutation) -> State { - var newState = state - - switch mutation { - case .setPostType(let type): - newState.postType = type - case .setDescriptionText(let str): - newState.description = str - case .setMission(let mission): - guard let mission else { return newState} - newState.mission = mission - } - - return newState - } -} diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainCameraReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainCameraReactor.swift index 5e272c38c..b4272a46b 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainCameraReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainCameraReactor.swift @@ -11,24 +11,22 @@ import Domain import ReactorKit +enum BalloonText: String { + case survivalStandard = "하루에 한번 사진을 올릴 수 있어요" + case cantMission = "아직 미션 사진을 찍을 수 없어요" + case canMission = "미션 사진을 찍으러 가볼까요?" +} + final class MainCameraReactor: Reactor { - enum BalloonText: String { - case survivalStandard = "하루에 한번 사진을 올릴 수 있어요" - case cantMission = "아직 미션 사진을 찍을 수 없어요" - case canMission = "미션 사진을 찍으러 가볼까요?" - } - enum Action { - case getType(Int) - case setText + case setText(BalloonText) } enum Mutation { - case setType(PostType) + case setText(BalloonText) } struct State { - var type: PostType = .survival var balloonText: BalloonText = .survivalStandard } @@ -38,10 +36,8 @@ final class MainCameraReactor: Reactor { extension MainCameraReactor { func mutate(action: Action) -> Observable { switch action { - case .getType(let index): - Observable.just(.setType(PostType.getPostType(index: index))) - case .setText: - Observable.empty() + case .setText(let text): + return Observable.just(.setText(text)) } } @@ -49,8 +45,8 @@ extension MainCameraReactor { var newState = state switch mutation { - case .setType(let type): - newState.type = type + case .setText(let text): + newState.balloonText = text } return newState diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift index 7435d7e52..2177a373a 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift @@ -5,18 +5,55 @@ // Created by 마경미 on 05.12.23. // -import Foundation +import UIKit import Core import Domain +import DesignSystem import ReactorKit import RxDataSources import Kingfisher + +enum Description { + case survivalNone + case survivalFull + case missionNone(Int) + case mission(String) + case missionFull + + var text: String { + switch self { + case .survivalNone: + return "매일 12-24시에 사진 한 장을 올려요" + case .survivalFull: + return "우리 가족 모두가 사진을 올린 날" + case .missionNone(let count): + return "가족 중 \(count)명만 더 올리면 미션 열쇠를 받아요!" + case .mission(let string): + return string + case .missionFull: + return "우리 가족 모두가 미션을 성공한 날" + } + } + + var image: UIImage { + switch self { + case .survivalNone, .mission: + return DesignSystemAsset.smile.image + case .missionFull, .survivalFull: + return DesignSystemAsset.congratulation.image + case .missionNone: + return DesignSystemAsset.missionKeyGraphic.image + } + } +} + final class MainViewReactor: Reactor { + enum Action { - case viewDidLoad + case calculateTime case fetchMainUseCase case didTapSegmentControl(PostType) @@ -31,8 +68,9 @@ final class MainViewReactor: Reactor { case updateMainData(MainData) case setInTime(Bool) - case setPageIndex(Int) + case setBalloonText + case setDescriptionText case setPickSuccessToastMessage(String) case setCopySuccessToastMessage @@ -48,16 +86,22 @@ final class MainViewReactor: Reactor { struct State { var isInTime: Bool var pageIndex: Int = 0 + var leftCount: Int = 0 + var missionText: String = "" + var balloonText: BalloonText = .survivalStandard + var description: Description = .survivalNone + + var isFamilySurvivalUploadedToday: Bool = false + var isFamilyMissionUploadedToday: Bool = false + var isMeSurvivalUploadedToday: Bool = false + var isMissionUnlocked: Bool = false - @Pulse var isMeSurvivalUploadedToday: Bool = false - @Pulse var isMissionUnlocked: Bool = false @Pulse var familySection: [FamilySection.Item] = [] - @Pulse var widgetPostDeepLink: WidgetDeepLink? @Pulse var notificationPostDeepLink: NotificationDeepLink? @Pulse var notificationCommentDeepLink: NotificationDeepLink? - + @Pulse var shouldPresentPickAlert: (String, String)? @Pulse var shouldPresentPickSuccessToastMessage: String? @Pulse var shouldPresentCopySuccessToastMessage: Bool = false @@ -115,9 +159,9 @@ extension MainViewReactor { guard let data = result else { return Observable.empty() } - return Observable.just(.updateMainData(data)) + return Observable.concat(Observable.just(.updateMainData(data)), Observable.just(.setBalloonText)) } - case .viewDidLoad: + case .calculateTime: let (_, time) = MainViewReactor.calculateRemainingTime() if self.currentState.isInTime { @@ -132,7 +176,7 @@ extension MainViewReactor { .flatMap {_ in return Observable.concat([Observable.just(Mutation.setInTime(true))]) } - + } case let .pushWidgetPostDeepLink(deepLink): return Observable.concat( @@ -149,7 +193,11 @@ extension MainViewReactor { Observable.just(.setNotificationCommentDeepLink(deepLink)) // 다음 화면으로 이동하기 ) case .didTapSegmentControl(let type): - return Observable.just(.setPageIndex(type.getIndex())) + return Observable.concat( + Observable.just(.setPageIndex(type.getIndex())), + Observable.just(.setBalloonText), + Observable.just(.setDescriptionText)) + case let .pickConfirmButtonTapped(name, id): return pickUseCase.executePickMember(memberId: id) @@ -187,6 +235,10 @@ extension MainViewReactor { case .updateMainData(let data): newState.isMissionUnlocked = data.isMissionUnlocked newState.isMeSurvivalUploadedToday = data.isMeSurvivalUploadedToday + newState.isFamilyMissionUploadedToday = data.isFamilyMissionUploadedToday + newState.isFamilySurvivalUploadedToday = data.isFamilySurvivalUploadedToday + newState.leftCount = data.leftUploadCountUntilMissionUnlock + newState.missionText = data.dailyMissionContent newState.familySection = FamilySection.Model( model: 0, items: data.mainFamilyProfileDatas.map { @@ -195,6 +247,34 @@ extension MainViewReactor { ).items case let .setPickAlertView(name, id): newState.shouldPresentPickAlert = (name, id) + case .setBalloonText: + if currentState.pageIndex == 0 { + newState.balloonText = .survivalStandard + } else { + if currentState.isMissionUnlocked { + newState.balloonText = .cantMission + } else { + newState.balloonText = .canMission + } + } + case .setDescriptionText: + if currentState.pageIndex == 0 { + if currentState.isFamilySurvivalUploadedToday { + newState.description = .survivalFull + } else { + newState.description = .survivalNone + } + } else { + if currentState.isMissionUnlocked { + newState.description = .missionNone(currentState.leftCount) + } else { + if currentState.isFamilyMissionUploadedToday { + newState.description = .missionFull + } else { + newState.description = .mission(currentState.missionText) + } + } + } } return newState @@ -207,7 +287,7 @@ extension MainViewReactor { let currentTime = Date() let currentHour = calendar.component(.hour, from: currentTime) - + if currentHour >= 12 { 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) @@ -219,8 +299,8 @@ extension MainViewReactor { return (false, max(0, timeDifference.second ?? 0)) } } - + return (false, 0) } - + } 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 ffbbcfdd4..b905319a0 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift @@ -19,14 +19,14 @@ final class MainViewController: BaseViewController, UICollectio private let familyViewController: MainFamilyViewController = MainFamilyViewDIContainer().makeViewController() private let timerView: TimerView = TimerDIContainer().makeView() - private let descriptionView: DescriptionView = DescriptionDIContainer().makeView() + private let descriptionLabel: BibbiLabel = BibbiLabel(.body2Regular, textAlignment: .center, textColor: .gray300) + private let imageView: UIImageView = UIImageView() private let segmentControl: BibbiSegmentedControl = BibbiSegmentedControl(isUpdated: true) private let pageViewController: SegmentPageViewController = SegmentPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal) - lazy var cameraButton: MainCameraButtonView = MainCameraDIContainer().makeView() + private let cameraButton: MainCameraButtonView = MainCameraDIContainer().makeView() - // MARK: - Properties private let memberRepo = App.Repository.member private let deepLinkRepo = App.Repository.deepLink @@ -50,8 +50,9 @@ final class MainViewController: BaseViewController, UICollectio addChild(familyViewController) addChild(pageViewController) - view.addSubviews(familyViewController.view, timerView, descriptionView, segmentControl, - pageViewController.view, cameraButton) + view.addSubviews(familyViewController.view, timerView, descriptionLabel, + imageView, segmentControl, pageViewController.view, + cameraButton) familyViewController.didMove(toParent: self) pageViewController.didMove(toParent: self) @@ -72,14 +73,20 @@ final class MainViewController: BaseViewController, UICollectio $0.horizontalEdges.equalTo(view.safeAreaLayoutGuide) } - descriptionView.snp.makeConstraints { + descriptionLabel.snp.makeConstraints { $0.top.equalTo(timerView.snp.bottom).offset(8) - $0.horizontalEdges.equalToSuperview() $0.height.equalTo(20) + $0.centerX.equalToSuperview() + } + + imageView.snp.makeConstraints { + $0.top.equalTo(descriptionLabel) + $0.size.equalTo(20) + $0.leading.equalTo(descriptionLabel.snp.trailing).offset(2) } segmentControl.snp.makeConstraints { - $0.top.equalTo(descriptionView.snp.bottom).offset(20) + $0.top.equalTo(descriptionLabel.snp.bottom).offset(20) $0.centerX.equalToSuperview() $0.width.equalTo(138) $0.height.equalTo(40) @@ -109,18 +116,13 @@ extension MainViewController { private func bindInput(reactor: MainViewReactor) { Observable.merge( Observable.just(()) - .map { Reactor.Action.viewDidLoad }, + .map { Reactor.Action.fetchMainUseCase }, NotificationCenter.default.rx.notification(UIScene.willEnterForegroundNotification) - .map { _ in Reactor.Action.viewDidLoad } + .map { _ in Reactor.Action.fetchMainUseCase } ) .bind(to: reactor.action) .disposed(by: disposeBag) - Observable.just(()) - .map { Reactor.Action.fetchMainUseCase } - .bind(to: reactor.action) - .disposed(by: disposeBag) - // self.rx.viewWillAppear // .withUnretained(self) // // 별도 딥링크를 받지 않으면 @@ -133,16 +135,14 @@ extension MainViewController { // .bind(to: reactor.action) // .disposed(by: disposeBag) - segmentControl - .survivalButton.rx.tap - .throttle(.milliseconds(300), scheduler: MainScheduler.instance) + segmentControl.survivalButton.rx.tap + .throttle(.milliseconds(1000), scheduler: MainScheduler.instance) .map { Reactor.Action.didTapSegmentControl(.survival) } .bind(to: reactor.action) .disposed(by: disposeBag) - segmentControl - .missionButton.rx.tap - .throttle(.milliseconds(300), scheduler: MainScheduler.instance) + segmentControl.missionButton.rx.tap + .throttle(.milliseconds(1000), scheduler: MainScheduler.instance) .map { Reactor.Action.didTapSegmentControl(.mission) } .bind(to: reactor.action) .disposed(by: disposeBag) @@ -159,7 +159,10 @@ extension MainViewController { .bind { $0.0.navigationController?.pushViewController(CalendarDIConatainer().makeViewController(), animated: true) } .disposed(by: disposeBag) - + alertConfirmRelay + .map { Reactor.Action.pickConfirmButtonTapped($0.0, $0.1) } + .bind(to: reactor.action) + .disposed(by: disposeBag) cameraButton.camerTapObservable .throttle(RxConst.throttleInterval, scheduler: MainScheduler.instance) @@ -195,20 +198,12 @@ extension MainViewController { } private func bindOutput(reactor: MainViewReactor) { - reactor.pulse(\.$familySection) .distinctUntilChanged() .observe(on: MainScheduler.instance) .bind(to: familyViewController.familySectionRelay) .disposed(by: disposeBag) -// reactor.pulse(\.$isSelfUploaded) -// .distinctUntilChanged() -// .observe(on: MainScheduler.instance) -// .withUnretained(self) -// .bind(onNext: { $0.0.hideCameraButton($0.1) }) -// .disposed(by: disposeBag) - reactor.state.map { $0.pageIndex } .distinctUntilChanged() .withUnretained(self) @@ -216,7 +211,21 @@ extension MainViewController { .bind(onNext: { $0.0.pageViewController.indexRelay.accept($0.1) $0.0.segmentControl.isSelected = ($0.1 == 0) - $0.0.descriptionView.postTypeRelay.accept(($0.1 == 0) ? .survival : .mission) + }) + .disposed(by: disposeBag) + + reactor.state.map { $0.balloonText } + .distinctUntilChanged() + .bind(to: cameraButton.textRelay) + .disposed(by: disposeBag) + + reactor.state.map { $0.description } + .distinctUntilChanged { $0.text } + .withUnretained(self) + .observe(on: MainScheduler.instance) + .bind(onNext: { + $0.0.descriptionLabel.text = $0.1.text + $0.0.imageView.image = $0.1.image }) .disposed(by: disposeBag) @@ -226,18 +235,12 @@ extension MainViewController { BibbiAlertBuilder(owner) .alertStyle(.pickMember(profile.0)) .setConfirmAction { - // (name, memberId) owner.alertConfirmRelay.accept((profile.0, profile.1)) } .present() } .disposed(by: disposeBag) - alertConfirmRelay - .map { Reactor.Action.pickConfirmButtonTapped($0.0, $0.1) } - .bind(to: reactor.action) - .disposed(by: disposeBag) - reactor.pulse(\.$shouldPresentPickSuccessToastMessage) .compactMap { $0 } .bind(with: self) { owner, name in @@ -293,78 +296,3 @@ extension MainViewController { // .disposed(by: disposeBag) } } - -//extension MainViewController { -// private func handlePostWidgetDeepLink(_ deepLink: WidgetDeepLink) { -// guard let reactor = reactor else { return } -// reactor.currentState.postSection.items.enumerated().forEach { (index, item) in -// switch item { -// case .main(let postListData): -// if postListData.postId == deepLink.postId { -// let indexPath = IndexPath(row: index, section: 0) -// self.navigationController?.pushViewController( -// PostListsDIContainer().makeViewController( -// postLists: reactor.currentState.postSection, -// selectedIndex: indexPath), -// animated: true -// ) -// } -// } -// } -// } -// -// private func handlePostNotificationDeepLink(_ deepLink: NotificationDeepLink) { -// guard let reactor = reactor else { return } -// reactor.currentState.postSection.items.enumerated().forEach { (index, item) in -// switch item { -// case .main(let post): -// if post.postId == deepLink.postId { -// let indexPath = IndexPath(row: index, section: 0) -// self.navigationController?.pushViewController( -// PostListsDIContainer().makeViewController( -// postLists: reactor.currentState.postSection, -// selectedIndex: indexPath), -// animated: true -// ) -// } -// } -// } -// } -// -// private func handleCommentNotificationDeepLink(_ deepLink: NotificationDeepLink) { -// guard let reactor = reactor else { return } -// -// // 오늘 올린 피드에 댓글이 달렸다면 -// if deepLink.dateOfPost.isToday { -// guard let selectedIndex = reactor.currentState.postSection.items.firstIndex(where: { postList in -// switch postList { -// case let .main(post): -// post.postId == deepLink.postId -// } -// }) else { return } -// let indexPath = IndexPath(row: selectedIndex, section: 0) -// -// let postListViewController = PostListsDIContainer().makeViewController( -// postLists: reactor.currentState.postSection, -// selectedIndex: indexPath, -// notificationDeepLink: deepLink -// ) -// -// navigationController?.pushViewController( -// postListViewController, -// animated: true -// ) -// // 이전에 올린 피드에 댓글이 달렸다면 -// } else { -// let calendarPostViewController = CalendarPostDIConatainer( -// selectedDate: deepLink.dateOfPost, -// notificationDeepLink: deepLink -// ).makeViewController() -// -// navigationController?.pushViewController( -// calendarPostViewController, -// animated: true -// ) -// } -// } -//} diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Views/DescriptionView.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Views/DescriptionView.swift deleted file mode 100644 index f33b7ce75..000000000 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Views/DescriptionView.swift +++ /dev/null @@ -1,97 +0,0 @@ -// -// DescriptionView.swift -// App -// -// Created by 마경미 on 21.04.24. -// - -import UIKit - -import Core -import Domain -import DesignSystem - -import RxSwift -import RxCocoa -// -//fileprivate extension TimerType { -// var title: String { -// switch self { -// case .standard: return "매일 12-24시에 사진 한 장을 올려요" -// case .widget: return "위젯을 추가하면 더 빠르게 사진을 볼 수 있어요" -// case .warning: return "시간이 얼마 남지 않았어요!" -// case .allUploaded: return "우리 가족 모두가 사진을 올린 날" -// } -// } -// -// var image: UIImage { -// switch self { -// case .standard: return DesignSystemAsset.smile.image -// case .widget: return DesignSystemAsset.widget.image -// case .warning: return DesignSystemAsset.fire.image -// case .allUploaded: return DesignSystemAsset.congratulation.image -// } -// } -// -// var timerTextColor: UIColor { -// switch self { -// case .warning: return .warningRed -// default: return UIColor.white -// } -// } -// -// var descTextColor: UIColor { -// switch self { -// case .warning: return .warningRed -// default: return .gray300 -// } -// } -//} - -final class DescriptionView: BaseView { - private let descriptionLabel: BibbiLabel = BibbiLabel(.body2Regular, textAlignment: .center, textColor: .gray300) - private let imageView: UIImageView = UIImageView() - - let postTypeRelay: BehaviorRelay = BehaviorRelay(value: .survival) - - override func bind(reactor: DescriptionReactor) { - bindInput(reactor: reactor) - bindOutput(reactor: reactor) - } - - override func setupUI() { - addSubviews(descriptionLabel, imageView) - } - - override func setupAutoLayout() { - descriptionLabel.snp.makeConstraints { - $0.top.equalToSuperview().inset(8) - $0.height.equalTo(20) - $0.centerX.equalToSuperview() - } - - imageView.snp.makeConstraints { - $0.top.equalTo(descriptionLabel) - $0.size.equalTo(20) - $0.leading.equalTo(descriptionLabel.snp.trailing).offset(2) - } - } -} - -extension DescriptionView { - private func bindInput(reactor: DescriptionReactor) { - postTypeRelay - .distinctUntilChanged() - .map { Reactor.Action.setPostType($0) } - .bind(to: reactor.action) - .disposed(by: disposeBag) - } - - private func bindOutput(reactor: DescriptionReactor) { - reactor.state.map { $0.description } - .compactMap { $0 } - .distinctUntilChanged() - .bind(to: descriptionLabel.rx.text) - .disposed(by: disposeBag) - } -} diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Views/MainCameraButtonView.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Views/MainCameraButtonView.swift index 669c2ae6e..21b1ac2c6 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Views/MainCameraButtonView.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Views/MainCameraButtonView.swift @@ -17,7 +17,7 @@ final class MainCameraButtonView: BaseView { private let balloonView: BalloonView = BalloonView() private let cameraButton: UIButton = UIButton() - let indexRelay: BehaviorRelay = BehaviorRelay(value: 0) + let textRelay: BehaviorRelay = BehaviorRelay(value: .survivalStandard) var camerTapObservable: ControlEvent { return cameraButton.rx.tap } @@ -53,8 +53,8 @@ final class MainCameraButtonView: BaseView { extension MainCameraButtonView { private func bindInput(reactor: MainCameraReactor) { - indexRelay - .map { Reactor.Action.getType($0) } + textRelay + .map { Reactor.Action.setText($0) } .bind(to: reactor.action) .disposed(by: disposeBag) } diff --git a/14th-team5-iOS/Core/Sources/Popover/BalloonView.swift b/14th-team5-iOS/Core/Sources/Popover/BalloonView.swift index 0fd341a3d..ee48032e3 100644 --- a/14th-team5-iOS/Core/Sources/Popover/BalloonView.swift +++ b/14th-team5-iOS/Core/Sources/Popover/BalloonView.swift @@ -66,7 +66,8 @@ public class BalloonView: UIView { public override func draw(_ rect: CGRect) { super.draw(rect) - let tipLeft = rect.origin.x + (rect.size.width / 2.0) - 6 + let screenSize = UIScreen.main.bounds.size + let tipLeft = (screenSize.width / 2.0) - 6 let tipBottom = CGPoint(x: rect.origin.x + (rect.size.width / 2.0), y: rect.size.height) let heightWithoutTip = 40 diff --git a/14th-team5-iOS/Data/Sources/Post/PostList/DTO/PostListDTO.swift b/14th-team5-iOS/Data/Sources/Post/PostList/DTO/PostListDTO.swift index eab1ddd23..40c2e33e2 100644 --- a/14th-team5-iOS/Data/Sources/Post/PostList/DTO/PostListDTO.swift +++ b/14th-team5-iOS/Data/Sources/Post/PostList/DTO/PostListDTO.swift @@ -54,7 +54,7 @@ struct PostListResponseDTO: Codable { } extension PostListResponseDTO { - func toDomain(_ selfUploaded: Bool, _ allFamilyMembersUploaded: Bool) -> PostListPage { - return .init(isLast: !hasNext, postLists: results.map { $0.toDomain() }, allFamilyMembersUploaded: allFamilyMembersUploaded, selfUploaded: selfUploaded) + func toDomain() -> PostListPage { + return .init(isLast: !hasNext, postLists: results.map { $0.toDomain() }) } } diff --git a/14th-team5-iOS/Data/Sources/Post/PostList/PostListAPI/PostListAPIWorker.swift b/14th-team5-iOS/Data/Sources/Post/PostList/PostListAPI/PostListAPIWorker.swift index 05941f2b9..c75867970 100644 --- a/14th-team5-iOS/Data/Sources/Post/PostList/PostListAPI/PostListAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Post/PostList/PostListAPI/PostListAPIWorker.swift @@ -72,18 +72,7 @@ extension PostListAPIWorker: PostListRepositoryProtocol { } .map(PostListResponseDTO.self) .map { - let myMemberId = FamilyUserDefaults.getMyMemberId() - let familyCount = FamilyUserDefaults.getMemberCount() - - let selfUploaded = $0?.results.map { $0.authorId == myMemberId }.contains(true) ?? false - let familyUploaded = $0?.results.count == familyCount - - if selfUploaded { - let repository = PostUserDefaultsRepository() - repository.checkUploadDate(date: query.date) - } - - return $0?.toDomain(selfUploaded, familyUploaded) + return $0?.toDomain() } .catchAndReturn(nil) .asSingle() diff --git a/14th-team5-iOS/Data/Sources/View/Main/DTO/MainResponseDTO.swift b/14th-team5-iOS/Data/Sources/View/Main/DTO/MainResponseDTO.swift index 9aa762889..8914fa517 100644 --- a/14th-team5-iOS/Data/Sources/View/Main/DTO/MainResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/View/Main/DTO/MainResponseDTO.swift @@ -73,6 +73,9 @@ struct MainResponseDTO: Codable { isMissionUnlocked: isMissionUnlocked, isMeSurvivalUploadedToday: isMeSurvivalUploadedToday, isMeMissionUploadedToday: isMeMissionUploadedToday, - pickers: pickers.map { $0.toDomain() }) + pickers: pickers.map { $0.toDomain() }, + survivalUploadCount: survivalFeeds.count, + missionUploadCount: missionFeeds.count, + dailyMissionContent: dailyMissionContent) } } diff --git a/14th-team5-iOS/Domain/Sources/Main/Entities/MainData.swift b/14th-team5-iOS/Domain/Sources/Main/Entities/MainData.swift index 993b0628a..9bfcd7976 100644 --- a/14th-team5-iOS/Domain/Sources/Main/Entities/MainData.swift +++ b/14th-team5-iOS/Domain/Sources/Main/Entities/MainData.swift @@ -22,18 +22,33 @@ public struct Picker { public struct MainData { public let mainFamilyProfileDatas: [ProfileData] public let leftUploadCountUntilMissionUnlock: Int + public let isFamilySurvivalUploadedToday: Bool + public let isFamilyMissionUploadedToday: Bool public let isMissionUnlocked: Bool public let isMeSurvivalUploadedToday: Bool public let isMeMissionUploadedToday: Bool + public let dailyMissionContent: String public let pickers: [Picker] - public init(mainFamilyProfileDatas: [ProfileData], leftUploadCountUntilMissionUnlock: Int, isMissionUnlocked: Bool, isMeSurvivalUploadedToday: Bool, isMeMissionUploadedToday: Bool, pickers: [Picker]) { + public init( + mainFamilyProfileDatas: [ProfileData], + leftUploadCountUntilMissionUnlock: Int, + isMissionUnlocked: Bool, + isMeSurvivalUploadedToday: Bool, + isMeMissionUploadedToday: Bool, + pickers: [Picker], + survivalUploadCount: Int, + missionUploadCount: Int, + dailyMissionContent: String) { self.mainFamilyProfileDatas = mainFamilyProfileDatas self.leftUploadCountUntilMissionUnlock = leftUploadCountUntilMissionUnlock self.isMissionUnlocked = isMissionUnlocked self.isMeSurvivalUploadedToday = isMeSurvivalUploadedToday self.isMeMissionUploadedToday = isMeMissionUploadedToday self.pickers = pickers + self.dailyMissionContent = dailyMissionContent + self.isFamilySurvivalUploadedToday = survivalUploadCount == mainFamilyProfileDatas.count + self.isFamilyMissionUploadedToday = missionUploadCount == mainFamilyProfileDatas.count } diff --git a/14th-team5-iOS/Domain/Sources/Post/Entities/PostListData.swift b/14th-team5-iOS/Domain/Sources/Post/Entities/PostListData.swift index 0bf0296c8..7fb7f168c 100644 --- a/14th-team5-iOS/Domain/Sources/Post/Entities/PostListData.swift +++ b/14th-team5-iOS/Domain/Sources/Post/Entities/PostListData.swift @@ -32,13 +32,9 @@ public struct PostListData: Equatable, Hashable { public struct PostListPage: Equatable { public let isLast: Bool public let postLists: [PostListData] - public let allFamilyMembersUploaded: Bool - public let selfUploaded: Bool - public init(isLast: Bool, postLists: [PostListData], allFamilyMembersUploaded: Bool, selfUploaded: Bool) { + public init(isLast: Bool, postLists: [PostListData]) { self.isLast = isLast self.postLists = postLists - self.allFamilyMembersUploaded = allFamilyMembersUploaded - self.selfUploaded = selfUploaded } } diff --git a/14th-team5-iOS/Domain/Sources/Post/UseCases/PostListUseCase.swift b/14th-team5-iOS/Domain/Sources/Post/UseCases/PostListUseCase.swift index 1a014e20b..172b3492d 100644 --- a/14th-team5-iOS/Domain/Sources/Post/UseCases/PostListUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/Post/UseCases/PostListUseCase.swift @@ -9,7 +9,7 @@ import Foundation import RxSwift public protocol PostListUseCaseProtocol { - func excute(query: PostQuery) -> Single + func executePost(query: PostQuery) -> Single func excute(query: PostListQuery) -> Single func excute() -> Single } @@ -27,7 +27,7 @@ public class PostListUseCase: PostListUseCaseProtocol { return postListRepository.fetchTodayPostList(query: query) } - public func excute(query: PostQuery) -> Single { + public func executePost(query: PostQuery) -> Single { return postListRepository.fetchPostDetail(query: query) } From fbf7b85028e66a022f82c00e5f37f807cfe38549 Mon Sep 17 00:00:00 2001 From: Kyoungmi <1mmgm.dev@gmail.com> Date: Sun, 5 May 2024 14:18:18 +0900 Subject: [PATCH 012/263] [feat]: add deepLink manager(#501) --- .../App/Sources/Application/AppDelegate.swift | 2 +- .../Sources/Application/DeepLinkManager.swift | 115 ++++++++++++++++++ .../Sources/Application/SceneDelegate.swift | 2 + 3 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 14th-team5-iOS/App/Sources/Application/DeepLinkManager.swift diff --git a/14th-team5-iOS/App/Sources/Application/AppDelegate.swift b/14th-team5-iOS/App/Sources/Application/AppDelegate.swift index 0a18f949e..e1dcac4c3 100644 --- a/14th-team5-iOS/App/Sources/Application/AppDelegate.swift +++ b/14th-team5-iOS/App/Sources/Application/AppDelegate.swift @@ -115,7 +115,6 @@ extension AppDelegate: MixpanelDelegate { } extension AppDelegate { - func appleApp(_ app: UIApplication, didFinishLauchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) { guard let accessToken = App.Repository.token.accessToken.value?.accessToken else { @@ -171,6 +170,7 @@ extension AppDelegate: UNUserNotificationCenterDelegate { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { App.Repository.deepLink.notification.accept(deepLink) + DeepLinkManager.shared.handleDeepLink(deepLink) } completionHandler() diff --git a/14th-team5-iOS/App/Sources/Application/DeepLinkManager.swift b/14th-team5-iOS/App/Sources/Application/DeepLinkManager.swift new file mode 100644 index 000000000..c5468b8b4 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/DeepLinkManager.swift @@ -0,0 +1,115 @@ +// +// DeepLinkManager.swift +// App +// +// Created by 마경미 on 05.05.24. +// + +import UIKit + +import Core +import Data +import Domain + +import RxSwift + +class DeepLinkManager { + static let shared = DeepLinkManager() + + // 이번 3차 끝나고, postdetailviewcontroller에서 post 불러오는 형태로 바꿔보겠습니다. + let disposeBag: DisposeBag = DisposeBag() + let postRepository: PostListRepositoryProtocol = PostListAPIs.Worker() + lazy var postUseCase: PostListUseCaseProtocol = PostListUseCase(postListRepository: postRepository) + + private init() {} + + func handleDeepLink(_ deepLink: NotificationDeepLink) { + } + + func handlePostWidgetDeepLink(_ deepLink: WidgetDeepLink) { + let query = PostListQuery(date: DateFormatter.dashYyyyMMdd.string(from: Date()), type: .survival) + postUseCase.excute(query: query) + .subscribe(onSuccess: { [weak self] result in + guard let self = self, let result = result else { return } + + let items = result.postLists.map(PostSection.Item.main) + + items.enumerated().forEach { (index, item) in + switch item { + case .main(let postListData): + if postListData.postId == deepLink.postId { + let indexPath = IndexPath(row: index, section: 0) + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let keyWindow = windowScene.windows.first(where: { $0.isKeyWindow }), + let navigationController = keyWindow.rootViewController as? UINavigationController { + let viewController = PostListsDIContainer().makeViewController( + postLists: PostSection.Model(model: 0, items: items), + selectedIndex: IndexPath(row: index, section: 0) + ) + navigationController.pushViewController(viewController, animated: true) + } + } + } + } + }, onFailure: { error in + // 에러 처리 + }) + .disposed(by: disposeBag) + } + + +// private func handlePostNotificationDeepLink(_ deepLink: NotificationDeepLink) { +// guard let reactor = reactor else { return } +// reactor.currentState.postSection.items.enumerated().forEach { (index, item) in +// switch item { +// case .main(let post): +// if post.postId == deepLink.postId { +// let indexPath = IndexPath(row: index, section: 0) +// self.navigationController?.pushViewController( +// PostListsDIContainer().makeViewController( +// postLists: reactor.currentState.postSection, +// selectedIndex: indexPath), +// animated: true +// ) +// } +// } +// } +// } +// +// private func handleCommentNotificationDeepLink(_ deepLink: NotificationDeepLink) { +// guard let reactor = reactor else { return } +// +// // 오늘 올린 피드에 댓글이 달렸다면 +// if deepLink.dateOfPost.isToday { +// guard let selectedIndex = reactor.currentState.postSection.items.firstIndex(where: { postList in +// switch postList { +// case let .main(post): +// post.postId == deepLink.postId +// } +// }) else { return } +// let indexPath = IndexPath(row: selectedIndex, section: 0) +// +// let postListViewController = PostListsDIContainer().makeViewController( +// postLists: reactor.currentState.postSection, +// selectedIndex: indexPath, +// notificationDeepLink: deepLink +// ) +// +// navigationController?.pushViewController( +// postListViewController, +// animated: true +// ) +// // 이전에 올린 피드에 댓글이 달렸다면 +// } else { +// let calendarPostViewController = CalendarPostDIConatainer( +// selectedDate: deepLink.dateOfPost, +// notificationDeepLink: deepLink +// ).makeViewController() +// +// navigationController?.pushViewController( +// calendarPostViewController, +// animated: true +// ) +// } +// } +} diff --git a/14th-team5-iOS/App/Sources/Application/SceneDelegate.swift b/14th-team5-iOS/App/Sources/Application/SceneDelegate.swift index 6485ec367..6318c98bd 100644 --- a/14th-team5-iOS/App/Sources/Application/SceneDelegate.swift +++ b/14th-team5-iOS/App/Sources/Application/SceneDelegate.swift @@ -29,6 +29,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { if let url = connectionOptions.urlContexts.first?.url { if let deepLink = decodeWidgetDeepLink(url) { App.Repository.deepLink.widget.accept(deepLink) + DeepLinkManager.shared.handlePostWidgetDeepLink(deepLink) } self.window = UIWindow(windowScene: scene) @@ -71,6 +72,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // 위젯 딥링크라면 if let deepLink = decodeWidgetDeepLink(url) { App.Repository.deepLink.widget.accept(deepLink) + DeepLinkManager.shared.handlePostWidgetDeepLink(deepLink) } } } From e1ac81ed41e00170b60df2445fa92d497e6b33ec Mon Sep 17 00:00:00 2001 From: Do-hyun-Kim Date: Mon, 6 May 2024 15:59:26 +0900 Subject: [PATCH 013/263] =?UTF-8?q?feat:=20swift.yml=20startsWIth=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=EB=A1=9C=20=EC=88=98=EC=A0=95=20-?= =?UTF-8?q?=20Project.swift=20CFBundleShortVersionString=20Major=EB=A1=9C?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/swift.yml | 12 ++++++------ 14th-team5-iOS/App/Project.swift | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index f5065fab5..b11b37051 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -59,8 +59,8 @@ jobs: - name: Tuist Generate Commnad run: tuist generate - - - name: fastlane upload_stg_testflight + + - name: fastlane upload_prd_testflight if: github.event.pull_request.base.ref == 'release' && github.head_ref == 'develop' env: APP_STORE_CONNECT_API_KEY_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY_ID }} @@ -78,11 +78,11 @@ jobs: APPLE_ID: ${{secrets.APPLE_ID}} TEAM_ID: ${{secrets.TEAM_ID}} WIDGET_NAME: ${{secrets.WIDGET_NAME}} - run: fastlane github_action_stg_upload_testflight + run: fastlane github_action_prd_upload_testflight - - name: fastlane upload_prd_testflight - if: github.event.pull_request.base.ref == 'develop' && github.head_ref == 'feat' + - name: fastlane upload_stg_testflight + if: github.event.pull_request.base.ref == 'develop' && startsWith(github.head_ref, 'feat/') 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 }} @@ -99,7 +99,7 @@ jobs: APPLE_ID: ${{secrets.APPLE_ID}} TEAM_ID: ${{secrets.TEAM_ID}} WIDGET_NAME: ${{secrets.WIDGET_NAME}} - run: fastlane github_action_prd_upload_testflight + run: fastlane github_action_stg_upload_testflight - name: Upload coverage to Codecov uses: codecov/codecov-action@v1.2.1 diff --git a/14th-team5-iOS/App/Project.swift b/14th-team5-iOS/App/Project.swift index 861faa1e9..c14954a65 100644 --- a/14th-team5-iOS/App/Project.swift +++ b/14th-team5-iOS/App/Project.swift @@ -19,7 +19,7 @@ private let targets: [Target] = [ "CFBundleDisplayName": .string("Bibbi"), "CFBundleVersion": .string("1"), "CFBuildVersion": .string("0"), - "CFBundleShortVersionString": .string("1.2"), + "CFBundleShortVersionString": .string("2.0"), "UILaunchStoryboardName": .string("LaunchScreen.storyboard"), "UISupportedInterfaceOrientations": .array([.string("UIInterfaceOrientationPortrait")]), "UIUserInterfaceStyle": .string("Light"), From 584b076ebaf0e5396e995c2e39c6d8c6ccff54c3 Mon Sep 17 00:00:00 2001 From: Do-hyun-Kim Date: Mon, 6 May 2024 16:41:47 +0900 Subject: [PATCH 014/263] =?UTF-8?q?feat:=20swift.yml=20multiple=20scheme?= =?UTF-8?q?=20issue=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/swift.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index b11b37051..1a3df593b 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -69,7 +69,7 @@ jobs: PROJECT_PATH: ${{ secrets.PROJECT_PATH }} MATCH_PASSWORD: ${{secrets.MATCH_PASSWORD}} MATCH_PERSONAL_TOKEN: ${{ secrets.MATCH_PERSONAL_TOKEN}} - DEV_SCHEME: ${{secrets.DEV_SCHEME}} + PRD_SCHEME: ${{secrets.PRD_SCHEME}} BUNDLE_ID: ${{secrets.BUNDLE_ID}} SLACK_HOOK_URL: ${{secrets.SLACK_HOOK_URL}} WIDGET_BUNDLE_ID: ${{secrets.WIDGET_BUNDLE_ID}} @@ -91,7 +91,7 @@ jobs: MATCH_PASSWORD: ${{secrets.MATCH_PASSWORD}} SLACK_HOOK_URL: ${{secrets.SLACK_HOOK_URL}} MATCH_PERSONAL_TOKEN: ${{ secrets.MATCH_PERSONAL_TOKEN}} - PRD_SCHEME: ${{secrets.PRD_SCHEME}} + DEV_SCHEME: ${{secrets.DEV_SCHEME}} BUNDLE_ID: ${{secrets.BUNDLE_ID}} WIDGET_BUNDLE_ID: ${{secrets.WIDGET_BUNDLE_ID}} PROFILE_PATH: ${{secrets.PROFILE_PATH}} From b5f43ae994aec73628d3a832c8ae97e604a1cdd3 Mon Sep 17 00:00:00 2001 From: Do-hyun-Kim Date: Mon, 6 May 2024 17:24:19 +0900 Subject: [PATCH 015/263] =?UTF-8?q?fix:=20CFBundleShortVersionString=20Min?= =?UTF-8?q?or=20Version=EC=9C=BC=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 14th-team5-iOS/App/Project.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/14th-team5-iOS/App/Project.swift b/14th-team5-iOS/App/Project.swift index c14954a65..861faa1e9 100644 --- a/14th-team5-iOS/App/Project.swift +++ b/14th-team5-iOS/App/Project.swift @@ -19,7 +19,7 @@ private let targets: [Target] = [ "CFBundleDisplayName": .string("Bibbi"), "CFBundleVersion": .string("1"), "CFBuildVersion": .string("0"), - "CFBundleShortVersionString": .string("2.0"), + "CFBundleShortVersionString": .string("1.2"), "UILaunchStoryboardName": .string("LaunchScreen.storyboard"), "UISupportedInterfaceOrientations": .array([.string("UIInterfaceOrientationPortrait")]), "UIUserInterfaceStyle": .string("Light"), From 6571cc9f356c12b2763b5f323fd1f8f5f0a10c1f Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Tue, 7 May 2024 14:40:35 +0900 Subject: [PATCH 016/263] =?UTF-8?q?refactor:=20CalendarPostCell=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EB=B6=84=EB=A6=AC=20(#500)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DataSource/CalendarPostSectionModel.swift | 13 + .../MonthlyCalendarSectionModel.swift | 15 +- .../CalendarPostCellDIContainer.swift | 44 ++++ .../Dependency/CalendarPostDIContainer.swift | 9 - .../Reactor/CalendarPostCellReactor.swift | 112 ++++++++ .../Reactor/CalendarPostViewReactor.swift | 30 +-- .../Reactor/CalendarViewReactor.swift | 8 +- .../Calendar/View/Cell/CalendarPostCell.swift | 246 ++++++++++++++++++ .../CalendarPostViewController.swift | 42 ++- .../CalendarViewController.swift | 6 +- .../CalendarAPI/CalendarAPIWorker.swift | 4 +- ...rrayResponseDailyCalendarResponseDTO.swift | 10 +- .../Repository/CalendarRepository.swift | 4 +- .../Member/Repository/MemberRepository.swift | 4 +- .../Sources/Post/PostList/DTO/PostDTO.swift | 2 +- .../Post/PostList/DTO/PostListDTO.swift | 2 +- .../UserDefaults/FamilyUserDefautls.swift | 2 +- .../icons/mission 1.imageset/Contents.json | 12 + .../icons/mission 1.imageset/mission.svg | 19 ++ .../ArrayResponseDailyCalendarEntity.swift | 1 + .../Repositories/CalendarRepository.swift | 2 +- .../Calendar/UseCases/CalendarUseCase.swift | 5 + 22 files changed, 519 insertions(+), 73 deletions(-) create mode 100644 14th-team5-iOS/App/Sources/Presentation/Calendar/DataSource/CalendarPostSectionModel.swift create mode 100644 14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/CalendarPostCellDIContainer.swift create mode 100644 14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPostCellReactor.swift create mode 100644 14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarPostCell.swift create mode 100644 14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/mission 1.imageset/Contents.json create mode 100644 14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/mission 1.imageset/mission.svg diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/DataSource/CalendarPostSectionModel.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/DataSource/CalendarPostSectionModel.swift new file mode 100644 index 000000000..9cf30f097 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/DataSource/CalendarPostSectionModel.swift @@ -0,0 +1,13 @@ +// +// WeeklyCalendarSectionModel.swift +// App +// +// Created by 김건우 on 5/7/24. +// + +import Domain +import Foundation + +import Differentiator + +typealias CalendarPostSectionModel = SectionModel diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/DataSource/MonthlyCalendarSectionModel.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/DataSource/MonthlyCalendarSectionModel.swift index 5a91ada45..d2a7121f8 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/DataSource/MonthlyCalendarSectionModel.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/DataSource/MonthlyCalendarSectionModel.swift @@ -8,17 +8,6 @@ import Domain import Foundation -import RxDataSources +import Differentiator -public struct SectionOfMonthlyCalendar { - public var items: [Item] -} - -extension SectionOfMonthlyCalendar: SectionModelType { - public typealias Item = String - - public init(original: SectionOfMonthlyCalendar, items: [Item]) { - self = original - self.items = items - } -} +typealias MonthlyCalendarSectionModel = SectionModel diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/CalendarPostCellDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/CalendarPostCellDIContainer.swift new file mode 100644 index 000000000..494729540 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/CalendarPostCellDIContainer.swift @@ -0,0 +1,44 @@ +// +// CalendarPostCellDIContainer.swift +// App +// +// Created by 김건우 on 5/7/24. +// + +import Core +import Data +import Domain +import UIKit + +public final class CalendarPostCellDIContainer { + // MARK: - Properties + let post: DailyCalendarEntity + + private var globalState: GlobalStateProviderProtocol { + guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { + return GlobalStateProvider() + } + return appDelegate.globalStateProvider + } + + init(post: DailyCalendarEntity) { + self.post = post + } + + // MARK: - Make + public func makeMeUseCase() -> MemberUseCaseProtocol { + return MemberUseCase(memberRepository: makeMeRepository()) + } + + public func makeMeRepository() -> MemberRepositoryProtocol { + return MemberRepository() + } + + public func makeReactor() -> CalendarPostCellReactor { + return CalendarPostCellReactor( + post: post, + meUseCase: makeMeUseCase(), + provider: globalState + ) + } +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/CalendarPostDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/CalendarPostDIContainer.swift index 429145340..c00505511 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/CalendarPostDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/CalendarPostDIContainer.swift @@ -41,24 +41,15 @@ public final class CalendarPostDIConatainer { return CalendarUseCase(calendarRepository: makeCalendarRepository()) } - public func makePostListUseCase() -> PostListUseCaseProtocol { - return PostListUseCase(postListRepository: makePostListRepository()) - } - public func makeCalendarRepository() -> CalendarRepositoryProtocol { return CalendarRepository() } - public func makePostListRepository() -> PostListRepositoryProtocol { - return PostListAPIWorker() - } - public func makeReactor() -> CalendarPostViewReactor { return CalendarPostViewReactor( selectedDate, notificationDeepLink: notificationDeepLink, calendarUseCase: makeCalendarUseCase(), - postListUseCase: makePostListUseCase(), provider: globalState ) } diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPostCellReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPostCellReactor.swift new file mode 100644 index 000000000..b0fc9e7fb --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPostCellReactor.swift @@ -0,0 +1,112 @@ +// +// CalendarPostCellReactor.swift +// App +// +// Created by 김건우 on 5/7/24. +// + +import Core +import Domain +import Foundation + +import ReactorKit + +public final class CalendarPostCellReactor: Reactor { + + // MARK: - Action + public enum Action { + case displayContent + case requestAuthorName + case requestAuthorImageUrl + case writerImageButtonTapped + } + + // MARK: - Mutation + public enum Mutation { + case setAuthorName(String) + case setAuthorImageUrl(String) + case setContent([DisplayEditItemModel]) + } + + // MARK: - State + public struct State { + var post: DailyCalendarEntity + var authorName: String? + var authorImageUrl: String? + var content: [DisplayEditSectionModel]? + } + + // MARK: - Properties + public var initialState: State + + public let meUseCase: MemberUseCaseProtocol + public let provider: GlobalStateProviderProtocol + + // MARK: - Intializer + public init( + post: DailyCalendarEntity, + meUseCase: MemberUseCaseProtocol, + provider: GlobalStateProviderProtocol + ) { + self.initialState = State( + post: post + ) + self.meUseCase = meUseCase + self.provider = provider + } + + // MARK: - Mutate + public func mutate(action: Action) -> Observable { + switch action { + case .displayContent: + let content: String = currentState.post.postContent + var sectionItem: [DisplayEditItemModel] = [] + content.forEach { + sectionItem.append( + .fetchDisplayItem( + DisplayEditCellReactor( + title: String($0), + radius: 10, + font: .head2Bold + ) + ) + ) + } + return Observable.just(.setContent(sectionItem)) + + case .requestAuthorName: + let authorId = initialState.post.authorId + let authorName = meUseCase.executeFetchUserName(memberId: authorId) + return Observable.just(.setAuthorName(authorName)) + + case .requestAuthorImageUrl: + let authorId = initialState.post.authorId + let authorImageUrl = meUseCase.executeProfileImageUrlString(memberId: authorId) + return Observable.just(.setAuthorImageUrl(authorImageUrl)) + + case .writerImageButtonTapped: + let authorId = initialState.post.authorId + provider.postGlobalState.pushProfileViewController(authorId) + return Observable.empty() + } + } + + // MARK: - Reduce + public func reduce(state: State, mutation: Mutation) -> State { + var newState = state + + switch mutation { + case let .setAuthorName(name): + newState.authorName = name + + case let .setAuthorImageUrl(url): + newState.authorImageUrl = url + + case let .setContent(section): + newState.content = [.displayKeyword(section)] + } + + return newState + } + +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPostViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPostViewReactor.swift index 6d537c07b..2ff738cb3 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPostViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPostViewReactor.swift @@ -30,9 +30,9 @@ public final class CalendarPostViewReactor: Reactor { public enum Mutation { case setAllUploadedToastMessageView(Bool) case injectCalendarResponse(String, ArrayResponseCalendarEntity) - case injectPostResponse([PostListData]) + case injectPostResponse([DailyCalendarEntity]) case injectBlurImageIndex(Int) - case injectVisiblePost(PostListData) + case injectVisiblePost(DailyCalendarEntity) case renewPostCommentCount(Int) case pushProfileViewController(String) case popViewController @@ -46,8 +46,8 @@ public final class CalendarPostViewReactor: Reactor { var notificationDeepLink: NotificationDeepLink? // 댓글 푸시 알림 체크 변수 var blurImageUrlString: String? - var visiblePost: PostListData? - @Pulse var displayPostResponse: [PostListSectionModel] + var visiblePost: DailyCalendarEntity? + @Pulse var displayPostResponse: [CalendarPostSectionModel] @Pulse var displayCalendarResponse: [String: [CalendarEntity]] @Pulse var shouldPresentAllUploadedToastMessageView: Bool @Pulse var shouldGenerateSelectionHaptic: Bool @@ -60,7 +60,6 @@ public final class CalendarPostViewReactor: Reactor { public let provider: GlobalStateProviderProtocol private let calendarUseCase: CalendarUseCaseProtocol - private let postListUseCase: PostListUseCaseProtocol private var hasReceivedPostEvent: Bool = false private var hasReceivedSelectionEvent: Bool = false @@ -72,7 +71,6 @@ public final class CalendarPostViewReactor: Reactor { _ selection: Date, notificationDeepLink deepLink: NotificationDeepLink?, calendarUseCase: CalendarUseCaseProtocol, - postListUseCase: PostListUseCaseProtocol, provider: GlobalStateProviderProtocol ) { self.initialState = State( @@ -87,7 +85,6 @@ public final class CalendarPostViewReactor: Reactor { ) self.calendarUseCase = calendarUseCase - self.postListUseCase = postListUseCase self.provider = provider } @@ -136,18 +133,15 @@ public final class CalendarPostViewReactor: Reactor { if !hasReceivedPostEvent || hasThumbnailImages.contains(date) { hasReceivedPostEvent = true // 가족이 게시한 포스트 가져오기 - let date: String = date.toFormatString(with: .dashYyyyMMdd) - let postListQuery: PostListQuery = PostListQuery(date: date) - return postListUseCase.excute(query: postListQuery).asObservable() - .withUnretained(self) - .flatMap { - guard let postResponse: [PostListData] = $0.1?.postLists, - !postResponse.isEmpty else { + let yearMonthDay: String = date.toFormatString(with: .dashYyyyMMdd) + return calendarUseCase.executeFetchDailyCalendarResponse(yearMonthDay: yearMonthDay) + .flatMap { entity in + guard let posts: [DailyCalendarEntity] = entity?.results else { return Observable.empty() } return Observable.concat( - Observable.just(.injectPostResponse(postResponse)), + Observable.just(.injectPostResponse(posts)), Observable.just(.injectBlurImageIndex(0)), Observable.just(.clearNotificationDeepLink) ) @@ -197,7 +191,7 @@ public final class CalendarPostViewReactor: Reactor { guard let items = newState.displayPostResponse.first?.items else { return newState } - newState.blurImageUrlString = items[index].imageURL + newState.blurImageUrlString = items[index].postImageUrl case let .renewPostCommentCount(count): guard var posts = currentState.displayPostResponse.first?.items, @@ -212,7 +206,7 @@ public final class CalendarPostViewReactor: Reactor { posts[index] = renewedPost newState.visiblePost = posts[index] // ReactionViewController 데이터 갱신하기 - newState.displayPostResponse = [.init(model: .none, items: posts)] // PostColelctionView 데이터 갱신하기 + newState.displayPostResponse = [.init(model: (), items: posts)] // PostColelctionView 데이터 갱신하기 case let .setAllUploadedToastMessageView(uploaded): newState.shouldPresentAllUploadedToastMessageView = uploaded @@ -221,7 +215,7 @@ public final class CalendarPostViewReactor: Reactor { newState.displayCalendarResponse[yearMonth] = arrayCalendarResponse.results case let .injectPostResponse(postResponse): - newState.displayPostResponse = [PostListSectionModel(model: .none, items: postResponse)] + newState.displayPostResponse = [CalendarPostSectionModel(model: (), items: postResponse)] case let .injectVisiblePost(post): newState.visiblePost = post diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarViewReactor.swift index d0e8d7818..e3d3f764c 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarViewReactor.swift @@ -33,7 +33,7 @@ public final class CalendarViewReactor: Reactor { @Pulse var shouldPopCalendarVC: Bool @Pulse var shouldPushCalendarPostVC: Date? @Pulse var shouldPresnetInfoPopover: UIView? - @Pulse var displayCalendar: [SectionOfMonthlyCalendar] + @Pulse var displayCalendar: [MonthlyCalendarSectionModel] } // MARK: - Properties @@ -46,7 +46,7 @@ public final class CalendarViewReactor: Reactor { init(calendarUseCase: CalendarUseCaseProtocol, provider: GlobalStateProviderProtocol) { self.initialState = State( shouldPopCalendarVC: false, - displayCalendar: [.init(items: [])] + displayCalendar: [.init(model: (), items: [])] ) self.calendarUseCase = calendarUseCase @@ -99,11 +99,11 @@ public final class CalendarViewReactor: Reactor { newState.shouldPresnetInfoPopover = sourceView case let .injectYearMonthItem(dateArray): - guard let datasource: SectionOfMonthlyCalendar = state.displayCalendar.first else { + guard let datasource: MonthlyCalendarSectionModel = state.displayCalendar.first else { return state } - let newDatasource = SectionOfMonthlyCalendar( + let newDatasource = MonthlyCalendarSectionModel( original: datasource, items: dateArray ) diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarPostCell.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarPostCell.swift new file mode 100644 index 000000000..e59d18baf --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarPostCell.swift @@ -0,0 +1,246 @@ +// +// CalendarPostCell.swift +// App +// +// Created by 김건우 on 5/7/24. +// + +import Core +import Domain +import UIKit + +import SnapKit +import Then +import RxSwift +import RxCocoa +import RxDataSources +import Kingfisher + +final class CalendarPostCell: BaseCollectionViewCell { + + // MARK: - Id + static let id = "CalendarPostCell" + + // MARK: - Views + private let authorStackView: UIStackView = UIStackView() + private let authorImageContainerView: UIView = UIView() + private let authorImageView: UIImageView = UIImageView() + private let authorNameLabel: BibbiLabel = BibbiLabel(.caption, textColor: .gray200) + private let authorFirstNameLabel: BibbiLabel = BibbiLabel(.caption, textColor: .bibbiWhite) + private let postImageView: UIImageView = UIImageView() + private let contentCollectionView: UICollectionView = UICollectionView( + frame: .zero, + collectionViewLayout: UICollectionViewFlowLayout() + ) + + // MARK: - Properties + private lazy var datasource = prepareContentDatasource() + private let flowLayout: UICollectionViewFlowLayout = UICollectionViewFlowLayout() + + // MARK: - Intializer + + + // MARK: - LifeCycles + override func prepareForReuse() { + super.prepareForReuse() + authorNameLabel.text = .unknown + authorFirstNameLabel.text = "알" + authorImageView.image = nil + postImageView.image = nil + } + + // MARK: - Helpers + override func bind(reactor: CalendarPostCellReactor) { + super.bind(reactor: reactor) + bindInput(reactor: reactor) + bindOutput(reactor: reactor) + } + + private func bindInput(reactor: CalendarPostCellReactor) { + Observable.just(()) + .flatMap { _ in + Observable.concat( + Observable.just(.displayContent), + Observable.just(.requestAuthorName), + Observable.just(.requestAuthorImageUrl) + ) + } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + authorImageContainerView.rx.tap + .throttle(RxConst.throttleInterval, scheduler: Schedulers.main) + .map { Reactor.Action.writerImageButtonTapped } + .bind(to: reactor.action) + .disposed(by: disposeBag) + } + + private func bindOutput(reactor: CalendarPostCellReactor) { + reactor.state.map { $0.post } + .distinctUntilChanged() + .bind(with: self) { owner, post in + owner.postImageView.kf.setImage( + with: URL(string: post.postImageUrl), + options: [.transition(.fade(0.15))] + ) + } + .disposed(by: disposeBag) + + reactor.state.map { $0.authorName } + .distinctUntilChanged() + .bind(to: authorNameLabel.rx.text) + .disposed(by: disposeBag) + + reactor.state.compactMap { $0.authorName } + .map { String($0.first ?? Character(" ")) } + .distinctUntilChanged() + .bind(to: authorFirstNameLabel.rx.text) + .disposed(by: disposeBag) + + reactor.state.compactMap { $0.authorImageUrl } + .distinctUntilChanged() + .bind(with: self) { owner, url in + owner.authorImageView.kf.setImage( + with: URL(string: url) + ) + } + .disposed(by: disposeBag) + + reactor.state.compactMap { $0.content } + .bind(to: contentCollectionView.rx.items(dataSource: datasource)) + .disposed(by: disposeBag) + } + + override func setupUI() { + super.setupUI() + + contentView.addSubviews(authorStackView, postImageView) + authorImageContainerView.addSubviews(authorFirstNameLabel, authorImageView) + authorStackView.addArrangedSubviews(authorImageContainerView, authorNameLabel) + postImageView.addSubview(contentCollectionView) + } + + override func setupAutoLayout() { + super.setupAutoLayout() + + authorStackView.snp.makeConstraints { + $0.horizontalEdges.equalToSuperview().inset(16) + $0.height.equalTo(34) + } + + authorImageContainerView.snp.makeConstraints { + $0.size.equalTo(34) + } + + authorFirstNameLabel.snp.makeConstraints { + $0.center.equalToSuperview() + } + + authorImageView.snp.makeConstraints { + $0.size.equalTo(34) + } + + contentCollectionView.snp.makeConstraints { + $0.height.equalTo(41) + $0.bottom.equalTo(postImageView.snp.bottom).offset(-20) + $0.horizontalEdges.equalToSuperview() + } + + postImageView.snp.makeConstraints { + $0.horizontalEdges.equalToSuperview() + $0.height.equalTo(postImageView.snp.width) + $0.top.equalTo(authorStackView.snp.bottom).offset(8) + } + } + + override func setupAttributes() { + super.setupAttributes() + + authorStackView.do { + $0.axis = .horizontal + $0.spacing = 12 + } + + authorImageContainerView.do { + $0.layer.masksToBounds = true + $0.layer.cornerRadius = 34 / 2 + $0.backgroundColor = UIColor.gray800 + $0.isUserInteractionEnabled = true + } + + authorImageView.do { + $0.contentMode = .scaleAspectFill + $0.layer.masksToBounds = true + $0.layer.cornerRadius = 34 / 2 + } + + authorNameLabel.do { + $0.text = String.unknown + } + + authorFirstNameLabel.do { + $0.text = "알" + } + + postImageView.do { + $0.clipsToBounds = true + $0.backgroundColor = UIColor.gray100 + $0.contentMode = .scaleAspectFill + $0.layer.cornerRadius = 48 + } + + contentCollectionView.do { + $0.backgroundColor = .clear + $0.isScrollEnabled = false + $0.showsVerticalScrollIndicator = false + $0.showsHorizontalScrollIndicator = false + $0.collectionViewLayout = flowLayout + $0.register( + DisplayEditCollectionViewCell.self, + forCellWithReuseIdentifier: DisplayEditCollectionViewCell.id + ) + $0.delegate = self + } + + flowLayout.do { + $0.itemSize = CGSize(width: 28, height: 41) + $0.minimumInteritemSpacing = 2 + } + } + +} + +// MARK: - Extensions +extension CalendarPostCell { + private func prepareContentDatasource() -> RxCollectionViewSectionedReloadDataSource { + return RxCollectionViewSectionedReloadDataSource { datasources, collectionView, indexPath, sectionItem in + switch sectionItem { + case let .fetchDisplayItem(reactor): + guard let cell = collectionView.dequeueReusableCell( + withReuseIdentifier: DisplayEditCollectionViewCell.id, + for: indexPath + ) as? DisplayEditCollectionViewCell else { + return UICollectionViewCell() + } + cell.reactor = reactor + return cell + } + } + } +} + +extension CalendarPostCell: UICollectionViewDelegateFlowLayout { + public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { + guard let count = reactor?.currentState.post.postContent.count else { + return .zero + } + + let totalCellWidth = 28 * count + let totalSpacingWidth = 2 * (count - 1) + + let leftInset = (collectionView.frame.width - CGFloat(totalCellWidth + totalSpacingWidth)) / 2 + let rightInset = leftInset + + return UIEdgeInsets(top: 0, left: leftInset, bottom: 0, right: rightInset) + } +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/CalendarPostViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/CalendarPostViewController.swift index ea692db67..2e6c3c709 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/CalendarPostViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/CalendarPostViewController.swift @@ -29,13 +29,18 @@ public final class CalendarPostViewController: BaseViewController = PublishRelay() - private lazy var dataSource: RxCollectionViewSectionedReloadDataSource = prepareDatasource() + private lazy var dataSource = prepareDatasource() private let deepLinkRepo = DeepLinkRepository() @@ -156,7 +161,6 @@ public final class CalendarPostViewController: BaseViewController RxCollectionViewSectionedReloadDataSource { - return RxCollectionViewSectionedReloadDataSource { datasource, collectionView, indexPath, post in - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: PostDetailCollectionViewCell.id, for: indexPath) as! PostDetailCollectionViewCell - cell.reactor = PostDetailCellDIContainer().makeReactor(type: .calendar, post: post) + private func prepareDatasource() -> RxCollectionViewSectionedReloadDataSource { + return RxCollectionViewSectionedReloadDataSource { datasource, collectionView, indexPath, post in + let cell = collectionView.dequeueReusableCell( + withReuseIdentifier: CalendarPostCell.id, + for: indexPath + ) as! CalendarPostCell + cell.reactor = CalendarPostCellDIContainer(post: post).makeReactor() return cell } } diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/CalendarViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/CalendarViewController.swift index 105277856..0b9590cc5 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/CalendarViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/CalendarViewController.swift @@ -25,7 +25,7 @@ public final class CalendarViewController: BaseViewController = prepareDatasource() + private lazy var dataSource: RxCollectionViewSectionedReloadDataSource = prepareDatasource() // MARK: - Lifecycles public override func viewDidLoad() { @@ -163,8 +163,8 @@ extension CalendarViewController { } extension CalendarViewController { - private func prepareDatasource() -> RxCollectionViewSectionedReloadDataSource { - return RxCollectionViewSectionedReloadDataSource { datasource, collectionView, indexPath, yearMonth in + private func prepareDatasource() -> RxCollectionViewSectionedReloadDataSource { + return RxCollectionViewSectionedReloadDataSource { datasource, collectionView, indexPath, yearMonth in let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CalendarPageCell.id, for: indexPath) as! CalendarPageCell cell.reactor = CalendarPageCellDIContainer(yearMonth: yearMonth).makeReactor() return cell diff --git a/14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/CalendarAPIWorker.swift b/14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/CalendarAPIWorker.swift index feecbc434..b5e998500 100644 --- a/14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/CalendarAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/CalendarAPIWorker.swift @@ -95,8 +95,8 @@ extension CalendarAPIWorker { .asSingle() } - public func fetchDailyCalendar(yearMonth: String) -> Single { - let spec = CalendarAPIs.monthlyCalendar(yearMonth).spec + public func fetchDailyCalendar(yearMonthDay: String) -> Single { + let spec = CalendarAPIs.dailyCalendar(yearMonthDay).spec return Observable.just(()) .withLatestFrom(self._headers) diff --git a/14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/DataMapping/ArrayResponseDailyCalendarResponseDTO.swift b/14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/DataMapping/ArrayResponseDailyCalendarResponseDTO.swift index 094b7684e..300423404 100644 --- a/14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/DataMapping/ArrayResponseDailyCalendarResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/DataMapping/ArrayResponseDailyCalendarResponseDTO.swift @@ -34,8 +34,8 @@ extension ArrayResponseDailyCalendarResponseDTO { var type: String var postId: String var postImageUrl: String - var postContent: String - var missionContent: String + var postContent: String? + var missionContent: String? var authorId: String var commentCount: Int var emojiCount: Int @@ -56,11 +56,11 @@ extension ArrayResponseDailyCalendarResponseDTO.DailyCalendarResponseDTO { func toDomain() -> DailyCalendarEntity { return DailyCalendarEntity( date: date.toDate(with: .dashYyyyMMdd), - type: .init(rawValue: type) ?? .survival, + type: PostType(rawValue: type) ?? .survival, postId: postId, postImageUrl: postImageUrl, - postContent: postContent, - missionContent: missionContent, + postContent: postContent ?? .none, + missionContent: missionContent ?? .none, authorId: authorId, commentCount: commentCount, emojiCount: emojiCount, diff --git a/14th-team5-iOS/Data/Sources/Calendar/Repository/CalendarRepository.swift b/14th-team5-iOS/Data/Sources/Calendar/Repository/CalendarRepository.swift index df5c38be5..e1d6ccb3d 100644 --- a/14th-team5-iOS/Data/Sources/Calendar/Repository/CalendarRepository.swift +++ b/14th-team5-iOS/Data/Sources/Calendar/Repository/CalendarRepository.swift @@ -36,8 +36,8 @@ extension CalendarRepository { .asObservable() } - public func fetchDailyCalendarResponse(yearMonth: String) -> Observable { - return calendarApiWorker.fetchDailyCalendar(yearMonth: yearMonth) + public func fetchDailyCalendarResponse(yearMonthDay: String) -> Observable { + return calendarApiWorker.fetchDailyCalendar(yearMonthDay: yearMonthDay) .asObservable() } diff --git a/14th-team5-iOS/Data/Sources/Member/Repository/MemberRepository.swift b/14th-team5-iOS/Data/Sources/Member/Repository/MemberRepository.swift index e3bd9a41a..02a186b31 100644 --- a/14th-team5-iOS/Data/Sources/Member/Repository/MemberRepository.swift +++ b/14th-team5-iOS/Data/Sources/Member/Repository/MemberRepository.swift @@ -14,11 +14,11 @@ public final class MemberRepository: MemberRepositoryProtocol { extension MemberRepository { public func fetchUserName(memberId: String) -> String { - return FamilyUserDefaults.loadMemberFromUserDefaults(memberId: memberId)?.name ?? .unknown + return FamilyUserDefaults.load(memberId: memberId)?.name ?? .unknown } public func fetchProfileImageUrlString(memberId: String) -> String { - return FamilyUserDefaults.loadMemberFromUserDefaults(memberId: memberId)?.profileImageURL ?? .unknown + return FamilyUserDefaults.load(memberId: memberId)?.profileImageURL ?? .unknown } public func checkIsMe(memberId: String) -> Bool { diff --git a/14th-team5-iOS/Data/Sources/Post/PostList/DTO/PostDTO.swift b/14th-team5-iOS/Data/Sources/Post/PostList/DTO/PostDTO.swift index 69fbf0715..9755f339d 100644 --- a/14th-team5-iOS/Data/Sources/Post/PostList/DTO/PostDTO.swift +++ b/14th-team5-iOS/Data/Sources/Post/PostList/DTO/PostDTO.swift @@ -30,7 +30,7 @@ struct PostResponseDTO: Codable { extension PostResponseDTO { func toDomain() -> PostData { - let writer = FamilyUserDefaults.loadMemberFromUserDefaults(memberId: authorId) + let writer = FamilyUserDefaults.load(memberId: authorId) return .init(writer: writer, time: createdAt, imageURL: imageUrl, imageText: content, emojis: []) } diff --git a/14th-team5-iOS/Data/Sources/Post/PostList/DTO/PostListDTO.swift b/14th-team5-iOS/Data/Sources/Post/PostList/DTO/PostListDTO.swift index eab1ddd23..34faa22f7 100644 --- a/14th-team5-iOS/Data/Sources/Post/PostList/DTO/PostListDTO.swift +++ b/14th-team5-iOS/Data/Sources/Post/PostList/DTO/PostListDTO.swift @@ -31,7 +31,7 @@ struct PostListDTO: Codable { extension PostListDTO { func toDomain() -> PostListData { - let author = FamilyUserDefaults.loadMemberFromUserDefaults(memberId: authorId) + let author = FamilyUserDefaults.load(memberId: authorId) return .init( postId: postId, missionId: missionId, diff --git a/14th-team5-iOS/Data/Sources/UserDefaults/FamilyUserDefautls.swift b/14th-team5-iOS/Data/Sources/UserDefaults/FamilyUserDefautls.swift index fc2a3b44b..26aa34287 100644 --- a/14th-team5-iOS/Data/Sources/UserDefaults/FamilyUserDefautls.swift +++ b/14th-team5-iOS/Data/Sources/UserDefaults/FamilyUserDefautls.swift @@ -66,7 +66,7 @@ public class FamilyUserDefaults { } } - public static func loadMemberFromUserDefaults(memberId: String) -> ProfileData? { + public static func load(memberId: String) -> ProfileData? { if let data = UserDefaults.standard.data(forKey: memberId) { do { let decoder = JSONDecoder() diff --git a/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/mission 1.imageset/Contents.json b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/mission 1.imageset/Contents.json new file mode 100644 index 000000000..c1d64cc3d --- /dev/null +++ b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/mission 1.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "mission.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/mission 1.imageset/mission.svg b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/mission 1.imageset/mission.svg new file mode 100644 index 000000000..9f515b9db --- /dev/null +++ b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/mission 1.imageset/mission.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/14th-team5-iOS/Domain/Sources/Calendar/Entities/ArrayResponseDailyCalendarEntity.swift b/14th-team5-iOS/Domain/Sources/Calendar/Entities/ArrayResponseDailyCalendarEntity.swift index 7c414ec3c..f7ecdc16c 100644 --- a/14th-team5-iOS/Domain/Sources/Calendar/Entities/ArrayResponseDailyCalendarEntity.swift +++ b/14th-team5-iOS/Domain/Sources/Calendar/Entities/ArrayResponseDailyCalendarEntity.swift @@ -55,3 +55,4 @@ public struct DailyCalendarEntity { } } +extension DailyCalendarEntity: Equatable { } diff --git a/14th-team5-iOS/Domain/Sources/Calendar/Interfaces/Repositories/CalendarRepository.swift b/14th-team5-iOS/Domain/Sources/Calendar/Interfaces/Repositories/CalendarRepository.swift index 1f7f84169..5fe518744 100644 --- a/14th-team5-iOS/Domain/Sources/Calendar/Interfaces/Repositories/CalendarRepository.swift +++ b/14th-team5-iOS/Domain/Sources/Calendar/Interfaces/Repositories/CalendarRepository.swift @@ -15,7 +15,7 @@ public protocol CalendarRepositoryProtocol { func fetchCalendarResponse(yearMonth: String) -> Observable func fetchMonthyCalendarResponse(yearMonth: String) -> Observable - func fetchDailyCalendarResponse(yearMonth: String) -> Observable + func fetchDailyCalendarResponse(yearMonthDay: String) -> Observable func fetchStatisticsSummary(yearMonth: String) -> Observable func fetchCalendarBanner(yearMonth: String) -> Observable } diff --git a/14th-team5-iOS/Domain/Sources/Calendar/UseCases/CalendarUseCase.swift b/14th-team5-iOS/Domain/Sources/Calendar/UseCases/CalendarUseCase.swift index 042f97e1f..3fcdda066 100644 --- a/14th-team5-iOS/Domain/Sources/Calendar/UseCases/CalendarUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/Calendar/UseCases/CalendarUseCase.swift @@ -11,6 +11,7 @@ import RxSwift public protocol CalendarUseCaseProtocol { func executeFetchCalednarResponse(yearMonth: String) -> Observable + func executeFetchDailyCalendarResponse(yearMonthDay: String) -> Observable func executeFetchStatisticsSummary(yearMonth: String) -> Observable func executeFetchCalendarBenner(yearMonth: String) -> Observable } @@ -26,6 +27,10 @@ public final class CalendarUseCase: CalendarUseCaseProtocol { return calendarRepository.fetchCalendarResponse(yearMonth: yearMonth) } + public func executeFetchDailyCalendarResponse(yearMonthDay: String) -> Observable { + return calendarRepository.fetchDailyCalendarResponse(yearMonthDay: yearMonthDay) + } + public func executeFetchStatisticsSummary(yearMonth: String) -> Observable { return calendarRepository.fetchStatisticsSummary(yearMonth: yearMonth) } From ac83b308da689710cd451ed7bb25ab5e0ccb5a8b Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Tue, 7 May 2024 15:53:22 +0900 Subject: [PATCH 017/263] =?UTF-8?q?feat:=20MisstionTextView=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#500)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Calendar/Reactor/MissionTextReactor.swift | 30 +++++++ .../Calendar/View/Cell/CalendarPostCell.swift | 10 ++- .../Calendar/View/MissionTextView.swift | 88 +++++++++++++++++++ .../Core/Sources/Extensions/UIView+Ext.swift | 10 ++- 4 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/MissionTextReactor.swift create mode 100644 14th-team5-iOS/App/Sources/Presentation/Calendar/View/MissionTextView.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/MissionTextReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/MissionTextReactor.swift new file mode 100644 index 000000000..db53c187f --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/MissionTextReactor.swift @@ -0,0 +1,30 @@ +// +// MissionTextReactor.swift +// App +// +// Created by 김건우 on 5/7/24. +// + +import Foundation + +import ReactorKit + +final class MissionTextReactor: Reactor { + // MARK: - Action + typealias Action = NoAction + + // MARK: - State + struct State { + var text: String + } + + // MARK: - Properties + var initialState: State + + // MARK: - Intializer + init(text: String) { + self.initialState = State( + text: text + ) + } +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarPostCell.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarPostCell.swift index e59d18baf..3044f566b 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarPostCell.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarPostCell.swift @@ -28,6 +28,7 @@ final class CalendarPostCell: BaseCollectionViewCell { private let authorNameLabel: BibbiLabel = BibbiLabel(.caption, textColor: .gray200) private let authorFirstNameLabel: BibbiLabel = BibbiLabel(.caption, textColor: .bibbiWhite) private let postImageView: UIImageView = UIImageView() + private let missionTextView: MissionTextView = MissionTextView() private let contentCollectionView: UICollectionView = UICollectionView( frame: .zero, collectionViewLayout: UICollectionViewFlowLayout() @@ -117,7 +118,7 @@ final class CalendarPostCell: BaseCollectionViewCell { contentView.addSubviews(authorStackView, postImageView) authorImageContainerView.addSubviews(authorFirstNameLabel, authorImageView) authorStackView.addArrangedSubviews(authorImageContainerView, authorNameLabel) - postImageView.addSubview(contentCollectionView) + postImageView.addSubviews(contentCollectionView, missionTextView) } override func setupAutoLayout() { @@ -146,6 +147,13 @@ final class CalendarPostCell: BaseCollectionViewCell { $0.horizontalEdges.equalToSuperview() } + missionTextView.snp.makeConstraints { + $0.top.equalToSuperview().offset(16) + $0.horizontalEdges.equalToSuperview().inset(32) + $0.height.equalTo(41) + } + missionTextView.reactor = MissionTextReactor(text: "입고 있는 옷이 나오도록 사진을 찍어주세요!") + postImageView.snp.makeConstraints { $0.horizontalEdges.equalToSuperview() $0.height.equalTo(postImageView.snp.width) diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/MissionTextView.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/MissionTextView.swift new file mode 100644 index 000000000..cde9c7145 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/MissionTextView.swift @@ -0,0 +1,88 @@ +// +// MissionTextView.swift +// App +// +// Created by 김건우 on 5/7/24. +// + +import Core +import DesignSystem +import UIKit + +import SnapKit +import Then +import RxSwift +import RxCocoa + +final class MissionTextView: BaseView { + + // MARK: - Views + private let containerView: UIView = UIView() + private let missionStackView: UIStackView = UIStackView() + private let missionImageView: UIImageView = UIImageView() + private let missionLabel: BibbiLabel = BibbiLabel(.body2Regular, textColor: .bibbiWhite) + + // MARK: - Helpers + override func bind(reactor: MissionTextReactor) { + super.bind(reactor: reactor) + bindOutput(reactor: reactor) + } + + private func bindOutput(reactor: MissionTextReactor) { + reactor.state.map { $0.text } + .distinctUntilChanged() + .bind(to: missionLabel.rx.text) + .disposed(by: disposeBag) + } + + override func setupUI() { + super.setupUI() + + self.addSubviews(containerView) + containerView.addSubview(missionStackView) + missionStackView.addArrangedSubviews(missionImageView, missionLabel) + } + + override func setupAutoLayout() { + super.setupAutoLayout() + + containerView.snp.makeConstraints { + $0.verticalEdges.equalToSuperview() + } + + missionStackView.snp.makeConstraints { + $0.verticalEdges.equalToSuperview().inset(8) + $0.horizontalEdges.equalToSuperview().inset(16) + } + + missionImageView.snp.makeConstraints { + $0.width.equalTo(40) + $0.height.equalTo(18) + } + } + + override func setupAttributes() { + super.setupAttributes() + + containerView.do { + $0.layer.cornerRadius = 21 + $0.layer.masksToBounds = true + } + containerView.addBlurEffect( + style: .systemThinMaterialDark + ) + + missionStackView.do { + $0.axis = .horizontal + $0.spacing = 10 + $0.alignment = .center + $0.distribution = .fillProportionally + } + + missionImageView.do { + $0.image = DesignSystemAsset.mission1.image + $0.contentMode = .scaleAspectFit + } + } +} + diff --git a/14th-team5-iOS/Core/Sources/Extensions/UIView+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/UIView+Ext.swift index a2b7d1386..6fb8fa872 100644 --- a/14th-team5-iOS/Core/Sources/Extensions/UIView+Ext.swift +++ b/14th-team5-iOS/Core/Sources/Extensions/UIView+Ext.swift @@ -40,5 +40,13 @@ extension UIView { layer.render(in: rendererContext.cgContext) } } - +} + +extension UIView { + public func addBlurEffect(style: UIBlurEffect.Style) { + let blurEffect = UIBlurEffect(style: style) + let visualEffectView = UIVisualEffectView(effect: blurEffect) + visualEffectView.frame = self.frame + self.insertSubview(visualEffectView, at: 0) + } } From 4a0c5588c668288ab5457f02bba75e5286e4b457 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Tue, 7 May 2024 17:48:41 +0900 Subject: [PATCH 018/263] =?UTF-8?q?fix:=20=EB=B8=94=EB=9F=AC=20=ED=9A=A8?= =?UTF-8?q?=EA=B3=BC=EA=B0=80=20=EC=A0=81=EC=9A=A9=EB=90=98=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?(#500)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Presentation/Calendar/View/MissionTextView.swift | 10 ++++++---- .../Core/Sources/Extensions/UIView+Ext.swift | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/MissionTextView.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/MissionTextView.swift index cde9c7145..c6b64f10f 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/MissionTextView.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/MissionTextView.swift @@ -20,7 +20,7 @@ final class MissionTextView: BaseView { private let containerView: UIView = UIView() private let missionStackView: UIStackView = UIStackView() private let missionImageView: UIImageView = UIImageView() - private let missionLabel: BibbiLabel = BibbiLabel(.body2Regular, textColor: .bibbiWhite) + let missionLabel: BibbiLabel = BibbiLabel(.body2Regular, textColor: .bibbiWhite) // MARK: - Helpers override func bind(reactor: MissionTextReactor) { @@ -49,6 +49,7 @@ final class MissionTextView: BaseView { containerView.snp.makeConstraints { $0.verticalEdges.equalToSuperview() } + containerView.addBlurEffect(style: .systemThinMaterialDark) missionStackView.snp.makeConstraints { $0.verticalEdges.equalToSuperview().inset(8) @@ -65,12 +66,12 @@ final class MissionTextView: BaseView { super.setupAttributes() containerView.do { + $0.backgroundColor = UIColor.gray800 + .withAlphaComponent(0.8) $0.layer.cornerRadius = 21 $0.layer.masksToBounds = true } - containerView.addBlurEffect( - style: .systemThinMaterialDark - ) + containerView.addBlurEffect(style: .systemThinMaterialDark) missionStackView.do { $0.axis = .horizontal @@ -86,3 +87,4 @@ final class MissionTextView: BaseView { } } +// TODO: - BlurEffect 집어넣기 diff --git a/14th-team5-iOS/Core/Sources/Extensions/UIView+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/UIView+Ext.swift index 6fb8fa872..e13d697f1 100644 --- a/14th-team5-iOS/Core/Sources/Extensions/UIView+Ext.swift +++ b/14th-team5-iOS/Core/Sources/Extensions/UIView+Ext.swift @@ -47,6 +47,6 @@ extension UIView { let blurEffect = UIBlurEffect(style: style) let visualEffectView = UIVisualEffectView(effect: blurEffect) visualEffectView.frame = self.frame - self.insertSubview(visualEffectView, at: 0) + self.addSubview(visualEffectView) } } From d27f4d5964d83a21684272968703530fbde20274 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Tue, 7 May 2024 19:04:40 +0900 Subject: [PATCH 019/263] =?UTF-8?q?feat:=20PostDetailCollectionViewCell?= =?UTF-8?q?=EC=97=90=20MissionTextView=20=EC=82=BD=EC=9E=85=20(#500)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Calendar/View/Cell/CalendarPostCell.swift | 15 ++++++++++++--- .../Views}/MissionTextView.swift | 10 +++++++--- .../Views/PostDetailCollectionViewCell.swift | 15 ++++++++++++++- 3 files changed, 33 insertions(+), 7 deletions(-) rename 14th-team5-iOS/App/Sources/Presentation/{Calendar/View => PostDetail/Views}/MissionTextView.swift (89%) diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarPostCell.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarPostCell.swift index 3044f566b..38cda1644 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarPostCell.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarPostCell.swift @@ -77,9 +77,13 @@ final class CalendarPostCell: BaseCollectionViewCell { } private func bindOutput(reactor: CalendarPostCellReactor) { - reactor.state.map { $0.post } + + let post = reactor.state.map { $0.post } + .asDriver(onErrorDriveWith: .empty()) + + post .distinctUntilChanged() - .bind(with: self) { owner, post in + .drive(with: self) { owner, post in owner.postImageView.kf.setImage( with: URL(string: post.postImageUrl), options: [.transition(.fade(0.15))] @@ -87,6 +91,12 @@ final class CalendarPostCell: BaseCollectionViewCell { } .disposed(by: disposeBag) + post + .map { _ in /*$0.missionContent*/ "정신차려~" } + .distinctUntilChanged() + .drive(missionTextView.missionLabel.rx.text) + .disposed(by: disposeBag) + reactor.state.map { $0.authorName } .distinctUntilChanged() .bind(to: authorNameLabel.rx.text) @@ -152,7 +162,6 @@ final class CalendarPostCell: BaseCollectionViewCell { $0.horizontalEdges.equalToSuperview().inset(32) $0.height.equalTo(41) } - missionTextView.reactor = MissionTextReactor(text: "입고 있는 옷이 나오도록 사진을 찍어주세요!") postImageView.snp.makeConstraints { $0.horizontalEdges.equalToSuperview() diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/MissionTextView.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/MissionTextView.swift similarity index 89% rename from 14th-team5-iOS/App/Sources/Presentation/Calendar/View/MissionTextView.swift rename to 14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/MissionTextView.swift index c6b64f10f..7955296e9 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/MissionTextView.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/MissionTextView.swift @@ -20,7 +20,7 @@ final class MissionTextView: BaseView { private let containerView: UIView = UIView() private let missionStackView: UIStackView = UIStackView() private let missionImageView: UIImageView = UIImageView() - let missionLabel: BibbiLabel = BibbiLabel(.body2Regular, textColor: .bibbiWhite) + let missionLabel: BibbiLabel = BibbiLabel(.body2Regular, textAlignment: .left, textColor: .bibbiWhite) // MARK: - Helpers override func bind(reactor: MissionTextReactor) { @@ -48,6 +48,7 @@ final class MissionTextView: BaseView { containerView.snp.makeConstraints { $0.verticalEdges.equalToSuperview() + $0.horizontalEdges.equalToSuperview() } containerView.addBlurEffect(style: .systemThinMaterialDark) @@ -60,6 +61,11 @@ final class MissionTextView: BaseView { $0.width.equalTo(40) $0.height.equalTo(18) } + + missionLabel.snp.makeConstraints { + $0.leading.equalTo(missionImageView.snp.trailing).offset(8) + $0.trailing.equalToSuperview().offset(-8) + } } override func setupAttributes() { @@ -86,5 +92,3 @@ final class MissionTextView: BaseView { } } } - -// TODO: - BlurEffect 집어넣기 diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/PostDetailCollectionViewCell.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/PostDetailCollectionViewCell.swift index c3d092d88..b39be3cf6 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/PostDetailCollectionViewCell.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/PostDetailCollectionViewCell.swift @@ -24,6 +24,7 @@ final class PostDetailCollectionViewCell: BaseCollectionViewCell Date: Tue, 7 May 2024 01:06:54 +0900 Subject: [PATCH 020/263] feat: deepLinkManager(#504) --- .../Sources/Application/DeepLinkManager.swift | 201 +++++++++++------- .../Sources/Application/SceneDelegate.swift | 6 +- 2 files changed, 121 insertions(+), 86 deletions(-) diff --git a/14th-team5-iOS/App/Sources/Application/DeepLinkManager.swift b/14th-team5-iOS/App/Sources/Application/DeepLinkManager.swift index c5468b8b4..bce6f0c2a 100644 --- a/14th-team5-iOS/App/Sources/Application/DeepLinkManager.swift +++ b/14th-team5-iOS/App/Sources/Application/DeepLinkManager.swift @@ -13,7 +13,26 @@ import Domain import RxSwift -class DeepLinkManager { +enum DeepLinkType { + /// 위젯 눌러 메인 진입 + case Widget + /// 푸시알림으로 오늘 생존신고 피드로 진입 + case TodaySurvival + /// 푸시알림으로 오늘 미션 피드로 진입 + case TodayMission + + + case Comment + +// /// 푸시알림으로 오늘 생존신고 피드의 댓글까지 진입 +// case TodaySurvivalComment +// /// 푸시알림으로 오늘 미션 피드의 댓글까지 진입 +// case TodayMissionComment +// /// 푸시알림으로 오늘 아닌 날짜의 캘린더 피드까지 진입 +// case SomedayPostComment +} + +final class DeepLinkManager { static let shared = DeepLinkManager() // 이번 3차 끝나고, postdetailviewcontroller에서 post 불러오는 형태로 바꿔보겠습니다. @@ -23,93 +42,111 @@ class DeepLinkManager { private init() {} - func handleDeepLink(_ deepLink: NotificationDeepLink) { + func handleWidgetDeepLink(data: WidgetDeepLink) { + fetchTodayPost(type: .survival) { result in + guard let result = result else { return } + let items = result.postLists.map(PostSection.Item.main) + + items.enumerated().forEach { (index, item) in + switch item { + case .main(let postListData): + if postListData.postId == data.postId { + let indexPath = IndexPath(row: index, section: 0) + self.pushViewController(data: nil, index: indexPath, items: items) + } + } + } + } } - func handlePostWidgetDeepLink(_ deepLink: WidgetDeepLink) { - let query = PostListQuery(date: DateFormatter.dashYyyyMMdd.string(from: Date()), type: .survival) - postUseCase.excute(query: query) - .subscribe(onSuccess: { [weak self] result in - guard let self = self, let result = result else { return } - - let items = result.postLists.map(PostSection.Item.main) - - items.enumerated().forEach { (index, item) in - switch item { - case .main(let postListData): - if postListData.postId == deepLink.postId { - let indexPath = IndexPath(row: index, section: 0) - if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let keyWindow = windowScene.windows.first(where: { $0.isKeyWindow }), - let navigationController = keyWindow.rootViewController as? UINavigationController { - let viewController = PostListsDIContainer().makeViewController( - postLists: PostSection.Model(model: 0, items: items), - selectedIndex: IndexPath(row: index, section: 0) - ) - navigationController.pushViewController(viewController, animated: true) - } - } + func handleDeepLink(type: DeepLinkType, data: NotificationDeepLink) { + switch type { + case .TodaySurvival: + todayDeepLink(type: .survival, data: data) + case .TodayMission: + todayDeepLink(type: .mission, data: data) + case .Comment: + if data.dateOfPost == Date() { + todayCommentDeepLink(type: .survival, data: data) + } else { + pushCalendarViewController(data: data) + } + default: + fatalError("원하는 정보를 찾을 수 없습니다.") + } + } + + private func todayDeepLink(type: PostType, data: NotificationDeepLink) { + fetchTodayPost(type: type) { result in + guard let result = result else { return } + let items = result.postLists.map(PostSection.Item.main) + + items.enumerated().forEach { (index, item) in + switch item { + case .main(let postListData): + if postListData.postId == data.postId { + let indexPath = IndexPath(row: index, section: 0) + self.pushViewController(data: nil, index: indexPath, items: items) + } + } + } + } + } + + private func todayCommentDeepLink(type: PostType, data: NotificationDeepLink) { + fetchTodayPost(type: type) { result in + guard let result = result else { return } + let items = result.postLists.map(PostSection.Item.main) + + items.enumerated().forEach { (index, item) in + switch item { + case .main(let postListData): + if postListData.postId == data.postId { + let indexPath = IndexPath(row: index, section: 0) + self.pushViewController(data: data, index: indexPath, items: items) } } - }, onFailure: { error in - // 에러 처리 + } + } + + } +} + +extension DeepLinkManager { + private func fetchTodayPost(type: PostType, completion: @escaping (PostListPage?) -> Void) { + let dateString = DateFormatter.dashYyyyMMdd.string(from: Date()) + let query = PostListQuery(date: dateString, type: type) + + postUseCase.excute(query: query) + .subscribe(onSuccess: { result in + completion(result) }) .disposed(by: disposeBag) } - -// private func handlePostNotificationDeepLink(_ deepLink: NotificationDeepLink) { -// guard let reactor = reactor else { return } -// reactor.currentState.postSection.items.enumerated().forEach { (index, item) in -// switch item { -// case .main(let post): -// if post.postId == deepLink.postId { -// let indexPath = IndexPath(row: index, section: 0) -// self.navigationController?.pushViewController( -// PostListsDIContainer().makeViewController( -// postLists: reactor.currentState.postSection, -// selectedIndex: indexPath), -// animated: true -// ) -// } -// } -// } -// } -// -// private func handleCommentNotificationDeepLink(_ deepLink: NotificationDeepLink) { -// guard let reactor = reactor else { return } -// -// // 오늘 올린 피드에 댓글이 달렸다면 -// if deepLink.dateOfPost.isToday { -// guard let selectedIndex = reactor.currentState.postSection.items.firstIndex(where: { postList in -// switch postList { -// case let .main(post): -// post.postId == deepLink.postId -// } -// }) else { return } -// let indexPath = IndexPath(row: selectedIndex, section: 0) -// -// let postListViewController = PostListsDIContainer().makeViewController( -// postLists: reactor.currentState.postSection, -// selectedIndex: indexPath, -// notificationDeepLink: deepLink -// ) -// -// navigationController?.pushViewController( -// postListViewController, -// animated: true -// ) -// // 이전에 올린 피드에 댓글이 달렸다면 -// } else { -// let calendarPostViewController = CalendarPostDIConatainer( -// selectedDate: deepLink.dateOfPost, -// notificationDeepLink: deepLink -// ).makeViewController() -// -// navigationController?.pushViewController( -// calendarPostViewController, -// animated: true -// ) -// } -// } + private func pushViewController(data: NotificationDeepLink?, index: IndexPath, items: [PostSection.Item]) { + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let keyWindow = windowScene.windows.first(where: { $0.isKeyWindow }), + let navigationController = keyWindow.rootViewController as? UINavigationController { + let viewController = PostListsDIContainer().makeViewController( + postLists: PostSection.Model(model: 0, items: items), + selectedIndex: index, + notificationDeepLink: data + ) + navigationController.pushViewController(viewController, animated: true) + } + } + + private func pushCalendarViewController(data: NotificationDeepLink) { + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let keyWindow = windowScene.windows.first(where: { $0.isKeyWindow }), + let navigationController = keyWindow.rootViewController as? UINavigationController { + let viewController = CalendarPostDIConatainer( + selectedDate: data.dateOfPost, + notificationDeepLink: data + ).makeViewController() + + navigationController.pushViewController(viewController, animated: true) + } + } } diff --git a/14th-team5-iOS/App/Sources/Application/SceneDelegate.swift b/14th-team5-iOS/App/Sources/Application/SceneDelegate.swift index 6318c98bd..7b2ad3d4f 100644 --- a/14th-team5-iOS/App/Sources/Application/SceneDelegate.swift +++ b/14th-team5-iOS/App/Sources/Application/SceneDelegate.swift @@ -28,8 +28,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // For when app is terminated if let url = connectionOptions.urlContexts.first?.url { if let deepLink = decodeWidgetDeepLink(url) { - App.Repository.deepLink.widget.accept(deepLink) - DeepLinkManager.shared.handlePostWidgetDeepLink(deepLink) + DeepLinkManager.shared.handleWidgetDeepLink(data: deepLink) } self.window = UIWindow(windowScene: scene) @@ -71,8 +70,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // 위젯 딥링크라면 if let deepLink = decodeWidgetDeepLink(url) { - App.Repository.deepLink.widget.accept(deepLink) - DeepLinkManager.shared.handlePostWidgetDeepLink(deepLink) + DeepLinkManager.shared.handleWidgetDeepLink(data: deepLink) } } } From 40faacd266a503ac2baf6633dad06b0f20e10336 Mon Sep 17 00:00:00 2001 From: Kyoungmi <1mmgm.dev@gmail.com> Date: Tue, 7 May 2024 18:15:33 +0900 Subject: [PATCH 021/263] [refactor]: refactor deepLink(#504) --- .../App/Sources/Application/AppDelegate.swift | 51 ++------ .../Sources/Application/DeepLinkManager.swift | 121 +++++++++++------- .../Core/Sources/Extensions/String+Ext.swift | 7 + 3 files changed, 95 insertions(+), 84 deletions(-) diff --git a/14th-team5-iOS/App/Sources/Application/AppDelegate.swift b/14th-team5-iOS/App/Sources/Application/AppDelegate.swift index e1dcac4c3..8d3605d45 100644 --- a/14th-team5-iOS/App/Sources/Application/AppDelegate.swift +++ b/14th-team5-iOS/App/Sources/Application/AppDelegate.swift @@ -163,46 +163,19 @@ extension AppDelegate: UNUserNotificationCenterDelegate { func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { let userInfo = response.notification.request.content.userInfo - guard let deepLink = decodeRemoteNotificationDeepLink(userInfo) else { - completionHandler() - return - } - - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - App.Repository.deepLink.notification.accept(deepLink) - DeepLinkManager.shared.handleDeepLink(deepLink) - } - +// guard let deepLink = decodeRemoteNotificationDeepLink(userInfo) else { +// completionHandler() +// return +// } +// + DeepLinkManager.shared.decodeRemoteNotificationDeepLink(userInfo) completionHandler() - } - - func decodeRemoteNotificationDeepLink(_ userInfo: [AnyHashable: Any]) -> NotificationDeepLink? { - if let link = userInfo[AnyHashable("iosDeepLink")] as? String { - let components = link.components(separatedBy: "?") - let parameters = components.last?.components(separatedBy: "&") - - // PostID 구하기 - let postId = components.first?.components(separatedBy: "/").last ?? "" - - // OpenComment 구하기 - let firstPart = parameters?.first - let openComment = firstPart?.components(separatedBy: "=").last == "true" ? true : false - - // dateOfPost 구하기 - let secondPart = parameters?.last - let dateOfPost = secondPart?.components(separatedBy: "=").last?.toDate(with: .dashYyyyMMdd) ?? Date() - - let deepLink = NotificationDeepLink( - postId: postId, - openComment: openComment, - dateOfPost: dateOfPost - ) - debugPrint("Push Notification Request UserInfo: \(postId), \(openComment), \(dateOfPost)") - return deepLink - } - - print("Error: Decoding Notification Request UserInfo") - return nil +// +// DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { +// DeepLinkManager.shared.handleDeepLink(deepLink) +// } +// +// completionHandler() } } diff --git a/14th-team5-iOS/App/Sources/Application/DeepLinkManager.swift b/14th-team5-iOS/App/Sources/Application/DeepLinkManager.swift index bce6f0c2a..54248b88e 100644 --- a/14th-team5-iOS/App/Sources/Application/DeepLinkManager.swift +++ b/14th-team5-iOS/App/Sources/Application/DeepLinkManager.swift @@ -22,14 +22,8 @@ enum DeepLinkType { case TodayMission - case Comment - -// /// 푸시알림으로 오늘 생존신고 피드의 댓글까지 진입 -// case TodaySurvivalComment -// /// 푸시알림으로 오늘 미션 피드의 댓글까지 진입 -// case TodayMissionComment -// /// 푸시알림으로 오늘 아닌 날짜의 캘린더 피드까지 진입 -// case SomedayPostComment + case TodayComment + case SomedayComment } final class DeepLinkManager { @@ -42,40 +36,6 @@ final class DeepLinkManager { private init() {} - func handleWidgetDeepLink(data: WidgetDeepLink) { - fetchTodayPost(type: .survival) { result in - guard let result = result else { return } - let items = result.postLists.map(PostSection.Item.main) - - items.enumerated().forEach { (index, item) in - switch item { - case .main(let postListData): - if postListData.postId == data.postId { - let indexPath = IndexPath(row: index, section: 0) - self.pushViewController(data: nil, index: indexPath, items: items) - } - } - } - } - } - - func handleDeepLink(type: DeepLinkType, data: NotificationDeepLink) { - switch type { - case .TodaySurvival: - todayDeepLink(type: .survival, data: data) - case .TodayMission: - todayDeepLink(type: .mission, data: data) - case .Comment: - if data.dateOfPost == Date() { - todayCommentDeepLink(type: .survival, data: data) - } else { - pushCalendarViewController(data: data) - } - default: - fatalError("원하는 정보를 찾을 수 없습니다.") - } - } - private func todayDeepLink(type: PostType, data: NotificationDeepLink) { fetchTodayPost(type: type) { result in guard let result = result else { return } @@ -110,11 +70,9 @@ final class DeepLinkManager { } } -} -extension DeepLinkManager { private func fetchTodayPost(type: PostType, completion: @escaping (PostListPage?) -> Void) { - let dateString = DateFormatter.dashYyyyMMdd.string(from: Date()) + let dateString = Date().toFormatString(with: "yyyy-MM-dd") let query = PostListQuery(date: dateString, type: type) postUseCase.excute(query: query) @@ -150,3 +108,76 @@ extension DeepLinkManager { } } } + +extension DeepLinkManager { + func handleWidgetDeepLink(data: WidgetDeepLink) { + fetchTodayPost(type: .survival) { result in + guard let result = result else { return } + let items = result.postLists.map(PostSection.Item.main) + + items.enumerated().forEach { (index, item) in + switch item { + case .main(let postListData): + if postListData.postId == data.postId { + let indexPath = IndexPath(row: index, section: 0) + self.pushViewController(data: nil, index: indexPath, items: items) + } + } + } + } + } + + func handleDeepLink(data: NotificationDeepLink) { + var type: DeepLinkType = .TodaySurvival + if data.openComment { + if data.dateOfPost.isToday { + type = .TodayComment + } else { + type = .SomedayComment + } + } + print(data) + + switch type { + case .TodaySurvival: + todayDeepLink(type: .survival, data: data) + case .TodayMission: + todayDeepLink(type: .mission, data: data) + case .TodayComment: + todayCommentDeepLink(type: .survival, data: data) + case .SomedayComment: + pushCalendarViewController(data: data) + default: + fatalError("원하는 정보를 찾을 수 없습니다.") + } + } + + func decodeRemoteNotificationDeepLink(_ userInfo: [AnyHashable: Any]) { + if let link = userInfo[AnyHashable("iosDeepLink")] as? String { + print(link) + let components = link.components(separatedBy: "?") + let parameters = components.last?.components(separatedBy: "&") + + // PostID 구하기 + let postId = components.first?.components(separatedBy: "/").last ?? "" + + // OpenComment 구하기 + let firstPart = parameters?.first + let openComment = firstPart?.components(separatedBy: "=").last == "true" ? true : false + + // dateOfPost 구하기 + let secondPart = parameters?.last + let dateOfPost = secondPart?.components(separatedBy: "=").last?.toDate() ?? Date() + + let deepLink = NotificationDeepLink( + postId: postId, + openComment: openComment, + dateOfPost: dateOfPost + ) + + handleDeepLink(data: deepLink) + } else { + print("Error: Decoding Notification Request UserInfo") + } + } +} diff --git a/14th-team5-iOS/Core/Sources/Extensions/String+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/String+Ext.swift index 84ca36e55..4626ccb53 100644 --- a/14th-team5-iOS/Core/Sources/Extensions/String+Ext.swift +++ b/14th-team5-iOS/Core/Sources/Extensions/String+Ext.swift @@ -36,6 +36,13 @@ extension String { guard let date = dateFormatter.date(from: self) else { return .now } return date } + + public func toDate() -> Date? { + let dateFormatter = DateFormatter() + dateFormatter.locale = Locale.current + dateFormatter.dateFormat = "yyyy-MM-dd" + return dateFormatter.date(from: self) + } } extension String { From 9d4e6a4a5c6dfa2c90467b9d7d95a57a16059387 Mon Sep 17 00:00:00 2001 From: Kyoungmi <1mmgm.dev@gmail.com> Date: Tue, 7 May 2024 18:16:54 +0900 Subject: [PATCH 022/263] [refactor]: remove deeplink comment(#504) --- .../ViewControllers/MainViewController.swift | 61 ------------------- 1 file changed, 61 deletions(-) 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 b905319a0..b58d1f21a 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift @@ -122,18 +122,6 @@ extension MainViewController { ) .bind(to: reactor.action) .disposed(by: disposeBag) - -// self.rx.viewWillAppear -// .withUnretained(self) -// // 별도 딥링크를 받지 않으면 -// .filter { -// let repo = $0.0.deepLinkRepo -// return repo.notification.value == nil && repo.widget.value == nil -// } -// // viewWillAppear 메서드 수행하기 -// .map { _ in Reactor.Action.viewWillAppear } -// .bind(to: reactor.action) -// .disposed(by: disposeBag) segmentControl.survivalButton.rx.tap .throttle(.milliseconds(1000), scheduler: MainScheduler.instance) @@ -173,28 +161,6 @@ extension MainViewController { $0.0.navigationController?.pushViewController(cameraViewController, animated: true) }) .disposed(by: disposeBag) - - // 위젯 딥링크 코드 - App.Repository.deepLink.widget - .compactMap { $0 } - .map { Reactor.Action.pushWidgetPostDeepLink($0) } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - // 푸시 노티피케이션 딥링크 코드 - App.Repository.deepLink.notification - .compactMap { $0 } - .flatMap { - // 댓글 푸시 알림이라면 - if $0.openComment { - return Observable.just(Reactor.Action.pushNotificationCommentDeepLink($0)) - // 포스트 푸시 알림이라면 - } else { - return Observable.just(Reactor.Action.pushNotificationPostDeepLink($0)) - } - } - .bind(to: reactor.action) - .disposed(by: disposeBag) } private func bindOutput(reactor: MainViewReactor) { @@ -267,32 +233,5 @@ extension MainViewController { owner.makeErrorBibbiToastView() } .disposed(by: disposeBag) - -// // 위젯 딥링크 코드 -// reactor.pulse(\.$widgetPostDeepLink) -// .delay(RxConst.smallDelayInterval, scheduler: Schedulers.main) -// .compactMap { $0 } -// .bind(with: self) { owner, deepLink in -// owner.handlePostWidgetDeepLink(deepLink) -// } -// .disposed(by: disposeBag) -// -// // 포스트 노티피케이션 딥링크 코드 -// reactor.pulse(\.$notificationPostDeepLink) -// .delay(RxConst.smallDelayInterval, scheduler: Schedulers.main) -// .compactMap { $0 } -// .bind(with: self) { owner, deepLink in -// owner.handlePostNotificationDeepLink(deepLink) -// } -// .disposed(by: disposeBag) -// -// // 댓글 노티피케이션 딥링크 코드 -// reactor.pulse(\.$notificationCommentDeepLink) -// .delay(RxConst.smallDelayInterval, scheduler: Schedulers.main) -// .compactMap { $0 } -// .bind(with: self) { owner, deepLink in -// owner.handleCommentNotificationDeepLink(deepLink) -// } -// .disposed(by: disposeBag) } } From b50621cb34e32dae431a75892fda7bb53029bb24 Mon Sep 17 00:00:00 2001 From: Kyoungmi <1mmgm.dev@gmail.com> Date: Tue, 7 May 2024 18:19:46 +0900 Subject: [PATCH 023/263] [refactor]: remove deepLink MainReactor(#504) --- .../Home/Reactor/MainViewReactor.swift | 33 ------------------- 1 file changed, 33 deletions(-) diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift index 2177a373a..68a372844 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift @@ -58,10 +58,6 @@ final class MainViewReactor: Reactor { case didTapSegmentControl(PostType) case pickConfirmButtonTapped(String, String) - - case pushWidgetPostDeepLink(WidgetDeepLink) - case pushNotificationPostDeepLink(NotificationDeepLink) - case pushNotificationCommentDeepLink(NotificationDeepLink) } enum Mutation { @@ -77,10 +73,6 @@ final class MainViewReactor: Reactor { case setFailureToastMessage case setPickAlertView(String, String) - - case setWidgetPostDeepLink(WidgetDeepLink) - case setNotificationPostDeepLink(NotificationDeepLink) - case setNotificationCommentDeepLink(NotificationDeepLink) } struct State { @@ -98,10 +90,6 @@ final class MainViewReactor: Reactor { @Pulse var familySection: [FamilySection.Item] = [] - @Pulse var widgetPostDeepLink: WidgetDeepLink? - @Pulse var notificationPostDeepLink: NotificationDeepLink? - @Pulse var notificationCommentDeepLink: NotificationDeepLink? - @Pulse var shouldPresentPickAlert: (String, String)? @Pulse var shouldPresentPickSuccessToastMessage: String? @Pulse var shouldPresentCopySuccessToastMessage: Bool = false @@ -178,26 +166,11 @@ extension MainViewReactor { } } - case let .pushWidgetPostDeepLink(deepLink): - return Observable.concat( - Observable.just(.setWidgetPostDeepLink(deepLink)) // 다음 화면으로 이동하기 - ) - - case let .pushNotificationPostDeepLink(deepLink): - return Observable.concat( - Observable.just(.setNotificationPostDeepLink(deepLink)) // 다음 화면으로 이동하기 - ) - - case let .pushNotificationCommentDeepLink(deepLink): - return Observable.concat( - Observable.just(.setNotificationCommentDeepLink(deepLink)) // 다음 화면으로 이동하기 - ) case .didTapSegmentControl(let type): return Observable.concat( Observable.just(.setPageIndex(type.getIndex())), Observable.just(.setBalloonText), Observable.just(.setDescriptionText)) - case let .pickConfirmButtonTapped(name, id): return pickUseCase.executePickMember(memberId: id) @@ -218,12 +191,6 @@ extension MainViewReactor { switch mutation { case .setInTime(let isInTime): newState.isInTime = isInTime - case let.setWidgetPostDeepLink(deepLink): - newState.widgetPostDeepLink = deepLink - case let .setNotificationPostDeepLink(deepLink): - newState.notificationPostDeepLink = deepLink - case let .setNotificationCommentDeepLink(deepLink): - newState.notificationCommentDeepLink = deepLink case .setCopySuccessToastMessage: newState.shouldPresentCopySuccessToastMessage = true case let .setPickSuccessToastMessage(name): From f11a2c672df2b6a46f9d847b8d66b39ded6ed0fc Mon Sep 17 00:00:00 2001 From: Kyoungmi <1mmgm.dev@gmail.com> Date: Tue, 7 May 2024 18:21:18 +0900 Subject: [PATCH 024/263] move file(#504) --- .../App/Sources/{Application => Manager}/DeepLinkManager.swift | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename 14th-team5-iOS/App/Sources/{Application => Manager}/DeepLinkManager.swift (100%) diff --git a/14th-team5-iOS/App/Sources/Application/DeepLinkManager.swift b/14th-team5-iOS/App/Sources/Manager/DeepLinkManager.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/DeepLinkManager.swift rename to 14th-team5-iOS/App/Sources/Manager/DeepLinkManager.swift From d8c5de322d3253c943849e460a52e437e9614f97 Mon Sep 17 00:00:00 2001 From: Do-hyun-Kim Date: Wed, 8 May 2024 00:25:50 +0900 Subject: [PATCH 025/263] =?UTF-8?q?fix:=20KakaoSigninHelper=20loginWithKak?= =?UTF-8?q?aoTalk=20Method=20UniversalLink=EC=97=90=EC=84=9C=20CustomSchem?= =?UTF-8?q?e=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20-=20KakaoTalkSDK=202.20.0=20?= =?UTF-8?q?=EC=97=85=EA=B7=B8=EB=A0=88=EC=9D=B4=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Service/Kakao/KakaoSignInHelper.swift | 2 +- .../xcschemes/Bibbi-Workspace.xcscheme | 70 +++++++++++++++++++ Tuist/Dependencies.swift | 1 - .../Package+Templates.swift | 5 +- 4 files changed, 73 insertions(+), 5 deletions(-) diff --git a/14th-team5-iOS/Data/Sources/Service/Kakao/KakaoSignInHelper.swift b/14th-team5-iOS/Data/Sources/Service/Kakao/KakaoSignInHelper.swift index 9e933db88..2e194106f 100644 --- a/14th-team5-iOS/Data/Sources/Service/Kakao/KakaoSignInHelper.swift +++ b/14th-team5-iOS/Data/Sources/Service/Kakao/KakaoSignInHelper.swift @@ -31,7 +31,7 @@ final class KakaoSignInHelper: AccountSignInHelperType { func signIn(on window: UIWindow) -> Observable { if UserApi.isKakaoTalkLoginAvailable() { - return UserApi.shared.rx.loginWithKakaoTalk() + return UserApi.shared.rx.loginWithKakaoTalk(launchMethod: .CustomScheme) .map { [weak self] response in self?._signInState.accept(AccountSignInStateInfo(snsType: .kakao, snsToken: response.accessToken)) return .success diff --git a/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme b/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme index 72399b415..73b9aa620 100644 --- a/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme +++ b/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme @@ -300,6 +300,20 @@ ReferencedContainer = "container:Tuist/Dependencies/SwiftPackageManager/.build/checkouts/GoogleDataTransport/GoogleDataTransport.xcodeproj"> + + + + + + + + + + + + + + + + + + + + Date: Thu, 9 May 2024 11:01:55 +0900 Subject: [PATCH 026/263] =?UTF-8?q?refactor:=20Calendar=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=BD=94=EB=93=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81=20(#509)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../App/Sources/Manager/DeepLinkManager.swift | 6 +- .../AccountSignInViewController.swift | 6 +- .../AccountDateViewController.swift | 6 +- .../AccountNicknameViewController.swift | 8 +- .../AccountProfileViewController.swift | 8 +- .../AccountSignUpViewController.swift | 2 +- ....swift => DailyCalendarSectionModel.swift} | 2 +- ...er.swift => CalendarCellDIContainer.swift} | 10 +- ...ift => CalendarImageCellDIContainer.swift} | 22 ++-- .../Dependency/CalendarPostDIContainer.swift | 27 ++-- ...wift => MonthlyCalendarDIConatainer.swift} | 10 +- ...r.swift => CalendarImageCellReactor.swift} | 22 ++-- .../Reactor/CalendarPageViewCellReactor.swift | 78 ++++++------ .../Reactor/CalendarPostCellReactor.swift | 8 +- ...r.swift => DailyCalendarViewReactor.swift} | 117 +++++++++--------- ...swift => MonthlyCalendarViewReactor.swift} | 47 ++++--- .../Reactor/ViewModel/BannerViewModel.swift | 4 +- ...endarPageCell.swift => CalendarCell.swift} | 100 +++++++-------- ...ndarCell.swift => CalendarImageCell.swift} | 16 +-- ...ll.swift => CalendarPlaceholderCell.swift} | 4 +- .../Calendar/View/Cell/CalendarPostCell.swift | 17 ++- ...wift => DailyCalendarViewController.swift} | 115 ++++++++--------- ...ft => MonthlyCalendarViewController.swift} | 56 +++++---- .../FamilyManagementViewController.swift | 2 +- .../MainFamilyViewController.swift | 6 +- .../MainPostViewController.swift | 2 +- .../ViewControllers/MainViewController.swift | 8 +- .../Views/MainFamilyCollectionViewCell.swift | 2 +- .../Presentation/Home/Views/TimerView.swift | 2 +- .../InputFamilyLinkViewController.swift | 6 +- .../JoinFamilyViewController.swift | 2 +- .../JoinedFamilyViewController.swift | 8 +- .../OnBoarding/OnBoardingViewController.swift | 6 +- .../PostComment/View/Cell/CommentCell.swift | 2 +- .../PostCommentViewController.swift | 8 +- .../Reactor/MissionTextReactor.swift | 0 .../ViewControllers/PostViewController.swift | 2 +- .../ReactionMembersViewController.swift | 2 +- .../ReactionViewController.swift | 2 +- .../SelectableEmojiViewController.swift | 2 +- .../Views/PostDetailCollectionViewCell.swift | 2 +- .../Splash/SplashViewController.swift | 4 +- .../BibbiNavigationBarView.swift | 4 +- .../Core/Sources/Extensions/Date+Ext.swift | 56 ++++----- .../Core/Sources/Extensions/String+Ext.swift | 5 +- .../Core/Sources/Reactive/RxUtils.swift | 10 +- .../Core/Sources/Token/TokenRepository.swift | 2 +- .../CalendarAPI/CalendarAPIWorker.swift | 4 +- .../Calendar/CalendarAPI/CalendarAPIs.swift | 12 +- .../Service/Apple/AppleSignInHelper.swift | 2 +- .../xcschemes/Bibbi-Workspace.xcscheme | 70 ----------- 51 files changed, 424 insertions(+), 500 deletions(-) rename 14th-team5-iOS/App/Sources/Presentation/Calendar/DataSource/{CalendarPostSectionModel.swift => DailyCalendarSectionModel.swift} (65%) rename 14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/{CalendarPageCellDIContainer.swift => CalendarCellDIContainer.swift} (76%) rename 14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/{ImageCalendarCellDIContainer.swift => CalendarImageCellDIContainer.swift} (66%) rename 14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/{CalendarDIConatainer.swift => MonthlyCalendarDIConatainer.swift} (73%) rename 14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/{ImageCalendarCellReactor.swift => CalendarImageCellReactor.swift} (79%) rename 14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/{CalendarPostViewReactor.swift => DailyCalendarViewReactor.swift} (67%) rename 14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/{CalendarViewReactor.swift => MonthlyCalendarViewReactor.swift} (65%) rename 14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/{CalendarPageCell.swift => CalendarCell.swift} (71%) rename 14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/{ImageCalendarCell.swift => CalendarImageCell.swift} (94%) rename 14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/{PlaceholderCalendarCell.swift => CalendarPlaceholderCell.swift} (86%) rename 14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/{CalendarPostViewController.swift => DailyCalendarViewController.swift} (81%) rename 14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/{CalendarViewController.swift => MonthlyCalendarViewController.swift} (78%) rename 14th-team5-iOS/App/Sources/Presentation/{Calendar => PostDetail}/Reactor/MissionTextReactor.swift (100%) diff --git a/14th-team5-iOS/App/Sources/Manager/DeepLinkManager.swift b/14th-team5-iOS/App/Sources/Manager/DeepLinkManager.swift index 54248b88e..c4e195f71 100644 --- a/14th-team5-iOS/App/Sources/Manager/DeepLinkManager.swift +++ b/14th-team5-iOS/App/Sources/Manager/DeepLinkManager.swift @@ -99,9 +99,9 @@ final class DeepLinkManager { if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, let keyWindow = windowScene.windows.first(where: { $0.isKeyWindow }), let navigationController = keyWindow.rootViewController as? UINavigationController { - let viewController = CalendarPostDIConatainer( - selectedDate: data.dateOfPost, - notificationDeepLink: data + let viewController = weeklyCalendarDIConatainer( + date: data.dateOfPost, + deepLink: data ).makeViewController() navigationController.pushViewController(viewController, animated: true) 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 81f6fe75a..3dc9b3a12 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignIn/AccountSignInViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignIn/AccountSignInViewController.swift @@ -110,19 +110,19 @@ public final class AccountSignInViewController: BaseViewController .disposed(by: disposeBag) nextButton.rx.tap - .throttle(RxConst.throttleInterval, scheduler: Schedulers.main) + .throttle(RxConst.milliseconds300Interval, scheduler: RxSchedulers.main) .map { Reactor.Action.didTapDateNextButton } .bind(to: reactor.action) .disposed(by: disposeBag) @@ -105,7 +105,7 @@ final class AccountDateViewController: BaseViewController private func bindOutput(reactor: AccountSignUpReactor) { reactor.state.map { $0.nickname } .withUnretained(self) - .observe(on: Schedulers.main) + .observe(on: RxSchedulers.main) .bind(onNext: { $0.0.setTitleLabel(with: $0.1) }) .disposed(by: disposeBag) @@ -129,7 +129,7 @@ final class AccountDateViewController: BaseViewController reactor.state.map { $0.isValidDateButton } .withUnretained(self) - .observe(on: Schedulers.main) + .observe(on: RxSchedulers.main) .bind(onNext: { $0.0.validationButton($0.1) }) .disposed(by: disposeBag) } diff --git a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountNicknameViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountNicknameViewController.swift index 2eaade0bc..f85535a9d 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountNicknameViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountNicknameViewController.swift @@ -54,7 +54,7 @@ public final class AccountNicknameViewController: BaseViewController +typealias DailyCalendarSectionModel = SectionModel diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/CalendarPageCellDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/CalendarCellDIContainer.swift similarity index 76% rename from 14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/CalendarPageCellDIContainer.swift rename to 14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/CalendarCellDIContainer.swift index 6cec52069..98b5daabf 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/CalendarPageCellDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/CalendarCellDIContainer.swift @@ -11,7 +11,7 @@ import Core import Data import Domain -public final class CalendarPageCellDIContainer { +public final class CalendarCellDIContainer { // MARK: - Properties public let yearMonth: String @@ -36,7 +36,11 @@ public final class CalendarPageCellDIContainer { return CalendarRepository() } - public func makeReactor() -> CalendarPageCellReactor { - return CalendarPageCellReactor(yearMonth, calendarUseCase: makeUseCase(), provider: globalState) + public func makeReactor() -> CalendarCellReactor { + return CalendarCellReactor( + yearMonth: yearMonth, + calendarUseCase: makeUseCase(), + provider: globalState + ) } } diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/ImageCalendarCellDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/CalendarImageCellDIContainer.swift similarity index 66% rename from 14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/ImageCalendarCellDIContainer.swift rename to 14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/CalendarImageCellDIContainer.swift index 9cd7ec593..2c8e00dae 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/ImageCalendarCellDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/CalendarImageCellDIContainer.swift @@ -11,11 +11,11 @@ import Core import Data import Domain -final public class ImageCalendarCellDIContainer { +final public class CalendarImageCellDIContainer { // MARK: - Properties - public let type: ImageCalendarCellReactor.CalendarType + public let type: CalendarImageCellReactor.CalendarType + public let monthlyEntity: CalendarEntity public let isSelected: Bool - public let dayResponse: CalendarEntity private var globalState: GlobalStateProviderProtocol { guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { @@ -26,13 +26,13 @@ final public class ImageCalendarCellDIContainer { // MARK: - Intializer public init( - _ type: ImageCalendarCellReactor.CalendarType, - isSelected: Bool = false, - dayResponse: CalendarEntity + type: CalendarImageCellReactor.CalendarType, + monthlyEntity: CalendarEntity, + isSelected: Bool = false ) { self.type = type self.isSelected = isSelected - self.dayResponse = dayResponse + self.monthlyEntity = monthlyEntity } // MARK: - Make @@ -44,11 +44,11 @@ final public class ImageCalendarCellDIContainer { return CalendarRepository() } - public func makeReactor() -> ImageCalendarCellReactor { - return ImageCalendarCellReactor( - type, + public func makeReactor() -> CalendarImageCellReactor { + return CalendarImageCellReactor( + type: type, + monthlyEntity: monthlyEntity, isSelected: isSelected, - dayResponse: dayResponse, calendarUseCase: makeUseCase(), provider: globalState ) diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/CalendarPostDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/CalendarPostDIContainer.swift index c00505511..62788928c 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/CalendarPostDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/CalendarPostDIContainer.swift @@ -11,18 +11,19 @@ import Data import Domain import UIKit -public final class CalendarPostDIConatainer { +public final class weeklyCalendarDIConatainer { // MARK: - Properties - let selectedDate: Date - let notificationDeepLink: NotificationDeepLink? // 댓글 푸시 알림 체크 변수 + let date: Date + + let deepLink: NotificationDeepLink? // 댓글 푸시 알림 체크 변수 // MARK: - Intializer init( - selectedDate selection: Date, - notificationDeepLink: NotificationDeepLink? = nil + date: Date, + deepLink: NotificationDeepLink? = nil ) { - self.selectedDate = selection - self.notificationDeepLink = notificationDeepLink + self.date = date + self.deepLink = deepLink } private var globalState: GlobalStateProviderProtocol { @@ -33,8 +34,8 @@ public final class CalendarPostDIConatainer { } // MARK: - Make - public func makeViewController() -> CalendarPostViewController { - return CalendarPostViewController(reactor: makeReactor()) + public func makeViewController() -> DailyCalendarViewController { + return DailyCalendarViewController(reactor: makeReactor()) } public func makeCalendarUseCase() -> CalendarUseCaseProtocol { @@ -45,10 +46,10 @@ public final class CalendarPostDIConatainer { return CalendarRepository() } - public func makeReactor() -> CalendarPostViewReactor { - return CalendarPostViewReactor( - selectedDate, - notificationDeepLink: notificationDeepLink, + public func makeReactor() -> DailyCalendarViewReactor { + return DailyCalendarViewReactor( + date: date, + notificationDeepLink: deepLink, calendarUseCase: makeCalendarUseCase(), provider: globalState ) diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/CalendarDIConatainer.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/MonthlyCalendarDIConatainer.swift similarity index 73% rename from 14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/CalendarDIConatainer.swift rename to 14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/MonthlyCalendarDIConatainer.swift index 7b5fda2d3..786c21685 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/CalendarDIConatainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/MonthlyCalendarDIConatainer.swift @@ -11,7 +11,7 @@ import Core import Data import Domain -public final class CalendarDIConatainer { +public final class MonthlyCalendarDIConatainer { // MARK: - Properties private var globalState: GlobalStateProviderProtocol { guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { @@ -21,8 +21,8 @@ public final class CalendarDIConatainer { } // MARK: - Make - public func makeViewController() -> CalendarViewController { - return CalendarViewController(reactor: makeReactor()) + public func makeViewController() -> MonthlyCalendarViewController { + return MonthlyCalendarViewController(reactor: makeReactor()) } public func makeCalendarUseCase() -> CalendarUseCaseProtocol { @@ -33,8 +33,8 @@ public final class CalendarDIConatainer { return CalendarRepository() } - public func makeReactor() -> CalendarViewReactor { - return CalendarViewReactor( + public func makeReactor() -> MonthlyCalendarViewReactor { + return MonthlyCalendarViewReactor( calendarUseCase: makeCalendarUseCase(), provider: globalState ) diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/ImageCalendarCellReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarImageCellReactor.swift similarity index 79% rename from 14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/ImageCalendarCellReactor.swift rename to 14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarImageCellReactor.swift index 3796fcdbd..96d6ce45b 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/ImageCalendarCellReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarImageCellReactor.swift @@ -12,11 +12,11 @@ import Domain import ReactorKit import RxSwift -final public class ImageCalendarCellReactor: Reactor { +final public class CalendarImageCellReactor: Reactor { // MARK: - Type public enum CalendarType { - case month case week + case month } // MARK: - Action @@ -43,21 +43,21 @@ final public class ImageCalendarCellReactor: Reactor { private let calendarUseCase: CalendarUseCaseProtocol private let provider: GlobalStateProviderProtocol - public var type: CalendarType + public let type: CalendarType // MARK: - Intializer init( - _ type: CalendarType, + type: CalendarType, + monthlyEntity: CalendarEntity, isSelected: Bool, - dayResponse: CalendarEntity, calendarUseCase: CalendarUseCaseProtocol, provider: GlobalStateProviderProtocol ) { self.initialState = State( - date: dayResponse.date, - representativePostId: dayResponse.representativePostId, - representativeThumbnailUrl: dayResponse.representativeThumbnailUrl, - allFamilyMemebersUploaded: dayResponse.allFamilyMemebersUploaded, + date: monthlyEntity.date, + representativePostId: monthlyEntity.representativePostId, + representativeThumbnailUrl: monthlyEntity.representativeThumbnailUrl, + allFamilyMemebersUploaded: monthlyEntity.allFamilyMemebersUploaded, isSelected: isSelected ) @@ -77,14 +77,10 @@ final public class ImageCalendarCellReactor: Reactor { if $0.0.initialState.date.isEqual(with: date) { let lastSelectedDate: Date = $0.0.provider.toastGlobalState.lastSelectedDate // 이전에 선택된 날짜와 같지 않다면 (셀이 재사용되더라도 ToastView가 다시 뜨게 하지 않기 위함) - debugPrint("============ \($0.0.initialState.allFamilyMemebersUploaded) \(date)") - debugPrint("======= \(!lastSelectedDate.isEqual(with: date)),, \($0.0.initialState.allFamilyMemebersUploaded)") if !lastSelectedDate.isEqual(with: date) && $0.0.initialState.allFamilyMemebersUploaded { - debugPrint("============ 토스트됨! \(date)") // 전체 가족 업로드 유무에 따른 토스트 뷰 출력 이벤트 방출함 $0.0.provider.toastGlobalState.showAllFamilyUploadedToastMessageView(selection: date) } - return Observable.just(.selectDate) } else { return Observable.just(.deselectDate) diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPageViewCellReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPageViewCellReactor.swift index 207e09891..fb8c340cc 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPageViewCellReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPageViewCellReactor.swift @@ -13,41 +13,45 @@ import Domain import ReactorKit import RxSwift -public final class CalendarPageCellReactor: Reactor { +public final class CalendarCellReactor: Reactor { // MARK: - Action public enum Action { - case didSelectDate(Date) - case fetchCalendarBanner - case fetchStatisticsSummary - case fetchCalendarResponse - case didTapInfoButton(UIView) + case dateSelected(Date) + case requestBanner + case requestStatistics + case requestMonthlyCalendar + case infoButtonTapped(UIView) } // MARK: - Mutation public enum Mutation { - case injectCalendarBanner(BannerEntity) - case injectStatisticsSummary(FamilyMonthlyStatisticsEntity) - case injectCalendarResponse(ArrayResponseCalendarEntity) + case setBanner(BannerEntity) + case setStatistics(FamilyMonthlyStatisticsEntity) + case setMonthlyCalendar(ArrayResponseCalendarEntity) } // MARK: - State public struct State { - var yearMonthDate: Date - var displayCalendarBanner: BannerViewModel.State? + var yearMonth: String + var displayBanner: BannerViewModel.State? var displayMemoryCount: Int - var displayCalendarResponse: ArrayResponseCalendarEntity? + var displayMonthlyCalendar: ArrayResponseCalendarEntity? } // MARK: - Properties public var initialState: State - public let provider: GlobalStateProviderProtocol private let calendarUseCase: CalendarUseCaseProtocol + private let provider: GlobalStateProviderProtocol // MARK: - Intializer - init(_ yearMonth: String, calendarUseCase: CalendarUseCaseProtocol, provider: GlobalStateProviderProtocol) { + init( + yearMonth: String, + calendarUseCase: CalendarUseCaseProtocol, + provider: GlobalStateProviderProtocol + ) { self.initialState = State( - yearMonthDate: yearMonth.toDate(with: "yyyy-MM"), + yearMonth: yearMonth, displayMemoryCount: 0 ) @@ -58,47 +62,45 @@ public final class CalendarPageCellReactor: Reactor { // MARK: - Mutate public func mutate(action: Action) -> Observable { switch action { - case let .didSelectDate(date): + case let .dateSelected(date): return provider.calendarGlabalState.pushCalendarPostVC(date) .flatMap { _ in Observable.empty() } - case .fetchStatisticsSummary: - let yearMonthString = currentState.yearMonthDate.toFormatString() + case .requestStatistics: + let yearMonth = currentState.yearMonth - return calendarUseCase.executeFetchStatisticsSummary(yearMonth: yearMonthString) + return calendarUseCase.executeFetchStatisticsSummary(yearMonth: yearMonth) .flatMap { guard let statistics = $0 else { return Observable.empty() } - return Observable.just(.injectStatisticsSummary(statistics)) + return Observable.just(.setStatistics(statistics)) } - case .fetchCalendarBanner: - let yearMonthString = currentState.yearMonthDate.toFormatString() + case .requestBanner: + let yearMonth = currentState.yearMonth - return calendarUseCase.executeFetchCalendarBenner(yearMonth: yearMonthString) + return calendarUseCase.executeFetchCalendarBenner(yearMonth: yearMonth) .flatMap { guard let banner = $0 else { return Observable.empty() } - return Observable.just(.injectCalendarBanner(banner)) + return Observable.just(.setBanner(banner)) } - case .fetchCalendarResponse: - let yearMonthString = currentState.yearMonthDate.toFormatString() + case .requestMonthlyCalendar: + let yearMonth = currentState.yearMonth - return calendarUseCase.executeFetchCalednarResponse(yearMonth: yearMonthString) + return calendarUseCase.executeFetchCalednarResponse(yearMonth: yearMonth) .map { guard let arrayCalendarResponse = $0 else { - return .injectCalendarResponse(.init(results: [])) + return .setMonthlyCalendar(.init(results: [])) } - return .injectCalendarResponse(arrayCalendarResponse) + return .setMonthlyCalendar(arrayCalendarResponse) } - case let .didTapInfoButton(sourceView): - return provider.calendarGlabalState.didTapCalendarInfoButton(sourceView) - .flatMap { _ in Observable.empty() } - + case let .infoButtonTapped(sourceView): provider.calendarGlabalState.didTapCalendarInfoButton(sourceView) + return Observable.empty() } } @@ -106,10 +108,10 @@ public final class CalendarPageCellReactor: Reactor { public func reduce(state: State, mutation: Mutation) -> State { var newState = state switch mutation { - case let .injectStatisticsSummary(statistics): + case let .setStatistics(statistics): newState.displayMemoryCount = statistics.totalImageCnt - case let .injectCalendarBanner(banner): + case let .setBanner(banner): let bannerState = BannerViewModel.State( familyTopPercentage: banner.familyTopPercentage, allFamilyMemberUploadedDays: banner.allFammilyMembersUploadedDays, @@ -117,10 +119,10 @@ public final class CalendarPageCellReactor: Reactor { bannerString: banner.bannerString, bannerColor: banner.bannerColor ) - newState.displayCalendarBanner = bannerState + newState.displayBanner = bannerState - case let .injectCalendarResponse(arrayCalendarResponse): - newState.displayCalendarResponse = arrayCalendarResponse + case let .setMonthlyCalendar(arrayCalendarResponse): + newState.displayMonthlyCalendar = arrayCalendarResponse } return newState } diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPostCellReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPostCellReactor.swift index b0fc9e7fb..ea6e8a95b 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPostCellReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPostCellReactor.swift @@ -15,10 +15,10 @@ public final class CalendarPostCellReactor: Reactor { // MARK: - Action public enum Action { - case displayContent + case requestDisplayContent case requestAuthorName case requestAuthorImageUrl - case writerImageButtonTapped + case authorImageButtonTapped } // MARK: - Mutation @@ -58,7 +58,7 @@ public final class CalendarPostCellReactor: Reactor { // MARK: - Mutate public func mutate(action: Action) -> Observable { switch action { - case .displayContent: + case .requestDisplayContent: let content: String = currentState.post.postContent var sectionItem: [DisplayEditItemModel] = [] content.forEach { @@ -84,7 +84,7 @@ public final class CalendarPostCellReactor: Reactor { let authorImageUrl = meUseCase.executeProfileImageUrlString(memberId: authorId) return Observable.just(.setAuthorImageUrl(authorImageUrl)) - case .writerImageButtonTapped: + case .authorImageButtonTapped: let authorId = initialState.post.authorId provider.postGlobalState.pushProfileViewController(authorId) return Observable.empty() diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPostViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/DailyCalendarViewReactor.swift similarity index 67% rename from 14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPostViewReactor.swift rename to 14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/DailyCalendarViewReactor.swift index 2ff738cb3..2119cdee4 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPostViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/DailyCalendarViewReactor.swift @@ -15,44 +15,47 @@ import FSCalendar import ReactorKit import RxSwift -public final class CalendarPostViewReactor: Reactor { +public final class DailyCalendarViewReactor: Reactor { // MARK: - Action public enum Action { - case didSelectDate(Date) - case fetchPostList(Date) - case fetchCalendarResponse(String) - case setBlurImageIndex(Int) - case sendPostToReaction(Int) + case dateSelected(Date) + case requestDailyCalendar(Date) + case requestMonthlyCalendar(String) + case imageIndex(Int) + case renewEmoji(Int) case popViewController } // MARK: - Mutation public enum Mutation { case setAllUploadedToastMessageView(Bool) - case injectCalendarResponse(String, ArrayResponseCalendarEntity) - case injectPostResponse([DailyCalendarEntity]) - case injectBlurImageIndex(Int) - case injectVisiblePost(DailyCalendarEntity) - case renewPostCommentCount(Int) + case setDailyCalendar([DailyCalendarEntity]) + case setMonthlyCalendar(String, ArrayResponseCalendarEntity) + case setImageIndex(Int) + case setVisiblePost(DailyCalendarEntity) + case setSelectionHaptic + case renewCommentCount(Int) case pushProfileViewController(String) case popViewController + case clearNotificationDeepLink - case generateSelectionHaptic } // MARK: - State public struct State { - var selectedDate: Date - var notificationDeepLink: NotificationDeepLink? // 댓글 푸시 알림 체크 변수 + var date: Date - var blurImageUrlString: String? + var imageUrl: String? var visiblePost: DailyCalendarEntity? - @Pulse var displayPostResponse: [CalendarPostSectionModel] - @Pulse var displayCalendarResponse: [String: [CalendarEntity]] + + @Pulse var displayDailyCalendar: [DailyCalendarSectionModel] + @Pulse var displayMonthlyCalendar: [String: [CalendarEntity]] @Pulse var shouldPresentAllUploadedToastMessageView: Bool @Pulse var shouldGenerateSelectionHaptic: Bool @Pulse var shouldPushProfileViewController: String? @Pulse var shouldPopViewController: Bool + + var notificationDeepLink: NotificationDeepLink? // 댓글 푸시 알림 체크 변수 } // MARK: - Properties @@ -68,20 +71,20 @@ public final class CalendarPostViewReactor: Reactor { // MARK: - Intializer init( - _ selection: Date, + date: Date, notificationDeepLink deepLink: NotificationDeepLink?, calendarUseCase: CalendarUseCaseProtocol, provider: GlobalStateProviderProtocol ) { self.initialState = State( - selectedDate: selection, - notificationDeepLink: deepLink, - displayPostResponse: [], - displayCalendarResponse: [:], + date: date, + displayDailyCalendar: [], + displayMonthlyCalendar: [:], shouldPresentAllUploadedToastMessageView: false, shouldGenerateSelectionHaptic: false, shouldPushProfileViewController: nil, - shouldPopViewController: false + shouldPopViewController: false, + notificationDeepLink: deepLink ) self.calendarUseCase = calendarUseCase @@ -91,20 +94,20 @@ public final class CalendarPostViewReactor: Reactor { // MARK: - Transfor public func transform(mutation: Observable) -> Observable { let toastMutation = provider.toastGlobalState.event - .flatMap { - switch $0 { + .flatMap { event -> Observable in + switch event { case let .showAllFamilyUploadedToastView(uploaded): return Observable.just(.setAllUploadedToastMessageView(uploaded)) } } let postMutation = provider.postGlobalState.event - .flatMap { event in + .flatMap { event -> Observable in switch event { case let .pushProfileViewController(memberId): return Observable.just(.pushProfileViewController(memberId)) case let .renewalPostCommentCount(count): - return Observable.just(.renewPostCommentCount(count)) + return Observable.just(.renewCommentCount(count)) } } @@ -118,17 +121,17 @@ public final class CalendarPostViewReactor: Reactor { provider.toastGlobalState.clearToastMessageEvent() return Observable.just(.popViewController) - case let .didSelectDate(date): + case let .dateSelected(date): // 처음 이벤트를 받거나 썸네일 이미지가 존재하는 셀에 한하여 if !hasReceivedSelectionEvent || hasThumbnailImages.contains(date) { hasReceivedSelectionEvent = true // 셀 클릭 이벤트 방출 provider.calendarGlabalState.didSelectDate(date) - return Observable.just(.generateSelectionHaptic) + return Observable.just(.setSelectionHaptic) } return Observable.empty() - case let .fetchPostList(date): + case let .requestDailyCalendar(date): // 처음 이벤트를 받거나 썸네일 이미지가 존재하는 셀에 한하여 if !hasReceivedPostEvent || hasThumbnailImages.contains(date) { hasReceivedPostEvent = true @@ -141,44 +144,44 @@ public final class CalendarPostViewReactor: Reactor { } return Observable.concat( - Observable.just(.injectPostResponse(posts)), - Observable.just(.injectBlurImageIndex(0)), + Observable.just(.setDailyCalendar(posts)), + Observable.just(.setImageIndex(0)), Observable.just(.clearNotificationDeepLink) ) } } return Observable.empty() - case let .fetchCalendarResponse(yearMonth): + case let .requestMonthlyCalendar(yearMonth): // 이전에 불러온 적이 없다면 if !hasFetchedCalendarResponse.contains(yearMonth) { return calendarUseCase.executeFetchCalednarResponse(yearMonth: yearMonth) .withUnretained(self) .map { guard let arrayCalendarResponse = $0.1 else { - return .injectCalendarResponse(yearMonth, .init(results: [])) + return .setMonthlyCalendar(yearMonth, .init(results: [])) } $0.0.hasFetchedCalendarResponse.append(yearMonth) $0.0.hasThumbnailImages.append( contentsOf: arrayCalendarResponse.results.map { $0.date } ) - // - 썸네일 이미지 등 데이터가 존재하는 일(日)자에 한하여 데이터를 불러옴 - return .injectCalendarResponse(yearMonth, arrayCalendarResponse) + // NOTE: - 썸네일 이미지가 존재하는 일(日)자에 한하여 데이터를 불러옴 + return .setMonthlyCalendar(yearMonth, arrayCalendarResponse) } // 이전에 불러온 적이 있다면 } else { return Observable.empty() } - case let .setBlurImageIndex(index): - return Observable.just(.injectBlurImageIndex(index)) + case let .imageIndex(index): + return Observable.just(.setImageIndex(index)) - case let .sendPostToReaction(index): - guard let dataSource = currentState.displayPostResponse.first else { + case let .renewEmoji(index): + guard let dataSource = currentState.displayDailyCalendar.first else { return Observable.empty() } let post = dataSource.items[index] - return Observable.just(.injectVisiblePost(post)) + return Observable.just(.setVisiblePost(post)) } } @@ -187,37 +190,37 @@ public final class CalendarPostViewReactor: Reactor { var newState = state switch mutation { - case let .injectBlurImageIndex(index): - guard let items = newState.displayPostResponse.first?.items else { + case let .setImageIndex(index): + guard let items = newState.displayDailyCalendar.first?.items else { return newState } - newState.blurImageUrlString = items[index].postImageUrl + newState.imageUrl = items[index].postImageUrl - case let .renewPostCommentCount(count): - guard var posts = currentState.displayPostResponse.first?.items, + case let .renewCommentCount(count): + guard var posts = currentState.displayDailyCalendar.first?.items, let index = posts.firstIndex(where: { post in post.postId == currentState.visiblePost?.postId }) else { return newState } - guard var renewedPost = currentState.visiblePost else { return newState } + guard var renewedPost = currentState.visiblePost else { + return newState + } renewedPost.commentCount = count - posts[index] = renewedPost - - newState.visiblePost = posts[index] // ReactionViewController 데이터 갱신하기 - newState.displayPostResponse = [.init(model: (), items: posts)] // PostColelctionView 데이터 갱신하기 + newState.visiblePost = posts[index] + newState.displayDailyCalendar = [.init(model: (), items: posts)] case let .setAllUploadedToastMessageView(uploaded): newState.shouldPresentAllUploadedToastMessageView = uploaded - case let .injectCalendarResponse(yearMonth, arrayCalendarResponse): - newState.displayCalendarResponse[yearMonth] = arrayCalendarResponse.results + case let .setMonthlyCalendar(yearMonth, arrayCalendarResponse): + newState.displayMonthlyCalendar[yearMonth] = arrayCalendarResponse.results - case let .injectPostResponse(postResponse): - newState.displayPostResponse = [CalendarPostSectionModel(model: (), items: postResponse)] + case let .setDailyCalendar(postResponse): + newState.displayDailyCalendar = [DailyCalendarSectionModel(model: (), items: postResponse)] - case let .injectVisiblePost(post): + case let .setVisiblePost(post): newState.visiblePost = post case let .pushProfileViewController(memberId): @@ -229,7 +232,7 @@ public final class CalendarPostViewReactor: Reactor { case .clearNotificationDeepLink: newState.notificationDeepLink = nil - case .generateSelectionHaptic: + case .setSelectionHaptic: newState.shouldGenerateSelectionHaptic = true } diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/MonthlyCalendarViewReactor.swift similarity index 65% rename from 14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarViewReactor.swift rename to 14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/MonthlyCalendarViewReactor.swift index e3d3f764c..0f0977639 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/MonthlyCalendarViewReactor.swift @@ -13,25 +13,25 @@ import Domain import ReactorKit import RxSwift -public final class CalendarViewReactor: Reactor { +public final class MonthlyCalendarViewReactor: Reactor { // MARK: - Action public enum Action { case popViewController - case addCalendarItem([String]) + case addCalendarItems([String]) } // MARK: - Mutation public enum Mutation { case popViewController - case pushCalendarPostVC(Date) - case makeCalendarPopoverVC(UIView) - case injectYearMonthItem([String]) + case pushDailyCalendarViewController(Date) + case setInfoPopover(UIView) + case setCalendarItems([String]) } // MARK: - State public struct State { - @Pulse var shouldPopCalendarVC: Bool - @Pulse var shouldPushCalendarPostVC: Date? + @Pulse var shouldPopViewController: Bool + @Pulse var shouldPushDailyCalendarViewController: Date? @Pulse var shouldPresnetInfoPopover: UIView? @Pulse var displayCalendar: [MonthlyCalendarSectionModel] } @@ -43,9 +43,12 @@ public final class CalendarViewReactor: Reactor { private let calendarUseCase: CalendarUseCaseProtocol // MARK: - Intializer - init(calendarUseCase: CalendarUseCaseProtocol, provider: GlobalStateProviderProtocol) { + init( + calendarUseCase: CalendarUseCaseProtocol, + provider: GlobalStateProviderProtocol + ) { self.initialState = State( - shouldPopCalendarVC: false, + shouldPopViewController: false, displayCalendar: [.init(model: (), items: [])] ) @@ -59,10 +62,10 @@ public final class CalendarViewReactor: Reactor { .flatMap { event -> Observable in switch event { case let .pushCalendarPostVC(date): - return Observable.just(.pushCalendarPostVC(date)) + return Observable.just(.pushDailyCalendarViewController(date)) case let .didTapInfoButton(sourceView): - return Observable.just(.makeCalendarPopoverVC(sourceView)) + return Observable.just(.setInfoPopover(sourceView)) default: return Observable.empty() @@ -79,8 +82,8 @@ public final class CalendarViewReactor: Reactor { provider.toastGlobalState.clearLastSelectedDate() return Observable.just(.popViewController) - case let .addCalendarItem(yearMonth): - return Observable.just(.injectYearMonthItem(yearMonth)) + case let .addCalendarItems(items): + return Observable.just(.setCalendarItems(items)) } } @@ -90,22 +93,18 @@ public final class CalendarViewReactor: Reactor { var newState = state switch mutation { case .popViewController: - newState.shouldPopCalendarVC = true + newState.shouldPopViewController = true - case let .pushCalendarPostVC(date): - newState.shouldPushCalendarPostVC = date + case let .pushDailyCalendarViewController(date): + newState.shouldPushDailyCalendarViewController = date - case let .makeCalendarPopoverVC(sourceView): + case let .setInfoPopover(sourceView): newState.shouldPresnetInfoPopover = sourceView - case let .injectYearMonthItem(dateArray): - guard let datasource: MonthlyCalendarSectionModel = state.displayCalendar.first else { - return state - } - + case let .setCalendarItems(items): let newDatasource = MonthlyCalendarSectionModel( - original: datasource, - items: dateArray + model: (), + items: items ) newState.displayCalendar = [newDatasource] } diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/ViewModel/BannerViewModel.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/ViewModel/BannerViewModel.swift index c8832c7ad..0b5f3b6f8 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/ViewModel/BannerViewModel.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/ViewModel/BannerViewModel.swift @@ -8,7 +8,7 @@ import Core import SwiftUI -public final class BannerViewModel: BaseViewModel { +public final class BannerViewModel: BaseViewModel { public struct State: ViewModelState { var familyTopPercentage: Int = 0 var allFamilyMemberUploadedDays: Int = 0 @@ -17,7 +17,7 @@ public final class BannerViewModel: BaseViewModel { +final class CalendarCell: BaseCollectionViewCell { + // MARK: - Id + static var id: String = "CalendarCell" + // MARK: - Views private lazy var labelStack: UIStackView = UIStackView() - private let calendarTitleLabel: BibbiLabel = BibbiLabel(.head2Bold, textAlignment: .center, textColor: .gray200) - private let memoryCountLabel: BibbiLabel = BibbiLabel(.body1Regular, textColor: .gray200) + private let titleLabel: BibbiLabel = BibbiLabel(.head2Bold, textAlignment: .center, textColor: .gray200) + private let countLabel: BibbiLabel = BibbiLabel(.body1Regular, textColor: .gray200) private let infoButton: UIButton = UIButton(type: .system) private lazy var bannerView: BannerView = BannerView(viewModel: bannerViewModel) @@ -31,59 +34,48 @@ final class CalendarPageCell: BaseCollectionViewCell { private let calendarView: FSCalendar = FSCalendar() // MARK: - Properties - private let infoCircleFill: UIImage = DesignSystemAsset.infoCircleFill.image + private let infoImage: UIImage = DesignSystemAsset.infoCircleFill.image .withRenderingMode(.alwaysTemplate) private lazy var bannerViewModel: BannerViewModel = BannerViewModel(reactor: reactor, state: .init()) - static var id: String = "CalendarPageCell" - - // MARK: - Intializer - override init(frame: CGRect) { - super.init(frame: .zero) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - // MARK: - Helpers - override func bind(reactor: CalendarPageCellReactor) { + override func bind(reactor: CalendarCellReactor) { super.bind(reactor: reactor) bindInput(reactor: reactor) bindOutput(reactor: reactor) } - private func bindInput(reactor: CalendarPageCellReactor) { + private func bindInput(reactor: CalendarCellReactor) { Observable.just(()) - .map { Reactor.Action.fetchCalendarBanner } + .map { Reactor.Action.requestBanner } .bind(to: reactor.action) .disposed(by: disposeBag) Observable.just(()) - .map { Reactor.Action.fetchStatisticsSummary } + .map { Reactor.Action.requestStatistics } .bind(to: reactor.action) .disposed(by: disposeBag) Observable.just(()) - .map { Reactor.Action.fetchCalendarResponse } + .map { Reactor.Action.requestMonthlyCalendar } .bind(to: reactor.action) .disposed(by: disposeBag) infoButton.rx.tap - .throttle(RxConst.throttleInterval, scheduler: MainScheduler.instance) + .throttle(RxConst.milliseconds300Interval, scheduler: MainScheduler.instance) .withUnretained(self) - .map { Reactor.Action.didTapInfoButton($0.0.infoButton) } + .map { Reactor.Action.infoButtonTapped($0.0.infoButton) } .bind(to: reactor.action) .disposed(by: disposeBag) calendarView.rx.didSelect - .map { Reactor.Action.didSelectDate($0) } + .map { Reactor.Action.dateSelected($0) } .bind(to: reactor.action) .disposed(by: disposeBag) } - private func bindOutput(reactor: CalendarPageCellReactor) { - reactor.state.compactMap { $0.displayCalendarBanner } + private func bindOutput(reactor: CalendarCellReactor) { + reactor.state.compactMap { $0.displayBanner } .distinctUntilChanged(\.familyTopPercentage) .withUnretained(self) .subscribe { @@ -93,34 +85,36 @@ final class CalendarPageCell: BaseCollectionViewCell { reactor.state.map { $0.displayMemoryCount } .distinctUntilChanged() - .bind(to: memoryCountLabel.rx.memoryCountText) + .bind(to: countLabel.rx.memoryCountText) .disposed(by: disposeBag) - reactor.state.map { $0.displayCalendarResponse } + reactor.state.map { $0.displayMonthlyCalendar } .withUnretained(self) .subscribe { $0.0.calendarView.reloadData() } .disposed(by: disposeBag) - let currentCellDate = reactor.state.map({ $0.yearMonthDate }).asDriver(onErrorJustReturn: .now) + let currentDate = reactor.state.map { $0.yearMonth } + .map { $0.toDate(with: .dashYyyyMM) } + .asDriver(onErrorJustReturn: .now) - currentCellDate + currentDate .distinctUntilChanged() .drive(calendarView.rx.currentPage) .disposed(by: disposeBag) - currentCellDate + currentDate .distinctUntilChanged() - .drive(calendarTitleLabel.rx.calendarTitleText) + .drive(titleLabel.rx.calendarTitleText) .disposed(by: disposeBag) } override func setupUI() { super.setupUI() contentView.addSubviews( - labelStack, memoryCountLabel, bannerController.view, calendarView + labelStack, countLabel, bannerController.view, calendarView ) labelStack.addArrangedSubviews( - calendarTitleLabel, infoButton + titleLabel, infoButton ) } @@ -131,7 +125,7 @@ final class CalendarPageCell: BaseCollectionViewCell { $0.leading.equalToSuperview().offset(24) } - memoryCountLabel.snp.makeConstraints { + countLabel.snp.makeConstraints { $0.top.equalToSuperview().offset(24) $0.trailing.equalToSuperview().offset(-24) } @@ -157,7 +151,7 @@ final class CalendarPageCell: BaseCollectionViewCell { super.setupAttributes() infoButton.do { $0.setImage( - infoCircleFill, + infoImage, for: .normal ) $0.tintColor = .gray300 @@ -194,8 +188,8 @@ final class CalendarPageCell: BaseCollectionViewCell { $0.backgroundColor = UIColor.clear $0.locale = Locale(identifier: "ko_kr") - $0.register(ImageCalendarCell.self, forCellReuseIdentifier: ImageCalendarCell.id) - $0.register(PlaceholderCalendarCell.self, forCellReuseIdentifier: PlaceholderCalendarCell.id) + $0.register(CalendarImageCell.self, forCellReuseIdentifier: CalendarImageCell.id) + $0.register(CalendarPlaceholderCell.self, forCellReuseIdentifier: CalendarPlaceholderCell.id) $0.delegate = self $0.dataSource = self @@ -208,14 +202,14 @@ final class CalendarPageCell: BaseCollectionViewCell { } // MARK: - Extensions -extension CalendarPageCell: FSCalendarDelegate { +extension CalendarCell: FSCalendarDelegate { func calendar(_ calendar: FSCalendar, shouldSelect date: Date, at monthPosition: FSCalendarMonthPosition) -> Bool { - let dateMonth = date.month + let month = date.month let currentMonth = calendar.currentPage.month - if let calendarCell = calendar.cell(for: date, at: monthPosition) as? ImageCalendarCell { + if let calendarCell = calendar.cell(for: date, at: monthPosition) as? CalendarImageCell { // 셀의 날짜가 현재 월(月)과 동일하고, 썸네일 이미지가 있다면 - if dateMonth == currentMonth && calendarCell.hasThumbnailImage { + if month == currentMonth && calendarCell.hasThumbnailImage { return true } } @@ -224,45 +218,45 @@ extension CalendarPageCell: FSCalendarDelegate { } } -extension CalendarPageCell: FSCalendarDataSource { +extension CalendarCell: FSCalendarDataSource { func calendar(_ calendar: FSCalendar, cellFor date: Date, at position: FSCalendarMonthPosition) -> FSCalendarCell { let calendarMonth = calendar.currentPage.month let positionMonth = date.month // 셀의 날짜가 현재 월(月)과 동일하다면 if calendarMonth == positionMonth { let cell = calendar.dequeueReusableCell( - withIdentifier: ImageCalendarCell.id, + withIdentifier: CalendarImageCell.id, for: date, at: position - ) as! ImageCalendarCell + ) as! CalendarImageCell // 해당 일자에 데이터가 존재하지 않는다면 - guard let dayResponse = reactor?.currentState.displayCalendarResponse?.results.filter({ $0.date == date }).first else { + guard let dayResponse = reactor?.currentState.displayMonthlyCalendar?.results.filter({ $0.date == date }).first else { let emptyResponse = CalendarEntity( date: date, representativePostId: .none, representativeThumbnailUrl: .none, allFamilyMemebersUploaded: false ) - cell.reactor = ImageCalendarCellDIContainer( - .month, - dayResponse: emptyResponse + cell.reactor = CalendarImageCellDIContainer( + type: .month, + monthlyEntity: emptyResponse ).makeReactor() return cell } - cell.reactor = ImageCalendarCellDIContainer( - .month, - dayResponse: dayResponse + cell.reactor = CalendarImageCellDIContainer( + type: .month, + monthlyEntity: dayResponse ).makeReactor() return cell // 셀의 날짜가 현재 월(月)과 동일하지 않다면 } else { let cell = calendar.dequeueReusableCell( - withIdentifier: PlaceholderCalendarCell.id, + withIdentifier: CalendarPlaceholderCell.id, for: date, at: position - ) as! PlaceholderCalendarCell + ) as! CalendarPlaceholderCell return cell } } diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/ImageCalendarCell.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarImageCell.swift similarity index 94% rename from 14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/ImageCalendarCell.swift rename to 14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarImageCell.swift index 349ef059f..f5cf31849 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/ImageCalendarCell.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarImageCell.swift @@ -17,7 +17,10 @@ import RxSwift import SnapKit import Then -final public class ImageCalendarCell: FSCalendarCell, ReactorKit.View { +final public class CalendarImageCell: FSCalendarCell, ReactorKit.View { + // MARK: - Id + static let id: String = "ImageCalendarCell" + // MARK: - Views private let dayLabel: BibbiLabel = BibbiLabel(.body1Regular, textAlignment: .center) private let containerView: UIView = UIView() @@ -28,8 +31,6 @@ final public class ImageCalendarCell: FSCalendarCell, ReactorKit.View { // MARK: - Properties public var disposeBag: RxSwift.DisposeBag = DisposeBag() - static let id: String = "ImageCalendarCell" - // MARK: - Intializer public override init!(frame: CGRect) { super.init(frame: .zero) @@ -42,6 +43,7 @@ final public class ImageCalendarCell: FSCalendarCell, ReactorKit.View { fatalError("init(coder:) has not been implemented") } + // MARK: - LifeCycles public override func prepareForReuse() { dayLabel.textColor = UIColor.bibbiWhite thumbnailView.image = nil @@ -52,14 +54,14 @@ final public class ImageCalendarCell: FSCalendarCell, ReactorKit.View { } // MARK: - Helpers - public func bind(reactor: ImageCalendarCellReactor) { + public func bind(reactor: CalendarImageCellReactor) { bindInput(reactor: reactor) bindOutput(reactor: reactor) } - private func bindInput(reactor: ImageCalendarCellReactor) { } + private func bindInput(reactor: CalendarImageCellReactor) { } - private func bindOutput(reactor: ImageCalendarCellReactor) { + private func bindOutput(reactor: CalendarImageCellReactor) { reactor.state.map { "\($0.date.day)" } .distinctUntilChanged() .bind(to: dayLabel.rx.text) @@ -183,7 +185,7 @@ final public class ImageCalendarCell: FSCalendarCell, ReactorKit.View { } // MARK: - Extensions -extension ImageCalendarCell { +extension CalendarImageCell { var hasThumbnailImage: Bool { return thumbnailView.image != nil ? true : false } diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/PlaceholderCalendarCell.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarPlaceholderCell.swift similarity index 86% rename from 14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/PlaceholderCalendarCell.swift rename to 14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarPlaceholderCell.swift index c2f05116e..8180f0f59 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/PlaceholderCalendarCell.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarPlaceholderCell.swift @@ -16,9 +16,9 @@ import RxSwift import SnapKit import Then -final class PlaceholderCalendarCell: FSCalendarCell { +final class CalendarPlaceholderCell: FSCalendarCell { // MARK: - Properties - static let id: String = "PlaceholderCalendarCell" + static let id: String = "CalendarPlaceholderCell" // MARK: - Intializer override init(frame: CGRect) { diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarPostCell.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarPostCell.swift index 38cda1644..7e2c27a0a 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarPostCell.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarPostCell.swift @@ -61,7 +61,7 @@ final class CalendarPostCell: BaseCollectionViewCell { Observable.just(()) .flatMap { _ in Observable.concat( - Observable.just(.displayContent), + Observable.just(.requestDisplayContent), Observable.just(.requestAuthorName), Observable.just(.requestAuthorImageUrl) ) @@ -70,8 +70,8 @@ final class CalendarPostCell: BaseCollectionViewCell { .disposed(by: disposeBag) authorImageContainerView.rx.tap - .throttle(RxConst.throttleInterval, scheduler: Schedulers.main) - .map { Reactor.Action.writerImageButtonTapped } + .throttle(RxConst.milliseconds300Interval, scheduler: RxSchedulers.main) + .map { Reactor.Action.authorImageButtonTapped } .bind(to: reactor.action) .disposed(by: disposeBag) } @@ -92,20 +92,25 @@ final class CalendarPostCell: BaseCollectionViewCell { .disposed(by: disposeBag) post - .map { _ in /*$0.missionContent*/ "정신차려~" } + .map { $0.missionContent} .distinctUntilChanged() .drive(missionTextView.missionLabel.rx.text) .disposed(by: disposeBag) + post + .map { $0.missionContent.isEmpty } + .distinctUntilChanged() + .drive(missionTextView.rx.isHidden) + .disposed(by: disposeBag) + reactor.state.map { $0.authorName } .distinctUntilChanged() .bind(to: authorNameLabel.rx.text) .disposed(by: disposeBag) reactor.state.compactMap { $0.authorName } - .map { String($0.first ?? Character(" ")) } .distinctUntilChanged() - .bind(to: authorFirstNameLabel.rx.text) + .bind(to: authorFirstNameLabel.rx.firtNameText) .disposed(by: disposeBag) reactor.state.compactMap { $0.authorImageUrl } diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/CalendarPostViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/DailyCalendarViewController.swift similarity index 81% rename from 14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/CalendarPostViewController.swift rename to 14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/DailyCalendarViewController.swift index 2e6c3c709..3991067c7 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/CalendarPostViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/DailyCalendarViewController.swift @@ -20,10 +20,9 @@ import SnapKit import Then fileprivate typealias _Str = CalendarStrings -public final class CalendarPostViewController: BaseViewController { +public final class DailyCalendarViewController: BaseViewController { // MARK: - Views - private let blurImageView: UIImageView = UIImageView() - + private let imageView: UIImageView = UIImageView() private let calendarView: FSCalendar = FSCalendar() private lazy var postCollectionView: UICollectionView = UICollectionView( frame: .zero, @@ -35,7 +34,6 @@ public final class CalendarPostViewController: BaseViewController.just(selectedDate) + private func bindInput(reactor: DailyCalendarViewReactor) { + Observable.just(reactor.initialState.date) .flatMap { Observable.merge( - Observable.just(Reactor.Action.didSelectDate($0)), - Observable.just(Reactor.Action.fetchPostList($0)) + Observable.just(Reactor.Action.dateSelected($0)), + Observable.just(Reactor.Action.requestDailyCalendar($0)) ) } .bind(to: reactor.action) .disposed(by: disposeBag) - let previousNextMonths: [String] = reactor.currentState.selectedDate.createPreviousNextDateStringArray() + let previousNextMonths: [String] = reactor.currentState.date.makePreviousNextMonth() Observable.from(previousNextMonths) - .map { Reactor.Action.fetchCalendarResponse($0) } + .map { Reactor.Action.requestMonthlyCalendar($0) } .bind(to: reactor.action) .disposed(by: disposeBag) @@ -90,8 +85,8 @@ public final class CalendarPostViewController: BaseViewController.from($0) - .map { Reactor.Action.fetchCalendarResponse($0) } + .map { Reactor.Action.requestMonthlyCalendar($0) } } .bind(to: reactor.action) .disposed(by: disposeBag) @@ -120,21 +115,11 @@ public final class CalendarPostViewController: BaseViewController RxCollectionViewSectionedReloadDataSource { - return RxCollectionViewSectionedReloadDataSource { datasource, collectionView, indexPath, post in +extension DailyCalendarViewController { + private func prepareDatasource() -> RxCollectionViewSectionedReloadDataSource { + return RxCollectionViewSectionedReloadDataSource { datasource, collectionView, indexPath, post in let cell = collectionView.dequeueReusableCell( withReuseIdentifier: CalendarPostCell.id, for: indexPath @@ -444,7 +429,7 @@ extension CalendarPostViewController { let blurEffect = UIBlurEffect(style: .systemThinMaterialDark) let visualEffectView = UIVisualEffectView(effect: blurEffect) visualEffectView.frame = view.frame - blurImageView.insertSubview(visualEffectView, at: 0) + imageView.insertSubview(visualEffectView, at: 0) } private func setupNavigationTitle(_ date: Date) { @@ -481,7 +466,7 @@ extension CalendarPostViewController { } } -extension CalendarPostViewController { +extension DailyCalendarViewController { private func didTapCameraButtonNotifcationHandler() { NotificationCenter.default .rx.notification(.didTapSelectableCameraButton) @@ -493,36 +478,36 @@ extension CalendarPostViewController { } } -extension CalendarPostViewController: FSCalendarDataSource { +extension DailyCalendarViewController: FSCalendarDataSource { public func calendar(_ calendar: FSCalendar, cellFor date: Date, at position: FSCalendarMonthPosition) -> FSCalendarCell { let cell = calendar.dequeueReusableCell( - withIdentifier: ImageCalendarCell.id, + withIdentifier: CalendarImageCell.id, for: date, at: position - ) as! ImageCalendarCell + ) as! CalendarImageCell // 해당 일에 불러온 데이터가 없다면 - let yyyyMM: String = date.toFormatString() + let yearMonth: String = date.toFormatString(with: .dashYyyyMM) guard let currentState = reactor?.currentState, - let dayResponse = currentState.displayCalendarResponse[yyyyMM]?.filter({ $0.date.isEqual(with: date) }).first + let monthlyEntity = currentState.displayMonthlyCalendar[yearMonth]?.filter({ $0.date.isEqual(with: date) }).first else { - let emptyResponse = CalendarEntity( + let emptyEntity = CalendarEntity( date: date, representativePostId: .none, representativeThumbnailUrl: .none, allFamilyMemebersUploaded: false ) - cell.reactor = ImageCalendarCellDIContainer( - .week, - dayResponse: emptyResponse + cell.reactor = CalendarImageCellDIContainer( + type: .week, + monthlyEntity: emptyEntity ).makeReactor() return cell } - cell.reactor = ImageCalendarCellDIContainer( - .week, - isSelected: currentState.selectedDate.isEqual(with: date), - dayResponse: dayResponse + cell.reactor = CalendarImageCellDIContainer( + type: .week, + monthlyEntity: monthlyEntity, + isSelected: currentState.date.isEqual(with: date) ).makeReactor() return cell } diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/CalendarViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/MonthlyCalendarViewController.swift similarity index 78% rename from 14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/CalendarViewController.swift rename to 14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/MonthlyCalendarViewController.swift index 0b9590cc5..304302739 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/CalendarViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/MonthlyCalendarViewController.swift @@ -17,7 +17,7 @@ import SnapKit import Then fileprivate typealias _Str = CalendarStrings -public final class CalendarViewController: BaseViewController { +public final class MonthlyCalendarViewController: BaseViewController { // MARK: - Views private lazy var calendarCollectionView: UICollectionView = UICollectionView( frame: .zero, @@ -33,19 +33,18 @@ public final class CalendarViewController: BaseViewController.just(()) - .delay(RxConst.smallDelayInterval, scheduler: Schedulers.main) - .withUnretained(self) - .subscribe { + .delay(RxConst.milliseconds100Interval, scheduler: RxSchedulers.main) + .bind(with: self) { owner, _ in UIView.transition( - with: $0.0.calendarCollectionView, + with: owner.calendarCollectionView, duration: 0.15, options: .transitionCrossDissolve ) { [weak self] in @@ -58,12 +57,12 @@ public final class CalendarViewController: BaseViewController RxCollectionViewSectionedReloadDataSource { return RxCollectionViewSectionedReloadDataSource { datasource, collectionView, indexPath, yearMonth in - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CalendarPageCell.id, for: indexPath) as! CalendarPageCell - cell.reactor = CalendarPageCellDIContainer(yearMonth: yearMonth).makeReactor() + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CalendarCell.id, for: indexPath) as! CalendarCell + cell.reactor = CalendarCellDIContainer( + yearMonth: yearMonth + ).makeReactor() return cell } } - private func pushCalendarPostView(_ date: Date) { + private func pushWeeklyCalendarViewController(_ date: Date) { navigationController?.pushViewController( - CalendarPostDIConatainer(selectedDate: date).makeViewController(), + weeklyCalendarDIConatainer( + date: date + ).makeViewController(), animated: true ) } @@ -192,12 +199,13 @@ extension CalendarViewController { } } -extension CalendarViewController { +extension MonthlyCalendarViewController { + // TODO: - Item 생성 로직을 다른 곳으로 이동하기 private func createCalendarItems(from startDate: Date, to endDate: Date = Date()) -> [String] { var items: [String] = [] let calendar: Calendar = Calendar.current - let monthInterval: Int = monthBetween(from: startDate, to: endDate) + let monthInterval: Int = getMonthInterval(from: startDate, to: endDate) for value in 0...monthInterval { if let date = calendar.date(byAdding: .month, value: value, to: startDate) { @@ -209,7 +217,7 @@ extension CalendarViewController { return items } - private func monthBetween(from startDate: Date, to endDate: Date) -> Int { + private func getMonthInterval(from startDate: Date, to endDate: Date) -> Int { let calendar: Calendar = Calendar.current let startComponents = calendar.dateComponents([.year, .month], from: startDate) @@ -223,7 +231,7 @@ extension CalendarViewController { } } -extension CalendarViewController: UIPopoverPresentationControllerDelegate { +extension MonthlyCalendarViewController: UIPopoverPresentationControllerDelegate { public func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle { return .none } diff --git a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/ViewController/FamilyManagementViewController.swift b/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/ViewController/FamilyManagementViewController.swift index 4ddd747cf..dd94c5bc5 100644 --- a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/ViewController/FamilyManagementViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/ViewController/FamilyManagementViewController.swift @@ -66,7 +66,7 @@ public final class FamilyManagementViewController: BaseViewController ProfileData? in switch item { case let .main(reactor): return reactor.currentState.profileData @@ -121,7 +121,7 @@ extension MainFamilyViewController { .disposed(by: disposeBag) reactor.pulse(\.$familyInvitationLink) - .observe(on: Schedulers.main) + .observe(on: RxSchedulers.main) .withUnretained(self) .bind(onNext: { $0.0.makeInvitationUrlSharePanel( diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainPostViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainPostViewController.swift index a52af396b..c4ede9230 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainPostViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainPostViewController.swift @@ -77,7 +77,7 @@ extension MainPostViewController { .disposed(by: disposeBag) postCollectionView.rx.itemSelected - .throttle(RxConst.throttleInterval, scheduler: Schedulers.main) + .throttle(RxConst.milliseconds300Interval, scheduler: RxSchedulers.main) .withUnretained(self) .bind(onNext: { $0.0.navigationController?.pushViewController( 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 b58d1f21a..8259a1adc 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift @@ -136,15 +136,15 @@ extension MainViewController { .disposed(by: disposeBag) navigationBarView.rx.leftButtonTap - .throttle(RxConst.throttleInterval, scheduler: MainScheduler.instance) + .throttle(RxConst.milliseconds300Interval, scheduler: MainScheduler.instance) .withUnretained(self) .bind { $0.0.navigationController?.pushViewController( FamilyManagementDIContainer().makeViewController(), animated: true) } .disposed(by: disposeBag) navigationBarView.rx.rightButtonTap - .throttle(RxConst.throttleInterval, scheduler: MainScheduler.instance) + .throttle(RxConst.milliseconds300Interval, scheduler: MainScheduler.instance) .withUnretained(self) - .bind { $0.0.navigationController?.pushViewController(CalendarDIConatainer().makeViewController(), animated: true) } + .bind { $0.0.navigationController?.pushViewController(MonthlyCalendarDIConatainer().makeViewController(), animated: true) } .disposed(by: disposeBag) alertConfirmRelay @@ -153,7 +153,7 @@ extension MainViewController { .disposed(by: disposeBag) cameraButton.camerTapObservable - .throttle(RxConst.throttleInterval, scheduler: MainScheduler.instance) + .throttle(RxConst.milliseconds300Interval, scheduler: MainScheduler.instance) .withUnretained(self) .bind(onNext: { MPEvent.Home.cameraTapped.track(with: nil) diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Views/MainFamilyCollectionViewCell.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Views/MainFamilyCollectionViewCell.swift index 4a95eef87..7b0b7ad07 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Views/MainFamilyCollectionViewCell.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Views/MainFamilyCollectionViewCell.swift @@ -123,7 +123,7 @@ extension MainFamilyCollectionViewCell { .disposed(by: disposeBag) pickButton.rx.tap - .throttle(RxConst.throttleInterval, scheduler: Schedulers.main) + .throttle(RxConst.milliseconds300Interval, scheduler: RxSchedulers.main) .map { Reactor.Action.pickButtonTapped } .bind(to: reactor.action) .disposed(by: disposeBag) diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Views/TimerView.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Views/TimerView.swift index 236216923..cef721990 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Views/TimerView.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Views/TimerView.swift @@ -57,7 +57,7 @@ extension TimerView { private func bindOutput(reactor: TimerReactor) { reactor.pulse(\.$time) .distinctUntilChanged() - .observe(on: Schedulers.main) + .observe(on: RxSchedulers.main) .map { $0.setTimerFormat() } .bind(to: timerLabel.rx.text) .disposed(by: disposeBag) diff --git a/14th-team5-iOS/App/Sources/Presentation/JoinFamily/ViewController/InputFamilyLinkViewController.swift b/14th-team5-iOS/App/Sources/Presentation/JoinFamily/ViewController/InputFamilyLinkViewController.swift index 35d06a895..a0dc55033 100644 --- a/14th-team5-iOS/App/Sources/Presentation/JoinFamily/ViewController/InputFamilyLinkViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/JoinFamily/ViewController/InputFamilyLinkViewController.swift @@ -135,13 +135,13 @@ final class InputFamilyLinkViewController: BaseViewController 0 } .withUnretained(self) .bind(onNext: { $0.0.makeBibbiToastView(text: $0.1, image: DesignSystemAsset.warning.image, offset: $0.0.keyboardHeight + 90) }) diff --git a/14th-team5-iOS/App/Sources/Presentation/JoinFamily/ViewController/JoinFamilyViewController.swift b/14th-team5-iOS/App/Sources/Presentation/JoinFamily/ViewController/JoinFamilyViewController.swift index 89ea7b0c0..e089709ce 100644 --- a/14th-team5-iOS/App/Sources/Presentation/JoinFamily/ViewController/JoinFamilyViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/JoinFamily/ViewController/JoinFamilyViewController.swift @@ -78,7 +78,7 @@ final class JoinFamilyViewController: BaseViewController { private func bindInput(reactor: JoinFamilyReactor) { makeFamilyButton.rx.tap .do(onNext: { MPEvent.Account.creatGroup.track(with: nil) }) - .throttle(RxConst.throttleInterval, scheduler: MainScheduler.instance) + .throttle(RxConst.milliseconds300Interval, scheduler: MainScheduler.instance) .withUnretained(self) .bind(onNext: { $0.0.newGroupAlertController()}) .disposed(by: disposeBag) diff --git a/14th-team5-iOS/App/Sources/Presentation/JoinFamily/ViewController/JoinedFamilyViewController.swift b/14th-team5-iOS/App/Sources/Presentation/JoinFamily/ViewController/JoinedFamilyViewController.swift index efb057c60..d4758a935 100644 --- a/14th-team5-iOS/App/Sources/Presentation/JoinFamily/ViewController/JoinedFamilyViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/JoinFamily/ViewController/JoinedFamilyViewController.swift @@ -105,20 +105,20 @@ final class JoinedFamilyViewController: BaseViewController super.bind(reactor: reactor) showHomeButton.rx.tap - .throttle(RxConst.throttleInterval, scheduler: MainScheduler.instance) + .throttle(RxConst.milliseconds300Interval, scheduler: MainScheduler.instance) .map { Reactor.Action.enterFamily } .bind(to: reactor.action) .disposed(by: disposeBag) showJoinedFamilyButton.rx.tap - .throttle(RxConst.throttleInterval, scheduler: MainScheduler.instance) + .throttle(RxConst.milliseconds300Interval, scheduler: MainScheduler.instance) .map { Reactor.Action.joinFamily } .bind(to: reactor.action) .disposed(by: disposeBag) reactor.state .map { $0.isShowHome } - .observe(on: Schedulers.main) + .observe(on: RxSchedulers.main) .distinctUntilChanged() .withUnretained(self) .bind(onNext: { $0.0.showHomeViewController($0.1) }) @@ -126,7 +126,7 @@ final class JoinedFamilyViewController: BaseViewController reactor.state .map { $0.isShowJoinFamily } - .observe(on: Schedulers.main) + .observe(on: RxSchedulers.main) .withUnretained(self) .bind(onNext: { $0.0.showInputLinkViewController($0.1) }) .disposed(by: disposeBag) diff --git a/14th-team5-iOS/App/Sources/Presentation/OnBoarding/OnBoardingViewController.swift b/14th-team5-iOS/App/Sources/Presentation/OnBoarding/OnBoardingViewController.swift index 8ac3449d2..814466c42 100644 --- a/14th-team5-iOS/App/Sources/Presentation/OnBoarding/OnBoardingViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/OnBoarding/OnBoardingViewController.swift @@ -110,7 +110,7 @@ final public class OnBoardingViewController: BaseViewController { .disposed(by: disposeBag) profileButton.rx.tap - .throttle(RxConst.throttleInterval, scheduler: Schedulers.main) + .throttle(RxConst.milliseconds300Interval, scheduler: RxSchedulers.main) .map { Reactor.Action.didTapProfileButton } .bind(to: reactor.action) .disposed(by: disposeBag) diff --git a/14th-team5-iOS/App/Sources/Presentation/PostComment/ViewController/PostCommentViewController.swift b/14th-team5-iOS/App/Sources/Presentation/PostComment/ViewController/PostCommentViewController.swift index 7555cbe02..80b3f933f 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostComment/ViewController/PostCommentViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostComment/ViewController/PostCommentViewController.swift @@ -61,7 +61,7 @@ final public class PostCommentViewController: BaseViewController { .disposed(by: disposeBag) collectionView.rx.contentOffset - .debounce(RxConst.throttleInterval, scheduler: MainScheduler.instance) + .debounce(RxConst.milliseconds300Interval, scheduler: MainScheduler.instance) .map { [unowned self] in UIView.animate(withDuration: 0.3) { self.reactionViewController.view.alpha = 1 diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/ReactionMembersViewController.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/ReactionMembersViewController.swift index 47f0026b1..3e38ade3f 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/ReactionMembersViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/ReactionMembersViewController.swift @@ -55,7 +55,7 @@ final class ReactionMembersViewController: BaseViewController { reactor.pulse(\.$memberInfo) .skip(1) .withUnretained(self) - .observe(on: Schedulers.main) + .observe(on: RxSchedulers.main) .bind(onNext: { $0.0.showNextPage(with: $0.1)}) .disposed(by: disposeBag) @@ -79,7 +79,7 @@ public final class SplashViewController: BaseViewController { .skip(1) .filter { $0 == nil } .withUnretained(self) - .observe(on: Schedulers.main) + .observe(on: RxSchedulers.main) .bind(onNext: { $0.0.showUpdateAlert($0.1) }) .disposed(by: disposeBag) } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BibbiNavigationBarView/BibbiNavigationBarView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BibbiNavigationBarView/BibbiNavigationBarView.swift index a68995a88..b8af3d1f5 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BibbiNavigationBarView/BibbiNavigationBarView.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BibbiNavigationBarView/BibbiNavigationBarView.swift @@ -356,14 +356,14 @@ extension Reactive where Base: BibbiNavigationBarView { let source = base.leftBarButton.rx.tap .withUnretained(base) .map { $0.0.leftBarItem } - .throttle(RxConst.throttleInterval, scheduler: MainScheduler.instance) + .throttle(RxConst.milliseconds300Interval, scheduler: MainScheduler.instance) return ControlEvent(events: source) } public var rightButtonTap: ControlEvent { let source = base.rightBarButton.rx.tap - .throttle(RxConst.throttleInterval, scheduler: MainScheduler.instance) + .throttle(RxConst.milliseconds300Interval, scheduler: MainScheduler.instance) return ControlEvent(events: source) } diff --git a/14th-team5-iOS/Core/Sources/Extensions/Date+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/Date+Ext.swift index f50f49088..ebe2826e8 100644 --- a/14th-team5-iOS/Core/Sources/Extensions/Date+Ext.swift +++ b/14th-team5-iOS/Core/Sources/Extensions/Date+Ext.swift @@ -85,8 +85,20 @@ extension Date { return dateFormatter.string(from: self) } +} + +extension Date { + public func toFormatString(with format: String = "yyyy-MM") -> String { + let dateFormatter = DateFormatter.withFormat(format) + return dateFormatter.string(from: self) + } - + public func toFormatString(with format: DateFormatter.Format) -> String { + return toFormatString(with: format.type) + } +} + +extension Date { public func isEqual( _ components: Set = [.year, .month, .day], with date: Date @@ -117,43 +129,25 @@ extension Date { } extension Date { - public func toFormatString(with format: String = "yyyy-MM") -> String { - let dateFormatter = DateFormatter.withFormat(format) - return dateFormatter.string(from: self) - } - - public func toFormatString(with format: DateFormatter.Format) -> String { - return toFormatString(with: format.type) - } -} - -extension Date { - static func + (date: Date, interval: TimeInterval) -> Date { - return date.addingTimeInterval(interval) - } - - static func - (date: Date, interval: TimeInterval) -> Date { - return date.addingTimeInterval(interval) - } -} - -extension Date { - public func createPreviousNextDateStringArray() -> [String] { - var dateArray: [String] = [] + public func makePreviousNextMonth() -> [String] { + let monthsToSubtract = -1 + let monthsToAdd = 1 + + var dateStrings: [String] = [] - for month in -1...1 { - if let date = calendar.date(byAdding: .month, value: month, to: self) { - let yyyyMM = date.toFormatString(with: .dashYyyyMM) - dateArray.append(yyyyMM) + for monthOffset in monthsToSubtract...monthsToAdd { + if let calculatedDate = calendar.date(byAdding: .month, value: monthOffset, to: self) { + let formattedDateString = calculatedDate.toFormatString(with: .dashYyyyMM) + dateStrings.append(formattedDateString) } } - return dateArray + return dateStrings } } extension Date { - public static var for20230101: Date { + public static var _20230101: Date { let calendar: Calendar = Calendar.current let dateComponents: DateComponents = DateComponents( year: 2023, @@ -163,7 +157,7 @@ extension Date { return calendar.date(from: dateComponents) ?? Date() } - public static var for20240101: Date { + public static var _20240101: Date { let calendar: Calendar = Calendar.current let dateComonents: DateComponents = DateComponents( year: 2024, diff --git a/14th-team5-iOS/Core/Sources/Extensions/String+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/String+Ext.swift index 4626ccb53..9f9988bfd 100644 --- a/14th-team5-iOS/Core/Sources/Extensions/String+Ext.swift +++ b/14th-team5-iOS/Core/Sources/Extensions/String+Ext.swift @@ -19,8 +19,9 @@ extension String { } return false } - - +} + +extension String { public func toDate(with format: String = "yyyy-MM-dd") -> Date { let dateFormatter = DateFormatter.withFormat(format) guard let date = dateFormatter.date(from: self) else { return .now } diff --git a/14th-team5-iOS/Core/Sources/Reactive/RxUtils.swift b/14th-team5-iOS/Core/Sources/Reactive/RxUtils.swift index 5b22ed221..5fed12199 100644 --- a/14th-team5-iOS/Core/Sources/Reactive/RxUtils.swift +++ b/14th-team5-iOS/Core/Sources/Reactive/RxUtils.swift @@ -10,16 +10,16 @@ import Foundation import RxSwift public enum RxConst { - static public var throttleInterval: RxTimeInterval { - return .milliseconds(300) + static public var milliseconds100Interval: RxTimeInterval { + return .milliseconds(100) } - static public var smallDelayInterval: RxTimeInterval { - return .milliseconds(100) + static public var milliseconds300Interval: RxTimeInterval { + return .milliseconds(300) } } -public enum Schedulers { +public enum RxSchedulers { public static let main = { MainScheduler.instance }() diff --git a/14th-team5-iOS/Core/Sources/Token/TokenRepository.swift b/14th-team5-iOS/Core/Sources/Token/TokenRepository.swift index 02a442d67..93b25c781 100644 --- a/14th-team5-iOS/Core/Sources/Token/TokenRepository.swift +++ b/14th-team5-iOS/Core/Sources/Token/TokenRepository.swift @@ -67,7 +67,7 @@ public class TokenRepository: RxObject { super.bind() accessToken .distinctUntilChanged() - .subscribe(on: Schedulers.io) + .subscribe(on: RxSchedulers.io) .withUnretained(self) .subscribe { guard let jsonData = try? JSONEncoder().encode($0.1), diff --git a/14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/CalendarAPIWorker.swift b/14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/CalendarAPIWorker.swift index b5e998500..22569a4ba 100644 --- a/14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/CalendarAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/CalendarAPIWorker.swift @@ -122,7 +122,7 @@ extension CalendarAPIWorker { } public func fetchStatisticsSummary(yearMonth: String) -> Single { - let spec = CalendarAPIs.statisticsSummary(yearMonth).spec + let spec = CalendarAPIs.statistics(yearMonth).spec return Observable.just(()) .withLatestFrom(self._headers) @@ -147,7 +147,7 @@ extension CalendarAPIWorker { } public func fetchCalendarBanner(yearMonth: String) -> Single { - let spec = CalendarAPIs.calendarBenner(yearMonth).spec + let spec = CalendarAPIs.banner(yearMonth).spec return Observable.just(()) .withLatestFrom(self._headers) diff --git a/14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/CalendarAPIs.swift b/14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/CalendarAPIs.swift index 289e1d6cf..cfe150cce 100644 --- a/14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/CalendarAPIs.swift +++ b/14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/CalendarAPIs.swift @@ -15,8 +15,8 @@ enum CalendarAPIs: API { case monthlyCalendar(String) case dailyCalendar(String) - case statisticsSummary(String) - case calendarBenner(String) + case statistics(String) + case banner(String) var spec: APISpec { switch self { @@ -25,11 +25,11 @@ enum CalendarAPIs: API { case let .monthlyCalendar(yearMonth): return APISpec(method: .get, url: "\(BibbiAPI.hostApi)/calendar/monthly?yearMonth=\(yearMonth)") - case let .dailyCalendar(yearMonth): - return APISpec(method: .get, url: "\(BibbiAPI.hostApi)/calendar/daily?yearMonthDay=\(yearMonth)") - case let .statisticsSummary(yearMonth): + case let .dailyCalendar(yearMonthDay): + return APISpec(method: .get, url: "\(BibbiAPI.hostApi)/calendar/daily?yearMonthDay=\(yearMonthDay)") + case let .statistics(yearMonth): return APISpec(method: .get, url: "\(BibbiAPI.hostApi)/calendar/summary?yearMonth=\(yearMonth)") - case let .calendarBenner(yearMonth): + case let .banner(yearMonth): return APISpec(method: .get, url: "\(BibbiAPI.hostApi)/calendar/banner?yearMonth=\(yearMonth)") } } diff --git a/14th-team5-iOS/Data/Sources/Service/Apple/AppleSignInHelper.swift b/14th-team5-iOS/Data/Sources/Service/Apple/AppleSignInHelper.swift index c38e9f633..175cf5501 100644 --- a/14th-team5-iOS/Data/Sources/Service/Apple/AppleSignInHelper.swift +++ b/14th-team5-iOS/Data/Sources/Service/Apple/AppleSignInHelper.swift @@ -39,7 +39,7 @@ class AppleSignInHelper: NSObject, AccountSignInHelperType { return Observable.create { observer in ASAuthorizationAppleIDProvider().rx.signIn(on: window) .asSingle() - .observe(on: Schedulers.utility) + .observe(on: RxSchedulers.utility) .subscribe( onSuccess: { [weak self] response in self?._signInState.accept(response) diff --git a/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme b/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme index 73b9aa620..72399b415 100644 --- a/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme +++ b/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme @@ -300,20 +300,6 @@ ReferencedContainer = "container:Tuist/Dependencies/SwiftPackageManager/.build/checkouts/GoogleDataTransport/GoogleDataTransport.xcodeproj"> - - - - - - - - - - - - - - - - - - - - Date: Thu, 9 May 2024 11:27:15 +0900 Subject: [PATCH 027/263] =?UTF-8?q?fix:=20CommentSheet=20Detents=EA=B0=80?= =?UTF-8?q?=20=EC=98=AC=EB=B0=94=EB=A5=B4=EC=A7=80=20=EC=95=8A=EC=9D=80=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95=20(#509)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DailyCalendarViewController.swift | 4 ++-- ...Container.swift => CommentDIContainer.swift} | 2 +- .../Dependency/ReactionDIContainer.swift | 13 ++++++++++++- .../Reactor/ReactionViewReactor.swift | 8 +++++++- .../ViewControllers/PostViewController.swift | 4 ++-- .../ReactionViewController.swift | 17 ++++++++++++----- 6 files changed, 36 insertions(+), 12 deletions(-) rename 14th-team5-iOS/App/Sources/Presentation/PostComment/Dependency/{PostCommentDIContainer.swift => CommentDIContainer.swift} (97%) diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/DailyCalendarViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/DailyCalendarViewController.swift index 3991067c7..837b58754 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/DailyCalendarViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/DailyCalendarViewController.swift @@ -28,7 +28,7 @@ public final class DailyCalendarViewController: BaseViewController ReactionViewReactor { - return ReactionViewReactor(provider: globalState, initialState: .init(postListData: post), emojiRepository: makeEmojiUseCase(), realEmojiRepository: makeRealEmojiUseCase()) + return ReactionViewReactor(provider: globalState, initialState: .init(type: type, postListData: post), emojiRepository: makeEmojiUseCase(), realEmojiRepository: makeRealEmojiUseCase()) } func makeViewController(post: PostListData) -> ReactionViewController { diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/ReactionViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/ReactionViewReactor.swift index 13ce8f5a1..25e1cc874 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/ReactionViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/ReactionViewReactor.swift @@ -36,6 +36,7 @@ final class ReactionViewReactor: Reactor { } struct State { + let type: ReactionType var postListData: PostListData var deSelectedReactionIndicies: [Int] = [] @@ -55,7 +56,12 @@ final class ReactionViewReactor: Reactor { let emojiRepository: EmojiUseCaseProtocol let realEmojiRepository: RealEmojiUseCaseProtocol - init(provider: GlobalStateProviderProtocol, initialState: State, emojiRepository: EmojiUseCaseProtocol, realEmojiRepository: RealEmojiUseCaseProtocol) { + init( + provider: GlobalStateProviderProtocol, + initialState: State, + emojiRepository: EmojiUseCaseProtocol, + realEmojiRepository: RealEmojiUseCaseProtocol + ) { self.initialState = initialState self.provider = provider self.emojiRepository = emojiRepository diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/PostViewController.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/PostViewController.swift index 83e06829c..0d6fce7cb 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/PostViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/PostViewController.swift @@ -19,7 +19,7 @@ final class PostViewController: BaseViewController { private var navigationView: PostNavigationView = PostNavigationView() private let collectionView: UICollectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) private let collectionViewLayout: UICollectionViewFlowLayout = UICollectionViewFlowLayout() - private let reactionViewController: ReactionViewController = ReactionDIContainer().makeViewController(post: .init(postId: "", author: nil, commentCount: 0, emojiCount: 0, imageURL: "", content: nil, time: "")) + private let reactionViewController: ReactionViewController = ReactionDIContainer(type: .post).makeViewController(post: .init(postId: "", author: nil, commentCount: 0, emojiCount: 0, imageURL: "", content: nil, time: "")) // MARK: - Properties private let deepLinkRepo = DeepLinkRepository() @@ -131,7 +131,7 @@ final class PostViewController: BaseViewController { }) { switch postList { case let .main(post): - let postCommentViewController = PostCommentDIContainer( + let postCommentViewController = CommentDIContainer( postId: post.postId ).makeViewController() diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/ReactionViewController.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/ReactionViewController.swift index 47b1ed0c6..18ae11aaf 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/ReactionViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/ReactionViewController.swift @@ -150,14 +150,21 @@ extension ReactionViewController { .withLatestFrom(postListData) .withUnretained(self) { ($0, $1) } .bind(onNext: { - let postCommentViewController = PostCommentDIContainer( + let commentViewController = CommentDIContainer( postId: $1.postId ).makeViewController() - $0.presentPostCommentSheet( - postCommentViewController, - from: .post - ) + if case .post = reactor.initialState.type { + $0.presentPostCommentSheet( + commentViewController, + from: .post + ) + } else { + $0.presentPostCommentSheet( + commentViewController, + from: .calendar + ) + } }) .disposed(by: disposeBag) } From b452428a36be4c56473d53cd5e01b4c8b991ac03 Mon Sep 17 00:00:00 2001 From: Do-hyun-Kim Date: Thu, 9 May 2024 22:29:45 +0900 Subject: [PATCH 028/263] =?UTF-8?q?feat:=20AccountViewController=20Login?= =?UTF-8?q?=20Button=20Spacing=20=EC=88=98=EC=A0=95=20-=20Mission=20APIs?= =?UTF-8?q?=20getMissionContent=20API=20=EC=B6=94=EA=B0=80=20-=20MissionCo?= =?UTF-8?q?ntentData,=20MissionContentResponse=20Entity,Response=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20-=20MissionContentUseCase,=20MisssonConten?= =?UTF-8?q?tRepository=20=EC=B6=94=EA=B0=80=20-=20PostGlobalState=20receiv?= =?UTF-8?q?eMissionContent=20=ED=95=A8=EC=88=98=20=EC=B6=94=EA=B0=80=20-?= =?UTF-8?q?=20PostReactor=20missionUseCase=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AccountSignInViewController.swift | 2 +- .../Reactor/CalendarPostViewReactor.swift | 2 ++ .../Camera/CameraViewController.swift | 17 ++++++---- .../Dependency/PostListsDIContainer.swift | 9 ++++++ .../Reactor/PostDetailViewReactor.swift | 18 +++++++++++ .../PostDetail/Reactor/PostReactor.swift | 19 ++++++++++- .../PostDetail/Views/MissionTextView.swift | 27 +++++++--------- .../Views/PostDetailCollectionViewCell.swift | 18 ++++++++--- .../Sources/GlobalState/PostGlobalState.swift | 9 ++++++ .../Mission/DTO/MissionContentDTO.swift | 32 +++++++++++++++++++ .../Mission/MissionAPI/MissionAPIWorker.swift | 27 ++++++++++++++++ .../Mission/MissionAPI/MissionAPIs.swift | 3 ++ .../Repository/MissionRepository.swift | 6 ++++ .../Mission/Entities/MissionContentData.swift | 20 ++++++++++++ .../MissionRepositoryProtocol.swift | 1 + .../UseCases/GetTodayMissionUseCase.swift | 22 +++++++++++++ .../UseCases/MissionContentUseCase.swift | 31 ++++++++++++++++++ 17 files changed, 235 insertions(+), 28 deletions(-) create mode 100644 14th-team5-iOS/Data/Sources/Mission/DTO/MissionContentDTO.swift create mode 100644 14th-team5-iOS/Domain/Sources/Mission/Entities/MissionContentData.swift create mode 100644 14th-team5-iOS/Domain/Sources/Mission/UseCases/GetTodayMissionUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/Mission/UseCases/MissionContentUseCase.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 81f6fe75a..69a0ef75c 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignIn/AccountSignInViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignIn/AccountSignInViewController.swift @@ -25,7 +25,7 @@ public final class AccountSignInViewController: BaseViewController.just(.pushProfileViewController(memberId)) case let .renewalPostCommentCount(count): return Observable.just(.renewPostCommentCount(count)) + default: + return .empty() } } diff --git a/14th-team5-iOS/App/Sources/Presentation/Camera/CameraViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Camera/CameraViewController.swift index 20a8875bc..8bba36871 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Camera/CameraViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Camera/CameraViewController.swift @@ -226,7 +226,7 @@ public final class CameraViewController: BaseViewController { guard let userInfo = notification.userInfo else { return nil } return userInfo["photo"] as? Data } - .debug("Notification didTapShutter Button") + .distinctUntilChanged() .map { Reactor.Action.didTapShutterButton($0) } .bind(to: reactor.action) .disposed(by: disposeBag) @@ -239,11 +239,16 @@ public final class CameraViewController: BaseViewController { }.disposed(by: disposeBag) - reactor.pulse(\.$feedImageData) - .compactMap { $0 } + + Observable + .zip( + reactor.state.compactMap { $0.feedImageData }.distinctUntilChanged(), + reactor.state.compactMap { $0.cameraType } + ) + .filter { $0.1.asPostType == .survival } .withUnretained(self) .bind { - let cameraDisplayViewController = CameraDisplayDIContainer(displayData: $0.1).makeViewController() + let cameraDisplayViewController = CameraDisplayDIContainer(displayData: $0.1.0).makeViewController() $0.0.navigationController?.pushViewController(cameraDisplayViewController, animated: true) }.disposed(by: disposeBag) @@ -251,7 +256,7 @@ public final class CameraViewController: BaseViewController { Observable .zip( reactor.state.compactMap { $0.feedImageData }.distinctUntilChanged(), - reactor.state.compactMap { $0.missionEnttiy?.missionContent}.distinctUntilChanged(), + reactor.state.compactMap { $0.missionEnttiy?.missionContent }, reactor.state.map { $0.cameraType.asPostType } ) .withUnretained(self) @@ -372,7 +377,7 @@ public final class CameraViewController: BaseViewController { shutterButton .rx.tap - .throttle(.seconds(4), scheduler: MainScheduler.asyncInstance) + .throttle(.seconds(4), scheduler: MainScheduler.instance) .do { _ in Haptic.selection() } .withUnretained(self) .subscribe { owner, _ in diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/PostListsDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/PostListsDIContainer.swift index c271fd7a2..63b04b372 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/PostListsDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/PostListsDIContainer.swift @@ -56,6 +56,14 @@ final class PostListsDIContainer { return RealEmojiAPIS.Worker() } + func makeMissionRepository() -> MissionRepositoryProtocol { + return MissionRepository() + } + + func makeMissionUseCase() -> MissionContentUseCaseProtocol { + return MissionContentUseCase(missionContentRepository: makeMissionRepository()) + } + func makeEmojiUseCase() -> EmojiUseCaseProtocol { return EmojiUseCase(emojiRepository: makeEmojiRepository()) } @@ -73,6 +81,7 @@ final class PostListsDIContainer { provider: globalState, realEmojiRepository: makeRealEmojiUseCase(), emojiRepository: makeEmojiUseCase(), + missionUseCase: makeMissionUseCase(), initialState: PostReactor.State( selectedIndex: selectedIndex, originPostLists: postLists, diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostDetailViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostDetailViewReactor.swift index a78277760..d2f96ecc4 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostDetailViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostDetailViewReactor.swift @@ -24,11 +24,13 @@ final class PostDetailViewReactor: Reactor { enum Mutation { case injectDisplayContent([DisplayEditItemModel]) + case setMissionContent(String) } struct State { let type: CellType let post: PostListData + @Pulse var missionContent: String? = nil var isShowingSelectableEmojiStackView: Bool = false var fetchedDisplayContent: [DisplayEditSectionModel] = [.displayKeyword([])] @@ -44,6 +46,20 @@ final class PostDetailViewReactor: Reactor { self.memberUseCase = memberUserCase self.initialState = initialState } + + func transform(mutation: Observable) -> Observable { + let missionMutation = provider.postGlobalState.event + .flatMap { event -> Observable in + switch event { + case let .receiveMissionContent(content): + return .just(.setMissionContent(content)) + default: + return .empty() + } + } + return .merge(mutation, missionMutation) + + } } extension PostDetailViewReactor { @@ -72,6 +88,8 @@ extension PostDetailViewReactor { switch mutation { case let .injectDisplayContent(section): newState.fetchedDisplayContent = [.displayKeyword(section)] + case let .setMissionContent(missionContent): + newState.missionContent = missionContent } return newState } diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostReactor.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostReactor.swift index 4a5c20d6a..44e48331a 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostReactor.swift @@ -21,6 +21,7 @@ final class PostReactor: Reactor { enum Mutation { case setPop case setSelectedPostIndex(Int) + case setMissionContent(MissionContentData) case setPushProfileViewController(String) } @@ -31,6 +32,7 @@ final class PostReactor: Reactor { var isPop: Bool = false var selectedPost: PostListData = .init(postId: "", author: .init(memberId: "", profileImageURL: "", name: ""), commentCount: 0, emojiCount: 0, imageURL: "", content: "", time: "") + @Pulse var missionContent: MissionContentData? = nil @Pulse var fetchedPost: PostData? = nil @Pulse var reactionMemberIds: [String] = [] @Pulse var shouldPushProfileViewController: String? @@ -42,6 +44,7 @@ final class PostReactor: Reactor { let realEmojiRepository: RealEmojiUseCaseProtocol let emojiRepository: EmojiUseCaseProtocol + let missionUseCase: MissionContentUseCaseProtocol let provider: GlobalStateProviderProtocol @@ -49,11 +52,13 @@ final class PostReactor: Reactor { provider: GlobalStateProviderProtocol, realEmojiRepository: RealEmojiUseCaseProtocol, emojiRepository: EmojiUseCaseProtocol, + missionUseCase: MissionContentUseCaseProtocol, initialState: State ) { self.provider = provider self.realEmojiRepository = realEmojiRepository self.emojiRepository = emojiRepository + self.missionUseCase = missionUseCase self.initialState = initialState } } @@ -76,7 +81,16 @@ extension PostReactor { func mutate(action: Action) -> Observable { switch action { case let .setPost(index): - return Observable.just(Mutation.setSelectedPostIndex(index)) + guard case let .main(postEntity) = currentState.originPostLists.items[index], + let missionId = postEntity.missionId else { return .empty() } + return missionUseCase.execute(missionId: missionId) + .flatMap { entity -> Observable in + return .concat( + .just(.setSelectedPostIndex(index)), + .just(.setMissionContent(entity)) + ) + + } case .tapBackButton: return Observable.just(Mutation.setPop) } @@ -94,6 +108,9 @@ extension PostReactor { case let .setPushProfileViewController(memberId): newState.shouldPushProfileViewController = memberId + case let .setMissionContent(missionContent): + provider.postGlobalState.missionContentText(missionContent.missionContent) + newState.missionContent = missionContent } return newState } diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/MissionTextView.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/MissionTextView.swift index 7955296e9..c913ae655 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/MissionTextView.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/MissionTextView.swift @@ -14,7 +14,7 @@ import Then import RxSwift import RxCocoa -final class MissionTextView: BaseView { +final class MissionTextView: UIView { // MARK: - Views private let containerView: UIView = UIView() @@ -23,28 +23,26 @@ final class MissionTextView: BaseView { let missionLabel: BibbiLabel = BibbiLabel(.body2Regular, textAlignment: .left, textColor: .bibbiWhite) // MARK: - Helpers - override func bind(reactor: MissionTextReactor) { - super.bind(reactor: reactor) - bindOutput(reactor: reactor) + + override init(frame: CGRect) { + super.init(frame: frame) + setupUI() + setupAttributes() + setupAutoLayout() } - private func bindOutput(reactor: MissionTextReactor) { - reactor.state.map { $0.text } - .distinctUntilChanged() - .bind(to: missionLabel.rx.text) - .disposed(by: disposeBag) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") } - override func setupUI() { - super.setupUI() + private func setupUI() { self.addSubviews(containerView) containerView.addSubview(missionStackView) missionStackView.addArrangedSubviews(missionImageView, missionLabel) } - override func setupAutoLayout() { - super.setupAutoLayout() + private func setupAutoLayout() { containerView.snp.makeConstraints { $0.verticalEdges.equalToSuperview() @@ -68,8 +66,7 @@ final class MissionTextView: BaseView { } } - override func setupAttributes() { - super.setupAttributes() + private func setupAttributes() { containerView.do { $0.backgroundColor = UIColor.gray800 diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/PostDetailCollectionViewCell.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/PostDetailCollectionViewCell.swift index b39be3cf6..e451be892 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/PostDetailCollectionViewCell.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/PostDetailCollectionViewCell.swift @@ -172,14 +172,22 @@ extension PostDetailCollectionViewCell { .bind(to: reactor.action) .disposed(by: disposeBag) - // - MisstionText에 바로 바인딩하시면 됩니다~ -// reactor.state.map { $0.missionContent } -// .distinctUntilChanged() -// .bind(to: missionTextView.missionLabel.rx.text) -// .disposed(by: disposeBag) + reactor.state.compactMap { $0.missionContent } + .distinctUntilChanged() + .debug("Mission Content Title: ") + .bind(to: missionTextView.missionLabel.rx.text) + .disposed(by: disposeBag) } private func bindOutput(reactor: PostDetailViewReactor) { + + reactor.state.map { $0.post.missionType == "survival" } + .debug("Post Detail Mission Type") + .distinctUntilChanged() + .bind(to: missionTextView.rx.isHidden) + .disposed(by: disposeBag) + + reactor.state.map { $0.post } .distinctUntilChanged() .withUnretained(self) diff --git a/14th-team5-iOS/Core/Sources/GlobalState/PostGlobalState.swift b/14th-team5-iOS/Core/Sources/GlobalState/PostGlobalState.swift index 86f3e3a55..81b589f15 100644 --- a/14th-team5-iOS/Core/Sources/GlobalState/PostGlobalState.swift +++ b/14th-team5-iOS/Core/Sources/GlobalState/PostGlobalState.swift @@ -12,6 +12,7 @@ import RxSwift public enum PostEvent { case pushProfileViewController(String) case renewalPostCommentCount(Int) + case receiveMissionContent(String) } public protocol PostGlobalStateType { @@ -27,6 +28,9 @@ public protocol PostGlobalStateType { @discardableResult func renewalPostCommentCount(_ count: Int) -> Observable + + @discardableResult + func missionContentText(_ content: String) -> Observable } final public class PostGlobalState: BaseGlobalState, PostGlobalStateType { @@ -51,4 +55,9 @@ final public class PostGlobalState: BaseGlobalState, PostGlobalStateType { public func clearCommentText() { input.onNext((.none, .none)) } + + public func missionContentText(_ content: String) -> Observable { + event.onNext(.receiveMissionContent(content)) + return Observable.just(content) + } } diff --git a/14th-team5-iOS/Data/Sources/Mission/DTO/MissionContentDTO.swift b/14th-team5-iOS/Data/Sources/Mission/DTO/MissionContentDTO.swift new file mode 100644 index 000000000..46d15f145 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/Mission/DTO/MissionContentDTO.swift @@ -0,0 +1,32 @@ +// +// MissionContentDTO.swift +// Data +// +// Created by Kim dohyun on 5/8/24. +// + +import Foundation + +import Domain + + +struct MissionContentResponse: Decodable { + let missionId: String + let missionContent: String + + + enum CodingKeys: String, CodingKey { + case missionId = "id" + case missionContent = "content" + } +} + + +extension MissionContentResponse { + func toDomain() -> MissionContentData { + return .init( + missionId: missionId, + missionContent: missionContent + ) + } +} diff --git a/14th-team5-iOS/Data/Sources/Mission/MissionAPI/MissionAPIWorker.swift b/14th-team5-iOS/Data/Sources/Mission/MissionAPI/MissionAPIWorker.swift index a058b1210..31828ef5f 100644 --- a/14th-team5-iOS/Data/Sources/Mission/MissionAPI/MissionAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Mission/MissionAPI/MissionAPIWorker.swift @@ -51,4 +51,31 @@ extension MissionAPIWorker { .flatMap { $0.0.getTodayMission(headers: $0.1) } .asSingle() } + + + private func getMissionContent(spec: APISpec, headers: [APIHeader]?) -> Single { + return request(spec: spec, headers: headers) + .subscribe(on: Self.queue) + .do { + if let str = String(data: $0.1, encoding: .utf8) { + debugPrint("Mission Content Result: \(str)") + } + } + .map(MissionContentResponse.self) + .catchAndReturn(nil) + .map { $0?.toDomain() } + .asSingle() + } + + + func getMissionContent(missionId: String) -> Single { + let spec = MissionAPIs.getMissionContent(missionId).spec + + return Observable.just(()) + .withLatestFrom(self._headers) + .observe(on: Self.queue) + .withUnretained(self) + .flatMap { $0.0.getMissionContent(spec: spec, headers: $0.1)} + .asSingle() + } } diff --git a/14th-team5-iOS/Data/Sources/Mission/MissionAPI/MissionAPIs.swift b/14th-team5-iOS/Data/Sources/Mission/MissionAPI/MissionAPIs.swift index 5efcd3f62..c6f952381 100644 --- a/14th-team5-iOS/Data/Sources/Mission/MissionAPI/MissionAPIs.swift +++ b/14th-team5-iOS/Data/Sources/Mission/MissionAPI/MissionAPIs.swift @@ -12,11 +12,14 @@ import Domain public enum MissionAPIs: API { case getTodayMission + case getMissionContent(String) var spec: APISpec { switch self { case .getTodayMission: return APISpec(method: .get, url: "\(BibbiAPI.hostApi)/missions/today") + case let .getMissionContent(missionId): + return APISpec(method: .get, url: "\(BibbiAPI.hostApi)/missions/\(missionId)") } } } diff --git a/14th-team5-iOS/Data/Sources/Mission/Repository/MissionRepository.swift b/14th-team5-iOS/Data/Sources/Mission/Repository/MissionRepository.swift index 9e5cd4486..b2536c0f2 100644 --- a/14th-team5-iOS/Data/Sources/Mission/Repository/MissionRepository.swift +++ b/14th-team5-iOS/Data/Sources/Mission/Repository/MissionRepository.swift @@ -25,4 +25,10 @@ extension MissionRepository { return missionAPIWorker.getTodayMission() .asObservable() } + + public func getMissionContent(missionId: String) -> Observable { + return missionAPIWorker.getMissionContent(missionId: missionId) + .asObservable() + } + } diff --git a/14th-team5-iOS/Domain/Sources/Mission/Entities/MissionContentData.swift b/14th-team5-iOS/Domain/Sources/Mission/Entities/MissionContentData.swift new file mode 100644 index 000000000..9daae3748 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Mission/Entities/MissionContentData.swift @@ -0,0 +1,20 @@ +// +// MissionContentData.swift +// Domain +// +// Created by Kim dohyun on 5/8/24. +// + +import Foundation + + +public struct MissionContentData { + public let missionId: String + public let missionContent: String + + public init(missionId: String, missionContent: String) { + self.missionId = missionId + self.missionContent = missionContent + } + +} diff --git a/14th-team5-iOS/Domain/Sources/Mission/Interfaces/MissionRepositoryProtocol.swift b/14th-team5-iOS/Domain/Sources/Mission/Interfaces/MissionRepositoryProtocol.swift index f9430a2b9..a26081d31 100644 --- a/14th-team5-iOS/Domain/Sources/Mission/Interfaces/MissionRepositoryProtocol.swift +++ b/14th-team5-iOS/Domain/Sources/Mission/Interfaces/MissionRepositoryProtocol.swift @@ -11,4 +11,5 @@ import RxSwift public protocol MissionRepositoryProtocol { func getTodayMission() -> Observable + func getMissionContent(missionId: String) -> Observable } diff --git a/14th-team5-iOS/Domain/Sources/Mission/UseCases/GetTodayMissionUseCase.swift b/14th-team5-iOS/Domain/Sources/Mission/UseCases/GetTodayMissionUseCase.swift new file mode 100644 index 000000000..224e2cd56 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Mission/UseCases/GetTodayMissionUseCase.swift @@ -0,0 +1,22 @@ +// +// GetTodayMissionUseCase.swift +// Domain +// +// Created by 마경미 on 21.04.24. +// + +import Foundation + +import RxSwift + +//public final class GetTodayMissionUseCase: GetTodayMissionUseCaseProtocol { +// private let missionRepository: MissionRepositoryProtocol +// +// public init(missionRepository: MissionRepositoryProtocol) { +// self.missionRepository = missionRepository +// } +// +// public func execute() -> Observable { +// return missionRepository.getTodayMission() +// } +//} diff --git a/14th-team5-iOS/Domain/Sources/Mission/UseCases/MissionContentUseCase.swift b/14th-team5-iOS/Domain/Sources/Mission/UseCases/MissionContentUseCase.swift new file mode 100644 index 000000000..1c1d5578b --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Mission/UseCases/MissionContentUseCase.swift @@ -0,0 +1,31 @@ +// +// MissionContentUseCase.swift +// Domain +// +// Created by Kim dohyun on 5/9/24. +// + +import Foundation + +import RxSwift + +public protocol MissionContentUseCaseProtocol { + func execute(missionId: String) -> Observable +} + + +public class MissionContentUseCase: MissionContentUseCaseProtocol { + + private let missionContentRepository: MissionRepositoryProtocol + + public init(missionContentRepository: MissionRepositoryProtocol) { + self.missionContentRepository = missionContentRepository + } + + public func execute(missionId: String) -> Observable { + return missionContentRepository.getMissionContent(missionId: missionId) + .compactMap { $0 } + .asObservable() + } + +} From 10ac82bfcff60d807ddccf51a7c3ddf427995da6 Mon Sep 17 00:00:00 2001 From: Do-hyun-Kim Date: Thu, 9 May 2024 23:33:17 +0900 Subject: [PATCH 029/263] =?UTF-8?q?fix:=20=EC=BD=94=EB=93=9C=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Calendar/Reactor/MissionTextReactor.swift | 30 ------------------- .../Camera/CameraViewController.swift | 4 +-- .../Camera/Reactor/CameraViewReactor.swift | 6 ++-- .../Mission/DTO/MissionContentDTO.swift | 4 +-- .../Mission/MissionAPI/MissionAPIWorker.swift | 2 +- 5 files changed, 8 insertions(+), 38 deletions(-) delete mode 100644 14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/MissionTextReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/MissionTextReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/MissionTextReactor.swift deleted file mode 100644 index db53c187f..000000000 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/MissionTextReactor.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// MissionTextReactor.swift -// App -// -// Created by 김건우 on 5/7/24. -// - -import Foundation - -import ReactorKit - -final class MissionTextReactor: Reactor { - // MARK: - Action - typealias Action = NoAction - - // MARK: - State - struct State { - var text: String - } - - // MARK: - Properties - var initialState: State - - // MARK: - Intializer - init(text: String) { - self.initialState = State( - text: text - ) - } -} diff --git a/14th-team5-iOS/App/Sources/Presentation/Camera/CameraViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Camera/CameraViewController.swift index 8bba36871..ffe154566 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Camera/CameraViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Camera/CameraViewController.swift @@ -256,7 +256,7 @@ public final class CameraViewController: BaseViewController { Observable .zip( reactor.state.compactMap { $0.feedImageData }.distinctUntilChanged(), - reactor.state.compactMap { $0.missionEnttiy?.missionContent }, + reactor.state.compactMap { $0.missionEntity?.missionContent }, reactor.state.map { $0.cameraType.asPostType } ) .withUnretained(self) @@ -272,7 +272,7 @@ public final class CameraViewController: BaseViewController { .bind(onNext: {$0.0.setupRealEmojiLayoutContent(isShow: !$0.1)}) .disposed(by: disposeBag) - reactor.pulse(\.$missionEnttiy) + reactor.pulse(\.$missionEntity) .map { $0?.missionContent } .bind(to: missionView.missionTitleView.rx.text) .disposed(by: disposeBag) diff --git a/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraViewReactor.swift index b25aee7d0..eff42b78a 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraViewReactor.swift @@ -61,7 +61,7 @@ public final class CameraViewReactor: Reactor { @Pulse var realEmojiURLEntity: CameraRealEmojiPreSignedResponse? @Pulse var realEmojiCreateEntity: CameraCreateRealEmojiResponse? @Pulse var realEmojiEntity: [CameraRealEmojiImageItemResponse?] - @Pulse var missionEnttiy: CameraTodayMissionResponse? + @Pulse var missionEntity: CameraTodayMissionResponse? @Pulse var realEmojiSection: [EmojiSectionModel] @Pulse var zoomScale: CGFloat @Pulse var pinchZoomScale: CGFloat @@ -94,7 +94,7 @@ public final class CameraViewReactor: Reactor { realEmojiURLEntity: nil, realEmojiCreateEntity: nil, realEmojiEntity: [], - missionEnttiy: nil, + missionEntity: nil, realEmojiSection: [.realEmoji([])], zoomScale: 1.0, pinchZoomScale: 1.0, @@ -183,7 +183,7 @@ public final class CameraViewReactor: Reactor { case let .setFeedImageData(feedImage): newState.feedImageData = feedImage case let .setMissionResponse(missionEntity): - newState.missionEnttiy = missionEntity + newState.missionEntity = missionEntity } return newState diff --git a/14th-team5-iOS/Data/Sources/Mission/DTO/MissionContentDTO.swift b/14th-team5-iOS/Data/Sources/Mission/DTO/MissionContentDTO.swift index 46d15f145..466fa8b27 100644 --- a/14th-team5-iOS/Data/Sources/Mission/DTO/MissionContentDTO.swift +++ b/14th-team5-iOS/Data/Sources/Mission/DTO/MissionContentDTO.swift @@ -10,7 +10,7 @@ import Foundation import Domain -struct MissionContentResponse: Decodable { +struct MissionContentDTO: Decodable { let missionId: String let missionContent: String @@ -22,7 +22,7 @@ struct MissionContentResponse: Decodable { } -extension MissionContentResponse { +extension MissionContentDTO { func toDomain() -> MissionContentData { return .init( missionId: missionId, diff --git a/14th-team5-iOS/Data/Sources/Mission/MissionAPI/MissionAPIWorker.swift b/14th-team5-iOS/Data/Sources/Mission/MissionAPI/MissionAPIWorker.swift index 31828ef5f..b2c5317db 100644 --- a/14th-team5-iOS/Data/Sources/Mission/MissionAPI/MissionAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Mission/MissionAPI/MissionAPIWorker.swift @@ -61,7 +61,7 @@ extension MissionAPIWorker { debugPrint("Mission Content Result: \(str)") } } - .map(MissionContentResponse.self) + .map(MissionContentDTO.self) .catchAndReturn(nil) .map { $0?.toDomain() } .asSingle() From 21e660ce2ad3a8aa8b86f70d8921b406271399c3 Mon Sep 17 00:00:00 2001 From: Kyoungmi <1mmgm.dev@gmail.com> Date: Thu, 9 May 2024 22:38:23 +0900 Subject: [PATCH 030/263] [feat]: add Images --- .../Home/Dependency/TimerDIContainer.swift | 20 -------- .../Reactor/ContributorProfileReactor.swift | 26 ++++++++++ .../View/Main/DTO/MainNightResponseDTO.swift | 47 +++++++++++++++++++ .../Rank/emptyRank1.imageset/Contents.json | 12 +++++ .../Rank/emptyRank1.imageset/emptyRank1.svg | 8 ++++ .../Rank/emptyRank2.imageset/Contents.json | 12 +++++ .../Rank/emptyRank2.imageset/emptyRank2.svg | 6 +++ .../Rank/emptyRank3.imageset/Contents.json | 12 +++++ .../Rank/emptyRank3.imageset/emptyRank3.svg | 6 +++ .../icons/question.imageset/Contents.json | 12 +++++ .../icons/question.imageset/_.svg | 3 ++ .../Sources/Main/Entities/MainNightData.swift | 46 ++++++++++++++++++ .../FetchMainNightUseCase.swift | 23 +++++++++ .../FetchMainNightUseCaseProtocol.swift | 14 ++++++ .../FetchMainUsecaseProtocol.swift | 14 ++++++ 15 files changed, 241 insertions(+), 20 deletions(-) delete mode 100644 14th-team5-iOS/App/Sources/Presentation/Home/Dependency/TimerDIContainer.swift create mode 100644 14th-team5-iOS/App/Sources/Presentation/Home/Reactor/ContributorProfileReactor.swift create mode 100644 14th-team5-iOS/Data/Sources/View/Main/DTO/MainNightResponseDTO.swift create mode 100644 14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/Rank/emptyRank1.imageset/Contents.json create mode 100644 14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/Rank/emptyRank1.imageset/emptyRank1.svg create mode 100644 14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/Rank/emptyRank2.imageset/Contents.json create mode 100644 14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/Rank/emptyRank2.imageset/emptyRank2.svg create mode 100644 14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/Rank/emptyRank3.imageset/Contents.json create mode 100644 14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/Rank/emptyRank3.imageset/emptyRank3.svg create mode 100644 14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/question.imageset/Contents.json create mode 100644 14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/question.imageset/_.svg create mode 100644 14th-team5-iOS/Domain/Sources/Main/Entities/MainNightData.swift create mode 100644 14th-team5-iOS/Domain/Sources/Main/FetchMainUseCase/FetchMainNightUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/Main/FetchMainUseCase/FetchMainNightUseCaseProtocol.swift create mode 100644 14th-team5-iOS/Domain/Sources/Main/FetchMainUseCase/FetchMainUsecaseProtocol.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Dependency/TimerDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Dependency/TimerDIContainer.swift deleted file mode 100644 index 3db551ab2..000000000 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Dependency/TimerDIContainer.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// TimerDIContainer.swift -// App -// -// Created by 마경미 on 30.01.24. -// - -import UIKit - -import Core - -final class TimerDIContainer { - func makeView() -> TimerView { - return TimerView(reactor: makeReactor()) - } - - func makeReactor() -> TimerReactor { - return TimerReactor() - } -} diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/ContributorProfileReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/ContributorProfileReactor.swift new file mode 100644 index 000000000..4e86074d6 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/ContributorProfileReactor.swift @@ -0,0 +1,26 @@ +// +// ContributorProfileReactor.swift +// App +// +// Created by 마경미 on 07.05.24. +// + +import Foundation + +import ReactorKit + +class ContributorProfileReactor: Reactor { + enum Action { + + } + + enum Mutation { + + } + + struct State { + + } + + let initialState: State = State() +} diff --git a/14th-team5-iOS/Data/Sources/View/Main/DTO/MainNightResponseDTO.swift b/14th-team5-iOS/Data/Sources/View/Main/DTO/MainNightResponseDTO.swift new file mode 100644 index 000000000..d876e636c --- /dev/null +++ b/14th-team5-iOS/Data/Sources/View/Main/DTO/MainNightResponseDTO.swift @@ -0,0 +1,47 @@ +// +// MainNightResponseDTO.swift +// Data +// +// Created by 마경미 on 07.05.24. +// + +import Foundation + +import Domain + +struct Ranker: Codable { + let profileImageUrl: String + let name: String + let survivalCount: Int + + func toDomain() -> RankerData { + return .init(imageURL: profileImageUrl, name: name, survivalCount: survivalCount) + } +} + +struct FamilyMemberMonthlyRanking: Codable { + let month: Int + let firstRanker: Ranker + let secondRanker: Ranker + let thirdRanker: Ranker + let mostRecentSurvivalPostDate: String + + func toDomain() -> FamilyRankData { + return .init(month: month, + recentPostDate: mostRecentSurvivalPostDate, + firstRanker: firstRanker.toDomain(), + secondRanker: secondRanker.toDomain(), + thirdRanker: thirdRanker.toDomain() + ) + } +} + +struct MainNightResponseDTO: Codable { + let topBarElements: [TopBarElement] + let familyMemberMonthlyRanking: FamilyMemberMonthlyRanking + + func toDomain() -> MainNightData { + return .init(mainFamilyProfileDatas: topBarElements.map { $0.toDomain() }, + familyRankData: familyMemberMonthlyRanking.toDomain()) + } +} diff --git a/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/Rank/emptyRank1.imageset/Contents.json b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/Rank/emptyRank1.imageset/Contents.json new file mode 100644 index 000000000..618e51d7f --- /dev/null +++ b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/Rank/emptyRank1.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "emptyRank1.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/Rank/emptyRank1.imageset/emptyRank1.svg b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/Rank/emptyRank1.imageset/emptyRank1.svg new file mode 100644 index 000000000..7fe44bbc5 --- /dev/null +++ b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/Rank/emptyRank1.imageset/emptyRank1.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/Rank/emptyRank2.imageset/Contents.json b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/Rank/emptyRank2.imageset/Contents.json new file mode 100644 index 000000000..81882d404 --- /dev/null +++ b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/Rank/emptyRank2.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "emptyRank2.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/Rank/emptyRank2.imageset/emptyRank2.svg b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/Rank/emptyRank2.imageset/emptyRank2.svg new file mode 100644 index 000000000..ec6b96dff --- /dev/null +++ b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/Rank/emptyRank2.imageset/emptyRank2.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/Rank/emptyRank3.imageset/Contents.json b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/Rank/emptyRank3.imageset/Contents.json new file mode 100644 index 000000000..3bae926ce --- /dev/null +++ b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/Rank/emptyRank3.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "emptyRank3.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/Rank/emptyRank3.imageset/emptyRank3.svg b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/Rank/emptyRank3.imageset/emptyRank3.svg new file mode 100644 index 000000000..fb2e6a570 --- /dev/null +++ b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/Rank/emptyRank3.imageset/emptyRank3.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/question.imageset/Contents.json b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/question.imageset/Contents.json new file mode 100644 index 000000000..7b6b8ebd8 --- /dev/null +++ b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/question.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "_.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/question.imageset/_.svg b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/question.imageset/_.svg new file mode 100644 index 000000000..7a5f524f7 --- /dev/null +++ b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/question.imageset/_.svg @@ -0,0 +1,3 @@ + + + diff --git a/14th-team5-iOS/Domain/Sources/Main/Entities/MainNightData.swift b/14th-team5-iOS/Domain/Sources/Main/Entities/MainNightData.swift new file mode 100644 index 000000000..00922b389 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Main/Entities/MainNightData.swift @@ -0,0 +1,46 @@ +// +// MainNightData.swift +// Domain +// +// Created by 마경미 on 07.05.24. +// + +import Foundation + +public struct RankerData { + public let imageURL: String + public let name: String + public let survivalCount: Int + + public init(imageURL: String, name: String, survivalCount: Int) { + self.imageURL = imageURL + self.name = name + self.survivalCount = survivalCount + } +} + +public struct FamilyRankData { + public let month: Int + public let recentPostDate: String + public let firstRanker: RankerData? + public let secondRanker: RankerData? + public let thirdRanker: RankerData? + + public init(month: Int, recentPostDate: String, firstRanker: RankerData?, secondRanker: RankerData?, thirdRanker: RankerData?) { + self.month = month + self.recentPostDate = recentPostDate + self.firstRanker = firstRanker + self.secondRanker = secondRanker + self.thirdRanker = thirdRanker + } +} + +public struct MainNightData { + public let mainFamilyProfileDatas: [ProfileData] + public let familyRankData: FamilyRankData + + public init(mainFamilyProfileDatas: [ProfileData], familyRankData: FamilyRankData) { + self.mainFamilyProfileDatas = mainFamilyProfileDatas + self.familyRankData = familyRankData + } +} diff --git a/14th-team5-iOS/Domain/Sources/Main/FetchMainUseCase/FetchMainNightUseCase.swift b/14th-team5-iOS/Domain/Sources/Main/FetchMainUseCase/FetchMainNightUseCase.swift new file mode 100644 index 000000000..a5c51129b --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Main/FetchMainUseCase/FetchMainNightUseCase.swift @@ -0,0 +1,23 @@ +// +// FetchMainNightUseCase.swift +// Domain +// +// Created by 마경미 on 07.05.24. +// + +import Foundation + +import RxSwift + +public final class FetchMainNightUseCase: FetchMainNightUseCaseProtocol { + private let mainRepository: MainRepositoryProtocol + + public init(mainRepository: MainRepositoryProtocol) { + self.mainRepository = mainRepository + } + + public func execute() -> Observable { + return mainRepository.fetchMainNight() + } +} + diff --git a/14th-team5-iOS/Domain/Sources/Main/FetchMainUseCase/FetchMainNightUseCaseProtocol.swift b/14th-team5-iOS/Domain/Sources/Main/FetchMainUseCase/FetchMainNightUseCaseProtocol.swift new file mode 100644 index 000000000..4af46a313 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Main/FetchMainUseCase/FetchMainNightUseCaseProtocol.swift @@ -0,0 +1,14 @@ +// +// FetchMainNightUseCaseProtocol.swift +// Domain +// +// Created by 마경미 on 07.05.24. +// + +import Foundation + +import RxSwift + +public protocol FetchMainNightUseCaseProtocol { + func execute() -> Observable +} diff --git a/14th-team5-iOS/Domain/Sources/Main/FetchMainUseCase/FetchMainUsecaseProtocol.swift b/14th-team5-iOS/Domain/Sources/Main/FetchMainUseCase/FetchMainUsecaseProtocol.swift new file mode 100644 index 000000000..2ab4cb145 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Main/FetchMainUseCase/FetchMainUsecaseProtocol.swift @@ -0,0 +1,14 @@ +// +// MainUseCaseProtocol.swift +// Domain +// +// Created by 마경미 on 20.04.24. +// + +import Foundation + +import RxSwift + +public protocol FetchMainUseCaseProtocol { + func execute() -> Observable +} From 8661faafb420dc189d5c2a515a4c3675cf5d7388 Mon Sep 17 00:00:00 2001 From: Kyoungmi <1mmgm.dev@gmail.com> Date: Thu, 9 May 2024 22:39:52 +0900 Subject: [PATCH 031/263] [feat]: add main night api --- .../View/Main/Repository/MainRepository.swift | 4 +++ .../View/Main/Worker/MainAPIWorker.swift | 25 ++++++++++++++++++- .../Sources/View/Main/Worker/MainAPIs.swift | 4 +++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/14th-team5-iOS/Data/Sources/View/Main/Repository/MainRepository.swift b/14th-team5-iOS/Data/Sources/View/Main/Repository/MainRepository.swift index 0f80dbd45..36c5fc8f6 100644 --- a/14th-team5-iOS/Data/Sources/View/Main/Repository/MainRepository.swift +++ b/14th-team5-iOS/Data/Sources/View/Main/Repository/MainRepository.swift @@ -23,4 +23,8 @@ extension MainRepository { public func fetchMain() -> Observable { return mainApiWorker.fetchMain().asObservable() } + + public func fetchMainNight() -> Observable { + return mainApiWorker.fetchMainNight().asObservable() + } } diff --git a/14th-team5-iOS/Data/Sources/View/Main/Worker/MainAPIWorker.swift b/14th-team5-iOS/Data/Sources/View/Main/Worker/MainAPIWorker.swift index 91264a22a..63b8f32ce 100644 --- a/14th-team5-iOS/Data/Sources/View/Main/Worker/MainAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/View/Main/Worker/MainAPIWorker.swift @@ -27,7 +27,7 @@ extension MainAPIs { } extension MainAPIWorker { - public func fetchMain() -> Single { + func fetchMain() -> Single { return Observable.just(()) .withLatestFrom(self._headers) .withUnretained(self) @@ -49,4 +49,27 @@ extension MainAPIWorker { .map { $0?.toDomain() } .asSingle() } + + func fetchMainNight() -> Single { + return Observable.just(()) + .withLatestFrom(self._headers) + .withUnretained(self) + .flatMap { $0.0.fetchMainNight(headers: $0.1) } + .asSingle() + } + + private func fetchMainNight(headers: [APIHeader]?) -> Single { + let spec = MainAPIs.fetchMainNight.spec + return request(spec: spec, headers: headers) + .subscribe(on: Self.queue) + .do { + if let str = String(data: $0.1, encoding: .utf8) { + debugPrint("Main Night Fetch Result: \(str)") + } + } + .map(MainNightResponseDTO.self) + .catchAndReturn(nil) + .map { $0?.toDomain() } + .asSingle() + } } diff --git a/14th-team5-iOS/Data/Sources/View/Main/Worker/MainAPIs.swift b/14th-team5-iOS/Data/Sources/View/Main/Worker/MainAPIs.swift index c9eb3c056..6d7a0b83a 100644 --- a/14th-team5-iOS/Data/Sources/View/Main/Worker/MainAPIs.swift +++ b/14th-team5-iOS/Data/Sources/View/Main/Worker/MainAPIs.swift @@ -11,12 +11,16 @@ import Core public enum MainAPIs: API { case fetchMain + case fetchMainNight var spec: APISpec { switch self { case .fetchMain: let urlString = "\(BibbiAPI.hostApi)/view/main/daytime-page" return APISpec(method: .get, url: urlString) + case .fetchMainNight: + let urlString = "\(BibbiAPI.hostApi)/view/main/nighttime-page" + return APISpec(method: .get, url: urlString) } } } From e8488f6bd4011fb7107191e44e6cc8e86e776084 Mon Sep 17 00:00:00 2001 From: Kyoungmi <1mmgm.dev@gmail.com> Date: Thu, 9 May 2024 22:40:07 +0900 Subject: [PATCH 032/263] [feat]: add main night domain --- .../Sources/Main/Entities/MainData.swift | 8 ---- .../Sources/Main/Entities/MainNightData.swift | 44 ++++++++++++++++++- .../Repository/MainRepositoryProtocol.swift | 1 + 3 files changed, 44 insertions(+), 9 deletions(-) diff --git a/14th-team5-iOS/Domain/Sources/Main/Entities/MainData.swift b/14th-team5-iOS/Domain/Sources/Main/Entities/MainData.swift index 9bfcd7976..c274543b9 100644 --- a/14th-team5-iOS/Domain/Sources/Main/Entities/MainData.swift +++ b/14th-team5-iOS/Domain/Sources/Main/Entities/MainData.swift @@ -50,12 +50,4 @@ public struct MainData { self.isFamilySurvivalUploadedToday = survivalUploadCount == mainFamilyProfileDatas.count self.isFamilyMissionUploadedToday = missionUploadCount == mainFamilyProfileDatas.count } - - -// public init(mainFamilyProfileDatas: [ProfileData], isMissionUnlocked: Bool, isMeUploadedToday: Bool, pickers: [Picker]) { -// self.mainFamilyProfileDatas = mainFamilyProfileDatas.map(FamilySection.Item.main) -// self.isMissionUnlocked = isMissionUnlocked -// self.isMeUploadedToday = isMeUploadedToday -// self.pickers = pickers -// } } diff --git a/14th-team5-iOS/Domain/Sources/Main/Entities/MainNightData.swift b/14th-team5-iOS/Domain/Sources/Main/Entities/MainNightData.swift index 00922b389..5e34c9d54 100644 --- a/14th-team5-iOS/Domain/Sources/Main/Entities/MainNightData.swift +++ b/14th-team5-iOS/Domain/Sources/Main/Entities/MainNightData.swift @@ -5,7 +5,43 @@ // Created by 마경미 on 07.05.24. // -import Foundation +import UIKit + +import DesignSystem + +public enum Rank { + case none + case first + case second + case third + + public var borderColor: UIColor { + switch self { + case .none: return .gray600 + case .first: return .mainYellow + case .second: return .graphicGreen + case .third: return .graphicOrange + } + } + + public var badgeImage: UIImage { + switch self { + case .first: return DesignSystemAsset.rank1.image + case .second: return DesignSystemAsset.rank2.image + case .third: return DesignSystemAsset.rank3.image + default: fatalError("Rank Not Founded") + } + } + + public var grayBadgeImage: UIImage { + switch self { + case .first: return DesignSystemAsset.emptyRank1.image + case .second: return DesignSystemAsset.emptyRank2.image + case .third: return DesignSystemAsset.emptyRank3.image + default: fatalError("Rank Not Founded") + } + } +} public struct RankerData { public let imageURL: String @@ -35,6 +71,12 @@ public struct FamilyRankData { } } +extension FamilyRankData { + public static var empty: FamilyRankData { + return .init(month: 0, recentPostDate: "", firstRanker: nil, secondRanker: nil, thirdRanker: nil) + } +} + public struct MainNightData { public let mainFamilyProfileDatas: [ProfileData] public let familyRankData: FamilyRankData diff --git a/14th-team5-iOS/Domain/Sources/Main/Repository/MainRepositoryProtocol.swift b/14th-team5-iOS/Domain/Sources/Main/Repository/MainRepositoryProtocol.swift index 2632ef01b..8896215e9 100644 --- a/14th-team5-iOS/Domain/Sources/Main/Repository/MainRepositoryProtocol.swift +++ b/14th-team5-iOS/Domain/Sources/Main/Repository/MainRepositoryProtocol.swift @@ -11,4 +11,5 @@ import RxSwift public protocol MainRepositoryProtocol { func fetchMain() -> Observable + func fetchMainNight() -> Observable } From e48df5ac12380e3aaa51e4bcf2de462b507023b2 Mon Sep 17 00:00:00 2001 From: Kyoungmi <1mmgm.dev@gmail.com> Date: Thu, 9 May 2024 23:05:59 +0900 Subject: [PATCH 033/263] [feat]: contributor view(#503) --- .../Home/Dependency/MainViewDIContainer.swift | 8 +- .../Reactor/ContributorProfileReactor.swift | 39 +- .../Home/Reactor/ContributorReactor.swift | 39 +- .../Home/Reactor/MainViewReactor.swift | 48 +- .../ViewControllers/MainViewController.swift | 42 +- .../Home/Views/ContributorProfileView.swift | 102 +- .../Home/Views/ContributorView.swift | 136 ++- .../xcschemes/Bibbi-Workspace.xcscheme | 1000 ----------------- 8 files changed, 355 insertions(+), 1059 deletions(-) delete mode 100644 Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Dependency/MainViewDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Dependency/MainViewDIContainer.swift index 5d63b82e5..977c6ffba 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Dependency/MainViewDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Dependency/MainViewDIContainer.swift @@ -29,8 +29,8 @@ final class MainViewDIContainer { extension MainViewDIContainer { private func makeReactor() -> MainViewReactor { return MainViewReactor( - initialState: .init(isInTime: false), - fetchMainUseCase: makeFetchMainUseCase(), + fetchMainUseCase: makeFetchMainUseCase(), + fetchMainNightUseCase: makeFetchMainNightUseCase(), pickUseCase: makePickUseCase(), provider: globalState ) @@ -51,4 +51,8 @@ extension MainViewDIContainer { private func makeFetchMainUseCase() -> FetchMainUseCaseProtocol { return FetchMainUseCase(mainRepository: makeMainRepository()) } + + private func makeFetchMainNightUseCase() -> FetchMainNightUseCaseProtocol { + return FetchMainNightUseCase(mainRepository: makeMainRepository()) + } } diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/ContributorProfileReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/ContributorProfileReactor.swift index 4e86074d6..c2a68fb94 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/ContributorProfileReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/ContributorProfileReactor.swift @@ -5,22 +5,49 @@ // Created by 마경미 on 07.05.24. // -import Foundation +import UIKit + +import Domain import ReactorKit -class ContributorProfileReactor: Reactor { +final class ContributorProfileReactor: Reactor { enum Action { - + case getRanker(RankerData?) } enum Mutation { - + case updateRanker(RankerData?) } struct State { - + let rank: Rank + @Pulse var ranker: RankerData? = nil } - let initialState: State = State() + let initialState: State + + init(initialState: State) { + self.initialState = initialState + } +} + +extension ContributorProfileReactor { + func mutate(action: Action) -> Observable { + switch action { + case .getRanker(let data): + return Observable.just(.updateRanker(data)) + } + } + + func reduce(state: State, mutation: Mutation) -> State { + var newState = state + + switch mutation { + case .updateRanker(let data): + newState.ranker = data + } + + return newState + } } diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/ContributorReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/ContributorReactor.swift index d3fcad40b..038016b08 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/ContributorReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/ContributorReactor.swift @@ -5,21 +5,50 @@ // Created by 마경미 on 19.04.24. // -import UIKit +import Foundation + +import Domain + import ReactorKit -class ContributorReactor: Reactor { +final class ContributorReactor: Reactor { enum Action { - + case setContributor(FamilyRankData) } enum Mutation { - + case updateState(FamilyRankData) } struct State { - + var month: Int = Date().month + @Pulse var firstRanker: RankerData? = nil + @Pulse var secondRanker: RankerData? = nil + @Pulse var thirdRanker: RankerData? = nil } let initialState: State = State() } + +extension ContributorReactor { + func mutate(action: Action) -> Observable { + switch action { + case .setContributor(let contributor): + return Observable.just(.updateState(contributor)) + } + } + + func reduce(state: State, mutation: Mutation) -> State { + var newState = state + + switch mutation { + case .updateState(let contributor): + newState.month = contributor.month + newState.firstRanker = contributor.firstRanker + newState.secondRanker = contributor.secondRanker + newState.thirdRanker = contributor.thirdRanker + } + + return newState + } +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift index 68a372844..811633a89 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift @@ -51,10 +51,10 @@ enum Description { } final class MainViewReactor: Reactor { - enum Action { case calculateTime case fetchMainUseCase + case fetchMainNightUseCase case didTapSegmentControl(PostType) case pickConfirmButtonTapped(String, String) @@ -62,6 +62,7 @@ final class MainViewReactor: Reactor { enum Mutation { case updateMainData(MainData) + case updateMainNight(MainNightData) case setInTime(Bool) case setPageIndex(Int) @@ -76,9 +77,10 @@ final class MainViewReactor: Reactor { } struct State { - var isInTime: Bool + var isInTime: Bool = true var pageIndex: Int = 0 var leftCount: Int = 0 + var missionText: String = "" var balloonText: BalloonText = .survivalStandard var description: Description = .survivalNone @@ -88,6 +90,7 @@ final class MainViewReactor: Reactor { var isMeSurvivalUploadedToday: Bool = false var isMissionUnlocked: Bool = false + @Pulse var contributor: FamilyRankData = FamilyRankData.empty @Pulse var familySection: [FamilySection.Item] = [] @Pulse var shouldPresentPickAlert: (String, String)? @@ -96,19 +99,19 @@ final class MainViewReactor: Reactor { @Pulse var shouldPresentFailureToastMessage: Bool = false } - let initialState: State + let initialState: State = State() let fetchMainUseCase: FetchMainUseCaseProtocol + let fetchMainNightUseCase: FetchMainNightUseCaseProtocol let pickUseCase: PickUseCaseProtocol let provider: GlobalStateProviderProtocol init( - initialState: State, fetchMainUseCase: FetchMainUseCaseProtocol, + fetchMainNightUseCase: FetchMainNightUseCaseProtocol, pickUseCase: PickUseCaseProtocol, - provider: GlobalStateProviderProtocol - ) { - self.initialState = initialState + provider: GlobalStateProviderProtocol) { self.fetchMainUseCase = fetchMainUseCase + self.fetchMainNightUseCase = fetchMainNightUseCase self.pickUseCase = pickUseCase self.provider = provider } @@ -143,26 +146,42 @@ extension MainViewReactor { switch action { case .fetchMainUseCase: return fetchMainUseCase.execute() + .asObservable() .flatMap { result -> Observable in guard let data = result else { return Observable.empty() } return Observable.concat(Observable.just(.updateMainData(data)), Observable.just(.setBalloonText)) } + case .fetchMainNightUseCase: + return fetchMainNightUseCase.execute() + .asObservable() + .flatMap { result -> Observable in + guard let data = result else { + return Observable.empty() + } + return Observable.just(.updateMainNight(data)) + } case .calculateTime: - let (_, time) = MainViewReactor.calculateRemainingTime() + let (isInTime, time) = MainViewReactor.calculateRemainingTime() - if self.currentState.isInTime { + if isInTime { return Observable .timer(.seconds(time), scheduler: MainScheduler.instance) .flatMap {_ in - return Observable.concat([Observable.just(Mutation.setInTime(false))]) + return Observable.concat([ + Observable.just(Mutation.setInTime(true)), + self.mutate(action: .fetchMainUseCase) + ]) } } else { return Observable .timer(.seconds(time), scheduler: MainScheduler.instance) .flatMap {_ in - return Observable.concat([Observable.just(Mutation.setInTime(true))]) + return Observable.concat([ + Observable.just(Mutation.setInTime(false)), + self.mutate(action: .fetchMainNightUseCase) + ]) } } @@ -212,6 +231,11 @@ extension MainViewReactor { .main(MainFamilyCellReactor($0, service: provider)) } ).items + case .updateMainNight(let data): + newState.familySection = FamilySection.Model(model: 0, items: data.mainFamilyProfileDatas.map { + .main(MainFamilyCellReactor($0, service: provider)) + }).items + newState.contributor = data.familyRankData case let .setPickAlertView(name, id): newState.shouldPresentPickAlert = (name, id) case .setBalloonText: @@ -255,7 +279,7 @@ extension MainViewReactor { let currentHour = calendar.component(.hour, from: currentTime) - if currentHour >= 12 { + 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)) 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 b58d1f21a..85b26c88d 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift @@ -18,18 +18,15 @@ import RxSwift final class MainViewController: BaseViewController, UICollectionViewDelegateFlowLayout { private let familyViewController: MainFamilyViewController = MainFamilyViewDIContainer().makeViewController() - private let timerView: TimerView = TimerDIContainer().makeView() + private let timerView: TimerView = TimerView(reactor: TimerReactor()) private let descriptionLabel: BibbiLabel = BibbiLabel(.body2Regular, textAlignment: .center, textColor: .gray300) private let imageView: UIImageView = UIImageView() + private let contributorView: ContributorView = ContributorView(reactor: ContributorReactor()) private let segmentControl: BibbiSegmentedControl = BibbiSegmentedControl(isUpdated: true) private let pageViewController: SegmentPageViewController = SegmentPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal) private let cameraButton: MainCameraButtonView = MainCameraDIContainer().makeView() - - private let memberRepo = App.Repository.member - private let deepLinkRepo = App.Repository.deepLink - private let alertConfirmRelay = PublishRelay<(String, String)>() override func viewDidLoad() { @@ -52,7 +49,7 @@ final class MainViewController: BaseViewController, UICollectio view.addSubviews(familyViewController.view, timerView, descriptionLabel, imageView, segmentControl, pageViewController.view, - cameraButton) + cameraButton, contributorView) familyViewController.didMove(toParent: self) pageViewController.didMove(toParent: self) @@ -85,6 +82,12 @@ final class MainViewController: BaseViewController, UICollectio $0.leading.equalTo(descriptionLabel.snp.trailing).offset(2) } + contributorView.snp.makeConstraints { + $0.top.equalTo(descriptionLabel.snp.bottom).offset(20) + $0.horizontalEdges.equalToSuperview() + $0.bottom.equalToSuperview() + } + segmentControl.snp.makeConstraints { $0.top.equalTo(descriptionLabel.snp.bottom).offset(20) $0.centerX.equalToSuperview() @@ -116,9 +119,9 @@ extension MainViewController { private func bindInput(reactor: MainViewReactor) { Observable.merge( Observable.just(()) - .map { Reactor.Action.fetchMainUseCase }, + .map { Reactor.Action.calculateTime }, NotificationCenter.default.rx.notification(UIScene.willEnterForegroundNotification) - .map { _ in Reactor.Action.fetchMainUseCase } + .map { _ in Reactor.Action.calculateTime } ) .bind(to: reactor.action) .disposed(by: disposeBag) @@ -164,6 +167,13 @@ extension MainViewController { } private func bindOutput(reactor: MainViewReactor) { + reactor.state.map { $0.isInTime } + .distinctUntilChanged() + .withUnretained(self) + .observe(on: MainScheduler.instance) + .bind(onNext: { $0.0.setInTimeView($0.1) }) + .disposed(by: disposeBag) + reactor.pulse(\.$familySection) .distinctUntilChanged() .observe(on: MainScheduler.instance) @@ -195,6 +205,10 @@ extension MainViewController { }) .disposed(by: disposeBag) + reactor.pulse(\.$contributor) + .bind(to: contributorView.contributorRelay) + .disposed(by: disposeBag) + reactor.pulse(\.$shouldPresentPickAlert) .compactMap { $0 } .bind(with: self) { owner, profile in @@ -235,3 +249,15 @@ extension MainViewController { .disposed(by: disposeBag) } } + +extension MainViewController { + private func setInTimeView(_ isInTime: Bool) { + if isInTime { + contributorView.isHidden = true + segmentControl.isHidden = false + } else { + contributorView.isHidden = false + segmentControl.isHidden = true + } + } +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Views/ContributorProfileView.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Views/ContributorProfileView.swift index f0940d8f5..153644bd3 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Views/ContributorProfileView.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Views/ContributorProfileView.swift @@ -6,44 +6,47 @@ // import UIKit + import Core import Domain +import DesignSystem import Kingfisher +import RxCocoa import RxSwift -final class ContributorProfileView: UIView { +final class ContributorProfileView: BaseView { private let nameLabel = BibbiLabel(.body2Bold, textAlignment: .center, textColor: .gray200) - private let countLabel = BibbiLabel(.body2Bold, textAlignment: .center, textColor: .gray300) + private let countLabel = BibbiLabel(.body2Regular, textAlignment: .center, textColor: .gray300) private let imageView = UIImageView() + private let questionView = UIImageView() private let badgeView = UIImageView() - - let rank: Int = 0 - let count: Int = 0 - let disposeBag = DisposeBag() - override init(frame: CGRect) { - super.init(frame: .zero) - setupUI() - setupAutoLayout() - setupAttributes() - } + let rankerRelay: BehaviorRelay = BehaviorRelay(value: nil) - required init?(coder: NSCoder) { - super.init(coder: coder) + override func bind(reactor: ContributorProfileReactor) { + bindInput(reactor: reactor) + bindOutput(reactor: reactor) } - private func setupUI() { - addSubviews(imageView, nameLabel, countLabel, badgeView) + override func setupUI() { + addSubviews(imageView, nameLabel, countLabel, + questionView, badgeView) } - private func setupAutoLayout() { + override func setupAutoLayout() { imageView.snp.makeConstraints { $0.horizontalEdges.equalToSuperview() $0.top.equalToSuperview() $0.height.equalTo(self.snp.width) } + questionView.snp.makeConstraints { + $0.center.equalTo(imageView) + $0.height.equalTo(26) + $0.width.equalTo(18) + } + badgeView.snp.makeConstraints { $0.width.equalTo(imageView.snp.width).dividedBy(3.5) $0.height.equalTo(imageView.snp.height).dividedBy(2.8) @@ -52,24 +55,77 @@ final class ContributorProfileView: UIView { } nameLabel.snp.makeConstraints { - $0.horizontalEdges.equalToSuperview() - $0.top.equalTo(imageView.snp.bottom).offset(7) + $0.horizontalEdges.equalToSuperview().inset(10) + $0.top.equalTo(badgeView.snp.bottom).offset(7) $0.height.equalTo(18) } countLabel.snp.makeConstraints { - $0.horizontalEdges.equalToSuperview() + $0.horizontalEdges.equalToSuperview().inset(20) $0.top.equalTo(nameLabel.snp.bottom).offset(4) $0.height.equalTo(18) } } - private func setupAttributes() { + override func setupAttributes() { imageView.do { $0.clipsToBounds = true - $0.contentMode = .scaleAspectFill - $0.layer.cornerRadius = 64 / 2 $0.layer.borderWidth = 4 + $0.tintColor = .gray400 + $0.contentMode = .scaleAspectFit + } + + nameLabel.do { + $0.clipsToBounds = true + $0.layer.cornerRadius = 4 + } + + countLabel.do { + $0.clipsToBounds = true + $0.layer.cornerRadius = 4 + } + } + + override func layoutSubviews() { + super.layoutSubviews() + imageView.layer.cornerRadius = self.frame.size.width / 2 + } +} + +extension ContributorProfileView { + private func bindInput(reactor: ContributorProfileReactor) { + rankerRelay.map { Reactor.Action.getRanker($0) } + .bind(to: reactor.action) + .disposed(by: disposeBag) + } + + private func bindOutput(reactor: ContributorProfileReactor) { + reactor.pulse(\.$ranker) + .withUnretained(self) + .observe(on: MainScheduler.instance) + .bind(onNext: { $0.0.setProfile($0.1) }) + .disposed(by: disposeBag) + } + + private func setProfile(_ data: RankerData?) { + if let data = data { + nameLabel.backgroundColor = .clear + nameLabel.text = data.name + countLabel.backgroundColor = .clear + countLabel.text = "\(data.survivalCount)회" + + imageView.layer.borderColor = reactor?.currentState.rank.borderColor.cgColor + imageView.kf.setImage(with: URL(string: data.imageURL)) + + badgeView.image = reactor?.currentState.rank.badgeImage + } else { + nameLabel.backgroundColor = .gray600 + countLabel.backgroundColor = .gray700 + + imageView.layer.borderColor = UIColor.gray600.cgColor + questionView.image = DesignSystemAsset.question.image + + badgeView.image = reactor?.currentState.rank.grayBadgeImage } } } diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Views/ContributorView.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Views/ContributorView.swift index 3f17de853..8caf7c5cc 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Views/ContributorView.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Views/ContributorView.swift @@ -8,9 +8,139 @@ import UIKit import Core +import Domain +import DesignSystem + +import RxSwift +import RxCocoa final class ContributorView: BaseView { - private let title: UIView = UIView() - private let firstProfileView: ContributorProfileView = ContributorProfileView() - private let secondProfileView: ContributorProfileView = ContributorProfileView() + private let containerView: UIView = UIView() + + private let titleLabel: BibbiLabel = BibbiLabel(.head2Bold, textColor: .gray200) + private let infoButton: UIButton = UIButton() + private let subTitleLabel: BibbiLabel = BibbiLabel(.body2Regular, textColor: .gray300) + + private let firstProfileView: ContributorProfileView = ContributorProfileView(reactor: ContributorProfileReactor(initialState: .init(rank: .first))) + private let secondProfileView: ContributorProfileView = ContributorProfileView(reactor: ContributorProfileReactor(initialState: .init(rank: .second))) + private let thirdProfileView: ContributorProfileView = ContributorProfileView(reactor: ContributorProfileReactor(initialState: .init(rank: .third))) + + private let nextButton: BibbiButton = BibbiButton() + + let contributorRelay: BehaviorRelay = .init(value: FamilyRankData.empty) + + override func bind(reactor: ContributorReactor) { + bindInput(reactor: reactor) + bindOutput(reactor: reactor) + } + + override func setupUI() { + addSubviews(containerView) + containerView.addSubviews(titleLabel, infoButton, subTitleLabel, + firstProfileView, secondProfileView, thirdProfileView, + nextButton) + } + + override func setupAutoLayout() { + + containerView.snp.makeConstraints { + $0.horizontalEdges.equalToSuperview().inset(20) + $0.top.equalToSuperview() + $0.height.equalTo(330) + } + titleLabel.snp.makeConstraints { + $0.top.leading.equalToSuperview().inset(20) + $0.height.equalTo(25) + } + + infoButton.snp.makeConstraints { + $0.size.equalTo(28) + $0.leading.equalTo(titleLabel.snp.trailing).offset(4) + $0.centerY.equalTo(titleLabel) + } + + subTitleLabel.snp.makeConstraints { + $0.trailing.top.equalToSuperview().inset(20) + $0.centerY.equalTo(titleLabel) + } + + firstProfileView.snp.makeConstraints { + $0.width.equalToSuperview().dividedBy(3.7) + $0.height.equalTo(155) + $0.centerX.equalToSuperview() + $0.bottom.equalTo(nextButton.snp.top).offset(-40) + } + + secondProfileView.snp.makeConstraints { + $0.width.equalToSuperview().dividedBy(4.65) + $0.height.equalTo(128) + $0.leading.equalToSuperview().inset(27) + $0.bottom.equalTo(nextButton.snp.top).offset(-32) + } + + thirdProfileView.snp.makeConstraints { + $0.width.equalToSuperview().dividedBy(4.65) + $0.height.equalTo(128) + $0.trailing.equalToSuperview().inset(27) + $0.bottom.equalTo(nextButton.snp.top).offset(-32) + } + + nextButton.snp.makeConstraints { + $0.horizontalEdges.bottom.equalToSuperview().inset(20) + $0.height.equalTo(44) + } + } + + override func setupAttributes() { + self.backgroundColor = .bibbiBlack + + containerView.do { + $0.layer.cornerRadius = 24 + $0.backgroundColor = .gray900 + } + + titleLabel.do { + $0.text = "이번달 최고 기여자" + } + + infoButton.do { + $0.setImage(DesignSystemAsset.infoCircleFill.image, for: .normal) + } + + nextButton.do { + $0.layer.cornerRadius = 8 + $0.setTitle("지난 날 생존신고 보기", for: .normal) + $0.setTitleColor(.bibbiBlack, for: .normal) + $0.backgroundColor = .mainYellow + $0.setTitleFontStyle(.body1Bold) + } + } +} + +extension ContributorView { + private func bindInput(reactor: ContributorReactor) { + contributorRelay.map { Reactor.Action.setContributor($0) } + .bind(to: reactor.action) + .disposed(by: disposeBag) + } + + private func bindOutput(reactor: ContributorReactor) { + reactor.state.map { $0.month } + .distinctUntilChanged() + .map { "\($0)월 생존신고 횟수" } + .bind(to: subTitleLabel.rx.text) + .disposed(by: disposeBag) + + reactor.pulse(\.$firstRanker) + .bind(to: firstProfileView.rankerRelay) + .disposed(by: disposeBag) + + reactor.pulse(\.$secondRanker) + .bind(to: secondProfileView.rankerRelay) + .disposed(by: disposeBag) + + reactor.pulse(\.$thirdRanker) + .bind(to: thirdProfileView.rankerRelay) + .disposed(by: disposeBag) + } } diff --git a/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme b/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme deleted file mode 100644 index 73b9aa620..000000000 --- a/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme +++ /dev/null @@ -1,1000 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From 1cd6f63f4a10297b297e504d9b4c389a1abb0c43 Mon Sep 17 00:00:00 2001 From: Kyoungmi <1mmgm.dev@gmail.com> Date: Thu, 9 May 2024 23:51:09 +0900 Subject: [PATCH 034/263] [feat]: add camera alpha --- .../Home/Reactor/MainViewReactor.swift | 65 ++++++++++++++----- .../ViewControllers/MainViewController.swift | 9 ++- .../Home/Views/MainCameraButtonView.swift | 11 ++++ 3 files changed, 68 insertions(+), 17 deletions(-) diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift index 811633a89..5af9de943 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift @@ -53,6 +53,7 @@ enum Description { final class MainViewReactor: Reactor { enum Action { case calculateTime + case setTimer(Bool, Int) case fetchMainUseCase case fetchMainNightUseCase @@ -66,6 +67,7 @@ final class MainViewReactor: Reactor { case setInTime(Bool) case setPageIndex(Int) + case setCamerEnabled case setBalloonText case setDescriptionText @@ -84,12 +86,15 @@ final class MainViewReactor: Reactor { var missionText: String = "" var balloonText: BalloonText = .survivalStandard var description: Description = .survivalNone - + var isFamilySurvivalUploadedToday: Bool = false var isFamilyMissionUploadedToday: Bool = false var isMeSurvivalUploadedToday: Bool = false + var isMeMissionUploadedToday: Bool = false var isMissionUnlocked: Bool = false + @Pulse var cameraEnabled: Bool = false + @Pulse var contributor: FamilyRankData = FamilyRankData.empty @Pulse var familySection: [FamilySection.Item] = [] @@ -110,11 +115,11 @@ final class MainViewReactor: Reactor { fetchMainNightUseCase: FetchMainNightUseCaseProtocol, pickUseCase: PickUseCaseProtocol, provider: GlobalStateProviderProtocol) { - self.fetchMainUseCase = fetchMainUseCase - self.fetchMainNightUseCase = fetchMainNightUseCase - self.pickUseCase = pickUseCase - self.provider = provider - } + self.fetchMainUseCase = fetchMainUseCase + self.fetchMainNightUseCase = fetchMainNightUseCase + self.pickUseCase = pickUseCase + self.provider = provider + } } extension MainViewReactor { @@ -151,7 +156,10 @@ extension MainViewReactor { guard let data = result else { return Observable.empty() } - return Observable.concat(Observable.just(.updateMainData(data)), Observable.just(.setBalloonText)) + return Observable.concat(Observable.just( + .updateMainData(data)), Observable.just(.setBalloonText), + Observable.just(.setCamerEnabled) + ) } case .fetchMainNightUseCase: return fetchMainNightUseCase.execute() @@ -162,16 +170,14 @@ extension MainViewReactor { } return Observable.just(.updateMainNight(data)) } - case .calculateTime: - let (isInTime, time) = MainViewReactor.calculateRemainingTime() - + case .setTimer(let isInTime, let time): if isInTime { return Observable .timer(.seconds(time), scheduler: MainScheduler.instance) .flatMap {_ in return Observable.concat([ - Observable.just(Mutation.setInTime(true)), - self.mutate(action: .fetchMainUseCase) + Observable.just(Mutation.setInTime(false)), + self.mutate(action: .fetchMainNightUseCase) ]) } } else { @@ -179,17 +185,35 @@ extension MainViewReactor { .timer(.seconds(time), scheduler: MainScheduler.instance) .flatMap {_ in return Observable.concat([ - Observable.just(Mutation.setInTime(false)), - self.mutate(action: .fetchMainNightUseCase) + Observable.just(Mutation.setInTime(true)), + self.mutate(action: .fetchMainUseCase) ]) } + } + case .calculateTime: + let (isInTime, time) = MainViewReactor.calculateRemainingTime() + + if isInTime { + return Observable.concat([ + Observable.just(Mutation.setInTime(true)), + self.mutate(action: .fetchMainUseCase), + self.mutate(action: .setTimer(isInTime, time)) + ]) + } else { + return Observable.concat([ + Observable.just(Mutation.setInTime(false)), + self.mutate(action: .fetchMainNightUseCase), + self.mutate(action: .setTimer(isInTime, time)) + ]) } case .didTapSegmentControl(let type): return Observable.concat( Observable.just(.setPageIndex(type.getIndex())), Observable.just(.setBalloonText), - Observable.just(.setDescriptionText)) + Observable.just(.setDescriptionText), + Observable.just(.setCamerEnabled) + ) case let .pickConfirmButtonTapped(name, id): return pickUseCase.executePickMember(memberId: id) @@ -221,6 +245,7 @@ extension MainViewReactor { case .updateMainData(let data): newState.isMissionUnlocked = data.isMissionUnlocked newState.isMeSurvivalUploadedToday = data.isMeSurvivalUploadedToday + newState.isMeMissionUploadedToday = data.isMeMissionUploadedToday newState.isFamilyMissionUploadedToday = data.isFamilyMissionUploadedToday newState.isFamilySurvivalUploadedToday = data.isFamilySurvivalUploadedToday newState.leftCount = data.leftUploadCountUntilMissionUnlock @@ -238,6 +263,16 @@ extension MainViewReactor { newState.contributor = data.familyRankData case let .setPickAlertView(name, id): newState.shouldPresentPickAlert = (name, id) + case .setCamerEnabled: + if currentState.pageIndex == 0 { + newState.cameraEnabled = !currentState.isMeSurvivalUploadedToday + } else { + if currentState.isMeMissionUploadedToday || currentState.isMissionUnlocked { + newState.cameraEnabled = false + } else { + newState.cameraEnabled = true + } + } case .setBalloonText: if currentState.pageIndex == 0 { newState.balloonText = .survivalStandard 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 85b26c88d..3b061d1ee 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift @@ -118,8 +118,8 @@ final class MainViewController: BaseViewController, UICollectio extension MainViewController { private func bindInput(reactor: MainViewReactor) { Observable.merge( - Observable.just(()) - .map { Reactor.Action.calculateTime }, + self.rx.viewWillAppear + .map { _ in Reactor.Action.calculateTime }, NotificationCenter.default.rx.notification(UIScene.willEnterForegroundNotification) .map { _ in Reactor.Action.calculateTime } ) @@ -190,6 +190,11 @@ extension MainViewController { }) .disposed(by: disposeBag) + reactor.pulse(\.$cameraEnabled) + .distinctUntilChanged() + .bind(to: cameraButton.cameraEnabledRelay) + .disposed(by: disposeBag) + reactor.state.map { $0.balloonText } .distinctUntilChanged() .bind(to: cameraButton.textRelay) diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Views/MainCameraButtonView.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Views/MainCameraButtonView.swift index 21b1ac2c6..e4c0af720 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Views/MainCameraButtonView.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Views/MainCameraButtonView.swift @@ -18,6 +18,8 @@ final class MainCameraButtonView: BaseView { private let cameraButton: UIButton = UIButton() let textRelay: BehaviorRelay = BehaviorRelay(value: .survivalStandard) + let cameraEnabledRelay: BehaviorRelay = BehaviorRelay(value: false) + var camerTapObservable: ControlEvent { return cameraButton.rx.tap } @@ -65,5 +67,14 @@ extension MainCameraButtonView { .observe(on: MainScheduler.instance) .bind(to: balloonView.text) .disposed(by: disposeBag) + + cameraEnabledRelay + .withUnretained(self) + .observe(on: MainScheduler.instance) + .bind(onNext: { + $0.0.alpha = $0.1 ? 1 : 0.5 + $0.0.isUserInteractionEnabled = $0.1 + }) + .disposed(by: disposeBag) } } From 77fbd96fe65e7b33c1c31215f1708bd590907848 Mon Sep 17 00:00:00 2001 From: Kyoungmi <1mmgm.dev@gmail.com> Date: Fri, 10 May 2024 00:01:11 +0900 Subject: [PATCH 035/263] [feat]: question view(#503) --- .../Presentation/Home/Views/ContributorProfileView.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Views/ContributorProfileView.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Views/ContributorProfileView.swift index 153644bd3..6ea491a9e 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Views/ContributorProfileView.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Views/ContributorProfileView.swift @@ -114,6 +114,7 @@ extension ContributorProfileView { countLabel.backgroundColor = .clear countLabel.text = "\(data.survivalCount)회" + questionView.isHidden = true imageView.layer.borderColor = reactor?.currentState.rank.borderColor.cgColor imageView.kf.setImage(with: URL(string: data.imageURL)) @@ -123,6 +124,7 @@ extension ContributorProfileView { countLabel.backgroundColor = .gray700 imageView.layer.borderColor = UIColor.gray600.cgColor + questionView.isHidden = false questionView.image = DesignSystemAsset.question.image badgeView.image = reactor?.currentState.rank.grayBadgeImage From 8fe9b2cca37e1e3f318e5c58c853dc389b2a5921 Mon Sep 17 00:00:00 2001 From: Do-hyun-Kim Date: Fri, 10 May 2024 00:33:00 +0900 Subject: [PATCH 036/263] fix: RxAlamofire iOS Deployment Target 12.0 Settings --- .../xcschemes/Bibbi-Workspace.xcscheme | 1000 +++++++++++++++++ 1 file changed, 1000 insertions(+) create mode 100644 Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme diff --git a/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme b/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme new file mode 100644 index 000000000..73b9aa620 --- /dev/null +++ b/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme @@ -0,0 +1,1000 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 443b27685bf40f3a21d99b3d7cf3a77716d39673 Mon Sep 17 00:00:00 2001 From: Kyoungmi <1mmgm.dev@gmail.com> Date: Fri, 10 May 2024 22:23:21 +0900 Subject: [PATCH 037/263] [feat]: pickBalloonView(#519) --- .../Home/Reactor/BalloonReactor.swift | 65 ++++++++ .../Home/Reactor/MainCameraReactor.swift | 33 +++- .../Home/Reactor/MainViewReactor.swift | 21 ++- .../ViewControllers/MainViewController.swift | 2 +- .../Presentation/Home/Views/BalloonView.swift | 150 ++++++++++++++++++ .../Home/Views/MainCameraButtonView.swift | 21 ++- .../Sources/Extensions/UIStackView+Ext.swift | 4 + .../Core/Sources/Popover/BalloonView.swift | 88 ---------- .../View/Main/DTO/MainResponseDTO.swift | 2 +- .../icons/polygonGray.imageset/Contents.json | 12 ++ .../icons/polygonGray.imageset/Polygon 1.svg | 3 + .../polygonYellow.imageset/Contents.json | 12 ++ .../polygonYellow.imageset/Polygon 1 (1).svg | 3 + .../Sources/Main/Entities/MainData.swift | 10 +- .../xcschemes/Bibbi-Workspace.xcscheme | 70 -------- 15 files changed, 311 insertions(+), 185 deletions(-) create mode 100644 14th-team5-iOS/App/Sources/Presentation/Home/Reactor/BalloonReactor.swift create mode 100644 14th-team5-iOS/App/Sources/Presentation/Home/Views/BalloonView.swift delete mode 100644 14th-team5-iOS/Core/Sources/Popover/BalloonView.swift create mode 100644 14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/polygonGray.imageset/Contents.json create mode 100644 14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/polygonGray.imageset/Polygon 1.svg create mode 100644 14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/polygonYellow.imageset/Contents.json create mode 100644 14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/polygonYellow.imageset/Polygon 1 (1).svg diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/BalloonReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/BalloonReactor.swift new file mode 100644 index 000000000..685d5f460 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/BalloonReactor.swift @@ -0,0 +1,65 @@ +// +// BalloonReactor.swift +// App +// +// Created by 마경미 on 10.05.24. +// + +import Foundation + +import Core +import Domain + +import ReactorKit + +enum BalloonType: Equatable { + static func == (lhs: BalloonType, rhs: BalloonType) -> Bool { + switch (lhs, rhs) { + case (.normal, .normal): + return true + case (.picks, .picks): + return true + default: + return false + } + } + + case normal + case picks([Picker]) +} + +final class BalloonReactor: Reactor { + enum Action { + case setType(BalloonType) + } + + enum Mutation { + case updateType(BalloonType) + } + + struct State { + var type: BalloonType = .normal + } + + let initialState: State = State() +} + +extension BalloonReactor { + func mutate(action: Action) -> Observable { + switch action { + case .setType(let type): + return Observable.just(.updateType(type)) + } + } + + func reduce(state: State, mutation: Mutation) -> State { + var newState = state + + switch mutation { + case .updateType(let type): + newState.type = type + } + + return newState + } +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainCameraReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainCameraReactor.swift index b4272a46b..59cac7e72 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainCameraReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainCameraReactor.swift @@ -11,10 +11,27 @@ import Domain import ReactorKit -enum BalloonText: String { - case survivalStandard = "하루에 한번 사진을 올릴 수 있어요" - case cantMission = "아직 미션 사진을 찍을 수 없어요" - case canMission = "미션 사진을 찍으러 가볼까요?" +enum BalloonText { + case survivalStandard + case cantMission + case canMission + case picker(Picker) + case pickers([Picker]) + + var message: String { + switch self { + case .survivalStandard: + return "하루에 한번 사진을 올릴 수 있어요" + case .cantMission: + return "아직 미션 사진을 찍을 수 없어요" + case .canMission: + return "미션 사진을 찍으러 가볼까요?" + case .picker(let picker): + return "\(picker.displayName)님이 기다리고 있어요" + case .pickers(let pickers): + return "\(pickers.first?.displayName ?? "알 수 없음")님 외 \(pickers.count - 1)명이 기다리고 있어요" + } + } } final class MainCameraReactor: Reactor { @@ -23,9 +40,9 @@ final class MainCameraReactor: Reactor { } enum Mutation { - case setText(BalloonText) + case updateText(BalloonText) } - + struct State { var balloonText: BalloonText = .survivalStandard } @@ -37,7 +54,7 @@ extension MainCameraReactor { func mutate(action: Action) -> Observable { switch action { case .setText(let text): - return Observable.just(.setText(text)) + return Observable.just(.updateText(text)) } } @@ -45,7 +62,7 @@ extension MainCameraReactor { var newState = state switch mutation { - case .setText(let text): + case .updateText(let text): newState.balloonText = text } diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift index 5af9de943..3838073ec 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift @@ -95,6 +95,7 @@ final class MainViewReactor: Reactor { @Pulse var cameraEnabled: Bool = false + @Pulse var pickers: [Picker] = [] @Pulse var contributor: FamilyRankData = FamilyRankData.empty @Pulse var familySection: [FamilySection.Item] = [] @@ -156,9 +157,10 @@ extension MainViewReactor { guard let data = result else { return Observable.empty() } - return Observable.concat(Observable.just( - .updateMainData(data)), Observable.just(.setBalloonText), - Observable.just(.setCamerEnabled) + return Observable.concat( + Observable.just(.updateMainData(data)), + Observable.just(.setBalloonText), + Observable.just(.setCamerEnabled) ) } case .fetchMainNightUseCase: @@ -250,6 +252,7 @@ extension MainViewReactor { newState.isFamilySurvivalUploadedToday = data.isFamilySurvivalUploadedToday newState.leftCount = data.leftUploadCountUntilMissionUnlock newState.missionText = data.dailyMissionContent + newState.pickers = data.pickers newState.familySection = FamilySection.Model( model: 0, items: data.mainFamilyProfileDatas.map { @@ -275,9 +278,17 @@ extension MainViewReactor { } case .setBalloonText: if currentState.pageIndex == 0 { - newState.balloonText = .survivalStandard + if !currentState.isMeSurvivalUploadedToday && !currentState.pickers.isEmpty { + if currentState.pickers.count <= 1 { + newState.balloonText = .picker(currentState.pickers[0]) + } else { + newState.balloonText = .pickers(currentState.pickers) + } + } else { + newState.balloonText = .survivalStandard + } } else { - if currentState.isMissionUnlocked { + if currentState.isMissionUnlocked || !currentState.isMeSurvivalUploadedToday { newState.balloonText = .cantMission } else { newState.balloonText = .canMission 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 9bf8b7fd0..9199601d8 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift @@ -196,7 +196,7 @@ extension MainViewController { .disposed(by: disposeBag) reactor.state.map { $0.balloonText } - .distinctUntilChanged() + .debug("balloonText") .bind(to: cameraButton.textRelay) .disposed(by: disposeBag) diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Views/BalloonView.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Views/BalloonView.swift new file mode 100644 index 000000000..a0b085140 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Views/BalloonView.swift @@ -0,0 +1,150 @@ +// +// PopOverView.swift +// Core +// +// Created by 마경미 on 29.12.23. +// + +import UIKit + +import Core +import DesignSystem + +import RxCocoa +import RxSwift + +final class BalloonView: BaseView { + private let containerView: UIView = UIView() + private let polygonImageView: UIImageView = UIImageView() + private let stackView: UIStackView = UIStackView() + private let textLabel: UILabel = BibbiLabel(.body2Regular) + + let balloonTypeRelay: BehaviorRelay = BehaviorRelay(value: .normal) + + public var text: Binder { + return textLabel.rx.text + } + + override func bind(reactor: BalloonReactor) { + bindInput(reactor: reactor) + bindOutput(reactor: reactor) + } + + override func setupUI() { + addSubviews(containerView, polygonImageView) + containerView.addSubviews(stackView, textLabel) + } + + override func setupAutoLayout() { + containerView.snp.makeConstraints { + $0.horizontalEdges.equalToSuperview() + $0.bottom.equalToSuperview().inset(12) + $0.height.equalTo(40) + } + + polygonImageView.snp.makeConstraints { + $0.top.equalTo(containerView.snp.bottom) + $0.centerX.equalToSuperview() + $0.width.equalTo(15) + $0.height.equalTo(12) + } + + stackView.snp.makeConstraints { + $0.leading.equalToSuperview().inset(16) + $0.width.equalTo(0) + $0.height.equalTo(20) + $0.centerY.equalToSuperview() + } + + textLabel.snp.makeConstraints { + $0.leading.equalTo(stackView.snp.trailing) + $0.trailing.equalToSuperview().inset(16) + $0.verticalEdges.equalToSuperview().inset(10) + } + } + + override func setupAttributes() { + backgroundColor = UIColor.clear + + containerView.do { + $0.backgroundColor = DesignSystemAsset.gray700.color + $0.layer.cornerRadius = 12 + } + + polygonImageView.do { + $0.backgroundColor = .clear + } + + stackView.do { + $0.spacing = -8 + $0.distribution = .fillEqually + } + + textLabel.do { + $0.textColor = .white + $0.textAlignment = .center + $0.numberOfLines = 0 + } + } +} + +extension BalloonView { + private func bindInput(reactor: BalloonReactor) { + balloonTypeRelay.map { Reactor.Action.setType($0) } + .bind(to: reactor.action) + .disposed(by: disposeBag) + } + + private func bindOutput(reactor: BalloonReactor) { + reactor.state.map { $0.type } + .distinctUntilChanged() + .withUnretained(self) + .observe(on: MainScheduler.instance) + .bind(onNext: { + $0.0.setBalloonView($0.1) + }) + .disposed(by: disposeBag) + } + + private func setBalloonView(_ type: BalloonType) { + if case .picks(let pickers) = type { + containerView.backgroundColor = .mainYellow + polygonImageView.image = DesignSystemAsset.polygonYellow.image + textLabel.textColor = .bibbiBlack + + stackView.snp.updateConstraints { + $0.width.equalTo(pickers.count <= 1 ? 20 : 16 * pickers.count) + } + + textLabel.snp.updateConstraints { + $0.leading.equalTo(stackView.snp.trailing).offset(6) + } + + pickers.forEach { + let imageView = UIImageView(frame: .init(x: 0, y: 0, width: 20, height: 20)) + imageView.contentMode = .scaleAspectFill + imageView.layer.borderColor = UIColor.mainYellow.cgColor + imageView.layer.borderWidth = 2 + imageView.clipsToBounds = true + imageView.layer.cornerRadius = 10 + imageView.kf.setImage(with: URL(string: $0.imageUrl)) + + stackView.addArrangedSubview(imageView) + } + } else { + containerView.backgroundColor = .gray700 + polygonImageView.image = DesignSystemAsset.polygonGray.image + textLabel.textColor = .white + + stackView.snp.updateConstraints { + $0.width.equalTo(0) + } + + textLabel.snp.updateConstraints { + $0.leading.equalTo(stackView.snp.trailing).offset(0) + } + + stackView.removeAllArrangedSubviews() + } + } +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Views/MainCameraButtonView.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Views/MainCameraButtonView.swift index e4c0af720..36e2bbd74 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Views/MainCameraButtonView.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Views/MainCameraButtonView.swift @@ -8,13 +8,14 @@ import UIKit import Core +import Domain import DesignSystem import RxSwift import RxCocoa final class MainCameraButtonView: BaseView { - private let balloonView: BalloonView = BalloonView() + private let balloonView: BalloonView = BalloonView(reactor: BalloonReactor()) private let cameraButton: UIButton = UIButton() let textRelay: BehaviorRelay = BehaviorRelay(value: .survivalStandard) @@ -55,19 +56,27 @@ final class MainCameraButtonView: BaseView { extension MainCameraButtonView { private func bindInput(reactor: MainCameraReactor) { - textRelay - .map { Reactor.Action.setText($0) } + textRelay.map { Reactor.Action.setText($0) } .bind(to: reactor.action) .disposed(by: disposeBag) } private func bindOutput(reactor: MainCameraReactor) { - reactor.state.map { $0.balloonText.rawValue } - .distinctUntilChanged() - .observe(on: MainScheduler.instance) + reactor.state.map { $0.balloonText.message } .bind(to: balloonView.text) .disposed(by: disposeBag) + reactor.state.map { $0.balloonText } + .map { (text) -> BalloonType in + switch text { + case .picker(let picker): return .picks([picker]) + case .pickers(let pickers): return .picks(pickers) + default: return .normal + } + } + .bind(to: balloonView.balloonTypeRelay) + .disposed(by: disposeBag) + cameraEnabledRelay .withUnretained(self) .observe(on: MainScheduler.instance) diff --git a/14th-team5-iOS/Core/Sources/Extensions/UIStackView+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/UIStackView+Ext.swift index baed62101..cfeb41128 100644 --- a/14th-team5-iOS/Core/Sources/Extensions/UIStackView+Ext.swift +++ b/14th-team5-iOS/Core/Sources/Extensions/UIStackView+Ext.swift @@ -13,4 +13,8 @@ extension UIStackView { self.addArrangedSubview($0) } } + + public func removeAllArrangedSubviews() { + arrangedSubviews.forEach { $0.removeFromSuperview() } + } } diff --git a/14th-team5-iOS/Core/Sources/Popover/BalloonView.swift b/14th-team5-iOS/Core/Sources/Popover/BalloonView.swift deleted file mode 100644 index ee48032e3..000000000 --- a/14th-team5-iOS/Core/Sources/Popover/BalloonView.swift +++ /dev/null @@ -1,88 +0,0 @@ -// -// PopOverView.swift -// Core -// -// Created by 마경미 on 29.12.23. -// - -import UIKit - -import DesignSystem - -import RxSwift - -public class BalloonView: UIView { - private let containerView: UIView = UIView() - private let textLabel: UILabel = BibbiLabel(.body2Regular) - - public var text: Binder { - return textLabel.rx.text - } - - override init(frame: CGRect) { - super.init(frame: frame) - setupUI() - setAutoLayout() - setAttributes() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupUI() { - addSubview(containerView) - containerView.addSubview(textLabel) - } - - private func setAutoLayout() { - containerView.snp.makeConstraints { - $0.horizontalEdges.equalToSuperview() - $0.bottom.equalToSuperview().inset(12) - $0.height.equalTo(40) - } - - textLabel.snp.makeConstraints { - $0.horizontalEdges.equalToSuperview().inset(16) - $0.verticalEdges.equalToSuperview().inset(10) - } - } - - private func setAttributes() { - backgroundColor = UIColor.clear - - containerView.do { - $0.backgroundColor = DesignSystemAsset.gray700.color - $0.layer.cornerRadius = 12 - } - - textLabel.do { - $0.textColor = .white - $0.textAlignment = .center - $0.numberOfLines = 0 - } - } - - public override func draw(_ rect: CGRect) { - super.draw(rect) - - let screenSize = UIScreen.main.bounds.size - let tipLeft = (screenSize.width / 2.0) - 6 - let tipBottom = CGPoint(x: rect.origin.x + (rect.size.width / 2.0), y: rect.size.height) - let heightWithoutTip = 40 - - // path 객체로 선분을 잇는 작업 - let path = UIBezierPath() - path.move(to: CGPoint(x: tipLeft, y: 40)) - path.addLine(to: CGPoint(x: tipBottom.x, y: tipBottom.y)) - path.addLine(to: CGPoint(x: Int(tipLeft) + 12, y: heightWithoutTip)) - path.close() - - // 만든 path 객체를 이용하여 shapeLayer로 색칠, layer.addSublayer에 사용 - let shapeLayer = CAShapeLayer() - shapeLayer.path = path.cgPath - shapeLayer.fillColor = DesignSystemAsset.gray700.color.cgColor - layer.addSublayer(shapeLayer) - - } -} diff --git a/14th-team5-iOS/Data/Sources/View/Main/DTO/MainResponseDTO.swift b/14th-team5-iOS/Data/Sources/View/Main/DTO/MainResponseDTO.swift index 8914fa517..f888b78d9 100644 --- a/14th-team5-iOS/Data/Sources/View/Main/DTO/MainResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/View/Main/DTO/MainResponseDTO.swift @@ -51,7 +51,7 @@ struct Picker: Codable { let displayName: String func toDomain() -> Domain.Picker { - return .init(memberId: memberId, imageUrl: imageUrl, displayName: displayName) + return .init(imageUrl: imageUrl, displayName: displayName) } } diff --git a/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/polygonGray.imageset/Contents.json b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/polygonGray.imageset/Contents.json new file mode 100644 index 000000000..fc25592dd --- /dev/null +++ b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/polygonGray.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Polygon 1.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/polygonGray.imageset/Polygon 1.svg b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/polygonGray.imageset/Polygon 1.svg new file mode 100644 index 000000000..3c7afef39 --- /dev/null +++ b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/polygonGray.imageset/Polygon 1.svg @@ -0,0 +1,3 @@ + + + diff --git a/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/polygonYellow.imageset/Contents.json b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/polygonYellow.imageset/Contents.json new file mode 100644 index 000000000..3b20ff905 --- /dev/null +++ b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/polygonYellow.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Polygon 1 (1).svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/polygonYellow.imageset/Polygon 1 (1).svg b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/polygonYellow.imageset/Polygon 1 (1).svg new file mode 100644 index 000000000..f13eb3c6e --- /dev/null +++ b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/polygonYellow.imageset/Polygon 1 (1).svg @@ -0,0 +1,3 @@ + + + diff --git a/14th-team5-iOS/Domain/Sources/Main/Entities/MainData.swift b/14th-team5-iOS/Domain/Sources/Main/Entities/MainData.swift index c274543b9..0a8077bcf 100644 --- a/14th-team5-iOS/Domain/Sources/Main/Entities/MainData.swift +++ b/14th-team5-iOS/Domain/Sources/Main/Entities/MainData.swift @@ -8,12 +8,10 @@ import Foundation public struct Picker { - let memberId: String - let imageUrl: String - let displayName: String + public let imageUrl: String + public let displayName: String - public init(memberId: String, imageUrl: String, displayName: String) { - self.memberId = memberId + public init(imageUrl: String, displayName: String) { self.imageUrl = imageUrl self.displayName = displayName } @@ -45,7 +43,7 @@ public struct MainData { self.isMissionUnlocked = isMissionUnlocked self.isMeSurvivalUploadedToday = isMeSurvivalUploadedToday self.isMeMissionUploadedToday = isMeMissionUploadedToday - self.pickers = pickers + self.pickers = isMeSurvivalUploadedToday ? []: pickers self.dailyMissionContent = dailyMissionContent self.isFamilySurvivalUploadedToday = survivalUploadCount == mainFamilyProfileDatas.count self.isFamilyMissionUploadedToday = missionUploadCount == mainFamilyProfileDatas.count diff --git a/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme b/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme index 73b9aa620..72399b415 100644 --- a/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme +++ b/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme @@ -300,20 +300,6 @@ ReferencedContainer = "container:Tuist/Dependencies/SwiftPackageManager/.build/checkouts/GoogleDataTransport/GoogleDataTransport.xcodeproj"> - - - - - - - - - - - - - - - - - - - - Date: Fri, 10 May 2024 23:08:48 +0900 Subject: [PATCH 038/263] [feat]: deploymenttarget (#519) --- .../xcschemes/Bibbi-Workspace.xcscheme | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme b/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme index 72399b415..73b9aa620 100644 --- a/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme +++ b/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme @@ -300,6 +300,20 @@ ReferencedContainer = "container:Tuist/Dependencies/SwiftPackageManager/.build/checkouts/GoogleDataTransport/GoogleDataTransport.xcodeproj"> + + + + + + + + + + + + + + + + + + + + Date: Fri, 10 May 2024 23:22:24 +0900 Subject: [PATCH 039/263] [feat]: github action(#519) --- .../xcschemes/Bibbi-Workspace.xcscheme | 56 ------------------- 1 file changed, 56 deletions(-) diff --git a/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme b/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme index 73b9aa620..2d6d16241 100644 --- a/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme +++ b/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme @@ -300,20 +300,6 @@ ReferencedContainer = "container:Tuist/Dependencies/SwiftPackageManager/.build/checkouts/GoogleDataTransport/GoogleDataTransport.xcodeproj"> - - - - - - - - - - - - - - - - Date: Fri, 10 May 2024 23:43:29 +0900 Subject: [PATCH 040/263] [feat]: deployment target(#519) --- .../xcschemes/Bibbi-Workspace.xcscheme | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme b/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme index 2d6d16241..73b9aa620 100644 --- a/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme +++ b/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme @@ -300,6 +300,20 @@ ReferencedContainer = "container:Tuist/Dependencies/SwiftPackageManager/.build/checkouts/GoogleDataTransport/GoogleDataTransport.xcodeproj"> + + + + + + + + + + + + + + + + Date: Sat, 11 May 2024 00:04:08 +0900 Subject: [PATCH 041/263] [feat]: github action --- .../xcschemes/Bibbi-Workspace.xcscheme | 106 +++++++++--------- .../Package+Templates.swift | 2 +- 2 files changed, 54 insertions(+), 54 deletions(-) diff --git a/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme b/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme index 73b9aa620..9d382a7b9 100644 --- a/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme +++ b/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme @@ -280,10 +280,10 @@ buildForAnalyzing = "YES"> + BlueprintIdentifier = "7669601E3503153E790457BC" + BuildableName = "Firebase_FirebaseCore.bundle" + BlueprintName = "Firebase_FirebaseCore" + ReferencedContainer = "container:Tuist/Dependencies/SwiftPackageManager/.build/checkouts/firebase-ios-sdk/Firebase.xcodeproj"> + BlueprintIdentifier = "C241B5678162C4E6FDA163B6" + BuildableName = "Firebase_FirebaseCoreInternal.bundle" + BlueprintName = "Firebase_FirebaseCoreInternal" + ReferencedContainer = "container:Tuist/Dependencies/SwiftPackageManager/.build/checkouts/firebase-ios-sdk/Firebase.xcodeproj"> + + + + + + + + + + + + @@ -426,34 +468,6 @@ ReferencedContainer = "container:Tuist/Dependencies/SwiftPackageManager/.build/checkouts/GoogleUtilities/GoogleUtilities.xcodeproj"> - - - - - - - - - - - - Date: Sat, 11 May 2024 06:07:26 +0900 Subject: [PATCH 042/263] =?UTF-8?q?[feat]:=20=EC=9E=94=EC=98=A4=EB=A5=98?= =?UTF-8?q?=20=EC=88=98=EC=A0=95(#523)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../App/Sources/Manager/DeepLinkManager.swift | 2 +- .../Dependency/CalendarPostDIContainer.swift | 2 +- .../MonthlyCalendarViewController.swift | 4 +- .../Dependency/CameraDisplayDIContainer.swift | 11 +- .../Reactor/CameraDisplayViewReactor.swift | 19 ++-- .../Dependency/MainPostViewDIContainer.swift | 11 +- .../Presentation/Home/Model/Balloon.swift | 66 ++++++++++++ .../Home/Model/DescriptionText.swift | 44 ++++++++ .../Home/Reactor/BalloonReactor.swift | 16 --- .../Reactor/Cell/MainFamilyCellReactor.swift | 4 +- .../Home/Reactor/MainCameraReactor.swift | 23 ---- .../Home/Reactor/MainFamilyViewReactor.swift | 2 +- .../Home/Reactor/MainPostViewReactor.swift | 21 +++- .../Home/Reactor/MainViewReactor.swift | 100 +++++++----------- .../Home/Reactor/TimerReactor.swift | 2 +- .../MainFamilyViewController.swift | 13 ++- .../MainPostViewController.swift | 20 ++-- .../ViewControllers/MainViewController.swift | 81 +++++++++++--- .../Presentation/Home/Views/BalloonView.swift | 69 +++++++----- .../Home/Views/ContributorProfileView.swift | 17 ++- .../Home/Views/ContributorView.swift | 6 +- .../Home/Views/MainCameraButtonView.swift | 11 +- .../Home/Views/NoPostTodayView.swift | 10 +- .../Profile/ProfileViewController.swift | 6 +- .../Profile/Reactor/ProfileViewReactor.swift | 7 +- .../Sources/Extensions/Reactive+Ext.swift | 5 + .../GlobalState/GlobalStateProvider.swift | 4 +- .../{HomeService.swift => MainService.swift} | 18 +++- .../DescriptionPopoverViewController.swift | 2 +- .../View/Main/DTO/MainNightResponseDTO.swift | 2 +- .../View/Main/DTO/MainResponseDTO.swift | 2 +- .../MissionEmptyCase.imageset/Contents.json | 21 ++++ .../Group 1171277222.svg | 50 +++++++++ .../icons/key.imageset/Contents.json | 12 +++ .../icons/key.imageset/key.svg | 11 ++ .../Sources/Main/Entities/MainData.swift | 4 +- .../Sources/Main/Entities/MainNightData.swift | 4 +- .../GetTodayMissionUseCase.swift | 22 ---- .../MissionRepositoryProtocol.metal | 11 -- 39 files changed, 487 insertions(+), 248 deletions(-) create mode 100644 14th-team5-iOS/App/Sources/Presentation/Home/Model/Balloon.swift create mode 100644 14th-team5-iOS/App/Sources/Presentation/Home/Model/DescriptionText.swift rename 14th-team5-iOS/Core/Sources/GlobalState/{HomeService.swift => MainService.swift} (64%) create mode 100644 14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/Character/MissionEmptyCase.imageset/Contents.json create mode 100644 14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/Character/MissionEmptyCase.imageset/Group 1171277222.svg create mode 100644 14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/key.imageset/Contents.json create mode 100644 14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/key.imageset/key.svg delete mode 100644 14th-team5-iOS/Domain/Sources/Mission/GetMissionUseCases/GetTodayMissionUseCase.swift delete mode 100644 14th-team5-iOS/Domain/Sources/Mission/Interfaces/MissionRepositoryProtocol.metal diff --git a/14th-team5-iOS/App/Sources/Manager/DeepLinkManager.swift b/14th-team5-iOS/App/Sources/Manager/DeepLinkManager.swift index c4e195f71..eb9d0d952 100644 --- a/14th-team5-iOS/App/Sources/Manager/DeepLinkManager.swift +++ b/14th-team5-iOS/App/Sources/Manager/DeepLinkManager.swift @@ -99,7 +99,7 @@ final class DeepLinkManager { if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, let keyWindow = windowScene.windows.first(where: { $0.isKeyWindow }), let navigationController = keyWindow.rootViewController as? UINavigationController { - let viewController = weeklyCalendarDIConatainer( + let viewController = WeeklyCalendarDIConatainer( date: data.dateOfPost, deepLink: data ).makeViewController() diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/CalendarPostDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/CalendarPostDIContainer.swift index 62788928c..962859024 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/CalendarPostDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/CalendarPostDIContainer.swift @@ -11,7 +11,7 @@ import Data import Domain import UIKit -public final class weeklyCalendarDIConatainer { +public final class WeeklyCalendarDIConatainer { // MARK: - Properties let date: Date diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/MonthlyCalendarViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/MonthlyCalendarViewController.swift index 304302739..f87b35029 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/MonthlyCalendarViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/MonthlyCalendarViewController.swift @@ -90,7 +90,7 @@ public final class MonthlyCalendarViewController: BaseViewController Reactor { - return CameraDisplayViewReactor(cameraDisplayUseCase: makeUseCase(), displayData: displayData, missionTitle: missionTitle, cameraType: cameraDisplayType) + return CameraDisplayViewReactor(provider: globalState, cameraDisplayUseCase: makeUseCase(), displayData: displayData, missionTitle: missionTitle, cameraType: cameraDisplayType) } } diff --git a/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraDisplayViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraDisplayViewReactor.swift index fde374d82..849ebe498 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraDisplayViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraDisplayViewReactor.swift @@ -13,8 +13,9 @@ import ReactorKit import Core public final class CameraDisplayViewReactor: Reactor { - + public var initialState: State + private let provider: GlobalStateProviderProtocol private var cameraDisplayUseCase: CameraDisplayViewUseCaseProtocol public enum Action { @@ -53,11 +54,13 @@ public final class CameraDisplayViewReactor: Reactor { init( - cameraDisplayUseCase: CameraDisplayViewUseCaseProtocol, - displayData: Data, - missionTitle: String, - cameraType: PostType = .survival + provider: GlobalStateProviderProtocol, + cameraDisplayUseCase: CameraDisplayViewUseCaseProtocol, + displayData: Data, + missionTitle: String, + cameraType: PostType = .survival ) { + self.provider = provider self.cameraDisplayUseCase = cameraDisplayUseCase self.initialState = State( isLoading: true, @@ -140,7 +143,7 @@ public final class CameraDisplayViewReactor: Reactor { case .didTapConfirmButton: MPEvent.Camera.uploadPhoto.track(with: nil) - + guard let presingedURL = currentState.displayEntity?.imageURL else { return .just(.setError(true)) } let originURL = configureOriginalS3URL(url: presingedURL) let cameraQuery = CameraMissionFeedQuery(type: currentState.cameraType.rawValue, isUploded: true) @@ -162,7 +165,9 @@ public final class CameraDisplayViewReactor: Reactor { .just(.setLoading(false)), .just(.setPostEntity(entity)), .just(.setLoading(true)), - .just(.setError(false)) + .just(.setError(false)), + self.provider.mainService.refreshMain() + .flatMap { _ in Observable.empty() } ) } } diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Dependency/MainPostViewDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Dependency/MainPostViewDIContainer.swift index e813d3069..dde7af3be 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Dependency/MainPostViewDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Dependency/MainPostViewDIContainer.swift @@ -5,7 +5,7 @@ // Created by 마경미 on 21.04.24. // -import Foundation +import UIKit import Core import Data @@ -13,12 +13,19 @@ import Domain final class MainPostViewDIContainer { + private var globalState: GlobalStateProviderProtocol { + guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { + return GlobalStateProvider() + } + return appDelegate.globalStateProvider + } + func makeViewController(type: PostType) -> MainPostViewController { return MainPostViewController(reactor: makeReactor(type: type)) } private func makeReactor(type: PostType) -> MainPostViewReactor { - return MainPostViewReactor(initialState: .init(type: type), postUseCase: makePostUseCase()) + return MainPostViewReactor(initialState: .init(type: type), provider: globalState, postUseCase: makePostUseCase()) } } diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Model/Balloon.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Model/Balloon.swift new file mode 100644 index 000000000..363890728 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Model/Balloon.swift @@ -0,0 +1,66 @@ +// +// BalloonText.swift +// App +// +// Created by 마경미 on 11.05.24. +// + +import Foundation + +import Domain + +enum BalloonType: Equatable { + static func == (lhs: BalloonType, rhs: BalloonType) -> Bool { + switch (lhs, rhs) { + case (.normal, .normal): + return true + case (.picks, .picks): + return true + default: + return false + } + } + + case normal + case picks([Picker]) +} + +enum BalloonText { + case survivalStandard + case survivalDone + case missionLocked + case cantMission + case canMission + case missionDone + case picker(Picker) + case pickers([Picker]) + + var message: String { + switch self { + case .survivalStandard: + return "하루에 한번 사진을 올릴 수 있어요" + case .survivalDone: + return "오늘의 생존신고는 완료되었어요" + case .missionLocked: + return "아직 미션 사진을 찍을 수 없어요" + case .cantMission: + return "생존신고 후 미션 사진을 올릴 수 있어요" + case .canMission: + return "미션 사진을 찍으러 가볼까요?" + case .missionDone: + return "오늘의 미션은 완료되었어요" + case .picker(let picker): + return "\(picker.displayName)님이 기다리고 있어요" + case .pickers(let pickers): + return "\(pickers.first?.displayName ?? "알 수 없음")님 외 \(pickers.count - 1)명이 기다리고 있어요" + } + } + + var balloonType: BalloonType { + switch self { + case .picker(let picker): return .picks([picker]) + case .pickers(let pickers): return .picks(pickers) + default: return .normal + } + } +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Model/DescriptionText.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Model/DescriptionText.swift new file mode 100644 index 000000000..9f6ed334c --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Model/DescriptionText.swift @@ -0,0 +1,44 @@ +// +// DescriptionText.swift +// App +// +// Created by 마경미 on 11.05.24. +// + +import UIKit + +import DesignSystem + +enum Description { + case survivalNone + case survivalFull + case missionNone(Int) + case mission(String) + case missionFull + + var text: String { + switch self { + case .survivalNone: + return "매일 10-24시에 사진 한 장을 올려요" + case .survivalFull: + return "우리 가족 모두가 사진을 올린 날" + case .missionNone(let count): + return "가족 중 \(count)명만 더 올리면 미션 열쇠를 받아요!" + case .mission(let string): + return string + case .missionFull: + return "우리 가족 모두가 미션을 성공한 날" + } + } + + var image: UIImage { + switch self { + case .survivalNone, .mission: + return DesignSystemAsset.smile.image + case .missionFull, .survivalFull: + return DesignSystemAsset.congratulation.image + case .missionNone: + return DesignSystemAsset.key.image + } + } +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/BalloonReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/BalloonReactor.swift index 685d5f460..df32cbc33 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/BalloonReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/BalloonReactor.swift @@ -12,22 +12,6 @@ import Domain import ReactorKit -enum BalloonType: Equatable { - static func == (lhs: BalloonType, rhs: BalloonType) -> Bool { - switch (lhs, rhs) { - case (.normal, .normal): - return true - case (.picks, .picks): - return true - default: - return false - } - } - - case normal - case picks([Picker]) -} - final class BalloonReactor: Reactor { enum Action { case setType(BalloonType) diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/Cell/MainFamilyCellReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/Cell/MainFamilyCellReactor.swift index 2187d866e..879f6dd9e 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/Cell/MainFamilyCellReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/Cell/MainFamilyCellReactor.swift @@ -53,7 +53,7 @@ final class MainFamilyCellReactor: Reactor { // MARK: - Transofrm func transform(mutation: Observable) -> Observable { - let homeMutation = provider.homeService.event + let homeMutation = provider.mainService.event .flatMap { event in switch event { case let .showPickButton(show, id): @@ -77,7 +77,7 @@ final class MainFamilyCellReactor: Reactor { return Observable.just(.setData) case .pickButtonTapped: - provider.homeService.pickButtonTapped( + provider.mainService.pickButtonTapped( name: currentState.profileData.name, memberId: currentState.profileData.memberId ) diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainCameraReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainCameraReactor.swift index 59cac7e72..04b4f0356 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainCameraReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainCameraReactor.swift @@ -11,29 +11,6 @@ import Domain import ReactorKit -enum BalloonText { - case survivalStandard - case cantMission - case canMission - case picker(Picker) - case pickers([Picker]) - - var message: String { - switch self { - case .survivalStandard: - return "하루에 한번 사진을 올릴 수 있어요" - case .cantMission: - return "아직 미션 사진을 찍을 수 없어요" - case .canMission: - return "미션 사진을 찍으러 가볼까요?" - case .picker(let picker): - return "\(picker.displayName)님이 기다리고 있어요" - case .pickers(let pickers): - return "\(pickers.first?.displayName ?? "알 수 없음")님 외 \(pickers.count - 1)명이 기다리고 있어요" - } - } -} - final class MainCameraReactor: Reactor { enum Action { case setText(BalloonText) diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainFamilyViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainFamilyViewReactor.swift index dc691d676..60dec4726 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainFamilyViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainFamilyViewReactor.swift @@ -33,7 +33,7 @@ final class MainFamilyViewReactor: Reactor { @Pulse var shouldPresentFetchFailureToastMessageView: Bool = false @Pulse var familySection: FamilySection.Model = FamilySection.Model(model: 0, items: []) - var isShowingInviteFamilyView: Bool = false + @Pulse var isShowingInviteFamilyView: Bool = false } let initialState: State = State() diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainPostViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainPostViewReactor.swift index c36fba658..f0036c6df 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainPostViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainPostViewReactor.swift @@ -20,6 +20,7 @@ final class MainPostViewReactor: Reactor { } enum Mutation { + case updateRefreshEnd(Bool) case setNoPostTodayView(Bool) case updatePostDataSource([PostSection.Item]) } @@ -33,10 +34,12 @@ final class MainPostViewReactor: Reactor { } let initialState: State + private let provider: GlobalStateProviderProtocol private let postUseCase: PostListUseCaseProtocol - init(initialState: State, postUseCase: PostListUseCaseProtocol) { + init(initialState: State, provider: GlobalStateProviderProtocol, postUseCase: PostListUseCaseProtocol) { self.initialState = initialState + self.provider = provider self.postUseCase = postUseCase } } @@ -45,7 +48,11 @@ extension MainPostViewReactor { func mutate(action: Action) -> Observable { switch action { case .refresh: - return self.mutate(action: .fetchPost) + return Observable.concat([ + provider.mainService.refreshMain() + .flatMap { _ in Observable.empty() }, + self.mutate(action: .fetchPost) + ]) case .fetchPost: let query = PostListQuery(date: DateFormatter.dashYyyyMMdd.string(from: Date()), type: currentState.type) return postUseCase.excute(query: query) @@ -53,13 +60,17 @@ extension MainPostViewReactor { .flatMap { (postList) -> Observable in guard let postList = postList, !postList.postLists.isEmpty else { - return Observable.from([Mutation.setNoPostTodayView(true)]) + return Observable.from([ + Mutation.setNoPostTodayView(true), + Mutation.updateRefreshEnd(true) + ]) } let postSectionItem = postList.postLists.map(PostSection.Item.main) let mutations = [ Mutation.updatePostDataSource(postSectionItem), - Mutation.setNoPostTodayView(false) + Mutation.setNoPostTodayView(false), + Mutation.updateRefreshEnd(true) ] return Observable.from(mutations) @@ -77,6 +88,8 @@ extension MainPostViewReactor { App.Repository.member.postId.accept(UserDefaults.standard.postId) case .setNoPostTodayView(let isShow): newState.isShowingNoPostTodayView = isShow + case .updateRefreshEnd(let status): + newState.isRefreshEnd = status } return newState diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift index 3838073ec..ff621ae53 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift @@ -15,41 +15,6 @@ import ReactorKit import RxDataSources import Kingfisher - -enum Description { - case survivalNone - case survivalFull - case missionNone(Int) - case mission(String) - case missionFull - - var text: String { - switch self { - case .survivalNone: - return "매일 12-24시에 사진 한 장을 올려요" - case .survivalFull: - return "우리 가족 모두가 사진을 올린 날" - case .missionNone(let count): - return "가족 중 \(count)명만 더 올리면 미션 열쇠를 받아요!" - case .mission(let string): - return string - case .missionFull: - return "우리 가족 모두가 미션을 성공한 날" - } - } - - var image: UIImage { - switch self { - case .survivalNone, .mission: - return DesignSystemAsset.smile.image - case .missionFull, .survivalFull: - return DesignSystemAsset.congratulation.image - case .missionNone: - return DesignSystemAsset.missionKeyGraphic.image - } - } -} - final class MainViewReactor: Reactor { enum Action { case calculateTime @@ -106,10 +71,10 @@ final class MainViewReactor: Reactor { } let initialState: State = State() - let fetchMainUseCase: FetchMainUseCaseProtocol - let fetchMainNightUseCase: FetchMainNightUseCaseProtocol - let pickUseCase: PickUseCaseProtocol - let provider: GlobalStateProviderProtocol + private let fetchMainUseCase: FetchMainUseCaseProtocol + private let fetchMainNightUseCase: FetchMainNightUseCaseProtocol + private let pickUseCase: PickUseCaseProtocol + private let provider: GlobalStateProviderProtocol init( fetchMainUseCase: FetchMainUseCaseProtocol, @@ -125,11 +90,13 @@ final class MainViewReactor: Reactor { extension MainViewReactor { func transform(mutation: Observable) -> Observable { - let homeMutation = provider.homeService.event + let homeMutation = provider.mainService.event .flatMap { event in switch event { case let .presentPickAlert(name, id): return Observable.just(.setPickAlertView(name, id)) + case .refreshMain: + return self.mutate(action: .calculateTime) default: return Observable.empty() } @@ -224,8 +191,11 @@ extension MainViewReactor { response.success else { return Observable.just(.setFailureToastMessage) } - self.provider.homeService.showPickButton(false, memberId: id) - return Observable.just(.setPickSuccessToastMessage(name)) + self.provider.mainService.showPickButton(false, memberId: id) + return Observable.concat( + Observable.just(.setPickSuccessToastMessage(name)), + self.mutate(action: .calculateTime) + ) } } } @@ -270,10 +240,10 @@ extension MainViewReactor { if currentState.pageIndex == 0 { newState.cameraEnabled = !currentState.isMeSurvivalUploadedToday } else { - if currentState.isMeMissionUploadedToday || currentState.isMissionUnlocked { - newState.cameraEnabled = false - } else { + if currentState.isMeMissionUploadedToday && currentState.isMissionUnlocked { newState.cameraEnabled = true + } else { + newState.cameraEnabled = false } } case .setBalloonText: @@ -288,7 +258,9 @@ extension MainViewReactor { newState.balloonText = .survivalStandard } } else { - if currentState.isMissionUnlocked || !currentState.isMeSurvivalUploadedToday { + if !currentState.isMissionUnlocked { + newState.balloonText = .missionLocked + } else if !currentState.isMeMissionUploadedToday { newState.balloonText = .cantMission } else { newState.balloonText = .canMission @@ -302,7 +274,7 @@ extension MainViewReactor { newState.description = .survivalNone } } else { - if currentState.isMissionUnlocked { + if !currentState.isMissionUnlocked { newState.description = .missionNone(currentState.leftCount) } else { if currentState.isFamilyMissionUploadedToday { @@ -320,24 +292,24 @@ extension MainViewReactor { extension MainViewReactor { private static func calculateRemainingTime() -> (Bool, 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)) - } - } +// 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, 0) + return (true, 1000) } } diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/TimerReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/TimerReactor.swift index 0a4bbf2f0..ed4b9bfbf 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/TimerReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/TimerReactor.swift @@ -81,7 +81,7 @@ extension TimerReactor { let calendar = Calendar.current let currentTime = Date() - let isAfterNoon = calendar.component(.hour, from: currentTime) >= 12 + let isAfterNoon = calendar.component(.hour, from: currentTime) >= 10 if isAfterNoon { if let nextMidnight = calendar.date(bySettingHour: 0, minute: 0, second: 0, of: currentTime.addingTimeInterval(24 * 60 * 60)) { diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainFamilyViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainFamilyViewController.swift index 60bc5698e..8ef04bbfb 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainFamilyViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainFamilyViewController.swift @@ -19,7 +19,7 @@ final class MainFamilyViewController: BaseViewController private let inviteFamilyView: InviteFamilyView = InviteFamilyView(openType: .makeUrl) private let familyCollectionView: UICollectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) - let familySectionRelay: BehaviorRelay<[FamilySection.Item]> = BehaviorRelay(value: .init()) + let familySectionRelay: PublishSubject<[FamilySection.Item]> = PublishSubject() override func viewDidLoad() { super.viewDidLoad() @@ -74,6 +74,7 @@ final class MainFamilyViewController: BaseViewController extension MainFamilyViewController { private func bindInput(reactor: MainFamilyViewReactor) { familySectionRelay + .skip(1) .map { Reactor.Action.updateFamilySection($0) } .bind(to: reactor.action) .disposed(by: disposeBag) @@ -106,17 +107,19 @@ extension MainFamilyViewController { .bind(to: familyCollectionView.rx.items(dataSource: createFamilyDataSource())) .disposed(by: disposeBag) - reactor.state.map { $0.isShowingInviteFamilyView} + reactor.pulse(\.$isShowingInviteFamilyView) .observe(on: MainScheduler.instance) - .distinctUntilChanged() .map { !$0 } + .distinctUntilChanged() + .debug("가족 없을 때 ") .bind(to: inviteFamilyView.rx.isHidden) .disposed(by: disposeBag) - reactor.state.map { $0.isShowingInviteFamilyView} + reactor.pulse(\.$isShowingInviteFamilyView) .observe(on: MainScheduler.instance) - .distinctUntilChanged() .map { $0 } + .distinctUntilChanged() + .debug("가족 있을 때") .bind(to: familyCollectionView.rx.isHidden) .disposed(by: disposeBag) diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainPostViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainPostViewController.swift index c4ede9230..f3d07544a 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainPostViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainPostViewController.swift @@ -18,7 +18,8 @@ final class MainPostViewController: BaseViewController, UIC private let postCollectionView: UICollectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) private let refreshControl: UIRefreshControl = UIRefreshControl() - private let noPostView: NoPostTodayView = NoPostTodayView() + + lazy var noPostView: NoPostTodayView = NoPostTodayView(type: reactor?.currentState.type ?? .survival, frame: .init()) override func viewDidLoad() { super.viewDidLoad() @@ -48,7 +49,9 @@ final class MainPostViewController: BaseViewController, UIC } noPostView.snp.makeConstraints { - $0.edges.equalToSuperview() + $0.width.equalTo(220) + $0.height.equalTo(180) + $0.top.centerX.equalTo(postCollectionView) } } @@ -63,6 +66,10 @@ final class MainPostViewController: BaseViewController, UIC $0.refreshControl?.tintColor = UIColor.bibbiWhite $0.register(MainPostCollectionViewCell.self, forCellWithReuseIdentifier: MainPostCollectionViewCell.id) } + + noPostView.do { + $0.isUserInteractionEnabled = false + } } } @@ -71,7 +78,7 @@ extension MainPostViewController { postCollectionView.rx.setDelegate(self) .disposed(by: disposeBag) - self.rx.viewWillAppear + Observable.just(()) .map { _ in Reactor.Action.fetchPost } .bind(to: reactor.action) .disposed(by: disposeBag) @@ -118,13 +125,6 @@ extension MainPostViewController { .map { !$0 } .bind(to: noPostView.rx.isHidden) .disposed(by: disposeBag) -// -// reactor.pulse(\.$postSection) -// .withUnretained(self) -// .bind(onNext: { -// $0.0.timerView.reactor = TimerDIContainer().makeReactor(isSelfUploaded: reactor.currentState.isSelfUploaded, isAllUploaded: reactor.currentState.isAllFamilyMembersUploaded) -// }) -// .disposed(by: disposeBag) } } 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 9199601d8..a305aac6c 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift @@ -17,13 +17,13 @@ import RxSwift final class MainViewController: BaseViewController, UICollectionViewDelegateFlowLayout { private let familyViewController: MainFamilyViewController = MainFamilyViewDIContainer().makeViewController() - + private let timerView: TimerView = TimerView(reactor: TimerReactor()) private let descriptionLabel: BibbiLabel = BibbiLabel(.body2Regular, textAlignment: .center, textColor: .gray300) private let imageView: UIImageView = UIImageView() private let contributorView: ContributorView = ContributorView(reactor: ContributorReactor()) - private let segmentControl: BibbiSegmentedControl = BibbiSegmentedControl(isUpdated: true) + private let segmentControl: BibbiSegmentedControl = BibbiSegmentedControl(isUpdated: false) private let pageViewController: SegmentPageViewController = SegmentPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal) private let cameraButton: MainCameraButtonView = MainCameraDIContainer().makeView() @@ -75,7 +75,7 @@ final class MainViewController: BaseViewController, UICollectio $0.height.equalTo(20) $0.centerX.equalToSuperview() } - + imageView.snp.makeConstraints { $0.top.equalTo(descriptionLabel) $0.size.equalTo(20) @@ -96,7 +96,7 @@ final class MainViewController: BaseViewController, UICollectio } pageViewController.view.snp.makeConstraints { - $0.top.equalTo(segmentControl.snp.bottom) + $0.top.equalTo(segmentControl.snp.bottom).offset(40) $0.horizontalEdges.bottom.equalToSuperview() } @@ -112,26 +112,30 @@ final class MainViewController: BaseViewController, UICollectio navigationBarView.do { $0.setNavigationView(leftItem: .family, centerItem: .logo, rightItem: .calendar) } + + contributorView.do { + $0.isHidden = true + } } } extension MainViewController { private func bindInput(reactor: MainViewReactor) { Observable.merge( - self.rx.viewWillAppear + Observable.just(()) .map { _ in Reactor.Action.calculateTime }, NotificationCenter.default.rx.notification(UIScene.willEnterForegroundNotification) .map { _ in Reactor.Action.calculateTime } ) .bind(to: reactor.action) .disposed(by: disposeBag) - + segmentControl.survivalButton.rx.tap .throttle(.milliseconds(1000), scheduler: MainScheduler.instance) .map { Reactor.Action.didTapSegmentControl(.survival) } .bind(to: reactor.action) .disposed(by: disposeBag) - + segmentControl.missionButton.rx.tap .throttle(.milliseconds(1000), scheduler: MainScheduler.instance) .map { Reactor.Action.didTapSegmentControl(.mission) } @@ -149,21 +153,46 @@ extension MainViewController { .withUnretained(self) .bind { $0.0.navigationController?.pushViewController(MonthlyCalendarDIConatainer().makeViewController(), animated: true) } .disposed(by: disposeBag) - + alertConfirmRelay .map { Reactor.Action.pickConfirmButtonTapped($0.0, $0.1) } .bind(to: reactor.action) .disposed(by: disposeBag) - cameraButton.camerTapObservable + cameraButton.camerTapEvent .throttle(RxConst.milliseconds300Interval, scheduler: MainScheduler.instance) .withUnretained(self) .bind(onNext: { MPEvent.Home.cameraTapped.track(with: nil) - let cameraViewController = CameraDIContainer(cameraType: .survival).makeViewController() + let cameraViewController = CameraDIContainer( + cameraType: reactor.currentState.pageIndex == 0 ? .survival : .mission).makeViewController() $0.0.navigationController?.pushViewController(cameraViewController, animated: true) }) .disposed(by: disposeBag) + + contributorView.infoButton.rx.tap + .throttle(RxConst.milliseconds300Interval, scheduler: MainScheduler.instance) + .withUnretained(self) + .bind(onNext: { + $0.0.makeDescriptionPopoverView( + $0.0, + sourceView: $0.0.contributorView.infoButton, + text: "생존신고 횟수가 동일한 경우\n이모지, 댓글 수를 합산해서 등수를 정해요", + popoverSize: CGSize(width: 260, height: 62), + permittedArrowDrections: [.up] + ) + }) + .disposed(by: disposeBag) + + contributorView.nextButtonTapEvent + .throttle(RxConst.milliseconds300Interval, scheduler: MainScheduler.instance) + .observe(on: MainScheduler.instance) + .bind(onNext: { [weak self] _ in + guard let self else { return } + let calendarViewController = WeeklyCalendarDIConatainer(date: reactor.currentState.contributor.recentPostDate.toDate()).makeViewController() + self.navigationController?.pushViewController(calendarViewController, animated: true) + }) + .disposed(by: disposeBag) } private func bindOutput(reactor: MainViewReactor) { @@ -196,7 +225,7 @@ extension MainViewController { .disposed(by: disposeBag) reactor.state.map { $0.balloonText } - .debug("balloonText") + .distinctUntilChanged { $0.message } .bind(to: cameraButton.textRelay) .disposed(by: disposeBag) @@ -204,10 +233,14 @@ extension MainViewController { .distinctUntilChanged { $0.text } .withUnretained(self) .observe(on: MainScheduler.instance) - .bind(onNext: { - $0.0.descriptionLabel.text = $0.1.text - $0.0.imageView.image = $0.1.image - }) + .bind(onNext: { $0.0.setDescription($0.1) }) + .disposed(by: disposeBag) + + reactor.state.map { $0.isMissionUnlocked } + .distinctUntilChanged() + .withUnretained(self) + .observe(on: MainScheduler.instance) + .bind(onNext: { $0.0.segmentControl.isUpdated = $0.1 }) .disposed(by: disposeBag) reactor.pulse(\.$contributor) @@ -265,4 +298,22 @@ extension MainViewController { segmentControl.isHidden = true } } + + private func setDescription(_ description: Description) { + if case let .missionNone(number) = description { + let attributedString = NSMutableAttributedString(string: description.text) + let range = (description.text as NSString).range(of: "\(number)명") + attributedString.addAttribute(.foregroundColor, value: UIColor.mainYellow, range: range) + descriptionLabel.attributedText = attributedString + } else { + descriptionLabel.text = description.text + } + imageView.image = description.image + } +} + +extension MainViewController: UIPopoverPresentationControllerDelegate { + public func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle { + return .none + } } diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Views/BalloonView.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Views/BalloonView.swift index a0b085140..dc17af52c 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Views/BalloonView.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Views/BalloonView.swift @@ -52,7 +52,7 @@ final class BalloonView: BaseView { stackView.snp.makeConstraints { $0.leading.equalToSuperview().inset(16) $0.width.equalTo(0) - $0.height.equalTo(20) + $0.height.equalTo(24) $0.centerY.equalToSuperview() } @@ -113,7 +113,7 @@ extension BalloonView { textLabel.textColor = .bibbiBlack stackView.snp.updateConstraints { - $0.width.equalTo(pickers.count <= 1 ? 20 : 16 * pickers.count) + $0.width.equalTo(pickers.count <= 1 ? 24 : 20 * pickers.count) } textLabel.snp.updateConstraints { @@ -121,30 +121,49 @@ extension BalloonView { } pickers.forEach { - let imageView = UIImageView(frame: .init(x: 0, y: 0, width: 20, height: 20)) - imageView.contentMode = .scaleAspectFill - imageView.layer.borderColor = UIColor.mainYellow.cgColor - imageView.layer.borderWidth = 2 - imageView.clipsToBounds = true - imageView.layer.cornerRadius = 10 - imageView.kf.setImage(with: URL(string: $0.imageUrl)) + if let url = $0.imageUrl { + makeImageView(url: url) + } else { + makeLabel(name: $0.displayName) + } - stackView.addArrangedSubview(imageView) } - } else { - containerView.backgroundColor = .gray700 - polygonImageView.image = DesignSystemAsset.polygonGray.image - textLabel.textColor = .white - - stackView.snp.updateConstraints { - $0.width.equalTo(0) - } - - textLabel.snp.updateConstraints { - $0.leading.equalTo(stackView.snp.trailing).offset(0) - } - - stackView.removeAllArrangedSubviews() - } + } else { + containerView.backgroundColor = .gray700 + polygonImageView.image = DesignSystemAsset.polygonGray.image + textLabel.textColor = .white + + stackView.snp.updateConstraints { + $0.width.equalTo(0) + } + + textLabel.snp.updateConstraints { + $0.leading.equalTo(stackView.snp.trailing).offset(0) + } + + stackView.removeAllArrangedSubviews() + } + } + + private func makeImageView(url: String) { + let imageView = UIImageView(frame: .init(x: 0, y: 0, width: 20, height: 20)) + imageView.contentMode = .scaleAspectFill + imageView.layer.borderColor = UIColor.mainYellow.cgColor + imageView.layer.borderWidth = 2 + imageView.clipsToBounds = true + imageView.layer.cornerRadius = 12 + imageView.kf.setImage(with: URL(string: url)) + stackView.addArrangedSubview(imageView) + } + + private func makeLabel(name: String){ + let label = BibbiLabel(.caption2, textAlignment: .center, textColor: .gray200) + label.backgroundColor = .gray800 + label.layer.borderWidth = 2 + label.layer.borderColor = UIColor.mainYellow.cgColor + label.clipsToBounds = true + label.layer.cornerRadius = 12 + label.text = "\(name.first ?? "알")" + stackView.addArrangedSubview(label) } } diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Views/ContributorProfileView.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Views/ContributorProfileView.swift index 6ea491a9e..7c896636e 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Views/ContributorProfileView.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Views/ContributorProfileView.swift @@ -18,6 +18,7 @@ import RxSwift final class ContributorProfileView: BaseView { private let nameLabel = BibbiLabel(.body2Bold, textAlignment: .center, textColor: .gray200) private let countLabel = BibbiLabel(.body2Regular, textAlignment: .center, textColor: .gray300) + private let defaultNameLabel = BibbiLabel(.head1, textAlignment: .center, textColor: .gray200) private let imageView = UIImageView() private let questionView = UIImageView() private let badgeView = UIImageView() @@ -31,7 +32,7 @@ final class ContributorProfileView: BaseView { override func setupUI() { addSubviews(imageView, nameLabel, countLabel, - questionView, badgeView) + defaultNameLabel, questionView, badgeView) } override func setupAutoLayout() { @@ -47,6 +48,10 @@ final class ContributorProfileView: BaseView { $0.width.equalTo(18) } + defaultNameLabel.snp.makeConstraints { + $0.edges.equalTo(questionView) + } + badgeView.snp.makeConstraints { $0.width.equalTo(imageView.snp.width).dividedBy(3.5) $0.height.equalTo(imageView.snp.height).dividedBy(2.8) @@ -72,7 +77,7 @@ final class ContributorProfileView: BaseView { $0.clipsToBounds = true $0.layer.borderWidth = 4 $0.tintColor = .gray400 - $0.contentMode = .scaleAspectFit + $0.contentMode = .scaleAspectFill } nameLabel.do { @@ -116,9 +121,15 @@ extension ContributorProfileView { questionView.isHidden = true imageView.layer.borderColor = reactor?.currentState.rank.borderColor.cgColor - imageView.kf.setImage(with: URL(string: data.imageURL)) badgeView.image = reactor?.currentState.rank.badgeImage + + guard let url = data.imageURL else { + imageView.image = nil + defaultNameLabel.text = "\(data.name.first ?? "알")" + return + } + imageView.kf.setImage(with: URL(string: url)) } else { nameLabel.backgroundColor = .gray600 countLabel.backgroundColor = .gray700 diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Views/ContributorView.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Views/ContributorView.swift index 8caf7c5cc..3688dcb68 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Views/ContributorView.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Views/ContributorView.swift @@ -18,7 +18,7 @@ final class ContributorView: BaseView { private let containerView: UIView = UIView() private let titleLabel: BibbiLabel = BibbiLabel(.head2Bold, textColor: .gray200) - private let infoButton: UIButton = UIButton() + let infoButton: UIButton = UIButton() private let subTitleLabel: BibbiLabel = BibbiLabel(.body2Regular, textColor: .gray300) private let firstProfileView: ContributorProfileView = ContributorProfileView(reactor: ContributorProfileReactor(initialState: .init(rank: .first))) @@ -29,6 +29,10 @@ final class ContributorView: BaseView { let contributorRelay: BehaviorRelay = .init(value: FamilyRankData.empty) + var nextButtonTapEvent: ControlEvent { + return nextButton.rx.tap + } + override func bind(reactor: ContributorReactor) { bindInput(reactor: reactor) bindOutput(reactor: reactor) diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Views/MainCameraButtonView.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Views/MainCameraButtonView.swift index 36e2bbd74..3f877490b 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Views/MainCameraButtonView.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Views/MainCameraButtonView.swift @@ -21,7 +21,7 @@ final class MainCameraButtonView: BaseView { let textRelay: BehaviorRelay = BehaviorRelay(value: .survivalStandard) let cameraEnabledRelay: BehaviorRelay = BehaviorRelay(value: false) - var camerTapObservable: ControlEvent { + var camerTapEvent: ControlEvent { return cameraButton.rx.tap } @@ -66,14 +66,7 @@ extension MainCameraButtonView { .bind(to: balloonView.text) .disposed(by: disposeBag) - reactor.state.map { $0.balloonText } - .map { (text) -> BalloonType in - switch text { - case .picker(let picker): return .picks([picker]) - case .pickers(let pickers): return .picks(pickers) - default: return .normal - } - } + reactor.state.map { $0.balloonText.balloonType } .bind(to: balloonView.balloonTypeRelay) .disposed(by: disposeBag) diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Views/NoPostTodayView.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Views/NoPostTodayView.swift index fb4be8a1e..faef8dfc4 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Views/NoPostTodayView.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Views/NoPostTodayView.swift @@ -6,6 +6,8 @@ // import UIKit + +import Domain import DesignSystem import SnapKit @@ -14,9 +16,11 @@ import Then final class NoPostTodayView: UIView { typealias Layout = HomeAutoLayout.NoPostTodayView + let type: PostType private let imageView: UIImageView = UIImageView() - override init(frame: CGRect) { + init(type: PostType, frame: CGRect) { + self.type = type super.init(frame: frame) setupUI() setupAutoLayout() @@ -35,13 +39,13 @@ final class NoPostTodayView: UIView { imageView.snp.makeConstraints { $0.size.equalTo(Layout.ImageView.size) $0.centerX.equalToSuperview() - $0.top.equalToSuperview().inset(Layout.ImageView.topInset) + $0.top.equalToSuperview().inset(12) } } private func setupAttributes() { imageView.do { - $0.image = DesignSystemAsset.emptyCaseGraphicEmoji.image + $0.image = type == .survival ? DesignSystemAsset.emptyCaseGraphicEmoji.image: DesignSystemAsset.missionEmptyCase.image $0.contentMode = .scaleAspectFill } } diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileViewController.swift index fe55ab9fd..42bc99cc0 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileViewController.swift @@ -124,6 +124,10 @@ public final class ProfileViewController: BaseViewController .bind(to: reactor.action) .disposed(by: disposeBag) + self.rx.viewDidDisappear + .map { _ in Reactor.Action.viewDidDisappear } + .bind(to: reactor.action) + .disposed(by: disposeBag) profileView.circleButton .rx.tap @@ -164,8 +168,6 @@ public final class ProfileViewController: BaseViewController .bind(onNext: { $0.0.setupProfileButton(title: $0.1)}) .disposed(by: disposeBag) - - profileView.profileNickNameButton .rx.tap .withLatestFrom(reactor.state.compactMap { $0.profileMemberEntity?.memberId }) diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift index e49381d65..cf84cea93 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift @@ -21,10 +21,12 @@ public final class ProfileViewReactor: Reactor { private let profileUseCase: ProfileViewUsecaseProtocol private let memberId: String private let isUser: Bool + private let provider: GlobalStateProviderProtocol public enum Action { case viewDidLoad case viewWillAppear + case viewDidDisappear case updateNickNameProfile(Data) case didSelectPHAssetsImage(Data) case didTapInitProfile @@ -61,8 +63,6 @@ public final class ProfileViewReactor: Reactor { self.provider = provider } - private var provider: GlobalStateProviderProtocol - public func mutate(action: Action) -> Observable { //TODO: Keychain, UserDefaults 추가 switch action { @@ -178,6 +178,9 @@ public final class ProfileViewReactor: Reactor { case let .didTapSegementControl(feedType): return .just(.setProfileFeedType(feedType)) + case .viewDidDisappear: + return provider.mainService.refreshMain() + .flatMap { _ in Observable.empty() } } } diff --git a/14th-team5-iOS/Core/Sources/Extensions/Reactive+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/Reactive+Ext.swift index ee9b7d1ac..742c10137 100644 --- a/14th-team5-iOS/Core/Sources/Extensions/Reactive+Ext.swift +++ b/14th-team5-iOS/Core/Sources/Extensions/Reactive+Ext.swift @@ -17,6 +17,11 @@ extension Reactive where Base: UIViewController { let event = self.methodInvoked(#selector(Base.viewWillAppear)).map { $0.first as? Bool ?? false } return ControlEvent(events: event) } + + public var viewDidDisappear: ControlEvent { + let source = self.methodInvoked(#selector(Base.viewDidDisappear)).map { $0.first as? Bool ?? false } + return ControlEvent(events: source) + } } extension Reactive where Base: UIView { diff --git a/14th-team5-iOS/Core/Sources/GlobalState/GlobalStateProvider.swift b/14th-team5-iOS/Core/Sources/GlobalState/GlobalStateProvider.swift index d99a4d6c0..a617eb8c0 100644 --- a/14th-team5-iOS/Core/Sources/GlobalState/GlobalStateProvider.swift +++ b/14th-team5-iOS/Core/Sources/GlobalState/GlobalStateProvider.swift @@ -15,7 +15,7 @@ public protocol GlobalStateProviderProtocol: AnyObject { var profileGlobalState: ProfileGlobalStateType { get } var timerGlobalState: TimerGlobalStateType { get } var realEmojiGlobalState: RealEmojiGlobalStateType { get } - var homeService: HomeServiceType { get } + var mainService: MainServiceType { get } } final public class GlobalStateProvider: GlobalStateProviderProtocol { @@ -28,7 +28,7 @@ final public class GlobalStateProvider: GlobalStateProviderProtocol { public lazy var timerGlobalState: TimerGlobalStateType = TimerGlobalState(provider: self) public lazy var realEmojiGlobalState: RealEmojiGlobalStateType = RealEmojiGlobalState(provider: self) - public lazy var homeService: HomeServiceType = HomeService(provider: self) + public lazy var mainService: MainServiceType = MainService(provider: self) public init() { } } diff --git a/14th-team5-iOS/Core/Sources/GlobalState/HomeService.swift b/14th-team5-iOS/Core/Sources/GlobalState/MainService.swift similarity index 64% rename from 14th-team5-iOS/Core/Sources/GlobalState/HomeService.swift rename to 14th-team5-iOS/Core/Sources/GlobalState/MainService.swift index c9c565cf3..8d9f3e403 100644 --- a/14th-team5-iOS/Core/Sources/GlobalState/HomeService.swift +++ b/14th-team5-iOS/Core/Sources/GlobalState/MainService.swift @@ -9,22 +9,25 @@ import Foundation import RxSwift -public enum HomeEvent { +public enum MainEvent { case presentPickAlert(String, String) case showPickButton(Bool, String) + case refreshMain } -public protocol HomeServiceType { - var event: PublishSubject { get } +public protocol MainServiceType { + var event: PublishSubject { get } @discardableResult func pickButtonTapped(name: String, memberId id: String) -> Observable @discardableResult func showPickButton(_ show: Bool, memberId id: String) -> Observable + @discardableResult + func refreshMain() -> Observable } -final public class HomeService: BaseGlobalState, HomeServiceType { - public var event = PublishSubject() +final public class MainService: BaseGlobalState, MainServiceType { + public var event = PublishSubject() @discardableResult public func pickButtonTapped(name: String, memberId id: String) -> Observable { @@ -38,4 +41,9 @@ final public class HomeService: BaseGlobalState, HomeServiceType { return Observable.just(show) } + @discardableResult + public func refreshMain() -> Observable { + event.onNext(.refreshMain) + return Observable.just(()) + } } diff --git a/14th-team5-iOS/Core/Sources/Popover/DescriptionPopoverViewController.swift b/14th-team5-iOS/Core/Sources/Popover/DescriptionPopoverViewController.swift index e3a3f4da7..413635e48 100644 --- a/14th-team5-iOS/Core/Sources/Popover/DescriptionPopoverViewController.swift +++ b/14th-team5-iOS/Core/Sources/Popover/DescriptionPopoverViewController.swift @@ -12,7 +12,7 @@ import Then public final class PopoverDescriptionViewController: UIViewController { // MARK: - Views - private let descrpitionLabel: UILabel = UILabel() + private let descrpitionLabel: BibbiLabel = BibbiLabel(.body2Regular) // MARK: - Properties public var labelText: String diff --git a/14th-team5-iOS/Data/Sources/View/Main/DTO/MainNightResponseDTO.swift b/14th-team5-iOS/Data/Sources/View/Main/DTO/MainNightResponseDTO.swift index d876e636c..118241787 100644 --- a/14th-team5-iOS/Data/Sources/View/Main/DTO/MainNightResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/View/Main/DTO/MainNightResponseDTO.swift @@ -10,7 +10,7 @@ import Foundation import Domain struct Ranker: Codable { - let profileImageUrl: String + let profileImageUrl: String? let name: String let survivalCount: Int diff --git a/14th-team5-iOS/Data/Sources/View/Main/DTO/MainResponseDTO.swift b/14th-team5-iOS/Data/Sources/View/Main/DTO/MainResponseDTO.swift index f888b78d9..1c7ec0cb0 100644 --- a/14th-team5-iOS/Data/Sources/View/Main/DTO/MainResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/View/Main/DTO/MainResponseDTO.swift @@ -47,7 +47,7 @@ struct MissionFeed: Codable { struct Picker: Codable { let memberId: String - let imageUrl: String + let imageUrl: String? let displayName: String func toDomain() -> Domain.Picker { diff --git a/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/Character/MissionEmptyCase.imageset/Contents.json b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/Character/MissionEmptyCase.imageset/Contents.json new file mode 100644 index 000000000..3b138d274 --- /dev/null +++ b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/Character/MissionEmptyCase.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Group 1171277222.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/Character/MissionEmptyCase.imageset/Group 1171277222.svg b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/Character/MissionEmptyCase.imageset/Group 1171277222.svg new file mode 100644 index 000000000..52a70f783 --- /dev/null +++ b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/Character/MissionEmptyCase.imageset/Group 1171277222.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/key.imageset/Contents.json b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/key.imageset/Contents.json new file mode 100644 index 000000000..9eff0bcf0 --- /dev/null +++ b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/key.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "key.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/key.imageset/key.svg b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/key.imageset/key.svg new file mode 100644 index 000000000..15da71b97 --- /dev/null +++ b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/key.imageset/key.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/14th-team5-iOS/Domain/Sources/Main/Entities/MainData.swift b/14th-team5-iOS/Domain/Sources/Main/Entities/MainData.swift index 0a8077bcf..dba16bd42 100644 --- a/14th-team5-iOS/Domain/Sources/Main/Entities/MainData.swift +++ b/14th-team5-iOS/Domain/Sources/Main/Entities/MainData.swift @@ -8,10 +8,10 @@ import Foundation public struct Picker { - public let imageUrl: String + public let imageUrl: String? public let displayName: String - public init(imageUrl: String, displayName: String) { + public init(imageUrl: String?, displayName: String) { self.imageUrl = imageUrl self.displayName = displayName } diff --git a/14th-team5-iOS/Domain/Sources/Main/Entities/MainNightData.swift b/14th-team5-iOS/Domain/Sources/Main/Entities/MainNightData.swift index 5e34c9d54..7c5cff92d 100644 --- a/14th-team5-iOS/Domain/Sources/Main/Entities/MainNightData.swift +++ b/14th-team5-iOS/Domain/Sources/Main/Entities/MainNightData.swift @@ -44,11 +44,11 @@ public enum Rank { } public struct RankerData { - public let imageURL: String + public let imageURL: String? public let name: String public let survivalCount: Int - public init(imageURL: String, name: String, survivalCount: Int) { + public init(imageURL: String?, name: String, survivalCount: Int) { self.imageURL = imageURL self.name = name self.survivalCount = survivalCount diff --git a/14th-team5-iOS/Domain/Sources/Mission/GetMissionUseCases/GetTodayMissionUseCase.swift b/14th-team5-iOS/Domain/Sources/Mission/GetMissionUseCases/GetTodayMissionUseCase.swift deleted file mode 100644 index a113ba296..000000000 --- a/14th-team5-iOS/Domain/Sources/Mission/GetMissionUseCases/GetTodayMissionUseCase.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// GetTodayMissionUseCase.swift -// Domain -// -// Created by 마경미 on 21.04.24. -// - -import Foundation - -import RxSwift - -public final class GetTodayMissionUseCase: GetTodayMissionUseCaseProtocol { - private let missionRepository: MissionRepositoryProtocol - - public init(missionRepository: MissionRepositoryProtocol) { - self.missionRepository = missionRepository - } - - public func execute() -> Observable { - return missionRepository.getTodayMission() - } -} diff --git a/14th-team5-iOS/Domain/Sources/Mission/Interfaces/MissionRepositoryProtocol.metal b/14th-team5-iOS/Domain/Sources/Mission/Interfaces/MissionRepositoryProtocol.metal deleted file mode 100644 index 93e7fc91b..000000000 --- a/14th-team5-iOS/Domain/Sources/Mission/Interfaces/MissionRepositoryProtocol.metal +++ /dev/null @@ -1,11 +0,0 @@ -// -// MissionRepositoryProtocol.metal -// Domain -// -// Created by 마경미 on 21.04.24. -// - -#include -using namespace metal; - - From 7ba16ebcfb67200bd95a0afc61db5c24b16181e3 Mon Sep 17 00:00:00 2001 From: Kyoungmi <1mmgm.dev@gmail.com> Date: Sat, 11 May 2024 06:10:58 +0900 Subject: [PATCH 043/263] revoert calculateTIme(#523) --- .../Home/Reactor/MainViewReactor.swift | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift index ff621ae53..598d00e2b 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift @@ -292,24 +292,24 @@ extension MainViewReactor { extension MainViewReactor { private static func calculateRemainingTime() -> (Bool, 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)) -// } -// } + let calendar = Calendar.current + let currentTime = Date() - return (true, 1000) + 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, 0) } } From 4a93217dd7500d94abdd275c91694aa23eb9b08b Mon Sep 17 00:00:00 2001 From: Kyoungmi <1mmgm.dev@gmail.com> Date: Sat, 11 May 2024 06:17:34 +0900 Subject: [PATCH 044/263] =?UTF-8?q?[feat]:=20=EB=A7=90=ED=92=8D=EC=84=A0?= =?UTF-8?q?=20=ED=85=8D=EC=8A=A4=ED=8A=B8=20=EC=88=98=EC=A0=95(#523)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Home/Reactor/MainViewReactor.swift | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift index 598d00e2b..8a9fa2ece 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift @@ -248,7 +248,9 @@ extension MainViewReactor { } case .setBalloonText: if currentState.pageIndex == 0 { - if !currentState.isMeSurvivalUploadedToday && !currentState.pickers.isEmpty { + if currentState.isMeSurvivalUploadedToday { + newState.balloonText = .survivalDone + } else if !currentState.pickers.isEmpty { if currentState.pickers.count <= 1 { newState.balloonText = .picker(currentState.pickers[0]) } else { @@ -260,10 +262,14 @@ extension MainViewReactor { } else { if !currentState.isMissionUnlocked { newState.balloonText = .missionLocked - } else if !currentState.isMeMissionUploadedToday { - newState.balloonText = .cantMission } else { - newState.balloonText = .canMission + if !currentState.isMeSurvivalUploadedToday { + newState.balloonText = .cantMission + } else if currentState.isMeMissionUploadedToday { + newState.balloonText = .missionDone + } else { + newState.balloonText = .canMission + } } } case .setDescriptionText: From d67a218dd38ab5393050058ba0cffe6adce765a9 Mon Sep 17 00:00:00 2001 From: Kyoungmi <1mmgm.dev@gmail.com> Date: Sat, 11 May 2024 18:24:29 +0900 Subject: [PATCH 045/263] =?UTF-8?q?[feat]:=20QA=EC=88=98=EC=A0=95(#523)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Home/Reactor/MainViewReactor.swift | 2 +- .../MainPostViewController.swift | 2 +- .../Views/MainPostCollectionViewCell.swift | 2 +- .../PostDetail/Reactor/PostReactor.swift | 2 +- .../ViewControllers/PostViewController.swift | 13 +----------- .../PostDetail/Views/PostNavigationView.swift | 5 ++--- .../UserDefaults/FamilyUserDefautls.swift | 20 +++++++++++-------- .../View/Main/Repository/MainRepository.swift | 9 +++++++-- 8 files changed, 26 insertions(+), 29 deletions(-) diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift index 8a9fa2ece..152ce5aa3 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift @@ -240,7 +240,7 @@ extension MainViewReactor { if currentState.pageIndex == 0 { newState.cameraEnabled = !currentState.isMeSurvivalUploadedToday } else { - if currentState.isMeMissionUploadedToday && currentState.isMissionUnlocked { + if !currentState.isMeMissionUploadedToday && currentState.isMissionUnlocked { newState.cameraEnabled = true } else { newState.cameraEnabled = false diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainPostViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainPostViewController.swift index f3d07544a..1ff96679d 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainPostViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainPostViewController.swift @@ -78,7 +78,7 @@ extension MainPostViewController { postCollectionView.rx.setDelegate(self) .disposed(by: disposeBag) - Observable.just(()) + self.rx.viewWillAppear .map { _ in Reactor.Action.fetchPost } .bind(to: reactor.action) .disposed(by: disposeBag) diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Views/MainPostCollectionViewCell.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Views/MainPostCollectionViewCell.swift index 16b53a3e3..3f1811bd6 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Views/MainPostCollectionViewCell.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Views/MainPostCollectionViewCell.swift @@ -20,7 +20,7 @@ final class MainPostCollectionViewCell: BaseCollectionViewCell.just(.setSelectedPostIndex(index)) } return missionUseCase.execute(missionId: missionId) .flatMap { entity -> Observable in return .concat( diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/PostViewController.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/PostViewController.swift index 0d6fce7cb..4c7a592ea 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/PostViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/PostViewController.swift @@ -21,9 +21,6 @@ final class PostViewController: BaseViewController { private let collectionViewLayout: UICollectionViewFlowLayout = UICollectionViewFlowLayout() private let reactionViewController: ReactionViewController = ReactionDIContainer(type: .post).makeViewController(post: .init(postId: "", author: nil, commentCount: 0, emojiCount: 0, imageURL: "", content: nil, time: "")) - // MARK: - Properties - private let deepLinkRepo = DeepLinkRepository() - convenience init(reactor: Reactor? = nil) { self.init() self.reactor = reactor @@ -34,15 +31,7 @@ final class PostViewController: BaseViewController { super.viewDidLoad() self.navigationController?.navigationBar.isHidden = true } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - // HomeViewController는 notification이 nil일 때만 - // ViewWillAppear시 가족과 피드를 불러오므로, nil 항목 전달이 필수임 - App.Repository.deepLink.widget.accept(nil) - App.Repository.deepLink.notification.accept(nil) - } - + override func bind(reactor: PostReactor) { collectionView.rx.setDelegate(self) .disposed(by: disposeBag) diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/PostNavigationView.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/PostNavigationView.swift index 0626fdb19..4249a74ad 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/PostNavigationView.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/PostNavigationView.swift @@ -42,11 +42,10 @@ final class PostNavigationView: BaseView { reactor.state .map { $0.selectedPost } + .debug("selectedPost") .asObservable() .withUnretained(self) - .bind(onNext: { - $0.0.setData(data: $0.1) - }) + .bind(onNext: { $0.0.setData(data: $0.1) }) .disposed(by: disposeBag) } diff --git a/14th-team5-iOS/Data/Sources/UserDefaults/FamilyUserDefautls.swift b/14th-team5-iOS/Data/Sources/UserDefaults/FamilyUserDefautls.swift index 26aa34287..7f26c7444 100644 --- a/14th-team5-iOS/Data/Sources/UserDefaults/FamilyUserDefautls.swift +++ b/14th-team5-iOS/Data/Sources/UserDefaults/FamilyUserDefautls.swift @@ -16,7 +16,7 @@ public class FamilyUserDefaults { /// familyId - memberId를 배열로 저장 /// 각 memberId - familymember 객체 저장 - private let familyIdKey = "familyId" + private static let familyIdKey = "familyId" private static let myMemberIdKey = "memberId" private static let memberIdsKey = "memberIds" private static let dayOfBirths = "dayOfBirths" @@ -33,15 +33,20 @@ public class FamilyUserDefaults { return UserDefaults.standard.string(forKey: myMemberIdKey) ?? "" } - func removeFamilyMembers() { - UserDefaults.standard.removeObject(forKey: familyIdKey) + public static func removeFamilyMembers() { + UserDefaults.standard.stringArray(forKey: memberIdsKey)?.forEach { + UserDefaults.standard.removeObject(forKey: $0) + print($0) + } + + UserDefaults.standard.removeObject(forKey: memberIdsKey) } - static func saveMyMemberId(memberId: String) { + public static func saveMyMemberId(memberId: String) { UserDefaults.standard.setValue(memberId, forKey: myMemberIdKey) } - static func getMyMemberId() -> String { + public static func getMyMemberId() -> String { return UserDefaults.standard.string(forKey: myMemberIdKey) ?? "" } @@ -59,11 +64,10 @@ public class FamilyUserDefaults { } public static func saveFamilyMembers(_ familyMembers: [ProfileData]) { + removeFamilyMembers() saveMemberIdToUserDefaults(memberIds: familyMembers.map { $0.memberId }) saveDayOfBirths(dateOfBirths: familyMembers.map { $0.dayOfBirth ?? Date() }) - familyMembers.forEach { - saveMemberToUserDefaults(familyMember: $0) - } + familyMembers.forEach { saveMemberToUserDefaults(familyMember: $0) } } public static func load(memberId: String) -> ProfileData? { diff --git a/14th-team5-iOS/Data/Sources/View/Main/Repository/MainRepository.swift b/14th-team5-iOS/Data/Sources/View/Main/Repository/MainRepository.swift index 36c5fc8f6..28ad66c09 100644 --- a/14th-team5-iOS/Data/Sources/View/Main/Repository/MainRepository.swift +++ b/14th-team5-iOS/Data/Sources/View/Main/Repository/MainRepository.swift @@ -21,10 +21,15 @@ public final class MainRepository: MainRepositoryProtocol { extension MainRepository { public func fetchMain() -> Observable { - return mainApiWorker.fetchMain().asObservable() + return mainApiWorker.fetchMain() + .do(onSuccess: { FamilyUserDefaults.saveFamilyMembers($0?.mainFamilyProfileDatas ?? []) }) + .asObservable() + } public func fetchMainNight() -> Observable { - return mainApiWorker.fetchMainNight().asObservable() + return mainApiWorker.fetchMainNight() + .do(onSuccess: { FamilyUserDefaults.saveFamilyMembers($0?.mainFamilyProfileDatas ?? []) }) + .asObservable() } } From 7def0c967887b37d48b05b02ffa98de470b596a5 Mon Sep 17 00:00:00 2001 From: Do-hyun-Kim Date: Wed, 15 May 2024 01:48:15 +0900 Subject: [PATCH 046/263] =?UTF-8?q?fix:=20MissionTextView=20ContentTitle?= =?UTF-8?q?=EC=9D=B4=20=EC=97=86=EC=9D=84=20=EA=B2=BD=EC=9A=B0=20Hidden=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20or=20=EC=A4=91=EB=B3=B5=20=EC=9D=B4?= =?UTF-8?q?=EB=B2=A4=ED=8A=B8=20=EB=B0=9C=EC=83=9D=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?-=20CameraViewController=20RealEmoji=20PresigendURL=20=EC=A4=91?= =?UTF-8?q?=EB=B3=B5=20API=20=ED=98=B8=EC=B6=9C=20=EC=88=98=EC=A0=95=20-?= =?UTF-8?q?=20CameraViewController=20Flash=20On,=20Off=20Button=20UI=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EA=B8=B0=EB=8A=A5=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20-=20ProfileViewController=20setLoding=20Mutation=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0,=20BirthDayView=20Hidden=20Default=EB=A1=9C?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Camera/CameraViewController.swift | 43 ++----- .../Reactor/PostDetailViewReactor.swift | 3 +- .../ViewControllers/PostViewController.swift | 1 + .../Views/PostDetailCollectionViewCell.swift | 17 ++- .../ProfileFeedPageViewController.swift | 14 +++ .../Profile/ProfileViewController.swift | 3 +- .../Reactor/ProfileFeedViewReactor.swift | 13 +-- .../Profile/Reactor/ProfileViewReactor.swift | 18 +-- .../Extensions/AVCapturePhotoOutput+Ext.swift | 1 + .../Sources/ShareView/BibbiProfileView.swift | 2 +- .../icons/flash.imageset/flash.svg | 4 - .../icons/flashOff.imageset/Contents.json | 12 ++ .../icons/flashOff.imageset/flashOff.svg | 5 + .../Contents.json | 2 +- .../icons/flashOn.imageset/flashOn.svg | 4 + .../xcschemes/Bibbi-Workspace.xcscheme | 106 +++++++++--------- 16 files changed, 121 insertions(+), 127 deletions(-) delete mode 100644 14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/flash.imageset/flash.svg create mode 100644 14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/flashOff.imageset/Contents.json create mode 100644 14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/flashOff.imageset/flashOff.svg rename 14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/{flash.imageset => flashOn.imageset}/Contents.json (78%) create mode 100644 14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/flashOn.imageset/flashOn.svg diff --git a/14th-team5-iOS/App/Sources/Presentation/Camera/CameraViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Camera/CameraViewController.swift index ffe154566..db7c94a5c 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Camera/CameraViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Camera/CameraViewController.swift @@ -134,7 +134,7 @@ public final class CameraViewController: BaseViewController { } flashButton.do { - $0.setImage(DesignSystemAsset.flash.image, for: .normal) + $0.setImage(DesignSystemAsset.flashOff.image, for: .normal) $0.backgroundColor = .darkGray } @@ -219,17 +219,7 @@ public final class CameraViewController: BaseViewController { .distinctUntilChanged() .bind(to: cameraIndicatorView.rx.isHidden) .disposed(by: disposeBag) - - - NotificationCenter.default.rx.notification(.AVCapturePhotoOutputDidFinishProcessingPhotoNotification) - .compactMap { notification -> Data? in - guard let userInfo = notification.userInfo else { return nil } - return userInfo["photo"] as? Data - } - .distinctUntilChanged() - .map { Reactor.Action.didTapShutterButton($0) } - .bind(to: reactor.action) - .disposed(by: disposeBag) + NotificationCenter.default .rx.notification(.didTapBibbiToastTranstionButton) @@ -363,7 +353,7 @@ public final class CameraViewController: BaseViewController { realEmojiCollectionView .rx.itemSelected - .debug("Tap item Select") + .throttle(.milliseconds(300), scheduler: MainScheduler.instance) .map { Reactor.Action.didTapRealEmojiPad($0)} .bind(to: reactor.action) .disposed(by: disposeBag) @@ -379,9 +369,11 @@ public final class CameraViewController: BaseViewController { .rx.tap .throttle(.seconds(4), scheduler: MainScheduler.instance) .do { _ in Haptic.selection() } + .withLatestFrom(reactor.state.map { $0.isFlashMode }) .withUnretained(self) - .subscribe { owner, _ in + .bind { owner, isFlash in let settings = AVCapturePhotoSettings(format: [AVVideoCodecKey: AVVideoCodecType.jpeg]) + settings.flashMode = isFlash == true ? .on : .off owner.captureOutputStream.capturePhoto(with: settings, delegate: owner) }.disposed(by: disposeBag) @@ -414,9 +406,8 @@ public final class CameraViewController: BaseViewController { .disposed(by: disposeBag) reactor.pulse(\.$isFlashMode) - .skip(1) - .withUnretained(self) - .bind(onNext: { $0.0.setupFlashMode(isFlash: $0.1) }) + .map { $0 == true ? DesignSystemAsset.flashOn.image : DesignSystemAsset.flashOff.image } + .bind(to: flashButton.rx.image()) .disposed(by: disposeBag) } @@ -553,22 +544,6 @@ extension CameraViewController { } - private func setupFlashMode(isFlash: Bool) { - do { - try backCamera.lockForConfiguration() - try backCamera.setTorchModeOn(level: 1.0) - - if isFlash { - backCamera.torchMode = .on - } else { - backCamera.torchMode = .off - } - backCamera.unlockForConfiguration() - } catch { - print(error.localizedDescription) - } - } - private func setupImageScale(scale: CGFloat, camera: AVCaptureDevice) { do { @@ -651,7 +626,7 @@ extension CameraViewController: AVCapturePhotoCaptureDelegate { public func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) { guard let photoData = photo.fileDataRepresentation(), let imageData = UIImage(data: photoData)?.asPhoto else { return } - output.photoOutputDidFinshProcessing(photo: imageData, error: error) + reactor?.action.onNext(.didTapShutterButton(imageData)) } public func photoOutput(_ output: AVCapturePhotoOutput, willCapturePhotoFor resolvedSettings: AVCaptureResolvedPhotoSettings) { diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostDetailViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostDetailViewReactor.swift index d2f96ecc4..87721d6df 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostDetailViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostDetailViewReactor.swift @@ -30,7 +30,8 @@ final class PostDetailViewReactor: Reactor { struct State { let type: CellType let post: PostListData - @Pulse var missionContent: String? = nil + + var missionContent: String = "" var isShowingSelectableEmojiStackView: Bool = false var fetchedDisplayContent: [DisplayEditSectionModel] = [.displayKeyword([])] diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/PostViewController.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/PostViewController.swift index 4c7a592ea..7900c055d 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/PostViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/PostViewController.swift @@ -53,6 +53,7 @@ final class PostViewController: BaseViewController { reactor.state.map { $0.originPostLists } .map(Array.init(with:)) + .distinctUntilChanged() .bind(to: collectionView.rx.items(dataSource: createDataSource())) .disposed(by: disposeBag) diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/PostDetailCollectionViewCell.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/PostDetailCollectionViewCell.swift index 548525236..0222b2e90 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/PostDetailCollectionViewCell.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/PostDetailCollectionViewCell.swift @@ -172,21 +172,20 @@ extension PostDetailCollectionViewCell { .bind(to: reactor.action) .disposed(by: disposeBag) - reactor.state.compactMap { $0.missionContent } - .distinctUntilChanged() - .debug("Mission Content Title: ") - .bind(to: missionTextView.missionLabel.rx.text) - .disposed(by: disposeBag) } private func bindOutput(reactor: PostDetailViewReactor) { - reactor.state.map { $0.post.missionType == "survival" } - .debug("Post Detail Mission Type") + reactor.state.map { $0.missionContent } + .distinctUntilChanged() + .bind(to: missionTextView.missionLabel.rx.text) + .disposed(by: disposeBag) + + reactor.state.map { $0.missionContent.isEmpty } .distinctUntilChanged() - .bind(to: missionTextView.rx.isHidden) + .asDriver(onErrorDriveWith: .empty()) + .drive(missionTextView.rx.isHidden) .disposed(by: disposeBag) - reactor.state.map { $0.post } .distinctUntilChanged() diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileFeedPageViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileFeedPageViewController.swift index f8a695125..aabbcee3a 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileFeedPageViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileFeedPageViewController.swift @@ -40,6 +40,8 @@ final class ProfileFeedPageViewController: UIPageViewController { super.viewDidLoad() setupUI() + self.delegate = self + self.dataSource = self } private func setupUI() { @@ -62,3 +64,15 @@ final class ProfileFeedPageViewController: UIPageViewController { + +extension ProfileFeedPageViewController: UIPageViewControllerDelegate, UIPageViewControllerDataSource { + func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { + return nil + } + + func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { + return nil + } + + +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileViewController.swift index 42bc99cc0..c71fc8013 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileViewController.swift @@ -58,6 +58,7 @@ public final class ProfileViewController: BaseViewController addChild(profileFeedViewController) view.addSubviews(profileView, profileLineView, profileFeedViewController.view, profileSegementControl, profileIndicatorView) + profileFeedViewController.didMove(toParent: self) } public override func setupAttributes() { @@ -157,7 +158,7 @@ public final class ProfileViewController: BaseViewController reactor.state .map { $0.isLoading } .distinctUntilChanged() - .asDriver(onErrorJustReturn: false) + .asDriver(onErrorJustReturn: true) .drive(profileIndicatorView.rx.isHidden) .disposed(by: disposeBag) diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileFeedViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileFeedViewReactor.swift index 23ce97bfe..c499b792f 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileFeedViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileFeedViewReactor.swift @@ -24,7 +24,6 @@ final class ProfileFeedViewReactor: Reactor { } enum Mutation { - case setLoading(Bool) case setFeedSectionItems([ProfileFeedSectionItem]) case setFeedItemPage(Int) case setFeedPaginationItems([PostListData]) @@ -33,7 +32,6 @@ final class ProfileFeedViewReactor: Reactor { } struct State { - var isLoading: Bool var memberId: String @Pulse var selectedIndex: IndexPath? @Pulse var feedDetailItem: PostSection.Model @@ -47,7 +45,6 @@ final class ProfileFeedViewReactor: Reactor { init(feedUseCase: ProfileFeedUseCaseProtocol, type: PostType, memberId: String) { self.feedUseCase = feedUseCase self.initialState = State( - isLoading: false, memberId: memberId, selectedIndex: nil, feedDetailItem: .init(model: 0, items: []), @@ -98,11 +95,9 @@ final class ProfileFeedViewReactor: Reactor { } } return .concat( - .just(.setLoading(false)), .just(.setFeedPaginationItems(entity.postLists)), .just(.setFeedItems(entity)), - .just(.setFeedSectionItems(sectionItem)), - .just(.setLoading(true)) + .just(.setFeedSectionItems(sectionItem)) ) } case .fetchMoreFeedItems: @@ -134,11 +129,9 @@ final class ProfileFeedViewReactor: Reactor { } return .concat( - .just(.setLoading(false)), .just(.setFeedPaginationItems(feedItems)), .just(.setFeedItemPage(updatePage)), - .just(.setFeedSectionItems(sectionItem)), - .just(.setLoading(true)) + .just(.setFeedSectionItems(sectionItem)) ) } case let .didTapProfileFeedItem(indexPath, feedItems): @@ -176,8 +169,6 @@ final class ProfileFeedViewReactor: Reactor { var newState = state switch mutation { - case let .setLoading(isLoading): - newState.isLoading = isLoading case let .setFeedItems(feedItems): newState.feedItems = feedItems case let .setFeedSectionItems(section): diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift index cf84cea93..f24b53685 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift @@ -54,7 +54,7 @@ public final class ProfileViewReactor: Reactor { self.memberId = memberId self.isUser = isUser self.initialState = State( - isLoading: false, + isLoading: true, memberId: memberId, isUser: isUser, feedType: .survival @@ -67,17 +67,11 @@ public final class ProfileViewReactor: Reactor { //TODO: Keychain, UserDefaults 추가 switch action { case .viewDidLoad: - return .concat( - .just(.setLoading(false)), - profileUseCase.executeProfileMemberItems(memberId: currentState.memberId) - .asObservable() - .flatMap { entity -> Observable in - return .concat( - .just(.setProfileMemberItems(entity)), - .just(.setLoading(true)) - ) - } - ) + return profileUseCase.executeProfileMemberItems(memberId: currentState.memberId) + .asObservable() + .flatMap { entity -> Observable in + .just(.setProfileMemberItems(entity)) + } case let .updateNickNameProfile(nickNameFileData): let nickNameProfileImage: String = "\(nickNameFileData.hashValue).jpg" let nickNameImageEditParameter: CameraDisplayImageParameters = CameraDisplayImageParameters(imageName: nickNameProfileImage) diff --git a/14th-team5-iOS/Core/Sources/Extensions/AVCapturePhotoOutput+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/AVCapturePhotoOutput+Ext.swift index 59d95610f..77aea0601 100644 --- a/14th-team5-iOS/Core/Sources/Extensions/AVCapturePhotoOutput+Ext.swift +++ b/14th-team5-iOS/Core/Sources/Extensions/AVCapturePhotoOutput+Ext.swift @@ -10,6 +10,7 @@ import Foundation import AVFoundation +@available(*, deprecated, message: "중복 이벤트 문제로 ReactorKit을 사용해주세요") extension AVCapturePhotoOutput { public func photoOutputDidFinshProcessing(photo: Data, error: Error?) { let userInfo: [AnyHashable: Any] = ["photo": photo, "error": error as Any] diff --git a/14th-team5-iOS/Core/Sources/ShareView/BibbiProfileView.swift b/14th-team5-iOS/Core/Sources/ShareView/BibbiProfileView.swift index 52d78f10f..06cd76892 100644 --- a/14th-team5-iOS/Core/Sources/ShareView/BibbiProfileView.swift +++ b/14th-team5-iOS/Core/Sources/ShareView/BibbiProfileView.swift @@ -77,7 +77,7 @@ public class BibbiProfileView: UIView { } birthDayView.do { - $0.isHidden = false + $0.isHidden = true $0.contentMode = .scaleAspectFill $0.image = DesignSystemAsset.birthday.image } diff --git a/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/flash.imageset/flash.svg b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/flash.imageset/flash.svg deleted file mode 100644 index 527abddd6..000000000 --- a/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/flash.imageset/flash.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/flashOff.imageset/Contents.json b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/flashOff.imageset/Contents.json new file mode 100644 index 000000000..83d69d8ee --- /dev/null +++ b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/flashOff.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "flashOff.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/flashOff.imageset/flashOff.svg b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/flashOff.imageset/flashOff.svg new file mode 100644 index 000000000..8f1006263 --- /dev/null +++ b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/flashOff.imageset/flashOff.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/flash.imageset/Contents.json b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/flashOn.imageset/Contents.json similarity index 78% rename from 14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/flash.imageset/Contents.json rename to 14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/flashOn.imageset/Contents.json index 5a7ace33a..27530c490 100644 --- a/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/flash.imageset/Contents.json +++ b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/flashOn.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "flash.svg", + "filename" : "flashOn.svg", "idiom" : "universal" } ], diff --git a/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/flashOn.imageset/flashOn.svg b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/flashOn.imageset/flashOn.svg new file mode 100644 index 000000000..1e68b25b7 --- /dev/null +++ b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/flashOn.imageset/flashOn.svg @@ -0,0 +1,4 @@ + + + + diff --git a/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme b/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme index 9d382a7b9..73b9aa620 100644 --- a/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme +++ b/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme @@ -280,52 +280,10 @@ buildForAnalyzing = "YES"> - - - - - - - - - - - - + BlueprintIdentifier = "4160F70E94342FB432F5ABF1" + BuildableName = "GoogleAppMeasurementTarget.framework" + BlueprintName = "GoogleAppMeasurementTarget" + ReferencedContainer = "container:Tuist/Dependencies/SwiftPackageManager/.build/checkouts/GoogleAppMeasurement/GoogleAppMeasurement.xcodeproj"> + BlueprintIdentifier = "08D12528663D0C8DD1A99BCB" + BuildableName = "GoogleDataTransport.framework" + BlueprintName = "GoogleDataTransport" + ReferencedContainer = "container:Tuist/Dependencies/SwiftPackageManager/.build/checkouts/GoogleDataTransport/GoogleDataTransport.xcodeproj"> @@ -468,6 +426,34 @@ ReferencedContainer = "container:Tuist/Dependencies/SwiftPackageManager/.build/checkouts/GoogleUtilities/GoogleUtilities.xcodeproj"> + + + + + + + + + + + + Date: Mon, 13 May 2024 19:32:51 +0900 Subject: [PATCH 047/263] [feat]: segment swipe(#526) --- .../AccountSignIn/AccountSignInStrings.swift | 2 +- .../Presentation/Home/Model/Balloon.swift | 2 +- .../Home/Reactor/MainViewReactor.swift | 3 +- .../ViewControllers/MainViewController.swift | 27 +++++---- .../SegmentPageViewController.swift | 60 ++++++++++++++----- .../Extensions/UIViewController+Ext.swift | 2 +- 6 files changed, 64 insertions(+), 32 deletions(-) diff --git a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignIn/AccountSignInStrings.swift b/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignIn/AccountSignInStrings.swift index acbd914b8..48e42f007 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignIn/AccountSignInStrings.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignIn/AccountSignInStrings.swift @@ -13,5 +13,5 @@ extension String { } extension AccountSignInStrings { - static let mainTitle: String = "하루 한번, 가족에게 보내는 생존 신고" + static let mainTitle: String = "하루 한 번, 가족에게 보내는 생존 신고" } diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Model/Balloon.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Model/Balloon.swift index 363890728..3ebdcc609 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Model/Balloon.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Model/Balloon.swift @@ -38,7 +38,7 @@ enum BalloonText { var message: String { switch self { case .survivalStandard: - return "하루에 한번 사진을 올릴 수 있어요" + return "하루에 한 번 사진을 올릴 수 있어요" case .survivalDone: return "오늘의 생존신고는 완료되었어요" case .missionLocked: diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift index 152ce5aa3..53b22cbb5 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift @@ -240,7 +240,8 @@ extension MainViewReactor { if currentState.pageIndex == 0 { newState.cameraEnabled = !currentState.isMeSurvivalUploadedToday } else { - if !currentState.isMeMissionUploadedToday && currentState.isMissionUnlocked { + + if currentState.isMeSurvivalUploadedToday && (!currentState.isMeMissionUploadedToday) && currentState.isMissionUnlocked { newState.cameraEnabled = true } else { newState.cameraEnabled = false 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 a305aac6c..f8c8d2adb 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift @@ -130,16 +130,20 @@ extension MainViewController { .bind(to: reactor.action) .disposed(by: disposeBag) - segmentControl.survivalButton.rx.tap - .throttle(.milliseconds(1000), scheduler: MainScheduler.instance) - .map { Reactor.Action.didTapSegmentControl(.survival) } - .bind(to: reactor.action) - .disposed(by: disposeBag) + Observable.merge( + segmentControl.survivalButton.rx.tap.map { Reactor.Action.didTapSegmentControl(.survival) }, + segmentControl.missionButton.rx.tap.map { Reactor.Action.didTapSegmentControl(.mission) }, + pageViewController.indexRelay.filter { $0.way == .scroll }.map { $0.index }.map { Reactor.Action.didTapSegmentControl($0 == 0 ? .survival : .mission)} + ) + .observe(on: MainScheduler.instance) + .throttle(.milliseconds(300), scheduler: MainScheduler.instance) + .bind(to: reactor.action) + .disposed(by: disposeBag) - segmentControl.missionButton.rx.tap - .throttle(.milliseconds(1000), scheduler: MainScheduler.instance) - .map { Reactor.Action.didTapSegmentControl(.mission) } - .bind(to: reactor.action) + pageViewController.segmentEnabled + .distinctUntilChanged() + .observe(on: MainScheduler.instance) + .bind(to: segmentControl.rx.isUserInteractionEnabled) .disposed(by: disposeBag) navigationBarView.rx.leftButtonTap @@ -154,8 +158,7 @@ extension MainViewController { .bind { $0.0.navigationController?.pushViewController(MonthlyCalendarDIConatainer().makeViewController(), animated: true) } .disposed(by: disposeBag) - alertConfirmRelay - .map { Reactor.Action.pickConfirmButtonTapped($0.0, $0.1) } + alertConfirmRelay.map { Reactor.Action.pickConfirmButtonTapped($0.0, $0.1) } .bind(to: reactor.action) .disposed(by: disposeBag) @@ -214,7 +217,7 @@ extension MainViewController { .withUnretained(self) .observe(on: MainScheduler.instance) .bind(onNext: { - $0.0.pageViewController.indexRelay.accept($0.1) + $0.0.pageViewController.indexRelay.accept(.init(way: .segmentTap, index: $0.1)) $0.0.segmentControl.isSelected = ($0.1 == 0) }) .disposed(by: disposeBag) diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/SegmentPageViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/SegmentPageViewController.swift index 70f0327ed..33c915f01 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/SegmentPageViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/SegmentPageViewController.swift @@ -12,6 +12,16 @@ import Domain import RxSwift import RxCocoa +enum PageChangeWay { + case scroll + case segmentTap +} + +struct PageRelay { + let way: PageChangeWay + let index: Int +} + final class SegmentPageViewController: UIPageViewController { private let survivalViewController: MainPostViewController = MainPostViewDIContainer().makeViewController(type: .survival) private let missionViewController: MainPostViewController = MainPostViewDIContainer().makeViewController(type: .mission) @@ -19,7 +29,8 @@ final class SegmentPageViewController: UIPageViewController { private lazy var pages: [UIViewController] = [survivalViewController, missionViewController] - let indexRelay: BehaviorRelay = BehaviorRelay(value: 0) + let indexRelay: BehaviorRelay = BehaviorRelay(value: .init(way: .segmentTap, index: 0)) + let segmentEnabled: BehaviorRelay = BehaviorRelay(value: true) override func viewDidLoad() { super.viewDidLoad() @@ -32,14 +43,19 @@ final class SegmentPageViewController: UIPageViewController { } private func bind() { - indexRelay + indexRelay.filter { $0.way == .segmentTap }.map { $0.index } + .distinctUntilChanged() .withUnretained(self) + .observe(on: MainScheduler.instance) .bind(onNext: { + $0.0.isPagingEnabled = false switch $0.1 { - case 0: - $0.0.setViewControllers([$0.0.survivalViewController], direction: .reverse, animated: true) - case 1: - $0.0.setViewControllers([$0.0.missionViewController], direction: .forward, animated: true) + case 0: $0.0.setViewControllers([$0.0.survivalViewController], direction: .reverse, animated: true) { [weak self] _ in + self?.isPagingEnabled = true + } + case 1: $0.0.setViewControllers([$0.0.missionViewController], direction: .forward, animated: true) { [weak self] _ in + self?.isPagingEnabled = true + } default: fatalError("INDEX OUT OF RANGE") } @@ -50,20 +66,32 @@ final class SegmentPageViewController: UIPageViewController { } extension SegmentPageViewController: UIPageViewControllerDelegate, UIPageViewControllerDataSource { + func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) { + segmentEnabled.accept(false) + } + func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { - return nil + guard let index = pages.firstIndex(of: viewController), + index - 1 >= 0 else { return nil } + + return pages[index - 1] } func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { - return nil + guard let index = pages.firstIndex(of: viewController), + index + 1 != pages.count else { return nil } + + return pages[index + 1] } -// func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) { -// if completed { -// if let currentViewController = pageViewController.viewControllers?.first, -// let currentIndex = pages.firstIndex(of: currentViewController) { -// print("현재 페이지: \(currentIndex)") -// } -// } -// } + func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) { + if let currentViewController = pageViewController.viewControllers?.first, + let currentIndex = pages.firstIndex(of: currentViewController) { + indexRelay.accept(.init(way: .scroll, index: currentIndex)) + } + + if completed && finished { + segmentEnabled.accept(true) + } + } } diff --git a/14th-team5-iOS/Core/Sources/Extensions/UIViewController+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/UIViewController+Ext.swift index a063c4c89..c96f3a850 100644 --- a/14th-team5-iOS/Core/Sources/Extensions/UIViewController+Ext.swift +++ b/14th-team5-iOS/Core/Sources/Extensions/UIViewController+Ext.swift @@ -165,7 +165,7 @@ extension UIViewController { public func makeInvitationUrlSharePanel(_ url: URL?, provider globalState: GlobalStateProviderProtocol? = nil) { guard let url = url else { return } let itemSource = UrlActivityItemSource( - title: "삐삐! 가족에게 보내는 하루 한번 생존 신고", + title: "삐삐! 가족에게 보내는 하루 한 번 생존 신고", url: url ) let copyToPastboard = CopyInvitationUrlActivity(url, provider: globalState) From da5e1b7e95b913fdc2d1f74c13aac2662dc30472 Mon Sep 17 00:00:00 2001 From: Kyoungmi <1mmgm.dev@gmail.com> Date: Mon, 13 May 2024 23:19:34 +0900 Subject: [PATCH 048/263] [feat]: alertView(#526) --- .../Home/Reactor/MainViewReactor.swift | 198 ++++++++++++------ .../ViewControllers/MainViewController.swift | 90 ++++---- .../Assets.xcassets/Common/Contents.json | 6 - .../icons/mission 1.imageset/Contents.json | 12 -- .../icons/mission 1.imageset/mission.svg | 19 -- .../shutter.imageset/Contents.json | 0 .../shutter.imageset/shutter.svg | 0 7 files changed, 178 insertions(+), 147 deletions(-) delete mode 100644 14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/Common/Contents.json delete mode 100644 14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/mission 1.imageset/Contents.json delete mode 100644 14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/mission 1.imageset/mission.svg rename 14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/{Common => icons}/shutter.imageset/Contents.json (100%) rename 14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/{Common => icons}/shutter.imageset/shutter.svg (100%) diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift index 53b22cbb5..e98f5840a 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift @@ -16,12 +16,29 @@ import RxDataSources import Kingfisher final class MainViewReactor: Reactor { + enum TapAction { + case cameraButtonTap + case navigationRightButtonTap + case navigationLeftButtonTap + case contributorNextButtonTap + } + + enum OpenType { + case cameraViewController(UploadLocation) + case survivalAlert + case pickAlert(String, String) + case weeklycalendarViewController(String) + case familyManagementViewController + case monthlyCalendarViewController + } + enum Action { case calculateTime case setTimer(Bool, Int) case fetchMainUseCase case fetchMainNightUseCase + case openNextViewController(TapAction) case didTapSegmentControl(PostType) case pickConfirmButtonTapped(String, String) } @@ -40,7 +57,7 @@ final class MainViewReactor: Reactor { case setCopySuccessToastMessage case setFailureToastMessage - case setPickAlertView(String, String) + case showNextView(OpenType) } struct State { @@ -58,13 +75,13 @@ final class MainViewReactor: Reactor { var isMeMissionUploadedToday: Bool = false var isMissionUnlocked: Bool = false + @Pulse var openNextView: OpenType? = nil @Pulse var cameraEnabled: Bool = false @Pulse var pickers: [Picker] = [] @Pulse var contributor: FamilyRankData = FamilyRankData.empty @Pulse var familySection: [FamilySection.Item] = [] - @Pulse var shouldPresentPickAlert: (String, String)? @Pulse var shouldPresentPickSuccessToastMessage: String? @Pulse var shouldPresentCopySuccessToastMessage: Bool = false @Pulse var shouldPresentFailureToastMessage: Bool = false @@ -94,7 +111,7 @@ extension MainViewReactor { .flatMap { event in switch event { case let .presentPickAlert(name, id): - return Observable.just(.setPickAlertView(name, id)) + return Observable.just(.showNextView(.pickAlert(name, id))) case .refreshMain: return self.mutate(action: .calculateTime) default: @@ -160,7 +177,7 @@ extension MainViewReactor { } } case .calculateTime: - let (isInTime, time) = MainViewReactor.calculateRemainingTime() + let (isInTime, time) = self.calculateRemainingTime() if isInTime { return Observable.concat([ @@ -197,6 +214,25 @@ extension MainViewReactor { self.mutate(action: .calculateTime) ) } + case .openNextViewController(let type): + switch type { + case .cameraButtonTap: + if currentState.pageIndex == 0 { + return Observable.just(.showNextView(.cameraViewController(.survival))) + } else { + if currentState.isMeSurvivalUploadedToday { + return Observable.just(.showNextView(.cameraViewController(.mission))) + } else { + return Observable.just(.showNextView(.survivalAlert)) + } + } + case .navigationRightButtonTap: + return Observable.just(.showNextView(.monthlyCalendarViewController)) + case .navigationLeftButtonTap: + return Observable.just(.showNextView(.familyManagementViewController)) + case .contributorNextButtonTap: + return Observable.just(.showNextView(.weeklycalendarViewController(currentState.contributor.recentPostDate))) + } } } @@ -215,90 +251,120 @@ extension MainViewReactor { case .setPageIndex(let index): newState.pageIndex = index case .updateMainData(let data): - newState.isMissionUnlocked = data.isMissionUnlocked - newState.isMeSurvivalUploadedToday = data.isMeSurvivalUploadedToday - newState.isMeMissionUploadedToday = data.isMeMissionUploadedToday - newState.isFamilyMissionUploadedToday = data.isFamilyMissionUploadedToday - newState.isFamilySurvivalUploadedToday = data.isFamilySurvivalUploadedToday - newState.leftCount = data.leftUploadCountUntilMissionUnlock - newState.missionText = data.dailyMissionContent - newState.pickers = data.pickers - newState.familySection = FamilySection.Model( - model: 0, - items: data.mainFamilyProfileDatas.map { - .main(MainFamilyCellReactor($0, service: provider)) - } - ).items + newState = updateMainData(newState, data) case .updateMainNight(let data): newState.familySection = FamilySection.Model(model: 0, items: data.mainFamilyProfileDatas.map { .main(MainFamilyCellReactor($0, service: provider)) }).items newState.contributor = data.familyRankData - case let .setPickAlertView(name, id): - newState.shouldPresentPickAlert = (name, id) case .setCamerEnabled: - if currentState.pageIndex == 0 { - newState.cameraEnabled = !currentState.isMeSurvivalUploadedToday - } else { + newState = setCameraEnabled(newState) + case .setBalloonText: + newState = setBalloonText(newState) + case .setDescriptionText: + newState = setDescriptionText(newState) + case .showNextView(let type): + newState.openNextView = type + } + + return newState + } +} - if currentState.isMeSurvivalUploadedToday && (!currentState.isMeMissionUploadedToday) && currentState.isMissionUnlocked { - newState.cameraEnabled = true - } else { - newState.cameraEnabled = false - } +extension MainViewReactor { + private func updateMainData(_ state: State, _ data: MainData) -> State { + var newState = state + newState.isMissionUnlocked = data.isMissionUnlocked + newState.isMeSurvivalUploadedToday = data.isMeSurvivalUploadedToday + newState.isMeMissionUploadedToday = data.isMeMissionUploadedToday + newState.isFamilyMissionUploadedToday = data.isFamilyMissionUploadedToday + newState.isFamilySurvivalUploadedToday = data.isFamilySurvivalUploadedToday + newState.leftCount = data.leftUploadCountUntilMissionUnlock + newState.missionText = data.dailyMissionContent + newState.pickers = data.pickers + newState.familySection = FamilySection.Model( + model: 0, + items: data.mainFamilyProfileDatas.map { + .main(MainFamilyCellReactor($0, service: provider)) } - case .setBalloonText: - if currentState.pageIndex == 0 { - if currentState.isMeSurvivalUploadedToday { - newState.balloonText = .survivalDone - } else if !currentState.pickers.isEmpty { - if currentState.pickers.count <= 1 { - newState.balloonText = .picker(currentState.pickers[0]) - } else { - newState.balloonText = .pickers(currentState.pickers) - } - } else { - newState.balloonText = .survivalStandard - } + ).items + + return newState + } + + private func setCameraEnabled(_ state: State) -> State { + var newState = state + + if currentState.pageIndex == 0 { + newState.cameraEnabled = !currentState.isMeSurvivalUploadedToday + } else { + if (!currentState.isMeMissionUploadedToday) && currentState.isMissionUnlocked { + newState.cameraEnabled = true } else { - if !currentState.isMissionUnlocked { - newState.balloonText = .missionLocked + newState.cameraEnabled = false + } + } + + return newState + } + + private func setBalloonText(_ state: State) -> State { + var newState = state + + if currentState.pageIndex == 0 { + if currentState.isMeSurvivalUploadedToday { + newState.balloonText = .survivalDone + } else if !currentState.pickers.isEmpty { + if currentState.pickers.count <= 1 { + newState.balloonText = .picker(currentState.pickers[0]) } else { - if !currentState.isMeSurvivalUploadedToday { - newState.balloonText = .cantMission - } else if currentState.isMeMissionUploadedToday { - newState.balloonText = .missionDone - } else { - newState.balloonText = .canMission - } + newState.balloonText = .pickers(currentState.pickers) } + } else { + newState.balloonText = .survivalStandard } - case .setDescriptionText: - if currentState.pageIndex == 0 { - if currentState.isFamilySurvivalUploadedToday { - newState.description = .survivalFull + } else { + if !currentState.isMissionUnlocked { + newState.balloonText = .missionLocked + } else { + if !currentState.isMeSurvivalUploadedToday { + newState.balloonText = .cantMission + } else if currentState.isMeMissionUploadedToday { + newState.balloonText = .missionDone } else { - newState.description = .survivalNone + newState.balloonText = .canMission } + } + } + + return newState + } + + private func setDescriptionText(_ state: State) -> State { + var newState = state + + if currentState.pageIndex == 0 { + if currentState.isFamilySurvivalUploadedToday { + newState.description = .survivalFull + } else { + newState.description = .survivalNone + } + } else { + if !currentState.isMissionUnlocked { + newState.description = .missionNone(currentState.leftCount) } else { - if !currentState.isMissionUnlocked { - newState.description = .missionNone(currentState.leftCount) + if currentState.isFamilyMissionUploadedToday { + newState.description = .missionFull } else { - if currentState.isFamilyMissionUploadedToday { - newState.description = .missionFull - } else { - newState.description = .mission(currentState.missionText) - } + newState.description = .mission(currentState.missionText) } } } return newState } -} - -extension MainViewReactor { - private static func calculateRemainingTime() -> (Bool, Int) { + + private func calculateRemainingTime() -> (Bool, Int) { let calendar = Calendar.current let currentTime = Date() 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 f8c8d2adb..d65ad9676 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift @@ -27,7 +27,7 @@ final class MainViewController: BaseViewController, UICollectio private let pageViewController: SegmentPageViewController = SegmentPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal) private let cameraButton: MainCameraButtonView = MainCameraDIContainer().makeView() - private let alertConfirmRelay = PublishRelay<(String, String)>() + private let alertConfirmRelay: PublishRelay<(String, String)> = PublishRelay<(String, String)>() override func viewDidLoad() { super.viewDidLoad() @@ -140,39 +140,27 @@ extension MainViewController { .bind(to: reactor.action) .disposed(by: disposeBag) + Observable.merge( + contributorView.nextButtonTapEvent.map { Reactor.Action.openNextViewController(.contributorNextButtonTap)}, + cameraButton.camerTapEvent.map { Reactor.Action.openNextViewController(.cameraButtonTap )}, + navigationBarView.rx.rightButtonTap.map { Reactor.Action.openNextViewController(.navigationRightButtonTap)}, + navigationBarView.rx.leftButtonTap.map { _ in Reactor.Action.openNextViewController(.navigationLeftButtonTap)} + ) + .observe(on: MainScheduler.instance) + .throttle(RxConst.milliseconds300Interval, scheduler: MainScheduler.instance) + .bind(to: reactor.action) + .disposed(by: disposeBag) + pageViewController.segmentEnabled .distinctUntilChanged() .observe(on: MainScheduler.instance) .bind(to: segmentControl.rx.isUserInteractionEnabled) .disposed(by: disposeBag) - navigationBarView.rx.leftButtonTap - .throttle(RxConst.milliseconds300Interval, scheduler: MainScheduler.instance) - .withUnretained(self) - .bind { $0.0.navigationController?.pushViewController( FamilyManagementDIContainer().makeViewController(), animated: true) } - .disposed(by: disposeBag) - - navigationBarView.rx.rightButtonTap - .throttle(RxConst.milliseconds300Interval, scheduler: MainScheduler.instance) - .withUnretained(self) - .bind { $0.0.navigationController?.pushViewController(MonthlyCalendarDIConatainer().makeViewController(), animated: true) } - .disposed(by: disposeBag) - alertConfirmRelay.map { Reactor.Action.pickConfirmButtonTapped($0.0, $0.1) } .bind(to: reactor.action) .disposed(by: disposeBag) - cameraButton.camerTapEvent - .throttle(RxConst.milliseconds300Interval, scheduler: MainScheduler.instance) - .withUnretained(self) - .bind(onNext: { - MPEvent.Home.cameraTapped.track(with: nil) - let cameraViewController = CameraDIContainer( - cameraType: reactor.currentState.pageIndex == 0 ? .survival : .mission).makeViewController() - $0.0.navigationController?.pushViewController(cameraViewController, animated: true) - }) - .disposed(by: disposeBag) - contributorView.infoButton.rx.tap .throttle(RxConst.milliseconds300Interval, scheduler: MainScheduler.instance) .withUnretained(self) @@ -186,16 +174,6 @@ extension MainViewController { ) }) .disposed(by: disposeBag) - - contributorView.nextButtonTapEvent - .throttle(RxConst.milliseconds300Interval, scheduler: MainScheduler.instance) - .observe(on: MainScheduler.instance) - .bind(onNext: { [weak self] _ in - guard let self else { return } - let calendarViewController = WeeklyCalendarDIConatainer(date: reactor.currentState.contributor.recentPostDate.toDate()).makeViewController() - self.navigationController?.pushViewController(calendarViewController, animated: true) - }) - .disposed(by: disposeBag) } private func bindOutput(reactor: MainViewReactor) { @@ -250,16 +228,10 @@ extension MainViewController { .bind(to: contributorView.contributorRelay) .disposed(by: disposeBag) - reactor.pulse(\.$shouldPresentPickAlert) - .compactMap { $0 } - .bind(with: self) { owner, profile in - BibbiAlertBuilder(owner) - .alertStyle(.pickMember(profile.0)) - .setConfirmAction { - owner.alertConfirmRelay.accept((profile.0, profile.1)) - } - .present() - } + reactor.pulse(\.$openNextView).compactMap { $0 } + .withUnretained(self) + .observe(on: MainScheduler.instance) + .bind(onNext: { $0.0.pushViewController(type: $0.1) }) .disposed(by: disposeBag) reactor.pulse(\.$shouldPresentPickSuccessToastMessage) @@ -313,6 +285,36 @@ extension MainViewController { } imageView.image = description.image } + + private func pushViewController(type: MainViewReactor.OpenType) { + switch type { + case .monthlyCalendarViewController: + navigationController?.pushViewController(MonthlyCalendarDIConatainer().makeViewController(), animated: true) + case .familyManagementViewController: + navigationController?.pushViewController(FamilyManagementDIContainer().makeViewController(), animated: true) + case .weeklycalendarViewController(let date): + navigationController?.pushViewController(WeeklyCalendarDIConatainer(date: date.toDate()).makeViewController(), animated: true) + case .cameraViewController(let type): + MPEvent.Home.cameraTapped.track(with: nil) + navigationController?.pushViewController(CameraDIContainer(cameraType: type).makeViewController(), animated: true) + case .survivalAlert: + BibbiAlertBuilder(self) + .alertStyle(.takeSurvival) + .setConfirmAction { [weak self] in + guard let self else { return } + self.navigationController?.pushViewController(CameraDIContainer(cameraType: .survival).makeViewController(), animated: true) + } + .present() + case .pickAlert(let name, let id): + BibbiAlertBuilder(self) + .alertStyle(.pickMember(name)) + .setConfirmAction { [weak self] in + guard let self else { return } + self.alertConfirmRelay.accept((name, id)) } + .present() + } + + } } extension MainViewController: UIPopoverPresentationControllerDelegate { diff --git a/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/Common/Contents.json b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/Common/Contents.json deleted file mode 100644 index 73c00596a..000000000 --- a/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/Common/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/mission 1.imageset/Contents.json b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/mission 1.imageset/Contents.json deleted file mode 100644 index c1d64cc3d..000000000 --- a/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/mission 1.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "mission.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/mission 1.imageset/mission.svg b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/mission 1.imageset/mission.svg deleted file mode 100644 index 9f515b9db..000000000 --- a/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/mission 1.imageset/mission.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/Common/shutter.imageset/Contents.json b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/shutter.imageset/Contents.json similarity index 100% rename from 14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/Common/shutter.imageset/Contents.json rename to 14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/shutter.imageset/Contents.json diff --git a/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/Common/shutter.imageset/shutter.svg b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/shutter.imageset/shutter.svg similarity index 100% rename from 14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/Common/shutter.imageset/shutter.svg rename to 14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/shutter.imageset/shutter.svg From c67164984f852d67354b05c19a71283b52107097 Mon Sep 17 00:00:00 2001 From: Kyoungmi <1mmgm.dev@gmail.com> Date: Mon, 13 May 2024 23:23:51 +0900 Subject: [PATCH 049/263] [feat]: layer color(#526) --- .../Presentation/Home/Views/MainFamilyCollectionViewCell.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Views/MainFamilyCollectionViewCell.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Views/MainFamilyCollectionViewCell.swift index 7b0b7ad07..d7d54c2ad 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Views/MainFamilyCollectionViewCell.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Views/MainFamilyCollectionViewCell.swift @@ -39,7 +39,7 @@ final class MainFamilyCollectionViewCell: BaseCollectionViewCell Date: Tue, 14 May 2024 00:08:39 +0900 Subject: [PATCH 050/263] [feat]: unknown error(#526) --- .../App/Sources/Manager/DeepLinkManager.swift | 2 -- .../AccountNicknameViewController.swift | 1 - .../AccountProfileViewController.swift | 1 - .../Camera/CameraViewController.swift | 1 - .../Content/WebContentViewController.swift | 1 - .../Home/Reactor/MainPostViewReactor.swift | 2 +- .../MainFamilyViewController.swift | 2 -- .../MainPostViewController.swift | 7 +------ .../ViewControllers/MainViewController.swift | 5 +++-- .../Profile/ProfileViewController.swift | 2 +- .../Presentation/Splash/SplashReactor.swift | 5 +++++ .../BibbiSegmentedControl.swift | 20 ++++++++++++++----- .../Family/Repository/FamilyRepository.swift | 8 +++++--- .../View/Main/DTO/MainResponseDTO.swift | 12 +++++------ .../View/Main/Repository/MainRepository.swift | 2 -- 15 files changed, 37 insertions(+), 34 deletions(-) diff --git a/14th-team5-iOS/App/Sources/Manager/DeepLinkManager.swift b/14th-team5-iOS/App/Sources/Manager/DeepLinkManager.swift index eb9d0d952..e99804f56 100644 --- a/14th-team5-iOS/App/Sources/Manager/DeepLinkManager.swift +++ b/14th-team5-iOS/App/Sources/Manager/DeepLinkManager.swift @@ -136,7 +136,6 @@ extension DeepLinkManager { type = .SomedayComment } } - print(data) switch type { case .TodaySurvival: @@ -154,7 +153,6 @@ extension DeepLinkManager { func decodeRemoteNotificationDeepLink(_ userInfo: [AnyHashable: Any]) { if let link = userInfo[AnyHashable("iosDeepLink")] as? String { - print(link) let components = link.components(separatedBy: "?") let parameters = components.last?.components(separatedBy: "&") diff --git a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountNicknameViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountNicknameViewController.swift index f85535a9d..0cf3e4022 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountNicknameViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountNicknameViewController.swift @@ -86,7 +86,6 @@ public final class AccountNicknameViewController: BaseViewController { realEmojiCollectionView .rx.itemSelected - .debug("Tap item Select") .map { Reactor.Action.didTapRealEmojiPad($0)} .bind(to: reactor.action) .disposed(by: disposeBag) diff --git a/14th-team5-iOS/App/Sources/Presentation/Content/WebContentViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Content/WebContentViewController.swift index 415da7416..3b3eb7766 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Content/WebContentViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Content/WebContentViewController.swift @@ -73,7 +73,6 @@ public class WebContentViewController: BaseViewController reactor.state .compactMap { $0.url?.lastPathComponent == "privacy" ? "개인정보처리방침" : "이용 약관" } - .debug("navigationTitle") .distinctUntilChanged() .withUnretained(self) .bind(onNext: { $0.0.navigationBarView.setNavigationTitle(title: $0.1) }) diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainPostViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainPostViewReactor.swift index f0036c6df..13150db4d 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainPostViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainPostViewReactor.swift @@ -62,7 +62,7 @@ extension MainPostViewReactor { !postList.postLists.isEmpty else { return Observable.from([ Mutation.setNoPostTodayView(true), - Mutation.updateRefreshEnd(true) + Mutation.updateRefreshEnd(true), ]) } diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainFamilyViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainFamilyViewController.swift index 8ef04bbfb..4c9ec75e9 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainFamilyViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainFamilyViewController.swift @@ -111,7 +111,6 @@ extension MainFamilyViewController { .observe(on: MainScheduler.instance) .map { !$0 } .distinctUntilChanged() - .debug("가족 없을 때 ") .bind(to: inviteFamilyView.rx.isHidden) .disposed(by: disposeBag) @@ -119,7 +118,6 @@ extension MainFamilyViewController { .observe(on: MainScheduler.instance) .map { $0 } .distinctUntilChanged() - .debug("가족 있을 때") .bind(to: familyCollectionView.rx.isHidden) .disposed(by: disposeBag) diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainPostViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainPostViewController.swift index 1ff96679d..e76b1e63e 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainPostViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainPostViewController.swift @@ -111,12 +111,7 @@ extension MainPostViewController { reactor.pulse(\.$isRefreshEnd) .withUnretained(self) .observe(on: MainScheduler.instance) - .withUnretained(self) - .bind(onNext: { owner, _ in - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - owner.refreshControl.endRefreshing() - } - }) + .bind(onNext: { $0.0.refreshControl.endRefreshing() }) .disposed(by: disposeBag) reactor.state.map { $0.isShowingNoPostTodayView } 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 d65ad9676..5ce38af71 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift @@ -23,7 +23,7 @@ final class MainViewController: BaseViewController, UICollectio private let imageView: UIImageView = UIImageView() private let contributorView: ContributorView = ContributorView(reactor: ContributorReactor()) - private let segmentControl: BibbiSegmentedControl = BibbiSegmentedControl(isUpdated: false) + private let segmentControl: BibbiSegmentedControl = BibbiSegmentedControl() private let pageViewController: SegmentPageViewController = SegmentPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal) private let cameraButton: MainCameraButtonView = MainCameraDIContainer().makeView() @@ -218,10 +218,11 @@ extension MainViewController { .disposed(by: disposeBag) reactor.state.map { $0.isMissionUnlocked } + .debug("냐밍") .distinctUntilChanged() .withUnretained(self) .observe(on: MainScheduler.instance) - .bind(onNext: { $0.0.segmentControl.isUpdated = $0.1 }) + .bind(onNext: { $0.0.segmentControl.isUpdatedRelay }) .disposed(by: disposeBag) reactor.pulse(\.$contributor) diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileViewController.swift index 42bc99cc0..9367617a8 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileViewController.swift @@ -23,7 +23,7 @@ public final class ProfileViewController: BaseViewController //MARK: Views - private lazy var profileSegementControl: BibbiSegmentedControl = BibbiSegmentedControl(isUpdated: true) + private lazy var profileSegementControl: BibbiSegmentedControl = BibbiSegmentedControl() private var pickerConfiguration: PHPickerConfiguration = { var configuration: PHPickerConfiguration = PHPickerConfiguration() configuration.filter = .images diff --git a/14th-team5-iOS/App/Sources/Presentation/Splash/SplashReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Splash/SplashReactor.swift index 6732c0a84..cdf676ad1 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Splash/SplashReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Splash/SplashReactor.swift @@ -16,6 +16,7 @@ public final class SplashViewReactor: Reactor { // MARK: - Action public enum Action { case viewDidLoad + case fetchFamily } // MARK: - Mutation @@ -81,6 +82,10 @@ public final class SplashViewReactor: Reactor { } ]) } + case .fetchFamily: + return familyUseCase.executeFetchPaginationFamilyMembers(query: .init()) + .asObservable() + .flatMap { _ in return Observable.empty() } } } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BibbiSegmentedControl/BibbiSegmentedControl.swift b/14th-team5-iOS/Core/Sources/Bibbi/BibbiSegmentedControl/BibbiSegmentedControl.swift index 5f0af56f4..d10777004 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BibbiSegmentedControl/BibbiSegmentedControl.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BibbiSegmentedControl/BibbiSegmentedControl.swift @@ -8,6 +8,9 @@ import UIKit import DesignSystem + +import RxCocoa +import RxSwift import SnapKit import Then @@ -21,7 +24,7 @@ public enum BibbiSegmentedType: String { public final class BibbiSegmentedControl: UIView { //MARK: Property - public var isUpdated: Bool + public var isUpdatedRelay: BehaviorSubject = BehaviorSubject(value: true) public let survivalButton: UIButton = UIButton() public let missionButton: UIButton = UIButton() public var isSelected: Bool = true { @@ -30,13 +33,20 @@ public final class BibbiSegmentedControl: UIView { } } - public init(isUpdated: Bool) { - self.isUpdated = isUpdated - super.init(frame: .zero) + private let disposeBag = DisposeBag() + + public override init(frame: CGRect) { + super.init(frame: frame) setupUI() setupAttributes() setupAutoLayout() + + isUpdatedRelay + .withUnretained(self) + .observe(on: MainScheduler.instance) + .bind(onNext: { $0.0.missionButton.configuration?.image = $0.1 ? DesignSystemAsset.mission.image : nil }) + .disposed(by: disposeBag) } required init?(coder: NSCoder) { @@ -69,7 +79,7 @@ public final class BibbiSegmentedControl: UIView { $0.configurationUpdateHandler = { [weak self] in guard let self = self else { return } - $0.configuration?.image = self.isUpdated ? nil : DesignSystemAsset.mission.image + $0.configuration?.image = nil } $0.configuration?.imagePlacement = .trailing $0.configuration?.imagePadding = 4 diff --git a/14th-team5-iOS/Data/Sources/Family/Repository/FamilyRepository.swift b/14th-team5-iOS/Data/Sources/Family/Repository/FamilyRepository.swift index be6001986..ab12d438a 100644 --- a/14th-team5-iOS/Data/Sources/Family/Repository/FamilyRepository.swift +++ b/14th-team5-iOS/Data/Sources/Family/Repository/FamilyRepository.swift @@ -35,9 +35,11 @@ extension FamilyRepository { public func joinFamily(body: JoinFamilyRequest) -> Observable { let body = JoinFamilyRequestDTO(inviteCode: body.inviteCode) return familyApiWorker.joinFamily(body: body) - .do(onSuccess: { - App.Repository.member.familyId.accept($0?.familyId) - App.Repository.member.familyCreatedAt.accept($0?.createdAt) + .do(onSuccess: { [weak self] response in + guard let self else { return } + App.Repository.member.familyId.accept(response?.familyId) + App.Repository.member.familyCreatedAt.accept(response?.createdAt) + fetchPaginationFamilyMembers(query: .init()) }) .asObservable() } diff --git a/14th-team5-iOS/Data/Sources/View/Main/DTO/MainResponseDTO.swift b/14th-team5-iOS/Data/Sources/View/Main/DTO/MainResponseDTO.swift index 1c7ec0cb0..656c9f8ed 100644 --- a/14th-team5-iOS/Data/Sources/View/Main/DTO/MainResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/View/Main/DTO/MainResponseDTO.swift @@ -13,7 +13,7 @@ struct TopBarElement: Codable { let memberId: String let imageUrl: String? let noImageLetter: String - let displayName: String + let displayName: String? let displayRank: Int? let shouldShowBirthdayMark: Bool let shouldShowPickIcon: Bool @@ -22,7 +22,7 @@ struct TopBarElement: Codable { return .init( memberId: memberId, profileImageURL: imageUrl, - name: displayName, + name: displayName ?? "알 수 없음", dayOfBirth: nil, isShowBirthdayMark: shouldShowBirthdayMark, isShowPickIcon: shouldShowPickIcon, @@ -34,24 +34,24 @@ struct TopBarElement: Codable { struct SurvivalFeed: Codable { let postId: String let imageUrl: String - let authorName: String + let authorName: String? let createdAt: String } struct MissionFeed: Codable { let postId: String let imageUrl: String - let authorName: String + let authorName: String? let createdAt: String } struct Picker: Codable { let memberId: String let imageUrl: String? - let displayName: String + let displayName: String? func toDomain() -> Domain.Picker { - return .init(imageUrl: imageUrl, displayName: displayName) + return .init(imageUrl: imageUrl, displayName: displayName ?? "알 수 없음") } } diff --git a/14th-team5-iOS/Data/Sources/View/Main/Repository/MainRepository.swift b/14th-team5-iOS/Data/Sources/View/Main/Repository/MainRepository.swift index 28ad66c09..f54384654 100644 --- a/14th-team5-iOS/Data/Sources/View/Main/Repository/MainRepository.swift +++ b/14th-team5-iOS/Data/Sources/View/Main/Repository/MainRepository.swift @@ -22,14 +22,12 @@ public final class MainRepository: MainRepositoryProtocol { extension MainRepository { public func fetchMain() -> Observable { return mainApiWorker.fetchMain() - .do(onSuccess: { FamilyUserDefaults.saveFamilyMembers($0?.mainFamilyProfileDatas ?? []) }) .asObservable() } public func fetchMainNight() -> Observable { return mainApiWorker.fetchMainNight() - .do(onSuccess: { FamilyUserDefaults.saveFamilyMembers($0?.mainFamilyProfileDatas ?? []) }) .asObservable() } } From 135f8c955c9243c54baff8ed641fc7d00a7a909e Mon Sep 17 00:00:00 2001 From: Kyoungmi <1mmgm.dev@gmail.com> Date: Tue, 14 May 2024 23:50:59 +0900 Subject: [PATCH 051/263] [feat]: contributorview hidden(#526) --- .../Presentation/Home/Reactor/MainViewReactor.swift | 4 ++-- .../Home/ViewControllers/MainViewController.swift | 9 +++++---- .../Presentation/PostDetail/Views/MissionTextView.swift | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift index e98f5840a..30a84f5a3 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift @@ -61,7 +61,7 @@ final class MainViewReactor: Reactor { } struct State { - var isInTime: Bool = true + var isInTime: Bool? var pageIndex: Int = 0 var leftCount: Int = 0 @@ -382,7 +382,7 @@ extension MainViewReactor { } } - return (false, 0) + return (false, 1000) } } 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 5ce38af71..9b55fbca2 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift @@ -177,7 +177,8 @@ extension MainViewController { } private func bindOutput(reactor: MainViewReactor) { - reactor.state.map { $0.isInTime } + reactor.state.map { $0.isInTime }.compactMap { $0 } + .debug("isInTime") .distinctUntilChanged() .withUnretained(self) .observe(on: MainScheduler.instance) @@ -218,11 +219,9 @@ extension MainViewController { .disposed(by: disposeBag) reactor.state.map { $0.isMissionUnlocked } - .debug("냐밍") .distinctUntilChanged() - .withUnretained(self) .observe(on: MainScheduler.instance) - .bind(onNext: { $0.0.segmentControl.isUpdatedRelay }) + .bind(to: segmentControl.isUpdatedRelay) .disposed(by: disposeBag) reactor.pulse(\.$contributor) @@ -268,9 +267,11 @@ extension MainViewController { private func setInTimeView(_ isInTime: Bool) { if isInTime { contributorView.isHidden = true + pageViewController.view.isHidden = false segmentControl.isHidden = false } else { contributorView.isHidden = false + pageViewController.view.isHidden = true segmentControl.isHidden = true } } diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/MissionTextView.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/MissionTextView.swift index c913ae655..240264808 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/MissionTextView.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/MissionTextView.swift @@ -84,7 +84,7 @@ final class MissionTextView: UIView { } missionImageView.do { - $0.image = DesignSystemAsset.mission1.image + $0.image = DesignSystemAsset.missionTitleBadge.image $0.contentMode = .scaleAspectFit } } From 17018a48543398d3afad79094ca389d2b853cbb6 Mon Sep 17 00:00:00 2001 From: Do-hyun-Kim Date: Thu, 16 May 2024 17:20:14 +0900 Subject: [PATCH 052/263] =?UTF-8?q?feat:=20ProfileViewController=20Page=20?= =?UTF-8?q?Animation=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Views/PostDetailCollectionViewCell.swift | 4 +++ .../ProfileFeedPageViewController.swift | 30 +++++++++++++---- .../Profile/ProfileViewController.swift | 32 ++++++++----------- 3 files changed, 40 insertions(+), 26 deletions(-) diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/PostDetailCollectionViewCell.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/PostDetailCollectionViewCell.swift index 0222b2e90..477412f45 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/PostDetailCollectionViewCell.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/PostDetailCollectionViewCell.swift @@ -178,14 +178,18 @@ extension PostDetailCollectionViewCell { reactor.state.map { $0.missionContent } .distinctUntilChanged() + .debug("미션 컨텐츠 뷰 타이틀") + .observe(on: MainScheduler.instance) .bind(to: missionTextView.missionLabel.rx.text) .disposed(by: disposeBag) reactor.state.map { $0.missionContent.isEmpty } .distinctUntilChanged() + .debug("미션 컨텐츠 뷰 히든 :") .asDriver(onErrorDriveWith: .empty()) .drive(missionTextView.rx.isHidden) .disposed(by: disposeBag) + reactor.state.map { $0.post } .distinctUntilChanged() diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileFeedPageViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileFeedPageViewController.swift index aabbcee3a..dfe875cae 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileFeedPageViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileFeedPageViewController.swift @@ -14,12 +14,13 @@ import RxCocoa final class ProfileFeedPageViewController: UIPageViewController { - public var currentPage: Int = 0 { + public var currentPageRelay: BehaviorRelay = .init(value: 0) { didSet { - setViewController(index: currentPage) + setViewController(index: currentPageRelay.value) } } + private lazy var feedViewControllers:[UIViewController] = [profileFeedSurivalViewController, profileFeedMissionViewController] public var memberId: String = "" private lazy var profileFeedSurivalViewController: ProfileFeedViewController = ProfileFeedDIContainer(postType: .survival, memberId: memberId).makeViewController() @@ -45,16 +46,20 @@ final class ProfileFeedPageViewController: UIPageViewController { } private func setupUI() { - setViewController(index: currentPage) + setViewController(index: currentPageRelay.value) } private func setViewController(index: Int) { switch index { case 0: - setViewControllers([profileFeedSurivalViewController], direction: .reverse, animated: true) + setViewControllers([profileFeedSurivalViewController], direction: .reverse, animated: true) { [weak self] _ in + self?.isPagingEnabled = true + } case 1: - setViewControllers([profileFeedMissionViewController], direction: .forward, animated: true) + setViewControllers([profileFeedMissionViewController], direction: .forward, animated: true) { [weak self] _ in + self?.isPagingEnabled = true + } default: break } @@ -67,12 +72,23 @@ final class ProfileFeedPageViewController: UIPageViewController { extension ProfileFeedPageViewController: UIPageViewControllerDelegate, UIPageViewControllerDataSource { func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { - return nil + guard let index = feedViewControllers.firstIndex(of: viewController), + index - 1 >= 0 else { return nil } + return feedViewControllers[index - 1] } func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { - return nil + guard let index = feedViewControllers.firstIndex(of: viewController), + index + 1 != feedViewControllers.count else { return nil } + return feedViewControllers[index + 1] } + func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) { + if let currentViewController = pageViewController.viewControllers?.first, + let currentIndex = feedViewControllers.firstIndex(of: currentViewController) { + currentPageRelay.accept(currentIndex) + } + } + } diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileViewController.swift index c71fc8013..a20819157 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileViewController.swift @@ -182,33 +182,27 @@ public final class ProfileViewController: BaseViewController .distinctUntilChanged() .bind(to: profileView.rx.isSetting) .disposed(by: disposeBag) - - profileSegementControl - .survivalButton.rx.tap - .throttle(.milliseconds(300), scheduler: MainScheduler.instance) - .map { Reactor.Action.didTapSegementControl(.survival) } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - profileSegementControl - .missionButton.rx.tap - .throttle(.milliseconds(300), scheduler: MainScheduler.instance) - .map { Reactor.Action.didTapSegementControl(.mission) } - .bind(to: reactor.action) - .disposed(by: disposeBag) + reactor.state .map { $0.memberId } .bind(to: profileFeedViewController.rx.memberId) .disposed(by: disposeBag) + Observable.merge( + profileFeedViewController.currentPageRelay.map { $0 == 0 ? BibbiFeedType.survival : BibbiFeedType.mission }.map { Reactor.Action.didTapSegementControl($0) }, + profileSegementControl.missionButton.rx.tap.map { Reactor.Action.didTapSegementControl(.mission)}, + profileSegementControl.survivalButton.rx.tap.map { Reactor.Action.didTapSegementControl(.survival)} + ) + .throttle(.milliseconds(300), scheduler: MainScheduler.instance) + .bind(to: reactor.action) + .disposed(by: disposeBag) + reactor.state - .map { $0.feedType.rawValue } - .distinctUntilChanged() - .bind(to: profileFeedViewController.rx.currentPage) + .map { $0.feedType == .survival ? 0 : 1 } + .bind(to: profileFeedViewController.currentPageRelay) .disposed(by: disposeBag) - - + reactor .state.map { $0.feedType == .survival ? true : false } .distinctUntilChanged() From e4cb9e8f4ea12e4c0020a75d3c0608d73627df71 Mon Sep 17 00:00:00 2001 From: Do-hyun-Kim Date: Fri, 17 May 2024 17:32:06 +0900 Subject: [PATCH 053/263] =?UTF-8?q?feat:=20PostDetailViewController=20Cell?= =?UTF-8?q?=20=EC=8A=A4=ED=81=AC=EB=A1=A4=EC=8B=9C=20=ED=95=B4=EB=8B=B9=20?= =?UTF-8?q?index=EC=97=90=20=EB=A7=9E=EB=8A=94=20Cell=20reloadData=20?= =?UTF-8?q?=ED=98=B8=EC=B6=9C=ED=95=98=EC=97=AC=20UI=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8=20-=20ProfileFeedPageDIContainer,=20ProfileP?= =?UTF-8?q?ageViewReactor=20=EC=B6=94=EA=B0=80=20-=20URLTypes,=20BibbiFeed?= =?UTF-8?q?Type=20Common=20Types=20=ED=8F=B4=EB=8D=94=EB=A1=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99=20-=20ProfileFeedGlobalState=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PostDetail/Reactor/PostReactor.swift | 3 +- .../ViewControllers/PostViewController.swift | 14 +++- .../Views/PostDetailCollectionViewCell.swift | 8 +- .../ProfileFeedPageDIContainer.swift | 39 ++++++++++ .../ProfileFeedPageViewController.swift | 42 +++++++---- .../Profile/ProfileViewController.swift | 37 ++++------ .../Reactor/ProfileFeedPageViewReactor.swift | 73 +++++++++++++++++++ .../Profile/Reactor/ProfileViewReactor.swift | 19 ++++- .../Sources/Common/{ => Types}/URLTypes.swift | 5 ++ .../GlobalState/GlobalStateProvider.swift | 2 + .../GlobalState/ProfileFeedGlobalState.swift | 41 +++++++++++ 11 files changed, 233 insertions(+), 50 deletions(-) create mode 100644 14th-team5-iOS/App/Sources/Presentation/Profile/Dependency/ProfileFeedPageDIContainer.swift create mode 100644 14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileFeedPageViewReactor.swift rename 14th-team5-iOS/Core/Sources/Common/{ => Types}/URLTypes.swift (91%) create mode 100644 14th-team5-iOS/Core/Sources/GlobalState/ProfileFeedGlobalState.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostReactor.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostReactor.swift index f5b02f2f8..8c099a3ff 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostReactor.swift @@ -26,7 +26,7 @@ final class PostReactor: Reactor { } struct State { - let selectedIndex: Int + var selectedIndex: Int let originPostLists: PostSection.Model var isPop: Bool = false @@ -103,6 +103,7 @@ extension PostReactor { if case let .main(postData) = newState.originPostLists.items[index] { newState.selectedPost = postData } + newState.selectedIndex = index case .setPop: newState.isPop = true diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/PostViewController.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/PostViewController.swift index 7900c055d..4a3b94c12 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/PostViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/PostViewController.swift @@ -49,8 +49,18 @@ final class PostViewController: BaseViewController { owner.navigationController?.pushViewController(cameraViewController, animated: true) }.disposed(by: disposeBag) - - + + Observable.zip( + collectionView.rx.didEndDisplayingCell.map { $0.at.item }.distinctUntilChanged(), + reactor.state.map { $0.selectedIndex }.distinctUntilChanged() + ) + .debug("포스트 뷰 ZIP 옵저버블 :") + .filter { $0.0 == $0.1 } + .withUnretained(self) + .subscribe { owner, indexPath in + owner.collectionView.reloadItems(at: [.init(row: indexPath.1, section: 0)]) + }.disposed(by: disposeBag) + reactor.state.map { $0.originPostLists } .map(Array.init(with:)) .distinctUntilChanged() diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/PostDetailCollectionViewCell.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/PostDetailCollectionViewCell.swift index 477412f45..8e00f4a76 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/PostDetailCollectionViewCell.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/PostDetailCollectionViewCell.swift @@ -34,6 +34,7 @@ final class PostDetailCollectionViewCell: BaseCollectionViewCell ViewController { + return ProfileFeedPageViewController(reactor: makeReactor(), memberId: memberId) + } + + func makeReactor() -> Reactor { + return ProfileFeedPageViewReactor(provider: globalState) + } + + +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileFeedPageViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileFeedPageViewController.swift index dfe875cae..f94b02e4f 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileFeedPageViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileFeedPageViewController.swift @@ -9,27 +9,25 @@ import UIKit import Core import RxSwift +import ReactorKit import RxCocoa final class ProfileFeedPageViewController: UIPageViewController { - + var disposeBag: DisposeBag = DisposeBag() - public var currentPageRelay: BehaviorRelay = .init(value: 0) { - didSet { - setViewController(index: currentPageRelay.value) - } - } private lazy var feedViewControllers:[UIViewController] = [profileFeedSurivalViewController, profileFeedMissionViewController] - public var memberId: String = "" + public var memberId: String private lazy var profileFeedSurivalViewController: ProfileFeedViewController = ProfileFeedDIContainer(postType: .survival, memberId: memberId).makeViewController() private lazy var profileFeedMissionViewController: ProfileFeedViewController = ProfileFeedDIContainer(postType: .mission, memberId: memberId).makeViewController() - override init(transitionStyle style: UIPageViewController.TransitionStyle, navigationOrientation: UIPageViewController.NavigationOrientation, options: [UIPageViewController.OptionsKey : Any]? = nil) { + init(reactor: ProfileFeedPageViewReactor, memberId: String) { + self.memberId = memberId super.init(transitionStyle: .scroll, navigationOrientation: .horizontal) + self.reactor = reactor } required init?(coder: NSCoder) { @@ -39,14 +37,13 @@ final class ProfileFeedPageViewController: UIPageViewController { override func viewDidLoad() { super.viewDidLoad() - - setupUI() self.delegate = self self.dataSource = self + setupUI() } private func setupUI() { - setViewController(index: currentPageRelay.value) + setViewController(index: 0) } @@ -68,6 +65,20 @@ final class ProfileFeedPageViewController: UIPageViewController { +extension ProfileFeedPageViewController: ReactorKit.View { + + func bind(reactor: ProfileFeedPageViewReactor) { + reactor.state + .map { $0.pageType == .survival ? 0 : 1 } + .distinctUntilChanged() + .withUnretained(self) + .observe(on: MainScheduler.instance) + .bind { owner, index in + owner.setViewController(index: index) + }.disposed(by: disposeBag) + } +} + extension ProfileFeedPageViewController: UIPageViewControllerDelegate, UIPageViewControllerDataSource { @@ -80,15 +91,16 @@ extension ProfileFeedPageViewController: UIPageViewControllerDelegate, UIPageVie func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { guard let index = feedViewControllers.firstIndex(of: viewController), index + 1 != feedViewControllers.count else { return nil } + return feedViewControllers[index + 1] } func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) { - if let currentViewController = pageViewController.viewControllers?.first, - let currentIndex = feedViewControllers.firstIndex(of: currentViewController) { - currentPageRelay.accept(currentIndex) - } + + guard let currentViewController = pageViewController.viewControllers?.first, + let currentIndex = feedViewControllers.firstIndex(of: currentViewController) else { return } + reactor?.action.onNext(.updatePageViewController(currentIndex)) } } diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileViewController.swift index 480e60790..e4475e219 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileViewController.swift @@ -36,7 +36,7 @@ public final class ProfileViewController: BaseViewController private let profileLineView: UIView = UIView() private lazy var profilePickerController: PHPickerViewController = PHPickerViewController(configuration: pickerConfiguration) - private lazy var profileFeedViewController: ProfileFeedPageViewController = ProfileFeedPageViewController() + private lazy var profileFeedViewController: ProfileFeedPageViewController = ProfileFeedPageDIContainer(memberId: reactor?.currentState.memberId ?? "").makeViewController() public override func viewWillAppear(_ animated: Bool) { @@ -183,33 +183,26 @@ public final class ProfileViewController: BaseViewController .bind(to: profileView.rx.isSetting) .disposed(by: disposeBag) - - reactor.state - .map { $0.memberId } - .bind(to: profileFeedViewController.rx.memberId) - .disposed(by: disposeBag) - - Observable.merge( - profileFeedViewController.currentPageRelay.map { $0 == 0 ? BibbiFeedType.survival : BibbiFeedType.mission }.map { Reactor.Action.didTapSegementControl($0) }, - profileSegementControl.missionButton.rx.tap.map { Reactor.Action.didTapSegementControl(.mission)}, - profileSegementControl.survivalButton.rx.tap.map { Reactor.Action.didTapSegementControl(.survival)} - ) - .throttle(.milliseconds(300), scheduler: MainScheduler.instance) - .bind(to: reactor.action) - .disposed(by: disposeBag) - - reactor.state - .map { $0.feedType == .survival ? 0 : 1 } - .bind(to: profileFeedViewController.currentPageRelay) - .disposed(by: disposeBag) - reactor .state.map { $0.feedType == .survival ? true : false } .distinctUntilChanged() .observe(on: MainScheduler.instance) .bind(to: profileSegementControl.rx.isSelected) .disposed(by: disposeBag) - + + profileSegementControl + .missionButton.rx.tap + .throttle(.milliseconds(100), scheduler: MainScheduler.instance) + .map { Reactor.Action.didTapSegementControl(.mission) } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + profileSegementControl + .survivalButton.rx.tap + .throttle(.milliseconds(100), scheduler: MainScheduler.instance) + .map { Reactor.Action.didTapSegementControl(.survival) } + .bind(to: reactor.action) + .disposed(by: disposeBag) diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileFeedPageViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileFeedPageViewReactor.swift new file mode 100644 index 000000000..3e4871dfb --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileFeedPageViewReactor.swift @@ -0,0 +1,73 @@ +// +// ProfileFeedPageViewReactor.swift +// App +// +// Created by Kim dohyun on 5/16/24. +// + +import Foundation + +import Core +import ReactorKit + +final class ProfileFeedPageViewReactor: Reactor { + + //MAKR: Property + public var initialState: State + private var provider: GlobalStateProviderProtocol + + enum Action { + case updatePageViewController(Int) + } + + struct State { + var pageType: BibbiFeedType + } + + enum Mutation { + case reloadPageViewController(BibbiFeedType) + case didShowPageViewController(BibbiFeedType) + } + + init(provider: GlobalStateProviderProtocol) { + self.initialState = State(pageType: .survival) + self.provider = provider + } + + func transform(mutation: Observable) -> Observable { + let reloadPageViewMutation = provider.profilePageGlobalState.event + .flatMap { event -> Observable in + switch event { + case let .didTapSegmentedPage(type): + + return .just(.reloadPageViewController(type)) + default: + return .empty() + } + } + + return .merge(mutation, reloadPageViewMutation) + } + + func mutate(action: Action) -> Observable { + switch action { + case let .updatePageViewController(pageIndex): + return .just(.didShowPageViewController(pageIndex == 0 ? .survival : .mission)) + } + } + + + func reduce(state: State, mutation: Mutation) -> State { + var newState = state + switch mutation { + case let .reloadPageViewController(pageType): + newState.pageType = pageType + + case let .didShowPageViewController(pageType): + provider.profilePageGlobalState.didReceiveMemberId(memberId: pageType) + newState.pageType = pageType + } + return newState + } + +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift index f24b53685..ff778c6da 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift @@ -11,10 +11,6 @@ import Core import Domain import ReactorKit -public enum BibbiFeedType: Int { - case survival = 0 - case mission = 1 -} public final class ProfileViewReactor: Reactor { public var initialState: State @@ -63,6 +59,20 @@ public final class ProfileViewReactor: Reactor { self.provider = provider } + public func transform(mutation: Observable) -> Observable { + let segmentedUpdateMutation = provider.profilePageGlobalState.event + .flatMap { event -> Observable in + switch event { + case let .didReceiveMemberId(type): + return .just(.setProfileFeedType(type)) + default: + return .empty() + } + } + return .merge(mutation, segmentedUpdateMutation) + } + + public func mutate(action: Action) -> Observable { //TODO: Keychain, UserDefaults 추가 switch action { @@ -171,6 +181,7 @@ public final class ProfileViewReactor: Reactor { } case let .didTapSegementControl(feedType): + provider.profilePageGlobalState.didTapSegmentedPageType(type: feedType) return .just(.setProfileFeedType(feedType)) case .viewDidDisappear: return provider.mainService.refreshMain() diff --git a/14th-team5-iOS/Core/Sources/Common/URLTypes.swift b/14th-team5-iOS/Core/Sources/Common/Types/URLTypes.swift similarity index 91% rename from 14th-team5-iOS/Core/Sources/Common/URLTypes.swift rename to 14th-team5-iOS/Core/Sources/Common/Types/URLTypes.swift index da1df5a5c..a8a34dbdc 100644 --- a/14th-team5-iOS/Core/Sources/Common/URLTypes.swift +++ b/14th-team5-iOS/Core/Sources/Common/Types/URLTypes.swift @@ -7,6 +7,11 @@ import UIKit +public enum BibbiFeedType: Int { + case survival = 0 + case mission = 1 +} + public enum URLTypes { case settings diff --git a/14th-team5-iOS/Core/Sources/GlobalState/GlobalStateProvider.swift b/14th-team5-iOS/Core/Sources/GlobalState/GlobalStateProvider.swift index a617eb8c0..4cd4e9a76 100644 --- a/14th-team5-iOS/Core/Sources/GlobalState/GlobalStateProvider.swift +++ b/14th-team5-iOS/Core/Sources/GlobalState/GlobalStateProvider.swift @@ -15,6 +15,7 @@ public protocol GlobalStateProviderProtocol: AnyObject { var profileGlobalState: ProfileGlobalStateType { get } var timerGlobalState: TimerGlobalStateType { get } var realEmojiGlobalState: RealEmojiGlobalStateType { get } + var profilePageGlobalState: ProfileFeedGlobalStateType { get } var mainService: MainServiceType { get } } @@ -27,6 +28,7 @@ final public class GlobalStateProvider: GlobalStateProviderProtocol { public lazy var timerGlobalState: TimerGlobalStateType = TimerGlobalState(provider: self) public lazy var realEmojiGlobalState: RealEmojiGlobalStateType = RealEmojiGlobalState(provider: self) + public lazy var profilePageGlobalState: ProfileFeedGlobalStateType = ProfileFeedGlobalState(provider: self) public lazy var mainService: MainServiceType = MainService(provider: self) diff --git a/14th-team5-iOS/Core/Sources/GlobalState/ProfileFeedGlobalState.swift b/14th-team5-iOS/Core/Sources/GlobalState/ProfileFeedGlobalState.swift new file mode 100644 index 000000000..d5fd90d43 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/GlobalState/ProfileFeedGlobalState.swift @@ -0,0 +1,41 @@ +// +// ProfileFeedGlobalState.swift +// Core +// +// Created by Kim dohyun on 5/16/24. +// + +import Foundation + +import RxSwift + +public enum ProfileFeedEvent { + case didTapSegmentedPage(BibbiFeedType) + case didReceiveMemberId(BibbiFeedType) +} + + +public protocol ProfileFeedGlobalStateType { + var event: PublishSubject { get } + + @discardableResult + func didTapSegmentedPageType(type: BibbiFeedType) -> Observable + @discardableResult + func didReceiveMemberId(memberId: BibbiFeedType) -> Observable +} + +public final class ProfileFeedGlobalState: BaseGlobalState, ProfileFeedGlobalStateType { + + public var event: PublishSubject = PublishSubject() + + public func didTapSegmentedPageType(type: BibbiFeedType) -> Observable { + event.onNext(.didTapSegmentedPage(type)) + return Observable.just(type) + } + + public func didReceiveMemberId(memberId: BibbiFeedType) -> Observable { + event.onNext(.didReceiveMemberId(memberId)) + return Observable.just(memberId) + } +} + From 84ba10a99b76116c3843313f41e9e0be19977e0b Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Mon, 20 May 2024 12:49:23 +0900 Subject: [PATCH 054/263] =?UTF-8?q?feat:=20KeychainWrapper=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#530)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Reactor/AccountSignUpReactor.swift | 5 +- .../KeychainItemAccessibility.swift | 52 +++ .../KeychainWrapper/KeychainWrapper.swift | 411 ++++++++++++++++++ .../KeychainWrapper/KeychainWrapperKey.swift | 16 + .../KeychainWrapperSubscript.swift | 114 +++++ .../Core/Sources/Token/TokenRepository.swift | 12 +- .../AccountRepository/AccountRepository.swift | 4 +- .../Sources/Account/MeAPI/MeAPIWorker.swift | 1 - .../Sources/Keychain/KeychainRepository.swift | 6 +- .../FetchMainUseCaseProtocol.swift | 14 - Bibbi.xcworkspace/contents.xcworkspacedata | 3 - .../xcschemes/Bibbi-Workspace.xcscheme | 14 - Tuist/Dependencies.swift | 1 - .../ModuleType+Templates.swift | 1 - .../Package+Templates.swift | 1 - graph.png | Bin 0 -> 1829355 bytes 16 files changed, 604 insertions(+), 51 deletions(-) create mode 100644 14th-team5-iOS/Core/Sources/BBStorage/KeychainWrapper/KeychainItemAccessibility.swift create mode 100644 14th-team5-iOS/Core/Sources/BBStorage/KeychainWrapper/KeychainWrapper.swift create mode 100644 14th-team5-iOS/Core/Sources/BBStorage/KeychainWrapper/KeychainWrapperKey.swift create mode 100644 14th-team5-iOS/Core/Sources/BBStorage/KeychainWrapper/KeychainWrapperSubscript.swift delete mode 100644 14th-team5-iOS/Domain/Sources/Main/FetchMainUseCase/FetchMainUseCaseProtocol.swift create mode 100644 graph.png diff --git a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/Reactor/AccountSignUpReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/Reactor/AccountSignUpReactor.swift index fa7e39a22..f395b1a65 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/Reactor/AccountSignUpReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/Reactor/AccountSignUpReactor.swift @@ -5,11 +5,10 @@ // Created by geonhui Yu on 12/8/23. // -import Foundation +import Core import Data import Domain -import Core -import SwiftKeychainWrapper +import Foundation import ReactorKit diff --git a/14th-team5-iOS/Core/Sources/BBStorage/KeychainWrapper/KeychainItemAccessibility.swift b/14th-team5-iOS/Core/Sources/BBStorage/KeychainWrapper/KeychainItemAccessibility.swift new file mode 100644 index 000000000..7b26d7fe4 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/BBStorage/KeychainWrapper/KeychainItemAccessibility.swift @@ -0,0 +1,52 @@ +// +// main.swift +// KeychainWrapper +// +// Created by 김건우 on 5/13/24. +// + +import Foundation + +protocol KeychainAttrRepresentable { + var keychainAttrValue: CFString { get } +} + +// MARK: - KeychainItemAccessibility +public enum KeychainItemAccessibility { + + case afterFirstUnlock + case afterFirstUnlockThisDeviceOnly + case whenPasscodeSetThisDeviceOnly + case whenUnlocked + case whenUnlockedThisDeviceOnly + + static func accessibilityForAttributeValue(_ keychainAttrValue: CFString) -> KeychainItemAccessibility? { + for (key, value) in keychainItemAccessibilityLookup { + if value == keychainAttrValue { + return key + } + } + + return nil + } +} + +private let keychainItemAccessibilityLookup: [KeychainItemAccessibility: CFString] = { + var lookup: [KeychainItemAccessibility: CFString] = [ + .afterFirstUnlock: kSecAttrAccessibleAfterFirstUnlock, + .afterFirstUnlockThisDeviceOnly: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, + .whenPasscodeSetThisDeviceOnly: kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, + .whenUnlocked: kSecAttrAccessibleWhenUnlocked, + .whenUnlockedThisDeviceOnly: kSecAttrAccessibleWhenUnlockedThisDeviceOnly + ] + + return lookup +}() + +// MARK: - Extensions +extension KeychainItemAccessibility: KeychainAttrRepresentable { + internal var keychainAttrValue: CFString { + return keychainItemAccessibilityLookup[self]! + } +} + diff --git a/14th-team5-iOS/Core/Sources/BBStorage/KeychainWrapper/KeychainWrapper.swift b/14th-team5-iOS/Core/Sources/BBStorage/KeychainWrapper/KeychainWrapper.swift new file mode 100644 index 000000000..a09ff7b7c --- /dev/null +++ b/14th-team5-iOS/Core/Sources/BBStorage/KeychainWrapper/KeychainWrapper.swift @@ -0,0 +1,411 @@ +// +// KeychainWrapper.swift +// KeychainWrapper +// +// Created by 김건우 on 5/13/24. +// + +import Foundation + +public class KeychainWrapper { + + // MARK: - Properties + public static let standard = KeychainWrapper() + + private let SecMatchLimit: String! = kSecMatchLimit as String + private let SecReturnData: String! = kSecReturnData as String + private let SecValueData: String! = kSecValueData as String + private let SecAttrAccessible: String! = kSecAttrAccessible as String + private let SecClass: String! = kSecClass as String + private let SecAttrService: String! = kSecAttrService as String + private let SecAttrGeneric: String! = kSecAttrGeneric as String + private let SecAttrAccount: String! = kSecAttrAccount as String + private let SecAttrAccessGroup: String! = kSecAttrAccessGroup as String + private let SecAttrSynchronizable: String! = kSecAttrSynchronizable as String + + private(set) public var serviceName: String + private(set) public var accessGroup: String? + + private static let defaultServiceName: String = { + return Bundle.main.bundleIdentifier ?? "KeychainWrapper" + }() + + // MARK: - Intializer + public init( + serviceName: String, + accessGroup: String? = nil + ) { + self.serviceName = serviceName + self.accessGroup = accessGroup + } + + private convenience init() { + self.init(serviceName: KeychainWrapper.defaultServiceName) + } + + + + // MARK: - Read + + public func integer( + forKey key: String, + withAccessibility accessibility: KeychainItemAccessibility? = nil, + isSynchronizable: Bool = false + ) -> Int? { + guard let numberValue = object( + forKey: key, + withAccessibility: accessibility, + isSynchronizable: isSynchronizable + ) as? NSNumber else { + return nil + } + + return numberValue.intValue + } + + public func float( + forKey key: String, + withAccessibility accessibility: KeychainItemAccessibility? = nil, + isSynchronizable: Bool = false + ) -> Float? { + guard let numberValue = object( + forKey: key, + withAccessibility: accessibility, + isSynchronizable: isSynchronizable + ) as? NSNumber else { + return nil + } + + return numberValue.floatValue + } + + public func double( + forKey key: String, + withAccessibility accessibility: KeychainItemAccessibility? = nil, + isSynchronizable: Bool = false + ) -> Double? { + guard let numberValue = object( + forKey: key, + withAccessibility: accessibility, + isSynchronizable: isSynchronizable + ) as? NSNumber else { + return nil + } + + return numberValue.doubleValue + } + + public func bool( + forKey key: String, + withAccessibility accessibility: KeychainItemAccessibility? = nil, + isSynchronizable: Bool = false + ) -> Bool? { + guard let numberValue = object( + forKey: key, + withAccessibility: accessibility, + isSynchronizable: isSynchronizable + ) as? NSNumber else { + return nil + } + + return numberValue.boolValue + } + + public func string( + forKey key: String, + withAccessibility accessibility: KeychainItemAccessibility? = nil, + isSynchronizable: Bool = false + ) -> String? { + guard let keychainData = data( + forKey: key, + withAccessibility: accessibility, + isSynchronizable: isSynchronizable + ) else { + return nil + } + + return String(data: keychainData, encoding: .utf8) + } + + public func object( + forKey key: String, + withAccessibility accessibility: KeychainItemAccessibility? = nil, + isSynchronizable: Bool = false + ) -> NSCoding? { + guard let keychainData = data( + forKey: key, + withAccessibility: accessibility, + isSynchronizable: isSynchronizable + ) else { + return nil + } + + return try? NSKeyedUnarchiver.unarchivedObject(ofClass: NSNumber.self, from: keychainData) + } + + public func data( + forKey key: String, + withAccessibility accessibility: KeychainItemAccessibility? = nil, + isSynchronizable: Bool = false + ) -> Data? { + var keychainQueryDictionary = setupKeychainQueryDictionary( + forkey: key, + withAccessibility: accessibility, + isSynchronizable: isSynchronizable + ) + keychainQueryDictionary[SecMatchLimit] = kSecMatchLimitOne + keychainQueryDictionary[SecReturnData] = kCFBooleanTrue + + var result: CFTypeRef? + let status = SecItemCopyMatching( + keychainQueryDictionary as CFDictionary, + &result + ) + + return status == noErr ? result as? Data : nil + } + + + + + // MARK: - Create + + @discardableResult + public func set( + _ value: Int, + forKey key: String, + withAccessibility accessibility: KeychainItemAccessibility? = nil, + isSynchronizable: Bool = false + ) -> Bool { + return set( + NSNumber(value: value), + forKey: key, + withAccessibility: accessibility, + isSynchronizable: isSynchronizable + ) + } + + @discardableResult + public func set( + _ value: Float, + forKey key: String, + withAccessibility accessibility: KeychainItemAccessibility? = nil, + isSynchronizable: Bool = false + ) -> Bool { + return set( + NSNumber(value: value), + forKey: key, + withAccessibility: accessibility, + isSynchronizable: isSynchronizable + ) + } + + @discardableResult + public func set( + _ value: Double, + forKey key: String, + withAccessibility accessibility: KeychainItemAccessibility? = nil, + isSynchronizable: Bool = false + ) -> Bool { + return set( + NSNumber(value: value), + forKey: key, + withAccessibility: accessibility, + isSynchronizable: isSynchronizable + ) + } + + @discardableResult + public func set( + _ value: Bool, + forKey key: String, + withAccessibility accessibility: KeychainItemAccessibility? = nil, + isSynchronizable: Bool = false + ) -> Bool { + return set( + NSNumber(value: value), + forKey: key, + withAccessibility: accessibility, + isSynchronizable: isSynchronizable + ) + } + + @discardableResult + public func set( + _ value: String, + forKey key: String, + withAccessibility accessibility: KeychainItemAccessibility? = nil, + isSynchronizable: Bool = false + ) -> Bool { + if let data = value.data(using: .utf8) { + return set( + data, + forKey: key, + withAccessibility: accessibility, + isSynchronizable: isSynchronizable + ) + } else { + return false + } + } + + @discardableResult + public func set( + _ value: NSCoding, + forKey key: String, + withAccessibility accessibiilty: KeychainItemAccessibility? = nil, + isSynchronizable: Bool = false + ) -> Bool { + if let data = try? NSKeyedArchiver.archivedData( + withRootObject: value, + requiringSecureCoding: false + ) { + return set( + data, + forKey: key, + withAccessibility: accessibiilty, + isSynchronizable: isSynchronizable + ) + } else { + return false + } + } + + @discardableResult + public func set( + _ value: Data, + forKey key: String, + withAccessibility accessibility: KeychainItemAccessibility? = nil, + isSynchronizable: Bool = false + ) -> Bool { + var keychainQueryDictionary: [String: Any] = setupKeychainQueryDictionary( + forkey: key, + withAccessibility: accessibility, + isSynchronizable: isSynchronizable + ) + keychainQueryDictionary[SecValueData] = value + + if let accessibility = accessibility { + keychainQueryDictionary[SecAttrAccessible] = accessibility.keychainAttrValue + } else { + keychainQueryDictionary[SecAttrAccessible] = KeychainItemAccessibility.afterFirstUnlock.keychainAttrValue + } + + let status: OSStatus = SecItemAdd(keychainQueryDictionary as CFDictionary, nil) + + if status == errSecSuccess { + return true + } else if status == errSecDuplicateItem { + return update( + value, + forKey: key, + withAccessibility: accessibility, + isSynchronizable: isSynchronizable + ) + } else { + return false + } + } + + // MARK: - Update + + private func update( + _ value: Data, + forKey key: String, + withAccessibility accessiblity: KeychainItemAccessibility? = nil, + isSynchronizable: Bool = false + ) -> Bool { + var keychainQueryDictionary: [String: Any] = setupKeychainQueryDictionary( + forkey: key, + withAccessibility: accessiblity, + isSynchronizable: isSynchronizable + ) + let updateDictionary = [SecValueData: value] + + if let accessibility = accessiblity { + keychainQueryDictionary[SecAttrAccessible] = accessibility.keychainAttrValue + } + + let status: OSStatus = SecItemUpdate( + keychainQueryDictionary as CFDictionary, + updateDictionary as CFDictionary + ) + + if status == errSecSuccess { + return true + } else { + return false + } + } + + + + // MARK: - Delete + + @discardableResult + public func remove( + forKey key: String, + withAccessibility accessibility: KeychainItemAccessibility? = nil, + isSynchronizable: Bool = false + ) -> Bool { + let keychainQueryDictionary = setupKeychainQueryDictionary( + forkey: key, + withAccessibility: accessibility, + isSynchronizable: isSynchronizable + ) + + let status: OSStatus = SecItemDelete(keychainQueryDictionary as CFDictionary) + + if status == errSecSuccess { + return true + } else { + return false + } + } + + @discardableResult + public func removeAllKeys() -> Bool { + var keychainQueryDictionary: [String: Any] = [ + SecClass: kSecClassGenericPassword, + SecAttrService: serviceName + ] + + if let accessGroup = accessGroup { + keychainQueryDictionary[SecAttrAccessGroup] = accessGroup + } + + let status: OSStatus = SecItemDelete(keychainQueryDictionary as CFDictionary) + + if status == errSecSuccess { + return true + } else { + return false + } + } + + + // MARK: - KeychainQueryDictionary + private func setupKeychainQueryDictionary( + forkey key: String, + withAccessibility accessibility: KeychainItemAccessibility? = nil, + isSynchronizable: Bool = false + ) -> [String: Any] { + var keychainQueryDictionary: [String: Any] = [SecClass: kSecClassGenericPassword] + + keychainQueryDictionary[SecAttrService] = serviceName + if let accessibility = accessibility { + keychainQueryDictionary[SecAttrAccessible] = accessibility.keychainAttrValue + } + + if let accessGroup = accessGroup { + keychainQueryDictionary[SecAttrAccessGroup] = accessGroup + } + + let encodedIdentifier: Data? = key.data(using: .utf8) + keychainQueryDictionary[SecAttrAccount] = encodedIdentifier + + keychainQueryDictionary[SecAttrSynchronizable] = isSynchronizable ? kCFBooleanTrue : kCFBooleanFalse + + return keychainQueryDictionary + } +} diff --git a/14th-team5-iOS/Core/Sources/BBStorage/KeychainWrapper/KeychainWrapperKey.swift b/14th-team5-iOS/Core/Sources/BBStorage/KeychainWrapper/KeychainWrapperKey.swift new file mode 100644 index 000000000..98e901ec9 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/BBStorage/KeychainWrapper/KeychainWrapperKey.swift @@ -0,0 +1,16 @@ +// +// KeychainWrapperKey.swift +// KeychainWrapper +// +// Created by 김건우 on 5/14/24. +// + +import Foundation + +public extension KeychainWrapper.Key { + + static let accessToken: Self = "accessToken" + static let refreshToken: Self = "refreshToken" + static let fcmToken: Self = "fcmToken" + +} diff --git a/14th-team5-iOS/Core/Sources/BBStorage/KeychainWrapper/KeychainWrapperSubscript.swift b/14th-team5-iOS/Core/Sources/BBStorage/KeychainWrapper/KeychainWrapperSubscript.swift new file mode 100644 index 000000000..dd1dae637 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/BBStorage/KeychainWrapper/KeychainWrapperSubscript.swift @@ -0,0 +1,114 @@ +// +// KeychainSubscript.swift +// KeychainWrapper +// +// Created by 김건우 on 5/13/24. +// + +import Foundation + +public extension KeychainWrapper { + + subscript(key: Key) -> Int? { + get { integer(forKey: key) } + set { + guard let value = newValue else { return } + set(value, forKey: key.rawValue) + } + } + + subscript(key: Key) -> Float? { + get { float(forKey: key) } + set { + guard let value = newValue else { return } + set(value, forKey: key.rawValue) + } + } + + public subscript(key: Key) -> Double? { + get { double(forKey: key) } + set { + guard let value = newValue else { return } + set(value, forKey: key.rawValue) + } + } + + subscript(key: Key) -> Bool? { + get { bool(forKey: key) } + set { + guard let value = newValue else { return } + set(value, forKey: key.rawValue) + } + } + + subscript(key: Key) -> String? { + get { string(forKey: key) } + set { + guard let value = newValue else { return } + set(value, forKey: key.rawValue) + } + } + + subscript(key: Key) -> Data? { + get { data(forKey: key) } + set { + guard let value = newValue else { return } + set(value, forKey: key.rawValue) + } + } + +} + +public extension KeychainWrapper { + + func integer(forKey key: Key) -> Int? { + integer(forKey: key.rawValue) + } + + func float(forKey key: Key) -> Float? { + float(forKey: key.rawValue) + } + + func double(forKey key: Key) -> Double? { + double(forKey: key.rawValue) + } + + func bool(forKey key: Key) -> Bool? { + bool(forKey: key.rawValue) + } + + func string(forKey key: Key) -> String? { + string(forKey: key.rawValue) + } + + func data(forKey key: Key) -> Data? { + data(forKey: key.rawValue) + } + +} + +public extension KeychainWrapper { + + func remove(forKey key: Key) { + remove(forKey: key.rawValue) + } + +} + +public extension KeychainWrapper { + + struct Key: Hashable, RawRepresentable, ExpressibleByStringLiteral { + + public var rawValue: String + + public init(rawValue: String) { + self.rawValue = rawValue + } + + public init(stringLiteral value: String) { + self.rawValue = value + } + + } + +} diff --git a/14th-team5-iOS/Core/Sources/Token/TokenRepository.swift b/14th-team5-iOS/Core/Sources/Token/TokenRepository.swift index 93b25c781..43ed0f03d 100644 --- a/14th-team5-iOS/Core/Sources/Token/TokenRepository.swift +++ b/14th-team5-iOS/Core/Sources/Token/TokenRepository.swift @@ -9,13 +9,13 @@ import Foundation import RxSwift import RxCocoa -import SwiftKeychainWrapper +//import SwiftKeychainWrapper -public extension KeychainWrapper.Key { - static let fcmToken: KeychainWrapper.Key = "FCMToken" - static let accessToken: KeychainWrapper.Key = "accessToken" - static let refreshToken: KeychainWrapper.Key = "refreshToken" -} +//public extension KeychainWrapper.Key { +// static let fcmToken: KeychainWrapper.Key = "FCMToken" +// static let accessToken: KeychainWrapper.Key = "accessToken" +// static let refreshToken: KeychainWrapper.Key = "refreshToken" +//} public struct AccessToken: Codable, Equatable { public var accessToken: String? diff --git a/14th-team5-iOS/Data/Sources/Account/AccountRepository/AccountRepository.swift b/14th-team5-iOS/Data/Sources/Account/AccountRepository/AccountRepository.swift index 768e59b12..430a46f85 100644 --- a/14th-team5-iOS/Data/Sources/Account/AccountRepository/AccountRepository.swift +++ b/14th-team5-iOS/Data/Sources/Account/AccountRepository/AccountRepository.swift @@ -5,15 +5,13 @@ // Created by geonhui Yu on 12/18/23. // -import UIKit - import Core import Domain +import UIKit import ReactorKit import RxCocoa import RxSwift -import SwiftKeychainWrapper public enum AccountLoaction { case profile diff --git a/14th-team5-iOS/Data/Sources/Account/MeAPI/MeAPIWorker.swift b/14th-team5-iOS/Data/Sources/Account/MeAPI/MeAPIWorker.swift index 25b30a0ad..e7230e575 100644 --- a/14th-team5-iOS/Data/Sources/Account/MeAPI/MeAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Account/MeAPI/MeAPIWorker.swift @@ -9,7 +9,6 @@ import Foundation import Domain import Core -import SwiftKeychainWrapper import RxSwift import Alamofire diff --git a/14th-team5-iOS/Data/Sources/Keychain/KeychainRepository.swift b/14th-team5-iOS/Data/Sources/Keychain/KeychainRepository.swift index e4db5f31f..91e1feddb 100644 --- a/14th-team5-iOS/Data/Sources/Keychain/KeychainRepository.swift +++ b/14th-team5-iOS/Data/Sources/Keychain/KeychainRepository.swift @@ -5,11 +5,9 @@ // Created by 마경미 on 23.02.24. // -import Foundation - +import Core import Domain - -import SwiftKeychainWrapper +import Foundation public final class KeychainRepository: KeychainRepositoryProtocol { public static let shared = KeychainRepository() diff --git a/14th-team5-iOS/Domain/Sources/Main/FetchMainUseCase/FetchMainUseCaseProtocol.swift b/14th-team5-iOS/Domain/Sources/Main/FetchMainUseCase/FetchMainUseCaseProtocol.swift deleted file mode 100644 index 2ab4cb145..000000000 --- a/14th-team5-iOS/Domain/Sources/Main/FetchMainUseCase/FetchMainUseCaseProtocol.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// MainUseCaseProtocol.swift -// Domain -// -// Created by 마경미 on 20.04.24. -// - -import Foundation - -import RxSwift - -public protocol FetchMainUseCaseProtocol { - func execute() -> Observable -} diff --git a/Bibbi.xcworkspace/contents.xcworkspacedata b/Bibbi.xcworkspace/contents.xcworkspacedata index c80cdd70f..1219ce311 100644 --- a/Bibbi.xcworkspace/contents.xcworkspacedata +++ b/Bibbi.xcworkspace/contents.xcworkspacedata @@ -68,9 +68,6 @@ - - diff --git a/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme b/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme index 9d382a7b9..5f307e6b4 100644 --- a/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme +++ b/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme @@ -860,20 +860,6 @@ ReferencedContainer = "container:Tuist/Dependencies/SwiftPackageManager/.build/checkouts/SnapKit/SnapKit.xcodeproj"> - - - - !7E>v>chap_UmTG7sLd@f4?@P(#6F7EoO4z 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 rg-gng_kDosXQc^4gK8y*D|o-iB^5x~plv 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�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

$;iWfr?1K@}5UkJNOvyP35O3a~Tl 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

$kM^MIm}l6??&}k|0CM2D=z= zjaI!q^dnBD5nns!_Hn89mzuZwCv zz)^`@bexdz4&lcUi<^249jqB@U85`X63yff-7yacp#EfoOfp-C zElQ)U*Hq+E;n{uB{&g+5yYRvq)}k&#F6Jr>hxwbk`|)p_7WPauo55P)GyVieY22aq zrza(3!(nzHYZdjl_QP8SjL0(tM=T08N6e_V`p ztzAXRu5$D+YrMvSzBTU=@p#_vch=D1ZdC;@%xpxlqa$CfLTRCd)JDS>!LFum ztv?bmjTw?!nwNryg663rS85gm$u3~i%Rt#F1vm<}^!@r6V_X%qjaA@OmpuKlIW!qr z8~F_k4B$%8Arr)^zP~$I74!xuIxlcL*j#md1O^V0)_(Ac9((K{-&jmd~RFpXZ2 z{fl0oaHwC@FP*faLnGzC&pB>h}h$BB0!1NqzO%#x6A|E^E`O?A16{(HPN6 z71_L*J1;K}Ozew5aVTimnekz?6+RYWK|uykFTf^n%+uRW4YQXrZW=mUywG^ZKWIOE zzW)B~SBH?tCookI{~jy*#iD;S#o3oEnUi?r_j7)8=k)aCW_d67FbF0N%oA~SzGxoUIq2*Bwc7gWps}nafT@P3X3%VX< zuvPH;+F_lErNuDu)uC%)oUWRiFC!!4kvSm~U6)ma@Vw$5KWa{2H1XAaETjdE^Od9gUdf8^a*Sv&QD0s>Z)nSB5k(0~!q1=Kzx08MA_ZFV_u2g@_^ z*fN_qY7}@ndOn9)E$LQpcziuV%s_%cVd5+V3$1U8ZHm;B=5|FYIDE=?mYAE-D91P< zQ`lhQ;(dJ`^mx!6t{@|FuuVwH65S>#?HG?(hT-v5O-Zh+JIp5n0!FV{$#psMjxK^q zHEJPGk#N0SX3*IcH^(Q#I`$T(y&Dv)xIH2HZm>bep>L~DQ=X{{g4;(fecHv*xfTYp zzj}w**^Q^oWq8rDF+%uG{|R^0HQxq%zB&B4E z^)v0G$67OQkYZ}0*EGgm=6=cfku&x1ewJ&^#SMhP=0j;pAq_hLuV~~G2z>IFll<=0 z#eSb7)3+>eY=z#sH4lgd{Y}bdA@bZ*(Yo6zQ=4v13MMuZlwwmF(aVjwk7>iS2_YgJ zw`aujfepKClAl4zvp)SMvf) zA}|^s?=!|YdmEIBplU-fLz!5IzkWgrt%UqqMdi&?p5gQ=Q=p^4k?D~#rAJSm4E7vd zHN7%vh-Y-#@*~Jp-xq8M6T~W>9r*ri-EcqtJ(=jXycF zx}ofR?>rpsmZcrWzV%wdbK#2VG!rM}ScF4nOK8m+V^9xko`HC*H>Q8-RTR4eUbrB%eYUeA&mR5gy?} z^d_M(7R@O(kaF4&t4`30EteqWoCWrM8_E@Q`A8^0(@szfo#}0X&=F74B_QVp;A;Ht zhU!TE^2$m-P{m|R0QvhschpCUTd|?PM41NcQDLPpHPYk~vGEt^G}6n_bBN}2@28C= zKT*Cx*vefWA1Dqi!p_6vPpJ^#ds?eksII4StFaQtID)+ zzg+EQJY?h%#=_|C%f^JPWdKMths2KyvOZ7BOhvHz7F<2jL*Scp_P<<3g)zDRP!zy0 z4FzqXU>T(7;KmpS&*@8XN8@IZ(C=UEEF>z*1Qq^2@&%a4dFKV@=?$#+dJ;2mRd-?* zXlqoNbKm6W&ufVcXea`>>3t^>Ov4LCg9ff4RvBYrhpJO7>Xp!8@Wn0wQDXbt4bbA% zKU?#<^NBUJ<^r}OZj<`kq#Z|}7f2lPLpIbx*s+LA#B7Z#OG3@uH@mXR0H0nlH1oBL zm(Ek67@ckO)7-*Eao(QBw2N6k4qRHZk%=GIoM`oj5B=gHnT`c_#_2x?JNnnEH4p@W zQqMm$;aC@tRB%lGR(l_a=-TXBYP(n?3091cjXC+wz5idsSS>-R1BXcabvfC@& zxz*+-CjC5?SF4jiWcC_mp~oG3_f*X~<0fRuv3#@_Xc7YeqQi}78mxZ2(-}$t_#5{4 z-haBq%zi<~3S~_B1XO*{ItX(5nICQUG@xyPRtdW1)XeTU> zV+R)tw^@IGYY@NU6`)cP(6QvlBR$qCv1E6(#Hz%JfXHNgkmn8k z79*KE8g7fh6@w8VkqkmT_k(1DelPaas_3e%z;L7@M)e7l+=$baLKPDV`cZ*~D_rag z;kuu7M=BNrYES_G8{0tKkE|>u2;(=~xh+f5IZ+F zGkm-W<$H>>LH44W8k`m#(%v8bH0xQ{P^bcc!c<63C)zTOX~^}L8u9IDI|9H@9RX|# z$b4hK)D6b~0cCzP0!Y?O(w7uWyj||jjG_+1GVtM*hbg%%_yV8tHF7&9HDS~QK*c+A zjX~=gttcpumX3F^;sxRbK~Vsq#}9jZ`6w)LrO9J1=z+J(-}>@J^QG!4NHQ?cIw?Y^ z2(#Ux0)j|9ro%A9#H`smvwk5a^~gH866lrUhBJqr?(}Mi<&2dJA^rgXPAJD`NJL8A zScfN3;G@=r$Obtj7^8YR>0*y6>H|pP6Oa5A#-leP49ISs`RcHesOV+>rnM!fx3(Mz z=}<}Xg#6y50BFWz?WFCkkGql=W->M33F(f}UfbpTeiLxgSGiJCS!gtzSJay(7lJ>rf<{D0zTiy<$Zi%9XktI-#dKHHh;_J9l&#~L#>eIykePBtSVpvDfB>($ijU+yvdXa9Sifdbg)arI5ts8YkVGfar&?6gEuVc$1icyX; z;{$`3`fE1mcKqCvxqh8etY)QgRMw%yKc!A1v3)K-8(m9=TGAa;T_+Fj*6rvn*497J z+cLgc6WU+5zEAygPbHJ)Xa`YkE)Wos?}((hSX|V6#sg33L>8-)FYqzCgnTX3pA#BptZbgqe8g#@&P6kNUS z-~>-!gt?K?-QJZri!fYBJ^e!Jj~)3-8NQW?jh#PReDTPUC3If!@dajNWSnU$Cos%X zsF{C@E6rj2+gALpi*G+Ox?L#IdBV4^ao?g_cF{W)VLAi!VRs;fwH*3$0$_jFYS4mB0cKPE-(M3+gFr0+gq_vkUSpdg78CQ5Kg`g+=wN$c;vtOnZ^8iR^1D6^`Ut}U3(bc1$L~wV=-|f~PQ4gK&{be|jD3)#I%9S|j6!34!7mjx< zKni1wS8BR>%De%XcLszLfeTi)094V4tf3mEoKOrPoKD;9?Pc+gx4@1`N}OCIhEgI5 zm>T*fSV6MtU3J!LXL#U+lEI5c%#EdX#qI@cLs>-y$fpRoU<;e1+l2Jrg^EDFJK-0& ze+Nt-xjNak^yqCUYh)b*cGKYk^Dt(M>SMWuPY-_HoWTmudb*DF|g zYBNPALvj%My|{M6*exQ@>9RqAUZp+Beh#)oiZh1}cvQ9^8F#_&)!UWbaiZi* zjhUc^qG@B4oYNj3K?;6KbB0n_@gGA(!H`}Ukr_S(CwZ6*;$Wxf!EJ1WVq`1XBY_+S zN8F^oqCbf7?vi*OC{ZP+_za6M8EaZUM_q*=N|r58ug4Eb82`%?!|?{}sP^CxEG^}Z zAJglc)~W{b7Nlpg6h76H8Vmysb{YH~Ie4j-o;TqV&~Q~FVFy4OEj?g~ezTx@5G_LD;Q`EGMzBsHo<_ z4CsnEI=+1TwH&-35?t^hAC*+S?mgmoQ8!4=Hdwvnih0dE`N{`2hu@=MC+>Z6xS08d*x(zjB~I7YhkfX{jt|9^%>QM4fMOJnfjyoJAwuLJ~~ zRq0?Ulxjsg^EFxuOh-q)>H3_L%bP+ls# zkjJpmd_9;}4qHiFZ*+h8oA%YDspE6^w@sQ8nTJtf3mz2IhjgU`JxH3*9e~U0I;rwK2WMR&LV^82?46H<;LwltQ zzMz8%qykE@u^g-{U9X%&=yp;?$)} zm)PMz1Of%tVDNxQcBZpIX3Ito)R@sk;ZjE(`n`Vz?SNxsfky3zqy=VX=DWh~+9^uH zENY(@-Q5?)Cuuf(d9 z&H!6OzE!aU(Q%g!CLD-?hh{ql1;Ku@yFzq-xD+EE?j4ewg^Wca)K}q=f61kayZ;K0 zD2Q)#X~acxx@aJet+V7apOx@OA!rLV8@Z(3pS3-$4qYQUDa9XKtWnnj8LB|+pLnFq zb%FQ2-7%q$i=nMX1BC=m_1nkhLUcT|-S2fez&c7z94cld%L7%>aKTMHX>nkT17x}z z>oXtU7cH3`u5i0`F0`x|@hfOFA7)D!U0s%3-fsIMS2f3^tf;(W5JD=*BVOe0PFg&w zc}i zBjq?tR&5a02R2;1nz3==82Lsr-e3^EQO^$w|-3}kad9w|);>o+U&uz`mnfM1ap zH)AJG0<;SuX$VI8IN7l-T)6bNw8PRZopJKt)JV=A6ia zOLSxy+dGF3e7OUX9qnoByE|8(HNBN}H1SB(hcRoGDj#285pi*&Z8?Tmc$On4eGiNU zqA)^*2Vi9z1T!+6$lD%$8;e{kI&61uky`8MJD2^~U%}|(qjMwEoH>GcEM|GWZc_|3 zd6ev0S2TFm{Be57_-Ti~n&RN=qifQ1`~f3{<$2{1a=R(~p`abg&t@lE7225G&X z7|OZ}GY_tbf-KSpkXgZx!KM7=t7kdq=u&S4(g_Lr8&p7gFlx_s%E9ALEnb9|LIqC~ zAbMf&!KF9YR`QWso>*#VRcV}ouFfz(1vkxYqirT*l|ge})L{z*CiedZG_=)-+S;a3 z_o`jXTxgX~@2B3o@ ziy?pGVqnKgBhCVVKvtrWLyG%i>nySVv;YC2fyL)BGRZZ;QIssXFW=EQyJt`Ak9RfK zBO^a``Lg&d!-=Zv(mR)K4C{CF0Ds8dV&q z6$SB8l?TMU04_@sN&^>K;wpgNh_(pxQKWDKQa>XHnJY}}Kx=iRP#%DF4?+-VvWz1R zpHU$2?Gxk30b=vbmo}Q3Bm%zo`zZ^ufI&{U{EN+^xe%~yp(+z#oSUnO1NVZIGO`wR z0dp5}!RvYVFm%FD%(x&9jEYS-@KBv%XKqBa+KyxWHiCmkU;f~%?TRj2Jb8VphO;=T z;obGsho#Pc;zp@+8}CU8;=iJY2k-ab+8IFHg6swd3e3R6yxFnm=iSI;JYY075jUC` zSXdQnY|HoK3EY7$10;}6rxSo@ambQW6cSoC50aCzk$sIyC2V>$O`C>M=j{KzxF}el zh{l-{rT=Hkrb{EW-EO`la6%8Htr|J6dC)JAXHRBI8zwl?g(EBvY&VconQh0D^fE_0 z0QcKi;@!&dUDQMYt|y}Ixxi5l^3`UnMq7T5u_mM}a!bT8^0n-&3zOEJxj|DW^) zxr3I#@GOO!Y)1Re$yc6)^&X;Os*PXY&=Z23>@^G(RE5HO$p|Oqea0-N*~n+wm6)+z zohEESyRh0g^@qlGT{X)!NtJ6jjt|~I6{tG#`WepEzdly@YY^xQ-U|N<#|<(L#$iaN zoEr-nUU;Y5-R_rGwp>)Z3rE9T1TG*O0iW?T^frLIfWy4o)CeNW0V3lM=;)QaMX2IX zqV4gmxr1{_F3f)GRgP^t=OQL%+x0=qfr5cB%r(7vxTn1F0ucRu?-G)Db6RgY-@3T) z+-$8`>ejQ>oh_Cw*mcWaLPqAcI;qGAMg9J*urp*iFE2!42pU=h{h?JC903HUUUUMoa|5PV5tI4Z5C42+x(y>Vl1sM_1~_pap6ypa0xquWR5u8ftHflE<@ z@lP%>uV0Ctxkc{0!c=tf6vl+(N=n0;7s8^FF#dG!1O3T4D0#w2d6!;mznt8okPcbn zuqVK`83YUDVR#+PpcVlbRv(Vn!$^hy^aps8{brILYdhV$j)x@+YJ;`lXWW!wkX(1F zEDaHKXPS}&Oz_5MdY9;mSvJ4pfFwb$z3c(S@ZtzbPBcfkj`Rv{C7g!(Y31DdgxMHQ|H{8{?eLJl2=-c0H>L+3A&mMr_0A5YBD%clz ze@!5trv#S>Fd{JQSNGs?{>B)wzuHs=G&N9;>!*Y>T|4v_-P%EpgH_CMx247KRZ zL|w|qK6a2}8^n$nOU;Y$;_;8^`_a2WT~P1Rzla!e5Go|mhUl5(YBZ@2I+PTTd3f}9 z|KBt-6Bn)prK`ZLL-HCF6CiMfxUSNJ%O>;?)jr!VCmI@r77bP@K2Qa}TvZ6J6};|b z&4y=^KG$y-w@Q}4(n%R5_#E~gS|r5}=rJJ4U|pD+C?WMli*^A7;}1Z6gTa_*=gOsP zIXBY|xfL$DLm*8tw)RnU%-ReDce}KXKT}pvSb+=V)w5`bugqc-Hw#K7XrS9g1G;L( zR(9l}90sS2+7FunK6egdx*JeDdPw>0wr}9oYq7>oI5yy)%?n8cu0MS&@TMk?rua_a zTd?p@;-L>=RDT4%R%{8l`T!#X$}0hj;TV{9uW?&*!cb)_Cr*+e0}CqzbaYBgjh47K zqK{fYdWkcgVsjX%(9}=raQ6rTfHt~VqSOJ=XctdTd9DGKdj{z2JsjQ?yvK%)fleioR1ri zD~p=p5+GD?=7|jnMNwO4{j{=Z9Xm8jxp*~WPe#dX<>IZ<6Kw9r7Sz14L|2yyuJ-N! zN>DY$F%4k_4)S?$GpS(R)URSSmIQxBgc2SD^339IM`exQy^ij#AO$Lo$Hm~Q26Ue& zK$vMF608=mLcZ=)-?Ae|j_8dIOqbL}`N|!QpKfwbZ;wtjdztRZWgL61e8B_F?OatH z=OY(*U1AXtzjWo}<_(_$8kGcWm#}l6S#smxYqxVeJ9k}O_~6D9&6|+sy6!JKZ!y$7 z?3-(|YR|HhM$Rv5k~^G|Y^WFT##26PmD)5+e9XsrGf{l&M zAxq-MUgvXWK54DG5U7Cakc+UPlNX^$S%c@KGlNflIO-+a{^bRN^0PNela+;P<&dqOBfcxBR;Piw1x_=uo|C(v7uzSc;`d zRPsnq13ehkK5PP?Tm6JCheW?gy?M`Y?Yeby58wMSxH`siua~g`nF&k&(JDrVSnoZY7t^5D$1lVk7A)c>t2G|4JFOG})2q ztCp_hZh?uZM4b>gCQ}qv2&FDipae`f9X-sM=fM9~rdtRQLWby-%15Zmz++RM(Zd6m z1~$X8M=FqLMdu9G2d4q$q%nJzML=nVFDu`@N%b6ufDaDtq+O8q&eP(FyxBZ5Y{XB(WI?KFhXdD=1Xmmn>e&sm12T% zB{U^(nCm0O0)3LW>mXB_Rl~rjans z&3EBxLkcW9N#=IH@q3(1br%RJ?s8~3Fb3j_a2FMx7%|{nZtQ`@&} zZ8mBlGFQ@;1yNK|zEIr9gE`yq?4*8o-g5hPJ)R@^I4GB3?j@&J^=)vp4V|5oJ{PcJ z3#W)`z-vDa5h<6E9?9G9Ok^Xlc_UA!_S!uRRowB{OBSmMwKnVC2hXZJ#5}ioKxmP^ z^WnNw{1+fs7`I$kR9^pzy>aSEu#SJvZ5-YB8lUa)u<2I-Ngr8rqk1sf+u-y?XXhOf zTBh~TKn-%GJF)98^vuDk!x#H-^p9S~F;Y0CCpN(YF zI=J5shCe`8vOu_xZwtSb8R&~6wvp^*?cjZ9)ReHRw8}5}jLe%x5G#~c0YN(D;bB)K zh8ONgN+yH5K{bFoZ%^60Rok~bpKi9V)@q0B4DCUJ!ihQSwV6 z@Jnc3d^v=x^rudl4%gMWlzvX-Cl!)x3Ox)JLL4g0qtlr|ioMz|hk7>@Eqa|?#0oak zyluXAkt~39>Iuyi*m1U$W zT)FZL;>)R7fd~z1R&aAWek?f+3K8&C2$FM<3Oisb^w1rjpl&ZEoP#>>(G>r4y<#tO z`>Ak)YLv2Ms(Rgb}A(6=buXq=+PqcbnD5 zuS^>Mk<_$PA-(pGEVz^FFaGg0?yO8o9URiB|MG?5YJqim+wesF%c_^b!EBBS{Fqbn zDm%NOrKR9=4B9!MWWbE{^!6e-AiJfhGH69^-Ji-Ais{BC?VX*)LERYENZ%tLMcRnt z5}i}knMvug=O3r$?sHadimpakJZ77>0IS`c%)&H_5CS|%Wgck-`gi-`lpxh8q>8RX zn>bZ6V0pxD0T1-tOzj|=D^gK$AEXsI?~{`)HXYav_zM`06$r6MRU{vf$R8(c?D#e8 zTwFc?jX^r+>d(vY&g4NNXzA^1&)F&GK5|hjq2npD(B%OJ8MmSD5S24bcdUhW#I#Ey zKIRDY9_OE@c=O+`-mpsmCK6lWekf8Equ z`4QyUpH5sgAaZASU0q$~IhW2Ch}@pquM|`NWQ%QhW4L;(dTaLK#3RI!Ur7BkUO2HR zTsR79AE?$Zq~4iyelh6SS@6mNm6L03_^|&)cjUi~M{n#Fc<%N*$GvP&d%EsZ8~e8B zC3ibI!>8AlRA;&sDq6Iitohb&|7n_^VQ4D@1M*(06_D|=FsruikA0NqXyiui>1pkS zxf@0qd1iTfD5k%6VdDNYyl7dl`$mm~=imDC07d^=zVspvC7!Lgoy+L8h3kCZA*a@) z>w6!Vual7(NGn`^Ejn;09jne67OW;*NlNC$1_a6)l&TkMIP`>~tliSeF-fe~S9yZb z0fqv!e2{BUVyC9KU^1s(3A{Un0poxyg6IdQo}c+e9aPJ3nH#{TM9yG57Zj0%N56c_ zKT1aE8j}bD#WF@i0AZ$^Vu?Un{s#H=8LZv|XFfSreYehcHrEngVp_j6-$q>`Z1$s6 z?H=vm;PjJEh#y3O#gEa{;5c7y%GCZ`CyT8YOoF-D&)1&ZQhE&N@=Q!lZrvn{7&e++ zJ);t0M*}P+F|*?|#S6@6A_F!xF*+D#KYI{`+OaoQ>zvNV&fyc?M9*qQ|)R zL0^38FZfH)IE4PXrb~dGodGNk(I?pSQ4Nx2D@KP}FRbDrCXnL9()QgU zuLu1r#`Wbx?)J`3{Pb_A7Qz*sW&iEKAD&%TOR%Q~&HHl?0Cb^hGBnYaa11%GaK6l`Lf+n@pF8tWD# z6nP*Z3c}A=8LvEys90`U)8h$>NobEj%z^t)%4NVNn%;t901#IYT~PVXOopFmJ{JM` zA~?Uc?LsYwiU%42G`<}OmCAV(rkSjXJ8;E(SoyAi(KP@Z$Ax~m9b$F@iZE8(>Cj{r zO1}%R1O)i5q=Y07P`*H>2vMF#sL1YTHYdr@84~i^F5~imcx|Zid-L=rOm*Ce`3abW zTzRf!$Ji|_s`->#2l)RhYmy768VYG)w7zJ@Kt%(4S&O)?Aa?7M9odncPp<4|7Mct< znP-eDo1&K>U1ZpHT zJ6a%CR@xeSdC%Afne#nIP3BrFShS7iytL-q*>Y#%o6RX_$*TqWj?T^w{aNMi@T(vp ziqhQvO`}GmyE?o?rpF{Uo^dwQQ-90bg{BmlxZQd(0EtT2L9ZH0zaKtX{k^iX^6?jZ zqbEfO3}wlSioP|=e$W9{!MuI^B*0ntM5SH(sOz#7D{3U>KWnpo?ZG%OeE@Gy9R%tr zp*^kXpHpAQ8K&&1coV~-teh+BKKF`3elL?(^rmXa6QwLtcPjCyufHr_xEU}MI}@BAt+80f3db? zDj;n^t<*XNg~%}f50}}?ctMTg4)c;@r6SgLD*_#XP#VZhg?^YHs!9e16v9DhI|MA3 zV}+U*E#lL>yPX$U;(W;3f7v{-{{3@7EwtPBdx$FD$gHOYFR4(s4~DT!l#bC zjgkkxyj!sru^-R8_o8;eax5O|MGQS9EK=wwni3QR0q^cX4hEI6Exa}Rvl7zMrff5i>Z2eqp7omFpOmH!XY)vh#d8sFhbtyLf-6l4pkA&O-56JiRK_?Sm36VujGUpDADFP6rDU%gRrwuBQ zlS_6pR7qGpG_(rVrF`^hs#>0iHkq25QnMIaqz^q5*fHk@7a`+R9w9P5U^Sc2#QI&@ zHq0zk>$@dw5pcDjppxk_jG?9->q4{99i6L$ajdT4{$`Oi1et48M% z*A^dtRk+SIKX5H)ngerilw2)%Bl+amTAWYSn8V_0(BIcL3uHh9uoAA_&+cwER6#?I zH@4WHyR%c6F{Wf51{Pvz04o7Y;R@pm6vM!A&VvdI@zbW(K`(&cHtu?_qN6I8=Xqe} z7{VPZi23S}Tl8SHh~oxy9RP2X;>elY_Ki@Q23H&;Hu;mDW^N~|n`&G1hqV{7+!u2$FitgPoNR&x5E z(#`IW@2^WulSJ2YXfv%d(^_-Gyy=MNO`GK}Zqy?ofESBuMYet!$BW!u%tEJ=!APK*COHsJ3P5P0 zNJ%1ze*Ra1AB^yDQfekKPGnw3bvQ7U>fu(u)we)D4-~T%+H`PhpTSmHJ2n9)CUJu> ztzf*A2O0ARe}98_q4WD@wF=0yxi_3;M*LS<$5rI3!o~!n?EFk^)>zGVcAGCh)$YHL zDpr3V1mO4K)*k|8#{`(4XWzfbe{{`K^O_7#u#o^A%YjFjsB?uuGN|ii`H^olGBR@P zl;L2M{q_8Y*GR;o0z(ATcd9FHG9SLpuloF}X@VkZ%{eps2|83gLNM|<#C=UFV{z;fp2$ut9F6+!lzU3dk?!Wf2#oWNS#X7S(QUUK5 zyfo}7BFK0EvtX)o{Jg}vjpk{}Hu6zOCZrrAtG?zvl*EI5#2d{WP4TxtgMma8Ja|C4 z;`r6A@@G}l)*Hx1Aa=j=Sj^@3$}VsK$%=)WoYvu3#r2;SfU`t3wVAnn1xA2CLj>sm z2K>W($do8E2M7|4@Tqkz$3-T0VEe+6Z#g+;$HFV~@MDa{w||aOS}fSctclE%{iLHM zb_+$c7c{Lt&o?_Q#?K+4IvLNX?E~%0%rsb~F>xBK0PNVDh694cWF(>_pCL{J=vb-i z2Gzw+d4;kA>i}IKXv{+>n6pdt6@VFd`}uhRP?#TIT}*md=h zK^7S}{JR@jKQyeBE(!if(XIa3T142o(+Z)Pau6jZVMwF4Bw3qMFv|jcB)vH$uRKo*IwxdDBw21dI8vQh(j{XG3Hj= zMN8StHUxL@Ah14%rd2R^Fa(1Wej`DTcA9zP9lfCq>qrWFk#l1EOQm=C0Z zyM@>@OFmYixW$lU;k+cL9)U_Q-DI@^@q)x?VDGeZXU}T#E0-nP?Vcjo+=%Evy61>Q zodJ`}0N}rT`J&M-(4K5N}b zXysH<%esxWEQ>@+6pn+sx;hBpZ967N5MpuX)v<)~Sj6h$Z}2=~niraZI37&u64-ReGcRl|F6bVvUit5iIA3hXEgF0*F z$1~TWO7&jkAk_vkk7xCHNr9s|5a(M~cdbTC_E6&2C*HokJK#itor^+gSy}B**{gbP z0L=|11$y!4{=xqbvJw+c^X=qfMi=>Cy-K2D1UZoN3tJ3Kc`5Iw#1AgPwQ>bdfOn2M zW#^6^JD$fMSrcJqvrc6J11=YOqjo0vT{9facy-&@R<-f+xQ%e)W2w%qU7hI z*hSNu7(AfWBIr=ipvkv;D1l?2D+S|l?Y8P*wQfhDjbIuEhMb(7ki8FzgS5g8Zek5y zDrvKsxT3Z@lj~>pi4>mr)FYRd8R__?r#5a(dUC(O5DY~E^f(11ivwDX#4{49le`SzQ(LUTOu_<|{_jAOuF z59!o)2xLI4h9bGBTgB?G(wtC|rc%y{14z*x+fnJg@+(6GbY7 z8~ZCCe7gV*F+@i4ul9qaLTZOu77u_G5C}d-Mn}>9&8SKP@db|{75ymXaDYIfg&zWv z2m2kg*?$bK>=Yj2b_e@GEQ!i;uo0{@JC-g&Su-xR^*ROB+$2WA~ zr{Nx8e8rb`TQi+x+(OxiQxghK`Z~Tlg}1}%356=K?5CuCqMYb53PH(1{9h*&6tg@T zfa<8@@4xepN#~Q86Rp#5w<4=6b5ra1{^Sa|>ee6dhK?(L=fpC=43nWO4e=$<0!Qom zYg$L6R^t`5wN(^%VxZn-Nv?6;YxZH@xZm`;h~k{ z#?(A4>ndqGM5|$`W6^?0!`HZGa<1zyD)18tjXC4kWMz}NgNq16BMbyz+`2_$Yqts@ zg$1r!& zgyDrk&DM6-9pQ*H;ffFi^dT_N`%+jFo}I&}Kj$;g-*!EAJ{vmKD($@Qg^e#I4Gav%i?!Dw z*%2-k+RQ0M5rzipjU(+6^Fawr(vePXPO8-T#8uRMP1dA+XBLVoF_NJyPh4bbPNS>? z*13v{3?%Td+9oip&X~eGAP&TGt5{bu3|`6r%qswM^NUp#V5oYKGTZpzmJXTa-=0{J zeGsWIO4AcIrbwa#=Aljp8c03ZGN4HIqfYobAO`}IGzklS+5ebI1XYaf&t+_gMgtfc zEK&rc#<1kmh#<^F5jWrvNoE|#A?o@ax`SRczZ$`Bf>&%pTN$ysGjs!Th`mr`QQ8y` zY`ku(RCgeF#uSJfDGRPien}Z436K#k_QZTESGReL>k!XWkFT;`!y9iJ;%+S9X@CQf zybs@#b%V)h1#tF+VysAXiR+{r^U52T2lW#0HC& z3hAKD(#->h%{iQrx6O&mWE4ER=2qLCU2lm@vi#shn^gJKFFbMBUi@~_9#Ae zW!U|=?0x5_qB^c!RH&HfR}4fbCR`^JMuDy$F`lWA-62Qx3?3N1oaBy+(-z^%PHR)uL4cCr_RIXK%16)hq8*HuR)7$ zvqRU4!7YPlAZ2)i8;wn6b%m^uYdFq+C`eI#5CjL#gaF#Js0pb%^8qqUk=?)&SS>WE zs?~M?nUH{dZy^#$!Qm`+3X^9L*D&=Te#zDVme!NvPDR(?FCdMTOExGYwQ4R6->Rp%6_dDix9jlBofO z@P013pZi_!dfsPQ=V_fg&fo9%9rm@az4v9%;PAEN;JZ`Sdf=wAH+DjI=7Ow`y9wp$ zjXY*Z`Ck_Jcw%MhqW;I)bQ)1}wQ;+(Yq8tK*w&1Hnt7*|1aH4udGz55UHn{9yJEPu zc{r2{?_FwA`@^j~77fmW;fEl|`E~R_;!DF8*~dbHi!<_Tz|Db^ZO)m7pqH_JsL&AA zAs02;MITz91YsaLwpdIEY<>ao6jb=yWwZWXCdI(skjX;w#+2$G9Hx^9#RZB=hs8}e zMf$~I#^#qBjYFT1#?8$ayk~kycnBR~HxrHAB%qOWlF?6MdHcYXk{`R9(y3-zr%U6O zBc~_Ae&8p{ogW(=45t`We^+H?C4$o!H9x3Kf>}5M>)IP{9lO(BPNO7K1TC|^e*gaE$*pY%ObCTq&H%tyH~wh~O{K=n zZ4rV@HZaKUu0}b_e@sbwB}ytI6}Xxe+YheCchlMwhy{hVt-7@;e|aL~^1}BKMwK+I zi$kMkg*|Sqf$^|I;`2=gCejk1_T&tWt7%d*!&Mhe$)hmLYP4W;H#WpWF=)_%0WA`* z+Qx-Q+0(J352TJp-zUFXr(W;R1akp=YfYORyY(ER%r`c+HLN8?bbA?vCxh*%+NAbn zTY3&#+XjDPnqP}>E6EPT#w8semPNjV_2swoMMR~js@aO;dbtN3sDi0)ZHU#C0eUhW z5;TCObP)BXn8SYL)O2z#jnwK=*=Xas3I>l@sIW((*kx1@{%%Qgdbg64NhDj?w#0QG z^!kLhl6(Bx;pPz${=Pl$_VAj40F~do#z;bwo}unwALeo3U=Q_&)j|%yEqu!wB`-^T z|HQh^_nv+^HK4)9t3V%RW#w%^Vffm5_3DY}gE%|_vjA0mal2D_(NLoJTIaJe+mc>4 zBrrNk>E(V*j5r8if?C_q9|`wFnl1D}uue1UB&`Ho0!YfQAY$ot)PB)@I1-htZJXMG zzLNMByB_4NC_}cc%T)Mr3sPGbmt$)tv&DlPH1szX zoL;^5WS-3q-DmG@Z>%~!ZIWTFDRHFTByQ+YtSuRqe9nCgp4vOxM!dB-Q8Zn(XJY7Z95f zYpOkN2An-xN5(A*BxJNxQ@O(+z~ZeJ!bREl?w~WxzV5|L6RbTXbf0^6cTMmOha{ec znDH=>8TzlR@Y+AITex}9YuuTy(}39Uj>2*l*xahj386x{mzH+Y_`97J-Ibt(Tq~#e z56D~5m0e3sE3o0OL9NrtxbflhzKP4jyn6N7M%{Z6d}jV8yVL@TC-Wam>(u7997$-_7Ks7n zxDar7vqajnYuD1iyndCwMn07T&^TxzVi{@mSsD6Cf_N~x%GG?JP6mr zo)7$S-ib0zxFMRQ8&wT_5dp=c2f0SJMu1+I^NSH$(p9En^Fbsn={)$>SrMpQsUXs# z9u=>(&u*Um&2C-Q(p0Tz zUYgvelYS|@n7R!VX)_f0rxz+SoK!xSD z&C?@a9@Ee8jhyKB;yryw$zGzJvYR^;ii|hI{3-34ctqeO9MBgJ?po6|N97;M{x}4vNaHXb_M37mxp%QVKopFQk78c&N zUP+xG#+*Tsk(7H-@aF?YS^?Ll2oBkVRzE`=Q6) zK45e?edfC)cU#h56>+Hax+Q0T;mMY&YF%z!pq!*T)I;3pLOMWUNPk^zbH0Y6nwDqY z*U6oJ5?5u)sCNA5PGdWR#^!rr-bC#a#|n|Y?UhjmaBZSV{mvGSrEzKnUX z^Qckn#pBB=y{+UMUk-F`O4v-ls*4dXAZKb47hu{lhG#zr4oOQm7El(oe${J}fY8|W zA?q$jE}nF(;NgX8f#?iCi$zyb?ofFntt2(7N-|GxV^j_|$6q6C2AWC9fQ7xL9-Dyj zQWBH_v+8cQ{}H7aSVZP@ZTSA%u{)m>2b7l~s^sB9xBwlQ!=;PpA?wOsDW$}aD$K_F z(1FCSJQ$JT?_hAuo4&UgnWUt*Q}N!3VQasBe>_k~Amg3i>$lLI92oreaqbB(XJy^; zkx*#RBJZ#!(RCLo4Leea`WW%ODf;4e2#OmhsWhTpIsEu68II@Y_xb{+^!ujEI*lMy zX8MX&wIQoE?HR3wfrn!Zwy^X>J)I|=Pq~F)YFg?3X_VbX|Ch-H;PA}}6%)-*Pra~4 zW0nCkN{N+tHC|lz1A0CpA#bTp%=w?}90`!;I_F5CPMx)6$=_f>1brMkx4O>u4!_Aa zV1ZuVvfGe{2V-JlGFtQ;a{pY|^5j5pE8lmre;|W39lr#j6G0=Gaw>|v0y%LWdl&8PF z>E*d^%q$OwY%1M)yJxuh>dkt3&Wleg$5>Zau5{?6zjxoh^UD|LHVUoJdlnlbl|!h~ zg*jY!Alje`ZSGyRy@U>u)UumEwvbVMo|#?;CJ*nI{O(N--J$ya^B=!;&2?T?eoCw9 zaETpMS9fdjqD8V0cajS&@)9@$CWh3Xpk=7^ym_yESOVhdOIROzOmpwpi}RN=0dl; zea#rhx+uwjS4d3xLk+^(Mn!Ft0TlInA@ij8193jUGX4;l8t#AVer9GTXe)pfE|nE6 zwL^P4n1=k{gM0g7Ptwm3MX9qoSwDdCm{$jLXW9nKxSe)X0GEZ4@WDsoWzBalfdp5Sj>D#Yw!4k~w+g95z_?tB~+L*P82=tHXLo z=-SYs1l=Aqwp9&Go&7^4ur1)SHo77vEv+Mz6G*{X;;akwS$A~-A zWW>*zvj&u~asn$&G^zC=VCkN7IhAf98<=>Rv~|NlkbD6;>8Rtx zYLjCWPM6s<$r$KKsMJf2#_X_LTJ@#F2iZC3zXQJ4o6)&Jp$3q8oA{$Jzv-a3eg9E#RjJNWjnE?&r~ zpB4O$tW6XF6~P9kHN1CqF)jE=>635-{&+TF;=~(yK=gd;Cz=l4h7*O6+_0I3olGXk zLajjOiwPI^fkOuwn8)C$ncl8xE0~CdD>+MSTYAi+lF0NUYXen=#5ct}QTY_v@hGd|7^&MtH>~meU_KWo@bkypYJV z`EbNjpt&zCk4)w?6ncT1@>j0!X$G4#W)G2}CEF9x((N4Y&AsD$b%}7vxgMBz-4Suu zXy04@C-ywKKBmk03P7{ucuKz;{7sQ6VjAHt7hXG)&_TiF=KU;-&NSR4lAn;w46~%o zWcoYDe^`e2FQfF7e;K<)=C=H2-uI*&2Xi{@s-=Y)d&S%X{uvK52F7rj)X72S5gWNq zdSKRvshg;;_K0c$A~Y7zUlYOPHQM95>ml`nfU}PuSI7wNyqSj9NX=!&5~oc(Ef{;N z#Gblu;PcP1l|YYHX(pf&!Dul4h=UrAxMy95934V>-wBe8iqi46d*4_DD=cCo*NNu$ zgmm?Y`dy!H-*em4y-clCTo7XTA1y#PL`#VHorqkNQ977)P-cgJ3do&K{UvBV<577xg*$7fVOlRW%W4k|Y<8~nGR;!+O4`A~aTYluwl&%MHY5T@ zMW|ajf2b_tNOJu4e9nwzu)+~OoQm!`ad;LdZTXQfg3Hw~DDU{GzqDWAYQpd61}`0y zCaVeS=Wi}FvC6wXv-ZbTw~XN)GP6;PM_E?KXN)PUM?6$-vA6KtHmD{}7XU7ZjTRR; zS5T=vh(QvbbkEM5X>eP8FyXJd4B1K(crS`0d1P1E;woH^)TTaOa@K5 zlw4ATUR7L~d*$SqiJLV>#g=Uu!%2w9>2;VDR!-U8leA%8Z< zOK^RoPTk-TxKAn_6;I?kJ^|X4 zI(qRW&(&;{BSoi+8+#qq$ra4`;DJTjeCBmE?>2EMDL}ON%gnULbQuterCUUS=n*CJ z7Rq_;|KcHfONmM?IY`fF;fEPc!!52-UV9bbh|yKP{aH@76sI%3U8a9rVfxwT^bgIApd zXfzBwQ9KvM0HLn=?hT$coOU$j6+~7=W6Mzg$Pl})OHT}X(G)W(_~MV-0O`$irJOMT zkdR}QI!lqo$s`n1l9g2}JIs0V(0Z}$j8-E~CIH)QrrGC*nxHgXnR;@{wsgaB>sERJ zT#No8iF@{Cd_{Y~!jy0!!8a31Y@MX%r?z)cLdmaK>&5!|`ca*JO}EzpZ4hDl&Mz=h zYOO?X?Y&(6a#rz3@57NId`Z8cVb|uXg;{_@1Mm6+w3;pk3H|)+=oC5CH)U8}_1FBm z-_o;RS2tHyzT0N{GPe<)kDovd&II8HRjOeCIJbaDUO9(MKnV^*I2I0%Z8{Bx-5g&Pfjb+~#Z4heoMC+67efiO4$Dpi9JYx)Cf= z32m0s7J+Z96fl%j-YMNjK_QAmPTCtX^N#u(37!lPuLlqFZfm@rzGdB&i{{U7;2Lw! z`_{51?FT>ZqPO#!`?y9~FNp?mLN^j-77ptU;hdi6k2v%_))qntz7#4?8F&*-sm!pG zGnn244m_6wYFO6=oYpXhP*vAFtTk2PIW{kXLS%6m&-Z&lKbFQde$_Q4nHBa*6rCN6|;8<054>c#QuA5ZLO zOxE=!`F6hFVl(h7&o$x1)C-V(t4rePS2=c`5r?7KxSS#Mq}Zp@ zp}%b|MHKlyb+!$-LS!>Q<-oy%buQ0+>mWT<1vMvcln z-g#!msdLPIXJ6L*Fkq=h|pjn?5Ig)y!AyVSCg z;_srA;3gE4vY;QgXEPSQ}&p=e*<1J zn4pd*;3a)gA}zt)6YT7M4Tzdiqt0~MOb(z|#o{@0>OyrOu&`MK)Z+hRxFj=JK>_6g z%k)JlWxpA@ zPMI9_a=zCgv|y#LXae58e_{_yoFtCPe`Bov#%zYpe*}`)k7OVwDWzb7*$$JF`^do~ zjDA~nqq`iP;RTy?e!Z(JgLCP{Xq%q4lQF8$lK{V+i0YDmrKBC9T`IF>f|ABK6OqN; zBd;AEV#k-g2TT(F%YaH85SPnx2CIq`idTDE$-THaETHHUBvBJ_06kyBxj6i>3&_}r zUkGNKw`vt@J=rBaL~z&)t#9=tDhY8)G{PmSJ`Vjjm|Y8;rqpNwF?Y-;v5W5fP$Kt8 z5eyG-SK>RnJGgBQWo~FXmIplhawLW-x!%Mf;DfPTgbK*!#ZZm~=PohM@YA#mL>dfT z&Vq`wzCB=rrkB4ODc3evnHwqI7xEcIOl-Z;>by2UM1n`~gS`WVC&b+pj7S9!zn;@O z+D-UW6UVZDBtli(KCcU8$Svk@SGl!5ITiVNek#CCv^_X^zGn!#$A9l1gxhsie+@5NbK2m6F-ckPtHza*;-hm`*0j+w_ zMg*kF5HXo^!M{-#{VTW-wSTh?kDu-26LcLmEI2LcaB*8Yxg2BCk3i#+U_H{oZEc}V z(uK8dZ9!IW&hP0c>7x6S2}Fq6jnD;1+>@xkxDe-el463oJi{{d<*Lj6z~W3o%6!}D{qy7AkYW$Usw^YmI`sjcmiZ; zc05X&E{znV6$&oEN1xg2$MhD3qr40#j71>)>+zDRXgvPMNJfKH!b|np)3taJOG+|& zqzz271!6-3tCHEgvAxim!W%V*TkJh>;3WDw($|1(I>)pwuCoDc5*rf=iMwBV>CKzp zYg4k+kl&j!Ve`jWVhWX@bg0#UzJf>T&ymPTCo11MAWiG=rUnff+^C-$6CEv)HF#ik zOT6pnA20FNe08{3i)1(mNb@O{fr#}uHmT(aE~}$Jp(To9B$zf2uQJYWmhY;8S%!9( zFZ`wdWVX6Tc#pp3exDEJ4L`0QSJr(j{YC_%x6aRavbG;hb3~aZ&nLq0af;>VSxn#Z zl3zQlU%B$sb& zG64F<;;D^uO}O}oBacH9yYkn;O4 zxDf293PpnAvSY_G#782A6qtRl~aefdeoQL?OK&6Wz^dbJr}o3PK{s;WkH9pIyBae?$}-L*tM%e z%^XUtZ?P?35i0Gy<@*%&h>KMn#EmeteEHAM3wi$U(EJi8$y$7YP{$Y%2e86_OeDVv zl&h1ob1jA-N*?XvD{Erp(KX_YFln=Uyzp87Kt6(V?@zQJh5d_*Cq!dMs%MQr6?H)1OeZ7xib;GKM zFOR8DN$D{??0mJA1_N-`t6}ZmvESvM6n++2P++??(KX9yaSt!ud~kmIVGdar{YlhuQxQ-k5zV zN8`AR;FBl}!avY){el>KhSOaEE8sY!7a{=`r)KP~=Iu1CdON5tru74;d*-knZb?cm z8IFc1r6jjZL)ImBfBoo*2o!Vm#7^bcXMz;H9{gI@cl!tyK z_DBnL#*7)|>)j3c&z!9|8UYv1M4jLZZsZZ;F7&mx{Ol> z1;b5z*v>+|R|7Yp#sx4;kzT2twU1hN3%$}zuHw*W~Z?d;Pq|sCTLFCeN zm0|P!WO6Y|bIfJA72_7XF*i3aEM2w9&hBYt(%h&G>u+6-T(u_S@#DvRqE1VKm|_D? z2iMIjOFxVa@6hdy<<;?#K61my6K@+U}6bI>mCv28lkjJX3OJ;k|_9oJA**06q&B2Zl%W>&}w64W*xc`_XK{Y9P~i zDBWL!*6*ts##^dfq6`-Jyo}NyM^Ou>vE^APXprjhF z9Qz_@VgFw|r33Vk2_1zx=)$>&W8CiJ!AWq<30AveJO6k#sgo!JNLRF3Qw;$%39W=o z#A0iwt=75K3rV6b9!|-J$2M?Kqqze^^_Jrv_ywG)0q&FqXb>clpPT%+l6&WXX)w`} z@Ci%HkVWB4FxAJCrsJu!zui0O*vk|qP8;=6^XV*f!`;btA#Awb>7-Sf-JL$Sl%5un zW!_Cq&L_hsn9g+<4Xq0*NC|h!RSw)sT9vfgX(S56BfSIgep5F7MZIq2+fdLzoQVxc zpsK#Bp4A6wwPWPHhX&`t>cLyZK2U-@2tqZ1n!0gHI-S!fHe^ z3>HpkIrvI(!sOzK#7PlsPsXF5&g7rpr9hCW2ZRu5Li#3H9Eli#X-8znG|U7{^8v3h zqg4oMxm~2Wc2O^X0U}np{s?^e8><$DgLXj#&$g@J#ySb*s8&~ z2;W{`$R7IYB%?O*O)Ft&;0qFp)kt+wZ9)F?cDy+%l7R(Y|b)a z^7=x-F`OK9qJ+DRuy58kGv!79f*<)t3A6oP*qltg)vfQ(*)yi~b=6<*R$!i+lVf|o zI-UwuawWYT%U8=IK{fm;YO4+-kz`T_*Q>k`tK2f{xjHqI^w^a{RF%dl8_XpV6^iK` z8$l`>q{5y`mnZSegbx=;zHL7Uay30>fF9EZ$vx*GB=Sp2?>(jTD9d@ zdAZ5AvbnQ%KnjT3jY10{k03vCPwN;}ZJEYg8$xEJbSuGW`u+Gs<;pW?oq-zKJ*^~T z1T=qa;F7}F2fOYqh2FEvz2G4?5izZ&b1fP<{i=o&phX2;L5uL&mH>$_m-bsc)ji+yn0JVaG=|#RF4s@|;Y787WaP~*H+kFvvXK6Zzz2u4{ zvcJyLGDGLEwC4jP}oTXUUKR8Fbqn6X?X1x`o%e@by2F0XB^rso%Ih6bP+lC^(A}D?_8_a`UbY1+53Fz z<;u#l7cNNe6J6{1_(gBrgU`n;p8X*cQ;ur(o4-8bO=f@SN0{2@>mJX4_8OM6_e9f) zamFoT-{I^Mj!dW^30TGMld^it?;%NA&Xf`m^GW~g+46VC;aB57?&H9N5S_r;lbhFV zmm?tufGH=D?hqAm^X8GrRddi!z5BlKEh-L*!)K%F=65{4YPyjh!|k$-Ls!t*~U9gK)?sIpvZ5I{Fb8*&&-V&jl}$E zJ$Ood897fpcQ&WnoRWt5IlVoO9izjQ62IBb&gx{5hatx2eWjTp!NHOuC-No=TSfpX zaK=dg@DKJ4@Uy8qE`2ii;q%&eZkw)hrRKglt$X%biczE4zAl{`^h8<+@z+{_25C;T zgGi|p)dO0QM*Ob&u9NR3zd!yTEr6n_1WR!qVsv;{eA)6Hf9ZEouS=MObHn->Sbr*@I+2ir zUr2&awOdpq&_z}#v>k}?@356#jNt(B$izj-`X{eTanjUcneJ5e&3!AGy}6Q7A@)Bs zXR3EU6^$d?ef!wE7DpDmBu3@z%*oZpjO#&$&SiTC)q)+*vAx^1tqf^+PtdD`a3X$i zM*A$BJ+M09@yD!i6G_yf%Vx5IbJG(kC6=rg)&mwHY!8CXY5SJ)3!)Rr@^H0ZV6 zyX_6X25zlv3eBWAA+?%DAHA5G&JE%^eb*FUZrcN>&Y`c{N(606R`DA+G7aWN$M}3g1HHUe1Buxw46h8rZS3A4q z>tpDks^p)@AOlm+`TIBHn3m~-^)o4wsTcW;@p~}N!z=N!^{~Ka9r=wQ@VNWAP`R9~ z92x=iBtwYmfN3M-3}SAd$o(%>1r!!_cloHfj%sWs#-}s+&!EC{Fa-rrOcJd2-5;x5 zZwRD~4C58_Am&vQ``jKLkvV0rmb=#^9D$;zME&gi9gWmd`rwjO4_xr10^U{Uk?Hj_ z6tq103u&~+p1yq904o7+CJg*|FDc1A_rUIsb5YjwXahWTa*^2F2eQ~#hIQKdTZ5F_ zsY%*d-|Vh&Zb9scdCy}^e76RX62*pmL%b9?8B($o|NR@*43cQ(!iAF^Jz~$qm|mAu zVL<&5)h3j{me%vHuhTZZC(v*RbkTub$zv>M*zUF`nueE+14BxAgqwIw_;}(~m2n2Z z0VAInm%gpoMtTE@I>A|O+U&*bU56AG?&>%J7KaYl z_+Bv)APjoVGYt)!Kr_DEaByVb=IX)wXlHT$mFawO3OLexzu}vfO?DvvBt=PSN{Z%~ zIIHNJg|B)!e)+Q5*T=`H@RjddKZh18t~68Cv>L1bgp=)db8sd=(CXyAw~mX|JyLd8 z_9<=rIlSC0D6=gPo;SYpZJ%+9`jDB>jj@PcEEogBnwPeZu=OHSpif~EVG!?FuVP~u zbz^dBDpU??{(>VPJ-V0VkM4Q+a(t3uh4qEmuP*JGo(lm-fAu|jKU9KW@%z4$JBbk(8>R>L@_3y4m*QNF zowVo1o}HbT3 zBcD~AzT)WU)5r2(`mPj73hLFFouEg>{=I)$Q$)Sp&CwYnEn7;Wm1MhUX=x22$x}i> zL{Rr&*wbPEu3f7)Wy)^7=h+5uCZ4R^=n=SD$2=u+*6ziDH)nKj(d@*>O7>T$FF*V)T)3YVly4Yvd6Sj#x36D6 z7nfQ4r_rcQO<%>JjD3`1QYpPyBV`h?gp^5isJHQn@oiJEMVKe0q_o7VQAMcIeG)8v zq7Gl)7pzjb+8iS&Oc(CCU?pYYr7QG8=+LDOQd7R~O#1IPW1|FS!s5 z7cUM^Ps+0Rit+-(Zp_1j&12sszkTaQj0-S|0XeU-BAX72N~c0YB}EHkqo;dI^pTWy zx5LAS{8ry}?>ryWZ$V%jPW7EgLbyR5z`$uAr*v!d{!Ojti``;{dA^SGmZ;Gl^Zo8iX+*7y{ylV^dN6h)NgW?;lfR9U zO&lVUPxM>I!iTQ(d+xcsp{2*Z)MflqySYD;Z3@$OlgdLyoV8Q+-1I@`ptB*2tPE88 z>LgM{O;5^T;I`5zOM0~OAEDIr(2NRC{!Lm0>{mT6gGrgbvW||}W;z<}a%IlAsqgp6 zBZd#x>|fBa#FLs1lELGr2Rcb?Sbs9qT&*kO^DAan@-HYyj{KY=Y)QRwgPb3o&b+Q> zYHJ;uo{>Qg`J|jCAxRze{Ru+!O;|mz?@^h1B7GTpv%p4?aQ>IYEI2Zlk8@Xl+`^QC9l?)_2 z@_DXKH8?9~!4iiZ7{q+5^Svcc=MRGoi#k2KOQ{AuMM6#*V8W8a10A}ACD0!>B^p`Z zb&_)GCrCS)f+}H9^eH8k60z){GLvcAg4T5*rj3~9Ca+=Lg?w`@s|R-D5)zdIn?38s z^0$FIU}#abeS@JzgS8wWEs%AhyIO+Pzm<|1Dl{C{S(U2&kP0ZJjK3Q$?t1WP$$-)C zzu7T{G#(OiH{(Q`^1YTDtP;f&Msb45Z;;laVXrbGuh~oMt#QkP)uvkZ!NloGtZ30>h!htZ71BDEBSw{SP zPCesIdi*mOdOuGL0CYl_1Oop|Ad6m3)tA+$*{^n53^24VW+s8iV7R#)D%bXe%#_&+ zQi5|Xv|W75e20LmF4j{n&y@r=sVfiy)G(nB`HSe|QSKV8QR_cWx$bG00^Iz@qet>) zX{9S{<3=Vq?OBO4^;Kg5CEa?Ow-UAyP!^sr#bH3%VR2x3jkeWH_c$N90F(88aI^|d znv|0I2Fe+5hM?6rN;3%+9;BMMjZxmPGwu>*M#7j}+YaQMiy8v;#!Agr6n^i&Ts3_i z_PA;)n9qw-AGT07*B^^MXO0kw^vrYYZU%fhM>9{^tD zQ$E-~wyg}c7s=G~=Q0TZdKH}y|CC1Rs#Ybhn<|I`lN+;XQ)dEmXsyt=Mm)Pu>^GRL z$@-7U8uTkM;3~xgtst3siljv1$YcOhrVd0-@pU*Ugjm$xwhLz`%xgH8Gc$b`l5L(D zcQdT2C?02DrZaVxFX9CF;twggD&Bd8g$Acr&B;lZ0X#JwU76Pz)v#{ex(`>FIeqRW zRoKdvJ>^0;cB~C?#wQFO^lGtNQ#DiLxUWO1 zNsymU9aRt1%u3F>J9N7^sJDPL<^zcNi%-NK-s1D`<@@_`*y5x zo^}7kcAYxSC@*a6<1wb}W6L{VqnjG{jHSqw*)S5K)}U)Qeb{x+u)3E8doyY)aG3!x+popadFMG2J!3H%xp-`V?tz)!)49K zZ%tkkER~qU*4;3refn&_w^ZS&b(de#o8Z+L>2+GF#J$M=dy z_h|G!&UEl`d1T4Sx`A{2KP=Za11K<@%eG?UC6^QkSfxrA)lPW{A*cgxEnh(U{PVZ6 zCOe{+d36|ZW%lLImKRDhfAZI`IJyP*pURp65eUo`R3n z++8J=#q_$=z{NS3*gJyU8BUsDasIA2KbX*C6`o1okonB}x^>xQvc}BpLi}oDB+EjY zL4PnaA$Jtk%q~!57djr*lc^U*>K<9PV-@F8%ZWA@H^fHN{8#`G{Pe@28F1zM%{W5om{i!+@oAQTDfcgqGyrjEO;d7pYf>j;A0B;63@_3Rz zSMz>XG4j5(T&LmO&dN_uC(O;PyUpPK^?dgq#l`32 z2U%++Mg~S#jZ|~bdFsA&>nZLChZ$!-SSqyFnWh|j*Ed!2DdaFXR)b|jQa&)-)|`Wd zb>L-kI40}q@)?0Pxlb%FA(2ZNxwv98MCqGlCk=~!9oJd$K$${1CNuMNVlO?jii!&9 z2*H0^Mb;%R!Z%`))9p6ElH!OH0UeJ1oO@yRsE<{WTgoxl<>2JcJ<+48sOaa6ZM}QE zarWli#-lR3HgfuW72hWuokToQjZ40~gjRv+=)QFu)$>$mZd0M=U^>KWg#1-QHF=wE zWTsaUH&U$Gt&E+5YH?4m)I6m|GNH^o6^4T2Q1p%*p{Bg{2>U90HIkwTlj7)R^>q_N zD}|_aP0uRjIO4*Q^nZHeqeq2BQpw7M>pzJ@TxN>87%or+Nj5knuOzBd>VDty0-6^e zwzPxjw-LbCm%9AvkIwdnYlfaM*!UcD0jRNmvHs0BjoFiS3V+(j)B+{Jb|ZD$OU_T{ zsGG9D;S3ChHNTV|%eAER;e0s&Cys(q1}zE8#~KAT-6KF#WR`ke4;Ggtb#HVL|1FNX zQP2CIHH5)O;J)qgMju^0z0oiF>}E6~_8Kj1?e>nns2qoV8B@T4B#8;k|BwMK5P3Lo zmuhOgZ!ibsg^%kuMLQh6b$L?tA4LXdcvyOMHBA#S{=z3p|L&ni$_&UFxdEh3;VD^Z zZ^x+q>q?UIy5VWt?y##2AfZI;_LTtphMbBEL)NvYG4`Oc{rf$l%E8j#f_1C@Gvn~V zgWCZr@45cF2kK~imiN!Q$sfGFzk7UL1;KEjqg7V}*0GP60d#trS-=+vLdp0waJ#xn=AMyX zF(LlRrwx)%N^cQc`zAS{tZBw}Powb`C2fg2?E_?|1)e9oK+HdS$yFK|DYLft|n}2FsI?>mgEig~l7) z%J?TNhW(SIN2pKp^7cy2Lu`e?z9*Z6#AZ!UP`6{1K$G^mbmx%B6=9@3m2$4ktX6B_ zy#3?zh`+vMhSD&YefdbPMFUAttdX&zc|rDgTq$SgN^-wHeVUNdF0TpGOvA%ROXh$n z>%m~3`MYOpf<+4~N)x{GS=qsqloT3D-#}}cA06_h#A|(1_0TTJOVC*GsMGqSF&vRSstf!JkyYq6{YgPaLVVwOhB?nnbRohRU zI^}1dKr#?5CaX|)M%b}08N(ILbQhlE=INF#$IrmFBEcBT2)odC9sJG=lc(g&pc`nI z5Yg62K|S+l0$w(RmB*EK2p_2Fw=g8~>Tnb|&0MGMn=@Zr0qn;|Ka(xdM~=E<0Puky>-U5~4?_6g>#% zDV{NB=%=Ihuq_K6@87Rg6Z@{)d8-^dDr5HMA3h{G>KBq7r|7?2Lc>Ll05X+i05ekL z;u}}4>``AsS??~fncR!D_|(^fHqnaNYE<lpt?t`C2S^#lx!6t-_iAC@!sXGbz_(xGGOaXW70H^udkZwLzG&)wC8X?T{2FwjZnalE~= zX@S;OkMSJz4Im6ec9#kNZ*NMvN+y4N@AgRNu$vsC$_XG1JqS+Vfe)V$WP4Y97npk|jk?!!|#0?B9OJ<4?(zm*$?$dTW08+o*caY&k~6 zbA^^&3|gWeZ{~Q}MSrtfN|s*>Z*-LpLM^9gO0odPijzbC`7= zhsKtdf`6V*(>dTUtlN}-Y{xIGREFrGtUHHs?__-HuMUVoDW&+lh|2d49WRxC+0ms0q+DE)yb|;ED zD(C&nmoK?FL>Q;EJBV*--lmQ8XKJCY44mg4po%LVGg#fTbGZj<2KvkbGms-Nu0HvP z27#lJP{~^vqVnm}x1pRl#%)Yw2}0Riw<$2BxM`G1bg67;w8a=pp8lMGgy`AZw@Q!# zoD)w&>vHiackH;A5g>JjC7o)=FJ4!&<@ozlr7+dDnmaq_&;VM`V=zTD^^4e=2^$oY zV-=F$yxGH}Qt*g!&5uuf)z${9L$O^P|0--ypLtsv-`486|3r#VJ^_~UQcpklcW83u z$+>lW-L6BgxK^$6pFb_>_|SHW#iPEK`0N>8$0KP@!QMtqcIet?5WnSZo12|IF=+^O zn9`=8eD{sEw!ZO7KwzSz)VG*#RiSPh+xheNkrfL^tBvqH@GXd1`7<0eOBSI=^TYy@2gO(w4y>dwZ3^hjY|@ogaj+{ zVp%$*g5(JErlc>+3YL($KXCWM=HtlQd-iqL=;QRc(TI*RXS0%vMXi8lrH1DT5fn=cQKbB`!YNu ziJ0Fudo9b0R}(Lwsd1yKB{g@v+Ga&Lx1bB_o`SaJ0@1c{Ix+1H1?KRR`yq|&l~$uT zA|e#Ru;|c6>6g=Z7sZEM2*-{k`_mVTT-An95iSF$%fAS6QhjXD>}45QVDn^%m9&2a zu#=-#?_@&c`yYTSufrv44CA%@w=EBN>9@M;!AogHGi!tbjiQGI-@N&Bqp!60=g*rr zVZ@r>9Taxme{W#jzF_SzV$_TmXi;+rDJ=7vgpGx|1b5u$+!;gxT(3a`lcL2v8~K7g z&j#jPR=GKs%h0DHaEIIX-}yao6=3CTt-gV>r=vXsZ4-dy}_53g{)>FOqTc1iXC zg}Fq0gMXE2+Ta1z?Rx6J@kO%@XEC?I>ESbeyfo6nQja#-zLDft%+S7 zxl~|3)H|=8hRvae+CB z#ji^8TQ;iq(Xq?8;)AgBa!_EI!umEduqbo%9Ckku>>3!AC5txKV%M0jNn5L0;-!=X zMNa_eu8zv{M*jkhT2;^qkhlEb4!`lw-yNJ30#51VB9solviUCL1f5jlNN;UuPU0jQ zG0q`>UcA2Yg6||WzJr3J_+6!Wq)s4}El(bnNb=}JPG8R499^{h{ftX+nN$2Niab+IDdA+Qgg}f5Uq?3_Q zBB{nRLi1Skb~Ul4``V}U8sSDbBR@$pKQ#|d2B3jrM2sZQye~0wrg1zZ>6dMfoo8P5u`+Ib^_*JMrcKW^?FZzw7abA@ zJ7VoDo;%_`5iu$=D2V<;ZK!SX{nbnbr~2dg%6bX~A@)JUDNiOsC*SUm&O%gbC}5;b#)BT+ z`q4!-hQ%T?Q%brJFoIJvY``&x9y-;(he#NLFbwE|MdOb9a?iDE*Q&PU)f=Zco1<9- zLnO*HV_*aO=mQLaxC&Cv)=|K;BfE`ZX*{b_>g_5x3QIRHnGVT`xHhJeEQ7z8kn?Q9 zLN-r~gZdin!GjOA`qtBYA&@4XN@(=-u?#YmRz#ylOM4GCy@ovq)~GIoG1QQL`N2jW zsbCPO$S`;z*Mb1GJkQ=qxN?O6-#wo1viKK19q6aDV>_M52cCnSEG##K-buL~#@e9S zkMM?;12ep6GqU8-E)@N!SOD8Ck}`=T;+RxL)L{4JvM-V9PUB9WInvn7%nYk*48^zw zq8K@sNWQ6OQMP3iKV@U~lY1CRH3&cO?RB+RBcC9qp#H^^EBxNURw8pDR@?8{*SUc= z93cox^GUP59H`XZpXz748hWRsCgHJlD6P6|IzGZU>R3+kr?s={A8C|2J}wC}k5R`fcFUG8|Ipf_ z@6kPp%g1Iczepl6Ijg%8m+))jNz@OVGpgC8Z?_WFqt^z#{`{+TllpXUy-sd74D|DJ zCJS(=Hd*n#ZR-s*tyesWo+@@7%G**r8I&)(X-#xU$ zbYGu~98nU6Gk&~F%gknS_y)aRC%=~1x#dnLq$KqHM$QyRh3fsUwiT~{ z9m38@yMz`nFIlJRS2vD}c6;pKeJd&1Vc?s$p%ViSNM?X|H|p7G$`VnkQlwfGf1ZU} zu|XpgIna0SFpi4@57nPc0VhaYft4_oLHMP4ubka}XxXQPJzAtttW|W*x2$u- zriCrvu6X;shCx$pl+*bz^W8)s!#ua1H+0*Krs4lvGC zATnUr^2_r+ai*WA(6$wukZ6CA0@oEw)cJ8U&d!w9uz)E^R*~xw5`(H?nS1d5`28Kx zKh9W=NRx9Wep{HwbVWyO+jihn+;2<7N+hXk%iXmnW>0VFML)HM=E58lZPojxL~4t= zPEwW-KODs;;Io7x+xCR>Qg`eP)Um7{MkL&6*{v?Mq6W6R+7>v`3D`>a6*~I|1jNz* zK&#Xka}@kL8`k+rx5#nRy|voInNa@F4}RZ7_$=U<^(giDrGswIEjV>6)3GBN68S>| zn%>q5$=lt_`!4IV-L8GN=ZmR^Cv$cFT3;3OR`A`(ZrfuM+_1qaf)dV^M)XfRMfhHi z$M*#xBWM}f=SHDFIS3Nq#o`qh`J4Ij`BwY>zh1((%0CTsxX*-}P|*BT6cqpA8#I;i z8hul$ms>yPkf_8_>i8?GArog`hHlqbO2sQaCN9Z_zpK+^U^iEf7K9)bQj*ok1!e?N zAmbshv`c8(=4Brr9F*1~6B{0&LJ+m5^eE6+oX(wTZYM5MWR@8Pj!-)=wab&dwa?GE z=Y}emXR{Y8vP3@8svufPFeBev2a{&*D2fAV$IC!VG80DWd>(^Q*p=zsAJ9}4zW9a~ zgN6?m6V_R)w>qGRkdTnIZ&E4r8KJd$$$D4{5t-Ke>vm|U4BYYd_O`IKEwb#yghTiE zybY%@6#OV7Zb0eRMpdWJ17Jbm-`=4WmOv!lQk>OfFNsj$SJ9D2Jy?D1HjW{qRB$vUgOJx_Nr4eP-xmT2_ZnpDa}3{G{LeL3GU zf5oJzs`7qx#=BcB;) zY)OY%My06lDo!LLuX+6!$3H2A7qDQ7fFf!1OBV`@I5hB|S*ehF^%X=biMdg{4jh|; zw`!$Ol-Nco+b%+%O04Ife7$EV0oZ%Oh)azYYx-?igfB^PMqyTsA^E(H5q@y@)8SWQ zi5NeAEYq#%Tf*mvq+Y@WqN4+@v~=z4DIz$$VM5HPzO7Xwh@^x`nn+53sh>ZN0({Vf z>0GzD*CoFdlje54R@g9k1Ko{Kt z3Y2z^ZGr2~r|0`!Xxi&d+nGmjf%&~qa!+f!pNK}QSreG8jQz{C1m`!WfJD#Zg=)|@NFWsA9!WqvJmv4L}Rc)t}qn);XLPOYjcpgV?xf-eAZ>Ft}<+Tr2h%r+7G znAKS_KXn5eLb-55s=pe2Ag~*)tz8{mp+w22)oZ@CBJw7^8dMP@ja+9wEV*1Yq{4C7 zno9d59cVjAE!aX2PojJ-%{o^zrHwQ1{K7utlpTEdY%*g5&!UJxSgK1uJvypB=U-{I z(I^rvjijcMkR@|U?7lzjMbS`LY=?}(VaC3`H|#i9%T2D0?R*!{nQGw>&RFKSdK-0Y z?yNTN=Ib%fTGpx(BsHo*w{8s%b1z~=ZGsNAsWd6Rzn(fMI&xQ_gGQ#Cd!M6^3?3Hc z2X-6XCcEI*Li!ke3??X#*cB2ooK+?_-GAGPGumz{E-oHV86x-v`c-WGzrJQMc*PPE zQ;n|Aw$OzSCI(nqRtG;Wt9-I_P)&@tBrVDT4*a5B$puW~=XOCznq**eE{yAYbg-&A zYaPQE;99fCmYfZtxS_;r+8gLQonrzO4h8&$oQ6kP|L4!wLoQUDd1!KX-P)KRDb{Gw z+M6IAd~hc6=RY`w7y!8C>%E3}s_se3K-(o-h|}BmIra3k{mmHkD2bP7&?usaefgZX z0Qwp_?ZEk+FIQLvv%0n8x|Ewe`dnRtJ%G)BTcVYWDtph+G=uLwcHuY=y45T7ktId zP}_0a_=0;@G|+gQk=rdJHd(lAy6*}Kq$9^6rsG?~#}6ORIMTQ-zE1#-_6gS0 z%bN~MI>&yHfdhT}>WbHP>QolAf{|Ls)@YHX`(Nv}rAvY?EO(yQk&c_HUeLVUPLbk6 zfg#g-5z0qfO;&;f7}_CW-Uxd+b2=Oirb=RMq+`Vh)Ry4~TgYEF7qV~x6clP1Y00kTUQlKl1eGfec>QTy=Pbx!d>Me|DHET`U>Yp$uBM_Bd`-J1IK%f{w=FX9&1o&Ec3i`- zL#i7Vnh_r$tt$y?6Kg!Qg~(>x3^Wl9m2ceS+sBwKuYi`EktsbtyfQMP@k9?K?5bDB zt5*+Z&h>ghcqyefWp0KVnj-1>NwH|K517gaXg6v@cNz@gUP~si61MC2-1vIqUAl%1 zL91VeW*=Zih>VsI6D%$?1GmJ3_+W)g#{UkcVWiVCY!5Q@Ab0ttZv{vUCT`xm+4oz) zh&=?lWP#^uZpW%fH&iP^OfxRw!R}`z`B16REY>%ZyJg@9LIET!S7~s*j zAJxl?5okm+Me47;iK`U|9&P8taY(DwonDn~{I_pfe1k8${G086+qQjBL}}*$Jb=g$q4| zvjDpvDzMmjhmhMW-fUrM`fehz5le6!-#v38=rqN?uwpFsEoX9e-#0y_wW$S+q72Of znM3x`C))`fpShr_UAJ!C#KI~asgi41KLs_r1OpkejserlGb0^pk}gQDzOFjEEmPga%^$EN zN=e=0vCYVjU0Szo8xt3|^6rv3Ep!{`kUsLU8R%>*SPQa+6HEGj5bY~bYpobCY%NuPpaUOGa^*p}n1TIV{XcYmYX z$_oOF#v$0`2ftQwJuoRQ%L0aeH#A!i6A)bvGz>a^?oR5HA-eyEt@i-yxo`i*zeI(! zB{WGk$%s-~6tblxZCQ!Z)K*DFMW`e#vsBX7R;fq>4W-i1)Y6jH^E$ci|8=f^-D7s@-Q7uV-5BpGyb&*Pxmw{sH*2M2?SKS31zDka4uGIE)KpkUMY z?>ULMj^R13Tvn*8s`~~o> zNq>JoC4oWRhHow(Iy(fG*Z-OZh_=+zGv%6^!e0iYp)?AqPCnS&7o zsfKQz=3nD{uVMFKVPSiE&RM&5 zt)8**(<bzj-QtTV zupU9KN6KBq@>Q&;y+)Grt@HB_#&<-EIeG{B7(Qhr@$YRK6wI2 zJkAivZ{B3Yc8wptx76js4@u8Zhz@``GU2stla_v4P%s;E>+^NESpN)-%Bwno9WW~! zFhHpbTM9XF9eCA8kGbn!vPLo~<;*w;bBa1BBUK+RbBZSQ=N*O87fpf0rcJKM%$b;+ z49ZMO+EORV^Y*N%Da{_?T)6OAvfN$~PAW6d@zPirN+v}lP<>L7M5i6_-jaz)nWQC) z7enXMTidJ6x?5e0xuav}1rw85j!HpI)^9!^bYOhZ~ zRs!M&qhbfQ;eBtk-HkIbNP9_$VbkDPd1Yl!e9H=>SK(s`NHomG`k^;PeuLOpN^-iA zjF6I zF$66z-ca3?);g5`628^w{Xu(Z$(59jZG18PVCamsbmJdT8}K=Kr=6R&^n+#Gq?gI0jG zflvh6Yhfq5fo=-+kY}JZZH67p9qSM!075Pq$x4fPpYL>+B@H4mXHDABRFB!TB5(m-p-gz5x z0El}claogtjo0-Zz13uGY-BVGGrWy2>FF_)l=9a1l%}ZLRR$t4k;X2Zf{*^i?C|$x zFW*<%54P=3O7qA5y?pudp%0ZWYfC&FI8iY>*o-oQM=ssZl$D}oV!>VBFgBqT(3&r?*T>3DQ@e20t?S3WHm+7th>Zw_V6 z|8(gGD2K{Q(qfX8{DK)p>tgp4=7&VT6W%R4P-ak68oKB2;B0;GKD)gml}1m4L881U zr0h2R{CSgDY19lZw{Fd+pjjxu?w+V#&dI80WaNfrQT|)7GXO966{c`g7$b%{zzCQF zh@^(y`_<1J7bcs@;c9&A=_128SwI3bO9WRkYKid0D~-yf!Hgp`CN^1z(GmD88f*v( zTzhm`4q0s&oX8L81`#`YrDo1PW4GVDd4ogYI?e|gmw+n&aOSC*4DzMo=UqlHJxIpa zg6wG?#3y6!){)>9n-KSeCHtEH-Wvc3_87HwK2)h5JRSvspaZW`xqVv&Qpk9-mfbWM z0eU6t$}aNZ%({B{)NrX3W5s$sEPSvq>@IdH!htaBYmZf z4VPQh56ATZ-Xubu$G28bboB^Go$oY*1mOaORx3GrXreE8K$^EKe(AIP6?8Pz5U8=Q zGq=WlMrs})fXc$qFwy5k1S|c@*um>e@LUwd_E1@oxmX^i|>kh+?g{5UU>O1@x{Hsmdm*cOk zG*{&QB|)>}Xdnu{q!32v)W)^#0^6U1;igo$fJ8w7vEwm<vlUwpt}{7z!O@V`E?`&N%vTF+rj)`FPxU&nRgS&0C9vibE3tGy@}lxo;Zx zxd)1UB+P86sjVGRMHT%K(J3=do;-=FJFw>vvpX~usc+saMKgyk%UDK0Q3#`=6wp6s z>nPC_>65|R-`AZn;ZkJIfO-(r>M|6X&@58zj3NjiKhO-I4OFFYaB#qjocHzXqV?<7 z<0$K_bWcV_MT?r;st`O-URqlFBiihJwP|ofkU|=32GURp8Acg~9AIa~a4P>%QA2p& z)YTPr{A|Jd_ffj@ow>}jj}>1&3Q+LQoooLznKZMIp{K7u0}&C%gZ=%^Ft!}kyEBwg z$75#MfM!0BT^scQ%8aPx4P6g!UU;=!22fs6aj|}u^?z|jU{0U9eygp0u*c#j$lMhx zR!|-B?%h+}wHeFZ{Br5A1*@~F1QZO>Ie2Ce{%&-yJ?1&A4y2NPJP!|#rlw}jmoL0j zM@vb0P?3k*(!gLQ=um~>$z4TC< zYyrnhV|O6|IZ!@OQURqHq7Z6oZT-^Sw~tkn0skMV^+*N`42UwuZkBj~?I{NAU<}Xy~feK-9SC#l%kprloDC*Nn_lgcfYu zwk-#$#yjf%Gdgjflik1aodlv0Rt?PUG@~3DpxG1;WPnX#;-SP$^UL6`K;|=kM?8Oi z;~YkLLQ0{%E$&0 zLtIhv<{Fr2$SuG^89OFp?8>doyq+(4f{Jp~U2Dl;^e)S4ke4R{aKF^DfZHMhayDcS>+ z4*dN562al&q`91dj);HJBFj1<%=d}W5NA4q4i;EB3|Z!7h34A?5NdRLIN~@KI(0e* z=4NIJ0L~_b#a#CSNhPr?505KE_OJrcs=%L9i#S=S>56)wu8G@R1&R$^Ihz4~yjiEp z!ho;g9Zvn3Qc@{$K0bmW-uH*vla-gIoe}+v4Bqg&r76ORlh4bt8>>H6ROn~2iiY5YWw#GeVb3!l<}D}EHW}O#|zIs zIkL*%j>6>8UZ+}i^Ci6aHFu_9+H7^(&FX2J1<}DbH_yc5sPb{)yyvmK@yJSE-1N}* zZO0Xw5QqO4+Yp`VA#LrOI0llkW~7Ba8Cd2M4gA5v3IB%y(e+Ej0Wk^&G7pmdwL#X4k5gHWu_;SmErW?FZZT zZ-AmK7fs(n7Q??}$zK62{(f9FLoT?i{?oMoSG6K#0r73f(Ecxg80Oc-&`b9tnApW- zMxFj9sHO;SzzP+&z7=KVr6vxUkxNL3pnv*1RCP!qxom0KQ#lwIo;9^M{LptWAv8CF zreBi_U-ZZnIr916LTERCb3xY)C*j7PCI_|Qs7%2sd!gvZ8(7}Itog@ zB5V?!J+U_3h7KT=w`rd`6_0tR$6HbZ(9Z%sxc1>kGcGcv$p4*NfqsY^Rg;tc$a!{IV!ttBhX zU=RV1162hy*w3CG0=IMqKkeIqIkyoX1Wuu@Am9vYsqVo*+zXuGDv$(fT3vBvo4{J# zECGy;-5>D4#^@|*))Eu*Vd054t(Q*D6CEWJKY(5w2p~>Cd6QQIU6F|gGXp4yM|5gKuG9j$4$;CTMa-Xs;a5Efsnp4cnB>p(70`=nD=otoB$Mf z^VY4(^D}1%Ha4M9p-D3MXm5{LOn=6`14#l6UO3hEba2d`fAM09WJ6I(8EVi??sxbQ z0@Q=X&yvb)eRdOF?Gy_eaUKexp`CT0Oj+;LvPL|y}NJ!yGLQ8gRJ`M_5Hr~}a;ClXUqU;?L-W>0>2EjaMMj4n6g>T)1sB#$ON zI4l`*Il`jIdge^~dfxz5F*MZ2yWP^O;n}cxzJNu{C=8JGf5G|$OMxH$cV~hYCFKT& zg8XxtxTE#PV*JW(SAGt0Lx)yo7~WY{nZFse^uX?PWb9eRE z`187&nl(LV5?6EG_4O?a+plwgRWy^(MIa?W)}XU;RTb-cjeI^zMKCmsti6JrNSFvJ zxTH*6pr4apzmAyY;KpWPy|4L&v5{$EvFPL5FJ5s6kgIv}CIZ%7lUoahcb_ zz&n)d1|umg0?gXj*jPz>Ux;G>N;{ga2bd6rb&ma%m8tbq2msSXZvcf7Dh`MvFeoTq zuvFHwpxHD|{iHTW9P)g;OFY^DZgp^zxLQ6BVL-U3ldP2;eV8%5D_%uB(Edz)&rmRr+ z%7VUu&U*a2Oox@Ty!n44PjinQ66&o@JD-PzIR{0)(AuYW z=@JJBGE~nJcK@(B+)X2uqJ*%wvSY;nKD!Nt7*J(nwp5tS@%_P4P*}%{7prj2;Jq?o z9ph^PkqO7lB6KBaf=}tx6&0~U$Z)8x@r5j8Nhk{|-pqgd27L+-Au1r7k=z3Oq2Y-q8NZfS#$s6G(>09^bGI>D1NDqrpcLA&7zN2K+5wSB2%?Sb9@U%4%7iH?`|nU z5d!$N`qEr0F7us~+<<5s%voxTzrhS-4v*XyM#Y_9?4Ens4~<&kQVlS;&Y`5P`8YcT z_T2+}Qs#(Ga(@g(zZOnOgkT|QhoZ_ty$^u$5d1k5BFxK+j`5~##_ii&SV+{hE?l?} z9WcRC=vp~4tCF4(B7py^`zC^hwB+F;bZg>$PoqS9&|Ch96LE-2DNB)#JC`k6=G`@I zgX?|JX5G4&_1)E%bU!yV4rV+=N)md5M4u2pA#sfP0eD2RFlbRyo;AV}hX(?fjWlrN zqQp>%;w8Mp9|(_A|sZ73CA0SwB3<BA*}}O zcg*oCVxpomP(=aALc($Hr^brVsRo3QpjD+{@z%)V_wTPATRj`K9=0Do34G5_jx$}q zwdG*qRFo2=U|^21zl=$L6K2-KB8CAreBQz~(J?Wnv$Cb>;>W>=1kiVheb$>cZHgRp z!ombMhg_S((5wLwh*q*ovT^b=O{B zh!eQfY9;9`2$g`yJy|L38F@?K&IfEy)V#kxPo3!e6KJ5(!|kNNdH}rEB@qViB?Q6`Y(_z5viR?6(b>5?&Ab&_-<=S+;9&m%|* z5nRq#YJ#BfPfcZdV0H>Xbw>@ue!RmR#BY_{6pS+el!?jYkoh0XzM+BP5vZo1Nl3dO z7fIq|#K82mbTG3cs}(qRGnpEW9`JA#T38=4}&yt$^z0QbE@-H8iMa+HJzv=+S& z#}kZ7{WAiaBrLm*CFj|S;2YDPFflRd$0x(pwrmUra1tpbNndE~#eYu&vxo!Km$^Hm z45$TcS$&lcdRnJ$p@heg#h6Nw>qVt#)0@_=>GQH3Zaf3%V^B54v`8P1;uW-F@ND`6&)Olqg76NX1I-KYUoCCWI=~+1Hl?+DjTS zN!DQS-$@0(aA-q6BSDug{5K^2MG__rh2NxEQ&siOBDUaP>=e}3aJ+z(-Hi_cK@jrI zSP+d|Q&;!-+C0hci1$PrAsO->fs{yIhQiAOS^}^zLDL_oH2n1|qV@F%NffpS!5I?^xpjE}~DYVM-82};RjlE+!z>261AY~ZzbOy-eb3HG46{(lV zm<^1<4No�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

c-W#I>|p}JoJP&*8Ku*agc|uR}_6(Im=+nn_{od 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%0D-l60?zjaHs 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

RmRf`n*~*&m7*6lAtbc`&SQN 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�`9Z%Sv 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�E&pQ5)095`M!ew!w?rs8Fg_;4OT zcD)WNaR{}t{Ovw@W^V9}=>Lw;#qPi~61J_!J5$Y;X&p!TUjifsNU{%PRDx=VOZzn5 zu*bx}F2oZxPDX>Q>%hK#9X*gi_fP^4K01~o3};Lc?43V%9~N2*H5$fWxir~5dm_o@ zjlpZc2zYaT&1~g|10qGw50!uC&S9VCnd9(mgU0eOZov`(q3vScWR)c)7pD|=vHh=H zm_>`{-ycVbO2Y^_?x9ysdodV=NmD44l5qKh4%>#Kc)I$`^!S&V)PmBCLD%~Q#ToZ4 zl$A?oCO4cGTaWR`1M2F;s!-LYyM)13y309TrTZ!Ykbi?W#2tyD#^QtFOtB}|v9aN% zoJBx62XJW6n)Gex4#cr>L@Fu(ObL2WMLZX9Jz7%QG~)8Qtjq_MB)|SI77s3`^aD?D z4r5AKKvh7AzZ+X}9j+t)_`958f_6-eB?u-y5s|=q_cnsM#_0RLb0{QP_SfEUXkG3#i&<)((ewaQ*}{W26zH0zwy_&g0zBQNLHZYLOad!!M$`U$y4xc?c zXf4ja*TuypEvn97376^%7+gFihm?`mHE!T8!Jzd4d%X14*OS_{wdePbi@(s3mF{v#AnmAtPZ-P7H8&CUBXoCVqukwHp zm||bwlX4sCcljEx1vbmr0xKIJu_(r zI>YJX;V(;@hO6RveWQTLOrU%N_=W^%+wk)7&GS@O*JHwQsEn0I%@VZcSUhjyK*7Qs zM+P)18_=8;r(fa^v6vgwhj}HTms#3UY zkiA9#boPYiRTTxS$5&yKG2be9`m`gbxWD>*rBkL)3cQx!)8uX3&ut?0cUg>Cn4!1G z{e5j7Q~EE@vL6iFN>$p|d;&97$k7#I393dT6pf#Bx?7=Az5uWAEg+@}Tzfdxpo`yt zYQ2TMKOEQIY5;%}vA``s$xAs^X5bUnKE4O5hubj`(p*y6W}oOIT!74(OLL zrFjTjl$J5w(EwGdw&w7XAp>gpacPc-%sWqTGuUmW@X;Z#zYbzr0Edh!-p0s=|LV6Y zv`Mh-;OXr!hXIHc96;NL`37BWfqJSG$f1ULc@V?qpgZ8xxB?B4Idbj>BX6AQXa(e^ z$2xLzYOJ!z903}dQXpZ}BHe1bjN5Pxg+Vl?@J#}C>0lW&En!>@AL6VRA4<`YHZ$>AE zhx*L8cqs|)c&W#?2JkxA+an{e155=4bV6XciURC_wg*TW--m0@o{6LZaL@p43JKcC z+KVh`1(6KS2)C8_qL^b`2-71dvJ@UEfGO|-b3yefrmF&5fxY7ayI|8?gTXH>r2GBH z6P4l$Yif}BRLc5TqCS@qrZ$ zG8HcbBg}aY)&hn28_p7%7{bIUh?nw6+gAs`?9kooWY~f9Fb8ZqO(mgwfq1hT73*zt zbL9AN_rVP-rzdZcCIgw;7}j5EZXju*s00V_dA9@VC_n=s83HX2c+tF%Yj!edeIaKO ziYvOy{b)f^vTjxH;`;MrxLPg|sU_eKp244D5g=%G>eN2G)B9G-;ESE*xWGIieKT%ix8{05?5LgeFGz>8ji|ohYkKpSMc0hTc;EH zr~1cm7tgSdNpXS5OrD5@#NVWe=wcv25;8%6;E_e=f`q8I8rk`C%PdU#moPeTadhS< zM1yJfhw4$PNL~e5#RokHSbV0so0p5r3*CZ6=*$!MI!iGi4lIYU=<4gQ0>V7u_Qu^y z@@VRTpivNC*@vs@7kJ3vEZ+?8FPJ;lNCyUa=EV#PmYe?mKVAR^h91Wk#-7uLDSE@u zv1kOC1WBNsu6(K$x6~8i#P9|ww(9@x=PglJi$IM|R8C;q+z1ThhdUICIryLGe^Xdv zJ&saet4;V}P-2|14Y-aimiV-#yQ_=5P!!|15VU3x3lELtDs=8ApQ`33*7agoU2Y|(Y$w(N+C)M21F2-Jsg^5P0lf+m z#1W}$O!7B8u~$vrcr$>lQkmJ!^@j(SK{IfAsOVb?sJTyui<|5Y?J`{`4b=6fSMGcEdT@>wh>*b-b|-AU zIcPo);ol9Hnodkm7%8^$h+IGFE<4_t1yCjm$dlKj8kEL!9ql3Ztg7-VPnc{smBz~7 zSiNn>j@8GGHM4Te&x&6MH#n4P9*nC2`sUkTWMN@}m7FF;p)kZ>_vY1s-6|ZuM6~B7 zIR5`bT>k?H?OB`SUWV=k?8EThJ*1$M3!GNiCpD;M5Po7E?;ZIL<`~`TU4#^aRy-{AevR;n7jIFU^>aMt z=bUYQoe;q9p6I5Uq3&f_89$sy@77m#eu!_x^1Me)ul=U$SRDTS+O_e^YE?igL8HRZ z!IsJ1##Byh$-`1sY4F=`qnp05$x$uL0EbkSUAXWtJ76cEIZDjM3{~xFql?TEOk{{J z!*aU=WKE4X<=?{k6&%2;`T)C9)VNc{L(1+l%PX0Swzcx>1i+~HZgS{PXT1B$Tkpgb z{XGNK`|2>hubvvV&$tb6Q3<9hHN) zc3coY)0C#iKr{dv1|ad(tgNNLTf>V58J8@1T~`+bWM2zLCLxYx12|3cD8dG|8q!^2 zv`0CFvEDDaJF%hPG&djXy#W|?lI%5~v(HStX$G%I{4?F2=sGG4q6-&Vt=G_~o~I&+ z*L&kmCpi@0lS3(74U-u*$N!*#B$VHh0QQ6Ulz2&9@cli+=}0sg&SM;*I{&jKUqk`HQBNL1+q|KYot!wJ-reCnNCX?W z3=7ccR8fI>XyTMu0D+@dr-{?6tUQ#TT?zr0G=_ny}LJ$TUgE*p;=X`GkBiq3rTp}V49 zWOc!_cKG&YA=>L@d9ATMc#kAD!X1W0o-%xSzxo-|eKboA_yrcz3+V1HuF7Hlr;z5^ zkdHtZltD_|#=nkn?5({T5x0pR2F%%E;9SU@6aM<}EX)pOyD<1A2G5g*KG4;ZG?l>J z<@)RET+ED5(3+y>dh8UIo$YH$_?+~55)5H@d|yU>EJ0tLWNQ~{L& z-W@T@zF6=7Q1tx48;#{)eyRXF37^sY8_W$4ae#n1REpadS98+VHZU@>+2Kr3c=!R- zD9HD_=-uujZajDwwh)Lg#JhJhTo(D-r#4m*an^1bxKykdVXEu{>z|K?NvdPIwN%9cT~~SVWr+)L@);5?qfQ z*d)c6cp%;-^Rx~CY&p;#oe^Z80Jl(1CX|;@F%UUlO4Bm zF(1LC4karCeB<$cr>rO71J%;i^3sU_Sq`1=R^dk|rBwlaq;tJ5lS?}0fq;Lj&{@K! zIJC@YI6EVQCUGycUgY;Yfy#;bWG>kCJSpuRMdB@3j{hb*U}%rUBn$TTa6;zQj zfeSPz-~PkEk!*%dmVrNf?l`ES6dRv86E))sT19ln4*3La8Y_n8ArwN+EL1v@aWT${ zY4Te^LHjXiV~`@@nd>!|yB*x@Mh)nb~%aKAi^OvwUB^Od7Cq5Xsio$jA|t$PR(8w97>ye9i?Ee(8rV*tbo`jioGgV(Kueg zflTBT1_43BJ2}~66XfMDEzR1&AB-_E_Wb3`E&rhnkwQREv}*L0ia!hC$>8G7*sye*ZIu40*1i=|L1JO!q7RvH`?b;7EIds6l3lMl?jaob*JFvPXf$U|0vR`hp`8i6?OcuwOVi6C^dZPS|I`U{Q!ZL zC@8yu7M`MsHG)}3Iw7GV4T-H>9myA zCUh+9$}>KeH*HN#DZ+;Gu)sZBl@ch*pji!6BZ;%0D{Hu~3tC%iIJ6nMv3=l#D(`E) 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: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%0AZZNDmJvD1sU}tLVC!Q(kA#<;` z>v?qlp>1oX7WD-61BQ`R+^bgu1l}@SnTkQ0YS2LGcv%1`TN*`Z0;z6+nhL~>Y2p6j zgh3 zu9A&G@y^HG&7;`aW$YN@a2Xd96kR5VY~yZDt)xm87OLCyw~pP*vX#+HP}0&+D#fh4 z&7<7p-~Rc%wLQm_Z?Y%E94A~)UEOXXuaX4Gm6o6DBnbcY&!=%l#*!31Az{_o(e~h^ z$E&goi+1`Oy9W-Fkt~DFBTs$Mh)xugL)dUI1yv+Rk(A3Li41EckSv=pLL6UDN;nDV zRvuq@KDjav)u1a>oh3kPw91Fe_wyZkG6j}yW>ZAG-^c~sBH0>fDM#9&0Xn~NUwGO6 zwr$&H#H)q=$&BfxIC=}c8-JMGoAOQ4!X8EZllONzhz%Ok1de)V^?zE%Fw1uj+1@ms zet)Ey(FaStj`{cyhK^uQ2Q5`hF)g7>#-pNc1OOBkAOl?LfRuDSnKJ?MBViI?vohn2 z_jBvfhqgA&kiYSsa#$$By0X1|u|C@A{|7v9qIF_Sfac3JWuwNl?5Hj)Ip{B&RcJ^p zQL=ERR^nmxGP;;Ih@BU9!ya~$Ai8YMHkQDH@C~1Yi6|~UOyRf$&xD{vf{61beSrwi zmFxaE!voMBIf&kzIY$E~<$nVsMbxiI-;{8;TXCDIJN~<+2cPao07dh&dg(c&2YSBG zyCZNE!=i*k@h}TK25}dy5;~8Ag;9~2R!7E!zEf*Dyx&x_ai0kKe!4ba>0)4PVegJ> z<|U^8G4K!HNqY^4&pYK=R^~8xDs+g$t)9=3iCE#Ry$M*i*B#9>O$ab0UB&CQp5|B(_$1dvanOD8QIi)rN93TQ>bAcyLx4q0H*XvG6cQ(bfmV#|b zxz?`dQ}OQXV_G{xfJWirHJhmjIGj0=smNg6hIaDcail{<|aSc7S@fbmtPk?tNew9SB$v==+?cyQ@DssHn^ z#(%+e`(HmD&yL%6LS<>sPxsrtu*hHRcXWoIo^PmI+!qHW&YkT96p+FtfrQfd@I=X+ zMB1DWcbfg=bxq#D#q7b@Y?*%w+U%53oyKBeQk6LV!NJv*1%|csTRzlw1~8aldprU% zKutiN5`v3Erq=6XyL9eFuLe6*diH5#=z4vZ5BkDaajv?}TW)CH%roZ_ayQ%7xv%f5 zcN)yuQe3P|mVIHv2!e`dBRp}n&A8J8Vx3k+uu?L#nXso2raJJ%qYu*eY3f-%GHjuc zqSU?7A9h48^^+_BF~(p~)1/Fd!Sysyw5=YX}}#}q)vw;NAwwp%}MJv=tfgQIkI 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ɇ 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 z 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�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 i*phU?(pG_6IK^|`xF6GzC4c;V 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

cEVB%oA1$Zz@>0Cu+#_! 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

-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

4|C=6@bW*A@ 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

h$ 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°<%MiAG()U00xr2m)C?9GITGp^*1!eYUmHfSj7N} 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

=gHm$WDa6$LARX^AxetaQ~k(-Bn--aECY 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 szgMC?Mjav+ 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 Date: Mon, 20 May 2024 12:51:14 +0900 Subject: [PATCH 055/263] =?UTF-8?q?feat:=20UserDefaultsWrapper=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#530)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../KeychainWrapper/KeychainWrapper.swift | 2 +- .../UserDefaultsWrapper/UserDefaultsKey.swift | 14 ++ .../UserDefaultsSubscript.swift | 129 +++++++++++++ .../UserDefaultsWrapper.swift | 169 ++++++++++++++++++ 4 files changed, 313 insertions(+), 1 deletion(-) create mode 100644 14th-team5-iOS/Core/Sources/BBStorage/UserDefaultsWrapper/UserDefaultsKey.swift create mode 100644 14th-team5-iOS/Core/Sources/BBStorage/UserDefaultsWrapper/UserDefaultsSubscript.swift create mode 100644 14th-team5-iOS/Core/Sources/BBStorage/UserDefaultsWrapper/UserDefaultsWrapper.swift diff --git a/14th-team5-iOS/Core/Sources/BBStorage/KeychainWrapper/KeychainWrapper.swift b/14th-team5-iOS/Core/Sources/BBStorage/KeychainWrapper/KeychainWrapper.swift index a09ff7b7c..c76c65355 100644 --- a/14th-team5-iOS/Core/Sources/BBStorage/KeychainWrapper/KeychainWrapper.swift +++ b/14th-team5-iOS/Core/Sources/BBStorage/KeychainWrapper/KeychainWrapper.swift @@ -7,7 +7,7 @@ import Foundation -public class KeychainWrapper { +final public class KeychainWrapper { // MARK: - Properties public static let standard = KeychainWrapper() diff --git a/14th-team5-iOS/Core/Sources/BBStorage/UserDefaultsWrapper/UserDefaultsKey.swift b/14th-team5-iOS/Core/Sources/BBStorage/UserDefaultsWrapper/UserDefaultsKey.swift new file mode 100644 index 000000000..1b1f06075 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/BBStorage/UserDefaultsWrapper/UserDefaultsKey.swift @@ -0,0 +1,14 @@ +// +// UserDefaultsKey.swift +// UserDefaultsWrapper +// +// Created by 김건우 on 5/14/24. +// + +import Foundation + +public extension UserDefaultsWrapper.Key { + + static let name: Self = "name" + +} diff --git a/14th-team5-iOS/Core/Sources/BBStorage/UserDefaultsWrapper/UserDefaultsSubscript.swift b/14th-team5-iOS/Core/Sources/BBStorage/UserDefaultsWrapper/UserDefaultsSubscript.swift new file mode 100644 index 000000000..adae29c23 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/BBStorage/UserDefaultsWrapper/UserDefaultsSubscript.swift @@ -0,0 +1,129 @@ +// +// UserDefaultsSubscript.swift +// UserDefaultsWrapper +// +// Created by 김건우 on 5/14/24. +// + +import Foundation + +public extension UserDefaultsWrapper { + + subscript(key: Key) -> Int? { + get { integer(forKey: key) } + set { + guard let value = newValue else { return } + set(value, forKey: key.rawValue) + } + } + + subscript(key: Key) -> Float? { + get { float(forKey: key) } + set { + guard let value = newValue else { return } + set(value, forKey: key.rawValue) + } + } + + subscript(key: Key) -> Double? { + get { double(forKey: key) } + set { + guard let value = newValue else { return } + set(value, forKey: key.rawValue) + } + } + + subscript(key: Key) -> Bool? { + get { bool(forKey: key) } + set { + guard let value = newValue else { return } + set(value, forKey: key.rawValue) + } + } + + subscript(key: Key) -> String? { + get { string(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, of: T.self) } + set { + guard let value = newValue else { return } + set(value, forKey: key.rawValue) + } + } + + subscript(key: Key) -> Data? { + get { data(forKey: key) } + set { + guard let value = newValue else { return } + set(value, forKey: key.rawValue) + } + } + +} + +public extension UserDefaultsWrapper { + + func integer(forKey key: Key) -> Int? { + integer(forKey: key.rawValue) + } + + func float(forKey key: Key) -> Float? { + float(forKey: key.rawValue) + } + + func double(forKey key: Key) -> Double? { + double(forKey: key.rawValue) + } + + func bool(forKey key: Key) -> Bool? { + bool(forKey: key.rawValue) + } + + func string(forKey key: Key) -> String? { + string(forKey: key.rawValue) + } + + func object( + forKey key: Key, + of: T.Type + ) -> T? where T: Decodable { + object(forKey: key.rawValue, of: T.self) + } + + func data(forKey key: Key) -> Data? { + data(forKey: key.rawValue) + } + +} + +public extension UserDefaultsWrapper { + + func remove(forKey key: Key) { + remove(forKey: key.rawValue) + } + +} + +public extension UserDefaultsWrapper { + + struct Key: Hashable, RawRepresentable, ExpressibleByStringLiteral { + + var rawValue: String + + init(rawValue: RawValue) { + self.rawValue = rawValue + } + + init(stringLiteral value: String) { + self.rawValue = value + } + + } + +} diff --git a/14th-team5-iOS/Core/Sources/BBStorage/UserDefaultsWrapper/UserDefaultsWrapper.swift b/14th-team5-iOS/Core/Sources/BBStorage/UserDefaultsWrapper/UserDefaultsWrapper.swift new file mode 100644 index 000000000..f7c152e0b --- /dev/null +++ b/14th-team5-iOS/Core/Sources/BBStorage/UserDefaultsWrapper/UserDefaultsWrapper.swift @@ -0,0 +1,169 @@ +// +// main.swift +// UserDefaultsWrapper +// +// Created by 김건우 on 5/14/24. +// + +import Foundation + +final public class UserDefaultsWrapper { + + // MARK: - Properties + public static let standard = UserDefaultsWrapper() + + private let userDefaults: UserDefaults! + + private(set) public var suitName: String + + private static let defaultSuitName: String = { + Bundle.main.bundleIdentifier ?? "UserDefaultsWrapper" + }() + + // MARK: - Intializer + convenience init() { + self.init(suitName: UserDefaultsWrapper.defaultSuitName) + } + + init(suitName: String) { + self.suitName = suitName + self.userDefaults = UserDefaults(suiteName: suitName) + } + + + // MARK: - Create + + public func set( + _ value: Int, + forKey key: String + ) { + userDefaults.set(value, forKey: key) + } + + public func set( + _ value: Float, + forKey key: String + ) { + userDefaults.set(value, forKey: key) + } + + public func set( + _ value: Double, + forKey key: String + ) { + userDefaults.set(value, forKey: key) + } + + public func set( + _ value: Bool, + forKey key: String + ) { + userDefaults.set(value, forKey: key) + } + + public func set( + _ value: String, + forKey key: String + ) { + if let data = value.data(using: .utf8) { + set(data, forKey: key) + } else { + return + } + } + + public func set( + _ value: T, + forKey key: String + ) where T: Encodable { + if let data = try? PropertyListEncoder().encode(value) { + set(data, forKey: key) + } else { + return + } + } + + public func set( + _ value: Data, + forKey key: String + ) { + userDefaults.set(value, forKey: key) + } + + + // MARK: - Read + + public func integer(forKey key: String) -> Int? { + guard let value = userDefaults.value(forKey: key) as? Int else { + return nil + } + return value + } + + public func float(forKey key: String) -> Float? { + guard let value = userDefaults.value(forKey: key) as? Float else { + return nil + } + return value + } + + public func double(forKey key: String) -> Double? { + guard let value = userDefaults.value(forKey: key) as? Double else { + return nil + } + return value + } + + public func bool(forKey key: String) -> Bool? { + guard let value = userDefaults.value(forKey: key) as? Bool else { + return nil + } + return value + } + + public func string(forKey key: String) -> String? { + guard let data = data(forKey: key) else { + return nil + } + + if let value = String(data: data, encoding: .utf8) { + return value + } else { + return nil + } + } + + public func object( + forKey key: String, + of type: T.Type + ) -> T? where T: Decodable { + guard let data = data(forKey: key) else { + return nil + } + + if let value = try? PropertyListDecoder().decode(type, from: data) { + return value + } else { + return nil + } + } + + public func data(forKey key: String) -> Data? { + guard let value = userDefaults.value(forKey: key) as? Data else { + return nil + } + return value + } + + + // MARK: - Delete + + public func remove(forKey key: String) { + userDefaults.removeObject(forKey: key) + } + + public func removeAll() { + userDefaults.removePersistentDomain(forName: suitName) + } + +} From 49d9ce90791632567712c3f94398b66b6e3d0337 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Mon, 20 May 2024 13:57:40 +0900 Subject: [PATCH 056/263] =?UTF-8?q?feat:=20InMemoryWrapper=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#530)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../InMemoryWrapper/InMemoryStore.swift | 48 +++++++++++++++ .../InMemoryWrapper/InMemoryWrapper.swift | 59 +++++++++++++++++++ .../UserDefaultsWrapper/UserDefaultsKey.swift | 3 +- 3 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 14th-team5-iOS/Core/Sources/BBStorage/InMemoryWrapper/InMemoryStore.swift create mode 100644 14th-team5-iOS/Core/Sources/BBStorage/InMemoryWrapper/InMemoryWrapper.swift diff --git a/14th-team5-iOS/Core/Sources/BBStorage/InMemoryWrapper/InMemoryStore.swift b/14th-team5-iOS/Core/Sources/BBStorage/InMemoryWrapper/InMemoryStore.swift new file mode 100644 index 000000000..32b40c29f --- /dev/null +++ b/14th-team5-iOS/Core/Sources/BBStorage/InMemoryWrapper/InMemoryStore.swift @@ -0,0 +1,48 @@ +// +// InMemoryStore.swift +// Core +// +// Created by 김건우 on 5/20/24. +// + +import Foundation + +import RxSwift +import RxRelay + +public struct InMemoryStore: ~Copyable { + + // MARK: - Properties + private var relay = BehaviorRelay(value: nil) + + // MARK: - Intializer + public init( + initalValue: T? = nil + ) { + self.relay.accept(initalValue) + } + + // MARK: - Deinitalizer + + deinit { relay.accept(nil) } + + + + // MARK: - Read + + public func value() -> T? { + relay.value + } + + public var stream: BehaviorRelay { relay } + + + // MARK: - Update + + public func set( + _ value: T? + ) { + relay.accept(value) + } + +} diff --git a/14th-team5-iOS/Core/Sources/BBStorage/InMemoryWrapper/InMemoryWrapper.swift b/14th-team5-iOS/Core/Sources/BBStorage/InMemoryWrapper/InMemoryWrapper.swift new file mode 100644 index 000000000..2dfe310ed --- /dev/null +++ b/14th-team5-iOS/Core/Sources/BBStorage/InMemoryWrapper/InMemoryWrapper.swift @@ -0,0 +1,59 @@ +// +// InMemoryWrapper.swift +// Core +// +// Created by 김건우 on 5/20/24. +// + +import Foundation + +import RxSwift +import RxCocoa + +final public class InMemoryWrapper { + + // MARK: - Properties + public static var standard = InMemoryWrapper() + + private var userDefaults = UserDefaultsWrapper() + + private var disposeBag = DisposeBag() + + + + + // MARK: - InMemoryStores + public var familyId = InMemoryStore() + public var familyCreatedAt = InMemoryStore() + + + + + + + // MARK: - Intializer + private init() { } + + // MARK: - Bind + public func bind() { + familyId.stream + .compactMap { $0 } + .bind(with: self) { owner, id in + owner.userDefaults[.familyId] = id + } + .disposed(by: disposeBag) + + familyCreatedAt.stream + .compactMap { $0 } + .bind(with: self) { owner, createdAt in + owner.userDefaults[.familyCreatedAt] = createdAt + } + .disposed(by: disposeBag) + } + + // MARK: - Unbind + public func unbind() { + disposeBag = DisposeBag() + } + +} diff --git a/14th-team5-iOS/Core/Sources/BBStorage/UserDefaultsWrapper/UserDefaultsKey.swift b/14th-team5-iOS/Core/Sources/BBStorage/UserDefaultsWrapper/UserDefaultsKey.swift index 1b1f06075..5c81a5006 100644 --- a/14th-team5-iOS/Core/Sources/BBStorage/UserDefaultsWrapper/UserDefaultsKey.swift +++ b/14th-team5-iOS/Core/Sources/BBStorage/UserDefaultsWrapper/UserDefaultsKey.swift @@ -9,6 +9,7 @@ import Foundation public extension UserDefaultsWrapper.Key { - static let name: Self = "name" + static let familyId: Self = "FamilyId" + static let familyCreatedAt: Self = "FamilyCreatedAt" } From 36015d22f541620af5b684832da60dc0c6470479 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Mon, 20 May 2024 14:02:54 +0900 Subject: [PATCH 057/263] =?UTF-8?q?feat:=20Shimmer=20Modifier=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#530)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Core/Sources/Modifiers/Shimmer.swift | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 14th-team5-iOS/Core/Sources/Modifiers/Shimmer.swift diff --git a/14th-team5-iOS/Core/Sources/Modifiers/Shimmer.swift b/14th-team5-iOS/Core/Sources/Modifiers/Shimmer.swift new file mode 100644 index 000000000..afea31ce9 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Modifiers/Shimmer.swift @@ -0,0 +1,100 @@ +// +// Shimmer.swift +// Core +// +// Created by 김건우 on 5/20/24. +// + +import Foundation +import SwiftUI + +struct Shimmer: ViewModifier { + private let animation: Animation + private let gradient: Gradient + private let min, max: CGFloat + @State private var isIntialState = true + @Environment(\.layoutDirection) private var layoutDirection + + init( + animation: Animation = Self.defaultAnimation, + gradient: Gradient = Self.defaultGradient, + bandSize: CGFloat = 0.3 + ) { + self.animation = animation + self.gradient = gradient + self.min = 0 - bandSize + self.max = 1 + bandSize + } + + static let defaultAnimation = Animation.linear(duration: 1.5).delay(0.25).repeatForever(autoreverses: false) + + static let defaultGradient = Gradient(colors: [ + Color.black.opacity(0.3), + Color.black, + Color.black.opacity(0.3) + ]) + + /* + Calculating the gradient's animated start and end unit points: + min,min + \ + ┌───────┐ ┌───────┐ + │0,0 │ Animate │ │ "forward" gradient + LTR │ │ ───────►│ 1,1│ / // / + └───────┘ └───────┘ + \ + max,max + max,min + / + ┌───────┐ ┌───────┐ + │ 1,0│ Animate │ │ "backward" gradient + RTL │ │ ───────►│0,1 │ \ \\ \ + └───────┘ └───────┘ + / + min,max + */ + + var startPoint: UnitPoint { + if layoutDirection == .rightToLeft { + return isIntialState ? UnitPoint(x: max, y: min) : UnitPoint(x: 0, y: 1) + } else { + return isIntialState ? UnitPoint(x: min, y: min) : UnitPoint(x: 1, y: 1) + } + } + + var endPoint: UnitPoint { + if layoutDirection == .rightToLeft { + return isIntialState ? UnitPoint(x: 1, y: 0) : UnitPoint(x: min, y: max) + } else { + return isIntialState ? UnitPoint(x: 0, y: 0) : UnitPoint(x: max, y: max) + } + } + + func body(content: Content) -> some View { + content + .mask(LinearGradient(gradient: gradient, startPoint: startPoint, endPoint: endPoint)) + .animation(animation, value: isIntialState) + .onAppear { + DispatchQueue.main.asyncAfter(deadline: .now()) { + isIntialState = false + } + } + } +} + +// MARK: - Extensions +extension View { + @ViewBuilder + func shimmering( + active: Bool = true, + animation: Animation = Shimmer.defaultAnimation, + gradient: Gradient = Shimmer.defaultGradient, + bandSize: CGFloat = 0.3 + ) -> some View { + if active { + modifier(Shimmer(animation: animation, gradient: gradient, bandSize: bandSize)) + } else { + self + } + } +} From 6172e0bfee53da6ac5e1591de6bf542940964b5a Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Mon, 20 May 2024 14:16:00 +0900 Subject: [PATCH 058/263] =?UTF-8?q?feat:=20=EB=8C=93=EA=B8=80=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20Haptic=20=ED=94=BC=EB=93=9C=EB=B0=B1=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#530)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PostDetail/Reactor/ReactionViewReactor.swift | 14 +++++++++++--- .../ViewControllers/ReactionViewController.swift | 7 +++++++ .../KeychainWrapper/KeychainWrapperSubscript.swift | 2 +- .../UserDefaultsSubscript.swift | 6 +++--- 4 files changed, 22 insertions(+), 7 deletions(-) diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/ReactionViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/ReactionViewReactor.swift index 25e1cc874..fe2038554 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/ReactionViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/ReactionViewReactor.swift @@ -33,6 +33,7 @@ final class ReactionViewReactor: Reactor { case setPost(PostListData) case setPostCommentCount(Int) case setInitialDataSource + case setSelectionHapticFeedback } struct State { @@ -49,6 +50,7 @@ final class ReactionViewReactor: Reactor { @Pulse var reactionMemberSheetEmoji: FetchedEmojiData? = nil @Pulse var isShowingCommentSheet: Bool = false @Pulse var isShowingEmojiSheet: Bool = false + @Pulse var selectionHapticFeedback: Bool = false } let initialState: State @@ -88,7 +90,10 @@ extension ReactionViewReactor { case .emptyAction: return Observable.empty() case .tapComment: - return Observable.just(Mutation.setCommentSheet) + return Observable.merge( + Observable.just(.setSelectionHapticFeedback), + Observable.just(.setCommentSheet) + ) case .tapAddEmoji: return Observable.just(Mutation.setEmojiSheet) case .longPressEmoji(let indexPath): @@ -143,7 +148,9 @@ extension ReactionViewReactor { switch item { case .addComment(var currentCount): currentCount = count - newState.reactionSections = ReactionSection.Model(model: newState.reactionSections.model, items: [.addComment(currentCount)] + newState.reactionSections.items.filter { item in + newState.reactionSections = ReactionSection.Model( + model: newState.reactionSections.model, + items: [.addComment(currentCount)] + newState.reactionSections.items.filter { item in if case .addReaction = item { return true } else { @@ -154,7 +161,8 @@ extension ReactionViewReactor { break } } - + case .setSelectionHapticFeedback: + newState.selectionHapticFeedback = true } return newState } diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/ReactionViewController.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/ReactionViewController.swift index 18ae11aaf..da8b7d8f2 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/ReactionViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/ReactionViewController.swift @@ -167,6 +167,13 @@ extension ReactionViewController { } }) .disposed(by: disposeBag) + + reactor.pulse(\.$selectionHapticFeedback) + .filter { $0 } + .bind { _ in + Haptic.selection() + } + .disposed(by: disposeBag) } private func presentCustomSheetViewController( diff --git a/14th-team5-iOS/Core/Sources/BBStorage/KeychainWrapper/KeychainWrapperSubscript.swift b/14th-team5-iOS/Core/Sources/BBStorage/KeychainWrapper/KeychainWrapperSubscript.swift index dd1dae637..694d796ea 100644 --- a/14th-team5-iOS/Core/Sources/BBStorage/KeychainWrapper/KeychainWrapperSubscript.swift +++ b/14th-team5-iOS/Core/Sources/BBStorage/KeychainWrapper/KeychainWrapperSubscript.swift @@ -25,7 +25,7 @@ public extension KeychainWrapper { } } - public subscript(key: Key) -> Double? { + subscript(key: Key) -> Double? { get { double(forKey: key) } set { guard let value = newValue else { return } diff --git a/14th-team5-iOS/Core/Sources/BBStorage/UserDefaultsWrapper/UserDefaultsSubscript.swift b/14th-team5-iOS/Core/Sources/BBStorage/UserDefaultsWrapper/UserDefaultsSubscript.swift index adae29c23..cf3381172 100644 --- a/14th-team5-iOS/Core/Sources/BBStorage/UserDefaultsWrapper/UserDefaultsSubscript.swift +++ b/14th-team5-iOS/Core/Sources/BBStorage/UserDefaultsWrapper/UserDefaultsSubscript.swift @@ -114,13 +114,13 @@ public extension UserDefaultsWrapper { struct Key: Hashable, RawRepresentable, ExpressibleByStringLiteral { - var rawValue: String + public var rawValue: String - init(rawValue: RawValue) { + public init(rawValue: RawValue) { self.rawValue = rawValue } - init(stringLiteral value: String) { + public init(stringLiteral value: String) { self.rawValue = value } From 250df836da9c745d776eef3fe3071082a00cff8f Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Mon, 20 May 2024 14:32:02 +0900 Subject: [PATCH 059/263] =?UTF-8?q?feat:=20CalendarBanner=EC=97=90=20Shimm?= =?UTF-8?q?er=20Effect=20=ED=9A=A8=EA=B3=BC=20=EC=A0=81=EC=9A=A9=20(#530)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Calendar/Reactor/ViewModel/BannerViewModel.swift | 7 +++++++ .../Presentation/Calendar/View/BannerView.swift | 6 ++++-- 14th-team5-iOS/Core/Sources/Modifiers/Shimmer.swift | 10 +++++----- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/ViewModel/BannerViewModel.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/ViewModel/BannerViewModel.swift index 0b5f3b6f8..d69a2b6b1 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/ViewModel/BannerViewModel.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/ViewModel/BannerViewModel.swift @@ -9,6 +9,11 @@ import Core import SwiftUI public final class BannerViewModel: BaseViewModel { + + // MARK: - Properties + @Published var shimmeringActive: Bool = true + + // MARK: - State public struct State: ViewModelState { var familyTopPercentage: Int = 0 var allFamilyMemberUploadedDays: Int = 0 @@ -17,11 +22,13 @@ public final class BannerViewModel: BaseViewModel some View { + public func body(content: Content) -> some View { content .mask(LinearGradient(gradient: gradient, startPoint: startPoint, endPoint: endPoint)) .animation(animation, value: isIntialState) @@ -83,7 +83,7 @@ struct Shimmer: ViewModifier { } // MARK: - Extensions -extension View { +public extension View { @ViewBuilder func shimmering( active: Bool = true, From 76f13004a441a97a8e46b433712ade4ef7c88c30 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Mon, 20 May 2024 15:09:33 +0900 Subject: [PATCH 060/263] =?UTF-8?q?feat:=20shimmerView=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#530)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../View/Cell/CalendarImageCell.swift | 7 ------ .../Calendar/View/ShimmeringView.swift | 25 +++++++++++++++++++ 2 files changed, 25 insertions(+), 7 deletions(-) create mode 100644 14th-team5-iOS/App/Sources/Presentation/Calendar/View/ShimmeringView.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarImageCell.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarImageCell.swift index f5cf31849..90d532d28 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarImageCell.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarImageCell.swift @@ -183,10 +183,3 @@ final public class CalendarImageCell: FSCalendarCell, ReactorKit.View { } } } - -// MARK: - Extensions -extension CalendarImageCell { - var hasThumbnailImage: Bool { - return thumbnailView.image != nil ? true : false - } -} diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/ShimmeringView.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/ShimmeringView.swift new file mode 100644 index 000000000..643f043c0 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/ShimmeringView.swift @@ -0,0 +1,25 @@ +// +// CalendarPlaceholderCell.swift +// App +// +// Created by 김건우 on 5/20/24. +// + +import SwiftUI + +struct ShimmeringView: View { + + // MARK: - Body + var body: some View { + Color.gray.opacity(0.1) + .shimmering() + } + +} + +// MARK: - Preview +struct ShimmeringView_Previews: PreviewProvider { + static var previews: some View { + ShimmeringView() + } +} From c14dd461da3e5198b092dab61b28aca6742b561b Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Mon, 20 May 2024 15:10:19 +0900 Subject: [PATCH 061/263] =?UTF-8?q?Revert=20"feat:=20shimmerView=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#530)"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 76f13004a441a97a8e46b433712ade4ef7c88c30. --- .../View/Cell/CalendarImageCell.swift | 7 ++++++ .../Calendar/View/ShimmeringView.swift | 25 ------------------- 2 files changed, 7 insertions(+), 25 deletions(-) delete mode 100644 14th-team5-iOS/App/Sources/Presentation/Calendar/View/ShimmeringView.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarImageCell.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarImageCell.swift index 90d532d28..f5cf31849 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarImageCell.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarImageCell.swift @@ -183,3 +183,10 @@ final public class CalendarImageCell: FSCalendarCell, ReactorKit.View { } } } + +// MARK: - Extensions +extension CalendarImageCell { + var hasThumbnailImage: Bool { + return thumbnailView.image != nil ? true : false + } +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/ShimmeringView.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/ShimmeringView.swift deleted file mode 100644 index 643f043c0..000000000 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/ShimmeringView.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// CalendarPlaceholderCell.swift -// App -// -// Created by 김건우 on 5/20/24. -// - -import SwiftUI - -struct ShimmeringView: View { - - // MARK: - Body - var body: some View { - Color.gray.opacity(0.1) - .shimmering() - } - -} - -// MARK: - Preview -struct ShimmeringView_Previews: PreviewProvider { - static var previews: some View { - ShimmeringView() - } -} From a3682456a08a682466e9428ac8a098125cd77022 Mon Sep 17 00:00:00 2001 From: Do-hyun-Kim Date: Fri, 31 May 2024 19:02:41 +0900 Subject: [PATCH 062/263] =?UTF-8?q?fix:=20MainViewController=20segmentCont?= =?UTF-8?q?rol=20UserInteractionEnabled=20=EC=B2=98=EB=A6=AC=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=A0=9C=EA=B1=B0=20-=20SegmentPageViewController?= =?UTF-8?q?=20isPagingEnabled=20Property=20=EC=BD=94=EB=93=9C=20or=20Deleg?= =?UTF-8?q?ate=20Method=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../App/Resources/PrivacyInfo.xcprivacy | 72 +++++++++++++++++++ .../ViewControllers/MainViewController.swift | 6 -- .../SegmentPageViewController.swift | 10 --- 3 files changed, 72 insertions(+), 16 deletions(-) create mode 100644 14th-team5-iOS/App/Resources/PrivacyInfo.xcprivacy diff --git a/14th-team5-iOS/App/Resources/PrivacyInfo.xcprivacy b/14th-team5-iOS/App/Resources/PrivacyInfo.xcprivacy new file mode 100644 index 000000000..e48ca94b3 --- /dev/null +++ b/14th-team5-iOS/App/Resources/PrivacyInfo.xcprivacy @@ -0,0 +1,72 @@ + + + + + NSPrivacyTracking + + NSPrivacyTrackingDomains + + NSPrivacyCollectedDataTypes + + + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypePhotosorVideos + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeAppFunctionality + + + + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypeOtherUserContactInfo + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeAppFunctionality + + + + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypeProductInteraction + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeAnalytics + + + + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypeUserID + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeAppFunctionality + + + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryUserDefaults + NSPrivacyAccessedAPITypeReasons + + 1C8F.1 + + + + + \ No newline at end of file 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 9b55fbca2..18aa7e4bd 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift @@ -151,12 +151,6 @@ extension MainViewController { .bind(to: reactor.action) .disposed(by: disposeBag) - pageViewController.segmentEnabled - .distinctUntilChanged() - .observe(on: MainScheduler.instance) - .bind(to: segmentControl.rx.isUserInteractionEnabled) - .disposed(by: disposeBag) - alertConfirmRelay.map { Reactor.Action.pickConfirmButtonTapped($0.0, $0.1) } .bind(to: reactor.action) .disposed(by: disposeBag) diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/SegmentPageViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/SegmentPageViewController.swift index 33c915f01..5476df541 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/SegmentPageViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/SegmentPageViewController.swift @@ -30,7 +30,6 @@ final class SegmentPageViewController: UIPageViewController { private lazy var pages: [UIViewController] = [survivalViewController, missionViewController] let indexRelay: BehaviorRelay = BehaviorRelay(value: .init(way: .segmentTap, index: 0)) - let segmentEnabled: BehaviorRelay = BehaviorRelay(value: true) override func viewDidLoad() { super.viewDidLoad() @@ -48,7 +47,6 @@ final class SegmentPageViewController: UIPageViewController { .withUnretained(self) .observe(on: MainScheduler.instance) .bind(onNext: { - $0.0.isPagingEnabled = false switch $0.1 { case 0: $0.0.setViewControllers([$0.0.survivalViewController], direction: .reverse, animated: true) { [weak self] _ in self?.isPagingEnabled = true @@ -66,10 +64,6 @@ final class SegmentPageViewController: UIPageViewController { } extension SegmentPageViewController: UIPageViewControllerDelegate, UIPageViewControllerDataSource { - func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) { - segmentEnabled.accept(false) - } - func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { guard let index = pages.firstIndex(of: viewController), index - 1 >= 0 else { return nil } @@ -89,9 +83,5 @@ extension SegmentPageViewController: UIPageViewControllerDelegate, UIPageViewCon let currentIndex = pages.firstIndex(of: currentViewController) { indexRelay.accept(.init(way: .scroll, index: currentIndex)) } - - if completed && finished { - segmentEnabled.accept(true) - } } } From 379dcdb65ff7bb466666074def19cb5530ba727c Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Sun, 2 Jun 2024 19:48:49 +0900 Subject: [PATCH 063/263] =?UTF-8?q?feat:=20InMemoryWrapper=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84,=20=ED=8C=8C=EC=9D=BC=20=EC=9E=AC=EA=B5=AC=ED=99=94?= =?UTF-8?q?=20(#537)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../App/Sources/Application/AppDelegate.swift | 1 + .../Activity/CopyInvitationUrlActivity.swift | 2 +- .../Activity/UrlActivityItemSource.swift | 0 .../BibbiAlertBuilder/BibbiAlertAction.swift | 0 .../BibbiAlertBuilder/BibbiAlertBuilder.swift | 0 .../BibbiAlertBuilder/BibbiAlertStyle.swift | 0 .../BibbiAlertBuilder/BibbiAlertTitle.swift | 0 .../BibbiAlertViewController.swift | 0 .../BibbiButton/BibbiButton.swift | 0 .../BibbiLabel/BibbiLabel.swift | 0 .../BibbiNavigationBarView.swift | 0 .../RxBibbiNavigationDelegateProxy.swift | 0 .../BibbiSegmentedControl.swift | 0 .../BibbiToastMessageView.swift | 0 .../Lottie/BibbiLoadingView.swift | 0 .../Lottie/BibbiLottileIndicator.swift | 0 .../{ => BBCommons}/Lottie/LottieType.swift | 0 .../{ => BBCommons}/Lottie/LottieView.swift | 0 .../Popover/BibbiPopOverView.swift | 0 .../DescriptionPopoverViewController.swift | 0 .../ShareView/BibbiInquireBannerView.swift | 0 .../ShareView/BibbiMissionView.swift | 0 .../ShareView/BibbiProfileView.swift | 0 .../ShareView/BibbiTermsView.swift | 0 .../{ => BBRx/Repositories}/App/App.swift | 0 .../DeepLink/DeepLinkRepository.swift | 0 .../Models/NotificationDeepLink.swift | 0 .../DeepLink/Models/WidgetDeepLink.swift | 0 .../Repositories/Member}/Emojis.swift | 0 .../Member/MemberRepository.swift | 0 .../Repositories}/Token/TokenRepository.swift | 0 .../Sources/{Reactive => BBRx}/RxObject.swift | 0 .../Sources/{Reactive => BBRx}/RxUtils.swift | 0 .../ActivityGlobalState.swift | 2 +- .../CalendarGlobalState.swift | 2 +- .../GlobalStateProvider.swift | 0 .../MainService.swift | 2 +- .../PostGlobalState.swift | 2 +- .../ProfileFeedGlobalState.swift | 2 +- .../ProfileGlobalState.swift | 2 +- .../RealEmojiGlobalState.swift | 2 +- .../TimerGlobalState.swift | 2 +- .../ToastMessageGlobalState.swift | 2 +- .../InMemoryWrapper/InMemoryWrapper.swift | 59 ---- .../InMemoryWrapper/InMemoryKey.swift | 14 + .../InMemoryWrapper/InMemoryStore.swift | 22 +- .../InMemoryWrapper/InMemorySubscript.swift | 164 ++++++++++ .../InMemoryWrapper/InMemoryType.swift | 25 ++ .../InMemoryWrapper/InMemoryWrapper.swift | 295 ++++++++++++++++++ .../KeychainItemAccessibility.swift | 0 .../KeychainWrapper/KeychainType.swift | 18 ++ .../KeychainWrapper/KeychainWrapper.swift | 0 .../KeychainWrapper/KeychainWrapperKey.swift | 0 .../KeychainWrapperSubscript.swift | 0 .../UserDefaultsWrapper/UserDefaultsKey.swift | 6 +- .../UserDefaultsSubscript.swift | 0 .../UserDefaultsType.swift | 18 ++ .../UserDefaultsWrapper.swift | 0 ...aseGlobalState.swift => BaseService.swift} | 2 +- .../Mixpanel/MixPanelService+Account.swift | 0 .../Mixpanel/MixPanelService+Camera.swift | 0 .../Mixpanel/MixPanelService+Family.swift | 0 .../Mixpanel/MixPanelService+Home.swift | 0 .../Analytics}/Mixpanel/MixpanelService.swift | 0 .../Sources/{Haptic => Utilies}/Haptic.swift | 0 .../{ => Utilies}/Modifiers/Shimmer.swift | 0 .../Types/Enums}/URLTypes.swift | 0 .../Types/Protocols}/ItemIndexable.swift | 0 .../CreatePostCommentReqeustDTO.swift | 0 ...nationResponsePostCommentResponseDTO.swift | 0 .../PostCommentDeleteResponseDTO.swift | 0 .../DataMapping/PostCommentResponseDTO.swift | 0 .../UpdatePostCommentRequestDTO.swift | 0 .../CommentAPI}/PostCommentAPIWorker.swift | 0 .../Comment/CommentAPI}/PostCommentAPIs.swift | 0 .../Repository/PostCommentRepository.swift | 0 .../Storages/Keychain/Keychain+Token.swift | 31 ++ .../UserDefaults/UserDefaults+Family.swift | 35 +++ .../xcschemes/Bibbi-Workspace.xcscheme | 106 +++---- fastlane/README.md | 12 +- 80 files changed, 689 insertions(+), 139 deletions(-) rename 14th-team5-iOS/Core/Sources/{ => BBCommons}/Activity/CopyInvitationUrlActivity.swift (98%) rename 14th-team5-iOS/Core/Sources/{ => BBCommons}/Activity/UrlActivityItemSource.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi => BBCommons}/BibbiAlertBuilder/BibbiAlertAction.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi => BBCommons}/BibbiAlertBuilder/BibbiAlertBuilder.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi => BBCommons}/BibbiAlertBuilder/BibbiAlertStyle.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi => BBCommons}/BibbiAlertBuilder/BibbiAlertTitle.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi => BBCommons}/BibbiAlertBuilder/BibbiAlertViewController.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi => BBCommons}/BibbiButton/BibbiButton.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi => BBCommons}/BibbiLabel/BibbiLabel.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi => BBCommons}/BibbiNavigationBarView/BibbiNavigationBarView.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi => BBCommons}/BibbiNavigationBarView/DelegateProxy/RxBibbiNavigationDelegateProxy.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi => BBCommons}/BibbiSegmentedControl/BibbiSegmentedControl.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi => BBCommons}/BibbiToastMessageView/BibbiToastMessageView.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => BBCommons}/Lottie/BibbiLoadingView.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => BBCommons}/Lottie/BibbiLottileIndicator.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => BBCommons}/Lottie/LottieType.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => BBCommons}/Lottie/LottieView.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => BBCommons}/Popover/BibbiPopOverView.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => BBCommons}/Popover/DescriptionPopoverViewController.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => BBCommons}/ShareView/BibbiInquireBannerView.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => BBCommons}/ShareView/BibbiMissionView.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => BBCommons}/ShareView/BibbiProfileView.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => BBCommons}/ShareView/BibbiTermsView.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => BBRx/Repositories}/App/App.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => BBRx/Repositories}/DeepLink/DeepLinkRepository.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => BBRx/Repositories}/DeepLink/Models/NotificationDeepLink.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => BBRx/Repositories}/DeepLink/Models/WidgetDeepLink.swift (100%) rename 14th-team5-iOS/Core/Sources/{GlobalState => BBRx/Repositories/Member}/Emojis.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => BBRx/Repositories}/Member/MemberRepository.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => BBRx/Repositories}/Token/TokenRepository.swift (100%) rename 14th-team5-iOS/Core/Sources/{Reactive => BBRx}/RxObject.swift (100%) rename 14th-team5-iOS/Core/Sources/{Reactive => BBRx}/RxUtils.swift (100%) rename 14th-team5-iOS/Core/Sources/{GlobalState => BBServices}/ActivityGlobalState.swift (91%) rename 14th-team5-iOS/Core/Sources/{GlobalState => BBServices}/CalendarGlobalState.swift (93%) rename 14th-team5-iOS/Core/Sources/{GlobalState => BBServices}/GlobalStateProvider.swift (100%) rename 14th-team5-iOS/Core/Sources/{GlobalState => BBServices}/MainService.swift (94%) rename 14th-team5-iOS/Core/Sources/{GlobalState => BBServices}/PostGlobalState.swift (96%) rename 14th-team5-iOS/Core/Sources/{GlobalState => BBServices}/ProfileFeedGlobalState.swift (92%) rename 14th-team5-iOS/Core/Sources/{GlobalState => BBServices}/ProfileGlobalState.swift (88%) rename 14th-team5-iOS/Core/Sources/{GlobalState => BBServices}/RealEmojiGlobalState.swift (94%) rename 14th-team5-iOS/Core/Sources/{GlobalState => BBServices}/TimerGlobalState.swift (90%) rename 14th-team5-iOS/Core/Sources/{GlobalState => BBServices}/ToastMessageGlobalState.swift (92%) delete mode 100644 14th-team5-iOS/Core/Sources/BBStorage/InMemoryWrapper/InMemoryWrapper.swift create mode 100644 14th-team5-iOS/Core/Sources/BBStorages/InMemoryWrapper/InMemoryKey.swift rename 14th-team5-iOS/Core/Sources/{BBStorage => BBStorages}/InMemoryWrapper/InMemoryStore.swift (59%) create mode 100644 14th-team5-iOS/Core/Sources/BBStorages/InMemoryWrapper/InMemorySubscript.swift create mode 100644 14th-team5-iOS/Core/Sources/BBStorages/InMemoryWrapper/InMemoryType.swift create mode 100644 14th-team5-iOS/Core/Sources/BBStorages/InMemoryWrapper/InMemoryWrapper.swift rename 14th-team5-iOS/Core/Sources/{BBStorage => BBStorages}/KeychainWrapper/KeychainItemAccessibility.swift (100%) create mode 100644 14th-team5-iOS/Core/Sources/BBStorages/KeychainWrapper/KeychainType.swift rename 14th-team5-iOS/Core/Sources/{BBStorage => BBStorages}/KeychainWrapper/KeychainWrapper.swift (100%) rename 14th-team5-iOS/Core/Sources/{BBStorage => BBStorages}/KeychainWrapper/KeychainWrapperKey.swift (100%) rename 14th-team5-iOS/Core/Sources/{BBStorage => BBStorages}/KeychainWrapper/KeychainWrapperSubscript.swift (100%) rename 14th-team5-iOS/Core/Sources/{BBStorage => BBStorages}/UserDefaultsWrapper/UserDefaultsKey.swift (53%) rename 14th-team5-iOS/Core/Sources/{BBStorage => BBStorages}/UserDefaultsWrapper/UserDefaultsSubscript.swift (100%) create mode 100644 14th-team5-iOS/Core/Sources/BBStorages/UserDefaultsWrapper/UserDefaultsType.swift rename 14th-team5-iOS/Core/Sources/{BBStorage => BBStorages}/UserDefaultsWrapper/UserDefaultsWrapper.swift (100%) rename 14th-team5-iOS/Core/Sources/Base/{BaseGlobalState.swift => BaseService.swift} (89%) rename 14th-team5-iOS/Core/Sources/{ => Utilies/Analytics}/Mixpanel/MixPanelService+Account.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Utilies/Analytics}/Mixpanel/MixPanelService+Camera.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Utilies/Analytics}/Mixpanel/MixPanelService+Family.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Utilies/Analytics}/Mixpanel/MixPanelService+Home.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Utilies/Analytics}/Mixpanel/MixpanelService.swift (100%) rename 14th-team5-iOS/Core/Sources/{Haptic => Utilies}/Haptic.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Utilies}/Modifiers/Shimmer.swift (100%) rename 14th-team5-iOS/Core/Sources/{Common/Types => Utilies/Types/Enums}/URLTypes.swift (100%) rename 14th-team5-iOS/Core/Sources/{Common/Protocol => Utilies/Types/Protocols}/ItemIndexable.swift (100%) rename 14th-team5-iOS/Data/Sources/{PostComment/PostCommentAPI => APIs/Comment/CommentAPI}/DataMapping/CreatePostCommentReqeustDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/{PostComment/PostCommentAPI => APIs/Comment/CommentAPI}/DataMapping/PaginationResponsePostCommentResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/{PostComment/PostCommentAPI => APIs/Comment/CommentAPI}/DataMapping/PostCommentDeleteResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/{PostComment/PostCommentAPI => APIs/Comment/CommentAPI}/DataMapping/PostCommentResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/{PostComment/PostCommentAPI => APIs/Comment/CommentAPI}/DataMapping/UpdatePostCommentRequestDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/{PostComment/PostCommentAPI => APIs/Comment/CommentAPI}/PostCommentAPIWorker.swift (100%) rename 14th-team5-iOS/Data/Sources/{PostComment/PostCommentAPI => APIs/Comment/CommentAPI}/PostCommentAPIs.swift (100%) rename 14th-team5-iOS/Data/Sources/{PostComment/PostCommentAPI => APIs/Comment/CommentAPI}/Repository/PostCommentRepository.swift (100%) create mode 100644 14th-team5-iOS/Data/Sources/Storages/Keychain/Keychain+Token.swift create mode 100644 14th-team5-iOS/Data/Sources/Storages/UserDefaults/UserDefaults+Family.swift diff --git a/14th-team5-iOS/App/Sources/Application/AppDelegate.swift b/14th-team5-iOS/App/Sources/Application/AppDelegate.swift index 8d3605d45..b768f1703 100644 --- a/14th-team5-iOS/App/Sources/Application/AppDelegate.swift +++ b/14th-team5-iOS/App/Sources/Application/AppDelegate.swift @@ -148,6 +148,7 @@ extension AppDelegate: MessagingDelegate { debugPrint("Firebase registration token: \(token)") + // TODO: - UseCase, APIWorker 삭제하기 let useCase = FCMUseCase(FCMRepository: MeAPIs.Worker()) useCase.executeSavingFCMToken(token: .init(fcmToken: token)) .asObservable() diff --git a/14th-team5-iOS/Core/Sources/Activity/CopyInvitationUrlActivity.swift b/14th-team5-iOS/Core/Sources/BBCommons/Activity/CopyInvitationUrlActivity.swift similarity index 98% rename from 14th-team5-iOS/Core/Sources/Activity/CopyInvitationUrlActivity.swift rename to 14th-team5-iOS/Core/Sources/BBCommons/Activity/CopyInvitationUrlActivity.swift index 8e22921c2..eda4181dd 100644 --- a/14th-team5-iOS/Core/Sources/Activity/CopyInvitationUrlActivity.swift +++ b/14th-team5-iOS/Core/Sources/BBCommons/Activity/CopyInvitationUrlActivity.swift @@ -8,7 +8,7 @@ import UIKit public class CopyInvitationUrlActivity: UIActivity { - enum Activity { + private enum Activity { static let activityTitle: String = "초대 링크 복사" static let activitySymbol: String = "doc.on.doc" } diff --git a/14th-team5-iOS/Core/Sources/Activity/UrlActivityItemSource.swift b/14th-team5-iOS/Core/Sources/BBCommons/Activity/UrlActivityItemSource.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Activity/UrlActivityItemSource.swift rename to 14th-team5-iOS/Core/Sources/BBCommons/Activity/UrlActivityItemSource.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BibbiAlertBuilder/BibbiAlertAction.swift b/14th-team5-iOS/Core/Sources/BBCommons/BibbiAlertBuilder/BibbiAlertAction.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/BibbiAlertBuilder/BibbiAlertAction.swift rename to 14th-team5-iOS/Core/Sources/BBCommons/BibbiAlertBuilder/BibbiAlertAction.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BibbiAlertBuilder/BibbiAlertBuilder.swift b/14th-team5-iOS/Core/Sources/BBCommons/BibbiAlertBuilder/BibbiAlertBuilder.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/BibbiAlertBuilder/BibbiAlertBuilder.swift rename to 14th-team5-iOS/Core/Sources/BBCommons/BibbiAlertBuilder/BibbiAlertBuilder.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BibbiAlertBuilder/BibbiAlertStyle.swift b/14th-team5-iOS/Core/Sources/BBCommons/BibbiAlertBuilder/BibbiAlertStyle.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/BibbiAlertBuilder/BibbiAlertStyle.swift rename to 14th-team5-iOS/Core/Sources/BBCommons/BibbiAlertBuilder/BibbiAlertStyle.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BibbiAlertBuilder/BibbiAlertTitle.swift b/14th-team5-iOS/Core/Sources/BBCommons/BibbiAlertBuilder/BibbiAlertTitle.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/BibbiAlertBuilder/BibbiAlertTitle.swift rename to 14th-team5-iOS/Core/Sources/BBCommons/BibbiAlertBuilder/BibbiAlertTitle.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BibbiAlertBuilder/BibbiAlertViewController.swift b/14th-team5-iOS/Core/Sources/BBCommons/BibbiAlertBuilder/BibbiAlertViewController.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/BibbiAlertBuilder/BibbiAlertViewController.swift rename to 14th-team5-iOS/Core/Sources/BBCommons/BibbiAlertBuilder/BibbiAlertViewController.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BibbiButton/BibbiButton.swift b/14th-team5-iOS/Core/Sources/BBCommons/BibbiButton/BibbiButton.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/BibbiButton/BibbiButton.swift rename to 14th-team5-iOS/Core/Sources/BBCommons/BibbiButton/BibbiButton.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BibbiLabel/BibbiLabel.swift b/14th-team5-iOS/Core/Sources/BBCommons/BibbiLabel/BibbiLabel.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/BibbiLabel/BibbiLabel.swift rename to 14th-team5-iOS/Core/Sources/BBCommons/BibbiLabel/BibbiLabel.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BibbiNavigationBarView/BibbiNavigationBarView.swift b/14th-team5-iOS/Core/Sources/BBCommons/BibbiNavigationBarView/BibbiNavigationBarView.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/BibbiNavigationBarView/BibbiNavigationBarView.swift rename to 14th-team5-iOS/Core/Sources/BBCommons/BibbiNavigationBarView/BibbiNavigationBarView.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BibbiNavigationBarView/DelegateProxy/RxBibbiNavigationDelegateProxy.swift b/14th-team5-iOS/Core/Sources/BBCommons/BibbiNavigationBarView/DelegateProxy/RxBibbiNavigationDelegateProxy.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/BibbiNavigationBarView/DelegateProxy/RxBibbiNavigationDelegateProxy.swift rename to 14th-team5-iOS/Core/Sources/BBCommons/BibbiNavigationBarView/DelegateProxy/RxBibbiNavigationDelegateProxy.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BibbiSegmentedControl/BibbiSegmentedControl.swift b/14th-team5-iOS/Core/Sources/BBCommons/BibbiSegmentedControl/BibbiSegmentedControl.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/BibbiSegmentedControl/BibbiSegmentedControl.swift rename to 14th-team5-iOS/Core/Sources/BBCommons/BibbiSegmentedControl/BibbiSegmentedControl.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BibbiToastMessageView/BibbiToastMessageView.swift b/14th-team5-iOS/Core/Sources/BBCommons/BibbiToastMessageView/BibbiToastMessageView.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/BibbiToastMessageView/BibbiToastMessageView.swift rename to 14th-team5-iOS/Core/Sources/BBCommons/BibbiToastMessageView/BibbiToastMessageView.swift diff --git a/14th-team5-iOS/Core/Sources/Lottie/BibbiLoadingView.swift b/14th-team5-iOS/Core/Sources/BBCommons/Lottie/BibbiLoadingView.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Lottie/BibbiLoadingView.swift rename to 14th-team5-iOS/Core/Sources/BBCommons/Lottie/BibbiLoadingView.swift diff --git a/14th-team5-iOS/Core/Sources/Lottie/BibbiLottileIndicator.swift b/14th-team5-iOS/Core/Sources/BBCommons/Lottie/BibbiLottileIndicator.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Lottie/BibbiLottileIndicator.swift rename to 14th-team5-iOS/Core/Sources/BBCommons/Lottie/BibbiLottileIndicator.swift diff --git a/14th-team5-iOS/Core/Sources/Lottie/LottieType.swift b/14th-team5-iOS/Core/Sources/BBCommons/Lottie/LottieType.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Lottie/LottieType.swift rename to 14th-team5-iOS/Core/Sources/BBCommons/Lottie/LottieType.swift diff --git a/14th-team5-iOS/Core/Sources/Lottie/LottieView.swift b/14th-team5-iOS/Core/Sources/BBCommons/Lottie/LottieView.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Lottie/LottieView.swift rename to 14th-team5-iOS/Core/Sources/BBCommons/Lottie/LottieView.swift diff --git a/14th-team5-iOS/Core/Sources/Popover/BibbiPopOverView.swift b/14th-team5-iOS/Core/Sources/BBCommons/Popover/BibbiPopOverView.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Popover/BibbiPopOverView.swift rename to 14th-team5-iOS/Core/Sources/BBCommons/Popover/BibbiPopOverView.swift diff --git a/14th-team5-iOS/Core/Sources/Popover/DescriptionPopoverViewController.swift b/14th-team5-iOS/Core/Sources/BBCommons/Popover/DescriptionPopoverViewController.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Popover/DescriptionPopoverViewController.swift rename to 14th-team5-iOS/Core/Sources/BBCommons/Popover/DescriptionPopoverViewController.swift diff --git a/14th-team5-iOS/Core/Sources/ShareView/BibbiInquireBannerView.swift b/14th-team5-iOS/Core/Sources/BBCommons/ShareView/BibbiInquireBannerView.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/ShareView/BibbiInquireBannerView.swift rename to 14th-team5-iOS/Core/Sources/BBCommons/ShareView/BibbiInquireBannerView.swift diff --git a/14th-team5-iOS/Core/Sources/ShareView/BibbiMissionView.swift b/14th-team5-iOS/Core/Sources/BBCommons/ShareView/BibbiMissionView.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/ShareView/BibbiMissionView.swift rename to 14th-team5-iOS/Core/Sources/BBCommons/ShareView/BibbiMissionView.swift diff --git a/14th-team5-iOS/Core/Sources/ShareView/BibbiProfileView.swift b/14th-team5-iOS/Core/Sources/BBCommons/ShareView/BibbiProfileView.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/ShareView/BibbiProfileView.swift rename to 14th-team5-iOS/Core/Sources/BBCommons/ShareView/BibbiProfileView.swift diff --git a/14th-team5-iOS/Core/Sources/ShareView/BibbiTermsView.swift b/14th-team5-iOS/Core/Sources/BBCommons/ShareView/BibbiTermsView.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/ShareView/BibbiTermsView.swift rename to 14th-team5-iOS/Core/Sources/BBCommons/ShareView/BibbiTermsView.swift diff --git a/14th-team5-iOS/Core/Sources/App/App.swift b/14th-team5-iOS/Core/Sources/BBRx/Repositories/App/App.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/App/App.swift rename to 14th-team5-iOS/Core/Sources/BBRx/Repositories/App/App.swift diff --git a/14th-team5-iOS/Core/Sources/DeepLink/DeepLinkRepository.swift b/14th-team5-iOS/Core/Sources/BBRx/Repositories/DeepLink/DeepLinkRepository.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/DeepLink/DeepLinkRepository.swift rename to 14th-team5-iOS/Core/Sources/BBRx/Repositories/DeepLink/DeepLinkRepository.swift diff --git a/14th-team5-iOS/Core/Sources/DeepLink/Models/NotificationDeepLink.swift b/14th-team5-iOS/Core/Sources/BBRx/Repositories/DeepLink/Models/NotificationDeepLink.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/DeepLink/Models/NotificationDeepLink.swift rename to 14th-team5-iOS/Core/Sources/BBRx/Repositories/DeepLink/Models/NotificationDeepLink.swift diff --git a/14th-team5-iOS/Core/Sources/DeepLink/Models/WidgetDeepLink.swift b/14th-team5-iOS/Core/Sources/BBRx/Repositories/DeepLink/Models/WidgetDeepLink.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/DeepLink/Models/WidgetDeepLink.swift rename to 14th-team5-iOS/Core/Sources/BBRx/Repositories/DeepLink/Models/WidgetDeepLink.swift diff --git a/14th-team5-iOS/Core/Sources/GlobalState/Emojis.swift b/14th-team5-iOS/Core/Sources/BBRx/Repositories/Member/Emojis.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/GlobalState/Emojis.swift rename to 14th-team5-iOS/Core/Sources/BBRx/Repositories/Member/Emojis.swift diff --git a/14th-team5-iOS/Core/Sources/Member/MemberRepository.swift b/14th-team5-iOS/Core/Sources/BBRx/Repositories/Member/MemberRepository.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Member/MemberRepository.swift rename to 14th-team5-iOS/Core/Sources/BBRx/Repositories/Member/MemberRepository.swift diff --git a/14th-team5-iOS/Core/Sources/Token/TokenRepository.swift b/14th-team5-iOS/Core/Sources/BBRx/Repositories/Token/TokenRepository.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Token/TokenRepository.swift rename to 14th-team5-iOS/Core/Sources/BBRx/Repositories/Token/TokenRepository.swift diff --git a/14th-team5-iOS/Core/Sources/Reactive/RxObject.swift b/14th-team5-iOS/Core/Sources/BBRx/RxObject.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Reactive/RxObject.swift rename to 14th-team5-iOS/Core/Sources/BBRx/RxObject.swift diff --git a/14th-team5-iOS/Core/Sources/Reactive/RxUtils.swift b/14th-team5-iOS/Core/Sources/BBRx/RxUtils.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Reactive/RxUtils.swift rename to 14th-team5-iOS/Core/Sources/BBRx/RxUtils.swift diff --git a/14th-team5-iOS/Core/Sources/GlobalState/ActivityGlobalState.swift b/14th-team5-iOS/Core/Sources/BBServices/ActivityGlobalState.swift similarity index 91% rename from 14th-team5-iOS/Core/Sources/GlobalState/ActivityGlobalState.swift rename to 14th-team5-iOS/Core/Sources/BBServices/ActivityGlobalState.swift index cf4545713..eee0cfdf1 100644 --- a/14th-team5-iOS/Core/Sources/GlobalState/ActivityGlobalState.swift +++ b/14th-team5-iOS/Core/Sources/BBServices/ActivityGlobalState.swift @@ -20,7 +20,7 @@ public protocol ActivityGlobalStateType { func hiddenInvitationUrlIndicatorView(_ hidden: Bool) -> Observable } -final public class ActivityGlobalState: BaseGlobalState, ActivityGlobalStateType { +final public class ActivityGlobalState: BaseService, ActivityGlobalStateType { public var event: PublishSubject = PublishSubject() public func didTapCopyInvitationUrlAction() -> Observable { diff --git a/14th-team5-iOS/Core/Sources/GlobalState/CalendarGlobalState.swift b/14th-team5-iOS/Core/Sources/BBServices/CalendarGlobalState.swift similarity index 93% rename from 14th-team5-iOS/Core/Sources/GlobalState/CalendarGlobalState.swift rename to 14th-team5-iOS/Core/Sources/BBServices/CalendarGlobalState.swift index 80a693e70..c0e77f802 100644 --- a/14th-team5-iOS/Core/Sources/GlobalState/CalendarGlobalState.swift +++ b/14th-team5-iOS/Core/Sources/BBServices/CalendarGlobalState.swift @@ -28,7 +28,7 @@ public protocol CalendarGlobalStateType { func didTapCalendarInfoButton(_ sourceView: UIView) -> Observable } -final public class CalendarGlobalState: BaseGlobalState, CalendarGlobalStateType { +final public class CalendarGlobalState: BaseService, CalendarGlobalStateType { public var event: BehaviorSubject = BehaviorSubject(value: .none) public func pushCalendarPostVC(_ date: Date) -> Observable { diff --git a/14th-team5-iOS/Core/Sources/GlobalState/GlobalStateProvider.swift b/14th-team5-iOS/Core/Sources/BBServices/GlobalStateProvider.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/GlobalState/GlobalStateProvider.swift rename to 14th-team5-iOS/Core/Sources/BBServices/GlobalStateProvider.swift diff --git a/14th-team5-iOS/Core/Sources/GlobalState/MainService.swift b/14th-team5-iOS/Core/Sources/BBServices/MainService.swift similarity index 94% rename from 14th-team5-iOS/Core/Sources/GlobalState/MainService.swift rename to 14th-team5-iOS/Core/Sources/BBServices/MainService.swift index 8d9f3e403..4bd6ae8a0 100644 --- a/14th-team5-iOS/Core/Sources/GlobalState/MainService.swift +++ b/14th-team5-iOS/Core/Sources/BBServices/MainService.swift @@ -26,7 +26,7 @@ public protocol MainServiceType { func refreshMain() -> Observable } -final public class MainService: BaseGlobalState, MainServiceType { +final public class MainService: BaseService, MainServiceType { public var event = PublishSubject() @discardableResult diff --git a/14th-team5-iOS/Core/Sources/GlobalState/PostGlobalState.swift b/14th-team5-iOS/Core/Sources/BBServices/PostGlobalState.swift similarity index 96% rename from 14th-team5-iOS/Core/Sources/GlobalState/PostGlobalState.swift rename to 14th-team5-iOS/Core/Sources/BBServices/PostGlobalState.swift index 81b589f15..b8d5981d6 100644 --- a/14th-team5-iOS/Core/Sources/GlobalState/PostGlobalState.swift +++ b/14th-team5-iOS/Core/Sources/BBServices/PostGlobalState.swift @@ -33,7 +33,7 @@ public protocol PostGlobalStateType { func missionContentText(_ content: String) -> Observable } -final public class PostGlobalState: BaseGlobalState, PostGlobalStateType { +final public class PostGlobalState: BaseService, PostGlobalStateType { public var input: BehaviorSubject<(String, String)> = BehaviorSubject<(String, String)>(value: ("", "")) public var event: PublishSubject = PublishSubject() diff --git a/14th-team5-iOS/Core/Sources/GlobalState/ProfileFeedGlobalState.swift b/14th-team5-iOS/Core/Sources/BBServices/ProfileFeedGlobalState.swift similarity index 92% rename from 14th-team5-iOS/Core/Sources/GlobalState/ProfileFeedGlobalState.swift rename to 14th-team5-iOS/Core/Sources/BBServices/ProfileFeedGlobalState.swift index d5fd90d43..a8fde1cc2 100644 --- a/14th-team5-iOS/Core/Sources/GlobalState/ProfileFeedGlobalState.swift +++ b/14th-team5-iOS/Core/Sources/BBServices/ProfileFeedGlobalState.swift @@ -24,7 +24,7 @@ public protocol ProfileFeedGlobalStateType { func didReceiveMemberId(memberId: BibbiFeedType) -> Observable } -public final class ProfileFeedGlobalState: BaseGlobalState, ProfileFeedGlobalStateType { +public final class ProfileFeedGlobalState: BaseService, ProfileFeedGlobalStateType { public var event: PublishSubject = PublishSubject() diff --git a/14th-team5-iOS/Core/Sources/GlobalState/ProfileGlobalState.swift b/14th-team5-iOS/Core/Sources/BBServices/ProfileGlobalState.swift similarity index 88% rename from 14th-team5-iOS/Core/Sources/GlobalState/ProfileGlobalState.swift rename to 14th-team5-iOS/Core/Sources/BBServices/ProfileGlobalState.swift index 871044b2a..473b1bf4d 100644 --- a/14th-team5-iOS/Core/Sources/GlobalState/ProfileGlobalState.swift +++ b/14th-team5-iOS/Core/Sources/BBServices/ProfileGlobalState.swift @@ -20,7 +20,7 @@ public protocol ProfileGlobalStateType { func refreshFamilyMembers() -> Observable } -final public class ProfileGlobalState: BaseGlobalState, ProfileGlobalStateType { +final public class ProfileGlobalState: BaseService, ProfileGlobalStateType { public var event: PublishSubject = PublishSubject() public func refreshFamilyMembers() -> Observable { diff --git a/14th-team5-iOS/Core/Sources/GlobalState/RealEmojiGlobalState.swift b/14th-team5-iOS/Core/Sources/BBServices/RealEmojiGlobalState.swift similarity index 94% rename from 14th-team5-iOS/Core/Sources/GlobalState/RealEmojiGlobalState.swift rename to 14th-team5-iOS/Core/Sources/BBServices/RealEmojiGlobalState.swift index 736fb3bd5..e2adb6ce1 100644 --- a/14th-team5-iOS/Core/Sources/GlobalState/RealEmojiGlobalState.swift +++ b/14th-team5-iOS/Core/Sources/BBServices/RealEmojiGlobalState.swift @@ -27,7 +27,7 @@ public protocol RealEmojiGlobalStateType { } -public final class RealEmojiGlobalState: BaseGlobalState, RealEmojiGlobalStateType { +public final class RealEmojiGlobalState: BaseService, RealEmojiGlobalStateType { public var event: PublishSubject = PublishSubject() public func didTapRealEmojiEvent(indexPath row: Int) -> RxSwift.Observable { diff --git a/14th-team5-iOS/Core/Sources/GlobalState/TimerGlobalState.swift b/14th-team5-iOS/Core/Sources/BBServices/TimerGlobalState.swift similarity index 90% rename from 14th-team5-iOS/Core/Sources/GlobalState/TimerGlobalState.swift rename to 14th-team5-iOS/Core/Sources/BBServices/TimerGlobalState.swift index c0be0b21b..6601ff750 100644 --- a/14th-team5-iOS/Core/Sources/GlobalState/TimerGlobalState.swift +++ b/14th-team5-iOS/Core/Sources/BBServices/TimerGlobalState.swift @@ -23,7 +23,7 @@ public protocol TimerGlobalStateType { func setInTime() -> Observable } -final public class TimerGlobalState: BaseGlobalState, TimerGlobalStateType { +final public class TimerGlobalState: BaseService, TimerGlobalStateType { public var event: BehaviorSubject = BehaviorSubject(value: .inTime) public func setNotTime() -> RxSwift.Observable { diff --git a/14th-team5-iOS/Core/Sources/GlobalState/ToastMessageGlobalState.swift b/14th-team5-iOS/Core/Sources/BBServices/ToastMessageGlobalState.swift similarity index 92% rename from 14th-team5-iOS/Core/Sources/GlobalState/ToastMessageGlobalState.swift rename to 14th-team5-iOS/Core/Sources/BBServices/ToastMessageGlobalState.swift index 186a537ee..f5ff37497 100644 --- a/14th-team5-iOS/Core/Sources/GlobalState/ToastMessageGlobalState.swift +++ b/14th-team5-iOS/Core/Sources/BBServices/ToastMessageGlobalState.swift @@ -24,7 +24,7 @@ public protocol ToastMessageGlobalStateType { func clearLastSelectedDate() } -final public class ToastMessageGlobalState: BaseGlobalState, ToastMessageGlobalStateType { +final public class ToastMessageGlobalState: BaseService, ToastMessageGlobalStateType { public var lastSelectedDate: Date = .distantFuture public var event: BehaviorSubject = BehaviorSubject(value: .showAllFamilyUploadedToastView(false)) diff --git a/14th-team5-iOS/Core/Sources/BBStorage/InMemoryWrapper/InMemoryWrapper.swift b/14th-team5-iOS/Core/Sources/BBStorage/InMemoryWrapper/InMemoryWrapper.swift deleted file mode 100644 index 2dfe310ed..000000000 --- a/14th-team5-iOS/Core/Sources/BBStorage/InMemoryWrapper/InMemoryWrapper.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// InMemoryWrapper.swift -// Core -// -// Created by 김건우 on 5/20/24. -// - -import Foundation - -import RxSwift -import RxCocoa - -final public class InMemoryWrapper { - - // MARK: - Properties - public static var standard = InMemoryWrapper() - - private var userDefaults = UserDefaultsWrapper() - - private var disposeBag = DisposeBag() - - - - - // MARK: - InMemoryStores - public var familyId = InMemoryStore() - public var familyCreatedAt = InMemoryStore() - - - - - - - // MARK: - Intializer - private init() { } - - // MARK: - Bind - public func bind() { - familyId.stream - .compactMap { $0 } - .bind(with: self) { owner, id in - owner.userDefaults[.familyId] = id - } - .disposed(by: disposeBag) - - familyCreatedAt.stream - .compactMap { $0 } - .bind(with: self) { owner, createdAt in - owner.userDefaults[.familyCreatedAt] = createdAt - } - .disposed(by: disposeBag) - } - - // MARK: - Unbind - public func unbind() { - disposeBag = DisposeBag() - } - -} diff --git a/14th-team5-iOS/Core/Sources/BBStorages/InMemoryWrapper/InMemoryKey.swift b/14th-team5-iOS/Core/Sources/BBStorages/InMemoryWrapper/InMemoryKey.swift new file mode 100644 index 000000000..1ef79d637 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/BBStorages/InMemoryWrapper/InMemoryKey.swift @@ -0,0 +1,14 @@ +// +// InMemoryKey.swift +// Hello +// +// Created by 김건우 on 6/2/24. +// + +import Foundation + +extension InMemoryWrapper.Key { + + static let member: Self = "member" + +} diff --git a/14th-team5-iOS/Core/Sources/BBStorage/InMemoryWrapper/InMemoryStore.swift b/14th-team5-iOS/Core/Sources/BBStorages/InMemoryWrapper/InMemoryStore.swift similarity index 59% rename from 14th-team5-iOS/Core/Sources/BBStorage/InMemoryWrapper/InMemoryStore.swift rename to 14th-team5-iOS/Core/Sources/BBStorages/InMemoryWrapper/InMemoryStore.swift index 32b40c29f..f1e245171 100644 --- a/14th-team5-iOS/Core/Sources/BBStorage/InMemoryWrapper/InMemoryStore.swift +++ b/14th-team5-iOS/Core/Sources/BBStorages/InMemoryWrapper/InMemoryStore.swift @@ -1,8 +1,8 @@ // // InMemoryStore.swift -// Core +// Hello // -// Created by 김건우 on 5/20/24. +// Created by 김건우 on 6/2/24. // import Foundation @@ -10,10 +10,13 @@ import Foundation import RxSwift import RxRelay -public struct InMemoryStore: ~Copyable { +public struct InMemoryStore { + + // MARK: - Typealias + public typealias Relay = BehaviorRelay // MARK: - Properties - private var relay = BehaviorRelay(value: nil) + private var relay = Relay(value: nil) // MARK: - Intializer public init( @@ -22,19 +25,13 @@ public struct InMemoryStore: ~Copyable { self.relay.accept(initalValue) } - // MARK: - Deinitalizer - - deinit { relay.accept(nil) } - - - // MARK: - Read - public func value() -> T? { + public var value: T? { relay.value } - public var stream: BehaviorRelay { relay } + public var stream: Relay { relay } // MARK: - Update @@ -46,3 +43,4 @@ public struct InMemoryStore: ~Copyable { } } + diff --git a/14th-team5-iOS/Core/Sources/BBStorages/InMemoryWrapper/InMemorySubscript.swift b/14th-team5-iOS/Core/Sources/BBStorages/InMemoryWrapper/InMemorySubscript.swift new file mode 100644 index 000000000..9c670af16 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/BBStorages/InMemoryWrapper/InMemorySubscript.swift @@ -0,0 +1,164 @@ +// +// InMemorySubscript.swift +// Hello +// +// Created by 김건우 on 6/2/24. +// + +import Foundation + +import RxSwift +import RxRelay + + +extension InMemoryWrapper { + + func registerInt(_ value: Int?, forKey key: Key) { + register(value, forKey: key.rawValue) + } + + func registerFloat(_ value: Float?, forKey key: Key) { + register(value, forKey: key.rawValue) + } + + func registerDouble(_ value: Double?, forKey key: Key) { + register(value, forKey: key.rawValue) + } + + func registerBool(_ value: Bool?, forKey key: Key) { + register(value, forKey: key.rawValue) + } + + func registerObject(_ value: T?, forKey key: Key) where T: Encodable { + register(value, forKey: key.rawValue) + } + + func registerData(_ value: Data?, forKey key: Key) { + register(value, forKey: key.rawValue) + } + +} + + +extension InMemoryWrapper { + + subscript(key: Key) -> Int? { + get { integer(forKey: key.rawValue) } + set { + guard let value = newValue else { return } + set(value, forKey: key.rawValue) + } + } + + subscript(key: Key) -> Float? { + get { float(forKey: key.rawValue) } + set { + guard let value = newValue else { return } + set(value, forKey: key.rawValue) + } + } + + subscript(key: Key) -> Double? { + get { double(forKey: key.rawValue) } + set { + guard let value = newValue else { return } + set(value, forKey: key.rawValue) + } + } + + subscript(key: Key) -> Bool? { + get { bool(forKey: key.rawValue) } + set { + guard let value = newValue else { return } + set(value, forKey: key.rawValue) + } + } + + subscript(key: Key) -> String? { + get { string(forKey: key.rawValue) } + set { + guard let value = newValue else { return } + set(value, forKey: key.rawValue) + } + } + + subscript(key: Key) -> T? where T: Codable { + get { object(forKey: key.rawValue, of: T.self) } + set { + guard let value = newValue else { return } + set(value, forKey: key.rawValue) + } + } + +} + + +extension InMemoryWrapper { + + subscript(stream key: Key) -> Observable? { + return stream(forKey: key.rawValue) + } + + subscript( + stream key: Key, + of type: T.Type + ) -> Observable? where T: Decodable { + return stream(forKey: key.rawValue, of: type) + } + +} + + +extension InMemoryWrapper { + + func integer(forKey key: Key) -> Int? { + integer(forKey: key.rawValue) + } + + func float(forKey key: Key) -> Float? { + float(forKey: key.rawValue) + } + + func double(forKey key: Key) -> Double? { + double(forKey: key.rawValue) + } + + func bool(forKey key: Key) -> Bool? { + bool(forKey: key.rawValue) + } + + func object(forKey key: Key, of type: T.Type) -> T? where T: Decodable { + object(forKey: key.rawValue, of: T.self) + } + +} + + + +extension InMemoryWrapper { + + func remove(forKey key: Key) { + remove(forKey: key.rawValue) + } + +} + + + +extension InMemoryWrapper { + + public struct Key: Hashable, RawRepresentable, ExpressibleByStringLiteral { + + public var rawValue: String + + public init(rawValue: String) { + self.rawValue = rawValue + } + + public init(stringLiteral value: StringLiteralType) { + self.rawValue = value + } + + } + +} diff --git a/14th-team5-iOS/Core/Sources/BBStorages/InMemoryWrapper/InMemoryType.swift b/14th-team5-iOS/Core/Sources/BBStorages/InMemoryWrapper/InMemoryType.swift new file mode 100644 index 000000000..4f5e24312 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/BBStorages/InMemoryWrapper/InMemoryType.swift @@ -0,0 +1,25 @@ +// +// InMemoryType.swift +// Hello +// +// Created by 김건우 on 6/2/24. +// + +import Foundation + +import RxSwift + +public protocol InMemoryType { + var disposeBag: DisposeBag { get } + + var inMemory: InMemoryWrapper { get } + + func bind() + func unbind() +} + +extension InMemoryType { + public var inMemory: InMemoryWrapper { + InMemoryWrapper.standard + } +} diff --git a/14th-team5-iOS/Core/Sources/BBStorages/InMemoryWrapper/InMemoryWrapper.swift b/14th-team5-iOS/Core/Sources/BBStorages/InMemoryWrapper/InMemoryWrapper.swift new file mode 100644 index 000000000..77cadbc72 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/BBStorages/InMemoryWrapper/InMemoryWrapper.swift @@ -0,0 +1,295 @@ +// +// InMemoryWraper.swift +// Hello +// +// Created by 김건우 on 6/2/24. +// + +import Foundation + +import RxSwift +import RxCocoa + +final public class InMemoryWrapper { + + // MARK: - Typealias + public typealias Store = InMemoryStore + + // MARK: - Properties + public static var standard = InMemoryWrapper() + + + + // MARK: - Dictionary + private var relays: [String: Store] = [:] + + + + // MARK: - Intializer + private init() { } + + + + // MARK: - Register + + public func register( + _ value: Int?, + forKey key: String + ) { + if relays[key] == nil { + relays[key] = Store(initalValue: value) + } + } + + public func register( + _ value: Float?, + forKey key: String + ) { + if relays[key] == nil { + relays[key] = Store(initalValue: value) + } + } + + public func register( + _ value: Double?, + forKey key: String + ) { + if relays[key] == nil { + relays[key] = Store(initalValue: value) + } + } + + public func register( + _ value: Bool?, + forKey key: String + ) { + if relays[key] == nil { + relays[key] = Store(initalValue: value) + } + } + + public func register( + _ value: T?, + forKey key: String + ) where T: Encodable { + if let data = try? JSONEncoder().encode(value) { + register(data, forKey: key) + } + } + + public func register( + _ data: Data?, + forKey key: String + ) { + if relays[key] == nil { + relays[key] = Store(initalValue: data) + } + } + + + + // MARK: - Stream + + public func stream( + forKey key: String + ) -> Observable? { + return relays[key]?.stream.asObservable() + .compactMap { $0 as? T } + } + + public func stream( + forKey key: String, + of type: T.Type + ) -> Observable? where T: Decodable { + return relays[key]?.stream.asObservable() + .compactMap { + if let data = $0 as? Data, + let value = try? JSONDecoder().decode(type, from: data) { + return value + } + return nil + } + } + + + + // MARK: - Read + + public func integer( + forKey key: String + ) -> Int? { + if let value = relays[key]?.value as? Int { + return value + } + return nil + } + + public func float( + forKey key: String + ) -> Float? { + if let value = relays[key]?.value as? Float { + return value + } + return nil + } + + public func double( + forKey key: String + ) -> Double? { + if let value = relays[key]?.value as? Double { + return value + } + return nil + } + + public func bool( + forKey key: String + ) -> Bool? { + if let value = relays[key]?.value as? Bool { + return value + } + return nil + } + + public func string( + forKey key: String + ) -> String? { + if let value = relays[key]?.value as? String { + return value + } + return nil + } + + public func object( + forKey key: String, + of type: T.Type + ) -> T? where T: Decodable { + if let data = data(forKey: key), + let value = try? JSONDecoder().decode(type, from: data) { + return value + } + return nil + } + + + public func data( + forKey key: String + ) -> Data? { + if let data = relays[key]?.value as? Data { + return data + } + return nil + } + + + + // MARK: - Update + + @discardableResult + public func set( + _ value: Int, + forKey key: String + ) -> Bool { + if let relay = relays[key] { + relay.set(value) + return true + } + return false + } + + @discardableResult + public func set( + _ value: Float, + forKey key: String + ) -> Bool { + if let relay = relays[key] { + relay.set(value) + return true + } + return false + } + + @discardableResult + public func set( + _ value: Double, + forKey key: String + ) -> Bool { + if let relay = relays[key] { + relay.set(value) + return true + } + return false + } + + @discardableResult + public func set( + _ value: Bool, + forKey key: String + ) -> Bool { + if let relay = relays[key] { + relay.set(value) + return true + } + return false + } + + @discardableResult + public func set( + _ value: String, + forKey key: String + ) -> Bool { + if let relay = relays[key] { + relay.set(value) + return true + } + return false + } + + @discardableResult + public func set( + _ value: T, + forKey key: String + ) -> Bool where T: Encodable { + if let data = try? JSONEncoder().encode(value) { + set(data, forKey: key) + return true + } + return false + } + + @discardableResult + public func set( + _ value: Data, + forKey key: String + ) -> Bool { + if let relay = relays[key] { + relay.set(value) + return true + } + return false + } + + + // MARK: - Delete + + public func remove( + forKey key: String + ) { + if let relay = relays[key] { + relay.set(nil) + } + } + + public func removeAll() { + for key in relays.keys { + remove(forKey: key) + } + } + + + + // MARK: - Helper + + + +} diff --git a/14th-team5-iOS/Core/Sources/BBStorage/KeychainWrapper/KeychainItemAccessibility.swift b/14th-team5-iOS/Core/Sources/BBStorages/KeychainWrapper/KeychainItemAccessibility.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBStorage/KeychainWrapper/KeychainItemAccessibility.swift rename to 14th-team5-iOS/Core/Sources/BBStorages/KeychainWrapper/KeychainItemAccessibility.swift diff --git a/14th-team5-iOS/Core/Sources/BBStorages/KeychainWrapper/KeychainType.swift b/14th-team5-iOS/Core/Sources/BBStorages/KeychainWrapper/KeychainType.swift new file mode 100644 index 000000000..ccd1a1967 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/BBStorages/KeychainWrapper/KeychainType.swift @@ -0,0 +1,18 @@ +// +// KeychainType.swift +// Core +// +// Created by 김건우 on 6/2/24. +// + +import Foundation + +public protocol KeychainType { + var keychain: KeychainWrapper { get } +} + +extension KeychainType { + public var keychain: KeychainWrapper { + KeychainWrapper.standard + } +} diff --git a/14th-team5-iOS/Core/Sources/BBStorage/KeychainWrapper/KeychainWrapper.swift b/14th-team5-iOS/Core/Sources/BBStorages/KeychainWrapper/KeychainWrapper.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBStorage/KeychainWrapper/KeychainWrapper.swift rename to 14th-team5-iOS/Core/Sources/BBStorages/KeychainWrapper/KeychainWrapper.swift diff --git a/14th-team5-iOS/Core/Sources/BBStorage/KeychainWrapper/KeychainWrapperKey.swift b/14th-team5-iOS/Core/Sources/BBStorages/KeychainWrapper/KeychainWrapperKey.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBStorage/KeychainWrapper/KeychainWrapperKey.swift rename to 14th-team5-iOS/Core/Sources/BBStorages/KeychainWrapper/KeychainWrapperKey.swift diff --git a/14th-team5-iOS/Core/Sources/BBStorage/KeychainWrapper/KeychainWrapperSubscript.swift b/14th-team5-iOS/Core/Sources/BBStorages/KeychainWrapper/KeychainWrapperSubscript.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBStorage/KeychainWrapper/KeychainWrapperSubscript.swift rename to 14th-team5-iOS/Core/Sources/BBStorages/KeychainWrapper/KeychainWrapperSubscript.swift diff --git a/14th-team5-iOS/Core/Sources/BBStorage/UserDefaultsWrapper/UserDefaultsKey.swift b/14th-team5-iOS/Core/Sources/BBStorages/UserDefaultsWrapper/UserDefaultsKey.swift similarity index 53% rename from 14th-team5-iOS/Core/Sources/BBStorage/UserDefaultsWrapper/UserDefaultsKey.swift rename to 14th-team5-iOS/Core/Sources/BBStorages/UserDefaultsWrapper/UserDefaultsKey.swift index 5c81a5006..178158f38 100644 --- a/14th-team5-iOS/Core/Sources/BBStorage/UserDefaultsWrapper/UserDefaultsKey.swift +++ b/14th-team5-iOS/Core/Sources/BBStorages/UserDefaultsWrapper/UserDefaultsKey.swift @@ -9,7 +9,9 @@ import Foundation public extension UserDefaultsWrapper.Key { - static let familyId: Self = "FamilyId" - static let familyCreatedAt: Self = "FamilyCreatedAt" + static let familyId: Self = "familyId" + static let familyCreatedAt: Self = "familyCreatedAt" + + static let memberId: Self = "memberId" } diff --git a/14th-team5-iOS/Core/Sources/BBStorage/UserDefaultsWrapper/UserDefaultsSubscript.swift b/14th-team5-iOS/Core/Sources/BBStorages/UserDefaultsWrapper/UserDefaultsSubscript.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBStorage/UserDefaultsWrapper/UserDefaultsSubscript.swift rename to 14th-team5-iOS/Core/Sources/BBStorages/UserDefaultsWrapper/UserDefaultsSubscript.swift diff --git a/14th-team5-iOS/Core/Sources/BBStorages/UserDefaultsWrapper/UserDefaultsType.swift b/14th-team5-iOS/Core/Sources/BBStorages/UserDefaultsWrapper/UserDefaultsType.swift new file mode 100644 index 000000000..1ede28fd8 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/BBStorages/UserDefaultsWrapper/UserDefaultsType.swift @@ -0,0 +1,18 @@ +// +// UserDefaultsType.swift +// Core +// +// Created by 김건우 on 6/2/24. +// + +import Foundation + +public protocol UserDefaultsType { + var userDefaults: UserDefaultsWrapper { get } +} + +extension UserDefaultsType { + public var userDefaults: UserDefaultsWrapper { + UserDefaultsWrapper.standard + } +} diff --git a/14th-team5-iOS/Core/Sources/BBStorage/UserDefaultsWrapper/UserDefaultsWrapper.swift b/14th-team5-iOS/Core/Sources/BBStorages/UserDefaultsWrapper/UserDefaultsWrapper.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBStorage/UserDefaultsWrapper/UserDefaultsWrapper.swift rename to 14th-team5-iOS/Core/Sources/BBStorages/UserDefaultsWrapper/UserDefaultsWrapper.swift diff --git a/14th-team5-iOS/Core/Sources/Base/BaseGlobalState.swift b/14th-team5-iOS/Core/Sources/Base/BaseService.swift similarity index 89% rename from 14th-team5-iOS/Core/Sources/Base/BaseGlobalState.swift rename to 14th-team5-iOS/Core/Sources/Base/BaseService.swift index a6fdbf38d..edd4c22fe 100644 --- a/14th-team5-iOS/Core/Sources/Base/BaseGlobalState.swift +++ b/14th-team5-iOS/Core/Sources/Base/BaseService.swift @@ -7,7 +7,7 @@ import ReactorKit -public class BaseGlobalState { +public class BaseService { unowned let provider: GlobalStateProviderProtocol init(provider: GlobalStateProviderProtocol) { diff --git a/14th-team5-iOS/Core/Sources/Mixpanel/MixPanelService+Account.swift b/14th-team5-iOS/Core/Sources/Utilies/Analytics/Mixpanel/MixPanelService+Account.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Mixpanel/MixPanelService+Account.swift rename to 14th-team5-iOS/Core/Sources/Utilies/Analytics/Mixpanel/MixPanelService+Account.swift diff --git a/14th-team5-iOS/Core/Sources/Mixpanel/MixPanelService+Camera.swift b/14th-team5-iOS/Core/Sources/Utilies/Analytics/Mixpanel/MixPanelService+Camera.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Mixpanel/MixPanelService+Camera.swift rename to 14th-team5-iOS/Core/Sources/Utilies/Analytics/Mixpanel/MixPanelService+Camera.swift diff --git a/14th-team5-iOS/Core/Sources/Mixpanel/MixPanelService+Family.swift b/14th-team5-iOS/Core/Sources/Utilies/Analytics/Mixpanel/MixPanelService+Family.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Mixpanel/MixPanelService+Family.swift rename to 14th-team5-iOS/Core/Sources/Utilies/Analytics/Mixpanel/MixPanelService+Family.swift diff --git a/14th-team5-iOS/Core/Sources/Mixpanel/MixPanelService+Home.swift b/14th-team5-iOS/Core/Sources/Utilies/Analytics/Mixpanel/MixPanelService+Home.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Mixpanel/MixPanelService+Home.swift rename to 14th-team5-iOS/Core/Sources/Utilies/Analytics/Mixpanel/MixPanelService+Home.swift diff --git a/14th-team5-iOS/Core/Sources/Mixpanel/MixpanelService.swift b/14th-team5-iOS/Core/Sources/Utilies/Analytics/Mixpanel/MixpanelService.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Mixpanel/MixpanelService.swift rename to 14th-team5-iOS/Core/Sources/Utilies/Analytics/Mixpanel/MixpanelService.swift diff --git a/14th-team5-iOS/Core/Sources/Haptic/Haptic.swift b/14th-team5-iOS/Core/Sources/Utilies/Haptic.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Haptic/Haptic.swift rename to 14th-team5-iOS/Core/Sources/Utilies/Haptic.swift diff --git a/14th-team5-iOS/Core/Sources/Modifiers/Shimmer.swift b/14th-team5-iOS/Core/Sources/Utilies/Modifiers/Shimmer.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Modifiers/Shimmer.swift rename to 14th-team5-iOS/Core/Sources/Utilies/Modifiers/Shimmer.swift diff --git a/14th-team5-iOS/Core/Sources/Common/Types/URLTypes.swift b/14th-team5-iOS/Core/Sources/Utilies/Types/Enums/URLTypes.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Common/Types/URLTypes.swift rename to 14th-team5-iOS/Core/Sources/Utilies/Types/Enums/URLTypes.swift diff --git a/14th-team5-iOS/Core/Sources/Common/Protocol/ItemIndexable.swift b/14th-team5-iOS/Core/Sources/Utilies/Types/Protocols/ItemIndexable.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Common/Protocol/ItemIndexable.swift rename to 14th-team5-iOS/Core/Sources/Utilies/Types/Protocols/ItemIndexable.swift diff --git a/14th-team5-iOS/Data/Sources/PostComment/PostCommentAPI/DataMapping/CreatePostCommentReqeustDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/CreatePostCommentReqeustDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/PostComment/PostCommentAPI/DataMapping/CreatePostCommentReqeustDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/CreatePostCommentReqeustDTO.swift diff --git a/14th-team5-iOS/Data/Sources/PostComment/PostCommentAPI/DataMapping/PaginationResponsePostCommentResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/PaginationResponsePostCommentResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/PostComment/PostCommentAPI/DataMapping/PaginationResponsePostCommentResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/PaginationResponsePostCommentResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/PostComment/PostCommentAPI/DataMapping/PostCommentDeleteResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/PostCommentDeleteResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/PostComment/PostCommentAPI/DataMapping/PostCommentDeleteResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/PostCommentDeleteResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/PostComment/PostCommentAPI/DataMapping/PostCommentResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/PostCommentResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/PostComment/PostCommentAPI/DataMapping/PostCommentResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/PostCommentResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/PostComment/PostCommentAPI/DataMapping/UpdatePostCommentRequestDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/UpdatePostCommentRequestDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/PostComment/PostCommentAPI/DataMapping/UpdatePostCommentRequestDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/UpdatePostCommentRequestDTO.swift diff --git a/14th-team5-iOS/Data/Sources/PostComment/PostCommentAPI/PostCommentAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/PostCommentAPIWorker.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/PostComment/PostCommentAPI/PostCommentAPIWorker.swift rename to 14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/PostCommentAPIWorker.swift diff --git a/14th-team5-iOS/Data/Sources/PostComment/PostCommentAPI/PostCommentAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/PostCommentAPIs.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/PostComment/PostCommentAPI/PostCommentAPIs.swift rename to 14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/PostCommentAPIs.swift diff --git a/14th-team5-iOS/Data/Sources/PostComment/PostCommentAPI/Repository/PostCommentRepository.swift b/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/Repository/PostCommentRepository.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/PostComment/PostCommentAPI/Repository/PostCommentRepository.swift rename to 14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/Repository/PostCommentRepository.swift diff --git a/14th-team5-iOS/Data/Sources/Storages/Keychain/Keychain+Token.swift b/14th-team5-iOS/Data/Sources/Storages/Keychain/Keychain+Token.swift new file mode 100644 index 000000000..c382f4b0c --- /dev/null +++ b/14th-team5-iOS/Data/Sources/Storages/Keychain/Keychain+Token.swift @@ -0,0 +1,31 @@ +// +// Keychain+Token.swift +// Data +// +// Created by 김건우 on 6/2/24. +// + +import Core +import Foundation + + +// NOTE: - 예시 코드 + +final public class TokenKeychain: KeychainType { + + // MARK: - Intializer + public init() { } + + + // MARK: - AccessToken + public func saveAccessToken(_ accessToken: String) { + keychain[.accessToken] = accessToken + } + + public func loadAccessToken() -> String? { + keychain[.accessToken] + } + + // ... + +} diff --git a/14th-team5-iOS/Data/Sources/Storages/UserDefaults/UserDefaults+Family.swift b/14th-team5-iOS/Data/Sources/Storages/UserDefaults/UserDefaults+Family.swift new file mode 100644 index 000000000..0109c43de --- /dev/null +++ b/14th-team5-iOS/Data/Sources/Storages/UserDefaults/UserDefaults+Family.swift @@ -0,0 +1,35 @@ +// +// File.swift +// Data +// +// Created by 김건우 on 6/2/24. +// + +import Core +import Foundation + + +// NOTE: - 예시 코드 + +final public class MemberUserDefaults: UserDefaultsType { + + // MARK: - Intializer + public init() { } + + + // MARK: - Member Id + + public func save(_ memberId: String) { + userDefaults[.memberId] = memberId + } + + public func loadMemberId() -> String? { + guard + let value: String = userDefaults[.memberId] + else { return nil } + return value + } + + // ... + +} diff --git a/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme b/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme index 7023994af..5f307e6b4 100644 --- a/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme +++ b/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme @@ -280,10 +280,10 @@ buildForAnalyzing = "YES"> + BlueprintIdentifier = "7669601E3503153E790457BC" + BuildableName = "Firebase_FirebaseCore.bundle" + BlueprintName = "Firebase_FirebaseCore" + ReferencedContainer = "container:Tuist/Dependencies/SwiftPackageManager/.build/checkouts/firebase-ios-sdk/Firebase.xcodeproj"> + BlueprintIdentifier = "C241B5678162C4E6FDA163B6" + BuildableName = "Firebase_FirebaseCoreInternal.bundle" + BlueprintName = "Firebase_FirebaseCoreInternal" + ReferencedContainer = "container:Tuist/Dependencies/SwiftPackageManager/.build/checkouts/firebase-ios-sdk/Firebase.xcodeproj"> + + + + + + + + + + + + @@ -426,34 +468,6 @@ ReferencedContainer = "container:Tuist/Dependencies/SwiftPackageManager/.build/checkouts/GoogleUtilities/GoogleUtilities.xcodeproj"> - - - - - - - - - - - - Date: Sun, 2 Jun 2024 20:14:35 +0900 Subject: [PATCH 064/263] =?UTF-8?q?refactor:=20APIWorker=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=A0=95=EB=A6=AC=20=EB=B0=8F=20=ED=8F=B4=EB=8D=94?= =?UTF-8?q?=20=EC=9E=AC=EA=B5=AC=EC=A1=B0=ED=99=94=20(#473)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Mixpanel/MixPanelService+Account.swift | 0 .../Mixpanel/MixPanelService+Camera.swift | 0 .../Mixpanel/MixPanelService+Family.swift | 0 .../Mixpanel/MixPanelService+Home.swift | 0 .../Analytics/Mixpanel/MixpanelService.swift | 0 .../{Utilies => Utilties}/Haptic.swift | 0 .../Modifiers/Shimmer.swift | 0 .../Types/Enums/URLTypes.swift | 0 .../Types/Protocols/ItemIndexable.swift | 0 .../CalendarAPI/CalendarAPIWorker.swift | 49 +++++++++++----- .../Calendar/CalendarAPI/CalendarAPIs.swift | 16 ++--- .../ArrayResponseCalendarResponseDTO.swift | 0 ...rrayResponseDailyCalendarResponseDTO.swift | 0 ...ayResponseMonthlyCalendarResponseDTO.swift | 0 .../DataMapping/BannerResponseDTO.swift | 0 .../FamilyMonthlyStatisticsResponseDTO.swift | 0 .../Repository/CalendarRepository.swift | 0 ...APIWorker.swift => CommentAPIWorker.swift} | 58 ++++++++++++------- ...ostCommentAPIs.swift => CommentAPIs.swift} | 4 +- .../Repository/PostCommentRepository.swift | 10 ++-- .../DataMapping/CreateFamilyResponseDTO.swift | 0 .../FamilyCreatedAtResponseDTO.swift | 0 .../FamilyInvitationLinkResponseDTO.swift | 0 .../FamilyMemberProfileResponseDTO.swift | 0 .../DataMapping/JoinFamilyRequestDTO.swift | 0 .../DataMapping/JoinFamilyResponseDTO.swift | 0 .../Family/FamilyAPI/FamilyAPIWorker.swift | 0 .../Family/FamilyAPI/FamilyAPIs.swift | 0 .../Family/Repository/FamilyRepository.swift | 19 +++--- .../PickMemberListResponseDTO.swift | 0 .../PickAPI/DataMapping/PickResponseDTO.swift | 0 .../Pick/PickAPI/PickAPIWorker.swift | 13 +++++ .../{ => APIs}/Pick/PickAPI/PickAPIs.swift | 0 .../Pick/Repository/PickRepository.swift | 0 34 files changed, 111 insertions(+), 58 deletions(-) rename 14th-team5-iOS/Core/Sources/{Utilies => Utilties}/Analytics/Mixpanel/MixPanelService+Account.swift (100%) rename 14th-team5-iOS/Core/Sources/{Utilies => Utilties}/Analytics/Mixpanel/MixPanelService+Camera.swift (100%) rename 14th-team5-iOS/Core/Sources/{Utilies => Utilties}/Analytics/Mixpanel/MixPanelService+Family.swift (100%) rename 14th-team5-iOS/Core/Sources/{Utilies => Utilties}/Analytics/Mixpanel/MixPanelService+Home.swift (100%) rename 14th-team5-iOS/Core/Sources/{Utilies => Utilties}/Analytics/Mixpanel/MixpanelService.swift (100%) rename 14th-team5-iOS/Core/Sources/{Utilies => Utilties}/Haptic.swift (100%) rename 14th-team5-iOS/Core/Sources/{Utilies => Utilties}/Modifiers/Shimmer.swift (100%) rename 14th-team5-iOS/Core/Sources/{Utilies => Utilties}/Types/Enums/URLTypes.swift (100%) rename 14th-team5-iOS/Core/Sources/{Utilies => Utilties}/Types/Protocols/ItemIndexable.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => APIs}/Calendar/CalendarAPI/CalendarAPIWorker.swift (91%) rename 14th-team5-iOS/Data/Sources/{ => APIs}/Calendar/CalendarAPI/CalendarAPIs.swift (72%) rename 14th-team5-iOS/Data/Sources/{ => APIs}/Calendar/CalendarAPI/DataMapping/ArrayResponseCalendarResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => APIs}/Calendar/CalendarAPI/DataMapping/ArrayResponseDailyCalendarResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => APIs}/Calendar/CalendarAPI/DataMapping/ArrayResponseMonthlyCalendarResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => APIs}/Calendar/CalendarAPI/DataMapping/BannerResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => APIs}/Calendar/CalendarAPI/DataMapping/FamilyMonthlyStatisticsResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => APIs}/Calendar/Repository/CalendarRepository.swift (100%) rename 14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/{PostCommentAPIWorker.swift => CommentAPIWorker.swift} (62%) rename 14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/{PostCommentAPIs.swift => CommentAPIs.swift} (94%) rename 14th-team5-iOS/Data/Sources/APIs/Comment/{CommentAPI => }/Repository/PostCommentRepository.swift (71%) rename 14th-team5-iOS/Data/Sources/{ => APIs}/Family/FamilyAPI/DataMapping/CreateFamilyResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => APIs}/Family/FamilyAPI/DataMapping/FamilyCreatedAtResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => APIs}/Family/FamilyAPI/DataMapping/FamilyInvitationLinkResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => APIs}/Family/FamilyAPI/DataMapping/FamilyMemberProfileResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => APIs}/Family/FamilyAPI/DataMapping/JoinFamilyRequestDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => APIs}/Family/FamilyAPI/DataMapping/JoinFamilyResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => APIs}/Family/FamilyAPI/FamilyAPIWorker.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => APIs}/Family/FamilyAPI/FamilyAPIs.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => APIs}/Family/Repository/FamilyRepository.swift (91%) rename 14th-team5-iOS/Data/Sources/{ => APIs}/Pick/PickAPI/DataMapping/PickMemberListResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => APIs}/Pick/PickAPI/DataMapping/PickResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => APIs}/Pick/PickAPI/PickAPIWorker.swift (95%) rename 14th-team5-iOS/Data/Sources/{ => APIs}/Pick/PickAPI/PickAPIs.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => APIs}/Pick/Repository/PickRepository.swift (100%) diff --git a/14th-team5-iOS/Core/Sources/Utilies/Analytics/Mixpanel/MixPanelService+Account.swift b/14th-team5-iOS/Core/Sources/Utilties/Analytics/Mixpanel/MixPanelService+Account.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Utilies/Analytics/Mixpanel/MixPanelService+Account.swift rename to 14th-team5-iOS/Core/Sources/Utilties/Analytics/Mixpanel/MixPanelService+Account.swift diff --git a/14th-team5-iOS/Core/Sources/Utilies/Analytics/Mixpanel/MixPanelService+Camera.swift b/14th-team5-iOS/Core/Sources/Utilties/Analytics/Mixpanel/MixPanelService+Camera.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Utilies/Analytics/Mixpanel/MixPanelService+Camera.swift rename to 14th-team5-iOS/Core/Sources/Utilties/Analytics/Mixpanel/MixPanelService+Camera.swift diff --git a/14th-team5-iOS/Core/Sources/Utilies/Analytics/Mixpanel/MixPanelService+Family.swift b/14th-team5-iOS/Core/Sources/Utilties/Analytics/Mixpanel/MixPanelService+Family.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Utilies/Analytics/Mixpanel/MixPanelService+Family.swift rename to 14th-team5-iOS/Core/Sources/Utilties/Analytics/Mixpanel/MixPanelService+Family.swift diff --git a/14th-team5-iOS/Core/Sources/Utilies/Analytics/Mixpanel/MixPanelService+Home.swift b/14th-team5-iOS/Core/Sources/Utilties/Analytics/Mixpanel/MixPanelService+Home.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Utilies/Analytics/Mixpanel/MixPanelService+Home.swift rename to 14th-team5-iOS/Core/Sources/Utilties/Analytics/Mixpanel/MixPanelService+Home.swift diff --git a/14th-team5-iOS/Core/Sources/Utilies/Analytics/Mixpanel/MixpanelService.swift b/14th-team5-iOS/Core/Sources/Utilties/Analytics/Mixpanel/MixpanelService.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Utilies/Analytics/Mixpanel/MixpanelService.swift rename to 14th-team5-iOS/Core/Sources/Utilties/Analytics/Mixpanel/MixpanelService.swift diff --git a/14th-team5-iOS/Core/Sources/Utilies/Haptic.swift b/14th-team5-iOS/Core/Sources/Utilties/Haptic.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Utilies/Haptic.swift rename to 14th-team5-iOS/Core/Sources/Utilties/Haptic.swift diff --git a/14th-team5-iOS/Core/Sources/Utilies/Modifiers/Shimmer.swift b/14th-team5-iOS/Core/Sources/Utilties/Modifiers/Shimmer.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Utilies/Modifiers/Shimmer.swift rename to 14th-team5-iOS/Core/Sources/Utilties/Modifiers/Shimmer.swift diff --git a/14th-team5-iOS/Core/Sources/Utilies/Types/Enums/URLTypes.swift b/14th-team5-iOS/Core/Sources/Utilties/Types/Enums/URLTypes.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Utilies/Types/Enums/URLTypes.swift rename to 14th-team5-iOS/Core/Sources/Utilties/Types/Enums/URLTypes.swift diff --git a/14th-team5-iOS/Core/Sources/Utilies/Types/Protocols/ItemIndexable.swift b/14th-team5-iOS/Core/Sources/Utilties/Types/Protocols/ItemIndexable.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Utilies/Types/Protocols/ItemIndexable.swift rename to 14th-team5-iOS/Core/Sources/Utilties/Types/Protocols/ItemIndexable.swift diff --git a/14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/CalendarAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/CalendarAPIWorker.swift similarity index 91% rename from 14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/CalendarAPIWorker.swift rename to 14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/CalendarAPIWorker.swift index 22569a4ba..4941ec470 100644 --- a/14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/CalendarAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/CalendarAPIWorker.swift @@ -26,8 +26,12 @@ extension CalendarAPIs { } // MARK: - Extensions + extension CalendarAPIWorker { + + // MARK: - Fetch Calendar + @available(*, deprecated) private func fetchCalendarResponse(spec: APISpec, headers: [APIHeader]?) -> Single { return request(spec: spec, headers: headers) @@ -55,6 +59,21 @@ extension CalendarAPIWorker { .asSingle() } + + + // MARK: - Fetch Statistics Summary + + public func fetchStatisticsSummary(yearMonth: String) -> Single { + let spec = CalendarAPIs.fetchStatisticsSummary(yearMonth).spec + + return Observable.just(()) + .withLatestFrom(self._headers) + .observe(on: Self.queue) + .withUnretained(self) + .flatMap { $0.0.fetchStatisticsSummary(spec: spec, headers: $0.1) } + .asSingle() + } + private func fetchStatisticsSummary(spec: APISpec, headers: [APIHeader]?) -> Single { return request(spec: spec, headers: headers) .subscribe(on: Self.queue) @@ -69,8 +88,12 @@ extension CalendarAPIWorker { .asSingle() } + + + // MARK: - Fetch Monthly Calendar + public func fetchMonthlyCalendar(yearMonth: String) -> Single { - let spec = CalendarAPIs.monthlyCalendar(yearMonth).spec + let spec = CalendarAPIs.fetchMonthlyCalendar(yearMonth).spec return Observable.just(()) .withLatestFrom(self._headers) @@ -95,8 +118,12 @@ extension CalendarAPIWorker { .asSingle() } + + + // MARK: - Fetch Daily Calendar + public func fetchDailyCalendar(yearMonthDay: String) -> Single { - let spec = CalendarAPIs.dailyCalendar(yearMonthDay).spec + let spec = CalendarAPIs.fetchDailyCalendar(yearMonthDay).spec return Observable.just(()) .withLatestFrom(self._headers) @@ -121,16 +148,10 @@ extension CalendarAPIWorker { .asSingle() } - public func fetchStatisticsSummary(yearMonth: String) -> Single { - let spec = CalendarAPIs.statistics(yearMonth).spec - - return Observable.just(()) - .withLatestFrom(self._headers) - .observe(on: Self.queue) - .withUnretained(self) - .flatMap { $0.0.fetchStatisticsSummary(spec: spec, headers: $0.1) } - .asSingle() - } + + + + // MARK: - Fetch Banner private func fetchCalendarBanner(spec: APISpec, headers: [APIHeader]?) -> Single { return request(spec: spec, headers: headers) @@ -147,7 +168,7 @@ extension CalendarAPIWorker { } public func fetchCalendarBanner(yearMonth: String) -> Single { - let spec = CalendarAPIs.banner(yearMonth).spec + let spec = CalendarAPIs.fetchBanner(yearMonth).spec return Observable.just(()) .withLatestFrom(self._headers) @@ -156,4 +177,6 @@ extension CalendarAPIWorker { .flatMap { $0.0.fetchCalendarBanner(spec: spec, headers: $0.1) } .asSingle() } + + } diff --git a/14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/CalendarAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/CalendarAPIs.swift similarity index 72% rename from 14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/CalendarAPIs.swift rename to 14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/CalendarAPIs.swift index cfe150cce..5738556f5 100644 --- a/14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/CalendarAPIs.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/CalendarAPIs.swift @@ -13,23 +13,23 @@ enum CalendarAPIs: API { @available(*, deprecated) case calendarResponse(String) - case monthlyCalendar(String) - case dailyCalendar(String) - case statistics(String) - case banner(String) + case fetchMonthlyCalendar(String) + case fetchDailyCalendar(String) + case fetchStatisticsSummary(String) + case fetchBanner(String) var spec: APISpec { switch self { case let .calendarResponse(yearMonth): return APISpec(method: .get, url: "\(BibbiAPI.hostApi)/calendar?type=MONTHLY&yearMonth=\(yearMonth)") - case let .monthlyCalendar(yearMonth): + case let .fetchMonthlyCalendar(yearMonth): return APISpec(method: .get, url: "\(BibbiAPI.hostApi)/calendar/monthly?yearMonth=\(yearMonth)") - case let .dailyCalendar(yearMonthDay): + case let .fetchDailyCalendar(yearMonthDay): return APISpec(method: .get, url: "\(BibbiAPI.hostApi)/calendar/daily?yearMonthDay=\(yearMonthDay)") - case let .statistics(yearMonth): + case let .fetchStatisticsSummary(yearMonth): return APISpec(method: .get, url: "\(BibbiAPI.hostApi)/calendar/summary?yearMonth=\(yearMonth)") - case let .banner(yearMonth): + case let .fetchBanner(yearMonth): return APISpec(method: .get, url: "\(BibbiAPI.hostApi)/calendar/banner?yearMonth=\(yearMonth)") } } diff --git a/14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/DataMapping/ArrayResponseCalendarResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/DataMapping/ArrayResponseCalendarResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/DataMapping/ArrayResponseCalendarResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/DataMapping/ArrayResponseCalendarResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/DataMapping/ArrayResponseDailyCalendarResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/DataMapping/ArrayResponseDailyCalendarResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/DataMapping/ArrayResponseDailyCalendarResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/DataMapping/ArrayResponseDailyCalendarResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/DataMapping/ArrayResponseMonthlyCalendarResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/DataMapping/ArrayResponseMonthlyCalendarResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/DataMapping/ArrayResponseMonthlyCalendarResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/DataMapping/ArrayResponseMonthlyCalendarResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/DataMapping/BannerResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/DataMapping/BannerResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/DataMapping/BannerResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/DataMapping/BannerResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/DataMapping/FamilyMonthlyStatisticsResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/DataMapping/FamilyMonthlyStatisticsResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Calendar/CalendarAPI/DataMapping/FamilyMonthlyStatisticsResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/DataMapping/FamilyMonthlyStatisticsResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/Calendar/Repository/CalendarRepository.swift b/14th-team5-iOS/Data/Sources/APIs/Calendar/Repository/CalendarRepository.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Calendar/Repository/CalendarRepository.swift rename to 14th-team5-iOS/Data/Sources/APIs/Calendar/Repository/CalendarRepository.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/PostCommentAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/CommentAPIWorker.swift similarity index 62% rename from 14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/PostCommentAPIWorker.swift rename to 14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/CommentAPIWorker.swift index 31376f2eb..8543946a4 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/PostCommentAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/CommentAPIWorker.swift @@ -1,5 +1,5 @@ // -// PostCommentAPIWorker.swift +// CommentAPIWorker.swift // Data // // Created by 김건우 on 1/17/24. @@ -11,8 +11,8 @@ import Foundation import RxSwift -typealias PostCommentAPIWorker = PostCommentAPIs.Worker -extension PostCommentAPIs { +typealias CommentAPIWorker = CommentAPIs.Worker +extension CommentAPIs { final class Worker: APIWorker { static let queue = { ConcurrentDispatchQueueScheduler(queue: DispatchQueue(label: "PostCommentAPIWorker", qos: .utility)) @@ -26,22 +26,27 @@ extension PostCommentAPIs { } // MARK: - Extensions -extension PostCommentAPIWorker { - public func fetchPostComment(postId: String, query: PostCommentPaginationQuery) -> Single { +extension CommentAPIWorker { + + + + // MARK: - Fetch Comment + + public func fetchComment(postId: String, query: PostCommentPaginationQuery) -> Single { let page = query.page let size = query.size let sort = query.sort.rawValue - let spec = PostCommentAPIs.fetchPostComment(postId, page, size, sort).spec + let spec = CommentAPIs.fetchPostComment(postId, page, size, sort).spec return Observable.just(()) .withLatestFrom(self._headers) .observe(on: Self.queue) .withUnretained(self) - .flatMap { $0.0.fetchPostComment(spec: spec, headers: $0.1) } + .flatMap { $0.0.fetchComment(spec: spec, headers: $0.1) } .asSingle() } - private func fetchPostComment(spec: APISpec, headers: [APIHeader]?) -> Single { + private func fetchComment(spec: APISpec, headers: [APIHeader]?) -> Single { return request(spec: spec, headers: headers) .subscribe(on: Self.queue) .do { @@ -55,18 +60,22 @@ extension PostCommentAPIWorker { .asSingle() } - public func createPostComment(postId: String, body: CreatePostCommentReqeustDTO) -> Single { - let spec = PostCommentAPIs.createPostComment(postId).spec + + + // MARK: - Create Comment + + public func createComment(postId: String, body: CreatePostCommentReqeustDTO) -> Single { + let spec = CommentAPIs.createPostComment(postId).spec return Observable.just(()) .withLatestFrom(self._headers) .observe(on: Self.queue) .withUnretained(self) - .flatMap { $0.0.createPostComment(spec: spec, headers: $0.1, jsonEncodable: body) } + .flatMap { $0.0.createComment(spec: spec, headers: $0.1, jsonEncodable: body) } .asSingle() } - private func createPostComment(spec: APISpec, headers: [APIHeader]?, jsonEncodable body: Encodable) -> Single { + private func createComment(spec: APISpec, headers: [APIHeader]?, jsonEncodable body: Encodable) -> Single { return request(spec: spec, headers: headers, jsonEncodable: body) .subscribe(on: Self.queue) .do { @@ -80,18 +89,22 @@ extension PostCommentAPIWorker { .asSingle() } - public func updatePostComment(postId: String, commentId: String, body: UpdatePostCommentReqeustDTO) -> Single { - let spec = PostCommentAPIs.updatePostComment(postId, commentId).spec + + + // MARK: - Update Comment + + public func updateComment(postId: String, commentId: String, body: UpdatePostCommentReqeustDTO) -> Single { + let spec = CommentAPIs.updatePostComment(postId, commentId).spec return Observable.just(()) .withLatestFrom(self._headers) .observe(on: Self.queue) .withUnretained(self) - .flatMap { $0.0.updatePostComment(spec: spec, headers: $0.1, jsonEncodable: body) } + .flatMap { $0.0.updateComment(spec: spec, headers: $0.1, jsonEncodable: body) } .asSingle() } - private func updatePostComment(spec: APISpec, headers: [APIHeader]?, jsonEncodable body: Encodable) -> Single { + private func updateComment(spec: APISpec, headers: [APIHeader]?, jsonEncodable body: Encodable) -> Single { return request(spec: spec, headers: headers, jsonEncodable: body) .subscribe(on: Self.queue) .do { @@ -105,18 +118,23 @@ extension PostCommentAPIWorker { .asSingle() } - public func deletePostComment(postId: String, commentId: String) -> Single { - let spec = PostCommentAPIs.deletePostComment(postId, commentId).spec + + + + // MARK: - Delete Commen + + public func deleteComment(postId: String, commentId: String) -> Single { + let spec = CommentAPIs.deletePostComment(postId, commentId).spec return Observable.just(()) .withLatestFrom(self._headers) .observe(on: Self.queue) .withUnretained(self) - .flatMap { $0.0.deletePostComment(spec: spec, headers: $0.1) } + .flatMap { $0.0.deleteComment(spec: spec, headers: $0.1) } .asSingle() } - private func deletePostComment(spec: APISpec, headers: [APIHeader]?) -> Single { + private func deleteComment(spec: APISpec, headers: [APIHeader]?) -> Single { return request(spec: spec, headers: headers) .subscribe(on: Self.queue) .do { diff --git a/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/PostCommentAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/CommentAPIs.swift similarity index 94% rename from 14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/PostCommentAPIs.swift rename to 14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/CommentAPIs.swift index 382272d3b..45a4947df 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/PostCommentAPIs.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/CommentAPIs.swift @@ -1,5 +1,5 @@ // -// PostCommentAPIs.swift +// CommentAPIs.swift // Data // // Created by 김건우 on 1/17/24. @@ -9,7 +9,7 @@ import Foundation import Domain -enum PostCommentAPIs: API { +enum CommentAPIs: API { case fetchPostComment(String, Int, Int, String) case createPostComment(String) case updatePostComment(String, String) diff --git a/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/Repository/PostCommentRepository.swift b/14th-team5-iOS/Data/Sources/APIs/Comment/Repository/PostCommentRepository.swift similarity index 71% rename from 14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/Repository/PostCommentRepository.swift rename to 14th-team5-iOS/Data/Sources/APIs/Comment/Repository/PostCommentRepository.swift index 373de604d..f799104d3 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/Repository/PostCommentRepository.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Comment/Repository/PostCommentRepository.swift @@ -13,31 +13,31 @@ import RxSwift public final class PostCommentRepository: PostCommentRepositoryProtocol { public let disposeBag: DisposeBag = DisposeBag() - private let postCommentApiWorker: PostCommentAPIWorker = PostCommentAPIWorker() + private let postCommentApiWorker: CommentAPIWorker = CommentAPIWorker() public init() { } } extension PostCommentRepository { public func fetchPostComment(postId: String, query: PostCommentPaginationQuery) -> Observable { - return postCommentApiWorker.fetchPostComment(postId: postId, query: query) + return postCommentApiWorker.fetchComment(postId: postId, query: query) .asObservable() } public func createPostComment(postId: String, body: CreatePostCommentRequest) -> Observable { let body = CreatePostCommentReqeustDTO(content: body.content) - return postCommentApiWorker.createPostComment(postId: postId, body: body) + return postCommentApiWorker.createComment(postId: postId, body: body) .asObservable() } public func updatePostComment(postId: String, commentId: String, body: UpdatePostCommentRequest) -> Observable { let body = UpdatePostCommentReqeustDTO(content: body.content) - return postCommentApiWorker.updatePostComment(postId: postId, commentId: commentId, body: body) + return postCommentApiWorker.updateComment(postId: postId, commentId: commentId, body: body) .asObservable() } public func deletePostComment(postId: String, commentId: String) -> Observable { - return postCommentApiWorker.deletePostComment(postId: postId, commentId: commentId) + return postCommentApiWorker.deleteComment(postId: postId, commentId: commentId) .asObservable() } } diff --git a/14th-team5-iOS/Data/Sources/Family/FamilyAPI/DataMapping/CreateFamilyResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/CreateFamilyResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Family/FamilyAPI/DataMapping/CreateFamilyResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/CreateFamilyResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/Family/FamilyAPI/DataMapping/FamilyCreatedAtResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/FamilyCreatedAtResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Family/FamilyAPI/DataMapping/FamilyCreatedAtResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/FamilyCreatedAtResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/Family/FamilyAPI/DataMapping/FamilyInvitationLinkResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/FamilyInvitationLinkResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Family/FamilyAPI/DataMapping/FamilyInvitationLinkResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/FamilyInvitationLinkResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/Family/FamilyAPI/DataMapping/FamilyMemberProfileResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/FamilyMemberProfileResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Family/FamilyAPI/DataMapping/FamilyMemberProfileResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/FamilyMemberProfileResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/Family/FamilyAPI/DataMapping/JoinFamilyRequestDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/JoinFamilyRequestDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Family/FamilyAPI/DataMapping/JoinFamilyRequestDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/JoinFamilyRequestDTO.swift diff --git a/14th-team5-iOS/Data/Sources/Family/FamilyAPI/DataMapping/JoinFamilyResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/JoinFamilyResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Family/FamilyAPI/DataMapping/JoinFamilyResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/JoinFamilyResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/Family/FamilyAPI/FamilyAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIWorker.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Family/FamilyAPI/FamilyAPIWorker.swift rename to 14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIWorker.swift diff --git a/14th-team5-iOS/Data/Sources/Family/FamilyAPI/FamilyAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIs.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Family/FamilyAPI/FamilyAPIs.swift rename to 14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIs.swift diff --git a/14th-team5-iOS/Data/Sources/Family/Repository/FamilyRepository.swift b/14th-team5-iOS/Data/Sources/APIs/Family/Repository/FamilyRepository.swift similarity index 91% rename from 14th-team5-iOS/Data/Sources/Family/Repository/FamilyRepository.swift rename to 14th-team5-iOS/Data/Sources/APIs/Family/Repository/FamilyRepository.swift index ab12d438a..01c94fde7 100644 --- a/14th-team5-iOS/Data/Sources/Family/Repository/FamilyRepository.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Family/Repository/FamilyRepository.swift @@ -18,17 +18,16 @@ public final class FamilyRepository: FamilyRepositoryProtocol { private var familyId: String = App.Repository.member.familyId.value ?? "" - public init() { - bind() - } - private func bind() { - App.Repository.member.familyId - .compactMap { $0 } - .withUnretained(self) - .bind(onNext: { $0.0.familyId = $0.1 }) - .disposed(by: disposeBag) - } + // NOTE: - 예시 코드 + private var inMemory = InMemoryWrapper.standard + private var userDefaults = UserDefaultsWrapper.standard + private var keychain = KeychainWrapper.standard + + + + + public init() { } } extension FamilyRepository { diff --git a/14th-team5-iOS/Data/Sources/Pick/PickAPI/DataMapping/PickMemberListResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/DataMapping/PickMemberListResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Pick/PickAPI/DataMapping/PickMemberListResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/DataMapping/PickMemberListResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/Pick/PickAPI/DataMapping/PickResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/DataMapping/PickResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Pick/PickAPI/DataMapping/PickResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/DataMapping/PickResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/Pick/PickAPI/PickAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/PickAPIWorker.swift similarity index 95% rename from 14th-team5-iOS/Data/Sources/Pick/PickAPI/PickAPIWorker.swift rename to 14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/PickAPIWorker.swift index 52da97469..78058a1e7 100644 --- a/14th-team5-iOS/Data/Sources/Pick/PickAPI/PickAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/PickAPIWorker.swift @@ -25,8 +25,14 @@ extension PickAPIs { } } + +// MARK: - Extensions + extension PickAPIWorker { + + // MARK: - Pick Member + private func pickMember(spec: APISpec, headers: [APIHeader]?) -> Single { return request(spec: spec, headers: headers) .subscribe(on: Self.queue) @@ -53,6 +59,9 @@ extension PickAPIWorker { } + + // MARK: - Who Did I Pick? + private func fetchWhoDidIPickMember(spec: APISpec, headers: [APIHeader]?) -> Single { return request(spec: spec, headers: headers) .subscribe(on: Self.queue) @@ -78,6 +87,10 @@ extension PickAPIWorker { .asSingle() } + + + // MARK: - Who Picked Me? + private func fetchWhoPickedMeMember(spec: APISpec, headers: [APIHeader]?) -> Single { return request(spec: spec, headers: headers) .subscribe(on: Self.queue) diff --git a/14th-team5-iOS/Data/Sources/Pick/PickAPI/PickAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/PickAPIs.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Pick/PickAPI/PickAPIs.swift rename to 14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/PickAPIs.swift diff --git a/14th-team5-iOS/Data/Sources/Pick/Repository/PickRepository.swift b/14th-team5-iOS/Data/Sources/APIs/Pick/Repository/PickRepository.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Pick/Repository/PickRepository.swift rename to 14th-team5-iOS/Data/Sources/APIs/Pick/Repository/PickRepository.swift From f71c314940ca4065857d612a722de9e15437310e Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Sun, 2 Jun 2024 20:26:15 +0900 Subject: [PATCH 065/263] =?UTF-8?q?refactor:=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=ED=86=B5=EC=9D=BC=20=EB=B0=8F=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81=20(#473)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Calendar/CalendarAPI/DataMapping/BannerResponseDTO.swift | 2 +- .../DataMapping/FamilyMonthlyStatisticsResponseDTO.swift | 4 +--- .../PaginationResponsePostCommentResponseDTO.swift | 3 +-- .../CommentAPI/DataMapping/PostCommentDeleteResponseDTO.swift | 3 +-- .../CommentAPI/DataMapping/PostCommentResponseDTO.swift | 3 +-- .../FamilyAPI/DataMapping/CreateFamilyResponseDTO.swift | 4 +--- .../FamilyAPI/DataMapping/FamilyCreatedAtResponseDTO.swift | 1 - .../DataMapping/FamilyInvitationLinkResponseDTO.swift | 4 +--- .../DataMapping/FamilyMemberProfileResponseDTO.swift | 4 +--- .../Pick/PickAPI/DataMapping/PickMemberListResponseDTO.swift | 3 +-- .../APIs/Pick/PickAPI/DataMapping/PickResponseDTO.swift | 3 +-- 11 files changed, 10 insertions(+), 24 deletions(-) diff --git a/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/DataMapping/BannerResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/DataMapping/BannerResponseDTO.swift index 27f945a25..4bdcc7039 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/DataMapping/BannerResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/DataMapping/BannerResponseDTO.swift @@ -5,8 +5,8 @@ // Created by 김건우 on 1/26/24. // -import DesignSystem import Domain +import DesignSystem import UIKit public struct BannerResponseDTO: Decodable { diff --git a/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/DataMapping/FamilyMonthlyStatisticsResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/DataMapping/FamilyMonthlyStatisticsResponseDTO.swift index dc665f3e4..0e949ccbf 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/DataMapping/FamilyMonthlyStatisticsResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/DataMapping/FamilyMonthlyStatisticsResponseDTO.swift @@ -5,11 +5,9 @@ // Created by 김건우 on 1/5/24. // -import Foundation - import Domain +import Foundation -// MARK: - Data Transfer Object(DTO) public struct FamilyMonthlyStatisticsResponseDTO: Decodable { private enum CodingKeys: String, CodingKey { case totalImageCnt diff --git a/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/PaginationResponsePostCommentResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/PaginationResponsePostCommentResponseDTO.swift index ef6fde728..9f3302afc 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/PaginationResponsePostCommentResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/PaginationResponsePostCommentResponseDTO.swift @@ -5,9 +5,8 @@ // Created by 김건우 on 1/17/24. // -import Foundation - import Domain +import Foundation public struct PaginationResponsePostCommentResponseDTO: Decodable { private enum CodingKeys: String, CodingKey { diff --git a/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/PostCommentDeleteResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/PostCommentDeleteResponseDTO.swift index 987d60412..53abb27e2 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/PostCommentDeleteResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/PostCommentDeleteResponseDTO.swift @@ -5,9 +5,8 @@ // Created by 김건우 on 1/17/24. // -import Foundation - import Domain +import Foundation public struct PostCommentDeleteResponseDTO: Decodable { var success: Bool diff --git a/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/PostCommentResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/PostCommentResponseDTO.swift index e122a97a4..2ac895ce3 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/PostCommentResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/PostCommentResponseDTO.swift @@ -5,9 +5,8 @@ // Created by 김건우 on 1/17/24. // -import Foundation - import Domain +import Foundation public struct PostCommentResponseDTO: Decodable { private enum CodingKeys: String, CodingKey { diff --git a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/CreateFamilyResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/CreateFamilyResponseDTO.swift index 2e08861b6..e58024670 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/CreateFamilyResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/CreateFamilyResponseDTO.swift @@ -5,11 +5,9 @@ // Created by 김건우 on 1/10/24. // -import Foundation - import Domain +import Foundation -// MARK: - Data Transfer Object (DTO) public struct CreateFamilyResponseDTO: Decodable { private enum CodingKeys: String, CodingKey { case familyId diff --git a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/FamilyCreatedAtResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/FamilyCreatedAtResponseDTO.swift index 65fabf9bc..131960c63 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/FamilyCreatedAtResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/FamilyCreatedAtResponseDTO.swift @@ -8,7 +8,6 @@ import Domain import Foundation -// MARK: - Data Transfer Object (DTO) public struct FamilyCreatedAtResponseDTO: Decodable { private enum CodingKeys: String, CodingKey { case familyId diff --git a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/FamilyInvitationLinkResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/FamilyInvitationLinkResponseDTO.swift index d73b87a0a..73c152c7e 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/FamilyInvitationLinkResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/FamilyInvitationLinkResponseDTO.swift @@ -5,11 +5,9 @@ // Created by 김건우 on 12/20/23. // -import Foundation - import Domain +import Foundation -// MARK: - Data Transfer Object (DTO) public struct FamilyInvitationLinkResponseDTO: Decodable { private enum CodingKeys: String, CodingKey { case linkId diff --git a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/FamilyMemberProfileResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/FamilyMemberProfileResponseDTO.swift index 6752ee35b..0b64e2101 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/FamilyMemberProfileResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/FamilyMemberProfileResponseDTO.swift @@ -5,11 +5,9 @@ // Created by 김건우 on 12/20/23. // -import Foundation - import Domain +import Foundation -// MARK: - Data Transfer Object (DTO) public struct PaginationResponseFamilyMemberProfileDTO: Decodable { private enum CodingKeys: String, CodingKey { case currentPage diff --git a/14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/DataMapping/PickMemberListResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/DataMapping/PickMemberListResponseDTO.swift index c0ee36380..7b1cd94e8 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/DataMapping/PickMemberListResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/DataMapping/PickMemberListResponseDTO.swift @@ -5,9 +5,8 @@ // Created by 김건우 on 4/15/24. // -import Foundation - import Domain +import Foundation public struct PickMemberListResponseDTO: Decodable { enum CodingKeys: String, CodingKey { diff --git a/14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/DataMapping/PickResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/DataMapping/PickResponseDTO.swift index 8d17d1770..b3e0848e2 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/DataMapping/PickResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/DataMapping/PickResponseDTO.swift @@ -5,9 +5,8 @@ // Created by 김건우 on 4/15/24. // -import Foundation - import Domain +import Foundation public struct PickResponseDTO: Decodable { enum CodingKeys: String, CodingKey { From 530877ea181a652c6911840e313d435d6c391795 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Sun, 2 Jun 2024 23:34:46 +0900 Subject: [PATCH 066/263] =?UTF-8?q?refactor:=20BBNetwork=20=ED=8F=B4?= =?UTF-8?q?=EB=8D=94=20=EC=83=9D=EC=84=B1=20=EB=B0=8F=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=20=EC=9D=B4=EB=8F=99,=20=EC=BD=94=EB=93=9C=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A7=81=20(#473)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../API => Core/Sources/BBNetwork}/API.swift | 35 +-- .../Core/Sources/BBNetwork/APIConfig.swift | 23 ++ .../Sources/BBNetwork}/APISpec.swift | 22 +- .../KeychainWrapper/KeychainWrapper.swift | 11 +- .../Sources/Extensions/Codable+ExtTTT.swift} | 65 ++--- .../Analytics/Mixpanel/MixpanelService.swift | 2 +- .../Data/Sources/API/APIConfig.swift | 20 -- .../Data/Sources/API/APIWorker.swift | 210 ---------------- .../Data/Sources/API/VoidResponse.swift | 16 -- .../Data/Sources/APIs/APIWorker.swift | 230 ++++++++++++++++++ .../Calendar/CalendarAPI/CalendarAPIs.swift | 4 +- .../APIs/Comment/CommentAPI/CommentAPIs.swift | 6 +- .../APIs/Family/FamilyAPI/FamilyAPIs.swift | 2 +- .../Sources/APIs/NetworkIntercepter.swift | 97 ++++++++ .../Sources/APIs/Pick/PickAPI/PickAPIs.swift | 3 +- .../Account/AccountAPI/AccountAPIs.swift | 1 + .../Account/DataMapping/VoidResponse.swift | 17 ++ .../Data/Sources/Account/MeAPI/MeAPIs.swift | 3 +- .../Service/AccountSignInHelper.swift | 0 .../Service/AccountSignInHelperConfig.swift | 0 .../Service/AccountSignInHelperType.swift | 1 + .../Service/Apple/AppleService+Rx.swift | 0 .../Service/Apple/AppleSignInHelper.swift | 0 .../Service/Kakao/KakaoService+Rx.swift | 0 .../Service/Kakao/KakaoSignInHelper.swift | 0 .../Sources/Camera/CameraAPI/CameraAPIs.swift | 4 +- .../Sources/Emoji/EmojiAPI/EmojiAPIs.swift | 3 +- .../Mission/MissionAPI/MissionAPIs.swift | 2 +- .../PostList/PostListAPI/PostListAPIs.swift | 2 +- .../Privacy/PrivacyAPI/PrivacyAPIWorker.swift | 2 +- .../Privacy/PrivacyAPI/PrivacyAPIs.swift | 4 +- .../Profile/ProfileAPI/ProfileAPIWorker.swift | 2 +- .../RealEmojiAPI/RealEmojiAPIS.swift | 2 +- .../Resign/ResignAPI/ResignAPIWorker.swift | 2 +- .../Sources/Resign/ResignAPI/ResignAPIs.swift | 3 +- .../Sources/View/Main/Worker/MainAPIs.swift | 2 +- 36 files changed, 467 insertions(+), 329 deletions(-) rename 14th-team5-iOS/{Data/Sources/API => Core/Sources/BBNetwork}/API.swift (74%) create mode 100644 14th-team5-iOS/Core/Sources/BBNetwork/APIConfig.swift rename 14th-team5-iOS/{Data/Sources/API => Core/Sources/BBNetwork}/APISpec.swift (69%) rename 14th-team5-iOS/{Data/Sources/API/Codable+Ext.swift => Core/Sources/Extensions/Codable+ExtTTT.swift} (71%) delete mode 100644 14th-team5-iOS/Data/Sources/API/APIConfig.swift delete mode 100644 14th-team5-iOS/Data/Sources/API/APIWorker.swift delete mode 100644 14th-team5-iOS/Data/Sources/API/VoidResponse.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/APIWorker.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/NetworkIntercepter.swift create mode 100644 14th-team5-iOS/Data/Sources/Account/DataMapping/VoidResponse.swift rename 14th-team5-iOS/Data/Sources/{ => Account}/Service/AccountSignInHelper.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => Account}/Service/AccountSignInHelperConfig.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => Account}/Service/AccountSignInHelperType.swift (96%) rename 14th-team5-iOS/Data/Sources/{ => Account}/Service/Apple/AppleService+Rx.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => Account}/Service/Apple/AppleSignInHelper.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => Account}/Service/Kakao/KakaoService+Rx.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => Account}/Service/Kakao/KakaoSignInHelper.swift (100%) diff --git a/14th-team5-iOS/Data/Sources/API/API.swift b/14th-team5-iOS/Core/Sources/BBNetwork/API.swift similarity index 74% rename from 14th-team5-iOS/Data/Sources/API/API.swift rename to 14th-team5-iOS/Core/Sources/BBNetwork/API.swift index c83694cf0..6ebd57d5f 100644 --- a/14th-team5-iOS/Data/Sources/API/API.swift +++ b/14th-team5-iOS/Core/Sources/BBNetwork/API.swift @@ -6,22 +6,23 @@ // import Foundation -import Core + // MARK: APIs of AFP -typealias BibbiHeader = BibbiAPI.Header -typealias BibbiResponse = BibbiAPI.Response -typealias BibbiNoResponse = BibbiAPI.NoResponse -typealias BibbiBoolResponse = BibbiAPI.BoolResponse -typealias BibbiCodableResponse = BibbiAPI.CodableResponse +public typealias BibbiHeader = BibbiAPI.Header +public typealias BibbiResponse = BibbiAPI.Response +public typealias BibbiNoResponse = BibbiAPI.NoResponse +public typealias BibbiBoolResponse = BibbiAPI.BoolResponse +public typealias BibbiCodableResponse = BibbiAPI.CodableResponse -enum BibbiAPI { +public enum BibbiAPI { private static let _config: BibbiAPIConfigType = BibbiAPIConfig() - static let hostApi: String = _config.hostApi + public static let hostApi: String = _config.hostApi + public static let xAppKey: String = _config.xAppKey // MARK: Common Headers - enum Header: APIHeader { + public enum Header: APIHeader { case auth(String) case xAppKey case xAuthToken(String) @@ -33,7 +34,7 @@ enum BibbiAPI { case xAppVersion case xUserID - var key: String { + public var key: String { switch self { case .auth: return "Authorization" case .xAppKey: return "X-APP-KEY" @@ -48,10 +49,10 @@ enum BibbiAPI { } } - var value: String { + public var value: String { switch self { case .auth(let value): return "Bearer \(value)" - case .xAppKey: return "7c5aaa36-570e-491f-b18a-26a1a0b72959" + case .xAppKey: return "\(BibbiAPI.xAppKey)" case .xAuthToken(let value): return "\(value)" case .contentForm: return "application/x-www-form-urlencoded" case .contentJson: return "application/json" @@ -63,12 +64,12 @@ enum BibbiAPI { } } - static var baseHeaders: [Self] { + public static var baseHeaders: [Self] { return [.xAppKey, .xAppVersion, .xUserPlatform, .xUserID] } } - struct Response: Codable { + public struct Response: Codable { let status: String? let code: Int? let errorCode: String? @@ -77,20 +78,20 @@ enum BibbiAPI { } } - struct NoResponse: Codable { + public struct NoResponse: Codable { // var status: String? // var code: Int? // var errorCode: String? } - struct BoolResponse: Codable { + public struct BoolResponse: Codable { var status: String? var code: Int? var errorCode: String? var result: Bool? } - struct CodableResponse: Codable { + public struct CodableResponse: Codable { // var status: String? // var code: String? // var message: String? diff --git a/14th-team5-iOS/Core/Sources/BBNetwork/APIConfig.swift b/14th-team5-iOS/Core/Sources/BBNetwork/APIConfig.swift new file mode 100644 index 000000000..2240509f2 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/BBNetwork/APIConfig.swift @@ -0,0 +1,23 @@ +// +// APIConfig.swift +// Data +// +// Created by geonhui Yu on 12/17/23. +// + +import Foundation + +public protocol BibbiAPIConfigType { + var hostApi: String { get } + var xAppKey: String { get } +} + +public struct BibbiAPIConfig: BibbiAPIConfigType { + #if PRD + public var hostApi: String = "https://api.no5ing.kr/v1" + #else + public var hostApi: String = "https://dev.api.no5ing.kr/v1" + #endif + + public var xAppKey: String = "7c5aaa36-570e-491f-b18a-26a1a0b72959" +} diff --git a/14th-team5-iOS/Data/Sources/API/APISpec.swift b/14th-team5-iOS/Core/Sources/BBNetwork/APISpec.swift similarity index 69% rename from 14th-team5-iOS/Data/Sources/API/APISpec.swift rename to 14th-team5-iOS/Core/Sources/BBNetwork/APISpec.swift index b19d4db48..eef5dea89 100644 --- a/14th-team5-iOS/Data/Sources/API/APISpec.swift +++ b/14th-team5-iOS/Core/Sources/BBNetwork/APISpec.swift @@ -11,29 +11,29 @@ import RxSwift // MARK: API Protocol typealias BaseAPI = API -protocol API { +public protocol API { var spec: APISpec { get } } // MARK: API Constants -enum APIConst { +public enum APIConst { static let boundary = UUID().uuidString } // MARK: API Header Protocol -protocol APIHeader { +public protocol APIHeader { var key: String { get } var value: String { get } } // MARK: API Prameter Protocol -protocol APIParameter { +public protocol APIParameter { var key: String { get } var value: Any? { get } } // MARK: API Media Prameter -struct APIMediaParameter { +public struct APIMediaParameter { let name: String let fileName: String let mimeType: String @@ -42,7 +42,7 @@ struct APIMediaParameter { } // MARK: API Result Type -typealias APIRes = APIResult +public typealias APIRes = APIResult public enum APIResult { static let ok: Int = 200 case success @@ -50,14 +50,14 @@ public enum APIResult { } // MARK: API Error Protocol -protocol APIError: CustomNSError, Equatable {} +public protocol APIError: CustomNSError, Equatable {} // MARK: API Specification -struct APISpec { - let method: HTTPMethod - let url: String +public struct APISpec { + public let method: HTTPMethod + public let url: String - init(method: HTTPMethod, url: String) { + public init(method: HTTPMethod, url: String) { self.method = method self.url = url } diff --git a/14th-team5-iOS/Core/Sources/BBStorages/KeychainWrapper/KeychainWrapper.swift b/14th-team5-iOS/Core/Sources/BBStorages/KeychainWrapper/KeychainWrapper.swift index c76c65355..183794aa5 100644 --- a/14th-team5-iOS/Core/Sources/BBStorages/KeychainWrapper/KeychainWrapper.swift +++ b/14th-team5-iOS/Core/Sources/BBStorages/KeychainWrapper/KeychainWrapper.swift @@ -27,7 +27,11 @@ final public class KeychainWrapper { private(set) public var accessGroup: String? private static let defaultServiceName: String = { - return Bundle.main.bundleIdentifier ?? "KeychainWrapper" + return "Bibbi" + }() + + private static let defaultAccessGroupName: String = { + return "P9P4WJ623F.com.5ing.bibbi" }() // MARK: - Intializer @@ -40,7 +44,10 @@ final public class KeychainWrapper { } private convenience init() { - self.init(serviceName: KeychainWrapper.defaultServiceName) + self.init( + serviceName: KeychainWrapper.defaultServiceName, + accessGroup: KeychainWrapper.defaultAccessGroupName + ) } diff --git a/14th-team5-iOS/Data/Sources/API/Codable+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/Codable+ExtTTT.swift similarity index 71% rename from 14th-team5-iOS/Data/Sources/API/Codable+Ext.swift rename to 14th-team5-iOS/Core/Sources/Extensions/Codable+ExtTTT.swift index f6879933f..eb991cab2 100644 --- a/14th-team5-iOS/Data/Sources/API/Codable+Ext.swift +++ b/14th-team5-iOS/Core/Sources/Extensions/Codable+ExtTTT.swift @@ -7,32 +7,35 @@ import Foundation -import Alamofire import RxSwift + + +// TODO: - 파일 별로 쪼개기 + // MARK: Data Decodable (Data to Decodable) public extension Data { - func decode(_ type: T.Type, using decoder: JSONDecoder? = nil) -> T? where T: Decodable { - - let decoder = decoder ?? JSONDecoder() - - var res: T? = nil - do { - res = try decoder.decode(type, from: self) - } catch { - debugPrint("\(T.self) Data Parsing Error: \(error)") - } - - return res - } +// func decode(_ type: T.Type, using decoder: JSONDecoder? = nil) -> T? where T: Decodable { +// +// let decoder = decoder ?? JSONDecoder() +// +// var res: T? = nil +// do { +// res = try decoder.decode(type, from: self) +// } catch { +// debugPrint("\(T.self) Data Parsing Error: \(error)") +// } +// +// return res +// } } // MARK: String Decodable (String to Decodable) public extension String { - func decode(_ type: T.Type, using decoder: JSONDecoder? = nil) -> T? where T: Decodable { - - return self.data(using: .utf8)?.decode(type, using: decoder) - } +// func decode(_ type: T.Type, using decoder: JSONDecoder? = nil) -> T? where T: Decodable { +// +// return self.data(using: .utf8)?.decode(type, using: decoder) +// } } // MARK: Dictionary([String : Any]) Decodable @@ -102,19 +105,19 @@ public extension PrimitiveSequenceType where Trait == SingleTrait, Element == (H } } -extension Encodable { - - func asDictionary() -> [String: Any]? { - do { - let data = try JSONEncoder().encode(self) - guard let dict = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any] else { return nil } - - return dict - } catch let error { - print(error) - return nil - } - } +public extension Encodable { +// +// func asDictionary() -> [String: Any]? { +// do { +// let data = try JSONEncoder().encode(self) +// guard let dict = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any] else { return nil } +// +// return dict +// } catch let error { +// print(error) +// return nil +// } +// } func asArray() throws -> [AnyObject] { let data = try JSONEncoder().encode(self) diff --git a/14th-team5-iOS/Core/Sources/Utilties/Analytics/Mixpanel/MixpanelService.swift b/14th-team5-iOS/Core/Sources/Utilties/Analytics/Mixpanel/MixpanelService.swift index a10daca98..bdb969473 100644 --- a/14th-team5-iOS/Core/Sources/Utilties/Analytics/Mixpanel/MixpanelService.swift +++ b/14th-team5-iOS/Core/Sources/Utilties/Analytics/Mixpanel/MixpanelService.swift @@ -62,7 +62,7 @@ extension MPEvent { } // MARK: Extensions -extension Encodable { +public extension Encodable { func asDictionary() -> [String: Any]? { do { let data = try JSONEncoder().encode(self) diff --git a/14th-team5-iOS/Data/Sources/API/APIConfig.swift b/14th-team5-iOS/Data/Sources/API/APIConfig.swift deleted file mode 100644 index dae5df606..000000000 --- a/14th-team5-iOS/Data/Sources/API/APIConfig.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// APIConfig.swift -// Data -// -// Created by geonhui Yu on 12/17/23. -// - -import Foundation - -protocol BibbiAPIConfigType { - var hostApi: String { get } -} - -struct BibbiAPIConfig: BibbiAPIConfigType { - #if PRD - var hostApi: String = "https://api.no5ing.kr/v1" - #else - var hostApi: String = "https://dev.api.no5ing.kr/v1" - #endif -} diff --git a/14th-team5-iOS/Data/Sources/API/APIWorker.swift b/14th-team5-iOS/Data/Sources/API/APIWorker.swift deleted file mode 100644 index c60ba0de1..000000000 --- a/14th-team5-iOS/Data/Sources/API/APIWorker.swift +++ /dev/null @@ -1,210 +0,0 @@ -// -// BaseAPIWorker.swift -// Data -// -// Created by geonhui Yu on 12/17/23. -// - -import Foundation - -import Alamofire -import RxSwift -import RxCocoa -import Core -import Domain - -protocol BibbiRouterInterface { - func httpHeaders(_ headers: [APIHeader]?) -> HTTPHeaders -} - -extension BibbiRouterInterface { - public func httpHeaders(_ headers: [APIHeader]?) -> HTTPHeaders { - var result: [String: String] = [:] - guard let headers = headers, !headers.isEmpty else { return HTTPHeaders() } - - for header in headers { - result[header.key] = header.value - } - - return HTTPHeaders(result) - } -} - -public final class BibbiRequestInterceptor: RequestInterceptor, BibbiRouterInterface { - - //TODO: Test용 KeychainWrapper 코드 다 제거하기 - private let accountAPIWorker: AccountAPIWorker = AccountAPIWorker() - private let disposeBag: DisposeBag = DisposeBag() - - var retryCount: Int = 0 - var retryLimit: Int = 3 - - public func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result) -> Void) { - var urlRequest = urlRequest - - guard urlRequest.url?.absoluteString.hasPrefix(BibbiAPI.hostApi) == true else { - completion(.success(urlRequest)) - return - } - guard let accessToken = App.Repository.token.accessToken.value?.accessToken else { - completion(.success(urlRequest)) - return - } - - urlRequest.setValue(accessToken, forHTTPHeaderField: "X-AUTH-TOKEN") - completion(.success(urlRequest)) - } - - public func retry(_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void) { - - guard let response = request.task?.response as? HTTPURLResponse, response.statusCode == 401 else { - completion(.doNotRetryWithError(error)) - return - } - - if retryCount < retryLimit { - retryCount += 1 - let parameter = AccountRefreshParameter(refreshToken: App.Repository.token.accessToken.value?.refreshToken ?? "") - - accountAPIWorker.accountRefreshToken(parameter: parameter) - .compactMap { $0?.toDomain() } - .asObservable() - .subscribe(onNext: { [weak self] entity in - let refreshToken = AccessToken(accessToken: entity.accessToken, refreshToken: entity.refreshToken, isTemporaryToken: entity.isTemporaryToken) - App.Repository.token.accessToken.accept(refreshToken) - self?.retryCount = 0 - completion(.retry) - }, onError: { error in - completion(.doNotRetryWithError(error)) - }) - .disposed(by: disposeBag) - } - } -} - -// MARK: API Worker -public class APIWorker: NSObject, BibbiRouterInterface { - - // MARK: - Headers - var _headers: Observable<[APIHeader]?> { - return App.Repository.token.accessToken - .map { - guard let token = $0, let accessToken = token.accessToken, !accessToken.isEmpty else { return [] } - return [BibbiAPI.Header.xAppKey, BibbiAPI.Header.xAuthToken(accessToken)] - } - } - - private func appendCommonHeaders(to headers: [APIHeader]?) -> [APIHeader] { - var result: [APIHeader] = BibbiAPI.Header.baseHeaders - guard let headers = headers else { return result } - - result.append(contentsOf: headers) - - return result - } - - private func parameters(_ parameters: [APIParameter]?) -> Parameters? { - guard let kvs = parameters else { return nil } - var result: [String: Any] = [:] - - for kv in kvs { - result[kv.key] = kv.value - } - - return result.isEmpty ? nil: result - } - - // MARK: Identifier - var id: String = "APIWorker" - - // MARK: Request - func request(spec: APISpec, headers: [APIHeader]? = nil, parameters: [APIParameter]? = nil, encoding: ParameterEncoding? = URLEncoding.default) -> Observable<(HTTPURLResponse, Data)> { - let params = self.parameters(parameters) - let hds = self.httpHeaders(self.appendCommonHeaders(to: headers)) - return AF.rx.request(spec.method, spec.url, parameters: params, encoding: encoding!, headers: hds, interceptor: BibbiRequestInterceptor()) - .validate(statusCode: 200..<300) - .responseData() - .debug("API Worker has received data from \"\(spec.url)\"") - } - - func request(spec: APISpec, headers: [APIHeader]? = nil, parameters: Encodable, encoding: ParameterEncoding? = URLEncoding.default) -> Observable<(HTTPURLResponse, Data)> { - let params = parameters.asDictionary() - let hds = self.httpHeaders(self.appendCommonHeaders(to: headers)) - - return AF.rx.request(spec.method, spec.url, parameters: params, encoding: encoding!, headers: hds, interceptor: BibbiRequestInterceptor()) - .validate(statusCode: 200..<300) - .responseData() - .debug("API Worker has received data from \"\(spec.url)\"") - } - - private func refreshRequest(spec: APISpec, headers: [APIHeader]? = nil, jsonData: Data) -> Observable<(HTTPURLResponse, Data)> { - let hds = self.httpHeaders(self.appendCommonHeaders(to: headers)) - guard let url = URL(string: spec.url) else { - return Observable.error(AFError.explicitlyCancelled) - } - - var request = URLRequest(url: url) - request.httpMethod = spec.method.rawValue - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - request.httpBody = jsonData - request.headers = hds - print("interCepter call with name \(url)") - return AF.rx.request(urlRequest: request) - .retry(5) - .validate(statusCode: 200..<300) - .responseData() - .debug("API Worker has received data from \"\(spec.url)\"") - } - - private func request(spec: APISpec, headers: [APIHeader]? = nil, jsonData: Data) -> Observable<(HTTPURLResponse, Data)> { - let hds = self.httpHeaders(self.appendCommonHeaders(to: headers)) - guard let url = URL(string: spec.url) else { - return Observable.error(AFError.explicitlyCancelled) - } - - var request = URLRequest(url: url) - request.httpMethod = spec.method.rawValue - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - request.httpBody = jsonData - request.headers = hds - print("interCepter call with name \(url)") - return AF.rx.request(urlRequest: request, interceptor: BibbiRequestInterceptor()) - .validate(statusCode: 200..<300) - .responseData() - .debug("API Worker has received data from \"\(spec.url)\"") - } - - func request(spec: APISpec, headers: [APIHeader]? = nil, jsonEncodable: Encodable) -> Observable<(HTTPURLResponse, Data)> { - guard let jsonData = jsonEncodable.encodeData() else { - return Observable.error(AFError.explicitlyCancelled) - } - - return self.refreshRequest(spec: spec, headers: headers, jsonData: jsonData) - } - - func upload(spec: APISpec, headers: [APIHeader]? = nil, image: Data) -> Single { - - let hds = self.httpHeaders(self.appendCommonHeaders(to: headers)) - guard let url = URL(string: spec.url) else { - return Single.error(AFError.explicitlyCancelled) - } - - var request = URLRequest(url: url) - request.headers = hds - - return Single.create { single -> Disposable in - AF.upload(image, to: url, method: spec.method, headers: hds, interceptor: BibbiRequestInterceptor()) - .validate(statusCode: [200, 204, 205]) - .responseData(emptyResponseCodes: [200, 204, 205]) { response in - switch response.result { - case .success(_): - single(.success(true)) - case let .failure(failure): - single(.failure(failure)) - } - } - return Disposables.create() - } - - } -} diff --git a/14th-team5-iOS/Data/Sources/API/VoidResponse.swift b/14th-team5-iOS/Data/Sources/API/VoidResponse.swift deleted file mode 100644 index 1be07ba4b..000000000 --- a/14th-team5-iOS/Data/Sources/API/VoidResponse.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// VoidResponse.swift -// Data -// -// Created by 마경미 on 20.02.24. -// - -import Foundation - -struct VoidResponse: Codable { - let success: Bool - - func toDomain() -> Void { - return () - } -} diff --git a/14th-team5-iOS/Data/Sources/APIs/APIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/APIWorker.swift new file mode 100644 index 000000000..ed852c0a9 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/APIWorker.swift @@ -0,0 +1,230 @@ +// +// BBAPIWorker.swift +// Data +// +// Created by 김건우 on 6/2/24. +// + +import Core +import Foundation + +import Alamofire +import RxAlamofire +import RxCocoa +import RxSwift + + +// MARK: - API Worker + +public class APIWorker: NSObject { + + // MARK: - Intercepter + + private let intercepter = NetworkIntercepter() + + + // MARK: - Identifier + + var id: String = "APIWorker" + + + + // MARK: - Request + + func request( + spec: APISpec, + headers: [APIHeader]? = nil, + parameters: [APIParameter]? = nil, + encoding: ParameterEncoding? = URLEncoding.default + ) -> Observable<(HTTPURLResponse, Data)> { + let params = self.parameters(parameters) + let hds = self.httpHeaders(self.appendCommonHeaders(to: headers)) + return AF.rx.request( + spec.method, spec.url, + parameters: params, + encoding: encoding!, + headers: hds, + interceptor: NetworkIntercepter() + ) + .validate(statusCode: 200..<300) + .responseData() + .debug("API Worker has received data from \"\(spec.url)\"") + } + + func request( + spec: APISpec, + headers: [APIHeader]? = nil, + parameters: Encodable, + encoding: ParameterEncoding? = URLEncoding.default + ) -> Observable<(HTTPURLResponse, Data)> { + let params = parameters.asDictionary() + let hds = self.httpHeaders(self.appendCommonHeaders(to: headers)) + + return AF.rx.request( + spec.method, + spec.url, + parameters: params, + encoding: encoding!, + headers: hds, + interceptor: NetworkIntercepter() + ) + .validate(statusCode: 200..<300) + .responseData() + .debug("API Worker has received data from \"\(spec.url)\"") + } + + private func refreshRequest( + spec: APISpec, + headers: [APIHeader]? = nil, + jsonData: Data + ) -> Observable<(HTTPURLResponse, Data)> { + let hds = self.httpHeaders(self.appendCommonHeaders(to: headers)) + guard let url = URL(string: spec.url) else { + return Observable.error(AFError.explicitlyCancelled) + } + + var request = URLRequest(url: url) + request.httpMethod = spec.method.rawValue + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.httpBody = jsonData + request.headers = hds + print("interCepter call with name \(url)") + + return AF.rx.request(urlRequest: request) + .retry(5) + .validate(statusCode: 200..<300) + .responseData() + .debug("API Worker has received data from \"\(spec.url)\"") + } + + private func request( + spec: APISpec, + headers: [APIHeader]? = nil, + jsonData: Data + ) -> Observable<(HTTPURLResponse, Data)> { + let hds = self.httpHeaders(self.appendCommonHeaders(to: headers)) + guard let url = URL(string: spec.url) else { + return Observable.error(AFError.explicitlyCancelled) + } + + var request = URLRequest(url: url) + request.httpMethod = spec.method.rawValue + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.httpBody = jsonData + request.headers = hds + print("interCepter call with name \(url)") + return AF.rx.request( + urlRequest: request, + interceptor: intercepter + ) + .validate(statusCode: 200..<300) + .responseData() + .debug("API Worker has received data from \"\(spec.url)\"") + } + + func request( + spec: APISpec, + headers: [APIHeader]? = nil, + jsonEncodable: Encodable + ) -> Observable<(HTTPURLResponse, Data)> { + guard let jsonData = jsonEncodable.encodeData() else { + return Observable.error(AFError.explicitlyCancelled) + } + + return self.refreshRequest( + spec: spec, + headers: headers, + jsonData: jsonData + ) + } + + func upload( + spec: APISpec, + headers: [APIHeader]? = nil, + image: Data + ) -> Single { + + let hds = self.httpHeaders(self.appendCommonHeaders(to: headers)) + guard let url = URL(string: spec.url) else { + return Single.error(AFError.explicitlyCancelled) + } + + var request = URLRequest(url: url) + request.headers = hds + + return Single.create { [weak self] single -> Disposable in + AF.upload( + image, + to: url, + method: spec.method, + headers: hds, + interceptor: self?.intercepter + ) + .validate(statusCode: [200, 204, 205]) + .responseData(emptyResponseCodes: [200, 204, 205]) { response in + switch response.result { + case .success(_): + single(.success(true)) + case let .failure(failure): + single(.failure(failure)) + } + } + + return Disposables.create() + } + + } +} + + + +// MARK: - Extensions + +extension APIWorker { + + + // MARK: - Headers + + var _headers: Observable<[APIHeader]?> { + return App.Repository.token.accessToken + .map { + guard let token = $0, let accessToken = token.accessToken, !accessToken.isEmpty else { return [] } + return [BibbiAPI.Header.xAppKey, BibbiAPI.Header.xAuthToken(accessToken)] + } + } + + + public func httpHeaders(_ headers: [APIHeader]?) -> HTTPHeaders { + var result: [String: String] = [:] + guard let headers = headers, !headers.isEmpty else { return HTTPHeaders() } + + for header in headers { + result[header.key] = header.value + } + + return HTTPHeaders(result) + } + + + private func appendCommonHeaders(to headers: [APIHeader]?) -> [APIHeader] { + var result: [APIHeader] = BibbiAPI.Header.baseHeaders + guard let headers = headers else { return result } + + result.append(contentsOf: headers) + + return result + } + + private func parameters(_ parameters: [APIParameter]?) -> Parameters? { + guard let kvs = parameters else { return nil } + var result: [String: Any] = [:] + + for kv in kvs { + result[kv.key] = kv.value + } + + return result.isEmpty ? nil: result + } + + +} diff --git a/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/CalendarAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/CalendarAPIs.swift index 5738556f5..e1e94514c 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/CalendarAPIs.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/CalendarAPIs.swift @@ -6,7 +6,7 @@ // import Foundation - +import Core import Domain enum CalendarAPIs: API { @@ -18,7 +18,7 @@ enum CalendarAPIs: API { case fetchStatisticsSummary(String) case fetchBanner(String) - var spec: APISpec { + public var spec: APISpec { switch self { case let .calendarResponse(yearMonth): return APISpec(method: .get, url: "\(BibbiAPI.hostApi)/calendar?type=MONTHLY&yearMonth=\(yearMonth)") 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 45a4947df..b53c1a06c 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/CommentAPIs.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/CommentAPIs.swift @@ -5,9 +5,9 @@ // Created by 김건우 on 1/17/24. // -import Foundation - +import Core import Domain +import Foundation enum CommentAPIs: API { case fetchPostComment(String, Int, Int, String) @@ -15,7 +15,7 @@ enum CommentAPIs: API { case updatePostComment(String, String) case deletePostComment(String, String) - var spec: APISpec { + public var spec: APISpec { 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)") diff --git a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIs.swift index 43cf54a25..f899a5f26 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIs.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIs.swift @@ -17,7 +17,7 @@ public enum FamilyAPIs: API { case fetchFamilyCreatedAt(String) case fetchPaginationFamilyMembers(Int, Int) - var spec: APISpec { + public var spec: APISpec { switch self { case .joinFamily: return APISpec(method: .post, url: "\(BibbiAPI.hostApi)/me/join-family") diff --git a/14th-team5-iOS/Data/Sources/APIs/NetworkIntercepter.swift b/14th-team5-iOS/Data/Sources/APIs/NetworkIntercepter.swift new file mode 100644 index 000000000..b37054a90 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/NetworkIntercepter.swift @@ -0,0 +1,97 @@ +// +// NetworkIntercepter.swift +// Data +// +// Created by 김건우 on 6/2/24. +// + +import Core +import Domain +import Foundation + +import Alamofire +import RxCocoa +import RxSwift + + +public final class NetworkIntercepter: RequestInterceptor { + + // MARK: - Properites + private let disposeBag = DisposeBag() + private let accountAPIWorker = AccountAPIWorker() + + private var retryCount: Int = 0 + private var retryLimit: Int = 3 + + + // MARK: - Storage + private let inMemory = InMemoryWrapper.standard + private let keychain = KeychainWrapper.standard + + + + // MARK: - Adapt + public func adapt( + _ urlRequest: URLRequest, + for session: Session, + completion: @escaping (Result + ) -> Void) { + var urlRequest: URLRequest = urlRequest + + guard + let urlString = urlRequest.url?.absoluteString, + urlString.hasPrefix(BibbiAPI.hostApi) == true + else { + completion(.success(urlRequest)) + return + } + guard + let accessToken = App.Repository.token.accessToken.value?.accessToken + else { + completion(.success(urlRequest)) + return + } + + urlRequest.setValue( + accessToken, + forHTTPHeaderField: "X-AUTH-TOKEN" + ) + completion(.success(urlRequest)) + } + + + + // MARK: - Retry + public func retry( + _ request: Request, + for session: Session, + dueTo error: Error, + completion: @escaping (RetryResult) -> Void + ) { + guard + let response = request.task?.response as? HTTPURLResponse, + response.statusCode == 401 + else { + completion(.doNotRetryWithError(error)) + return + } + + if retryCount < retryLimit { + retryCount += 1 + let parameter = AccountRefreshParameter(refreshToken: App.Repository.token.accessToken.value?.refreshToken ?? "") + + accountAPIWorker.accountRefreshToken(parameter: parameter) + .compactMap { $0?.toDomain() } + .asObservable() + .subscribe(onNext: { [weak self] entity in + let refreshToken = AccessToken(accessToken: entity.accessToken, refreshToken: entity.refreshToken, isTemporaryToken: entity.isTemporaryToken) + App.Repository.token.accessToken.accept(refreshToken) + self?.retryCount = 0 + completion(.retry) + }, onError: { error in + completion(.doNotRetryWithError(error)) + }) + .disposed(by: disposeBag) + } + } +} diff --git a/14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/PickAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/PickAPIs.swift index 81843b73d..9547fae13 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/PickAPIs.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/PickAPIs.swift @@ -5,6 +5,7 @@ // Created by 김건우 on 4/15/24. // +import Core import Foundation enum PickAPIs: API { @@ -12,7 +13,7 @@ enum PickAPIs: API { case whoDidIPick(String) case whoPickedMe(String) - var spec: APISpec { + public var spec: APISpec { switch self { case let .pick(memberId): return APISpec(method: .post, url: "\(BibbiAPI.hostApi)/members/\(memberId)/pick") diff --git a/14th-team5-iOS/Data/Sources/Account/AccountAPI/AccountAPIs.swift b/14th-team5-iOS/Data/Sources/Account/AccountAPI/AccountAPIs.swift index 24aca728c..8ee99b50e 100644 --- a/14th-team5-iOS/Data/Sources/Account/AccountAPI/AccountAPIs.swift +++ b/14th-team5-iOS/Data/Sources/Account/AccountAPI/AccountAPIs.swift @@ -7,6 +7,7 @@ import Foundation import Domain +import Core enum AccountAPIs: API { case forceToken(String) diff --git a/14th-team5-iOS/Data/Sources/Account/DataMapping/VoidResponse.swift b/14th-team5-iOS/Data/Sources/Account/DataMapping/VoidResponse.swift new file mode 100644 index 000000000..88efbfbe4 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/Account/DataMapping/VoidResponse.swift @@ -0,0 +1,17 @@ +// +// VoidResponse.swift +// Data +// +// Created by 김건우 on 6/2/24. +// + +import Foundation + +public struct VoidResponse: Codable { + let success: Bool + + public func toDomain() -> Void { + return () + } +} +// TODO: - 코드 리팩토링 diff --git a/14th-team5-iOS/Data/Sources/Account/MeAPI/MeAPIs.swift b/14th-team5-iOS/Data/Sources/Account/MeAPI/MeAPIs.swift index de27ff107..630060318 100644 --- a/14th-team5-iOS/Data/Sources/Account/MeAPI/MeAPIs.swift +++ b/14th-team5-iOS/Data/Sources/Account/MeAPI/MeAPIs.swift @@ -5,6 +5,7 @@ // Created by geonhui Yu on 1/3/24. // +import Core import Foundation import Domain @@ -15,7 +16,7 @@ public enum MeAPIs: API { case joinFamily case appVersion - var spec: APISpec { + public var spec: APISpec { switch self { case .saveFcmToken: return APISpec(method: .post, url: "\(BibbiAPI.hostApi)/me/fcm") diff --git a/14th-team5-iOS/Data/Sources/Service/AccountSignInHelper.swift b/14th-team5-iOS/Data/Sources/Account/Service/AccountSignInHelper.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Service/AccountSignInHelper.swift rename to 14th-team5-iOS/Data/Sources/Account/Service/AccountSignInHelper.swift diff --git a/14th-team5-iOS/Data/Sources/Service/AccountSignInHelperConfig.swift b/14th-team5-iOS/Data/Sources/Account/Service/AccountSignInHelperConfig.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Service/AccountSignInHelperConfig.swift rename to 14th-team5-iOS/Data/Sources/Account/Service/AccountSignInHelperConfig.swift diff --git a/14th-team5-iOS/Data/Sources/Service/AccountSignInHelperType.swift b/14th-team5-iOS/Data/Sources/Account/Service/AccountSignInHelperType.swift similarity index 96% rename from 14th-team5-iOS/Data/Sources/Service/AccountSignInHelperType.swift rename to 14th-team5-iOS/Data/Sources/Account/Service/AccountSignInHelperType.swift index ec43a22ff..b6c90c768 100644 --- a/14th-team5-iOS/Data/Sources/Service/AccountSignInHelperType.swift +++ b/14th-team5-iOS/Data/Sources/Account/Service/AccountSignInHelperType.swift @@ -7,6 +7,7 @@ import UIKit import Domain +import Core import RxSwift diff --git a/14th-team5-iOS/Data/Sources/Service/Apple/AppleService+Rx.swift b/14th-team5-iOS/Data/Sources/Account/Service/Apple/AppleService+Rx.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Service/Apple/AppleService+Rx.swift rename to 14th-team5-iOS/Data/Sources/Account/Service/Apple/AppleService+Rx.swift diff --git a/14th-team5-iOS/Data/Sources/Service/Apple/AppleSignInHelper.swift b/14th-team5-iOS/Data/Sources/Account/Service/Apple/AppleSignInHelper.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Service/Apple/AppleSignInHelper.swift rename to 14th-team5-iOS/Data/Sources/Account/Service/Apple/AppleSignInHelper.swift diff --git a/14th-team5-iOS/Data/Sources/Service/Kakao/KakaoService+Rx.swift b/14th-team5-iOS/Data/Sources/Account/Service/Kakao/KakaoService+Rx.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Service/Kakao/KakaoService+Rx.swift rename to 14th-team5-iOS/Data/Sources/Account/Service/Kakao/KakaoService+Rx.swift diff --git a/14th-team5-iOS/Data/Sources/Service/Kakao/KakaoSignInHelper.swift b/14th-team5-iOS/Data/Sources/Account/Service/Kakao/KakaoSignInHelper.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Service/Kakao/KakaoSignInHelper.swift rename to 14th-team5-iOS/Data/Sources/Account/Service/Kakao/KakaoSignInHelper.swift diff --git a/14th-team5-iOS/Data/Sources/Camera/CameraAPI/CameraAPIs.swift b/14th-team5-iOS/Data/Sources/Camera/CameraAPI/CameraAPIs.swift index 2f1336f70..1d3793876 100644 --- a/14th-team5-iOS/Data/Sources/Camera/CameraAPI/CameraAPIs.swift +++ b/14th-team5-iOS/Data/Sources/Camera/CameraAPI/CameraAPIs.swift @@ -6,7 +6,7 @@ // import Foundation - +import Core import Domain public enum CameraAPIs: API { @@ -21,7 +21,7 @@ public enum CameraAPIs: API { case modifyRealEmojiImage(String, String) case fetchMissionToday - var spec: APISpec { + public var spec: APISpec { switch self { case .uploadProfileImageURL: return APISpec(method: .post, url: "\(BibbiAPI.hostApi)/members/image-upload-request") diff --git a/14th-team5-iOS/Data/Sources/Emoji/EmojiAPI/EmojiAPIs.swift b/14th-team5-iOS/Data/Sources/Emoji/EmojiAPI/EmojiAPIs.swift index 8d8ac6a20..ca8708178 100644 --- a/14th-team5-iOS/Data/Sources/Emoji/EmojiAPI/EmojiAPIs.swift +++ b/14th-team5-iOS/Data/Sources/Emoji/EmojiAPI/EmojiAPIs.swift @@ -5,6 +5,7 @@ // Created by 마경미 on 01.01.24. // +import Core import Foundation public enum EmojiAPIs: API { @@ -12,7 +13,7 @@ public enum EmojiAPIs: API { case removeReactions(String) case fetchReactions(FetchEmojiRequestDTO) - var spec: APISpec { + public var spec: APISpec { switch self { case let .addReactions(postId): let urlString = "\(BibbiAPI.hostApi)/posts/\(postId)/reactions" diff --git a/14th-team5-iOS/Data/Sources/Mission/MissionAPI/MissionAPIs.swift b/14th-team5-iOS/Data/Sources/Mission/MissionAPI/MissionAPIs.swift index c6f952381..0078b43e3 100644 --- a/14th-team5-iOS/Data/Sources/Mission/MissionAPI/MissionAPIs.swift +++ b/14th-team5-iOS/Data/Sources/Mission/MissionAPI/MissionAPIs.swift @@ -14,7 +14,7 @@ public enum MissionAPIs: API { case getTodayMission case getMissionContent(String) - var spec: APISpec { + public var spec: APISpec { switch self { case .getTodayMission: return APISpec(method: .get, url: "\(BibbiAPI.hostApi)/missions/today") diff --git a/14th-team5-iOS/Data/Sources/Post/PostList/PostListAPI/PostListAPIs.swift b/14th-team5-iOS/Data/Sources/Post/PostList/PostListAPI/PostListAPIs.swift index 01edad2e1..6ddb7e3c8 100644 --- a/14th-team5-iOS/Data/Sources/Post/PostList/PostListAPI/PostListAPIs.swift +++ b/14th-team5-iOS/Data/Sources/Post/PostList/PostListAPI/PostListAPIs.swift @@ -12,7 +12,7 @@ public enum PostListAPIs: API { case fetchPostList case fetchPostDetail(PostRequestDTO) - var spec: APISpec { + public var spec: APISpec { switch self { case .fetchPostList: let urlString = "\(BibbiAPI.hostApi)/posts" diff --git a/14th-team5-iOS/Data/Sources/Privacy/PrivacyAPI/PrivacyAPIWorker.swift b/14th-team5-iOS/Data/Sources/Privacy/PrivacyAPI/PrivacyAPIWorker.swift index d938f1266..3c63c77d4 100644 --- a/14th-team5-iOS/Data/Sources/Privacy/PrivacyAPI/PrivacyAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Privacy/PrivacyAPI/PrivacyAPIWorker.swift @@ -6,7 +6,7 @@ // import Foundation - +import Core import Alamofire import Domain import RxSwift diff --git a/14th-team5-iOS/Data/Sources/Privacy/PrivacyAPI/PrivacyAPIs.swift b/14th-team5-iOS/Data/Sources/Privacy/PrivacyAPI/PrivacyAPIs.swift index 760a39440..7a2e291e9 100644 --- a/14th-team5-iOS/Data/Sources/Privacy/PrivacyAPI/PrivacyAPIs.swift +++ b/14th-team5-iOS/Data/Sources/Privacy/PrivacyAPI/PrivacyAPIs.swift @@ -6,14 +6,14 @@ // import Foundation - +import Core public enum PrivacyAPIs: API { case bibbiAppInfo case accountFamilyResign - var spec: APISpec { + public var spec: APISpec { switch self { case .bibbiAppInfo: return APISpec(method: .get, url: "\(BibbiAPI.hostApi)/me/app-version") diff --git a/14th-team5-iOS/Data/Sources/Profile/ProfileAPI/ProfileAPIWorker.swift b/14th-team5-iOS/Data/Sources/Profile/ProfileAPI/ProfileAPIWorker.swift index 25d5a2f93..7b00edd89 100644 --- a/14th-team5-iOS/Data/Sources/Profile/ProfileAPI/ProfileAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Profile/ProfileAPI/ProfileAPIWorker.swift @@ -6,7 +6,7 @@ // import Foundation - +import Core import Alamofire import Domain import RxSwift diff --git a/14th-team5-iOS/Data/Sources/RealEmoji/RealEmojiAPI/RealEmojiAPIS.swift b/14th-team5-iOS/Data/Sources/RealEmoji/RealEmojiAPI/RealEmojiAPIS.swift index 7213433a9..3f9253b7a 100644 --- a/14th-team5-iOS/Data/Sources/RealEmoji/RealEmojiAPI/RealEmojiAPIS.swift +++ b/14th-team5-iOS/Data/Sources/RealEmoji/RealEmojiAPI/RealEmojiAPIS.swift @@ -14,7 +14,7 @@ public enum RealEmojiAPIS: API { case addRealEmoji(AddRealEmojiParameters) case removeRealEmoji(RemoveRealEmojiParameters) - var spec: APISpec { + public var spec: APISpec { switch self { case .addRealEmoji(let parameter): let urlString = "\(BibbiAPI.hostApi)/posts/\(parameter.postId)/real-emoji" diff --git a/14th-team5-iOS/Data/Sources/Resign/ResignAPI/ResignAPIWorker.swift b/14th-team5-iOS/Data/Sources/Resign/ResignAPI/ResignAPIWorker.swift index fcf1e73bc..1a0a5540d 100644 --- a/14th-team5-iOS/Data/Sources/Resign/ResignAPI/ResignAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Resign/ResignAPI/ResignAPIWorker.swift @@ -6,7 +6,7 @@ // import Foundation - +import Core import Alamofire import Domain import RxSwift diff --git a/14th-team5-iOS/Data/Sources/Resign/ResignAPI/ResignAPIs.swift b/14th-team5-iOS/Data/Sources/Resign/ResignAPI/ResignAPIs.swift index cef43f6e8..cbf8f9821 100644 --- a/14th-team5-iOS/Data/Sources/Resign/ResignAPI/ResignAPIs.swift +++ b/14th-team5-iOS/Data/Sources/Resign/ResignAPI/ResignAPIs.swift @@ -5,6 +5,7 @@ // Created by Kim dohyun on 1/2/24. // +import Core import Foundation @@ -12,7 +13,7 @@ public enum ResignAPIs: API { case accountResign(String) case accountFcmResign(String) - var spec: APISpec { + public var spec: APISpec { switch self { case let .accountResign(memeberId): return APISpec(method: .delete, url: "\(BibbiAPI.hostApi)/members/\(memeberId)") diff --git a/14th-team5-iOS/Data/Sources/View/Main/Worker/MainAPIs.swift b/14th-team5-iOS/Data/Sources/View/Main/Worker/MainAPIs.swift index 6d7a0b83a..009cbc3fa 100644 --- a/14th-team5-iOS/Data/Sources/View/Main/Worker/MainAPIs.swift +++ b/14th-team5-iOS/Data/Sources/View/Main/Worker/MainAPIs.swift @@ -13,7 +13,7 @@ public enum MainAPIs: API { case fetchMain case fetchMainNight - var spec: APISpec { + public var spec: APISpec { switch self { case .fetchMain: let urlString = "\(BibbiAPI.hostApi)/view/main/daytime-page" From 1563f147e5d140af5104ca2124b95c433d84f761 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Mon, 3 Jun 2024 11:44:00 +0900 Subject: [PATCH 067/263] =?UTF-8?q?Revert=20"refactor:=20BBNetwork=20?= =?UTF-8?q?=ED=8F=B4=EB=8D=94=20=EC=83=9D=EC=84=B1=20=EB=B0=8F=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=9D=B4=EB=8F=99,=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20(#473)"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 530877ea181a652c6911840e313d435d6c391795. --- .../Core/Sources/BBNetwork/APIConfig.swift | 23 -- .../KeychainWrapper/KeychainWrapper.swift | 11 +- .../Analytics/Mixpanel/MixpanelService.swift | 2 +- .../BBNetwork => Data/Sources/API}/API.swift | 35 ++- .../Data/Sources/API/APIConfig.swift | 20 ++ .../Sources/API}/APISpec.swift | 22 +- .../Data/Sources/API/APIWorker.swift | 210 ++++++++++++++++ .../Sources/API/Codable+Ext.swift} | 65 +++-- .../Data/Sources/API/VoidResponse.swift | 16 ++ .../Data/Sources/APIs/APIWorker.swift | 230 ------------------ .../Calendar/CalendarAPI/CalendarAPIs.swift | 4 +- .../APIs/Comment/CommentAPI/CommentAPIs.swift | 6 +- .../APIs/Family/FamilyAPI/FamilyAPIs.swift | 2 +- .../Sources/APIs/NetworkIntercepter.swift | 97 -------- .../Sources/APIs/Pick/PickAPI/PickAPIs.swift | 3 +- .../Account/AccountAPI/AccountAPIs.swift | 1 - .../Account/DataMapping/VoidResponse.swift | 17 -- .../Data/Sources/Account/MeAPI/MeAPIs.swift | 3 +- .../Sources/Camera/CameraAPI/CameraAPIs.swift | 4 +- .../Sources/Emoji/EmojiAPI/EmojiAPIs.swift | 3 +- .../Mission/MissionAPI/MissionAPIs.swift | 2 +- .../PostList/PostListAPI/PostListAPIs.swift | 2 +- .../Privacy/PrivacyAPI/PrivacyAPIWorker.swift | 2 +- .../Privacy/PrivacyAPI/PrivacyAPIs.swift | 4 +- .../Profile/ProfileAPI/ProfileAPIWorker.swift | 2 +- .../RealEmojiAPI/RealEmojiAPIS.swift | 2 +- .../Resign/ResignAPI/ResignAPIWorker.swift | 2 +- .../Sources/Resign/ResignAPI/ResignAPIs.swift | 3 +- .../Service/AccountSignInHelper.swift | 0 .../Service/AccountSignInHelperConfig.swift | 0 .../Service/AccountSignInHelperType.swift | 1 - .../Service/Apple/AppleService+Rx.swift | 0 .../Service/Apple/AppleSignInHelper.swift | 0 .../Service/Kakao/KakaoService+Rx.swift | 0 .../Service/Kakao/KakaoSignInHelper.swift | 0 .../Sources/View/Main/Worker/MainAPIs.swift | 2 +- 36 files changed, 329 insertions(+), 467 deletions(-) delete mode 100644 14th-team5-iOS/Core/Sources/BBNetwork/APIConfig.swift rename 14th-team5-iOS/{Core/Sources/BBNetwork => Data/Sources/API}/API.swift (74%) create mode 100644 14th-team5-iOS/Data/Sources/API/APIConfig.swift rename 14th-team5-iOS/{Core/Sources/BBNetwork => Data/Sources/API}/APISpec.swift (69%) create mode 100644 14th-team5-iOS/Data/Sources/API/APIWorker.swift rename 14th-team5-iOS/{Core/Sources/Extensions/Codable+ExtTTT.swift => Data/Sources/API/Codable+Ext.swift} (71%) create mode 100644 14th-team5-iOS/Data/Sources/API/VoidResponse.swift delete mode 100644 14th-team5-iOS/Data/Sources/APIs/APIWorker.swift delete mode 100644 14th-team5-iOS/Data/Sources/APIs/NetworkIntercepter.swift delete mode 100644 14th-team5-iOS/Data/Sources/Account/DataMapping/VoidResponse.swift rename 14th-team5-iOS/Data/Sources/{Account => }/Service/AccountSignInHelper.swift (100%) rename 14th-team5-iOS/Data/Sources/{Account => }/Service/AccountSignInHelperConfig.swift (100%) rename 14th-team5-iOS/Data/Sources/{Account => }/Service/AccountSignInHelperType.swift (96%) rename 14th-team5-iOS/Data/Sources/{Account => }/Service/Apple/AppleService+Rx.swift (100%) rename 14th-team5-iOS/Data/Sources/{Account => }/Service/Apple/AppleSignInHelper.swift (100%) rename 14th-team5-iOS/Data/Sources/{Account => }/Service/Kakao/KakaoService+Rx.swift (100%) rename 14th-team5-iOS/Data/Sources/{Account => }/Service/Kakao/KakaoSignInHelper.swift (100%) diff --git a/14th-team5-iOS/Core/Sources/BBNetwork/APIConfig.swift b/14th-team5-iOS/Core/Sources/BBNetwork/APIConfig.swift deleted file mode 100644 index 2240509f2..000000000 --- a/14th-team5-iOS/Core/Sources/BBNetwork/APIConfig.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// APIConfig.swift -// Data -// -// Created by geonhui Yu on 12/17/23. -// - -import Foundation - -public protocol BibbiAPIConfigType { - var hostApi: String { get } - var xAppKey: String { get } -} - -public struct BibbiAPIConfig: BibbiAPIConfigType { - #if PRD - public var hostApi: String = "https://api.no5ing.kr/v1" - #else - public var hostApi: String = "https://dev.api.no5ing.kr/v1" - #endif - - public var xAppKey: String = "7c5aaa36-570e-491f-b18a-26a1a0b72959" -} diff --git a/14th-team5-iOS/Core/Sources/BBStorages/KeychainWrapper/KeychainWrapper.swift b/14th-team5-iOS/Core/Sources/BBStorages/KeychainWrapper/KeychainWrapper.swift index 183794aa5..c76c65355 100644 --- a/14th-team5-iOS/Core/Sources/BBStorages/KeychainWrapper/KeychainWrapper.swift +++ b/14th-team5-iOS/Core/Sources/BBStorages/KeychainWrapper/KeychainWrapper.swift @@ -27,11 +27,7 @@ final public class KeychainWrapper { private(set) public var accessGroup: String? private static let defaultServiceName: String = { - return "Bibbi" - }() - - private static let defaultAccessGroupName: String = { - return "P9P4WJ623F.com.5ing.bibbi" + return Bundle.main.bundleIdentifier ?? "KeychainWrapper" }() // MARK: - Intializer @@ -44,10 +40,7 @@ final public class KeychainWrapper { } private convenience init() { - self.init( - serviceName: KeychainWrapper.defaultServiceName, - accessGroup: KeychainWrapper.defaultAccessGroupName - ) + self.init(serviceName: KeychainWrapper.defaultServiceName) } diff --git a/14th-team5-iOS/Core/Sources/Utilties/Analytics/Mixpanel/MixpanelService.swift b/14th-team5-iOS/Core/Sources/Utilties/Analytics/Mixpanel/MixpanelService.swift index bdb969473..a10daca98 100644 --- a/14th-team5-iOS/Core/Sources/Utilties/Analytics/Mixpanel/MixpanelService.swift +++ b/14th-team5-iOS/Core/Sources/Utilties/Analytics/Mixpanel/MixpanelService.swift @@ -62,7 +62,7 @@ extension MPEvent { } // MARK: Extensions -public extension Encodable { +extension Encodable { func asDictionary() -> [String: Any]? { do { let data = try JSONEncoder().encode(self) diff --git a/14th-team5-iOS/Core/Sources/BBNetwork/API.swift b/14th-team5-iOS/Data/Sources/API/API.swift similarity index 74% rename from 14th-team5-iOS/Core/Sources/BBNetwork/API.swift rename to 14th-team5-iOS/Data/Sources/API/API.swift index 6ebd57d5f..c83694cf0 100644 --- a/14th-team5-iOS/Core/Sources/BBNetwork/API.swift +++ b/14th-team5-iOS/Data/Sources/API/API.swift @@ -6,23 +6,22 @@ // import Foundation - +import Core // MARK: APIs of AFP -public typealias BibbiHeader = BibbiAPI.Header -public typealias BibbiResponse = BibbiAPI.Response -public typealias BibbiNoResponse = BibbiAPI.NoResponse -public typealias BibbiBoolResponse = BibbiAPI.BoolResponse -public typealias BibbiCodableResponse = BibbiAPI.CodableResponse +typealias BibbiHeader = BibbiAPI.Header +typealias BibbiResponse = BibbiAPI.Response +typealias BibbiNoResponse = BibbiAPI.NoResponse +typealias BibbiBoolResponse = BibbiAPI.BoolResponse +typealias BibbiCodableResponse = BibbiAPI.CodableResponse -public enum BibbiAPI { +enum BibbiAPI { private static let _config: BibbiAPIConfigType = BibbiAPIConfig() - public static let hostApi: String = _config.hostApi - public static let xAppKey: String = _config.xAppKey + static let hostApi: String = _config.hostApi // MARK: Common Headers - public enum Header: APIHeader { + enum Header: APIHeader { case auth(String) case xAppKey case xAuthToken(String) @@ -34,7 +33,7 @@ public enum BibbiAPI { case xAppVersion case xUserID - public var key: String { + var key: String { switch self { case .auth: return "Authorization" case .xAppKey: return "X-APP-KEY" @@ -49,10 +48,10 @@ public enum BibbiAPI { } } - public var value: String { + var value: String { switch self { case .auth(let value): return "Bearer \(value)" - case .xAppKey: return "\(BibbiAPI.xAppKey)" + case .xAppKey: return "7c5aaa36-570e-491f-b18a-26a1a0b72959" case .xAuthToken(let value): return "\(value)" case .contentForm: return "application/x-www-form-urlencoded" case .contentJson: return "application/json" @@ -64,12 +63,12 @@ public enum BibbiAPI { } } - public static var baseHeaders: [Self] { + static var baseHeaders: [Self] { return [.xAppKey, .xAppVersion, .xUserPlatform, .xUserID] } } - public struct Response: Codable { + struct Response: Codable { let status: String? let code: Int? let errorCode: String? @@ -78,20 +77,20 @@ public enum BibbiAPI { } } - public struct NoResponse: Codable { + struct NoResponse: Codable { // var status: String? // var code: Int? // var errorCode: String? } - public struct BoolResponse: Codable { + struct BoolResponse: Codable { var status: String? var code: Int? var errorCode: String? var result: Bool? } - public struct CodableResponse: Codable { + struct CodableResponse: Codable { // var status: String? // var code: String? // var message: String? diff --git a/14th-team5-iOS/Data/Sources/API/APIConfig.swift b/14th-team5-iOS/Data/Sources/API/APIConfig.swift new file mode 100644 index 000000000..dae5df606 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/API/APIConfig.swift @@ -0,0 +1,20 @@ +// +// APIConfig.swift +// Data +// +// Created by geonhui Yu on 12/17/23. +// + +import Foundation + +protocol BibbiAPIConfigType { + var hostApi: String { get } +} + +struct BibbiAPIConfig: BibbiAPIConfigType { + #if PRD + var hostApi: String = "https://api.no5ing.kr/v1" + #else + var hostApi: String = "https://dev.api.no5ing.kr/v1" + #endif +} diff --git a/14th-team5-iOS/Core/Sources/BBNetwork/APISpec.swift b/14th-team5-iOS/Data/Sources/API/APISpec.swift similarity index 69% rename from 14th-team5-iOS/Core/Sources/BBNetwork/APISpec.swift rename to 14th-team5-iOS/Data/Sources/API/APISpec.swift index eef5dea89..b19d4db48 100644 --- a/14th-team5-iOS/Core/Sources/BBNetwork/APISpec.swift +++ b/14th-team5-iOS/Data/Sources/API/APISpec.swift @@ -11,29 +11,29 @@ import RxSwift // MARK: API Protocol typealias BaseAPI = API -public protocol API { +protocol API { var spec: APISpec { get } } // MARK: API Constants -public enum APIConst { +enum APIConst { static let boundary = UUID().uuidString } // MARK: API Header Protocol -public protocol APIHeader { +protocol APIHeader { var key: String { get } var value: String { get } } // MARK: API Prameter Protocol -public protocol APIParameter { +protocol APIParameter { var key: String { get } var value: Any? { get } } // MARK: API Media Prameter -public struct APIMediaParameter { +struct APIMediaParameter { let name: String let fileName: String let mimeType: String @@ -42,7 +42,7 @@ public struct APIMediaParameter { } // MARK: API Result Type -public typealias APIRes = APIResult +typealias APIRes = APIResult public enum APIResult { static let ok: Int = 200 case success @@ -50,14 +50,14 @@ public enum APIResult { } // MARK: API Error Protocol -public protocol APIError: CustomNSError, Equatable {} +protocol APIError: CustomNSError, Equatable {} // MARK: API Specification -public struct APISpec { - public let method: HTTPMethod - public let url: String +struct APISpec { + let method: HTTPMethod + let url: String - public init(method: HTTPMethod, url: String) { + init(method: HTTPMethod, url: String) { self.method = method self.url = url } diff --git a/14th-team5-iOS/Data/Sources/API/APIWorker.swift b/14th-team5-iOS/Data/Sources/API/APIWorker.swift new file mode 100644 index 000000000..c60ba0de1 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/API/APIWorker.swift @@ -0,0 +1,210 @@ +// +// BaseAPIWorker.swift +// Data +// +// Created by geonhui Yu on 12/17/23. +// + +import Foundation + +import Alamofire +import RxSwift +import RxCocoa +import Core +import Domain + +protocol BibbiRouterInterface { + func httpHeaders(_ headers: [APIHeader]?) -> HTTPHeaders +} + +extension BibbiRouterInterface { + public func httpHeaders(_ headers: [APIHeader]?) -> HTTPHeaders { + var result: [String: String] = [:] + guard let headers = headers, !headers.isEmpty else { return HTTPHeaders() } + + for header in headers { + result[header.key] = header.value + } + + return HTTPHeaders(result) + } +} + +public final class BibbiRequestInterceptor: RequestInterceptor, BibbiRouterInterface { + + //TODO: Test용 KeychainWrapper 코드 다 제거하기 + private let accountAPIWorker: AccountAPIWorker = AccountAPIWorker() + private let disposeBag: DisposeBag = DisposeBag() + + var retryCount: Int = 0 + var retryLimit: Int = 3 + + public func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result) -> Void) { + var urlRequest = urlRequest + + guard urlRequest.url?.absoluteString.hasPrefix(BibbiAPI.hostApi) == true else { + completion(.success(urlRequest)) + return + } + guard let accessToken = App.Repository.token.accessToken.value?.accessToken else { + completion(.success(urlRequest)) + return + } + + urlRequest.setValue(accessToken, forHTTPHeaderField: "X-AUTH-TOKEN") + completion(.success(urlRequest)) + } + + public func retry(_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void) { + + guard let response = request.task?.response as? HTTPURLResponse, response.statusCode == 401 else { + completion(.doNotRetryWithError(error)) + return + } + + if retryCount < retryLimit { + retryCount += 1 + let parameter = AccountRefreshParameter(refreshToken: App.Repository.token.accessToken.value?.refreshToken ?? "") + + accountAPIWorker.accountRefreshToken(parameter: parameter) + .compactMap { $0?.toDomain() } + .asObservable() + .subscribe(onNext: { [weak self] entity in + let refreshToken = AccessToken(accessToken: entity.accessToken, refreshToken: entity.refreshToken, isTemporaryToken: entity.isTemporaryToken) + App.Repository.token.accessToken.accept(refreshToken) + self?.retryCount = 0 + completion(.retry) + }, onError: { error in + completion(.doNotRetryWithError(error)) + }) + .disposed(by: disposeBag) + } + } +} + +// MARK: API Worker +public class APIWorker: NSObject, BibbiRouterInterface { + + // MARK: - Headers + var _headers: Observable<[APIHeader]?> { + return App.Repository.token.accessToken + .map { + guard let token = $0, let accessToken = token.accessToken, !accessToken.isEmpty else { return [] } + return [BibbiAPI.Header.xAppKey, BibbiAPI.Header.xAuthToken(accessToken)] + } + } + + private func appendCommonHeaders(to headers: [APIHeader]?) -> [APIHeader] { + var result: [APIHeader] = BibbiAPI.Header.baseHeaders + guard let headers = headers else { return result } + + result.append(contentsOf: headers) + + return result + } + + private func parameters(_ parameters: [APIParameter]?) -> Parameters? { + guard let kvs = parameters else { return nil } + var result: [String: Any] = [:] + + for kv in kvs { + result[kv.key] = kv.value + } + + return result.isEmpty ? nil: result + } + + // MARK: Identifier + var id: String = "APIWorker" + + // MARK: Request + func request(spec: APISpec, headers: [APIHeader]? = nil, parameters: [APIParameter]? = nil, encoding: ParameterEncoding? = URLEncoding.default) -> Observable<(HTTPURLResponse, Data)> { + let params = self.parameters(parameters) + let hds = self.httpHeaders(self.appendCommonHeaders(to: headers)) + return AF.rx.request(spec.method, spec.url, parameters: params, encoding: encoding!, headers: hds, interceptor: BibbiRequestInterceptor()) + .validate(statusCode: 200..<300) + .responseData() + .debug("API Worker has received data from \"\(spec.url)\"") + } + + func request(spec: APISpec, headers: [APIHeader]? = nil, parameters: Encodable, encoding: ParameterEncoding? = URLEncoding.default) -> Observable<(HTTPURLResponse, Data)> { + let params = parameters.asDictionary() + let hds = self.httpHeaders(self.appendCommonHeaders(to: headers)) + + return AF.rx.request(spec.method, spec.url, parameters: params, encoding: encoding!, headers: hds, interceptor: BibbiRequestInterceptor()) + .validate(statusCode: 200..<300) + .responseData() + .debug("API Worker has received data from \"\(spec.url)\"") + } + + private func refreshRequest(spec: APISpec, headers: [APIHeader]? = nil, jsonData: Data) -> Observable<(HTTPURLResponse, Data)> { + let hds = self.httpHeaders(self.appendCommonHeaders(to: headers)) + guard let url = URL(string: spec.url) else { + return Observable.error(AFError.explicitlyCancelled) + } + + var request = URLRequest(url: url) + request.httpMethod = spec.method.rawValue + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.httpBody = jsonData + request.headers = hds + print("interCepter call with name \(url)") + return AF.rx.request(urlRequest: request) + .retry(5) + .validate(statusCode: 200..<300) + .responseData() + .debug("API Worker has received data from \"\(spec.url)\"") + } + + private func request(spec: APISpec, headers: [APIHeader]? = nil, jsonData: Data) -> Observable<(HTTPURLResponse, Data)> { + let hds = self.httpHeaders(self.appendCommonHeaders(to: headers)) + guard let url = URL(string: spec.url) else { + return Observable.error(AFError.explicitlyCancelled) + } + + var request = URLRequest(url: url) + request.httpMethod = spec.method.rawValue + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.httpBody = jsonData + request.headers = hds + print("interCepter call with name \(url)") + return AF.rx.request(urlRequest: request, interceptor: BibbiRequestInterceptor()) + .validate(statusCode: 200..<300) + .responseData() + .debug("API Worker has received data from \"\(spec.url)\"") + } + + func request(spec: APISpec, headers: [APIHeader]? = nil, jsonEncodable: Encodable) -> Observable<(HTTPURLResponse, Data)> { + guard let jsonData = jsonEncodable.encodeData() else { + return Observable.error(AFError.explicitlyCancelled) + } + + return self.refreshRequest(spec: spec, headers: headers, jsonData: jsonData) + } + + func upload(spec: APISpec, headers: [APIHeader]? = nil, image: Data) -> Single { + + let hds = self.httpHeaders(self.appendCommonHeaders(to: headers)) + guard let url = URL(string: spec.url) else { + return Single.error(AFError.explicitlyCancelled) + } + + var request = URLRequest(url: url) + request.headers = hds + + return Single.create { single -> Disposable in + AF.upload(image, to: url, method: spec.method, headers: hds, interceptor: BibbiRequestInterceptor()) + .validate(statusCode: [200, 204, 205]) + .responseData(emptyResponseCodes: [200, 204, 205]) { response in + switch response.result { + case .success(_): + single(.success(true)) + case let .failure(failure): + single(.failure(failure)) + } + } + return Disposables.create() + } + + } +} diff --git a/14th-team5-iOS/Core/Sources/Extensions/Codable+ExtTTT.swift b/14th-team5-iOS/Data/Sources/API/Codable+Ext.swift similarity index 71% rename from 14th-team5-iOS/Core/Sources/Extensions/Codable+ExtTTT.swift rename to 14th-team5-iOS/Data/Sources/API/Codable+Ext.swift index eb991cab2..f6879933f 100644 --- a/14th-team5-iOS/Core/Sources/Extensions/Codable+ExtTTT.swift +++ b/14th-team5-iOS/Data/Sources/API/Codable+Ext.swift @@ -7,35 +7,32 @@ import Foundation +import Alamofire import RxSwift - - -// TODO: - 파일 별로 쪼개기 - // MARK: Data Decodable (Data to Decodable) public extension Data { -// func decode(_ type: T.Type, using decoder: JSONDecoder? = nil) -> T? where T: Decodable { -// -// let decoder = decoder ?? JSONDecoder() -// -// var res: T? = nil -// do { -// res = try decoder.decode(type, from: self) -// } catch { -// debugPrint("\(T.self) Data Parsing Error: \(error)") -// } -// -// return res -// } + func decode(_ type: T.Type, using decoder: JSONDecoder? = nil) -> T? where T: Decodable { + + let decoder = decoder ?? JSONDecoder() + + var res: T? = nil + do { + res = try decoder.decode(type, from: self) + } catch { + debugPrint("\(T.self) Data Parsing Error: \(error)") + } + + return res + } } // MARK: String Decodable (String to Decodable) public extension String { -// func decode(_ type: T.Type, using decoder: JSONDecoder? = nil) -> T? where T: Decodable { -// -// return self.data(using: .utf8)?.decode(type, using: decoder) -// } + func decode(_ type: T.Type, using decoder: JSONDecoder? = nil) -> T? where T: Decodable { + + return self.data(using: .utf8)?.decode(type, using: decoder) + } } // MARK: Dictionary([String : Any]) Decodable @@ -105,19 +102,19 @@ public extension PrimitiveSequenceType where Trait == SingleTrait, Element == (H } } -public extension Encodable { -// -// func asDictionary() -> [String: Any]? { -// do { -// let data = try JSONEncoder().encode(self) -// guard let dict = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any] else { return nil } -// -// return dict -// } catch let error { -// print(error) -// return nil -// } -// } +extension Encodable { + + func asDictionary() -> [String: Any]? { + do { + let data = try JSONEncoder().encode(self) + guard let dict = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any] else { return nil } + + return dict + } catch let error { + print(error) + return nil + } + } func asArray() throws -> [AnyObject] { let data = try JSONEncoder().encode(self) diff --git a/14th-team5-iOS/Data/Sources/API/VoidResponse.swift b/14th-team5-iOS/Data/Sources/API/VoidResponse.swift new file mode 100644 index 000000000..1be07ba4b --- /dev/null +++ b/14th-team5-iOS/Data/Sources/API/VoidResponse.swift @@ -0,0 +1,16 @@ +// +// VoidResponse.swift +// Data +// +// Created by 마경미 on 20.02.24. +// + +import Foundation + +struct VoidResponse: Codable { + let success: Bool + + func toDomain() -> Void { + return () + } +} diff --git a/14th-team5-iOS/Data/Sources/APIs/APIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/APIWorker.swift deleted file mode 100644 index ed852c0a9..000000000 --- a/14th-team5-iOS/Data/Sources/APIs/APIWorker.swift +++ /dev/null @@ -1,230 +0,0 @@ -// -// BBAPIWorker.swift -// Data -// -// Created by 김건우 on 6/2/24. -// - -import Core -import Foundation - -import Alamofire -import RxAlamofire -import RxCocoa -import RxSwift - - -// MARK: - API Worker - -public class APIWorker: NSObject { - - // MARK: - Intercepter - - private let intercepter = NetworkIntercepter() - - - // MARK: - Identifier - - var id: String = "APIWorker" - - - - // MARK: - Request - - func request( - spec: APISpec, - headers: [APIHeader]? = nil, - parameters: [APIParameter]? = nil, - encoding: ParameterEncoding? = URLEncoding.default - ) -> Observable<(HTTPURLResponse, Data)> { - let params = self.parameters(parameters) - let hds = self.httpHeaders(self.appendCommonHeaders(to: headers)) - return AF.rx.request( - spec.method, spec.url, - parameters: params, - encoding: encoding!, - headers: hds, - interceptor: NetworkIntercepter() - ) - .validate(statusCode: 200..<300) - .responseData() - .debug("API Worker has received data from \"\(spec.url)\"") - } - - func request( - spec: APISpec, - headers: [APIHeader]? = nil, - parameters: Encodable, - encoding: ParameterEncoding? = URLEncoding.default - ) -> Observable<(HTTPURLResponse, Data)> { - let params = parameters.asDictionary() - let hds = self.httpHeaders(self.appendCommonHeaders(to: headers)) - - return AF.rx.request( - spec.method, - spec.url, - parameters: params, - encoding: encoding!, - headers: hds, - interceptor: NetworkIntercepter() - ) - .validate(statusCode: 200..<300) - .responseData() - .debug("API Worker has received data from \"\(spec.url)\"") - } - - private func refreshRequest( - spec: APISpec, - headers: [APIHeader]? = nil, - jsonData: Data - ) -> Observable<(HTTPURLResponse, Data)> { - let hds = self.httpHeaders(self.appendCommonHeaders(to: headers)) - guard let url = URL(string: spec.url) else { - return Observable.error(AFError.explicitlyCancelled) - } - - var request = URLRequest(url: url) - request.httpMethod = spec.method.rawValue - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - request.httpBody = jsonData - request.headers = hds - print("interCepter call with name \(url)") - - return AF.rx.request(urlRequest: request) - .retry(5) - .validate(statusCode: 200..<300) - .responseData() - .debug("API Worker has received data from \"\(spec.url)\"") - } - - private func request( - spec: APISpec, - headers: [APIHeader]? = nil, - jsonData: Data - ) -> Observable<(HTTPURLResponse, Data)> { - let hds = self.httpHeaders(self.appendCommonHeaders(to: headers)) - guard let url = URL(string: spec.url) else { - return Observable.error(AFError.explicitlyCancelled) - } - - var request = URLRequest(url: url) - request.httpMethod = spec.method.rawValue - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - request.httpBody = jsonData - request.headers = hds - print("interCepter call with name \(url)") - return AF.rx.request( - urlRequest: request, - interceptor: intercepter - ) - .validate(statusCode: 200..<300) - .responseData() - .debug("API Worker has received data from \"\(spec.url)\"") - } - - func request( - spec: APISpec, - headers: [APIHeader]? = nil, - jsonEncodable: Encodable - ) -> Observable<(HTTPURLResponse, Data)> { - guard let jsonData = jsonEncodable.encodeData() else { - return Observable.error(AFError.explicitlyCancelled) - } - - return self.refreshRequest( - spec: spec, - headers: headers, - jsonData: jsonData - ) - } - - func upload( - spec: APISpec, - headers: [APIHeader]? = nil, - image: Data - ) -> Single { - - let hds = self.httpHeaders(self.appendCommonHeaders(to: headers)) - guard let url = URL(string: spec.url) else { - return Single.error(AFError.explicitlyCancelled) - } - - var request = URLRequest(url: url) - request.headers = hds - - return Single.create { [weak self] single -> Disposable in - AF.upload( - image, - to: url, - method: spec.method, - headers: hds, - interceptor: self?.intercepter - ) - .validate(statusCode: [200, 204, 205]) - .responseData(emptyResponseCodes: [200, 204, 205]) { response in - switch response.result { - case .success(_): - single(.success(true)) - case let .failure(failure): - single(.failure(failure)) - } - } - - return Disposables.create() - } - - } -} - - - -// MARK: - Extensions - -extension APIWorker { - - - // MARK: - Headers - - var _headers: Observable<[APIHeader]?> { - return App.Repository.token.accessToken - .map { - guard let token = $0, let accessToken = token.accessToken, !accessToken.isEmpty else { return [] } - return [BibbiAPI.Header.xAppKey, BibbiAPI.Header.xAuthToken(accessToken)] - } - } - - - public func httpHeaders(_ headers: [APIHeader]?) -> HTTPHeaders { - var result: [String: String] = [:] - guard let headers = headers, !headers.isEmpty else { return HTTPHeaders() } - - for header in headers { - result[header.key] = header.value - } - - return HTTPHeaders(result) - } - - - private func appendCommonHeaders(to headers: [APIHeader]?) -> [APIHeader] { - var result: [APIHeader] = BibbiAPI.Header.baseHeaders - guard let headers = headers else { return result } - - result.append(contentsOf: headers) - - return result - } - - private func parameters(_ parameters: [APIParameter]?) -> Parameters? { - guard let kvs = parameters else { return nil } - var result: [String: Any] = [:] - - for kv in kvs { - result[kv.key] = kv.value - } - - return result.isEmpty ? nil: result - } - - -} diff --git a/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/CalendarAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/CalendarAPIs.swift index e1e94514c..5738556f5 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/CalendarAPIs.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/CalendarAPIs.swift @@ -6,7 +6,7 @@ // import Foundation -import Core + import Domain enum CalendarAPIs: API { @@ -18,7 +18,7 @@ enum CalendarAPIs: API { case fetchStatisticsSummary(String) case fetchBanner(String) - public var spec: APISpec { + var spec: APISpec { switch self { case let .calendarResponse(yearMonth): return APISpec(method: .get, url: "\(BibbiAPI.hostApi)/calendar?type=MONTHLY&yearMonth=\(yearMonth)") 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 b53c1a06c..45a4947df 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/CommentAPIs.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/CommentAPIs.swift @@ -5,17 +5,17 @@ // Created by 김건우 on 1/17/24. // -import Core -import Domain import Foundation +import Domain + enum CommentAPIs: API { case fetchPostComment(String, Int, Int, String) case createPostComment(String) case updatePostComment(String, String) case deletePostComment(String, String) - public var spec: APISpec { + var spec: APISpec { 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)") diff --git a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIs.swift index f899a5f26..43cf54a25 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIs.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIs.swift @@ -17,7 +17,7 @@ public enum FamilyAPIs: API { case fetchFamilyCreatedAt(String) case fetchPaginationFamilyMembers(Int, Int) - public var spec: APISpec { + var spec: APISpec { switch self { case .joinFamily: return APISpec(method: .post, url: "\(BibbiAPI.hostApi)/me/join-family") diff --git a/14th-team5-iOS/Data/Sources/APIs/NetworkIntercepter.swift b/14th-team5-iOS/Data/Sources/APIs/NetworkIntercepter.swift deleted file mode 100644 index b37054a90..000000000 --- a/14th-team5-iOS/Data/Sources/APIs/NetworkIntercepter.swift +++ /dev/null @@ -1,97 +0,0 @@ -// -// NetworkIntercepter.swift -// Data -// -// Created by 김건우 on 6/2/24. -// - -import Core -import Domain -import Foundation - -import Alamofire -import RxCocoa -import RxSwift - - -public final class NetworkIntercepter: RequestInterceptor { - - // MARK: - Properites - private let disposeBag = DisposeBag() - private let accountAPIWorker = AccountAPIWorker() - - private var retryCount: Int = 0 - private var retryLimit: Int = 3 - - - // MARK: - Storage - private let inMemory = InMemoryWrapper.standard - private let keychain = KeychainWrapper.standard - - - - // MARK: - Adapt - public func adapt( - _ urlRequest: URLRequest, - for session: Session, - completion: @escaping (Result - ) -> Void) { - var urlRequest: URLRequest = urlRequest - - guard - let urlString = urlRequest.url?.absoluteString, - urlString.hasPrefix(BibbiAPI.hostApi) == true - else { - completion(.success(urlRequest)) - return - } - guard - let accessToken = App.Repository.token.accessToken.value?.accessToken - else { - completion(.success(urlRequest)) - return - } - - urlRequest.setValue( - accessToken, - forHTTPHeaderField: "X-AUTH-TOKEN" - ) - completion(.success(urlRequest)) - } - - - - // MARK: - Retry - public func retry( - _ request: Request, - for session: Session, - dueTo error: Error, - completion: @escaping (RetryResult) -> Void - ) { - guard - let response = request.task?.response as? HTTPURLResponse, - response.statusCode == 401 - else { - completion(.doNotRetryWithError(error)) - return - } - - if retryCount < retryLimit { - retryCount += 1 - let parameter = AccountRefreshParameter(refreshToken: App.Repository.token.accessToken.value?.refreshToken ?? "") - - accountAPIWorker.accountRefreshToken(parameter: parameter) - .compactMap { $0?.toDomain() } - .asObservable() - .subscribe(onNext: { [weak self] entity in - let refreshToken = AccessToken(accessToken: entity.accessToken, refreshToken: entity.refreshToken, isTemporaryToken: entity.isTemporaryToken) - App.Repository.token.accessToken.accept(refreshToken) - self?.retryCount = 0 - completion(.retry) - }, onError: { error in - completion(.doNotRetryWithError(error)) - }) - .disposed(by: disposeBag) - } - } -} diff --git a/14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/PickAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/PickAPIs.swift index 9547fae13..81843b73d 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/PickAPIs.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/PickAPIs.swift @@ -5,7 +5,6 @@ // Created by 김건우 on 4/15/24. // -import Core import Foundation enum PickAPIs: API { @@ -13,7 +12,7 @@ enum PickAPIs: API { case whoDidIPick(String) case whoPickedMe(String) - public var spec: APISpec { + var spec: APISpec { switch self { case let .pick(memberId): return APISpec(method: .post, url: "\(BibbiAPI.hostApi)/members/\(memberId)/pick") diff --git a/14th-team5-iOS/Data/Sources/Account/AccountAPI/AccountAPIs.swift b/14th-team5-iOS/Data/Sources/Account/AccountAPI/AccountAPIs.swift index 8ee99b50e..24aca728c 100644 --- a/14th-team5-iOS/Data/Sources/Account/AccountAPI/AccountAPIs.swift +++ b/14th-team5-iOS/Data/Sources/Account/AccountAPI/AccountAPIs.swift @@ -7,7 +7,6 @@ import Foundation import Domain -import Core enum AccountAPIs: API { case forceToken(String) diff --git a/14th-team5-iOS/Data/Sources/Account/DataMapping/VoidResponse.swift b/14th-team5-iOS/Data/Sources/Account/DataMapping/VoidResponse.swift deleted file mode 100644 index 88efbfbe4..000000000 --- a/14th-team5-iOS/Data/Sources/Account/DataMapping/VoidResponse.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// VoidResponse.swift -// Data -// -// Created by 김건우 on 6/2/24. -// - -import Foundation - -public struct VoidResponse: Codable { - let success: Bool - - public func toDomain() -> Void { - return () - } -} -// TODO: - 코드 리팩토링 diff --git a/14th-team5-iOS/Data/Sources/Account/MeAPI/MeAPIs.swift b/14th-team5-iOS/Data/Sources/Account/MeAPI/MeAPIs.swift index 630060318..de27ff107 100644 --- a/14th-team5-iOS/Data/Sources/Account/MeAPI/MeAPIs.swift +++ b/14th-team5-iOS/Data/Sources/Account/MeAPI/MeAPIs.swift @@ -5,7 +5,6 @@ // Created by geonhui Yu on 1/3/24. // -import Core import Foundation import Domain @@ -16,7 +15,7 @@ public enum MeAPIs: API { case joinFamily case appVersion - public var spec: APISpec { + var spec: APISpec { switch self { case .saveFcmToken: return APISpec(method: .post, url: "\(BibbiAPI.hostApi)/me/fcm") diff --git a/14th-team5-iOS/Data/Sources/Camera/CameraAPI/CameraAPIs.swift b/14th-team5-iOS/Data/Sources/Camera/CameraAPI/CameraAPIs.swift index 1d3793876..2f1336f70 100644 --- a/14th-team5-iOS/Data/Sources/Camera/CameraAPI/CameraAPIs.swift +++ b/14th-team5-iOS/Data/Sources/Camera/CameraAPI/CameraAPIs.swift @@ -6,7 +6,7 @@ // import Foundation -import Core + import Domain public enum CameraAPIs: API { @@ -21,7 +21,7 @@ public enum CameraAPIs: API { case modifyRealEmojiImage(String, String) case fetchMissionToday - public var spec: APISpec { + var spec: APISpec { switch self { case .uploadProfileImageURL: return APISpec(method: .post, url: "\(BibbiAPI.hostApi)/members/image-upload-request") diff --git a/14th-team5-iOS/Data/Sources/Emoji/EmojiAPI/EmojiAPIs.swift b/14th-team5-iOS/Data/Sources/Emoji/EmojiAPI/EmojiAPIs.swift index ca8708178..8d8ac6a20 100644 --- a/14th-team5-iOS/Data/Sources/Emoji/EmojiAPI/EmojiAPIs.swift +++ b/14th-team5-iOS/Data/Sources/Emoji/EmojiAPI/EmojiAPIs.swift @@ -5,7 +5,6 @@ // Created by 마경미 on 01.01.24. // -import Core import Foundation public enum EmojiAPIs: API { @@ -13,7 +12,7 @@ public enum EmojiAPIs: API { case removeReactions(String) case fetchReactions(FetchEmojiRequestDTO) - public var spec: APISpec { + var spec: APISpec { switch self { case let .addReactions(postId): let urlString = "\(BibbiAPI.hostApi)/posts/\(postId)/reactions" diff --git a/14th-team5-iOS/Data/Sources/Mission/MissionAPI/MissionAPIs.swift b/14th-team5-iOS/Data/Sources/Mission/MissionAPI/MissionAPIs.swift index 0078b43e3..c6f952381 100644 --- a/14th-team5-iOS/Data/Sources/Mission/MissionAPI/MissionAPIs.swift +++ b/14th-team5-iOS/Data/Sources/Mission/MissionAPI/MissionAPIs.swift @@ -14,7 +14,7 @@ public enum MissionAPIs: API { case getTodayMission case getMissionContent(String) - public var spec: APISpec { + var spec: APISpec { switch self { case .getTodayMission: return APISpec(method: .get, url: "\(BibbiAPI.hostApi)/missions/today") diff --git a/14th-team5-iOS/Data/Sources/Post/PostList/PostListAPI/PostListAPIs.swift b/14th-team5-iOS/Data/Sources/Post/PostList/PostListAPI/PostListAPIs.swift index 6ddb7e3c8..01edad2e1 100644 --- a/14th-team5-iOS/Data/Sources/Post/PostList/PostListAPI/PostListAPIs.swift +++ b/14th-team5-iOS/Data/Sources/Post/PostList/PostListAPI/PostListAPIs.swift @@ -12,7 +12,7 @@ public enum PostListAPIs: API { case fetchPostList case fetchPostDetail(PostRequestDTO) - public var spec: APISpec { + var spec: APISpec { switch self { case .fetchPostList: let urlString = "\(BibbiAPI.hostApi)/posts" diff --git a/14th-team5-iOS/Data/Sources/Privacy/PrivacyAPI/PrivacyAPIWorker.swift b/14th-team5-iOS/Data/Sources/Privacy/PrivacyAPI/PrivacyAPIWorker.swift index 3c63c77d4..d938f1266 100644 --- a/14th-team5-iOS/Data/Sources/Privacy/PrivacyAPI/PrivacyAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Privacy/PrivacyAPI/PrivacyAPIWorker.swift @@ -6,7 +6,7 @@ // import Foundation -import Core + import Alamofire import Domain import RxSwift diff --git a/14th-team5-iOS/Data/Sources/Privacy/PrivacyAPI/PrivacyAPIs.swift b/14th-team5-iOS/Data/Sources/Privacy/PrivacyAPI/PrivacyAPIs.swift index 7a2e291e9..760a39440 100644 --- a/14th-team5-iOS/Data/Sources/Privacy/PrivacyAPI/PrivacyAPIs.swift +++ b/14th-team5-iOS/Data/Sources/Privacy/PrivacyAPI/PrivacyAPIs.swift @@ -6,14 +6,14 @@ // import Foundation -import Core + public enum PrivacyAPIs: API { case bibbiAppInfo case accountFamilyResign - public var spec: APISpec { + var spec: APISpec { switch self { case .bibbiAppInfo: return APISpec(method: .get, url: "\(BibbiAPI.hostApi)/me/app-version") diff --git a/14th-team5-iOS/Data/Sources/Profile/ProfileAPI/ProfileAPIWorker.swift b/14th-team5-iOS/Data/Sources/Profile/ProfileAPI/ProfileAPIWorker.swift index 7b00edd89..25d5a2f93 100644 --- a/14th-team5-iOS/Data/Sources/Profile/ProfileAPI/ProfileAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Profile/ProfileAPI/ProfileAPIWorker.swift @@ -6,7 +6,7 @@ // import Foundation -import Core + import Alamofire import Domain import RxSwift diff --git a/14th-team5-iOS/Data/Sources/RealEmoji/RealEmojiAPI/RealEmojiAPIS.swift b/14th-team5-iOS/Data/Sources/RealEmoji/RealEmojiAPI/RealEmojiAPIS.swift index 3f9253b7a..7213433a9 100644 --- a/14th-team5-iOS/Data/Sources/RealEmoji/RealEmojiAPI/RealEmojiAPIS.swift +++ b/14th-team5-iOS/Data/Sources/RealEmoji/RealEmojiAPI/RealEmojiAPIS.swift @@ -14,7 +14,7 @@ public enum RealEmojiAPIS: API { case addRealEmoji(AddRealEmojiParameters) case removeRealEmoji(RemoveRealEmojiParameters) - public var spec: APISpec { + var spec: APISpec { switch self { case .addRealEmoji(let parameter): let urlString = "\(BibbiAPI.hostApi)/posts/\(parameter.postId)/real-emoji" diff --git a/14th-team5-iOS/Data/Sources/Resign/ResignAPI/ResignAPIWorker.swift b/14th-team5-iOS/Data/Sources/Resign/ResignAPI/ResignAPIWorker.swift index 1a0a5540d..fcf1e73bc 100644 --- a/14th-team5-iOS/Data/Sources/Resign/ResignAPI/ResignAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Resign/ResignAPI/ResignAPIWorker.swift @@ -6,7 +6,7 @@ // import Foundation -import Core + import Alamofire import Domain import RxSwift diff --git a/14th-team5-iOS/Data/Sources/Resign/ResignAPI/ResignAPIs.swift b/14th-team5-iOS/Data/Sources/Resign/ResignAPI/ResignAPIs.swift index cbf8f9821..cef43f6e8 100644 --- a/14th-team5-iOS/Data/Sources/Resign/ResignAPI/ResignAPIs.swift +++ b/14th-team5-iOS/Data/Sources/Resign/ResignAPI/ResignAPIs.swift @@ -5,7 +5,6 @@ // Created by Kim dohyun on 1/2/24. // -import Core import Foundation @@ -13,7 +12,7 @@ public enum ResignAPIs: API { case accountResign(String) case accountFcmResign(String) - public var spec: APISpec { + var spec: APISpec { switch self { case let .accountResign(memeberId): return APISpec(method: .delete, url: "\(BibbiAPI.hostApi)/members/\(memeberId)") diff --git a/14th-team5-iOS/Data/Sources/Account/Service/AccountSignInHelper.swift b/14th-team5-iOS/Data/Sources/Service/AccountSignInHelper.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Account/Service/AccountSignInHelper.swift rename to 14th-team5-iOS/Data/Sources/Service/AccountSignInHelper.swift diff --git a/14th-team5-iOS/Data/Sources/Account/Service/AccountSignInHelperConfig.swift b/14th-team5-iOS/Data/Sources/Service/AccountSignInHelperConfig.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Account/Service/AccountSignInHelperConfig.swift rename to 14th-team5-iOS/Data/Sources/Service/AccountSignInHelperConfig.swift diff --git a/14th-team5-iOS/Data/Sources/Account/Service/AccountSignInHelperType.swift b/14th-team5-iOS/Data/Sources/Service/AccountSignInHelperType.swift similarity index 96% rename from 14th-team5-iOS/Data/Sources/Account/Service/AccountSignInHelperType.swift rename to 14th-team5-iOS/Data/Sources/Service/AccountSignInHelperType.swift index b6c90c768..ec43a22ff 100644 --- a/14th-team5-iOS/Data/Sources/Account/Service/AccountSignInHelperType.swift +++ b/14th-team5-iOS/Data/Sources/Service/AccountSignInHelperType.swift @@ -7,7 +7,6 @@ import UIKit import Domain -import Core import RxSwift diff --git a/14th-team5-iOS/Data/Sources/Account/Service/Apple/AppleService+Rx.swift b/14th-team5-iOS/Data/Sources/Service/Apple/AppleService+Rx.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Account/Service/Apple/AppleService+Rx.swift rename to 14th-team5-iOS/Data/Sources/Service/Apple/AppleService+Rx.swift diff --git a/14th-team5-iOS/Data/Sources/Account/Service/Apple/AppleSignInHelper.swift b/14th-team5-iOS/Data/Sources/Service/Apple/AppleSignInHelper.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Account/Service/Apple/AppleSignInHelper.swift rename to 14th-team5-iOS/Data/Sources/Service/Apple/AppleSignInHelper.swift diff --git a/14th-team5-iOS/Data/Sources/Account/Service/Kakao/KakaoService+Rx.swift b/14th-team5-iOS/Data/Sources/Service/Kakao/KakaoService+Rx.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Account/Service/Kakao/KakaoService+Rx.swift rename to 14th-team5-iOS/Data/Sources/Service/Kakao/KakaoService+Rx.swift diff --git a/14th-team5-iOS/Data/Sources/Account/Service/Kakao/KakaoSignInHelper.swift b/14th-team5-iOS/Data/Sources/Service/Kakao/KakaoSignInHelper.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Account/Service/Kakao/KakaoSignInHelper.swift rename to 14th-team5-iOS/Data/Sources/Service/Kakao/KakaoSignInHelper.swift diff --git a/14th-team5-iOS/Data/Sources/View/Main/Worker/MainAPIs.swift b/14th-team5-iOS/Data/Sources/View/Main/Worker/MainAPIs.swift index 009cbc3fa..6d7a0b83a 100644 --- a/14th-team5-iOS/Data/Sources/View/Main/Worker/MainAPIs.swift +++ b/14th-team5-iOS/Data/Sources/View/Main/Worker/MainAPIs.swift @@ -13,7 +13,7 @@ public enum MainAPIs: API { case fetchMain case fetchMainNight - public var spec: APISpec { + var spec: APISpec { switch self { case .fetchMain: let urlString = "\(BibbiAPI.hostApi)/view/main/daytime-page" From c95a884ea674359781599a6ad8cea687d95355ab Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Mon, 3 Jun 2024 12:45:28 +0900 Subject: [PATCH 068/263] =?UTF-8?q?refactor:=20Network=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=BD=94=EB=93=9C=20BBNetwork=EB=A1=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99=20(#473)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/BBNetwork}/API/API.swift | 30 ++++++++-------- .../Sources/BBNetwork/API/APIConfig.swift | 20 +++++++++++ .../Sources/BBNetwork}/API/APISpec.swift | 26 +++++++------- .../Sources/BBNetwork/API/APIWorker.swift | 1 + .../Sources/BBNetwork}/API/Codable+Ext.swift | 36 +++++++++---------- .../Sources/BBNetwork}/API/VoidResponse.swift | 6 ++-- .../Analytics/Mixpanel/MixpanelService.swift | 26 +++++++------- .../Data/Sources/API/APIConfig.swift | 20 ----------- .../Sources/{API => APIs}/APIWorker.swift | 11 +++--- .../Calendar/CalendarAPI/CalendarAPIs.swift | 3 +- .../APIs/Comment/CommentAPI/CommentAPIs.swift | 3 +- .../APIs/Family/FamilyAPI/FamilyAPIs.swift | 2 +- .../Sources/APIs/Pick/PickAPI/PickAPIs.swift | 1 + .../Account/AccountAPI/AccountAPIs.swift | 3 +- .../Data/Sources/Account/MeAPI/MeAPIs.swift | 4 +-- .../Sources/Camera/CameraAPI/CameraAPIs.swift | 5 +-- .../Sources/Emoji/EmojiAPI/EmojiAPIs.swift | 3 +- .../Mission/MissionAPI/MissionAPIs.swift | 2 +- .../PostList/PostListAPI/PostListAPIs.swift | 4 +-- .../Privacy/PrivacyAPI/PrivacyAPIWorker.swift | 1 + .../Privacy/PrivacyAPI/PrivacyAPIs.swift | 3 +- .../Profile/ProfileAPI/ProfileAPIWorker.swift | 1 + .../RealEmojiAPI/RealEmojiAPIS.swift | 4 +-- .../Resign/ResignAPI/ResignAPIWorker.swift | 1 + .../Sources/Resign/ResignAPI/ResignAPIs.swift | 3 +- .../Service/AccountSignInHelperType.swift | 3 +- .../Sources/View/Main/Worker/MainAPIs.swift | 5 ++- 27 files changed, 118 insertions(+), 109 deletions(-) rename 14th-team5-iOS/{Data/Sources => Core/Sources/BBNetwork}/API/API.swift (77%) create mode 100644 14th-team5-iOS/Core/Sources/BBNetwork/API/APIConfig.swift rename 14th-team5-iOS/{Data/Sources => Core/Sources/BBNetwork}/API/APISpec.swift (65%) create mode 100644 14th-team5-iOS/Core/Sources/BBNetwork/API/APIWorker.swift rename 14th-team5-iOS/{Data/Sources => Core/Sources/BBNetwork}/API/Codable+Ext.swift (83%) rename 14th-team5-iOS/{Data/Sources => Core/Sources/BBNetwork}/API/VoidResponse.swift (55%) delete mode 100644 14th-team5-iOS/Data/Sources/API/APIConfig.swift rename 14th-team5-iOS/Data/Sources/{API => APIs}/APIWorker.swift (99%) diff --git a/14th-team5-iOS/Data/Sources/API/API.swift b/14th-team5-iOS/Core/Sources/BBNetwork/API/API.swift similarity index 77% rename from 14th-team5-iOS/Data/Sources/API/API.swift rename to 14th-team5-iOS/Core/Sources/BBNetwork/API/API.swift index c83694cf0..f3ce60d17 100644 --- a/14th-team5-iOS/Data/Sources/API/API.swift +++ b/14th-team5-iOS/Core/Sources/BBNetwork/API/API.swift @@ -9,19 +9,19 @@ import Foundation import Core // MARK: APIs of AFP -typealias BibbiHeader = BibbiAPI.Header -typealias BibbiResponse = BibbiAPI.Response -typealias BibbiNoResponse = BibbiAPI.NoResponse -typealias BibbiBoolResponse = BibbiAPI.BoolResponse -typealias BibbiCodableResponse = BibbiAPI.CodableResponse +public typealias BibbiHeader = BibbiAPI.Header +public typealias BibbiResponse = BibbiAPI.Response +public typealias BibbiNoResponse = BibbiAPI.NoResponse +public typealias BibbiBoolResponse = BibbiAPI.BoolResponse +public typealias BibbiCodableResponse = BibbiAPI.CodableResponse -enum BibbiAPI { +public enum BibbiAPI { private static let _config: BibbiAPIConfigType = BibbiAPIConfig() - static let hostApi: String = _config.hostApi + public static let hostApi: String = _config.hostApi // MARK: Common Headers - enum Header: APIHeader { + public enum Header: APIHeader { case auth(String) case xAppKey case xAuthToken(String) @@ -33,7 +33,7 @@ enum BibbiAPI { case xAppVersion case xUserID - var key: String { + public var key: String { switch self { case .auth: return "Authorization" case .xAppKey: return "X-APP-KEY" @@ -48,7 +48,7 @@ enum BibbiAPI { } } - var value: String { + public var value: String { switch self { case .auth(let value): return "Bearer \(value)" case .xAppKey: return "7c5aaa36-570e-491f-b18a-26a1a0b72959" @@ -63,12 +63,12 @@ enum BibbiAPI { } } - static var baseHeaders: [Self] { + public static var baseHeaders: [Self] { return [.xAppKey, .xAppVersion, .xUserPlatform, .xUserID] } } - struct Response: Codable { + public struct Response: Codable { let status: String? let code: Int? let errorCode: String? @@ -77,20 +77,20 @@ enum BibbiAPI { } } - struct NoResponse: Codable { + public struct NoResponse: Codable { // var status: String? // var code: Int? // var errorCode: String? } - struct BoolResponse: Codable { + public struct BoolResponse: Codable { var status: String? var code: Int? var errorCode: String? var result: Bool? } - struct CodableResponse: Codable { + public struct CodableResponse: Codable { // var status: String? // var code: String? // var message: String? diff --git a/14th-team5-iOS/Core/Sources/BBNetwork/API/APIConfig.swift b/14th-team5-iOS/Core/Sources/BBNetwork/API/APIConfig.swift new file mode 100644 index 000000000..6cd6a0cc3 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/BBNetwork/API/APIConfig.swift @@ -0,0 +1,20 @@ +// +// APIConfig.swift +// Data +// +// Created by geonhui Yu on 12/17/23. +// + +import Foundation + +public protocol BibbiAPIConfigType { + var hostApi: String { get } +} + +public struct BibbiAPIConfig: BibbiAPIConfigType { + #if PRD + public var hostApi: String = "https://api.no5ing.kr/v1" + #else + public var hostApi: String = "https://dev.api.no5ing.kr/v1" + #endif +} diff --git a/14th-team5-iOS/Data/Sources/API/APISpec.swift b/14th-team5-iOS/Core/Sources/BBNetwork/API/APISpec.swift similarity index 65% rename from 14th-team5-iOS/Data/Sources/API/APISpec.swift rename to 14th-team5-iOS/Core/Sources/BBNetwork/API/APISpec.swift index b19d4db48..ae37c5b39 100644 --- a/14th-team5-iOS/Data/Sources/API/APISpec.swift +++ b/14th-team5-iOS/Core/Sources/BBNetwork/API/APISpec.swift @@ -10,30 +10,30 @@ import Alamofire import RxSwift // MARK: API Protocol -typealias BaseAPI = API -protocol API { +public typealias BaseAPI = API +public protocol API { var spec: APISpec { get } } // MARK: API Constants -enum APIConst { +public enum APIConst { static let boundary = UUID().uuidString } // MARK: API Header Protocol -protocol APIHeader { +public protocol APIHeader { var key: String { get } var value: String { get } } // MARK: API Prameter Protocol -protocol APIParameter { +public protocol APIParameter { var key: String { get } var value: Any? { get } } // MARK: API Media Prameter -struct APIMediaParameter { +public struct APIMediaParameter { let name: String let fileName: String let mimeType: String @@ -42,22 +42,22 @@ struct APIMediaParameter { } // MARK: API Result Type -typealias APIRes = APIResult -public enum APIResult { +public typealias APIRes = APIResult +public enum APIResult { static let ok: Int = 200 case success case failed } // MARK: API Error Protocol -protocol APIError: CustomNSError, Equatable {} +public protocol APIError: CustomNSError, Equatable {} // MARK: API Specification -struct APISpec { - let method: HTTPMethod - let url: String +public struct APISpec { + public let method: HTTPMethod + public let url: String - init(method: HTTPMethod, url: String) { + public init(method: HTTPMethod, url: String) { self.method = method self.url = url } diff --git a/14th-team5-iOS/Core/Sources/BBNetwork/API/APIWorker.swift b/14th-team5-iOS/Core/Sources/BBNetwork/API/APIWorker.swift new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/BBNetwork/API/APIWorker.swift @@ -0,0 +1 @@ + diff --git a/14th-team5-iOS/Data/Sources/API/Codable+Ext.swift b/14th-team5-iOS/Core/Sources/BBNetwork/API/Codable+Ext.swift similarity index 83% rename from 14th-team5-iOS/Data/Sources/API/Codable+Ext.swift rename to 14th-team5-iOS/Core/Sources/BBNetwork/API/Codable+Ext.swift index f6879933f..02de28b97 100644 --- a/14th-team5-iOS/Data/Sources/API/Codable+Ext.swift +++ b/14th-team5-iOS/Core/Sources/BBNetwork/API/Codable+Ext.swift @@ -12,27 +12,27 @@ import RxSwift // MARK: Data Decodable (Data to Decodable) public extension Data { - func decode(_ type: T.Type, using decoder: JSONDecoder? = nil) -> T? where T: Decodable { - - let decoder = decoder ?? JSONDecoder() - - var res: T? = nil - do { - res = try decoder.decode(type, from: self) - } catch { - debugPrint("\(T.self) Data Parsing Error: \(error)") - } - - return res - } +// func decode(_ type: T.Type, using decoder: JSONDecoder? = nil) -> T? where T: Decodable { +// +// let decoder = decoder ?? JSONDecoder() +// +// var res: T? = nil +// do { +// res = try decoder.decode(type, from: self) +// } catch { +// debugPrint("\(T.self) Data Parsing Error: \(error)") +// } +// +// return res +// } } // MARK: String Decodable (String to Decodable) public extension String { - func decode(_ type: T.Type, using decoder: JSONDecoder? = nil) -> T? where T: Decodable { - - return self.data(using: .utf8)?.decode(type, using: decoder) - } +// func decode(_ type: T.Type, using decoder: JSONDecoder? = nil) -> T? where T: Decodable { +// +// return self.data(using: .utf8)?.decode(type, using: decoder) +// } } // MARK: Dictionary([String : Any]) Decodable @@ -102,7 +102,7 @@ public extension PrimitiveSequenceType where Trait == SingleTrait, Element == (H } } -extension Encodable { +public extension Encodable { func asDictionary() -> [String: Any]? { do { diff --git a/14th-team5-iOS/Data/Sources/API/VoidResponse.swift b/14th-team5-iOS/Core/Sources/BBNetwork/API/VoidResponse.swift similarity index 55% rename from 14th-team5-iOS/Data/Sources/API/VoidResponse.swift rename to 14th-team5-iOS/Core/Sources/BBNetwork/API/VoidResponse.swift index 1be07ba4b..692535ee2 100644 --- a/14th-team5-iOS/Data/Sources/API/VoidResponse.swift +++ b/14th-team5-iOS/Core/Sources/BBNetwork/API/VoidResponse.swift @@ -7,10 +7,10 @@ import Foundation -struct VoidResponse: Codable { - let success: Bool +public struct VoidResponse: Codable { + public let success: Bool - func toDomain() -> Void { + public func toDomain() -> Void { return () } } diff --git a/14th-team5-iOS/Core/Sources/Utilties/Analytics/Mixpanel/MixpanelService.swift b/14th-team5-iOS/Core/Sources/Utilties/Analytics/Mixpanel/MixpanelService.swift index a10daca98..a28abb49c 100644 --- a/14th-team5-iOS/Core/Sources/Utilties/Analytics/Mixpanel/MixpanelService.swift +++ b/14th-team5-iOS/Core/Sources/Utilties/Analytics/Mixpanel/MixpanelService.swift @@ -62,19 +62,19 @@ extension MPEvent { } // MARK: Extensions -extension Encodable { - func asDictionary() -> [String: Any]? { - do { - let data = try JSONEncoder().encode(self) - guard let dict = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any] else { return nil } - - return dict - } catch let error { - print(error) - return nil - } - } -} +//extension Encodable { +// func asDictionary() -> [String: Any]? { +// do { +// let data = try JSONEncoder().encode(self) +// guard let dict = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any] else { return nil } +// +// return dict +// } catch let error { +// print(error) +// return nil +// } +// } +//} extension Encodable { func asMixpanelDictionary() -> [String: MixpanelType]? { diff --git a/14th-team5-iOS/Data/Sources/API/APIConfig.swift b/14th-team5-iOS/Data/Sources/API/APIConfig.swift deleted file mode 100644 index dae5df606..000000000 --- a/14th-team5-iOS/Data/Sources/API/APIConfig.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// APIConfig.swift -// Data -// -// Created by geonhui Yu on 12/17/23. -// - -import Foundation - -protocol BibbiAPIConfigType { - var hostApi: String { get } -} - -struct BibbiAPIConfig: BibbiAPIConfigType { - #if PRD - var hostApi: String = "https://api.no5ing.kr/v1" - #else - var hostApi: String = "https://dev.api.no5ing.kr/v1" - #endif -} diff --git a/14th-team5-iOS/Data/Sources/API/APIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/APIWorker.swift similarity index 99% rename from 14th-team5-iOS/Data/Sources/API/APIWorker.swift rename to 14th-team5-iOS/Data/Sources/APIs/APIWorker.swift index c60ba0de1..cc0feb34c 100644 --- a/14th-team5-iOS/Data/Sources/API/APIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/APIWorker.swift @@ -1,17 +1,17 @@ // -// BaseAPIWorker.swift +// APIWorker.swift // Data // -// Created by geonhui Yu on 12/17/23. +// Created by 김건우 on 6/3/24. // +import Core +import Domain import Foundation import Alamofire -import RxSwift import RxCocoa -import Core -import Domain +import RxSwift protocol BibbiRouterInterface { func httpHeaders(_ headers: [APIHeader]?) -> HTTPHeaders @@ -208,3 +208,4 @@ public class APIWorker: NSObject, BibbiRouterInterface { } } + diff --git a/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/CalendarAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/CalendarAPIs.swift index 5738556f5..3e2bb0cb4 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/CalendarAPIs.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/CalendarAPIs.swift @@ -5,10 +5,9 @@ // Created by 김건우 on 12/21/23. // +import Core import Foundation -import Domain - enum CalendarAPIs: API { @available(*, deprecated) case calendarResponse(String) 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 45a4947df..7d039f5d2 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/CommentAPIs.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/CommentAPIs.swift @@ -5,10 +5,9 @@ // Created by 김건우 on 1/17/24. // +import Core import Foundation -import Domain - enum CommentAPIs: API { case fetchPostComment(String, Int, Int, String) case createPostComment(String) diff --git a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIs.swift index 43cf54a25..f899a5f26 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIs.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIs.swift @@ -17,7 +17,7 @@ public enum FamilyAPIs: API { case fetchFamilyCreatedAt(String) case fetchPaginationFamilyMembers(Int, Int) - var spec: APISpec { + public var spec: APISpec { switch self { case .joinFamily: return APISpec(method: .post, url: "\(BibbiAPI.hostApi)/me/join-family") diff --git a/14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/PickAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/PickAPIs.swift index 81843b73d..e71844518 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/PickAPIs.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/PickAPIs.swift @@ -5,6 +5,7 @@ // Created by 김건우 on 4/15/24. // +import Core import Foundation enum PickAPIs: API { diff --git a/14th-team5-iOS/Data/Sources/Account/AccountAPI/AccountAPIs.swift b/14th-team5-iOS/Data/Sources/Account/AccountAPI/AccountAPIs.swift index 24aca728c..9f9b022ea 100644 --- a/14th-team5-iOS/Data/Sources/Account/AccountAPI/AccountAPIs.swift +++ b/14th-team5-iOS/Data/Sources/Account/AccountAPI/AccountAPIs.swift @@ -5,6 +5,7 @@ // Created by geonhui Yu on 12/18/23. // +import Core import Foundation import Domain @@ -15,7 +16,7 @@ enum AccountAPIs: API { case signIn(SNS) case profileNickNameEdit(String) - var spec: APISpec { + public var spec: APISpec { switch self { case .forceToken(let id): return APISpec(method: .post, url: "\(BibbiAPI.hostApi)/auth/force-token/\(id)") diff --git a/14th-team5-iOS/Data/Sources/Account/MeAPI/MeAPIs.swift b/14th-team5-iOS/Data/Sources/Account/MeAPI/MeAPIs.swift index de27ff107..29ac716e5 100644 --- a/14th-team5-iOS/Data/Sources/Account/MeAPI/MeAPIs.swift +++ b/14th-team5-iOS/Data/Sources/Account/MeAPI/MeAPIs.swift @@ -5,8 +5,8 @@ // Created by geonhui Yu on 1/3/24. // +import Core import Foundation -import Domain public enum MeAPIs: API { case saveFcmToken @@ -15,7 +15,7 @@ public enum MeAPIs: API { case joinFamily case appVersion - var spec: APISpec { + public var spec: APISpec { switch self { case .saveFcmToken: return APISpec(method: .post, url: "\(BibbiAPI.hostApi)/me/fcm") diff --git a/14th-team5-iOS/Data/Sources/Camera/CameraAPI/CameraAPIs.swift b/14th-team5-iOS/Data/Sources/Camera/CameraAPI/CameraAPIs.swift index 2f1336f70..484af9d77 100644 --- a/14th-team5-iOS/Data/Sources/Camera/CameraAPI/CameraAPIs.swift +++ b/14th-team5-iOS/Data/Sources/Camera/CameraAPI/CameraAPIs.swift @@ -5,11 +5,12 @@ // Created by Kim dohyun on 12/21/23. // +import Core +import Domain import Foundation -import Domain -public enum CameraAPIs: API { +enum CameraAPIs: API { case uploadImageURL case presignedURL(String) case updateImage(CameraMissionFeedQuery) diff --git a/14th-team5-iOS/Data/Sources/Emoji/EmojiAPI/EmojiAPIs.swift b/14th-team5-iOS/Data/Sources/Emoji/EmojiAPI/EmojiAPIs.swift index 8d8ac6a20..ca8708178 100644 --- a/14th-team5-iOS/Data/Sources/Emoji/EmojiAPI/EmojiAPIs.swift +++ b/14th-team5-iOS/Data/Sources/Emoji/EmojiAPI/EmojiAPIs.swift @@ -5,6 +5,7 @@ // Created by 마경미 on 01.01.24. // +import Core import Foundation public enum EmojiAPIs: API { @@ -12,7 +13,7 @@ public enum EmojiAPIs: API { case removeReactions(String) case fetchReactions(FetchEmojiRequestDTO) - var spec: APISpec { + public var spec: APISpec { switch self { case let .addReactions(postId): let urlString = "\(BibbiAPI.hostApi)/posts/\(postId)/reactions" diff --git a/14th-team5-iOS/Data/Sources/Mission/MissionAPI/MissionAPIs.swift b/14th-team5-iOS/Data/Sources/Mission/MissionAPI/MissionAPIs.swift index c6f952381..c290b7ba0 100644 --- a/14th-team5-iOS/Data/Sources/Mission/MissionAPI/MissionAPIs.swift +++ b/14th-team5-iOS/Data/Sources/Mission/MissionAPI/MissionAPIs.swift @@ -10,7 +10,7 @@ import Foundation import Core import Domain -public enum MissionAPIs: API { +enum MissionAPIs: API { case getTodayMission case getMissionContent(String) diff --git a/14th-team5-iOS/Data/Sources/Post/PostList/PostListAPI/PostListAPIs.swift b/14th-team5-iOS/Data/Sources/Post/PostList/PostListAPI/PostListAPIs.swift index 01edad2e1..b0c80c6fc 100644 --- a/14th-team5-iOS/Data/Sources/Post/PostList/PostListAPI/PostListAPIs.swift +++ b/14th-team5-iOS/Data/Sources/Post/PostList/PostListAPI/PostListAPIs.swift @@ -5,14 +5,14 @@ // Created by 마경미 on 25.12.23. // -import Foundation import Core +import Foundation public enum PostListAPIs: API { case fetchPostList case fetchPostDetail(PostRequestDTO) - var spec: APISpec { + public var spec: APISpec { switch self { case .fetchPostList: let urlString = "\(BibbiAPI.hostApi)/posts" diff --git a/14th-team5-iOS/Data/Sources/Privacy/PrivacyAPI/PrivacyAPIWorker.swift b/14th-team5-iOS/Data/Sources/Privacy/PrivacyAPI/PrivacyAPIWorker.swift index d938f1266..44c5d0974 100644 --- a/14th-team5-iOS/Data/Sources/Privacy/PrivacyAPI/PrivacyAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Privacy/PrivacyAPI/PrivacyAPIWorker.swift @@ -5,6 +5,7 @@ // Created by Kim dohyun on 1/1/24. // +import Core import Foundation import Alamofire diff --git a/14th-team5-iOS/Data/Sources/Privacy/PrivacyAPI/PrivacyAPIs.swift b/14th-team5-iOS/Data/Sources/Privacy/PrivacyAPI/PrivacyAPIs.swift index 760a39440..92fb5906c 100644 --- a/14th-team5-iOS/Data/Sources/Privacy/PrivacyAPI/PrivacyAPIs.swift +++ b/14th-team5-iOS/Data/Sources/Privacy/PrivacyAPI/PrivacyAPIs.swift @@ -5,11 +5,12 @@ // Created by Kim dohyun on 1/1/24. // +import Core import Foundation -public enum PrivacyAPIs: API { +enum PrivacyAPIs: API { case bibbiAppInfo case accountFamilyResign diff --git a/14th-team5-iOS/Data/Sources/Profile/ProfileAPI/ProfileAPIWorker.swift b/14th-team5-iOS/Data/Sources/Profile/ProfileAPI/ProfileAPIWorker.swift index 25d5a2f93..9f73eb882 100644 --- a/14th-team5-iOS/Data/Sources/Profile/ProfileAPI/ProfileAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Profile/ProfileAPI/ProfileAPIWorker.swift @@ -5,6 +5,7 @@ // Created by Kim dohyun on 12/25/23. // +import Core import Foundation import Alamofire diff --git a/14th-team5-iOS/Data/Sources/RealEmoji/RealEmojiAPI/RealEmojiAPIS.swift b/14th-team5-iOS/Data/Sources/RealEmoji/RealEmojiAPI/RealEmojiAPIS.swift index 7213433a9..5373e4472 100644 --- a/14th-team5-iOS/Data/Sources/RealEmoji/RealEmojiAPI/RealEmojiAPIS.swift +++ b/14th-team5-iOS/Data/Sources/RealEmoji/RealEmojiAPI/RealEmojiAPIS.swift @@ -5,8 +5,8 @@ // Created by 마경미 on 22.01.24. // -import Foundation import Core +import Foundation public enum RealEmojiAPIS: API { case fetchRealEmojiList(FetchRealEmojiListParameter) @@ -14,7 +14,7 @@ public enum RealEmojiAPIS: API { case addRealEmoji(AddRealEmojiParameters) case removeRealEmoji(RemoveRealEmojiParameters) - var spec: APISpec { + public var spec: APISpec { switch self { case .addRealEmoji(let parameter): let urlString = "\(BibbiAPI.hostApi)/posts/\(parameter.postId)/real-emoji" diff --git a/14th-team5-iOS/Data/Sources/Resign/ResignAPI/ResignAPIWorker.swift b/14th-team5-iOS/Data/Sources/Resign/ResignAPI/ResignAPIWorker.swift index fcf1e73bc..8999d1166 100644 --- a/14th-team5-iOS/Data/Sources/Resign/ResignAPI/ResignAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Resign/ResignAPI/ResignAPIWorker.swift @@ -5,6 +5,7 @@ // Created by Kim dohyun on 1/2/24. // +import Core import Foundation import Alamofire diff --git a/14th-team5-iOS/Data/Sources/Resign/ResignAPI/ResignAPIs.swift b/14th-team5-iOS/Data/Sources/Resign/ResignAPI/ResignAPIs.swift index cef43f6e8..ba57e42ba 100644 --- a/14th-team5-iOS/Data/Sources/Resign/ResignAPI/ResignAPIs.swift +++ b/14th-team5-iOS/Data/Sources/Resign/ResignAPI/ResignAPIs.swift @@ -5,10 +5,11 @@ // Created by Kim dohyun on 1/2/24. // +import Core import Foundation -public enum ResignAPIs: API { +enum ResignAPIs: API { case accountResign(String) case accountFcmResign(String) diff --git a/14th-team5-iOS/Data/Sources/Service/AccountSignInHelperType.swift b/14th-team5-iOS/Data/Sources/Service/AccountSignInHelperType.swift index ec43a22ff..5ca9c341c 100644 --- a/14th-team5-iOS/Data/Sources/Service/AccountSignInHelperType.swift +++ b/14th-team5-iOS/Data/Sources/Service/AccountSignInHelperType.swift @@ -5,8 +5,9 @@ // Created by geonhui Yu on 12/6/23. // -import UIKit +import Core import Domain +import UIKit import RxSwift diff --git a/14th-team5-iOS/Data/Sources/View/Main/Worker/MainAPIs.swift b/14th-team5-iOS/Data/Sources/View/Main/Worker/MainAPIs.swift index 6d7a0b83a..6dd7974f3 100644 --- a/14th-team5-iOS/Data/Sources/View/Main/Worker/MainAPIs.swift +++ b/14th-team5-iOS/Data/Sources/View/Main/Worker/MainAPIs.swift @@ -5,11 +5,10 @@ // Created by 마경미 on 20.04.24. // -import Foundation - import Core +import Foundation -public enum MainAPIs: API { +enum MainAPIs: API { case fetchMain case fetchMainNight From 4e8f843ae0f237651623bf0e2a11620b6f81bb32 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Mon, 3 Jun 2024 14:03:13 +0900 Subject: [PATCH 069/263] =?UTF-8?q?refactor:=20APIWorker,=20Intercepter=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20(#4?= =?UTF-8?q?73)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Core/Sources/BBNetwork/API/API.swift | 11 + .../Sources/BBNetwork/API/APIWorker.swift | 1 - .../Core/Sources/BBNetwork/API/File.swift | 8 + .../Repositories/Token/TokenRepository.swift | 33 +- .../API => Extensions}/Codable+Ext.swift | 38 +-- .../Sources/Extensions/URLRequeset+Ext.swift | 35 +++ .../Sources/Extensions/UserDefaults+Ext.swift | 2 + .../Data/Sources/APIs/APIWorker.swift | 289 ++++++++++-------- .../Comment/CommentAPI/CommentAPIWorker.swift | 5 +- .../Family/FamilyAPI/FamilyAPIWorker.swift | 27 ++ .../APIs/Family/FamilyAPI/FamilyAPIs.swift | 5 +- .../Sources/APIs/NetworkIntercepter.swift | 110 +++++++ .../Account/AccountAPI}/VoidResponse.swift | 2 +- 13 files changed, 375 insertions(+), 191 deletions(-) delete mode 100644 14th-team5-iOS/Core/Sources/BBNetwork/API/APIWorker.swift create mode 100644 14th-team5-iOS/Core/Sources/BBNetwork/API/File.swift rename 14th-team5-iOS/Core/Sources/{BBNetwork/API => Extensions}/Codable+Ext.swift (83%) create mode 100644 14th-team5-iOS/Core/Sources/Extensions/URLRequeset+Ext.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/NetworkIntercepter.swift rename 14th-team5-iOS/{Core/Sources/BBNetwork/API => Data/Sources/Account/AccountAPI}/VoidResponse.swift (83%) diff --git a/14th-team5-iOS/Core/Sources/BBNetwork/API/API.swift b/14th-team5-iOS/Core/Sources/BBNetwork/API/API.swift index f3ce60d17..25e6ae46e 100644 --- a/14th-team5-iOS/Core/Sources/BBNetwork/API/API.swift +++ b/14th-team5-iOS/Core/Sources/BBNetwork/API/API.swift @@ -63,6 +63,17 @@ public enum BibbiAPI { } } + public static func commonHeaders(_ accessToken: String) -> [Self] { + return [ + .xAppKey, + .xAppVersion, + .xUserPlatform, + .xUserID, + .xAuthToken(accessToken) + ] + } + + @available(*, deprecated, renamed: "commonHeaders(_:)") public static var baseHeaders: [Self] { return [.xAppKey, .xAppVersion, .xUserPlatform, .xUserID] } diff --git a/14th-team5-iOS/Core/Sources/BBNetwork/API/APIWorker.swift b/14th-team5-iOS/Core/Sources/BBNetwork/API/APIWorker.swift deleted file mode 100644 index 8b1378917..000000000 --- a/14th-team5-iOS/Core/Sources/BBNetwork/API/APIWorker.swift +++ /dev/null @@ -1 +0,0 @@ - diff --git a/14th-team5-iOS/Core/Sources/BBNetwork/API/File.swift b/14th-team5-iOS/Core/Sources/BBNetwork/API/File.swift new file mode 100644 index 000000000..d449a83e9 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/BBNetwork/API/File.swift @@ -0,0 +1,8 @@ +// +// File.swift +// Core +// +// Created by 김건우 on 6/3/24. +// + +import Foundation diff --git a/14th-team5-iOS/Core/Sources/BBRx/Repositories/Token/TokenRepository.swift b/14th-team5-iOS/Core/Sources/BBRx/Repositories/Token/TokenRepository.swift index 43ed0f03d..41116db69 100644 --- a/14th-team5-iOS/Core/Sources/BBRx/Repositories/Token/TokenRepository.swift +++ b/14th-team5-iOS/Core/Sources/BBRx/Repositories/Token/TokenRepository.swift @@ -7,15 +7,8 @@ import Foundation -import RxSwift import RxCocoa -//import SwiftKeychainWrapper - -//public extension KeychainWrapper.Key { -// static let fcmToken: KeychainWrapper.Key = "FCMToken" -// static let accessToken: KeychainWrapper.Key = "accessToken" -// static let refreshToken: KeychainWrapper.Key = "refreshToken" -//} +import RxSwift public struct AccessToken: Codable, Equatable { public var accessToken: String? @@ -29,30 +22,6 @@ public struct AccessToken: Codable, Equatable { } } -public extension Data { - - func decode(_ type: T.Type, using decoder: JSONDecoder? = nil) -> T? where T: Decodable { - - let decoder = decoder ?? JSONDecoder() - - var res: T? = nil - do { - res = try decoder.decode(type, from: self) - } catch { - debugPrint("\(T.self) Data Parsing Error: \(error)") - } - - return res - } -} - -public extension String { - - func decode(_ type: T.Type, using decoder: JSONDecoder? = nil) -> T? where T: Decodable { - return self.data(using: .utf8)?.decode(type, using: decoder) - } -} - public class TokenRepository: RxObject { public lazy var keychain = KeychainWrapper(serviceName: "Bibbi", accessGroup: "P9P4WJ623F.com.5ing.bibbi") diff --git a/14th-team5-iOS/Core/Sources/BBNetwork/API/Codable+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/Codable+Ext.swift similarity index 83% rename from 14th-team5-iOS/Core/Sources/BBNetwork/API/Codable+Ext.swift rename to 14th-team5-iOS/Core/Sources/Extensions/Codable+Ext.swift index 02de28b97..b4d35a703 100644 --- a/14th-team5-iOS/Core/Sources/BBNetwork/API/Codable+Ext.swift +++ b/14th-team5-iOS/Core/Sources/Extensions/Codable+Ext.swift @@ -10,29 +10,33 @@ import Foundation import Alamofire import RxSwift +// TODO: - Extension을 파일 별로 쪼개기 + + + // MARK: Data Decodable (Data to Decodable) public extension Data { -// func decode(_ type: T.Type, using decoder: JSONDecoder? = nil) -> T? where T: Decodable { -// -// let decoder = decoder ?? JSONDecoder() -// -// var res: T? = nil -// do { -// res = try decoder.decode(type, from: self) -// } catch { -// debugPrint("\(T.self) Data Parsing Error: \(error)") -// } -// -// return res -// } + func decode(_ type: T.Type, using decoder: JSONDecoder? = nil) -> T? where T: Decodable { + + let decoder = decoder ?? JSONDecoder() + + var res: T? = nil + do { + res = try decoder.decode(type, from: self) + } catch { + debugPrint("\(T.self) Data Parsing Error: \(error)") + } + + return res + } } // MARK: String Decodable (String to Decodable) public extension String { -// func decode(_ type: T.Type, using decoder: JSONDecoder? = nil) -> T? where T: Decodable { -// -// return self.data(using: .utf8)?.decode(type, using: decoder) -// } + func decode(_ type: T.Type, using decoder: JSONDecoder? = nil) -> T? where T: Decodable { + + return self.data(using: .utf8)?.decode(type, using: decoder) + } } // MARK: Dictionary([String : Any]) Decodable diff --git a/14th-team5-iOS/Core/Sources/Extensions/URLRequeset+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/URLRequeset+Ext.swift new file mode 100644 index 000000000..e4781a325 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Extensions/URLRequeset+Ext.swift @@ -0,0 +1,35 @@ +// +// URLRequest.swift +// Core +// +// Created by 김건우 on 6/3/24. +// + +import Foundation + +import Alamofire + + +public extension URLRequest { + + mutating func setHeaders(_ headers: [HTTPHeader]) { + headers.forEach { header in + self.setValue(header.value, forHTTPHeaderField: header.name) + } + } + + mutating func setHeaders(_ headers: [BibbiHeader]) { + let headers = headers.map { header in + HTTPHeader(name: header.key, value: header.value) + } + setHeaders(headers) + } + + mutating func setHeaders(_ headers: [String: String]) { + let headers = headers.map { key, value in + HTTPHeader(name: key, value: value) + } + setHeaders(headers) + } + +} diff --git a/14th-team5-iOS/Core/Sources/Extensions/UserDefaults+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/UserDefaults+Ext.swift index 991d17b63..ff44bc05a 100644 --- a/14th-team5-iOS/Core/Sources/Extensions/UserDefaults+Ext.swift +++ b/14th-team5-iOS/Core/Sources/Extensions/UserDefaults+Ext.swift @@ -8,6 +8,8 @@ import Foundation extension UserDefaults { + + @available(*, deprecated, message: "UserDefaultsWrpper를 사용하세요.") public enum Key: String, CaseIterable { case isFirstLaunch case chekcPermission diff --git a/14th-team5-iOS/Data/Sources/APIs/APIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/APIWorker.swift index cc0feb34c..145f366e9 100644 --- a/14th-team5-iOS/Data/Sources/APIs/APIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/APIWorker.swift @@ -6,139 +6,79 @@ // import Core -import Domain import Foundation import Alamofire +import RxAlamofire import RxCocoa import RxSwift -protocol BibbiRouterInterface { - func httpHeaders(_ headers: [APIHeader]?) -> HTTPHeaders -} -extension BibbiRouterInterface { - public func httpHeaders(_ headers: [APIHeader]?) -> HTTPHeaders { - var result: [String: String] = [:] - guard let headers = headers, !headers.isEmpty else { return HTTPHeaders() } - - for header in headers { - result[header.key] = header.value - } - - return HTTPHeaders(result) - } -} +// MARK: - Base API Worker -public final class BibbiRequestInterceptor: RequestInterceptor, BibbiRouterInterface { +public class APIWorker: NSObject { - //TODO: Test용 KeychainWrapper 코드 다 제거하기 - private let accountAPIWorker: AccountAPIWorker = AccountAPIWorker() - private let disposeBag: DisposeBag = DisposeBag() + // MARK: - Identifier - var retryCount: Int = 0 - var retryLimit: Int = 3 + var id: String = "APIWorker" - public func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result) -> Void) { - var urlRequest = urlRequest - - guard urlRequest.url?.absoluteString.hasPrefix(BibbiAPI.hostApi) == true else { - completion(.success(urlRequest)) - return - } - guard let accessToken = App.Repository.token.accessToken.value?.accessToken else { - completion(.success(urlRequest)) - return - } - - urlRequest.setValue(accessToken, forHTTPHeaderField: "X-AUTH-TOKEN") - completion(.success(urlRequest)) - } - public func retry(_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void) { - - guard let response = request.task?.response as? HTTPURLResponse, response.statusCode == 401 else { - completion(.doNotRetryWithError(error)) - return - } - - if retryCount < retryLimit { - retryCount += 1 - let parameter = AccountRefreshParameter(refreshToken: App.Repository.token.accessToken.value?.refreshToken ?? "") - - accountAPIWorker.accountRefreshToken(parameter: parameter) - .compactMap { $0?.toDomain() } - .asObservable() - .subscribe(onNext: { [weak self] entity in - let refreshToken = AccessToken(accessToken: entity.accessToken, refreshToken: entity.refreshToken, isTemporaryToken: entity.isTemporaryToken) - App.Repository.token.accessToken.accept(refreshToken) - self?.retryCount = 0 - completion(.retry) - }, onError: { error in - completion(.doNotRetryWithError(error)) - }) - .disposed(by: disposeBag) - } - } -} - -// MARK: API Worker -public class APIWorker: NSObject, BibbiRouterInterface { - // MARK: - Headers - var _headers: Observable<[APIHeader]?> { - return App.Repository.token.accessToken - .map { - guard let token = $0, let accessToken = token.accessToken, !accessToken.isEmpty else { return [] } - return [BibbiAPI.Header.xAppKey, BibbiAPI.Header.xAuthToken(accessToken)] - } - } - - private func appendCommonHeaders(to headers: [APIHeader]?) -> [APIHeader] { - var result: [APIHeader] = BibbiAPI.Header.baseHeaders - guard let headers = headers else { return result } - - result.append(contentsOf: headers) - - return result - } + // MARK: - Request - private func parameters(_ parameters: [APIParameter]?) -> Parameters? { - guard let kvs = parameters else { return nil } - var result: [String: Any] = [:] - - for kv in kvs { - result[kv.key] = kv.value - } - - return result.isEmpty ? nil: result + func request( + spec: APISpec, + headers: [APIHeader]? = nil, + parameters: [APIParameter]? = nil, + encoding: ParameterEncoding? = URLEncoding.default + ) -> Observable<(HTTPURLResponse, Data)> { + + let headers = self.httpHeaders(headers) + let parameters = self.parameters(parameters) + + return AF.rx.request( + spec.method, + spec.url, + parameters: parameters, + encoding: encoding!, + headers: headers, + interceptor: NetworkInterceptor() + ) + .validate(statusCode: 200..<300) + .responseData() + .debug("API Worker has received data from \"\(spec.url)\"") } - // MARK: Identifier - var id: String = "APIWorker" - - // MARK: Request - func request(spec: APISpec, headers: [APIHeader]? = nil, parameters: [APIParameter]? = nil, encoding: ParameterEncoding? = URLEncoding.default) -> Observable<(HTTPURLResponse, Data)> { - let params = self.parameters(parameters) - let hds = self.httpHeaders(self.appendCommonHeaders(to: headers)) - return AF.rx.request(spec.method, spec.url, parameters: params, encoding: encoding!, headers: hds, interceptor: BibbiRequestInterceptor()) - .validate(statusCode: 200..<300) - .responseData() - .debug("API Worker has received data from \"\(spec.url)\"") + func request( + spec: APISpec, + headers: [APIHeader]? = nil, + parameters: Encodable, + encoding: ParameterEncoding? = URLEncoding.default + ) -> Observable<(HTTPURLResponse, Data)> { + + let headers = self.httpHeaders(headers) + let parameters = parameters.asDictionary() + + return AF.rx.request( + spec.method, + spec.url, + parameters: parameters, + encoding: encoding!, + headers: headers, + interceptor: NetworkInterceptor() + ) + .validate(statusCode: 200..<300) + .responseData() + .debug("API Worker has received data from \"\(spec.url)\"") } - func request(spec: APISpec, headers: [APIHeader]? = nil, parameters: Encodable, encoding: ParameterEncoding? = URLEncoding.default) -> Observable<(HTTPURLResponse, Data)> { - let params = parameters.asDictionary() - let hds = self.httpHeaders(self.appendCommonHeaders(to: headers)) + private func refreshRequest( + spec: APISpec, + headers: [APIHeader]? = nil, + jsonData: Data + ) -> Observable<(HTTPURLResponse, Data)> { - return AF.rx.request(spec.method, spec.url, parameters: params, encoding: encoding!, headers: hds, interceptor: BibbiRequestInterceptor()) - .validate(statusCode: 200..<300) - .responseData() - .debug("API Worker has received data from \"\(spec.url)\"") - } - - private func refreshRequest(spec: APISpec, headers: [APIHeader]? = nil, jsonData: Data) -> Observable<(HTTPURLResponse, Data)> { - let hds = self.httpHeaders(self.appendCommonHeaders(to: headers)) + let headers = self.httpHeaders(headers) guard let url = URL(string: spec.url) else { return Observable.error(AFError.explicitlyCancelled) } @@ -146,9 +86,10 @@ public class APIWorker: NSObject, BibbiRouterInterface { var request = URLRequest(url: url) request.httpMethod = spec.method.rawValue request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.headers = headers request.httpBody = jsonData - request.headers = hds print("interCepter call with name \(url)") + return AF.rx.request(urlRequest: request) .retry(5) .validate(statusCode: 200..<300) @@ -156,8 +97,13 @@ public class APIWorker: NSObject, BibbiRouterInterface { .debug("API Worker has received data from \"\(spec.url)\"") } - private func request(spec: APISpec, headers: [APIHeader]? = nil, jsonData: Data) -> Observable<(HTTPURLResponse, Data)> { - let hds = self.httpHeaders(self.appendCommonHeaders(to: headers)) + private func request( + spec: APISpec, + headers: [APIHeader]? = nil, + jsonData: Data + ) -> Observable<(HTTPURLResponse, Data)> { + + let headers = self.httpHeaders(headers) guard let url = URL(string: spec.url) else { return Observable.error(AFError.explicitlyCancelled) } @@ -165,16 +111,24 @@ public class APIWorker: NSObject, BibbiRouterInterface { var request = URLRequest(url: url) request.httpMethod = spec.method.rawValue request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.headers = headers request.httpBody = jsonData - request.headers = hds print("interCepter call with name \(url)") - return AF.rx.request(urlRequest: request, interceptor: BibbiRequestInterceptor()) - .validate(statusCode: 200..<300) - .responseData() - .debug("API Worker has received data from \"\(spec.url)\"") + + return AF.rx.request( + urlRequest: request, + interceptor: NetworkInterceptor() + ) + .validate(statusCode: 200..<300) + .responseData() + .debug("API Worker has received data from \"\(spec.url)\"") } - func request(spec: APISpec, headers: [APIHeader]? = nil, jsonEncodable: Encodable) -> Observable<(HTTPURLResponse, Data)> { + func request( + spec: APISpec, + headers: [APIHeader]? = nil, + jsonEncodable: Encodable + ) -> Observable<(HTTPURLResponse, Data)> { guard let jsonData = jsonEncodable.encodeData() else { return Observable.error(AFError.explicitlyCancelled) } @@ -182,30 +136,95 @@ public class APIWorker: NSObject, BibbiRouterInterface { return self.refreshRequest(spec: spec, headers: headers, jsonData: jsonData) } - func upload(spec: APISpec, headers: [APIHeader]? = nil, image: Data) -> Single { + + + // MARK: - Upload + + func upload( + spec: APISpec, + headers: [APIHeader]? = nil, + image: Data + ) -> Single { - let hds = self.httpHeaders(self.appendCommonHeaders(to: headers)) + let headers = self.httpHeaders(headers) guard let url = URL(string: spec.url) else { return Single.error(AFError.explicitlyCancelled) } var request = URLRequest(url: url) - request.headers = hds + request.headers = headers return Single.create { single -> Disposable in - AF.upload(image, to: url, method: spec.method, headers: hds, interceptor: BibbiRequestInterceptor()) - .validate(statusCode: [200, 204, 205]) - .responseData(emptyResponseCodes: [200, 204, 205]) { response in - switch response.result { - case .success(_): - single(.success(true)) - case let .failure(failure): - single(.failure(failure)) - } + AF.upload( + image, + to: url, + method: spec.method, + headers: headers, + interceptor: NetworkInterceptor() + ) + .validate(statusCode: [200, 204, 205]) + .responseData(emptyResponseCodes: [200, 204, 205]) { response in + switch response.result { + case .success(_): + single(.success(true)) + case let .failure(failure): + single(.failure(failure)) } + } return Disposables.create() } } } + +// MARK: - Extensions + +extension APIWorker { + + private func parameters(_ parameters: [APIParameter]?) -> Parameters? { + guard let kvs = parameters else { return nil } + var result: [String: Any] = [:] + + for kv in kvs { + result[kv.key] = kv.value + } + + return result.isEmpty ? nil: result + } + + private func httpHeaders(_ headers: [APIHeader]?) -> HTTPHeaders { + var result: [String: String] = [:] + guard let headers = headers, !headers.isEmpty else { return HTTPHeaders() } + + for header in headers { + result[header.key] = header.value + } + + return HTTPHeaders(result) + } + + + + + @available(*, deprecated) + private func appendCommonHeaders(to headers: [APIHeader]?) -> [APIHeader] { + var result: [APIHeader] = BibbiAPI.Header.baseHeaders + guard let headers = headers else { return result } + + result.append(contentsOf: headers) + + return result + } + + @available(*, deprecated, message: "Intercepter에서 필요한 헤더가 자동으로 추가됩니다.") + var _headers: Observable<[APIHeader]?> { + return App.Repository.token.accessToken + .map { + guard let token = $0, let accessToken = token.accessToken, !accessToken.isEmpty else { return [] } + return [BibbiAPI.Header.xAppKey, BibbiAPI.Header.xAuthToken(accessToken)] + } + } + +} + 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 8543946a4..5971a8770 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/CommentAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/CommentAPIWorker.swift @@ -25,9 +25,10 @@ extension CommentAPIs { } } + // MARK: - Extensions -extension CommentAPIWorker { - + +extension CommentAPIWorker { // MARK: - Fetch Comment diff --git a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIWorker.swift index 15882c48d..0d3bb8bab 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIWorker.swift @@ -26,7 +26,14 @@ extension FamilyAPIs { } } + +// MARK: - Extensions + extension FamilyAPIWorker { + + + // MARK: - Join Family + private func joinFamily(headers: [APIHeader]?, jsonEncodable body: JoinFamilyRequestDTO) -> Single { let spec = MeAPIs.joinFamily.spec @@ -52,6 +59,10 @@ extension FamilyAPIWorker { .asSingle() } + + + // MARK: - ResignFamily + private func resignFamily(spec: APISpec, headers: [APIHeader]?) -> Single { return request(spec: spec, headers: headers) .subscribe(on: Self.queue) @@ -75,6 +86,10 @@ extension FamilyAPIWorker { .asSingle() } + + + // MARK: - CreateFamily + private func createFamily(spec: APISpec, headers: [APIHeader]?) -> Single { return request(spec: spec, headers: headers) .subscribe(on: Self.queue) @@ -100,6 +115,10 @@ extension FamilyAPIWorker { .asSingle() } + + + // MARK: - Fetch Invititaion URL + private func fetchInvitationUrl(spec: APISpec, headers: [APIHeader]?) -> Single { return request(spec: spec, headers: headers) .subscribe(on: Self.queue) @@ -125,6 +144,10 @@ extension FamilyAPIWorker { .asSingle() } + + + // MARK: - Fetch FamilyCreatedAt + private func fetchFamilyCreatedAt(spec: APISpec, headers: [APIHeader]?) -> Single { return request(spec: spec, headers: headers) .subscribe(on: Self.queue) @@ -150,6 +173,10 @@ extension FamilyAPIWorker { .asSingle() } + + + // MARK: - Fetch Family Member + private func fetchPaginationFamilyMember(spec: APISpec, headers: [APIHeader]?) -> Single { return request(spec: spec, headers: headers) .subscribe(on: Self.queue) diff --git a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIs.swift index f899a5f26..b407f0e1f 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIs.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIs.swift @@ -6,10 +6,9 @@ // import Core -import Domain import Foundation -public enum FamilyAPIs: API { +enum FamilyAPIs: API { case joinFamily case createFamily case resignFamily @@ -17,7 +16,7 @@ public enum FamilyAPIs: API { case fetchFamilyCreatedAt(String) case fetchPaginationFamilyMembers(Int, Int) - public var spec: APISpec { + var spec: APISpec { switch self { case .joinFamily: return APISpec(method: .post, url: "\(BibbiAPI.hostApi)/me/join-family") diff --git a/14th-team5-iOS/Data/Sources/APIs/NetworkIntercepter.swift b/14th-team5-iOS/Data/Sources/APIs/NetworkIntercepter.swift new file mode 100644 index 000000000..0747ba0ad --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/NetworkIntercepter.swift @@ -0,0 +1,110 @@ +// +// NetworkIntercepter.swift +// Data +// +// Created by 김건우 on 6/3/24. +// + +import Core +import Domain +import Foundation + +import Alamofire +import RxCocoa +import RxSwift + +public final class NetworkInterceptor: RequestInterceptor { + + // MARK: - Properties + + private let disposeBag: DisposeBag = DisposeBag() + private let accountAPIWorker: AccountAPIWorker = AccountAPIWorker() + + private var retryCount: Int = 0 + private var retryLimit: Int = 3 + + + // MARK: - Storage + let inMemoryWrapper = InMemoryWrapper.standard + let keychainWrapper = KeychainWrapper.standard + + + // MARK: - Adapt + + public func adapt( + _ urlRequest: URLRequest, + for session: Session, + completion: @escaping (Result + ) -> Void) { + + var urlRequest = urlRequest + + guard + let urlString = urlRequest.url?.absoluteString, + urlString.hasPrefix(BibbiAPI.hostApi) == true + else { + completion(.success(urlRequest)) + return + } + + // TODO: - KeychainWrapper로 바꾸기 + guard + let accessToken = App.Repository.token.accessToken.value?.accessToken + else { + completion(.success(urlRequest)) + return + } + + urlRequest.setHeaders(BibbiHeader.commonHeaders(accessToken)) + completion(.success(urlRequest)) + } + + + + // MARK: - Retry + + public func retry( + _ request: Request, + for session: Session, + dueTo error: Error, + completion: @escaping (RetryResult + ) -> Void) { + + guard + let response = request.task?.response, + (response as? HTTPURLResponse)?.statusCode == 401 + else { + completion(.doNotRetryWithError(error)) + return + } + + if retryCount < retryLimit { + retryCount += 1 + // TODO: - KeychainWrapper로 바꾸기 + let parameter = AccountRefreshParameter( + refreshToken: App.Repository.token.accessToken.value?.refreshToken ?? "" + ) + + accountAPIWorker.accountRefreshToken(parameter: parameter) + .compactMap { $0?.toDomain() } + .asObservable() + .subscribe(onNext: { [weak self] entity in + let refreshToken = AccessToken( + accessToken: entity.accessToken, + refreshToken: entity.refreshToken, + isTemporaryToken: entity.isTemporaryToken + ) + // TODO: - InMemoryWrapper로 바꾸기 + App.Repository.token.accessToken.accept(refreshToken) + + self?.retryCount = 0 + completion(.retry) + }, onError: { error in + completion(.doNotRetryWithError(error)) + }) + .disposed(by: disposeBag) + } + } + +} + diff --git a/14th-team5-iOS/Core/Sources/BBNetwork/API/VoidResponse.swift b/14th-team5-iOS/Data/Sources/Account/AccountAPI/VoidResponse.swift similarity index 83% rename from 14th-team5-iOS/Core/Sources/BBNetwork/API/VoidResponse.swift rename to 14th-team5-iOS/Data/Sources/Account/AccountAPI/VoidResponse.swift index 692535ee2..9fa1c0e7e 100644 --- a/14th-team5-iOS/Core/Sources/BBNetwork/API/VoidResponse.swift +++ b/14th-team5-iOS/Data/Sources/Account/AccountAPI/VoidResponse.swift @@ -2,7 +2,7 @@ // VoidResponse.swift // Data // -// Created by 마경미 on 20.02.24. +// Created by 김건우 on 6/3/24. // import Foundation From ce00a41ce3cf06fc750c4bdb57215ac5df83d395 Mon Sep 17 00:00:00 2001 From: Kyoungmi <1mmgm.dev@gmail.com> Date: Mon, 3 Jun 2024 22:12:35 +0900 Subject: [PATCH 070/263] [fix]: unknown error(#540) --- .../Dependency/OnBoardingDIContainer.swift | 25 +++++----- .../OnBoarding/OnBoardingReactor.swift | 48 ++++++++++++------- .../Presentation/Splash/SplashReactor.swift | 3 -- 3 files changed, 45 insertions(+), 31 deletions(-) diff --git a/14th-team5-iOS/App/Sources/Presentation/OnBoarding/Dependency/OnBoardingDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/OnBoarding/Dependency/OnBoardingDIContainer.swift index 3982b0175..0445d4109 100644 --- a/14th-team5-iOS/App/Sources/Presentation/OnBoarding/Dependency/OnBoardingDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/OnBoarding/Dependency/OnBoardingDIContainer.swift @@ -7,22 +7,25 @@ import Foundation -import Data import Core +import Data +import Domain -public final class OnBoardingDIContainer: BaseDIContainer { +public final class OnBoardingDIContainer { - public typealias ViewContrller = OnBoardingViewController - public typealias Repository = AccountImpl - public typealias Reactor = OnBoardingReactor - - public func makeViewController() -> ViewContrller { + public func makeViewController() -> OnBoardingViewController { return OnBoardingViewController(reactor: makeReactor()) } - public func makeRepository() -> Repository { - return AccountRepository() + + public func makeUseCase() -> FamilyUseCaseProtocol { + return FamilyUseCase(familyRepository: makeRepository()) } - public func makeReactor() -> Reactor { - return OnBoardingReactor(accountRepository: AccountRepository()) + + public func makeRepository() -> FamilyRepositoryProtocol { + return FamilyRepository() + } + + public func makeReactor() -> OnBoardingReactor { + return OnBoardingReactor(familyUseCase: makeUseCase()) } } diff --git a/14th-team5-iOS/App/Sources/Presentation/OnBoarding/OnBoardingReactor.swift b/14th-team5-iOS/App/Sources/Presentation/OnBoarding/OnBoardingReactor.swift index 5ab6a611b..9ee65df26 100644 --- a/14th-team5-iOS/App/Sources/Presentation/OnBoarding/OnBoardingReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/OnBoarding/OnBoardingReactor.swift @@ -6,17 +6,18 @@ // -import Foundation import UIKit -import Data + import Core +import Data +import Domain import ReactorKit public final class OnBoardingReactor: Reactor { - public var initialState: State - private var accountRepository: AccountImpl + public var initialState: State = State() + private var familyUseCase: FamilyUseCaseProtocol public enum Action { case permissionTapped @@ -30,8 +31,8 @@ public final class OnBoardingReactor: Reactor { var permissionTappedFinish: Bool = false } - init(accountRepository: AccountRepository) { - self.accountRepository = accountRepository + init(familyUseCase: FamilyUseCaseProtocol) { + self.familyUseCase = familyUseCase self.initialState = State() } } @@ -40,18 +41,31 @@ extension OnBoardingReactor { public func mutate(action: Action) -> Observable { switch action { case .permissionTapped: - return Observable.create { observer in - MPEvent.Account.invitedGroupFinished.track(with: nil) - UNUserNotificationCenter.current().requestAuthorization( - options: [.alert, .badge, .sound], - completionHandler: { granted, error in - if granted { MPEvent.Account.allowNotification.track(with: nil) } - observer.onNext(Mutation.permissionTapped) - observer.onCompleted() - } - ) - return Disposables.create() + return Observable.zip( + Observable.create { observer in + MPEvent.Account.invitedGroupFinished.track(with: nil) + UNUserNotificationCenter.current().requestAuthorization( + options: [.alert, .badge, .sound], + completionHandler: { granted, error in + if granted { + MPEvent.Account.allowNotification.track(with: nil) + } + observer.onNext(granted) + observer.onCompleted() + } + ) + return Disposables.create() + }, + familyUseCase.executeFetchPaginationFamilyMembers(query: .init()) + ) + .flatMap { (granted: Bool, _) -> Observable in + if granted { + return Observable.just(.permissionTapped) + } else { + return Observable.empty() + } } + } } diff --git a/14th-team5-iOS/App/Sources/Presentation/Splash/SplashReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Splash/SplashReactor.swift index cdf676ad1..eb7672e84 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Splash/SplashReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Splash/SplashReactor.swift @@ -74,9 +74,6 @@ public final class SplashViewReactor: Reactor { return Observable.concat([ Observable.just(Mutation.setMemberInfo(memberInfo)), - - owner.familyUseCase.executeFetchCreatedAtFamily(familyId) - .flatMap { _ in Observable.empty() } ]) } } From b2cf1cfdc77a35db2c4039cd03e302d123cf9317 Mon Sep 17 00:00:00 2001 From: Kyoungmi <1mmgm.dev@gmail.com> Date: Mon, 3 Jun 2024 22:13:08 +0900 Subject: [PATCH 071/263] [fix]: mission unlocked alert and description(#540) --- .../Home/Dependency/MainViewDIContainer.swift | 9 ++++ .../Home/Reactor/MainViewReactor.swift | 51 ++++++++++++++----- .../ViewControllers/MainViewController.swift | 8 +++ .../MissionUserDefaultsRepository.swift | 43 ++++++++++++++++ ...CheckMissionAlertShowUseCaseProtocol.swift | 14 +++++ ...issionUserdefaultsRepositoryProtocol.swift | 14 +++++ .../CheckMissionAlertShowUseCase.swift | 22 ++++++++ 7 files changed, 148 insertions(+), 13 deletions(-) create mode 100644 14th-team5-iOS/Data/Sources/UserDefaults/MissionUserDefaultsRepository.swift create mode 100644 14th-team5-iOS/Domain/Sources/Mission/Interfaces/CheckMissionAlertShowUseCaseProtocol.swift create mode 100644 14th-team5-iOS/Domain/Sources/Mission/Interfaces/MissionUserdefaultsRepositoryProtocol.swift create mode 100644 14th-team5-iOS/Domain/Sources/Mission/UseCases/CheckMissionAlertShowUseCase.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Dependency/MainViewDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Dependency/MainViewDIContainer.swift index 977c6ffba..58b981583 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Dependency/MainViewDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Dependency/MainViewDIContainer.swift @@ -32,6 +32,7 @@ extension MainViewDIContainer { fetchMainUseCase: makeFetchMainUseCase(), fetchMainNightUseCase: makeFetchMainNightUseCase(), pickUseCase: makePickUseCase(), + checkMissionAlertShowUseCase: makeCheckMissionAlertShowUseCase(), provider: globalState ) } @@ -48,6 +49,10 @@ extension MainViewDIContainer { return MainRepository() } + private func makeMissionUserDefaultsRepository() -> MissionUserDefaultsRepository { + return MissionUserDefaultsRepository() + } + private func makeFetchMainUseCase() -> FetchMainUseCaseProtocol { return FetchMainUseCase(mainRepository: makeMainRepository()) } @@ -55,4 +60,8 @@ extension MainViewDIContainer { private func makeFetchMainNightUseCase() -> FetchMainNightUseCaseProtocol { return FetchMainNightUseCase(mainRepository: makeMainRepository()) } + + private func makeCheckMissionAlertShowUseCase() -> CheckMissionAlertShowUseCaseProtocol { + return CheckMissionAlertShowUseCase(missionUserDefaultsRepository: makeMissionUserDefaultsRepository()) + } } diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift index 30a84f5a3..04728cf55 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift @@ -27,6 +27,7 @@ final class MainViewReactor: Reactor { case cameraViewController(UploadLocation) case survivalAlert case pickAlert(String, String) + case missionUnlockedAlert case weeklycalendarViewController(String) case familyManagementViewController case monthlyCalendarViewController @@ -38,6 +39,7 @@ final class MainViewReactor: Reactor { case fetchMainUseCase case fetchMainNightUseCase + case checkMissionAlert(Bool, Bool) case openNextViewController(TapAction) case didTapSegmentControl(PostType) case pickConfirmButtonTapped(String, String) @@ -91,18 +93,20 @@ final class MainViewReactor: Reactor { private let fetchMainUseCase: FetchMainUseCaseProtocol private let fetchMainNightUseCase: FetchMainNightUseCaseProtocol private let pickUseCase: PickUseCaseProtocol + private let checkMissionAlertShowUseCase: CheckMissionAlertShowUseCaseProtocol private let provider: GlobalStateProviderProtocol - init( - fetchMainUseCase: FetchMainUseCaseProtocol, - fetchMainNightUseCase: FetchMainNightUseCaseProtocol, - pickUseCase: PickUseCaseProtocol, - provider: GlobalStateProviderProtocol) { - self.fetchMainUseCase = fetchMainUseCase - self.fetchMainNightUseCase = fetchMainNightUseCase - self.pickUseCase = pickUseCase - self.provider = provider - } + init(fetchMainUseCase: FetchMainUseCaseProtocol, + fetchMainNightUseCase: FetchMainNightUseCaseProtocol, + pickUseCase: PickUseCaseProtocol, + checkMissionAlertShowUseCase: CheckMissionAlertShowUseCaseProtocol, + provider: GlobalStateProviderProtocol) { + self.fetchMainUseCase = fetchMainUseCase + self.fetchMainNightUseCase = fetchMainNightUseCase + self.pickUseCase = pickUseCase + self.checkMissionAlertShowUseCase = checkMissionAlertShowUseCase + self.provider = provider + } } extension MainViewReactor { @@ -144,7 +148,8 @@ extension MainViewReactor { return Observable.concat( Observable.just(.updateMainData(data)), Observable.just(.setBalloonText), - Observable.just(.setCamerEnabled) + Observable.just(.setCamerEnabled), + self.mutate(action: .checkMissionAlert(data.isMissionUnlocked, data.isMeSurvivalUploadedToday)) ) } case .fetchMainNightUseCase: @@ -163,7 +168,8 @@ extension MainViewReactor { .flatMap {_ in return Observable.concat([ Observable.just(Mutation.setInTime(false)), - self.mutate(action: .fetchMainNightUseCase) + self.mutate(action: .fetchMainNightUseCase), + Observable.just(Mutation.setDescriptionText) ]) } } else { @@ -233,6 +239,19 @@ extension MainViewReactor { case .contributorNextButtonTap: return Observable.just(.showNextView(.weeklycalendarViewController(currentState.contributor.recentPostDate))) } + case .checkMissionAlert(let isUnlocked, let isMeSurvivalUploadedToday): + if isUnlocked && isMeSurvivalUploadedToday { + return checkMissionAlertShowUseCase.execute() + .flatMap { isAlreadyShown in + if !isAlreadyShown { + return Observable.just(.showNextView(.missionUnlockedAlert)) + } else { + return Observable.empty() + } + } + } else { + return Observable.empty() + } } } @@ -298,7 +317,7 @@ extension MainViewReactor { if currentState.pageIndex == 0 { newState.cameraEnabled = !currentState.isMeSurvivalUploadedToday } else { - if (!currentState.isMeMissionUploadedToday) && currentState.isMissionUnlocked { + if (!currentState.isMeMissionUploadedToday) && currentState.isMissionUnlocked { newState.cameraEnabled = true } else { newState.cameraEnabled = false @@ -343,6 +362,12 @@ extension MainViewReactor { private func setDescriptionText(_ state: State) -> State { var newState = state + guard let inTime = currentState.isInTime, + inTime else { + newState.description = .survivalNone + return newState + } + if currentState.pageIndex == 0 { if currentState.isFamilySurvivalUploadedToday { newState.description = .survivalFull 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 18aa7e4bd..54cbd86b7 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift @@ -308,6 +308,14 @@ extension MainViewController { guard let self else { return } self.alertConfirmRelay.accept((name, id)) } .present() + case .missionUnlockedAlert: + BibbiAlertBuilder(self) + .alertStyle(.missionKey) + .setConfirmAction { [weak self] in + guard let self else { return } + self.navigationController?.pushViewController(CameraDIContainer(cameraType: .mission).makeViewController(), animated: true) + } + .present() } } diff --git a/14th-team5-iOS/Data/Sources/UserDefaults/MissionUserDefaultsRepository.swift b/14th-team5-iOS/Data/Sources/UserDefaults/MissionUserDefaultsRepository.swift new file mode 100644 index 000000000..262f41de9 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/UserDefaults/MissionUserDefaultsRepository.swift @@ -0,0 +1,43 @@ +// +// MissionUserDefaultsRepository.swift +// Data +// +// Created by 마경미 on 03.06.24. +// + +import Foundation + +import Domain + +import RxSwift + +public final class MissionUserDefaultsRepository: MissionUserdefaultsRepositoryProtocol { + + private let lastMissionUploadDateId = "lastMissionUploadDateId" + + public init() { } + + public func isAlreadyShowMissionAlert() -> Observable { + guard let lastDate = UserDefaults.standard.string(forKey: lastMissionUploadDateId) else { + saveMissionUploadDate() + return .just(false) + } + + if lastDate == Date().toFormatString(with: "yyyy-MM-dd") { + return .just(true) + } else { + saveMissionUploadDate() + return .just(false) + } + } + + private func saveMissionUploadDate() { + let isoDateFormatter = ISO8601DateFormatter() + isoDateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd" + let extractedDate = dateFormatter.string(from: Date()) + UserDefaults.standard.set(extractedDate, forKey: lastMissionUploadDateId) + } +} diff --git a/14th-team5-iOS/Domain/Sources/Mission/Interfaces/CheckMissionAlertShowUseCaseProtocol.swift b/14th-team5-iOS/Domain/Sources/Mission/Interfaces/CheckMissionAlertShowUseCaseProtocol.swift new file mode 100644 index 000000000..825b5734c --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Mission/Interfaces/CheckMissionAlertShowUseCaseProtocol.swift @@ -0,0 +1,14 @@ +// +// MissionAlertUseCaseProtocol.swift +// Domain +// +// Created by 마경미 on 03.06.24. +// + +import Foundation + +import RxSwift + +public protocol CheckMissionAlertShowUseCaseProtocol { + func execute() -> Observable +} diff --git a/14th-team5-iOS/Domain/Sources/Mission/Interfaces/MissionUserdefaultsRepositoryProtocol.swift b/14th-team5-iOS/Domain/Sources/Mission/Interfaces/MissionUserdefaultsRepositoryProtocol.swift new file mode 100644 index 000000000..c26c8cc05 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Mission/Interfaces/MissionUserdefaultsRepositoryProtocol.swift @@ -0,0 +1,14 @@ +// +// MissionUserdefaultsRepositoryProtocol.swift +// Domain +// +// Created by 마경미 on 03.06.24. +// + +import Foundation + +import RxSwift + +public protocol MissionUserdefaultsRepositoryProtocol { + func isAlreadyShowMissionAlert() -> Observable +} diff --git a/14th-team5-iOS/Domain/Sources/Mission/UseCases/CheckMissionAlertShowUseCase.swift b/14th-team5-iOS/Domain/Sources/Mission/UseCases/CheckMissionAlertShowUseCase.swift new file mode 100644 index 000000000..253667e8b --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Mission/UseCases/CheckMissionAlertShowUseCase.swift @@ -0,0 +1,22 @@ +// +// CheckMissionUseCase.swift +// Domain +// +// Created by 마경미 on 03.06.24. +// + +import Foundation + +import RxSwift + +public class CheckMissionAlertShowUseCase: CheckMissionAlertShowUseCaseProtocol { + private let missionUserDefaultsRepository: MissionUserdefaultsRepositoryProtocol + + public init(missionUserDefaultsRepository: MissionUserdefaultsRepositoryProtocol) { + self.missionUserDefaultsRepository = missionUserDefaultsRepository + } + + public func execute() -> Observable { + return missionUserDefaultsRepository.isAlreadyShowMissionAlert().asObservable() + } +} From 207d8a4678653b37cf0521435d3ce10b7873a13a Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Tue, 4 Jun 2024 21:05:31 +0900 Subject: [PATCH 072/263] =?UTF-8?q?feat:=20DIContainer=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#538)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Core/Sources/DIContainer/Container.swift | 91 ++++++++++++ .../Sources/DIContainer/DIContainer.swift | 129 ++++++++++++++++++ .../DIContainer/InjectIdentifier.swift | 60 ++++++++ 3 files changed, 280 insertions(+) create mode 100644 14th-team5-iOS/Core/Sources/DIContainer/Container.swift create mode 100644 14th-team5-iOS/Core/Sources/DIContainer/DIContainer.swift create mode 100644 14th-team5-iOS/Core/Sources/DIContainer/InjectIdentifier.swift diff --git a/14th-team5-iOS/Core/Sources/DIContainer/Container.swift b/14th-team5-iOS/Core/Sources/DIContainer/Container.swift new file mode 100644 index 000000000..093ef2b45 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/DIContainer/Container.swift @@ -0,0 +1,91 @@ +// +// Container.swift +// DIContainer +// +// Created by 김건우 on 6/3/24. +// + +import Foundation + + +// MARK: - Container + +public class Container: Injectable { + + public static var standard = Container() + + public var dependencies: [AnyHashable : Any] = [:] + + required public init() { } + +} + + +// MARK: - Property Wrapper + +@propertyWrapper +public struct Injected { + + // MARK: - Container + public static func container() -> Injectable { + Container.standard + } + + // MARK: - Properties + private let identifier: InjectIdentifier + private let container: Resolvable + private let `default`: V? + + // MARK: - Intializer + public init( + identifier: InjectIdentifier? = nil, + container: Resolvable? = nil, + default: V? = nil + ) { + self.identifier = identifier ?? .by(type: V.self) + self.container = container ?? Self.container() + self.default = `default` + } + + // MARK: - Wrapped Value + public lazy var wrappedValue: V = { + if let value = try? container.resolve(identifier) { + return value + } + + if let `default` { + return `default` + } + + fatalError("Could not resolve with \(identifier) and default is nil") + }() + +} + + + +@propertyWrapper +public struct InejctedSafe { + + // MARK: - Container + public static func container() -> Injectable { + Container.standard + } + + // MARK: - Properties + private let identifier: InjectIdentifier + private let container: Resolvable + + // MARK: - Intiaializer + public init( + identifier: InjectIdentifier? = nil, + container: Resolvable? = nil + ) { + self.identifier = identifier ?? .by(type: V.self) + self.container = container ?? Self.container() + } + + // MARK: - WrappedValue + public lazy var wrappedValue: V? = try? container.resolve(identifier) + +} diff --git a/14th-team5-iOS/Core/Sources/DIContainer/DIContainer.swift b/14th-team5-iOS/Core/Sources/DIContainer/DIContainer.swift new file mode 100644 index 000000000..0ed50d29b --- /dev/null +++ b/14th-team5-iOS/Core/Sources/DIContainer/DIContainer.swift @@ -0,0 +1,129 @@ +// +// DIContainer.swift +// DIContainer +// +// Created by 김건우 on 6/3/24. +// + +import Foundation + +// MARK: - Resolvable + +public protocol Resolvable { + + func resolve(_ identifier: InjectIdentifier) throws -> V + +} + + +// MARK: - ResolvableError + +public enum ResolvableError: Error { + + case dependencyNotFound(Any.Type?, String?) + +} + +extension ResolvableError: LocalizedError { + + public var errorDescription: String? { + switch self { + case let .dependencyNotFound(type, key): + var message = "Could not find dependency for " + if let type = type { + message += "type: \(type) " + } else if let key = key { + message += "key: \(key)" + } + return message + } + } + +} + + + +// MARK: - Injectable + +public protocol Injectable: Resolvable, AnyObject { + + init() + + var dependencies: [AnyHashable: Any] { get set } + + func register(_ identifier: InjectIdentifier, _ resolve: (Resolvable) throws -> V) + func remove(_ identifier: InjectIdentifier) + +} + +public extension Injectable { + + + // MARK: - Register + + func register( + _ identifier: InjectIdentifier, + _ resolve: (Resolvable) throws -> V + ) { + do { + self.dependencies[identifier] = try resolve(self) + } catch { + assertionFailure(error.localizedDescription) + } + } + + func register( + _ identifier: InjectIdentifier, + object: @autoclosure () -> V + ) { + self.register(identifier) { _ in object() } + } + + func register( + type: V.Type? = nil, + key: String? = nil, + _ resolve: (Resolvable) throws -> V + ) { + self.register(.by(type: type, key: key), resolve) + } + + + // MARK: - Remove + + func remove(_ identifier: InjectIdentifier) { + self.dependencies.removeValue(forKey: identifier) + } + + func remove( + type: V.Type? = nil, + key: String? = nil + ) { + let identifier = InjectIdentifier.by(type: type, key: key) + self.dependencies.removeValue(forKey: identifier) + } + + func removeAllDependencies() { + self.dependencies.removeAll() + } + + + // MARK: - Resolve + + func resolve(_ identifier: InjectIdentifier) throws -> V { + guard + let dependency = dependencies[identifier] as? V + else { + throw ResolvableError.dependencyNotFound(identifier.type, identifier.key) + } + return dependency + } + + func resolve( + type: V.Type? = nil, + key: String? = nil + ) throws -> V { + try self.resolve(.by(type: type, key: key)) + } + + +} diff --git a/14th-team5-iOS/Core/Sources/DIContainer/InjectIdentifier.swift b/14th-team5-iOS/Core/Sources/DIContainer/InjectIdentifier.swift new file mode 100644 index 000000000..84e11cd35 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/DIContainer/InjectIdentifier.swift @@ -0,0 +1,60 @@ +// +// InjectIdentifier.swift +// DIContainer +// +// Created by 김건우 on 6/3/24. +// + +import Foundation + + +// MARK: - Identifier + +public struct InjectIdentifier { + + private(set) var type: V.Type? = nil + + private(set) var key: String? = nil + + private init( + type: V.Type? = nil, + key: String? = nil + ) { + self.type = type + self.key = key + } + +} + + +// MARK: - Extensions + +extension InjectIdentifier: Hashable { + + public static func == ( + lhs: InjectIdentifier, + rhs: InjectIdentifier + ) -> Bool { + return lhs.hashValue == rhs.hashValue + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(self.key) + if let type = type { + hasher.combine(ObjectIdentifier(type)) + } + } + +} + + +public extension InjectIdentifier { + + static func by( + type: V.Type? = nil, + key: String? = nil + ) -> InjectIdentifier { + return .init(type: type, key: key) + } + +} From 5a39960982b44cfe44357032c7a2f387bc1fffe8 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Tue, 4 Jun 2024 22:08:42 +0900 Subject: [PATCH 073/263] =?UTF-8?q?feat:=20CalendarDIContainer=20=EC=98=88?= =?UTF-8?q?=EC=8B=9C=20=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1=20(#538)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../App/Sources/Application/AppDelegate.swift | 11 +++- .../DIContainer/CalendarDIContainer.swift | 58 +++++++++++++++++++ .../Application/DIContainer/DIContainer.swift | 48 +++++++++++++++ .../DIContainer/Wrapper/BaseWrapper.swift | 21 +++++++ ...MonthlyCalendarViewControllerWrapper.swift | 47 +++++++++++++++ .../Dependency/CalendarCellDIContainer.swift | 6 +- .../MonthlyCalendarDIConatainer.swift | 16 ++--- .../Reactor/CalendarPageViewCellReactor.swift | 17 +++--- .../Reactor/MonthlyCalendarViewReactor.swift | 15 +++-- .../Repository/CalendarRepository.swift | 16 ++++- .../UseCase/PostCommentUseCase.swift | 15 ++++- 11 files changed, 241 insertions(+), 29 deletions(-) create mode 100644 14th-team5-iOS/App/Sources/Application/DIContainer/CalendarDIContainer.swift create mode 100644 14th-team5-iOS/App/Sources/Application/DIContainer/DIContainer.swift create mode 100644 14th-team5-iOS/App/Sources/Application/DIContainer/Wrapper/BaseWrapper.swift create mode 100644 14th-team5-iOS/App/Sources/Application/DIContainer/Wrapper/Calendar/MonthlyCalendarViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/AppDelegate.swift b/14th-team5-iOS/App/Sources/Application/AppDelegate.swift index b768f1703..996c04f7b 100644 --- a/14th-team5-iOS/App/Sources/Application/AppDelegate.swift +++ b/14th-team5-iOS/App/Sources/Application/AppDelegate.swift @@ -37,7 +37,16 @@ class AppDelegate: UIResponder, UIApplicationDelegate { setupUserNotificationCenter(application) removeKeychainAtFirstLaunch() bindRepositories() - App.indicator.bind() + App.indicator.bind() // TODO: - Data 지우기 + + + let containers: [DIContainer] = [ + CalendarDIContainer() + ] + containers.forEach { + $0.registerDependencies() + } + return true } diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/CalendarDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/CalendarDIContainer.swift new file mode 100644 index 000000000..95e135ca0 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/CalendarDIContainer.swift @@ -0,0 +1,58 @@ +// +// CommentDIContainer.swift +// App +// +// Created by 김건우 on 6/3/24. +// + +import Core +import Data +import Domain + + +// NOTE: +// - APIWorker를 나눈대로 DIContainer도 분리합니다. +// - CalendarDIContainer는 유지보수 용이를 위한 컨테어너의 역할만 수행합니다. + +final class CalendarDIContainer: DIContainer { + + // MARK: - Make UseCase + + func makeUseCase() -> CalendarUseCaseProtocol { + return CalendarUseCase( + calendarRepository: makeCalendarRepository() + ) + } + + // NOTE: - 추후 UseCase 리팩토링하면 make() 메서드가 더 많아지겠죠? + + + // MARK: - Make Repository + + func makeCalendarRepository() -> CalendarRepositoryProtocol { + return CalendarRepository( + // calendarApiWorker: ... + keychain: Storage.Keychain.token, + userDefaults: Storage.UserDefaults.member + ) + + // NOTE: - 궁극적인 목표는 DIContainer 안에서 APIWorker, Storage 등 모든 의존성을 주입하게 만들어 유지보수를 용이하게 하는 겁니다. + } + + + // MARK: - Register + + override func registerDependencies() { + super.registerDependencies() + + // NOTE: - 등록하면 @Injected로 편하게 의존성을 받아올 수 있습니다. + // - (MonthlyCalendarViewReactor.swift 참조) + container.register(type: CalendarUseCaseProtocol.self) { _ in makeUseCase() } + } + + + +} + + + diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/DIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/DIContainer.swift new file mode 100644 index 000000000..3f69b8445 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/DIContainer.swift @@ -0,0 +1,48 @@ +// +// DIContainer.swift +// App +// +// Created by 김건우 on 6/4/24. +// + +import Core +import Data +import Foundation + +class DIContainer { + + // MARK: - Properties + var container: Container = { + Container.standard + }() + + // MARK: - Persistent Storage + enum Storage { + enum Keychain { + static let token = TokenKeychain() + } + + enum UserDefaults { + static let member = MemberUserDefaults() + } + + enum InMemory { + // NOTE: - 사실 CleanArchitecture 잘 준수만 하면 인-메모리 DB는 사용할 일이 딱히 없어 보여요. + // 곧바로 Repository에서 Keychain이나 UserDefaults에 접근하면 되니까요. + } + } + + // MARK: - Register + func registerDependencies() { + + // ServiceProvider 등록 + container.register(type: GlobalStateProviderProtocol.self) { _ in + return GlobalStateProvider() + } + + // NOTE: - 앞으로 @Injected var provider: ServiceProviderProtocol처럼 작성해야 합니다. + // 직접 Service를 주입하면 항목(Item) 전달이 제대로 되지 않아요. + + } + +} diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/Wrapper/BaseWrapper.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/Wrapper/BaseWrapper.swift new file mode 100644 index 000000000..29479daed --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/Wrapper/BaseWrapper.swift @@ -0,0 +1,21 @@ +// +// BaseWrapper.swift +// App +// +// Created by 김건우 on 6/4/24. +// + +import Core +import UIKit + +import ReactorKit + +// NOTE: - Bibbi-Package에 이미 포함되어 있어 삭제할 코드 + +protocol BaseWrapper { + + associatedtype R: Reactor + associatedtype V: ReactorKit.View + + func makeReactor() -> R +} diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/Wrapper/Calendar/MonthlyCalendarViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/Wrapper/Calendar/MonthlyCalendarViewControllerWrapper.swift new file mode 100644 index 000000000..87f5ef7a4 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/Wrapper/Calendar/MonthlyCalendarViewControllerWrapper.swift @@ -0,0 +1,47 @@ +// +// MonthlyCalendarViewControllerWrapper.swift +// App +// +// Created by 김건우 on 6/4/24. +// + +import Foundation + + +// MARK: - Wrapper + +final class MonthlyCalendarViewControllerWrapper { + + // NOTE: - 기존 DIContainer의 역할은 Wrapper가 대신하게 됩니다. + // - 파일 위치는 의논해봐야 합니다만, 한 부류의 화면으로만 이동하는 게 아니니 전역에 위치하는 게 좋아보입니다. + // - @Wrapper 매크로가 도입되면 코드가 많이 단축됩니다. + + func makeReactor() -> R { + return MonthlyCalendarViewReactor() + } + + + // Begin expansion of "@Wrapper" + + typealias R = MonthlyCalendarViewReactor + typealias V = MonthlyCalendarViewController + + func makeViewController() -> V { + return MonthlyCalendarViewController(reactor: makeReactor()) + } + + var viewController: V { + makeViewController() + } + + var reactor: R { + makeReactor() + } + + // End expansion of "@Wrapper" + +} + +// Begin expansion of "@Wrapper" +extension MonthlyCalendarViewControllerWrapper: BaseWrapper { } +// End expansion of "@Wrapper" diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/CalendarCellDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/CalendarCellDIContainer.swift index 98b5daabf..f1e6a3c53 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/CalendarCellDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/CalendarCellDIContainer.swift @@ -38,9 +38,9 @@ public final class CalendarCellDIContainer { public func makeReactor() -> CalendarCellReactor { return CalendarCellReactor( - yearMonth: yearMonth, - calendarUseCase: makeUseCase(), - provider: globalState + yearMonth: yearMonth +// calendarUseCase: makeUseCase(), +// provider: globalState ) } } diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/MonthlyCalendarDIConatainer.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/MonthlyCalendarDIConatainer.swift index 786c21685..92a674536 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/MonthlyCalendarDIConatainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/MonthlyCalendarDIConatainer.swift @@ -13,12 +13,12 @@ import Domain public final class MonthlyCalendarDIConatainer { // MARK: - Properties - private var globalState: GlobalStateProviderProtocol { - guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { - return GlobalStateProvider() - } - return appDelegate.globalStateProvider - } +// private var globalState: GlobalStateProviderProtocol { +// guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { +// return GlobalStateProvider() +// } +// return appDelegate.globalStateProvider +// } // MARK: - Make public func makeViewController() -> MonthlyCalendarViewController { @@ -35,8 +35,8 @@ public final class MonthlyCalendarDIConatainer { public func makeReactor() -> MonthlyCalendarViewReactor { return MonthlyCalendarViewReactor( - calendarUseCase: makeCalendarUseCase(), - provider: globalState +// calendarUseCase: makeCalendarUseCase(), +// provider: globalState ) } } diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPageViewCellReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPageViewCellReactor.swift index fb8c340cc..f271a0628 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPageViewCellReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPageViewCellReactor.swift @@ -41,22 +41,25 @@ public final class CalendarCellReactor: Reactor { // MARK: - Properties public var initialState: State - private let calendarUseCase: CalendarUseCaseProtocol - private let provider: GlobalStateProviderProtocol + @Injected var calendarUseCase: CalendarUseCaseProtocol + @Injected var provider: GlobalStateProviderProtocol + +// private let calendarUseCase: CalendarUseCaseProtocol +// private let provider: GlobalStateProviderProtocol // MARK: - Intializer init( - yearMonth: String, - calendarUseCase: CalendarUseCaseProtocol, - provider: GlobalStateProviderProtocol + yearMonth: String +// calendarUseCase: CalendarUseCaseProtocol, +// provider: GlobalStateProviderProtocol ) { self.initialState = State( yearMonth: yearMonth, displayMemoryCount: 0 ) - self.calendarUseCase = calendarUseCase - self.provider = provider +// self.calendarUseCase = calendarUseCase +// self.provider = provider } // MARK: - Mutate diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/MonthlyCalendarViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/MonthlyCalendarViewReactor.swift index 0f0977639..4e3274d5b 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/MonthlyCalendarViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/MonthlyCalendarViewReactor.swift @@ -36,24 +36,27 @@ public final class MonthlyCalendarViewReactor: Reactor { @Pulse var displayCalendar: [MonthlyCalendarSectionModel] } + @Injected var provider: GlobalStateProviderProtocol + @Injected var calendarUseCase: CalendarUseCaseProtocol + // MARK: - Properties public var initialState: State - public let provider: GlobalStateProviderProtocol - private let calendarUseCase: CalendarUseCaseProtocol +// public let provider: GlobalStateProviderProtocol +// private let calendarUseCase: CalendarUseCaseProtocol // MARK: - Intializer init( - calendarUseCase: CalendarUseCaseProtocol, - provider: GlobalStateProviderProtocol +// calendarUseCase: CalendarUseCaseProtocol, +// provider: GlobalStateProviderProtocol ) { self.initialState = State( shouldPopViewController: false, displayCalendar: [.init(model: (), items: [])] ) - self.calendarUseCase = calendarUseCase - self.provider = provider +// self.calendarUseCase = calendarUseCase +// self.provider = provider } // MARK: - Transform diff --git a/14th-team5-iOS/Data/Sources/APIs/Calendar/Repository/CalendarRepository.swift b/14th-team5-iOS/Data/Sources/APIs/Calendar/Repository/CalendarRepository.swift index e1d6ccb3d..16c1c8504 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Calendar/Repository/CalendarRepository.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Calendar/Repository/CalendarRepository.swift @@ -12,13 +12,23 @@ import Foundation import RxSwift public final class CalendarRepository: CalendarRepositoryProtocol { + // MARK: - Properties - public let disposeBag: DisposeBag = DisposeBag() + public let disposeBag = DisposeBag() + + // MARK: - API EndPoint + private let calendarApiWorker = CalendarAPIWorker() - private let calendarApiWorker: CalendarAPIWorker = CalendarAPIWorker() + // MARK: - Persistent Storage // MARK: - Intializer - public init() { } + public init( + keychain: KeychainType? = nil, + userDefaults: UserDefaultsType? = nil, + inMemory: InMemoryType? = nil + ) { + + } } // MARK: - Extensions diff --git a/14th-team5-iOS/Domain/Sources/PostComment/UseCase/PostCommentUseCase.swift b/14th-team5-iOS/Domain/Sources/PostComment/UseCase/PostCommentUseCase.swift index c03bcc62f..4d07071e2 100644 --- a/14th-team5-iOS/Domain/Sources/PostComment/UseCase/PostCommentUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/PostComment/UseCase/PostCommentUseCase.swift @@ -5,10 +5,12 @@ // Created by 김건우 on 1/22/24. // +import Core import Foundation import RxSwift + public protocol PostCommentUseCaseProtocol { func executeFetchPostComment(postId: String, query: PostCommentPaginationQuery) -> Observable func executeCreatePostComment(postId: String, body: CreatePostCommentRequest) -> Observable @@ -18,7 +20,9 @@ public protocol PostCommentUseCaseProtocol { public final class PostCommentUseCase: PostCommentUseCaseProtocol { private let postCommentRepository: PostCommentRepositoryProtocol - public init(postCommentRepository: PostCommentRepositoryProtocol) { + public init( + postCommentRepository: PostCommentRepositoryProtocol + ) { self.postCommentRepository = postCommentRepository } @@ -34,3 +38,12 @@ public final class PostCommentUseCase: PostCommentUseCaseProtocol { return postCommentRepository.deletePostComment(postId: postId, commentId: commentId) } } + + +public extension InjectIdentifier { + + static var commentUseCase: InjectIdentifier { + .by(type: PostCommentUseCaseProtocol.self) + } + +} From 6a97f1ec577c46b29a86a5fdd416015717e77cfb Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Wed, 5 Jun 2024 14:41:01 +0900 Subject: [PATCH 074/263] =?UTF-8?q?feat:=20=EC=88=9C=ED=99=98=20=EC=B0=B8?= =?UTF-8?q?=EC=A1=B0=20=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95=20(#538)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../App/Sources/Application/AppDelegate.swift | 41 ++++++++++++++++++- .../DIContainer/CalendarDIContainer.swift | 6 ++- .../Application/DIContainer/DIContainer.swift | 5 +++ .../MonthlyCalendarDIConatainer.swift | 12 +++--- .../CalendarAPI/CalendarAPIWorker.swift | 6 +-- .../Calendar/CalendarAPI/CalendarAPIs.swift | 4 +- .../UserDefaults/UserDefaults+Family.swift | 8 +++- .../Calendar/UseCases/CalendarUseCase.swift | 8 ++++ 8 files changed, 76 insertions(+), 14 deletions(-) diff --git a/14th-team5-iOS/App/Sources/Application/AppDelegate.swift b/14th-team5-iOS/App/Sources/Application/AppDelegate.swift index 996c04f7b..e1b422c7e 100644 --- a/14th-team5-iOS/App/Sources/Application/AppDelegate.swift +++ b/14th-team5-iOS/App/Sources/Application/AppDelegate.swift @@ -21,13 +21,25 @@ import RxKakaoSDKCommon import RxSwift import Mixpanel + + +// MARK: - AppDelegate + @main class AppDelegate: UIResponder, UIApplicationDelegate { - let disposeBag = DisposeBag() + // MARK: - Properties + var window: UIWindow? + let disposeBag = DisposeBag() + + @available(*, deprecated, message: "@Injected var provider: ServiceProviderProtocol") let globalStateProvider: GlobalStateProviderProtocol = GlobalStateProvider() + + + // MARK: - DidFinishLaunchingWithOptions + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { kakaoApp(application, didFinishLauchingWithOptions: launchOptions) appleApp(application, didFinishLauchingWithOptions: launchOptions) @@ -51,6 +63,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { return true } + + // MARK: - Open Url + func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { if self.kakaoApp(app, open: url, options: options) { return true @@ -59,6 +74,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { return false } + + // MARK: - WillTerminate + func applicationWillTerminate(_ application: UIApplication) { unbindRepositories() App.indicator.unbind() @@ -84,8 +102,13 @@ extension AppDelegate { } } + +// MARK: - Kakao + extension AppDelegate { + func kakaoApp(_ app: UIApplication, didFinishLauchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) { + // TODO: - Bundle+Ext로 보내기 guard let kakaoLoginAPIKey = Bundle.main.object(forInfoDictionaryKey: "KAKAO_LOGIN_API_KEY") as? String else { return } @@ -109,6 +132,9 @@ extension AppDelegate { } } + +// MARK: - MixPanel + extension AppDelegate: MixpanelDelegate { func mixpanelApp(_ app: UIApplication, didFinishLaunchingWithOptions launchOption: [UIApplication.LaunchOptionsKey: Any]?) { @@ -123,6 +149,9 @@ extension AppDelegate: MixpanelDelegate { } } + +// MARK: - Apple + extension AppDelegate { func appleApp(_ app: UIApplication, didFinishLauchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) { @@ -151,6 +180,9 @@ extension AppDelegate { } } + +// MARK: - FCM Token + extension AppDelegate: MessagingDelegate { func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { guard let token = fcmToken else { return } @@ -166,6 +198,9 @@ extension AppDelegate: MessagingDelegate { } } + +// MARK: - UNUserNotification + extension AppDelegate: UNUserNotificationCenterDelegate { func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { completionHandler([.banner, .badge, .sound]) @@ -189,6 +224,10 @@ extension AppDelegate: UNUserNotificationCenterDelegate { } } + + + + extension AppDelegate { func bindRepositories() { App.Repository.token.bind() diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/CalendarDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/CalendarDIContainer.swift index 95e135ca0..f51ad8ea8 100644 --- a/14th-team5-iOS/App/Sources/Application/DIContainer/CalendarDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/CalendarDIContainer.swift @@ -47,7 +47,11 @@ final class CalendarDIContainer: DIContainer { // NOTE: - 등록하면 @Injected로 편하게 의존성을 받아올 수 있습니다. // - (MonthlyCalendarViewReactor.swift 참조) - container.register(type: CalendarUseCaseProtocol.self) { _ in makeUseCase() } + container.register( + type: CalendarUseCaseProtocol.self + ) { [unowned self] _ in + self.makeUseCase() + } } diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/DIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/DIContainer.swift index 3f69b8445..80dd64d20 100644 --- a/14th-team5-iOS/App/Sources/Application/DIContainer/DIContainer.swift +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/DIContainer.swift @@ -16,6 +16,11 @@ class DIContainer { Container.standard }() + // MARK: - API Workers + enum API { + static let calendar = CalendarAPIWorker() + } + // MARK: - Persistent Storage enum Storage { enum Keychain { diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/MonthlyCalendarDIConatainer.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/MonthlyCalendarDIConatainer.swift index 92a674536..6d7a37161 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/MonthlyCalendarDIConatainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/MonthlyCalendarDIConatainer.swift @@ -13,12 +13,12 @@ import Domain public final class MonthlyCalendarDIConatainer { // MARK: - Properties -// private var globalState: GlobalStateProviderProtocol { -// guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { -// return GlobalStateProvider() -// } -// return appDelegate.globalStateProvider -// } + private var globalState: GlobalStateProviderProtocol { + guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { + return GlobalStateProvider() + } + return appDelegate.globalStateProvider + } // MARK: - Make public func makeViewController() -> MonthlyCalendarViewController { diff --git a/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/CalendarAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/CalendarAPIWorker.swift index 4941ec470..8654ddbd1 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/CalendarAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/CalendarAPIWorker.swift @@ -11,14 +11,14 @@ import Foundation import RxSwift -typealias CalendarAPIWorker = CalendarAPIs.Worker +public typealias CalendarAPIWorker = CalendarAPIs.Worker extension CalendarAPIs { - final class Worker: APIWorker { + public final class Worker: APIWorker { static let queue = { ConcurrentDispatchQueueScheduler(queue: DispatchQueue(label: "CalendarAPIQueue", qos: .utility)) }() - override init() { + public override init() { super.init() self.id = "CalendarAPIWorker" } diff --git a/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/CalendarAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/CalendarAPIs.swift index 3e2bb0cb4..9ee695640 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/CalendarAPIs.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/CalendarAPIs.swift @@ -8,7 +8,7 @@ import Core import Foundation -enum CalendarAPIs: API { +public enum CalendarAPIs: API { @available(*, deprecated) case calendarResponse(String) @@ -17,7 +17,7 @@ enum CalendarAPIs: API { case fetchStatisticsSummary(String) case fetchBanner(String) - var spec: APISpec { + public var spec: APISpec { switch self { case let .calendarResponse(yearMonth): return APISpec(method: .get, url: "\(BibbiAPI.hostApi)/calendar?type=MONTHLY&yearMonth=\(yearMonth)") diff --git a/14th-team5-iOS/Data/Sources/Storages/UserDefaults/UserDefaults+Family.swift b/14th-team5-iOS/Data/Sources/Storages/UserDefaults/UserDefaults+Family.swift index 0109c43de..ee4017197 100644 --- a/14th-team5-iOS/Data/Sources/Storages/UserDefaults/UserDefaults+Family.swift +++ b/14th-team5-iOS/Data/Sources/Storages/UserDefaults/UserDefaults+Family.swift @@ -11,7 +11,13 @@ import Foundation // NOTE: - 예시 코드 -final public class MemberUserDefaults: UserDefaultsType { +public protocol MemberUserDefaultsType: UserDefaultsType { + func save(_ memberId: String) + func loadMemberId() -> String? +} + + +final public class MemberUserDefaults: MemberUserDefaultsType { // MARK: - Intializer public init() { } diff --git a/14th-team5-iOS/Domain/Sources/Calendar/UseCases/CalendarUseCase.swift b/14th-team5-iOS/Domain/Sources/Calendar/UseCases/CalendarUseCase.swift index 3fcdda066..838714dca 100644 --- a/14th-team5-iOS/Domain/Sources/Calendar/UseCases/CalendarUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/Calendar/UseCases/CalendarUseCase.swift @@ -5,6 +5,7 @@ // Created by 김건우 on 12/29/23. // +import Core import Foundation import RxSwift @@ -39,3 +40,10 @@ public final class CalendarUseCase: CalendarUseCaseProtocol { return calendarRepository.fetchCalendarBanner(yearMonth: yearMonth) } } + + +public extension InjectIdentifier { + static var calendarUseCase: InjectIdentifier { + .by(type: CalendarUseCaseProtocol.self) + } +} From 08f249d3891a44ff584eff184afda1d896fb679a Mon Sep 17 00:00:00 2001 From: Do-hyun-Kim Date: Wed, 5 Jun 2024 15:30:37 +0900 Subject: [PATCH 075/263] =?UTF-8?q?refactor:=20ProfileAPIs,=20ProfileAPIWo?= =?UTF-8?q?rker=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20MembersAPI,=20MembersAPIWo?= =?UTF-8?q?rker=EB=A1=9C=20=EC=88=98=EC=A0=95=20-=20MembersProfileResponse?= =?UTF-8?q?DTO=20=ED=8C=8C=EC=9D=BC=20=EC=B6=94=EA=B0=80,=20ProfileMemberD?= =?UTF-8?q?TO=20=ED=8C=8C=EC=9D=BC=20=EC=A0=9C=EA=B1=B0=20-=20ProfileMembe?= =?UTF-8?q?rResponse=20=ED=8C=8C=EC=9D=BC=20=EB=AA=85=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=20-=20MembersRepositoryProtocol=20=EC=B6=94=EA=B0=80=20or=20Me?= =?UTF-8?q?mbersUseCase=20=ED=8C=8C=EC=9D=BC=20=EC=B6=94=EA=B0=80=20-=20Pr?= =?UTF-8?q?ofileViewInterface,=20ProfileViewUsecase=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Camera/Reactor/CameraViewReactor.swift | 4 +- .../Dependency/ProfileDIContainer.swift | 10 +- .../Profile/Reactor/ProfileViewReactor.swift | 28 ++--- .../AccountRepository/AccountRepository.swift | 2 +- .../Camera/CameraAPI/CameraAPIWorker.swift | 4 +- .../Repositories/CameraViewRepository.swift | 4 +- .../MembersProfileResponseDTO.swift | 43 +++++++ .../MemberAPI/MembersAPIWorker.swift} | 33 +++--- .../MemberAPI/MembersAPIs.swift} | 8 +- .../Repositories/MembersRepository.swift} | 42 +++---- .../DataMapping/ProfileMemberDTO.swift | 43 ------- .../Interfaces/CameraViewInterface.swift | 2 +- .../Camera/UseCases/CameraViewUseCase.swift | 4 +- .../Entities/MemberProfileResponse.swift} | 18 +-- .../MembersRepositoryProtocol.swift | 22 ++++ .../ProfileImageEditParameter.swift | 0 .../Members/UseCases/MembersUseCase.swift | 51 +++++++++ .../UseCases/ProfileFeedUseCase.swift | 0 .../Interfaces/ProfileViewInterface.swift | 35 ------ .../Profile/UseCases/ProfileViewUseCase.swift | 51 --------- .../xcschemes/Bibbi-Workspace.xcscheme | 106 +++++++++--------- 21 files changed, 248 insertions(+), 262 deletions(-) create mode 100644 14th-team5-iOS/Data/Sources/Members/DataMapping/MembersProfileResponseDTO.swift rename 14th-team5-iOS/Data/Sources/{Profile/ProfileAPI/ProfileAPIWorker.swift => Members/MemberAPI/MembersAPIWorker.swift} (80%) rename 14th-team5-iOS/Data/Sources/{Profile/ProfileAPI/ProfileAPIs.swift => Members/MemberAPI/MembersAPIs.swift} (93%) rename 14th-team5-iOS/Data/Sources/{Profile/Repositories/ProfileViewRepository.swift => Members/Repositories/MembersRepository.swift} (58%) delete mode 100644 14th-team5-iOS/Data/Sources/Profile/DataMapping/ProfileMemberDTO.swift rename 14th-team5-iOS/Domain/Sources/{Profile/Entity/ProfileMemberResponse.swift => Members/Entities/MemberProfileResponse.swift} (55%) create mode 100644 14th-team5-iOS/Domain/Sources/Members/Interfaces/MembersRepositoryProtocol.swift rename 14th-team5-iOS/Domain/Sources/{Profile => Members}/Parameters/ProfileImageEditParameter.swift (100%) create mode 100644 14th-team5-iOS/Domain/Sources/Members/UseCases/MembersUseCase.swift rename 14th-team5-iOS/Domain/Sources/{Profile => Members}/UseCases/ProfileFeedUseCase.swift (100%) delete mode 100644 14th-team5-iOS/Domain/Sources/Profile/Interfaces/ProfileViewInterface.swift delete mode 100644 14th-team5-iOS/Domain/Sources/Profile/UseCases/ProfileViewUseCase.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraViewReactor.swift index eff42b78a..2d3d06640 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraViewReactor.swift @@ -41,7 +41,7 @@ public final class CameraViewReactor: Reactor { case setPinchZoomScale(CGFloat) case setZoomScale(CGFloat) case setProfileImageURLResponse(CameraDisplayImageResponse?) - case setProfileMemberResponse(ProfileMemberResponse?) + case setProfileMemberResponse(MembersProfileResponse?) case setRealEmojiImageURLResponse(CameraRealEmojiPreSignedResponse?) case setRealEmojiImageCreateResponse(CameraCreateRealEmojiResponse?) case setRealEmojiItems([CameraRealEmojiImageItemResponse?]) @@ -73,7 +73,7 @@ public final class CameraViewReactor: Reactor { var memberId: String var isUpload: Bool @Pulse var isError: Bool - @Pulse var profileMemberEntity: ProfileMemberResponse? + @Pulse var profileMemberEntity: MembersProfileResponse? } init(cameraUseCase: CameraViewUseCaseProtocol, diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/Dependency/ProfileDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/Dependency/ProfileDIContainer.swift index a8af9c31e..b6fed08af 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/Dependency/ProfileDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/Dependency/ProfileDIContainer.swift @@ -14,9 +14,9 @@ import Domain public final class ProfileDIContainer: BaseDIContainer { public typealias ViewContrller = ProfileViewController - public typealias Repository = ProfileViewInterface + public typealias Repository = MembersRepositoryProtocol public typealias Reactor = ProfileViewReactor - public typealias UseCase = ProfileViewUsecaseProtocol + public typealias UseCase = MembersUseCaseProtocol private var globalState: GlobalStateProviderProtocol { guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { @@ -38,15 +38,15 @@ public final class ProfileDIContainer: BaseDIContainer { } public func makeUseCase() -> UseCase { - return ProfileViewUseCase(profileViewRepository: makeRepository()) + return MembersUseCase(membersRepository: makeRepository()) } public func makeRepository() -> Repository { - return ProfileViewRepository() + return MembersRepository() } public func makeReactor() -> ProfileViewReactor { - return ProfileViewReactor(profileUseCase: makeUseCase(), provider: globalState, memberId: memberId, isUser: isUser) + return ProfileViewReactor(membersUseCase: makeUseCase(), provider: globalState, memberId: memberId, isUser: isUser) } diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift index ff778c6da..fdcbf8a71 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift @@ -14,7 +14,7 @@ import ReactorKit public final class ProfileViewReactor: Reactor { public var initialState: State - private let profileUseCase: ProfileViewUsecaseProtocol + private let membersUseCase: MembersUseCaseProtocol private let memberId: String private let isUser: Bool private let provider: GlobalStateProviderProtocol @@ -32,7 +32,7 @@ public final class ProfileViewReactor: Reactor { public enum Mutation { case setLoading(Bool) case setProfilePresingedURL(CameraDisplayImageResponse?) - case setProfileMemberItems(ProfileMemberResponse?) + case setProfileMemberItems(MembersProfileResponse?) case setProfileFeedType(BibbiFeedType) } @@ -41,12 +41,12 @@ public final class ProfileViewReactor: Reactor { var memberId: String var isUser: Bool var feedType: BibbiFeedType - @Pulse var profileMemberEntity: ProfileMemberResponse? + @Pulse var profileMemberEntity: MembersProfileResponse? @Pulse var profilePresingedURLEntity: CameraDisplayImageResponse? } - init(profileUseCase: ProfileViewUsecaseProtocol, provider: GlobalStateProviderProtocol, memberId: String, isUser: Bool) { - self.profileUseCase = profileUseCase + init(membersUseCase: MembersUseCaseProtocol, provider: GlobalStateProviderProtocol, memberId: String, isUser: Bool) { + self.membersUseCase = membersUseCase self.memberId = memberId self.isUser = isUser self.initialState = State( @@ -77,7 +77,7 @@ public final class ProfileViewReactor: Reactor { //TODO: Keychain, UserDefaults 추가 switch action { case .viewDidLoad: - return profileUseCase.executeProfileMemberItems(memberId: currentState.memberId) + return membersUseCase.executeProfileMemberItems(memberId: currentState.memberId) .asObservable() .flatMap { entity -> Observable in .just(.setProfileMemberItems(entity)) @@ -87,20 +87,20 @@ public final class ProfileViewReactor: Reactor { let nickNameImageEditParameter: CameraDisplayImageParameters = CameraDisplayImageParameters(imageName: nickNameProfileImage) return .concat( .just(.setLoading(false)), - profileUseCase.executeProfileImageURLCreate(parameter: nickNameImageEditParameter) + membersUseCase.executeProfileImageURLCreate(parameter: nickNameImageEditParameter) .withUnretained(self) .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) .asObservable() .flatMap { owner, entity -> Observable in guard let profilePresingedURL = entity?.imageURL else { return .empty() } - return owner.profileUseCase.executeProfileImageToPresingedUpload(to: profilePresingedURL, data: nickNameFileData) + return owner.membersUseCase.executeProfileImageToPresingedUpload(to: profilePresingedURL, data: nickNameFileData) .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) .asObservable() .flatMap { isSuccess -> Observable in let originalPath = owner.configureProfileOriginalS3URL(url: profilePresingedURL) let profileEditParameter: ProfileImageEditParameter = ProfileImageEditParameter(profileImageUrl: originalPath) if isSuccess { - return owner.profileUseCase.executeReloadProfileImage(memberId: self.currentState.memberId, parameter: profileEditParameter) + return owner.membersUseCase.executeReloadProfileImage(memberId: self.currentState.memberId, parameter: profileEditParameter) .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) .asObservable() .flatMap { memberEntity -> Observable in @@ -119,7 +119,7 @@ public final class ProfileViewReactor: Reactor { }) case .viewWillAppear: - return profileUseCase.executeProfileMemberItems(memberId: currentState.memberId) + return membersUseCase.executeProfileMemberItems(memberId: currentState.memberId) .asObservable() .withUnretained(self) .flatMap { owner ,entity -> Observable in @@ -135,20 +135,20 @@ public final class ProfileViewReactor: Reactor { let profileImageEditParameter: CameraDisplayImageParameters = CameraDisplayImageParameters(imageName: profileImage) return .concat( .just(.setLoading(false)), - profileUseCase.executeProfileImageURLCreate(parameter: profileImageEditParameter) + membersUseCase.executeProfileImageURLCreate(parameter: profileImageEditParameter) .withUnretained(self) .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) .asObservable() .flatMap { owner, entity -> Observable in guard let profilePresingedURL = entity?.imageURL else { return .empty() } - return owner.profileUseCase.executeProfileImageToPresingedUpload(to: profilePresingedURL, data: fileData) + return owner.membersUseCase.executeProfileImageToPresingedUpload(to: profilePresingedURL, data: fileData) .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) .asObservable() .flatMap { isSuccess -> Observable in let originalPath = owner.configureProfileOriginalS3URL(url: profilePresingedURL) let profileEditParameter: ProfileImageEditParameter = ProfileImageEditParameter(profileImageUrl: originalPath) if isSuccess { - return owner.profileUseCase.executeReloadProfileImage(memberId: self.currentState.memberId, parameter: profileEditParameter) + return owner.membersUseCase.executeReloadProfileImage(memberId: self.currentState.memberId, parameter: profileEditParameter) .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) .asObservable() .flatMap { memberEntity -> Observable in @@ -169,7 +169,7 @@ public final class ProfileViewReactor: Reactor { ) case .didTapInitProfile: - return profileUseCase.executeDeleteProfileImage(memberId: memberId) + return membersUseCase.executeDeleteProfileImage(memberId: memberId) .asObservable() .flatMap { entity -> Observable in return .concat( diff --git a/14th-team5-iOS/Data/Sources/Account/AccountRepository/AccountRepository.swift b/14th-team5-iOS/Data/Sources/Account/AccountRepository/AccountRepository.swift index 430a46f85..e1bfb6a1d 100644 --- a/14th-team5-iOS/Data/Sources/Account/AccountRepository/AccountRepository.swift +++ b/14th-team5-iOS/Data/Sources/Account/AccountRepository/AccountRepository.swift @@ -37,7 +37,7 @@ public final class AccountRepository: AccountImpl { let signInHelper = AccountSignInHelper() private let apiWorker = AccountAPIWorker() - private let profileWorker = ProfileAPIWorker() + private let profileWorker = MembersAPIWorker() private let meApiWorekr = MeAPIWorker() private let fetchMemberInfo = PublishRelay() diff --git a/14th-team5-iOS/Data/Sources/Camera/CameraAPI/CameraAPIWorker.swift b/14th-team5-iOS/Data/Sources/Camera/CameraAPI/CameraAPIWorker.swift index d8e4b3e4d..79f0add24 100644 --- a/14th-team5-iOS/Data/Sources/Camera/CameraAPI/CameraAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Camera/CameraAPI/CameraAPIWorker.swift @@ -66,7 +66,7 @@ extension CameraAPIWorker { .asSingle() } - public func editProfileImageToS3(accessToken: String, memberId: String, parameters: Encodable) -> Single { + public func editProfileImageToS3(accessToken: String, memberId: String, parameters: Encodable) -> Single { let spec = CameraAPIs.editProfileImage(memberId).spec return request(spec: spec, headers: [BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.xAuthToken(accessToken)], jsonEncodable: parameters) .subscribe(on: Self.queue) @@ -75,7 +75,7 @@ extension CameraAPIWorker { debugPrint("editProfile Image Upload Result: \(str)") } } - .map(ProfileMemberDTO.self) + .map(MembersProfileResponseDTO.self) .catchAndReturn(nil) .asSingle() } diff --git a/14th-team5-iOS/Data/Sources/Camera/Repositories/CameraViewRepository.swift b/14th-team5-iOS/Data/Sources/Camera/Repositories/CameraViewRepository.swift index 2eb3d112b..823bb56df 100644 --- a/14th-team5-iOS/Data/Sources/Camera/Repositories/CameraViewRepository.swift +++ b/14th-team5-iOS/Data/Sources/Camera/Repositories/CameraViewRepository.swift @@ -60,10 +60,10 @@ extension CameraViewRepository: CameraViewInterface { .asObservable() } - public func editProfleImageToS3(memberId: String, parameter: ProfileImageEditParameter) -> Observable { + public func editProfleImageToS3(memberId: String, parameter: ProfileImageEditParameter) -> Observable { return cameraAPIWorker.editProfileImageToS3(accessToken: accessToken, memberId: memberId, parameters: parameter) .do { - guard let userEntity = $0?.toProfileDomain() else { return } + guard let userEntity = $0?.toProfileEntity() else { return } FamilyUserDefaults.saveMemberToUserDefaults(familyMember: userEntity) } .compactMap { $0?.toDomain() } diff --git a/14th-team5-iOS/Data/Sources/Members/DataMapping/MembersProfileResponseDTO.swift b/14th-team5-iOS/Data/Sources/Members/DataMapping/MembersProfileResponseDTO.swift new file mode 100644 index 000000000..dd71cb004 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/Members/DataMapping/MembersProfileResponseDTO.swift @@ -0,0 +1,43 @@ +// +// MemberProfileResponseDTO.swift +// Data +// +// Created by Kim dohyun on 6/3/24. +// + +import Foundation + +import Domain + + +struct MembersProfileResponseDTO: Decodable { + let memberId: String + let name: String + let imageUrl: String + let dayOfBirth: String + let familyJoinAt: String +} + + +extension MembersProfileResponseDTO { + //MARK: 프로필 정보 Entity + func toDomain() -> MembersProfileResponse { + return .init( + memberId: memberId, + memberName: name, + memberImage: URL(string: imageUrl) ?? URL(fileURLWithPath: ""), + dayOfBirth: dayOfBirth.toDate(), + familyJoinAt: familyJoinAt.toDate(with: "yyyy-MM-dd").realativeFormatterYYMM() + ) + } + + //MARK: 프로빌 정보 공유 데이터 Entity + func toProfileEntity() -> ProfileData { + return .init( + memberId: memberId, + profileImageURL: imageUrl, + name: name, + dayOfBirth: dayOfBirth.toDate() + ) + } +} diff --git a/14th-team5-iOS/Data/Sources/Profile/ProfileAPI/ProfileAPIWorker.swift b/14th-team5-iOS/Data/Sources/Members/MemberAPI/MembersAPIWorker.swift similarity index 80% rename from 14th-team5-iOS/Data/Sources/Profile/ProfileAPI/ProfileAPIWorker.swift rename to 14th-team5-iOS/Data/Sources/Members/MemberAPI/MembersAPIWorker.swift index 25d5a2f93..fe6659c85 100644 --- a/14th-team5-iOS/Data/Sources/Profile/ProfileAPI/ProfileAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Members/MemberAPI/MembersAPIWorker.swift @@ -1,8 +1,8 @@ // -// ProfileAPIWorker.swift +// MembersAPIWorker.swift // Data // -// Created by Kim dohyun on 12/25/23. +// Created by Kim dohyun on 6/5/24. // import Foundation @@ -12,9 +12,9 @@ import Domain import RxSwift -typealias ProfileAPIWorker = ProfileAPIs.Worker +typealias MembersAPIWorker = MembersAPIs.Worker -extension ProfileAPIs { +extension MembersAPIs { final class Worker: APIWorker { static let queue = { @@ -30,11 +30,10 @@ extension ProfileAPIs { } -extension ProfileAPIWorker { +extension MembersAPIWorker { - - public func fetchProfileMember(accessToken: String, memberId: String) -> Single { - let spec = ProfileAPIs.profileMember(memberId).spec + public func fetchProfileMember(accessToken: String, memberId: String) -> Single { + let spec = MembersAPIs.profileMember(memberId).spec return request(spec: spec, headers: [BibbiAPI.Header.xAppVersion, BibbiAPI.Header.xUserPlatform, BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.xAuthToken(accessToken)]) .subscribe(on: Self.queue) @@ -43,14 +42,14 @@ extension ProfileAPIWorker { debugPrint("fetch Profile Member Result: \(str)") } } - .map(ProfileMemberDTO.self) + .map(MembersProfileResponseDTO.self) .catchAndReturn(nil) .asSingle() } public func createProfileImagePresingedURL(accessToken: String, parameters: Encodable) -> Single { - let spec = ProfileAPIs.profileAlbumUploadImageURL.spec + let spec = MembersAPIs.profileAlbumUploadImageURL.spec return request(spec: spec, headers: [BibbiAPI.Header.xAppVersion, BibbiAPI.Header.xUserPlatform, BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.xAuthToken(accessToken)], jsonEncodable: parameters) .subscribe(on: Self.queue) @@ -65,7 +64,7 @@ extension ProfileAPIWorker { } public func uploadToProfilePresingedURL(accessToken: String, toURL url: String, with imageData: Data) -> Single { - let spec = ProfileAPIs.profileUploadToPreSignedURL(url).spec + let spec = MembersAPIs.profileUploadToPreSignedURL(url).spec return upload(spec: spec, headers: [BibbiAPI.Header.xAppVersion, BibbiAPI.Header.xUserPlatform, BibbiAPI.Header.xAppKey, BibbiAPI.Header.xAuthToken(accessToken)], image: imageData) .subscribe(on: Self.queue) @@ -74,8 +73,8 @@ extension ProfileAPIWorker { .map { _ in true } } - public func updateProfileAlbumImageToS3(accessToken: String, memberId: String, parameter: Encodable) -> Single { - let spec = ProfileAPIs.profileEditImage(memberId).spec + public func updateProfileAlbumImageToS3(accessToken: String, memberId: String, parameter: Encodable) -> Single { + let spec = MembersAPIs.profileEditImage(memberId).spec return request(spec: spec, headers: [BibbiAPI.Header.xAppVersion, BibbiAPI.Header.xUserPlatform, BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.xAuthToken(accessToken)], jsonEncodable: parameter) .subscribe(on: Self.queue) @@ -84,13 +83,13 @@ extension ProfileAPIWorker { debugPrint("updateProfile Image Result: \(str)") } } - .map(ProfileMemberDTO.self) + .map(MembersProfileResponseDTO.self) .catchAndReturn(nil) .asSingle() } - public func deleteProfileImageToS3(accessToken: String, memberId: String) -> Single { - let spec = ProfileAPIs.profileDeleteImage(memberId).spec + public func deleteProfileImageToS3(accessToken: String, memberId: String) -> Single { + let spec = MembersAPIs.profileDeleteImage(memberId).spec return request(spec: spec,headers: [BibbiAPI.Header.xAppVersion, BibbiAPI.Header.xUserPlatform, BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson]) .subscribe(on: Self.queue) @@ -99,7 +98,7 @@ extension ProfileAPIWorker { debugPrint("delete Profile Image Result: \(str)") } } - .map(ProfileMemberDTO.self) + .map(MembersProfileResponseDTO.self) .catchAndReturn(nil) .asSingle() } diff --git a/14th-team5-iOS/Data/Sources/Profile/ProfileAPI/ProfileAPIs.swift b/14th-team5-iOS/Data/Sources/Members/MemberAPI/MembersAPIs.swift similarity index 93% rename from 14th-team5-iOS/Data/Sources/Profile/ProfileAPI/ProfileAPIs.swift rename to 14th-team5-iOS/Data/Sources/Members/MemberAPI/MembersAPIs.swift index 0735af7e8..7e90bf073 100644 --- a/14th-team5-iOS/Data/Sources/Profile/ProfileAPI/ProfileAPIs.swift +++ b/14th-team5-iOS/Data/Sources/Members/MemberAPI/MembersAPIs.swift @@ -1,16 +1,15 @@ // -// ProfileAPIs.swift +// MembersAPIs.swift // Data // -// Created by Kim dohyun on 12/25/23. +// Created by Kim dohyun on 6/5/24. // import Foundation import Core - -enum ProfileAPIs: API { +enum MembersAPIs: API { case profileMember(String) case profilePost case profileAlbumUploadImageURL @@ -38,3 +37,4 @@ enum ProfileAPIs: API { } + diff --git a/14th-team5-iOS/Data/Sources/Profile/Repositories/ProfileViewRepository.swift b/14th-team5-iOS/Data/Sources/Members/Repositories/MembersRepository.swift similarity index 58% rename from 14th-team5-iOS/Data/Sources/Profile/Repositories/ProfileViewRepository.swift rename to 14th-team5-iOS/Data/Sources/Members/Repositories/MembersRepository.swift index 29f745cfe..f37ad58bf 100644 --- a/14th-team5-iOS/Data/Sources/Profile/Repositories/ProfileViewRepository.swift +++ b/14th-team5-iOS/Data/Sources/Members/Repositories/MembersRepository.swift @@ -1,8 +1,8 @@ // -// ProfileViewRepository.swift +// MembersRepository.swift // Data // -// Created by Kim dohyun on 12/25/23. +// Created by Kim dohyun on 6/5/24. // import Foundation @@ -13,57 +13,57 @@ import RxSwift import RxCocoa - - - -public final class ProfileViewRepository { +public final class MembersRepository { public var disposeBag: DisposeBag = DisposeBag() - private let profileAPIWorker: ProfileAPIWorker = ProfileAPIWorker() + private let membersAPIWorker: MembersAPIWorker = MembersAPIWorker() private let accessToken: String = App.Repository.token.accessToken.value?.accessToken ?? "" public init() { } } -extension ProfileViewRepository: ProfileViewInterface { +extension MembersRepository: MembersRepositoryProtocol { - public func fetchProfileMemberItems(memberId: String) -> Observable { - return profileAPIWorker.fetchProfileMember(accessToken: accessToken, memberId: memberId) - .compactMap { $0?.toDomain() } + public func fetchProfileMemberItems(memberId: String) -> Observable { + return membersAPIWorker.fetchProfileMember(accessToken: accessToken, memberId: memberId) + .map { $0?.toDomain() } + .catchAndReturn(nil) .asObservable() } public func fetchProfileAlbumImageURL(parameter: CameraDisplayImageParameters) -> Observable { - return profileAPIWorker.createProfileImagePresingedURL(accessToken: accessToken, parameters: parameter) + return membersAPIWorker.createProfileImagePresingedURL(accessToken: accessToken, parameters: parameter) .compactMap { $0?.toDomain() } .asObservable() } public func uploadProfileImageToPresingedURL(to url: String, imageData: Data) -> Observable { - return profileAPIWorker.uploadToProfilePresingedURL(accessToken: accessToken, toURL: url, with: imageData) + return membersAPIWorker.uploadToProfilePresingedURL(accessToken: accessToken, toURL: url, with: imageData) .asObservable() } - public func updataProfileImageToS3(memberId: String, parameter: ProfileImageEditParameter) -> Observable { - return profileAPIWorker.updateProfileAlbumImageToS3(accessToken: accessToken, memberId: memberId, parameter: parameter) + public func updataProfileImageToS3(memberId: String, parameter: ProfileImageEditParameter) -> Observable { + return membersAPIWorker.updateProfileAlbumImageToS3(accessToken: accessToken, memberId: memberId, parameter: parameter) .do { - guard let userEntity = $0?.toProfileDomain() else { return } + guard let userEntity = $0?.toProfileEntity() else { return } FamilyUserDefaults.saveMemberToUserDefaults(familyMember: userEntity) } - .compactMap { $0?.toDomain() } + .map { $0?.toDomain() } + .catchAndReturn(nil) .asObservable() } - public func deleteProfileImageToS3(memberId: String) -> Observable { - return profileAPIWorker.deleteProfileImageToS3(accessToken: accessToken, memberId: memberId) + public func deleteProfileImageToS3(memberId: String) -> Observable { + return membersAPIWorker.deleteProfileImageToS3(accessToken: accessToken, memberId: memberId) .do { - guard let userEntity = $0?.toProfileDomain() else { return } + guard let userEntity = $0?.toProfileEntity() else { return } FamilyUserDefaults.saveMemberToUserDefaults(familyMember: userEntity) } - .compactMap { $0?.toDomain() } + .map { $0?.toDomain() } + .catchAndReturn(nil) .asObservable() } diff --git a/14th-team5-iOS/Data/Sources/Profile/DataMapping/ProfileMemberDTO.swift b/14th-team5-iOS/Data/Sources/Profile/DataMapping/ProfileMemberDTO.swift deleted file mode 100644 index 59959f460..000000000 --- a/14th-team5-iOS/Data/Sources/Profile/DataMapping/ProfileMemberDTO.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// ProfileMemberDTO.swift -// Domain -// -// Created by Kim dohyun on 12/25/23. -// - -import Foundation - -import Core -import Domain - -public struct ProfileMemberDTO: Decodable { - public var memberId: String? - public var name: String? - public var imageUrl: String? - public var dayOfBirth: String - public var familyJoinAt: String -} - -extension ProfileMemberDTO { - public func toDomain() -> ProfileMemberResponse { - - - return .init( - memberId: memberId ?? "" , - memberName: name ?? "", - memberImage: URL(string: imageUrl ?? "") ?? URL(fileURLWithPath: ""), - dayOfBirth: dayOfBirth.toDate(), - familyJoinAt: familyJoinAt.toDate(with: "yyyy-MM-dd").realativeFormatterYYMM() - ) - } - - public func toProfileDomain() -> ProfileData { - return .init( - memberId: memberId ?? "", - profileImageURL: imageUrl ?? "", - name: name ?? "", - dayOfBirth: dayOfBirth.toDate() - ) - } - -} diff --git a/14th-team5-iOS/Domain/Sources/Camera/Interfaces/CameraViewInterface.swift b/14th-team5-iOS/Domain/Sources/Camera/Interfaces/CameraViewInterface.swift index 8a424ac8f..483db38f0 100644 --- a/14th-team5-iOS/Domain/Sources/Camera/Interfaces/CameraViewInterface.swift +++ b/14th-team5-iOS/Domain/Sources/Camera/Interfaces/CameraViewInterface.swift @@ -19,7 +19,7 @@ public protocol CameraViewInterface: AnyObject { func toggleCameraFlash(_ isState: Bool) -> Observable func fetchProfileImageURL(parameters: CameraDisplayImageParameters) -> Observable func uploadImageToS3(toURL url: String, imageData: Data) -> Observable - func editProfleImageToS3(memberId: String, parameter: ProfileImageEditParameter) -> Observable + func editProfleImageToS3(memberId: String, parameter: ProfileImageEditParameter) -> Observable func fetchRealEmojiImageURL(memberId: String, parameters: CameraRealEmojiParameters) -> Observable func uploadRealEmojiImageToS3(memberId: String, parameters: CameraCreateRealEmojiParameters) -> Observable func fetchRealEmojiItems(memberId: String) -> Observable<[CameraRealEmojiImageItemResponse?]> diff --git a/14th-team5-iOS/Domain/Sources/Camera/UseCases/CameraViewUseCase.swift b/14th-team5-iOS/Domain/Sources/Camera/UseCases/CameraViewUseCase.swift index 622507f2e..e126c4275 100644 --- a/14th-team5-iOS/Domain/Sources/Camera/UseCases/CameraViewUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/Camera/UseCases/CameraViewUseCase.swift @@ -16,7 +16,7 @@ public protocol CameraViewUseCaseProtocol { func executeToggleCameraFlash(_ isState: Bool) -> Observable func executeProfileImageURL(parameter: CameraDisplayImageParameters) -> Observable func executeUploadToS3(toURL url: String, imageData: Data) -> Observable - func executeEditProfileImage(memberId: String, parameter: ProfileImageEditParameter) -> Observable + func executeEditProfileImage(memberId: String, parameter: ProfileImageEditParameter) -> Observable func executeRealEmojiImageURL(memberId: String, parameter: CameraRealEmojiParameters) -> Observable func executeRealEmojiUploadToS3(memberId: String, parameter: CameraCreateRealEmojiParameters) -> Observable func executeRealEmojiItems(memberId: String) -> Observable<[CameraRealEmojiImageItemResponse?]> @@ -50,7 +50,7 @@ public final class CameraViewUseCase: CameraViewUseCaseProtocol { return cameraViewRepository.uploadImageToS3(toURL: url, imageData: imageData) } - public func executeEditProfileImage(memberId: String, parameter: ProfileImageEditParameter) -> Observable { + public func executeEditProfileImage(memberId: String, parameter: ProfileImageEditParameter) -> Observable { return cameraViewRepository.editProfleImageToS3(memberId: memberId, parameter: parameter) } diff --git a/14th-team5-iOS/Domain/Sources/Profile/Entity/ProfileMemberResponse.swift b/14th-team5-iOS/Domain/Sources/Members/Entities/MemberProfileResponse.swift similarity index 55% rename from 14th-team5-iOS/Domain/Sources/Profile/Entity/ProfileMemberResponse.swift rename to 14th-team5-iOS/Domain/Sources/Members/Entities/MemberProfileResponse.swift index cac9347dc..876a9a22c 100644 --- a/14th-team5-iOS/Domain/Sources/Profile/Entity/ProfileMemberResponse.swift +++ b/14th-team5-iOS/Domain/Sources/Members/Entities/MemberProfileResponse.swift @@ -1,21 +1,21 @@ // -// ProfileMemberResponse.swift +// MemberProfileResponse.swift // Domain // -// Created by Kim dohyun on 12/25/23. +// Created by Kim dohyun on 6/3/24. // import Foundation -public struct ProfileMemberResponse { - - public var memberId: String - public var memberName: String - public var memberImage: URL - public var dayOfBirth: Date - public var familyJoinAt: String +public struct MembersProfileResponse { + public let memberId: String + public let memberName: String + public let memberImage: URL + public let dayOfBirth: Date + public let familyJoinAt: String + public init(memberId: String, memberName: String, memberImage: URL, dayOfBirth: Date, familyJoinAt: String) { self.memberId = memberId self.memberName = memberName diff --git a/14th-team5-iOS/Domain/Sources/Members/Interfaces/MembersRepositoryProtocol.swift b/14th-team5-iOS/Domain/Sources/Members/Interfaces/MembersRepositoryProtocol.swift new file mode 100644 index 000000000..c43a0e93b --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Members/Interfaces/MembersRepositoryProtocol.swift @@ -0,0 +1,22 @@ +// +// MembersRepositoryProtocol.swift +// Domain +// +// Created by Kim dohyun on 6/5/24. +// + +import Foundation + +import RxCocoa +import RxSwift + + +public protocol MembersRepositoryProtocol { + var disposeBag: DisposeBag { get } + + func fetchProfileMemberItems(memberId: String) -> Observable + func fetchProfileAlbumImageURL(parameter: CameraDisplayImageParameters) -> Observable + func uploadProfileImageToPresingedURL(to url: String, imageData: Data) -> Observable + func updataProfileImageToS3(memberId: String, parameter: ProfileImageEditParameter) -> Observable + func deleteProfileImageToS3(memberId: String) -> Observable +} diff --git a/14th-team5-iOS/Domain/Sources/Profile/Parameters/ProfileImageEditParameter.swift b/14th-team5-iOS/Domain/Sources/Members/Parameters/ProfileImageEditParameter.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Profile/Parameters/ProfileImageEditParameter.swift rename to 14th-team5-iOS/Domain/Sources/Members/Parameters/ProfileImageEditParameter.swift diff --git a/14th-team5-iOS/Domain/Sources/Members/UseCases/MembersUseCase.swift b/14th-team5-iOS/Domain/Sources/Members/UseCases/MembersUseCase.swift new file mode 100644 index 000000000..84b6c836d --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Members/UseCases/MembersUseCase.swift @@ -0,0 +1,51 @@ +// +// MembersUseCase.swift +// Domain +// +// Created by Kim dohyun on 6/5/24. +// + +import Foundation + +import RxCocoa +import RxSwift + + +public protocol MembersUseCaseProtocol { + func executeProfileMemberItems(memberId: String) -> Observable + func executeProfileImageURLCreate(parameter: CameraDisplayImageParameters) -> Observable + func executeProfileImageToPresingedUpload(to url: String, data: Data) -> Observable + func executeReloadProfileImage(memberId: String, parameter: ProfileImageEditParameter) -> Observable + func executeDeleteProfileImage(memberId: String) -> Observable +} + +public final class MembersUseCase: MembersUseCaseProtocol { + private let membersRepository: MembersRepositoryProtocol + + public init(membersRepository: MembersRepositoryProtocol) { + self.membersRepository = membersRepository + } + + public func executeProfileMemberItems(memberId: String) -> Observable { + return membersRepository.fetchProfileMemberItems(memberId: memberId) + .asObservable() + } + + public func executeProfileImageURLCreate(parameter: CameraDisplayImageParameters) -> Observable { + return membersRepository.fetchProfileAlbumImageURL(parameter: parameter) + } + + + public func executeProfileImageToPresingedUpload(to url: String, data: Data) -> Observable { + return membersRepository.uploadProfileImageToPresingedURL(to: url, imageData: data) + } + + public func executeReloadProfileImage(memberId: String, parameter: ProfileImageEditParameter) -> Observable { + return membersRepository.updataProfileImageToS3(memberId: memberId, parameter: parameter) + } + + public func executeDeleteProfileImage(memberId: String) -> Observable { + return membersRepository.deleteProfileImageToS3(memberId: memberId) + } + +} diff --git a/14th-team5-iOS/Domain/Sources/Profile/UseCases/ProfileFeedUseCase.swift b/14th-team5-iOS/Domain/Sources/Members/UseCases/ProfileFeedUseCase.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Profile/UseCases/ProfileFeedUseCase.swift rename to 14th-team5-iOS/Domain/Sources/Members/UseCases/ProfileFeedUseCase.swift diff --git a/14th-team5-iOS/Domain/Sources/Profile/Interfaces/ProfileViewInterface.swift b/14th-team5-iOS/Domain/Sources/Profile/Interfaces/ProfileViewInterface.swift deleted file mode 100644 index ee7b0608a..000000000 --- a/14th-team5-iOS/Domain/Sources/Profile/Interfaces/ProfileViewInterface.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// ProfileViewInterface.swift -// Domain -// -// Created by Kim dohyun on 12/25/23. -// - -import Foundation - -import RxCocoa -import RxSwift - -public struct FeedEntity { - public var imageURL: String - public var descrption: String - public var subTitle: String - - public init(imageURL: String, descrption: String, subTitle: String) { - self.imageURL = imageURL - self.descrption = descrption - self.subTitle = subTitle - } -} - - - -public protocol ProfileViewInterface: AnyObject { - var disposeBag: DisposeBag { get } - - func fetchProfileMemberItems(memberId: String) -> Observable - func fetchProfileAlbumImageURL(parameter: CameraDisplayImageParameters) -> Observable - func uploadProfileImageToPresingedURL(to url: String, imageData: Data) -> Observable - func updataProfileImageToS3(memberId: String, parameter: ProfileImageEditParameter) -> Observable - func deleteProfileImageToS3(memberId: String) -> Observable -} diff --git a/14th-team5-iOS/Domain/Sources/Profile/UseCases/ProfileViewUseCase.swift b/14th-team5-iOS/Domain/Sources/Profile/UseCases/ProfileViewUseCase.swift deleted file mode 100644 index 34e3aba83..000000000 --- a/14th-team5-iOS/Domain/Sources/Profile/UseCases/ProfileViewUseCase.swift +++ /dev/null @@ -1,51 +0,0 @@ -// -// ProfileViewUseCase.swift -// Data -// -// Created by Kim dohyun on 12/25/23. -// - -import Foundation - -import RxCocoa -import RxSwift - -public protocol ProfileViewUsecaseProtocol { - func executeProfileMemberItems(memberId: String) -> Observable - func executeProfileImageURLCreate(parameter: CameraDisplayImageParameters) -> Observable - func executeProfileImageToPresingedUpload(to url: String, data: Data) -> Observable - func executeReloadProfileImage(memberId: String, parameter: ProfileImageEditParameter) -> Observable - func executeDeleteProfileImage(memberId: String) -> Observable -} - - -public final class ProfileViewUseCase: ProfileViewUsecaseProtocol { - private let profileViewRepository: ProfileViewInterface - - public init(profileViewRepository: ProfileViewInterface) { - self.profileViewRepository = profileViewRepository - } - - public func executeProfileMemberItems(memberId: String) -> Observable { - return profileViewRepository.fetchProfileMemberItems(memberId: memberId) - } - - public func executeProfileImageURLCreate(parameter: CameraDisplayImageParameters) -> Observable { - return profileViewRepository.fetchProfileAlbumImageURL(parameter: parameter) - } - - - public func executeProfileImageToPresingedUpload(to url: String, data: Data) -> Observable { - return profileViewRepository.uploadProfileImageToPresingedURL(to: url, imageData: data) - } - - public func executeReloadProfileImage(memberId: String, parameter: ProfileImageEditParameter) -> Observable { - return profileViewRepository.updataProfileImageToS3(memberId: memberId, parameter: parameter) - } - - public func executeDeleteProfileImage(memberId: String) -> Observable { - return profileViewRepository.deleteProfileImageToS3(memberId: memberId) - } - - -} diff --git a/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme b/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme index 5f307e6b4..7023994af 100644 --- a/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme +++ b/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme @@ -280,52 +280,10 @@ buildForAnalyzing = "YES"> - - - - - - - - - - - - + BlueprintIdentifier = "4160F70E94342FB432F5ABF1" + BuildableName = "GoogleAppMeasurementTarget.framework" + BlueprintName = "GoogleAppMeasurementTarget" + ReferencedContainer = "container:Tuist/Dependencies/SwiftPackageManager/.build/checkouts/GoogleAppMeasurement/GoogleAppMeasurement.xcodeproj"> + BlueprintIdentifier = "08D12528663D0C8DD1A99BCB" + BuildableName = "GoogleDataTransport.framework" + BlueprintName = "GoogleDataTransport" + ReferencedContainer = "container:Tuist/Dependencies/SwiftPackageManager/.build/checkouts/GoogleDataTransport/GoogleDataTransport.xcodeproj"> @@ -468,6 +426,34 @@ ReferencedContainer = "container:Tuist/Dependencies/SwiftPackageManager/.build/checkouts/GoogleUtilities/GoogleUtilities.xcodeproj"> + + + + + + + + + + + + Date: Wed, 5 Jun 2024 16:16:38 +0900 Subject: [PATCH 076/263] =?UTF-8?q?refactor:=20Domain=20Entities=20?= =?UTF-8?q?=EB=84=A4=EC=9D=B4=EB=B0=8D=20MissionContentResponse,=20TodayMi?= =?UTF-8?q?ssionResponse=EB=A1=9C=20=EC=88=98=EC=A0=95=20-=20GetTodayMissi?= =?UTF-8?q?onUseCase=20=ED=8C=8C=EC=9D=BC=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PostDetail/Reactor/PostReactor.swift | 4 +- .../MissionContentDTO.swift | 2 +- .../TodayMissionDTO.swift | 2 +- .../Mission/MissionAPI/MissionAPIWorker.swift | 8 +- .../Repository/MissionRepository.swift | 4 +- ...ata.swift => MissionContentResponse.swift} | 8 +- ...nData.swift => TodayMissionResponse.swift} | 6 +- .../GetTodayMissionUseCaseProtocol.swift | 2 +- .../MissionRepositoryProtocol.swift | 4 +- .../UseCases/GetTodayMissionUseCase.swift | 22 ---- .../UseCases/MissionContentUseCase.swift | 4 +- .../xcschemes/Bibbi-Workspace.xcscheme | 106 +++++++++--------- 12 files changed, 74 insertions(+), 98 deletions(-) rename 14th-team5-iOS/Data/Sources/Mission/{DTO => DataMapping}/MissionContentDTO.swift (91%) rename 14th-team5-iOS/Data/Sources/Mission/{DTO => DataMapping}/TodayMissionDTO.swift (86%) rename 14th-team5-iOS/Domain/Sources/Mission/Entities/{MissionContentData.swift => MissionContentResponse.swift} (70%) rename 14th-team5-iOS/Domain/Sources/Mission/Entities/{TodayMissionData.swift => TodayMissionResponse.swift} (65%) delete mode 100644 14th-team5-iOS/Domain/Sources/Mission/UseCases/GetTodayMissionUseCase.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostReactor.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostReactor.swift index 8c099a3ff..aa3a70da2 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostReactor.swift @@ -21,7 +21,7 @@ final class PostReactor: Reactor { enum Mutation { case setPop case setSelectedPostIndex(Int) - case setMissionContent(MissionContentData) + case setMissionContent(MissionContentResponse) case setPushProfileViewController(String) } @@ -32,7 +32,7 @@ final class PostReactor: Reactor { var isPop: Bool = false var selectedPost: PostListData = .init(postId: "", author: .init(memberId: "", profileImageURL: "", name: ""), commentCount: 0, emojiCount: 0, imageURL: "", content: "", time: "") - @Pulse var missionContent: MissionContentData? = nil + @Pulse var missionContent: MissionContentResponse? = nil @Pulse var fetchedPost: PostData? = nil @Pulse var reactionMemberIds: [String] = [] @Pulse var shouldPushProfileViewController: String? diff --git a/14th-team5-iOS/Data/Sources/Mission/DTO/MissionContentDTO.swift b/14th-team5-iOS/Data/Sources/Mission/DataMapping/MissionContentDTO.swift similarity index 91% rename from 14th-team5-iOS/Data/Sources/Mission/DTO/MissionContentDTO.swift rename to 14th-team5-iOS/Data/Sources/Mission/DataMapping/MissionContentDTO.swift index 466fa8b27..2db8216e1 100644 --- a/14th-team5-iOS/Data/Sources/Mission/DTO/MissionContentDTO.swift +++ b/14th-team5-iOS/Data/Sources/Mission/DataMapping/MissionContentDTO.swift @@ -23,7 +23,7 @@ struct MissionContentDTO: Decodable { extension MissionContentDTO { - func toDomain() -> MissionContentData { + func toDomain() -> MissionContentResponse { return .init( missionId: missionId, missionContent: missionContent diff --git a/14th-team5-iOS/Data/Sources/Mission/DTO/TodayMissionDTO.swift b/14th-team5-iOS/Data/Sources/Mission/DataMapping/TodayMissionDTO.swift similarity index 86% rename from 14th-team5-iOS/Data/Sources/Mission/DTO/TodayMissionDTO.swift rename to 14th-team5-iOS/Data/Sources/Mission/DataMapping/TodayMissionDTO.swift index d2724889a..62d83f758 100644 --- a/14th-team5-iOS/Data/Sources/Mission/DTO/TodayMissionDTO.swift +++ b/14th-team5-iOS/Data/Sources/Mission/DataMapping/TodayMissionDTO.swift @@ -14,7 +14,7 @@ struct GetTodayMissionResponse: Codable { let id: String let content: String - func toDomain() -> TodayMissionData { + func toDomain() -> TodayMissionResponse { return .init(id: id, content: content) } } diff --git a/14th-team5-iOS/Data/Sources/Mission/MissionAPI/MissionAPIWorker.swift b/14th-team5-iOS/Data/Sources/Mission/MissionAPI/MissionAPIWorker.swift index b2c5317db..698ef8d3f 100644 --- a/14th-team5-iOS/Data/Sources/Mission/MissionAPI/MissionAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Mission/MissionAPI/MissionAPIWorker.swift @@ -27,7 +27,7 @@ extension MissionAPIs { } extension MissionAPIWorker { - private func getTodayMission(headers: [APIHeader]?) -> Single { + private func getTodayMission(headers: [APIHeader]?) -> Single { let spec = MissionAPIs.getTodayMission.spec return request(spec: spec, headers: headers) @@ -43,7 +43,7 @@ extension MissionAPIWorker { .asSingle() } - func getTodayMission() -> Single { + func getTodayMission() -> Single { return Observable.just(()) .withLatestFrom(self._headers) .observe(on: Self.queue) @@ -53,7 +53,7 @@ extension MissionAPIWorker { } - private func getMissionContent(spec: APISpec, headers: [APIHeader]?) -> Single { + private func getMissionContent(spec: APISpec, headers: [APIHeader]?) -> Single { return request(spec: spec, headers: headers) .subscribe(on: Self.queue) .do { @@ -68,7 +68,7 @@ extension MissionAPIWorker { } - func getMissionContent(missionId: String) -> Single { + func getMissionContent(missionId: String) -> Single { let spec = MissionAPIs.getMissionContent(missionId).spec return Observable.just(()) diff --git a/14th-team5-iOS/Data/Sources/Mission/Repository/MissionRepository.swift b/14th-team5-iOS/Data/Sources/Mission/Repository/MissionRepository.swift index b2536c0f2..f4eabce61 100644 --- a/14th-team5-iOS/Data/Sources/Mission/Repository/MissionRepository.swift +++ b/14th-team5-iOS/Data/Sources/Mission/Repository/MissionRepository.swift @@ -21,12 +21,12 @@ public final class MissionRepository: MissionRepositoryProtocol { } extension MissionRepository { - public func getTodayMission() -> Observable { + public func getTodayMission() -> Observable { return missionAPIWorker.getTodayMission() .asObservable() } - public func getMissionContent(missionId: String) -> Observable { + public func getMissionContent(missionId: String) -> Observable { return missionAPIWorker.getMissionContent(missionId: missionId) .asObservable() } diff --git a/14th-team5-iOS/Domain/Sources/Mission/Entities/MissionContentData.swift b/14th-team5-iOS/Domain/Sources/Mission/Entities/MissionContentResponse.swift similarity index 70% rename from 14th-team5-iOS/Domain/Sources/Mission/Entities/MissionContentData.swift rename to 14th-team5-iOS/Domain/Sources/Mission/Entities/MissionContentResponse.swift index 9daae3748..25ef66092 100644 --- a/14th-team5-iOS/Domain/Sources/Mission/Entities/MissionContentData.swift +++ b/14th-team5-iOS/Domain/Sources/Mission/Entities/MissionContentResponse.swift @@ -1,14 +1,13 @@ // -// MissionContentData.swift +// MissionContentResponse.swift // Domain // -// Created by Kim dohyun on 5/8/24. +// Created by Kim dohyun on 6/5/24. // import Foundation - -public struct MissionContentData { +public struct MissionContentResponse { public let missionId: String public let missionContent: String @@ -16,5 +15,4 @@ public struct MissionContentData { self.missionId = missionId self.missionContent = missionContent } - } diff --git a/14th-team5-iOS/Domain/Sources/Mission/Entities/TodayMissionData.swift b/14th-team5-iOS/Domain/Sources/Mission/Entities/TodayMissionResponse.swift similarity index 65% rename from 14th-team5-iOS/Domain/Sources/Mission/Entities/TodayMissionData.swift rename to 14th-team5-iOS/Domain/Sources/Mission/Entities/TodayMissionResponse.swift index 07ca54b4b..9dad60c2b 100644 --- a/14th-team5-iOS/Domain/Sources/Mission/Entities/TodayMissionData.swift +++ b/14th-team5-iOS/Domain/Sources/Mission/Entities/TodayMissionResponse.swift @@ -1,13 +1,13 @@ // -// MissionData.swift +// TodayMissionResponse.swift // Domain // -// Created by 마경미 on 21.04.24. +// Created by Kim dohyun on 6/5/24. // import Foundation -public struct TodayMissionData { +public struct TodayMissionResponse { let id: String public let content: String diff --git a/14th-team5-iOS/Domain/Sources/Mission/Interfaces/GetTodayMissionUseCaseProtocol.swift b/14th-team5-iOS/Domain/Sources/Mission/Interfaces/GetTodayMissionUseCaseProtocol.swift index 3adb28a63..d66ee1bbb 100644 --- a/14th-team5-iOS/Domain/Sources/Mission/Interfaces/GetTodayMissionUseCaseProtocol.swift +++ b/14th-team5-iOS/Domain/Sources/Mission/Interfaces/GetTodayMissionUseCaseProtocol.swift @@ -10,5 +10,5 @@ import Foundation import RxSwift public protocol GetTodayMissionUseCaseProtocol { - func execute() -> Observable + func execute() -> Observable } diff --git a/14th-team5-iOS/Domain/Sources/Mission/Interfaces/MissionRepositoryProtocol.swift b/14th-team5-iOS/Domain/Sources/Mission/Interfaces/MissionRepositoryProtocol.swift index a26081d31..751780dda 100644 --- a/14th-team5-iOS/Domain/Sources/Mission/Interfaces/MissionRepositoryProtocol.swift +++ b/14th-team5-iOS/Domain/Sources/Mission/Interfaces/MissionRepositoryProtocol.swift @@ -10,6 +10,6 @@ import Foundation import RxSwift public protocol MissionRepositoryProtocol { - func getTodayMission() -> Observable - func getMissionContent(missionId: String) -> Observable + func getTodayMission() -> Observable + func getMissionContent(missionId: String) -> Observable } diff --git a/14th-team5-iOS/Domain/Sources/Mission/UseCases/GetTodayMissionUseCase.swift b/14th-team5-iOS/Domain/Sources/Mission/UseCases/GetTodayMissionUseCase.swift deleted file mode 100644 index 224e2cd56..000000000 --- a/14th-team5-iOS/Domain/Sources/Mission/UseCases/GetTodayMissionUseCase.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// GetTodayMissionUseCase.swift -// Domain -// -// Created by 마경미 on 21.04.24. -// - -import Foundation - -import RxSwift - -//public final class GetTodayMissionUseCase: GetTodayMissionUseCaseProtocol { -// private let missionRepository: MissionRepositoryProtocol -// -// public init(missionRepository: MissionRepositoryProtocol) { -// self.missionRepository = missionRepository -// } -// -// public func execute() -> Observable { -// return missionRepository.getTodayMission() -// } -//} diff --git a/14th-team5-iOS/Domain/Sources/Mission/UseCases/MissionContentUseCase.swift b/14th-team5-iOS/Domain/Sources/Mission/UseCases/MissionContentUseCase.swift index 1c1d5578b..89a4bb8d8 100644 --- a/14th-team5-iOS/Domain/Sources/Mission/UseCases/MissionContentUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/Mission/UseCases/MissionContentUseCase.swift @@ -10,7 +10,7 @@ import Foundation import RxSwift public protocol MissionContentUseCaseProtocol { - func execute(missionId: String) -> Observable + func execute(missionId: String) -> Observable } @@ -22,7 +22,7 @@ public class MissionContentUseCase: MissionContentUseCaseProtocol { self.missionContentRepository = missionContentRepository } - public func execute(missionId: String) -> Observable { + public func execute(missionId: String) -> Observable { return missionContentRepository.getMissionContent(missionId: missionId) .compactMap { $0 } .asObservable() diff --git a/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme b/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme index 5f307e6b4..7023994af 100644 --- a/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme +++ b/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme @@ -280,52 +280,10 @@ buildForAnalyzing = "YES"> - - - - - - - - - - - - + BlueprintIdentifier = "4160F70E94342FB432F5ABF1" + BuildableName = "GoogleAppMeasurementTarget.framework" + BlueprintName = "GoogleAppMeasurementTarget" + ReferencedContainer = "container:Tuist/Dependencies/SwiftPackageManager/.build/checkouts/GoogleAppMeasurement/GoogleAppMeasurement.xcodeproj"> + BlueprintIdentifier = "08D12528663D0C8DD1A99BCB" + BuildableName = "GoogleDataTransport.framework" + BlueprintName = "GoogleDataTransport" + ReferencedContainer = "container:Tuist/Dependencies/SwiftPackageManager/.build/checkouts/GoogleDataTransport/GoogleDataTransport.xcodeproj"> @@ -468,6 +426,34 @@ ReferencedContainer = "container:Tuist/Dependencies/SwiftPackageManager/.build/checkouts/GoogleUtilities/GoogleUtilities.xcodeproj"> + + + + + + + + + + + + Date: Wed, 5 Jun 2024 16:42:25 +0900 Subject: [PATCH 077/263] =?UTF-8?q?refactor:=20Calendar=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20UseCase=20=EC=BD=94=EB=93=9C=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81=20(#538)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DIContainer/CalendarDIContainer.swift | 53 +++++++++++++++++-- .../Dependency/CommentDIContainer.swift | 6 +-- .../Sources/BBNetwork/{API => }/API.swift | 0 .../Core/Sources/BBNetwork/API/File.swift | 8 --- .../BBNetwork/{API => }/APIConfig.swift | 0 .../Sources/BBNetwork/{API => }/APISpec.swift | 0 ...pository.swift => CommentRepository.swift} | 14 ++--- .../Calendar/UseCases/CalendarUseCase.swift | 10 ++-- .../UseCases/FetchCalendarBannerUseCase.swift | 31 +++++++++++ .../UseCases/FetchDailyCalendarUseCase.swift | 31 +++++++++++ .../FetchMonthlyCalendarUseCase.swift | 31 +++++++++++ .../FetchStatisticsSummaryUseCase.swift | 31 +++++++++++ ...pository.swift => CommentRepository.swift} | 2 +- .../UseCase/PostCommentUseCase.swift | 12 ++--- 14 files changed, 193 insertions(+), 36 deletions(-) rename 14th-team5-iOS/Core/Sources/BBNetwork/{API => }/API.swift (100%) delete mode 100644 14th-team5-iOS/Core/Sources/BBNetwork/API/File.swift rename 14th-team5-iOS/Core/Sources/BBNetwork/{API => }/APIConfig.swift (100%) rename 14th-team5-iOS/Core/Sources/BBNetwork/{API => }/APISpec.swift (100%) rename 14th-team5-iOS/Data/Sources/APIs/Comment/Repository/{PostCommentRepository.swift => CommentRepository.swift} (66%) create mode 100644 14th-team5-iOS/Domain/Sources/Calendar/UseCases/FetchCalendarBannerUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/Calendar/UseCases/FetchDailyCalendarUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/Calendar/UseCases/FetchMonthlyCalendarUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/Calendar/UseCases/FetchStatisticsSummaryUseCase.swift rename 14th-team5-iOS/Domain/Sources/PostComment/Interfaces/Repositories/{PostCommentRepository.swift => CommentRepository.swift} (93%) diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/CalendarDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/CalendarDIContainer.swift index f51ad8ea8..915510338 100644 --- a/14th-team5-iOS/App/Sources/Application/DIContainer/CalendarDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/CalendarDIContainer.swift @@ -18,13 +18,39 @@ final class CalendarDIContainer: DIContainer { // MARK: - Make UseCase + func makeFetchCalendarBannerUseCase() -> FetchCalendarBannerUseCaseProtocol { + FetchCalendarBannerUseCase( + calendarRepository: makeCalendarRepository() + ) + } + + func makeFetchStatisticsSummaryUseCase() -> FetchStatisticsSummaryUseCaseProtocol { + FetchStatisticsSummaryUseCase( + calendarRepository: makeCalendarRepository() + ) + } + + func makeFetchDailyCalendarUseCase() -> FetchDailyCalendarUseCaseProtocol { + FetchDailyCalendarUseCase( + calendarRepository: makeCalendarRepository() + ) + } + + func makeFetchMonthlyCalendarUseCase() -> FetchMonthlyCalendarUseCaseProtocol { + FetchMonthlyCalendarUseCase( + calendarRepository: makeCalendarRepository() + ) + } + + // NOTE: - 추후 UseCase 리팩토링하면 make() 메서드가 더 많아지겠죠? + + func makeUseCase() -> CalendarUseCaseProtocol { return CalendarUseCase( calendarRepository: makeCalendarRepository() ) } - // NOTE: - 추후 UseCase 리팩토링하면 make() 메서드가 더 많아지겠죠? // MARK: - Make Repository @@ -47,11 +73,30 @@ final class CalendarDIContainer: DIContainer { // NOTE: - 등록하면 @Injected로 편하게 의존성을 받아올 수 있습니다. // - (MonthlyCalendarViewReactor.swift 참조) - container.register( - type: CalendarUseCaseProtocol.self - ) { [unowned self] _ in + + container.register(type: FetchCalendarBannerUseCaseProtocol.self) { [unowned self] _ in + self.makeFetchCalendarBannerUseCase() + } + + container.register(type: FetchStatisticsSummaryUseCaseProtocol.self) { [unowned self] _ in + self.makeFetchStatisticsSummaryUseCase() + } + + container.register(type: FetchDailyCalendarUseCaseProtocol.self) { [unowned self] _ in + self.makeFetchDailyCalendarUseCase() + } + + container.register(type: FetchMonthlyCalendarUseCaseProtocol.self) { [unowned self] _ in + self.makeFetchMonthlyCalendarUseCase() + } + + + // ... + + container.register(type: CalendarUseCaseProtocol.self) { [unowned self] _ in self.makeUseCase() } + } diff --git a/14th-team5-iOS/App/Sources/Presentation/PostComment/Dependency/CommentDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/PostComment/Dependency/CommentDIContainer.swift index 2859e53a3..9c934b6d1 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostComment/Dependency/CommentDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostComment/Dependency/CommentDIContainer.swift @@ -35,8 +35,8 @@ public final class CommentDIContainer { return MemberRepository() } - public func makePostCommentRespository() -> PostCommentRepositoryProtocol { - return PostCommentRepository() + public func makePostCommentRespository() -> CommentRepositoryProtocol { + return CommentRepository() } public func makeMemberUseCase() -> MemberUseCaseProtocol { @@ -44,7 +44,7 @@ public final class CommentDIContainer { } public func makePostCommentUseCase() -> PostCommentUseCaseProtocol { - return PostCommentUseCase(postCommentRepository: makePostCommentRespository()) + return PostCommentUseCase(commentRepository: makePostCommentRespository()) } public func makeReactor() -> CommentViewReactor { diff --git a/14th-team5-iOS/Core/Sources/BBNetwork/API/API.swift b/14th-team5-iOS/Core/Sources/BBNetwork/API.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBNetwork/API/API.swift rename to 14th-team5-iOS/Core/Sources/BBNetwork/API.swift diff --git a/14th-team5-iOS/Core/Sources/BBNetwork/API/File.swift b/14th-team5-iOS/Core/Sources/BBNetwork/API/File.swift deleted file mode 100644 index d449a83e9..000000000 --- a/14th-team5-iOS/Core/Sources/BBNetwork/API/File.swift +++ /dev/null @@ -1,8 +0,0 @@ -// -// File.swift -// Core -// -// Created by 김건우 on 6/3/24. -// - -import Foundation diff --git a/14th-team5-iOS/Core/Sources/BBNetwork/API/APIConfig.swift b/14th-team5-iOS/Core/Sources/BBNetwork/APIConfig.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBNetwork/API/APIConfig.swift rename to 14th-team5-iOS/Core/Sources/BBNetwork/APIConfig.swift diff --git a/14th-team5-iOS/Core/Sources/BBNetwork/API/APISpec.swift b/14th-team5-iOS/Core/Sources/BBNetwork/APISpec.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBNetwork/API/APISpec.swift rename to 14th-team5-iOS/Core/Sources/BBNetwork/APISpec.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/Comment/Repository/PostCommentRepository.swift b/14th-team5-iOS/Data/Sources/APIs/Comment/Repository/CommentRepository.swift similarity index 66% rename from 14th-team5-iOS/Data/Sources/APIs/Comment/Repository/PostCommentRepository.swift rename to 14th-team5-iOS/Data/Sources/APIs/Comment/Repository/CommentRepository.swift index f799104d3..e42acc84f 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Comment/Repository/PostCommentRepository.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Comment/Repository/CommentRepository.swift @@ -10,34 +10,34 @@ import Foundation import RxSwift -public final class PostCommentRepository: PostCommentRepositoryProtocol { +public final class CommentRepository: CommentRepositoryProtocol { public let disposeBag: DisposeBag = DisposeBag() - private let postCommentApiWorker: CommentAPIWorker = CommentAPIWorker() + private let commentApiWorker: CommentAPIWorker = CommentAPIWorker() public init() { } } -extension PostCommentRepository { +extension CommentRepository { public func fetchPostComment(postId: String, query: PostCommentPaginationQuery) -> Observable { - return postCommentApiWorker.fetchComment(postId: postId, query: query) + return commentApiWorker.fetchComment(postId: postId, query: query) .asObservable() } public func createPostComment(postId: String, body: CreatePostCommentRequest) -> Observable { let body = CreatePostCommentReqeustDTO(content: body.content) - return postCommentApiWorker.createComment(postId: postId, body: body) + return commentApiWorker.createComment(postId: postId, body: body) .asObservable() } public func updatePostComment(postId: String, commentId: String, body: UpdatePostCommentRequest) -> Observable { let body = UpdatePostCommentReqeustDTO(content: body.content) - return postCommentApiWorker.updateComment(postId: postId, commentId: commentId, body: body) + return commentApiWorker.updateComment(postId: postId, commentId: commentId, body: body) .asObservable() } public func deletePostComment(postId: String, commentId: String) -> Observable { - return postCommentApiWorker.deleteComment(postId: postId, commentId: commentId) + return commentApiWorker.deleteComment(postId: postId, commentId: commentId) .asObservable() } } diff --git a/14th-team5-iOS/Domain/Sources/Calendar/UseCases/CalendarUseCase.swift b/14th-team5-iOS/Domain/Sources/Calendar/UseCases/CalendarUseCase.swift index 838714dca..c1dc424a6 100644 --- a/14th-team5-iOS/Domain/Sources/Calendar/UseCases/CalendarUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/Calendar/UseCases/CalendarUseCase.swift @@ -10,6 +10,7 @@ import Foundation import RxSwift +@available(*, deprecated) public protocol CalendarUseCaseProtocol { func executeFetchCalednarResponse(yearMonth: String) -> Observable func executeFetchDailyCalendarResponse(yearMonthDay: String) -> Observable @@ -17,6 +18,8 @@ public protocol CalendarUseCaseProtocol { func executeFetchCalendarBenner(yearMonth: String) -> Observable } + +@available(*, deprecated) public final class CalendarUseCase: CalendarUseCaseProtocol { private let calendarRepository: CalendarRepositoryProtocol @@ -40,10 +43,3 @@ public final class CalendarUseCase: CalendarUseCaseProtocol { return calendarRepository.fetchCalendarBanner(yearMonth: yearMonth) } } - - -public extension InjectIdentifier { - static var calendarUseCase: InjectIdentifier { - .by(type: CalendarUseCaseProtocol.self) - } -} diff --git a/14th-team5-iOS/Domain/Sources/Calendar/UseCases/FetchCalendarBannerUseCase.swift b/14th-team5-iOS/Domain/Sources/Calendar/UseCases/FetchCalendarBannerUseCase.swift new file mode 100644 index 000000000..0b7edf197 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Calendar/UseCases/FetchCalendarBannerUseCase.swift @@ -0,0 +1,31 @@ +// +// FetchCalendarBannerUseCase.swift +// Domain +// +// Created by 김건우 on 6/5/24. +// + +import Foundation + +import RxSwift + +public protocol FetchCalendarBannerUseCaseProtocol { + func execute(yearMonth: String) -> Observable +} + +public class FetchCalendarBannerUseCase: FetchCalendarBannerUseCaseProtocol { + + // MARK: - Repositories + let calendarRepository: CalendarRepositoryProtocol + + // MARK: - Intitlalizer + public init(calendarRepository: CalendarRepositoryProtocol) { + self.calendarRepository = calendarRepository + } + + // MARK: - Execute + + public func execute(yearMonth: String) -> Observable { + calendarRepository.fetchCalendarBanner(yearMonth: yearMonth) + } +} diff --git a/14th-team5-iOS/Domain/Sources/Calendar/UseCases/FetchDailyCalendarUseCase.swift b/14th-team5-iOS/Domain/Sources/Calendar/UseCases/FetchDailyCalendarUseCase.swift new file mode 100644 index 000000000..98080d055 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Calendar/UseCases/FetchDailyCalendarUseCase.swift @@ -0,0 +1,31 @@ +// +// FetchDailyCalendarUseCase.swift +// Domain +// +// Created by 김건우 on 6/5/24. +// + +import Foundation + +import RxSwift + +public protocol FetchDailyCalendarUseCaseProtocol { + func execute(yearMonthDay: String) -> Observable +} + +public class FetchDailyCalendarUseCase: FetchDailyCalendarUseCaseProtocol { + + // MARK: - Repositories + let calendarRepository: CalendarRepositoryProtocol + + // MARK: - Intializer + public init(calendarRepository: CalendarRepositoryProtocol) { + self.calendarRepository = calendarRepository + } + + // MARK: - Execute + public func execute(yearMonthDay: String) -> Observable { + calendarRepository.fetchDailyCalendarResponse(yearMonthDay: yearMonthDay) + } + +} diff --git a/14th-team5-iOS/Domain/Sources/Calendar/UseCases/FetchMonthlyCalendarUseCase.swift b/14th-team5-iOS/Domain/Sources/Calendar/UseCases/FetchMonthlyCalendarUseCase.swift new file mode 100644 index 000000000..b55a1f1ee --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Calendar/UseCases/FetchMonthlyCalendarUseCase.swift @@ -0,0 +1,31 @@ +// +// FetchMonthlyCalendarInfoUseCase.swift +// Domain +// +// Created by 김건우 on 6/5/24. +// + +import Foundation + +import RxSwift + +public protocol FetchMonthlyCalendarUseCaseProtocol { + func execute(yearMonth: String) -> Observable +} + +public class FetchMonthlyCalendarUseCase: FetchMonthlyCalendarUseCaseProtocol { + + // MARK: - Repositories + let calendarRepository: CalendarRepositoryProtocol + + // MARK: - Intitlalizer + public init(calendarRepository: CalendarRepositoryProtocol) { + self.calendarRepository = calendarRepository + } + + // MARK: - Execute + public func execute(yearMonth: String) -> Observable { + calendarRepository.fetchMonthyCalendarResponse(yearMonth: yearMonth) + } + +} diff --git a/14th-team5-iOS/Domain/Sources/Calendar/UseCases/FetchStatisticsSummaryUseCase.swift b/14th-team5-iOS/Domain/Sources/Calendar/UseCases/FetchStatisticsSummaryUseCase.swift new file mode 100644 index 000000000..62948f4a6 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Calendar/UseCases/FetchStatisticsSummaryUseCase.swift @@ -0,0 +1,31 @@ +// +// FetchStatisticsSummaryUseCase.swift +// Domain +// +// Created by 김건우 on 6/5/24. +// + +import Foundation + +import RxSwift + +public protocol FetchStatisticsSummaryUseCaseProtocol { + func execute(yearMonth: String) -> Observable +} + + +public class FetchStatisticsSummaryUseCase: FetchStatisticsSummaryUseCaseProtocol { + + // MARK: - Repositories + let calendarRepository: CalendarRepositoryProtocol + + // MARK: - Intializer + public init(calendarRepository: CalendarRepositoryProtocol) { + self.calendarRepository = calendarRepository + } + + // MARK: - Execute + public func execute(yearMonth: String) -> Observable { + calendarRepository.fetchStatisticsSummary(yearMonth: yearMonth) + } +} diff --git a/14th-team5-iOS/Domain/Sources/PostComment/Interfaces/Repositories/PostCommentRepository.swift b/14th-team5-iOS/Domain/Sources/PostComment/Interfaces/Repositories/CommentRepository.swift similarity index 93% rename from 14th-team5-iOS/Domain/Sources/PostComment/Interfaces/Repositories/PostCommentRepository.swift rename to 14th-team5-iOS/Domain/Sources/PostComment/Interfaces/Repositories/CommentRepository.swift index b08ca03a4..ee6751f07 100644 --- a/14th-team5-iOS/Domain/Sources/PostComment/Interfaces/Repositories/PostCommentRepository.swift +++ b/14th-team5-iOS/Domain/Sources/PostComment/Interfaces/Repositories/CommentRepository.swift @@ -9,7 +9,7 @@ import Foundation import RxSwift -public protocol PostCommentRepositoryProtocol { +public protocol CommentRepositoryProtocol { var disposeBag: DisposeBag { get } func fetchPostComment(postId: String, query: PostCommentPaginationQuery) -> Observable diff --git a/14th-team5-iOS/Domain/Sources/PostComment/UseCase/PostCommentUseCase.swift b/14th-team5-iOS/Domain/Sources/PostComment/UseCase/PostCommentUseCase.swift index 4d07071e2..1c98dd8f6 100644 --- a/14th-team5-iOS/Domain/Sources/PostComment/UseCase/PostCommentUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/PostComment/UseCase/PostCommentUseCase.swift @@ -18,24 +18,24 @@ public protocol PostCommentUseCaseProtocol { } public final class PostCommentUseCase: PostCommentUseCaseProtocol { - private let postCommentRepository: PostCommentRepositoryProtocol + private let commentRepository: CommentRepositoryProtocol public init( - postCommentRepository: PostCommentRepositoryProtocol + commentRepository: CommentRepositoryProtocol ) { - self.postCommentRepository = postCommentRepository + self.commentRepository = commentRepository } public func executeFetchPostComment(postId: String, query: PostCommentPaginationQuery) -> Observable { - return postCommentRepository.fetchPostComment(postId: postId, query: query) + return commentRepository.fetchPostComment(postId: postId, query: query) } public func executeCreatePostComment(postId: String, body: CreatePostCommentRequest) -> Observable { - return postCommentRepository.createPostComment(postId: postId, body: body) + return commentRepository.createPostComment(postId: postId, body: body) } public func executeDeletePostComment(postId: String, commentId: String) -> Observable { - return postCommentRepository.deletePostComment(postId: postId, commentId: commentId) + return commentRepository.deletePostComment(postId: postId, commentId: commentId) } } From 9c814ce4564a9a4945db451d231cbd28eda16152 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Wed, 5 Jun 2024 20:09:32 +0900 Subject: [PATCH 078/263] =?UTF-8?q?feat:=20ReactorViewController=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#546)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Base/ReactorViewController.swift | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 14th-team5-iOS/Core/Sources/Base/ReactorViewController.swift diff --git a/14th-team5-iOS/Core/Sources/Base/ReactorViewController.swift b/14th-team5-iOS/Core/Sources/Base/ReactorViewController.swift new file mode 100644 index 000000000..206887f69 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Base/ReactorViewController.swift @@ -0,0 +1,52 @@ +// +// ReactorViewController.swift +// Core +// +// Created by 김건우 on 6/5/24. +// + +import DesignSystem +import UIKit + +import ReactorKit +import RxSwift + +open class ReactorViewController: UIViewController, ReactorKit.View where R: Reactor { + + // MARK: - Typealias + public typealias Reactor = R + + // MARK: - Properties + public var disposeBag: RxSwift.DisposeBag = DisposeBag() + + // MARK: - Intializer + public init() { + super.init(nibName: nil, bundle: nil) + } + + public convenience init(reactor: Reactor? = nil) { + self.init() + self.reactor = reactor + } + + required public init?(coder: NSCoder) { + super.init(coder: coder) + } + + // MARK: - Lifecycles + override open func viewDidLoad() { + super.viewDidLoad() + setupUI() + setupAutoLayout() + setupAttributes() + } + + // MARK: - Helpers + open func bind(reactor: R) { } + + open func setupUI() { } + + open func setupAutoLayout() { } + + open func setupAttributes() { } +} From 3041deaa5299f1decc5664f099592f3539fa4f60 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Wed, 5 Jun 2024 20:23:11 +0900 Subject: [PATCH 079/263] =?UTF-8?q?feat:=20BBNavigationBarView=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#546)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BibbiAlertAction.swift | 0 .../BibbiAlertBuilder.swift | 0 .../BibbiAlertStyle.swift | 0 .../BibbiAlertTitle.swift | 0 .../BibbiAlertViewController.swift | 0 .../BibbiButton.swift | 0 .../{BibbiLabel => BBLabel}/BibbiLabel.swift | 0 .../BBNavigationBar+DelegateProxy.swift | 57 ++++ .../BBNavigationBarButtonStyle.swift | 67 +++++ .../BBNavigationBarView.swift | 251 ++++++++++++++++++ .../BibbiSegmentedControl.swift | 0 11 files changed, 375 insertions(+) rename 14th-team5-iOS/Core/Sources/BBCommons/{BibbiAlertBuilder => BBAlertBuilder}/BibbiAlertAction.swift (100%) rename 14th-team5-iOS/Core/Sources/BBCommons/{BibbiAlertBuilder => BBAlertBuilder}/BibbiAlertBuilder.swift (100%) rename 14th-team5-iOS/Core/Sources/BBCommons/{BibbiAlertBuilder => BBAlertBuilder}/BibbiAlertStyle.swift (100%) rename 14th-team5-iOS/Core/Sources/BBCommons/{BibbiAlertBuilder => BBAlertBuilder}/BibbiAlertTitle.swift (100%) rename 14th-team5-iOS/Core/Sources/BBCommons/{BibbiAlertBuilder => BBAlertBuilder}/BibbiAlertViewController.swift (100%) rename 14th-team5-iOS/Core/Sources/BBCommons/{BibbiButton => BBButton}/BibbiButton.swift (100%) rename 14th-team5-iOS/Core/Sources/BBCommons/{BibbiLabel => BBLabel}/BibbiLabel.swift (100%) create mode 100644 14th-team5-iOS/Core/Sources/BBCommons/BBNavigationBarView/BBNavigationBar+DelegateProxy.swift create mode 100644 14th-team5-iOS/Core/Sources/BBCommons/BBNavigationBarView/BBNavigationBarButtonStyle.swift create mode 100644 14th-team5-iOS/Core/Sources/BBCommons/BBNavigationBarView/BBNavigationBarView.swift rename 14th-team5-iOS/Core/Sources/BBCommons/{BibbiSegmentedControl => BBSegmentedControl}/BibbiSegmentedControl.swift (100%) diff --git a/14th-team5-iOS/Core/Sources/BBCommons/BibbiAlertBuilder/BibbiAlertAction.swift b/14th-team5-iOS/Core/Sources/BBCommons/BBAlertBuilder/BibbiAlertAction.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBCommons/BibbiAlertBuilder/BibbiAlertAction.swift rename to 14th-team5-iOS/Core/Sources/BBCommons/BBAlertBuilder/BibbiAlertAction.swift diff --git a/14th-team5-iOS/Core/Sources/BBCommons/BibbiAlertBuilder/BibbiAlertBuilder.swift b/14th-team5-iOS/Core/Sources/BBCommons/BBAlertBuilder/BibbiAlertBuilder.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBCommons/BibbiAlertBuilder/BibbiAlertBuilder.swift rename to 14th-team5-iOS/Core/Sources/BBCommons/BBAlertBuilder/BibbiAlertBuilder.swift diff --git a/14th-team5-iOS/Core/Sources/BBCommons/BibbiAlertBuilder/BibbiAlertStyle.swift b/14th-team5-iOS/Core/Sources/BBCommons/BBAlertBuilder/BibbiAlertStyle.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBCommons/BibbiAlertBuilder/BibbiAlertStyle.swift rename to 14th-team5-iOS/Core/Sources/BBCommons/BBAlertBuilder/BibbiAlertStyle.swift diff --git a/14th-team5-iOS/Core/Sources/BBCommons/BibbiAlertBuilder/BibbiAlertTitle.swift b/14th-team5-iOS/Core/Sources/BBCommons/BBAlertBuilder/BibbiAlertTitle.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBCommons/BibbiAlertBuilder/BibbiAlertTitle.swift rename to 14th-team5-iOS/Core/Sources/BBCommons/BBAlertBuilder/BibbiAlertTitle.swift diff --git a/14th-team5-iOS/Core/Sources/BBCommons/BibbiAlertBuilder/BibbiAlertViewController.swift b/14th-team5-iOS/Core/Sources/BBCommons/BBAlertBuilder/BibbiAlertViewController.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBCommons/BibbiAlertBuilder/BibbiAlertViewController.swift rename to 14th-team5-iOS/Core/Sources/BBCommons/BBAlertBuilder/BibbiAlertViewController.swift diff --git a/14th-team5-iOS/Core/Sources/BBCommons/BibbiButton/BibbiButton.swift b/14th-team5-iOS/Core/Sources/BBCommons/BBButton/BibbiButton.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBCommons/BibbiButton/BibbiButton.swift rename to 14th-team5-iOS/Core/Sources/BBCommons/BBButton/BibbiButton.swift diff --git a/14th-team5-iOS/Core/Sources/BBCommons/BibbiLabel/BibbiLabel.swift b/14th-team5-iOS/Core/Sources/BBCommons/BBLabel/BibbiLabel.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBCommons/BibbiLabel/BibbiLabel.swift rename to 14th-team5-iOS/Core/Sources/BBCommons/BBLabel/BibbiLabel.swift diff --git a/14th-team5-iOS/Core/Sources/BBCommons/BBNavigationBarView/BBNavigationBar+DelegateProxy.swift b/14th-team5-iOS/Core/Sources/BBCommons/BBNavigationBarView/BBNavigationBar+DelegateProxy.swift new file mode 100644 index 000000000..697f52f0f --- /dev/null +++ b/14th-team5-iOS/Core/Sources/BBCommons/BBNavigationBarView/BBNavigationBar+DelegateProxy.swift @@ -0,0 +1,57 @@ +// +// BBNavigationBarDelegateProxy.swift +// Core +// +// Created by 김건우 on 6/5/24. +// + +import UIKit + +import RxSwift +import RxCocoa + + +// MARK: - Delegate + +@objc public protocol BBNavigationBarViewDelegate { + @objc optional func navigationBarView(_ button: UIButton, didTapRightBarButton event: UIControl.Event) + @objc optional func navigationBarView(_ button: UIButton, didTapLeftBarButton event: UIControl.Event) +} + + +// MARK: - DelgateProxy + +final class RxBBNavigationBarViewDelegateProxy: DelegateProxy, DelegateProxyType, BBNavigationBarViewDelegate { + static func registerKnownImplementations() { + self.register { + RxBBNavigationBarViewDelegateProxy(parentObject: $0, delegateProxy: self) + } + } +} + +extension BBNavigationBarView: HasDelegate { + public typealias Delegate = BBNavigationBarViewDelegate +} + + +extension Reactive where Base: BBNavigationBarView { + public var delegate: DelegateProxy { + return RxBBNavigationBarViewDelegateProxy.proxy(for: self.base) + } + + public var didTapLeftBarButton: ControlEvent { + let source = delegate.sentMessage(#selector(BBNavigationBarViewDelegate.navigationBarView(_:didTapLeftBarButton:))) + .debug("navigationBarView(_:didTapLeftBarButton:) 메서드 호출 성공") + .map { $0[0] as! UIButton } + + return ControlEvent(events: source) + } + + public var didTapRightBarButton: ControlEvent { + let source = delegate.sentMessage(#selector(BBNavigationBarViewDelegate.navigationBarView(_:didTapRightBarButton:))) + .debug("navigationBarView(_:didTapRightBarButton:) 메서드 호출 성공") + .map { $0[0] as! UIButton } + + return ControlEvent(events: source) + } +} diff --git a/14th-team5-iOS/Core/Sources/BBCommons/BBNavigationBarView/BBNavigationBarButtonStyle.swift b/14th-team5-iOS/Core/Sources/BBCommons/BBNavigationBarView/BBNavigationBarButtonStyle.swift new file mode 100644 index 000000000..94a92f61c --- /dev/null +++ b/14th-team5-iOS/Core/Sources/BBCommons/BBNavigationBarView/BBNavigationBarButtonStyle.swift @@ -0,0 +1,67 @@ +// +// BBNavigationBarButtonStyle.swift +// Core +// +// Created by 김건우 on 6/5/24. +// + +import DesignSystem +import UIKit + + +// MARK: - Typealias + +public typealias TitleStyle = BBNavigationBarView.TitleStyle +public typealias TopBarButtonStyle = BBNavigationBarView.TopBarButtonStyle + + +// MARK: - Extensions + +public extension BBNavigationBarView { + + enum TitleStyle { + case bibbi + case newBibbi + } + + enum TopBarButtonStyle { + case addPerson + case arrowLeft + case heartCalendar + case setting + case xmark + } + +} + +extension TitleStyle { + + var image: UIImage? { + switch self { + case .bibbi: + return DesignSystemAsset.bibbiLogo.image + case .newBibbi: + return DesignSystemAsset.bibbiLogo.image + } + } + +} + +extension TopBarButtonStyle { + + var barButtonImage: UIImage? { + switch self { + case .addPerson: + return DesignSystemAsset.addPerson.image + case .arrowLeft: + return UIImage(systemName: "chevron.backward") + case .heartCalendar: + return DesignSystemAsset.heartCalendar.image + case .setting: + return DesignSystemAsset.setting.image + case .xmark: + return DesignSystemAsset.xmark.image + } + } + +} diff --git a/14th-team5-iOS/Core/Sources/BBCommons/BBNavigationBarView/BBNavigationBarView.swift b/14th-team5-iOS/Core/Sources/BBCommons/BBNavigationBarView/BBNavigationBarView.swift new file mode 100644 index 000000000..cb5c73e1a --- /dev/null +++ b/14th-team5-iOS/Core/Sources/BBCommons/BBNavigationBarView/BBNavigationBarView.swift @@ -0,0 +1,251 @@ +// +// BBNavigationBarView.swift +// Core +// +// Created by 김건우 on 6/5/24. +// + +import UIKit + +import DesignSystem +import Then +import SnapKit + +import RxSwift +import RxCocoa + +public final class BBNavigationBarView: UIView { + + // MARK: - Views + private let containerView: UIView = UIView() + + private let navigationTitleLabel: BibbiLabel = BibbiLabel(.head2Bold, textColor: .gray200) + private var navigationImageView: UIImageView = UIImageView() + + private let leftBarButton: UIButton = UIButton(type: .system) + private let rightBarButton: UIButton = UIButton(type: .system) + + // MARK: - Properties + public weak var delegate: BBNavigationBarViewDelegate? + + public var navigationTitle: String? { + didSet { + navigationImageView.isHidden = true + navigationTitleLabel.isHidden = false + + navigationTitleLabel.text = navigationTitle + } + } + + public var navigationImage: UIImage.TopBarImageType? { + didSet { + navigationImageView.isHidden = false + navigationTitleLabel.isHidden = true + + navigationImageView.image = navigationImage?.barImage + } + } + + public var leftBarButtonItem: UIImage.TopBarIconType? { + didSet { + leftBarButton.setImage( + leftBarButtonItem?.barButtonImage, + for: .normal + ) + setupButtonBackground(leftBarButton, type: leftBarButtonItem) + } + } + + public var rightBarButtonItem: UIImage.TopBarIconType? { + didSet { + rightBarButton.setImage( + rightBarButtonItem?.barButtonImage, + for: .normal + ) + setupButtonBackground(rightBarButton, type: leftBarButtonItem) + } + } + + public var navigationImageScale: CGFloat = 1.0 { + didSet { + setupNavigationImageScale(navigationImageScale) + } + } + + public var leftBarButtonItemScale: CGFloat = 1.0 { + didSet { + setupLeftButtonImageScale(leftBarButtonItemScale) + } + } + + public var rightBarButtonItemScale: CGFloat = 1.0 { + didSet { + setupRightButtonImageScale(rightBarButtonItemScale) + } + } + + public var navigationTitleTextColor: UIColor = UIColor.gray200 { + didSet { + navigationTitleLabel.textColor = navigationTitleTextColor + } + } + + public var leftBarButtonItemTintColor: UIColor = UIColor.gray300 { + didSet { + leftBarButton.tintColor = leftBarButtonItemTintColor + } + } + + public var rightBarButtonItemTintColor: UIColor = UIColor.gray300 { + didSet { + rightBarButton.tintColor = rightBarButtonItemTintColor + } + } + + public var leftBarButtonItemYOffset: CGFloat = 0.0 { + didSet { + leftBarButton.snp.updateConstraints { + $0.leading.equalTo(leftBarButtonItemYOffset) + } + } + } + + public var rightBarButtonItemYOffset: CGFloat = 0.0 { + didSet { + rightBarButton.snp.updateConstraints { + $0.trailing.equalTo(rightBarButtonItemYOffset) + } + rightBarButton.layoutIfNeeded() + } + } + + // MARK: - Intializer + public override init(frame: CGRect) { + super.init(frame: .zero) + setupUI() + setupAutolayout() + setupAttributes() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Helpers + func setupUI() { + addSubview(containerView) + containerView.addSubviews( + leftBarButton, navigationImageView, navigationTitleLabel, rightBarButton + ) + } + + func setupAutolayout() { + containerView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + + navigationImageView.snp.makeConstraints { + $0.center.equalToSuperview() + $0.width.equalTo(self.snp.width).multipliedBy(0.15) + $0.height.equalTo(self.snp.height) + } + + navigationTitleLabel.snp.makeConstraints { + $0.center.equalToSuperview() + } + + leftBarButton.snp.makeConstraints { + $0.leading.equalTo(0.0) + $0.centerY.equalTo(self.snp.centerY) + $0.width.height.equalTo(52.0) + } + + rightBarButton.snp.makeConstraints { + $0.trailing.equalTo(0.0) + $0.centerY.equalTo(self.snp.centerY) + $0.width.height.equalTo(52.0) + } + } + + func setupAttributes() { + containerView.do { + $0.backgroundColor = UIColor.clear + } + + navigationImageView.do { + $0.contentMode = .scaleAspectFit + } + + leftBarButton.do { + $0.layer.masksToBounds = true + $0.layer.cornerRadius = 10.0 + $0.tintColor = DesignSystemAsset.gray300.color + + $0.addTarget( + self, + action: #selector(didTapLeftButton), + for: .touchUpInside + ) + + } + + rightBarButton.do { + $0.layer.masksToBounds = true + $0.layer.cornerRadius = 10.0 + $0.tintColor = DesignSystemAsset.gray300.color + + $0.addTarget( + self, + action: #selector(didTapRightButton), + for: .touchUpInside + ) + } + + setupNavigationImageScale(navigationImageScale) + setupLeftButtonImageScale(leftBarButtonItemScale) + setupRightButtonImageScale(rightBarButtonItemScale) + } +} + + +// MARK: - Extensions + +extension BBNavigationBarView { + private func setupNavigationImageScale(_ scale: CGFloat) { + navigationImageView.layer.transform = CATransform3DMakeScale( + scale, scale, scale + ) + } + + private func setupLeftButtonImageScale(_ scale: CGFloat) { + leftBarButton.imageView?.layer.transform = CATransform3DMakeScale( + scale, scale, scale + ) + } + + private func setupRightButtonImageScale(_ scale: CGFloat) { + rightBarButton.imageView?.layer.transform = CATransform3DMakeScale( + scale, scale, scale + ) + } + + private func setupButtonBackground(_ button: UIButton, type: UIImage.TopBarIconType?) { + if type == .arrowLeft || type == .xmark { + button.backgroundColor = .gray900 + } else { + button.backgroundColor = .clear + } + } +} + +extension BBNavigationBarView { + @objc func didTapLeftButton(_ button: UIButton, event: UIButton.Event) { + guard let _ = button.currentImage else { return } + delegate?.navigationBarView?(button, didTapLeftBarButton: event) + } + + @objc func didTapRightButton(_ button: UIButton, event: UIButton.Event) { + guard let _ = button.currentImage else { return } + delegate?.navigationBarView?(button, didTapRightBarButton: event) + } +} diff --git a/14th-team5-iOS/Core/Sources/BBCommons/BibbiSegmentedControl/BibbiSegmentedControl.swift b/14th-team5-iOS/Core/Sources/BBCommons/BBSegmentedControl/BibbiSegmentedControl.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBCommons/BibbiSegmentedControl/BibbiSegmentedControl.swift rename to 14th-team5-iOS/Core/Sources/BBCommons/BBSegmentedControl/BibbiSegmentedControl.swift From 07222075d843e6a1b3a532d3069ab1f6ee924247 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Wed, 5 Jun 2024 21:08:36 +0900 Subject: [PATCH 080/263] =?UTF-8?q?feat:=20BBNavigationViewController=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#546)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BBNavigationBarButtonStyle.swift | 2 +- .../BBNavigationBarView.swift | 30 ++++- .../Base/BBNavigationViewController.swift | 125 ++++++++++++++++++ 3 files changed, 149 insertions(+), 8 deletions(-) create mode 100644 14th-team5-iOS/Core/Sources/Base/BBNavigationViewController.swift diff --git a/14th-team5-iOS/Core/Sources/BBCommons/BBNavigationBarView/BBNavigationBarButtonStyle.swift b/14th-team5-iOS/Core/Sources/BBCommons/BBNavigationBarView/BBNavigationBarButtonStyle.swift index 94a92f61c..644284261 100644 --- a/14th-team5-iOS/Core/Sources/BBCommons/BBNavigationBarView/BBNavigationBarButtonStyle.swift +++ b/14th-team5-iOS/Core/Sources/BBCommons/BBNavigationBarView/BBNavigationBarButtonStyle.swift @@ -49,7 +49,7 @@ extension TitleStyle { extension TopBarButtonStyle { - var barButtonImage: UIImage? { + var image: UIImage? { switch self { case .addPerson: return DesignSystemAsset.addPerson.image diff --git a/14th-team5-iOS/Core/Sources/BBCommons/BBNavigationBarView/BBNavigationBarView.swift b/14th-team5-iOS/Core/Sources/BBCommons/BBNavigationBarView/BBNavigationBarView.swift index cb5c73e1a..1b535783a 100644 --- a/14th-team5-iOS/Core/Sources/BBCommons/BBNavigationBarView/BBNavigationBarView.swift +++ b/14th-team5-iOS/Core/Sources/BBCommons/BBNavigationBarView/BBNavigationBarView.swift @@ -14,6 +14,7 @@ import SnapKit import RxSwift import RxCocoa +///. 삐삐 스타일의 NavigationBar가 구현된 View입니다. public final class BBNavigationBarView: UIView { // MARK: - Views @@ -28,6 +29,9 @@ public final class BBNavigationBarView: UIView { // MARK: - Properties public weak var delegate: BBNavigationBarViewDelegate? + + /// NavigationBar의 Title을 바꿉니다. + /// Title을 적용하면 Image가 사라집니다. public var navigationTitle: String? { didSet { navigationImageView.isHidden = true @@ -37,71 +41,82 @@ public final class BBNavigationBarView: UIView { } } - public var navigationImage: UIImage.TopBarImageType? { + /// NavigationBar의 Image를 바꿉니다. + /// Image를 적용하면 Title이 사라집니다. + public var navigationImage: TopBarButtonStyle? { didSet { navigationImageView.isHidden = false navigationTitleLabel.isHidden = true - navigationImageView.image = navigationImage?.barImage + navigationImageView.image = navigationImage?.image } } - public var leftBarButtonItem: UIImage.TopBarIconType? { + /// 왼쪽 버튼의 스타일을 설정합니다. + public var leftBarButtonItem: TopBarButtonStyle? { didSet { leftBarButton.setImage( - leftBarButtonItem?.barButtonImage, + leftBarButtonItem?.image, for: .normal ) setupButtonBackground(leftBarButton, type: leftBarButtonItem) } } - public var rightBarButtonItem: UIImage.TopBarIconType? { + // 오른쪽 버튼의 스타일을 설정합니다. + public var rightBarButtonItem: TopBarButtonStyle? { didSet { rightBarButton.setImage( - rightBarButtonItem?.barButtonImage, + rightBarButtonItem?.image, for: .normal ) setupButtonBackground(rightBarButton, type: leftBarButtonItem) } } + // Navigation Image의 크기를 설정합니다. 기본값은 1.0입니다. public var navigationImageScale: CGFloat = 1.0 { didSet { setupNavigationImageScale(navigationImageScale) } } + // 왼쪽 버튼 이미지의 크기를 설정합니다. 기본값은 1.0입니다. public var leftBarButtonItemScale: CGFloat = 1.0 { didSet { setupLeftButtonImageScale(leftBarButtonItemScale) } } + // 오른쪽 버튼 이미지의 크기를 설정합니다. 기본값은 1.0입니다. public var rightBarButtonItemScale: CGFloat = 1.0 { didSet { setupRightButtonImageScale(rightBarButtonItemScale) } } + // Navigation Title의 색상을 설정합니다. public var navigationTitleTextColor: UIColor = UIColor.gray200 { didSet { navigationTitleLabel.textColor = navigationTitleTextColor } } + // 왼쪽 버튼의 강조 색상을 설정합니다. public var leftBarButtonItemTintColor: UIColor = UIColor.gray300 { didSet { leftBarButton.tintColor = leftBarButtonItemTintColor } } + // 오른쪽 버튼의 강조 색상을 설정합니다. public var rightBarButtonItemTintColor: UIColor = UIColor.gray300 { didSet { rightBarButton.tintColor = rightBarButtonItemTintColor } } + // 왼쪽 버튼이 leading으로부터 얼마나 떨어져 있는지 설정합니다. public var leftBarButtonItemYOffset: CGFloat = 0.0 { didSet { leftBarButton.snp.updateConstraints { @@ -110,6 +125,7 @@ public final class BBNavigationBarView: UIView { } } + // 오른쪽 버튼이 leading으로부터 얼마나 떨어져 있는지 설정합니다. public var rightBarButtonItemYOffset: CGFloat = 0.0 { didSet { rightBarButton.snp.updateConstraints { @@ -229,7 +245,7 @@ extension BBNavigationBarView { ) } - private func setupButtonBackground(_ button: UIButton, type: UIImage.TopBarIconType?) { + private func setupButtonBackground(_ button: UIButton, type: TopBarButtonStyle?) { if type == .arrowLeft || type == .xmark { button.backgroundColor = .gray900 } else { diff --git a/14th-team5-iOS/Core/Sources/Base/BBNavigationViewController.swift b/14th-team5-iOS/Core/Sources/Base/BBNavigationViewController.swift new file mode 100644 index 000000000..02afabc82 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Base/BBNavigationViewController.swift @@ -0,0 +1,125 @@ +// +// BBNavigationViewController.swift +// Core +// +// Created by 김건우 on 6/5/24. +// + +import UIKit + +import SnapKit +import ReactorKit + +/// 삐삐 스타일의 NavigationBar가 기본으로 포함되어 있는 ViewController입니다. +/// +/// 이 ViewController는 ReactorViewController를 상속하고 있으며, bind() 메서드또한 제공합니다. +/// +/// 삐삐 스타일의 NavigationBar의 UI나 스타일을 변경해야 한다면, 직접 해당 NavigationBar에 정의되어 있는 메서드나 프로퍼티를 통해 변경할 수 있습니다. 가령, NavigationBar의 높이를 바꿔야 한다면. `setHeight(_:)` 메서드를 호출하면 됩니다. +/// +/// LeftBarButton의 타입이 .arrowLeft나 .xmark라면 popViewController() 구현이 기본으로 제공됩니다. +/// +/// 왼쪽, 오른쪽 버튼의 동작을 정의하고 싶다면 `navigationBarView.rx.didTapRightButton`과 같이 정의하면 됩니다. +/// +/// - NOTE: BBNavigationBarView의 스타일 변경을 위해 미리 정의된 편리한 메서드는 해당 뷰의 퀵헬프를 참조해주세요. +/// +open class BBNavigationViewController: ReactorViewController where R: Reactor { + + // MARK: - Typealias + typealias Reactor = R + + // MARK: - Views + + public let navigationBarView = BBNavigationBarView() + public let contentView = UIView() + + // MARK: - Intitalizer + public override init() { + super.init() + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Lifecycles + open override func viewDidLoad() { + super.viewDidLoad() + + } + + // MARK: - Helpers + open override func bind(reactor: R) { + super.bind(reactor: reactor) + + // 왼쪽 버튼이 특정 타입시, popViewController 기본 구현 제공 + navigationBarView.rx.didTapLeftBarButton + .bind(with: self) { owner, _ in + let buttonItem = owner.navigationBarView.leftBarButtonItem + + owner.popViewController(buttonItem) + } + .disposed(by: disposeBag) + } + + open override func setupUI() { + super.setupUI() + + view.addSubviews(navigationBarView, contentView) + } + + open override func setupAutoLayout() { + super.setupAutoLayout() + + navigationBarView.snp.makeConstraints { + $0.top.equalTo(view.safeAreaLayoutGuide) + $0.horizontalEdges.equalToSuperview() + $0.height.equalTo(52) + } + + contentView.snp.makeConstraints { + $0.top.equalTo(navigationBarView.snp.bottom) + $0.horizontalEdges.equalToSuperview() + $0.bottom.equalToSuperview() + } + } + + open override func setupAttributes() { + super.setupAttributes() + } + +} + + +// MARK: - Extensions + +extension BBNavigationViewController { + + public func setHeight(_ height: CGFloat) { + navigationBarView.snp.updateConstraints { + $0.top.equalTo(view.safeAreaLayoutGuide) + $0.horizontalEdges.equalToSuperview() + $0.height.equalTo(height) + } + + contentView.snp.makeConstraints { + $0.top.equalTo(navigationBarView.snp.bottom) + $0.horizontalEdges.equalToSuperview() + $0.bottom.equalToSuperview() + } + } + +} + + +extension BBNavigationViewController { + + private func popViewController(_ ifTypeIsXMark: TopBarButtonStyle?) { + switch ifTypeIsXMark { + case .arrowLeft, .xmark: + self.navigationController?.popViewController(animated: true) + @unknown default: + return + } + } + +} From 7586d0b956c3d38e15c15667e1e97283d8b1a05c Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Wed, 5 Jun 2024 21:32:17 +0900 Subject: [PATCH 081/263] =?UTF-8?q?feat:=20DailyCalendarViewController?= =?UTF-8?q?=EC=97=90=20=EC=98=88=EC=8B=9C=20=EC=BD=94=EB=93=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20(#546)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DailyCalendarViewController.swift | 26 +++++++------------ .../Base/BBNavigationViewController.swift | 15 +++++++++-- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/DailyCalendarViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/DailyCalendarViewController.swift index 837b58754..5d6178a7f 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/DailyCalendarViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/DailyCalendarViewController.swift @@ -20,7 +20,7 @@ import SnapKit import Then fileprivate typealias _Str = CalendarStrings -public final class DailyCalendarViewController: BaseViewController { +public final class DailyCalendarViewController: BBNavigationViewController { // MARK: - Views private let imageView: UIImageView = UIImageView() private let calendarView: FSCalendar = FSCalendar() @@ -76,7 +76,7 @@ public final class DailyCalendarViewController: BaseViewController: ReactorViewController where R: Reac $0.horizontalEdges.equalToSuperview() $0.bottom.equalToSuperview() } + } open override func setupAttributes() { @@ -94,6 +98,7 @@ open class BBNavigationViewController: ReactorViewController where R: Reac extension BBNavigationViewController { + /// NavigationBar의 높이를 바꿉니다. public func setHeight(_ height: CGFloat) { navigationBarView.snp.updateConstraints { $0.top.equalTo(view.safeAreaLayoutGuide) @@ -108,6 +113,12 @@ extension BBNavigationViewController { } } + /// NavigationBar를 View의 맨 앞으로 가져옵니다. + /// contentView가 아닌 view에 새로운 UI를 배치할 때, 꼭 호출해주어야 합니다. + public func bringNavigationBarViewToFront() { + view.bringSubviewToFront(navigationBarView) + } + } From 86909673942fc8d04156095b0208df614394495e Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Wed, 5 Jun 2024 21:37:41 +0900 Subject: [PATCH 082/263] =?UTF-8?q?feat:=20BaseViewController=EB=A5=BC=20D?= =?UTF-8?q?eprecated=20=EC=B2=98=EB=A6=AC=20(#546)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 14th-team5-iOS/Core/Sources/Base/BaseViewController.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/14th-team5-iOS/Core/Sources/Base/BaseViewController.swift b/14th-team5-iOS/Core/Sources/Base/BaseViewController.swift index 39bd696a1..152bb8754 100644 --- a/14th-team5-iOS/Core/Sources/Base/BaseViewController.swift +++ b/14th-team5-iOS/Core/Sources/Base/BaseViewController.swift @@ -11,6 +11,7 @@ import DesignSystem import ReactorKit import RxSwift +@available(*, deprecated, renamed: "ReactorViewController") open class BaseViewController: UIViewController, ReactorKit.View where R: Reactor { public typealias Reactor = R From d9a8824f8308ad5d573ecd5b00a0dfd0d2dae14f Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Thu, 6 Jun 2024 13:19:07 +0900 Subject: [PATCH 083/263] =?UTF-8?q?feat:=20OAuth=20=EA=B4=80=EB=A0=A8=20AP?= =?UTF-8?q?IWoker,=20DTO=20=EA=B5=AC=ED=98=84=20(#555)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DataMapping/AddFCMTokenRequestDTO.swift | 15 ++ .../DataMapping/AuthResultResponseDTO.swift | 30 ++++ .../CreateNewMemberRequestDTO.swift | 19 +++ .../DataMapping/DefaultResponseDTO.swift | 22 +++ .../NativeSocialLoginRequestDTO.swift | 15 ++ .../RefreshAccessTokenRequestDTO.swift | 16 +++ .../APIs/OAuth/OAuthAPI/OAuthAPIWorker.swift | 128 ++++++++++++++++++ .../APIs/OAuth/OAuthAPI/OAuthAPIs.swift | 34 +++++ .../APIs/SignIn/Helpers/SignInType.swift | 13 ++ .../Sources/Service/AccountSignInHelper.swift | 101 -------------- .../Service/AccountSignInHelperConfig.swift | 16 --- .../Service/AccountSignInHelperType.swift | 18 --- .../Service/Apple/AppleService+Rx.swift | 117 ---------------- .../Service/Apple/AppleSignInHelper.swift | 65 --------- .../Service/Kakao/KakaoService+Rx.swift | 38 ------ .../Service/Kakao/KakaoSignInHelper.swift | 67 --------- .../OAuth/Entities/AuthResultEntity.swift | 20 +++ .../Entities/DefaultResponseEntity.swift | 16 +++ .../Repository/OAuthRepository.swift | 8 ++ 19 files changed, 336 insertions(+), 422 deletions(-) create mode 100644 14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/DataMapping/AddFCMTokenRequestDTO.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/DataMapping/AuthResultResponseDTO.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/DataMapping/CreateNewMemberRequestDTO.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/DataMapping/DefaultResponseDTO.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/DataMapping/NativeSocialLoginRequestDTO.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/DataMapping/RefreshAccessTokenRequestDTO.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/OAuthAPIWorker.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/OAuthAPIs.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/SignIn/Helpers/SignInType.swift delete mode 100644 14th-team5-iOS/Data/Sources/Service/AccountSignInHelper.swift delete mode 100644 14th-team5-iOS/Data/Sources/Service/AccountSignInHelperConfig.swift delete mode 100644 14th-team5-iOS/Data/Sources/Service/AccountSignInHelperType.swift delete mode 100644 14th-team5-iOS/Data/Sources/Service/Apple/AppleService+Rx.swift delete mode 100644 14th-team5-iOS/Data/Sources/Service/Apple/AppleSignInHelper.swift delete mode 100644 14th-team5-iOS/Data/Sources/Service/Kakao/KakaoService+Rx.swift delete mode 100644 14th-team5-iOS/Data/Sources/Service/Kakao/KakaoSignInHelper.swift create mode 100644 14th-team5-iOS/Domain/Sources/OAuth/Entities/AuthResultEntity.swift create mode 100644 14th-team5-iOS/Domain/Sources/OAuth/Entities/DefaultResponseEntity.swift create mode 100644 14th-team5-iOS/Domain/Sources/OAuth/Interfaces/Repository/OAuthRepository.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/DataMapping/AddFCMTokenRequestDTO.swift b/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/DataMapping/AddFCMTokenRequestDTO.swift new file mode 100644 index 000000000..57e7a25f5 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/DataMapping/AddFCMTokenRequestDTO.swift @@ -0,0 +1,15 @@ +// +// AddFCMTokenRequestDTO.swift +// Data +// +// Created by 김건우 on 6/6/24. +// + +import Foundation + +public struct AddFCMTokenRequestDTO: Encodable { + private enum CodingKeys: String, CodingKey { + case fcmToken + } + var fcmToken: String +} diff --git a/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/DataMapping/AuthResultResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/DataMapping/AuthResultResponseDTO.swift new file mode 100644 index 000000000..1d58ec3b8 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/DataMapping/AuthResultResponseDTO.swift @@ -0,0 +1,30 @@ +// +// AuthResultResponseDTO.swift +// Data +// +// Created by 김건우 on 6/6/24. +// + +import Domain +import Foundation + +public struct AuthResultResponseDTO: Decodable { + private enum CodingKeys: String, CodingKey { + case accessToken + case refreshToken + case isTemporaryToken + } + var accessToken: String + var refreshToken: String + var isTemporaryToken: Bool +} + +extension AuthResultResponseDTO { + public func toDomain() -> AuthResultEntity { + return .init( + refreshToken: self.refreshToken, + accessToken: self.accessToken, + isTemporaryToken: self.isTemporaryToken + ) + } +} diff --git a/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/DataMapping/CreateNewMemberRequestDTO.swift b/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/DataMapping/CreateNewMemberRequestDTO.swift new file mode 100644 index 000000000..7f85c8f54 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/DataMapping/CreateNewMemberRequestDTO.swift @@ -0,0 +1,19 @@ +// +// CreateNewMemberRequestDTO.swift +// Data +// +// Created by 김건우 on 6/6/24. +// + +import Foundation + +public struct CreateNewMemberRequestDTO: Encodable { + private enum CodingKeys: String, CodingKey { + case memberName + case dayOfBirth + case profileImageUrl + } + var memberName: String + var dayOfBirth: String + var profileImageUrl: String +} diff --git a/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/DataMapping/DefaultResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/DataMapping/DefaultResponseDTO.swift new file mode 100644 index 000000000..31123199a --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/DataMapping/DefaultResponseDTO.swift @@ -0,0 +1,22 @@ +// +// DefaultResponseDTO.swift +// Data +// +// Created by 김건우 on 6/6/24. +// + +import Domain +import Foundation + +public struct DefaultResponseDTO: Decodable { + private enum CodingKeys: String, CodingKey { + case success + } + var success: Bool +} + +extension DefaultResponseDTO { + func toDomain() -> DefaultResponseEntity { + return .init(success: self.success) + } +} diff --git a/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/DataMapping/NativeSocialLoginRequestDTO.swift b/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/DataMapping/NativeSocialLoginRequestDTO.swift new file mode 100644 index 000000000..879a86ab8 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/DataMapping/NativeSocialLoginRequestDTO.swift @@ -0,0 +1,15 @@ +// +// NativeSocialLoginRequestDTO.swift +// Data +// +// Created by 김건우 on 6/6/24. +// + +import Foundation + +public struct NativeSocialLoginRequestDTO: Encodable { + private enum CodingKeys: String, CodingKey { + case accessToken + } + var accessToken: String +} diff --git a/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/DataMapping/RefreshAccessTokenRequestDTO.swift b/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/DataMapping/RefreshAccessTokenRequestDTO.swift new file mode 100644 index 000000000..0ab28a00f --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/DataMapping/RefreshAccessTokenRequestDTO.swift @@ -0,0 +1,16 @@ +// +// RefreshAccessTokenRequestDTO.swift +// Data +// +// Created by 김건우 on 6/6/24. +// + +import Domain +import Foundation + +public struct RefreshAccessTokenRequestDTO: Encodable { + private enum CodingKeys: String, CodingKey { + case refreshToken + } + var refreshToken: String +} diff --git a/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/OAuthAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/OAuthAPIWorker.swift new file mode 100644 index 000000000..fe793745f --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/OAuthAPIWorker.swift @@ -0,0 +1,128 @@ +// +// OAuthAPIWorker.swift +// Data +// +// Created by 김건우 on 6/6/24. +// + +import Core +import Domain +import Foundation + +import RxSwift + +typealias OAuthAPIWorker = OAuthAPIs.Worker +extension OAuthAPIs { + final class Worker: APIWorker { + static let queue = { + ConcurrentDispatchQueueScheduler(queue: DispatchQueue(label: "OAuthAPIQueue", qos: .utility)) + }() + + override init() { + super.init() + self.id = "OAuthAPIWorker" + } + } +} + +// MARK: - Extensions + +extension CalendarAPIWorker { + + // MARK: - Refresh Access Token + + public func refreshAccessToken(body: RefreshAccessTokenRequestDTO) -> Single { + let spec = OAuthAPIs.refreshToken.spec + + return request(spec: spec, jsonEncodable: body) + .subscribe(on: Self.queue) + .do { + if let str = String(data: $0.1, encoding: .utf8) { + debugPrint("Refresh Token Result: \(str)") + } + } + .map(AuthResultResponseDTO.self) + .catchAndReturn(nil) + .map { $0?.toDomain() } + .asSingle() + } + + + // MARK: - Register New Member + + public func registerNewMember(body: CreateNewMemberRequestDTO) -> Single { + let spec = OAuthAPIs.registerMember.spec + + return request(spec: spec, jsonEncodable: body) + .subscribe(on: Self.queue) + .do { + if let str = String(data: $0.1, encoding: .utf8) { + debugPrint("Auth Token Result: \(str)") + } + } + .map(AuthResultResponseDTO.self) + .catchAndReturn(nil) + .map { $0?.toDomain() } + .asSingle() + } + + + // MARK: - Sign In With SNS + + public func signIn(_ with: SignInType, body: NativeSocialLoginRequestDTO) -> Single { + let spec = OAuthAPIs.signIn(with).spec + + return request(spec: spec, jsonEncodable: body) + .subscribe(on: Self.queue) + .do { + if let str = String(data: $0.1, encoding: .utf8) { + debugPrint("Auth Token Result: \(str)") + } + } + .map(AuthResultResponseDTO.self) + .catchAndReturn(nil) + .map { $0?.toDomain() } + .asSingle() + } + + + + // MARK: - Regieter FCM Token + + public func registerNewFCMToken(body: AddFCMTokenRequestDTO) -> Single { + let spec = OAuthAPIs.registerFCMToken.spec + + return request(spec: spec, jsonEncodable: body) + .subscribe(on: Self.queue) + .do { + if let str = String(data: $0.1, encoding: .utf8) { + debugPrint("FCM Register Result: \(str)") + } + } + .map(DefaultResponseDTO.self) + .catchAndReturn(nil) + .map { $0?.toDomain() } + .asSingle() + } + + + // MARK: - Delete FCM Token + + public func deleteFCMToken(fcmToken token: String) -> Single { + let spec = OAuthAPIs.deleteFCMToken(token).spec + + return request(spec: spec) + .subscribe(on: Self.queue) + .do { + if let str = String(data: $0.1, encoding: .utf8) { + debugPrint("FCM Delete Result: \(str)") + } + } + .map(DefaultResponseDTO.self) + .catchAndReturn(nil) + .map { $0?.toDomain() } + .asSingle() + } + + +} diff --git a/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/OAuthAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/OAuthAPIs.swift new file mode 100644 index 000000000..98b4e82f7 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/OAuthAPIs.swift @@ -0,0 +1,34 @@ +// +// OAuthAPIs.swift +// Data +// +// Created by 김건우 on 6/6/24. +// + +import Core +import Foundation + +public enum OAuthAPIs: API { + case refreshToken + case registerMember + case signIn(SignInType) + + case registerFCMToken + case deleteFCMToken(String) + + public var spec: APISpec { + switch self { + case .refreshToken: + return APISpec(method: .post, url: "\(BibbiAPI.hostApi)/auth/refresh") + case .registerMember: + return APISpec(method: .post, url: "\(BibbiAPI.hostApi)/auth/register") + case let .signIn(type): + return APISpec(method: .post, url: "\(BibbiAPI.hostApi)/auth/social/\(type.rawValue)") + + case .registerFCMToken: + return APISpec(method: .post, url: "\(BibbiAPI.hostApi)/me/fcm") + case let .deleteFCMToken(token): + return APISpec(method: .delete, url: "\(BibbiAPI.hostApi)/me/fcm/\(token)") + } + } +} diff --git a/14th-team5-iOS/Data/Sources/APIs/SignIn/Helpers/SignInType.swift b/14th-team5-iOS/Data/Sources/APIs/SignIn/Helpers/SignInType.swift new file mode 100644 index 000000000..442ffe430 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/SignIn/Helpers/SignInType.swift @@ -0,0 +1,13 @@ +// +// SignInType.swift +// Data +// +// Created by 김건우 on 6/6/24. +// + +import Foundation + +public enum SignInType: String { + case kakao = "KAKAO" + case apple = "APPLE" +} diff --git a/14th-team5-iOS/Data/Sources/Service/AccountSignInHelper.swift b/14th-team5-iOS/Data/Sources/Service/AccountSignInHelper.swift deleted file mode 100644 index 8ba38b9d6..000000000 --- a/14th-team5-iOS/Data/Sources/Service/AccountSignInHelper.swift +++ /dev/null @@ -1,101 +0,0 @@ -// -// AccountSignInHelper.swift -// Data -// -// Created by geonhui Yu on 12/18/23. -// - -import Foundation -import Domain -import Core - -import RxSwift -import Alamofire -import RxCocoa -import AuthenticationServices - -protocol AccountSignInHelperConfigType { - var snsHelpers: [String: AccountSignInHelperType] { get } -} - -final class AccountSignInHelper: NSObject { - private(set) var disposeBag = DisposeBag() - // MARK: Config - private let config: AccountSignInHelperConfigType = AccountSignInHelperConfig() - - // MARK: SNS SignIn Helpers - private var signInHelper: [String: AccountSignInHelperType] { - return self.config.snsHelpers - } - - // MARK: API Worker - private let apiWorker = AccountAPIWorker() - let snsSignInResult = PublishRelay<(APIResult, AccountSignInStateInfo)>() - - func bind() { - Observable.from(signInHelper.values.map { $0.signInState }).merge() - .withUnretained(self) - .flatMap { (_self, state) -> Single<(APIResult, AccountSignInStateInfo)> in - - guard let token = state.snsToken else { - return Single.just((.failed, state)) - } - - return _self.signInWith(snsType: state.snsType, snsToken: token) - .map { res -> (APIResult, AccountSignInStateInfo) in - return (res, state) - } - } - .withUnretained(self) - .bind(onNext: { $0.snsSignInResult.accept($1) }) - .disposed(by: self.disposeBag) - - snsSignInResult.map { $0.1 } - .withUnretained(self) - .bind(onNext: { UserDefaults.standard.snsType = $0.1.snsType.rawValue }) - .disposed(by: disposeBag) - } -} - -// MARK: SignIn Functions -extension AccountSignInHelper { - func trySignInWith(sns: SNS, window: UIWindow?) -> Observable { - guard let helper = signInHelper[sns.rawValue], let window = window else { - return Observable.just(.failed) - } - return helper.signIn(on: window) - } - - func signInWith(snsType: SNS, snsToken: String) -> Single { - return apiWorker.signInWith(snsType: snsType, snsToken: snsToken) - .flatMap { - - let accessToken = $0?.accessToken - let refreshToken = $0?.refreshToken - let isTemporaryToken = $0?.isTemporaryToken - - let token = AccessToken(accessToken: accessToken, refreshToken: refreshToken, isTemporaryToken: isTemporaryToken) - - return Single.just(token) - } - .map { (token: AccessToken?) -> APIResult in - guard let token = token else { - return .failed - } - App.Repository.token.accessToken.accept(token) - - return .success - } - } - - public func signOut(sns: String) -> Observable { - return Observable.create { [weak self] observer in - guard let signOut = self?.signInHelper[sns]?.signOut() else { return Disposables.create() } - observer.onNext(signOut) - - App.Repository.token.clearAccessToken() - - return Disposables.create() - } - } -} diff --git a/14th-team5-iOS/Data/Sources/Service/AccountSignInHelperConfig.swift b/14th-team5-iOS/Data/Sources/Service/AccountSignInHelperConfig.swift deleted file mode 100644 index 3c82bf6e3..000000000 --- a/14th-team5-iOS/Data/Sources/Service/AccountSignInHelperConfig.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// AccountSignInHelperConfig.swift -// Domain -// -// Created by geonhui Yu on 12/18/23. -// - -import Foundation -import Domain - -struct AccountSignInHelperConfig: AccountSignInHelperConfigType { - let snsHelpers: [String: AccountSignInHelperType] = [ - "APPLE": AppleSignInHelper(), - "KAKAO": KakaoSignInHelper() - ] -} diff --git a/14th-team5-iOS/Data/Sources/Service/AccountSignInHelperType.swift b/14th-team5-iOS/Data/Sources/Service/AccountSignInHelperType.swift deleted file mode 100644 index 5ca9c341c..000000000 --- a/14th-team5-iOS/Data/Sources/Service/AccountSignInHelperType.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// AccountSignInHelperType.swift -// Domain -// -// Created by geonhui Yu on 12/6/23. -// - -import Core -import Domain -import UIKit - -import RxSwift - -public protocol AccountSignInHelperType: AnyObject { - var signInState: Observable { get } - func signIn(on window: UIWindow) -> Observable - func signOut() -} diff --git a/14th-team5-iOS/Data/Sources/Service/Apple/AppleService+Rx.swift b/14th-team5-iOS/Data/Sources/Service/Apple/AppleService+Rx.swift deleted file mode 100644 index 4bb71fdc4..000000000 --- a/14th-team5-iOS/Data/Sources/Service/Apple/AppleService+Rx.swift +++ /dev/null @@ -1,117 +0,0 @@ -// -// AppleService+Rx.swift -// Data -// -// Created by geonhui Yu on 12/20/23. -// - -import UIKit -import Domain - -import AuthenticationServices -import RxSwift -import RxCocoa - -extension ASAuthorizationController: HasDelegate { - public typealias Delegate = ASAuthorizationControllerDelegate -} - -class RxASAuthorizationControllerDelegateProxy: DelegateProxy, DelegateProxyType, ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding { - - private let disposeBag = DisposeBag() - - var presentationWindow: UIWindow = UIWindow() - - public init(controller: ASAuthorizationController) { - super.init(parentObject: controller, delegateProxy: RxASAuthorizationControllerDelegateProxy.self) - } - - static func registerKnownImplementations() { - register { RxASAuthorizationControllerDelegateProxy(controller: $0) } - } - - internal lazy var didComplete = PublishSubject() - - func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { - return presentationWindow - } - - func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { - - switch authorization.credential { - case let appleIDCredential as ASAuthorizationAppleIDCredential: - if let identityToken = appleIDCredential.identityToken, - let tokenString = String(data: identityToken, encoding: .utf8) { - let state = AccountSignInStateInfo(snsType: .apple, snsToken: tokenString) - - self.didComplete.onNext(state) - self.didComplete.onCompleted() - } - - default: - return - } - } - - func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) { - - guard let err = error as? ASAuthorizationError else { - self.didComplete.onNext(AccountSignInStateInfo(snsType: .apple)) - self.didComplete.onCompleted() - return - } - - switch err.code { - case .canceled: - debugPrint("Apple auth canceled!!!") - case .failed: - debugPrint("Apple auth failed!!!") - case .invalidResponse: - debugPrint("Apple auth invalid response!!!") - case .notHandled: - debugPrint("Apple auth not handled!!!") - case .unknown: - debugPrint("Apple auth unknown!!!") - - ASAuthorizationAppleIDProvider().rx.signIn(on: self.presentationWindow) - .withUnretained(self) - .subscribe(onNext: { _self, result in - _self.didComplete.onNext(result) - _self.didComplete.onCompleted() - - }, onError: { [weak self] err in - self?.didComplete.onNext(AccountSignInStateInfo(snsType: .apple)) - self?.didComplete.onCompleted() - }) - .disposed(by: self.disposeBag) - return - default: - debugPrint("???") - } - - self.didComplete.onNext(AccountSignInStateInfo(snsType: .apple)) - self.didComplete.onCompleted() - } - - // MARK: Completed - deinit { - self.didComplete.onCompleted() - } -} - -extension Reactive where Base: ASAuthorizationAppleIDProvider { - - func signIn(on window: UIWindow) -> Observable { - let req = base.createRequest() - req.requestedScopes = [.fullName, .email] - - let ctrl = ASAuthorizationController(authorizationRequests: [req]) - let proxy = RxASAuthorizationControllerDelegateProxy.proxy(for: ctrl) - proxy.presentationWindow = window - - ctrl.presentationContextProvider = proxy - ctrl.performRequests() - - return proxy.didComplete - } -} diff --git a/14th-team5-iOS/Data/Sources/Service/Apple/AppleSignInHelper.swift b/14th-team5-iOS/Data/Sources/Service/Apple/AppleSignInHelper.swift deleted file mode 100644 index 175cf5501..000000000 --- a/14th-team5-iOS/Data/Sources/Service/Apple/AppleSignInHelper.swift +++ /dev/null @@ -1,65 +0,0 @@ -// -// AppleSignInHelper.swift -// Data -// -// Created by geonhui Yu on 12/20/23. -// - -import UIKit -import AuthenticationServices -import Domain -import Core - -import RxSwift -import RxCocoa - -// MARK: Apple SignIn Helper -class AppleSignInHelper: NSObject, AccountSignInHelperType { - - private var disposeBag = DisposeBag() - - private let _signInState = PublishRelay() - let signInState: Observable - - override init() { - self.signInState = self._signInState.asObservable() - super.init() - } - - deinit { - self.disposeBag = DisposeBag() - } - - func signIn(on window: UIWindow) -> Observable { - guard #available(iOS 13.0, *) else { - self._signInState.accept(AccountSignInStateInfo(snsType: .apple)) - return Observable.just(.failed) - } - - return Observable.create { observer in - ASAuthorizationAppleIDProvider().rx.signIn(on: window) - .asSingle() - .observe(on: RxSchedulers.utility) - .subscribe( - onSuccess: { [weak self] response in - self?._signInState.accept(response) - observer.onNext(.success) - observer.onCompleted() - }, - onFailure: { [weak self] error in - self?._signInState.accept(AccountSignInStateInfo(snsType: .apple)) - observer.onNext(.failed) - observer.onCompleted() - } - ) - .disposed(by: self.disposeBag) - - return Disposables.create() - } - - } - - func signOut() { - debugPrint("Apple AuthenticationServices does not support signOut!") - } -} diff --git a/14th-team5-iOS/Data/Sources/Service/Kakao/KakaoService+Rx.swift b/14th-team5-iOS/Data/Sources/Service/Kakao/KakaoService+Rx.swift deleted file mode 100644 index c00915690..000000000 --- a/14th-team5-iOS/Data/Sources/Service/Kakao/KakaoService+Rx.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// KakaoService+Rx.swift -// Data -// -// Created by geonhui Yu on 12/18/23. -// - -import Domain - -import RxSwift -import RxCocoa -import KakaoSDKAuth -import KakaoSDKUser -import RxKakaoSDKUser - -extension Reactive where Base: UserApi { - func signIn() -> Observable { - var loginObservable: Observable? = nil - if UserApi.isKakaoTalkLoginAvailable() { - loginObservable = base.rx.loginWithKakaoTalk() - } else { - loginObservable = base.rx.loginWithKakaoAccount(prompts: [.Login]) - } - - guard let observable = loginObservable else { - return Observable.just(AccountSignInStateInfo(snsType: .kakao)) - } - - return observable - .do(onError: { error in - debugPrint("Kakao SignIn Error: \(error)") - }) - .map { oauthToken -> AccountSignInStateInfo in - return AccountSignInStateInfo(snsType: .kakao, snsToken: oauthToken.accessToken) - } - .catchAndReturn(AccountSignInStateInfo(snsType: .kakao)) - } -} diff --git a/14th-team5-iOS/Data/Sources/Service/Kakao/KakaoSignInHelper.swift b/14th-team5-iOS/Data/Sources/Service/Kakao/KakaoSignInHelper.swift deleted file mode 100644 index 2e194106f..000000000 --- a/14th-team5-iOS/Data/Sources/Service/Kakao/KakaoSignInHelper.swift +++ /dev/null @@ -1,67 +0,0 @@ -// -// KakaoSignInHelper.swift -// Data -// -// Created by geonhui Yu on 12/6/23. -// - -import UIKit -import Domain -import Core -import RxSwift -import RxCocoa -import RxKakaoSDKCommon -import KakaoSDKAuth -import RxKakaoSDKAuth -import KakaoSDKUser -import RxKakaoSDKUser - -final class KakaoSignInHelper: AccountSignInHelperType { - - private var disposeBag = DisposeBag() - - private let _signInState = PublishRelay() - var signInState: Observable { - self._signInState.asObservable() - } - - deinit { - self.disposeBag = DisposeBag() - } - - func signIn(on window: UIWindow) -> Observable { - if UserApi.isKakaoTalkLoginAvailable() { - return UserApi.shared.rx.loginWithKakaoTalk(launchMethod: .CustomScheme) - .map { [weak self] response in - self?._signInState.accept(AccountSignInStateInfo(snsType: .kakao, snsToken: response.accessToken)) - return .success - } - .catch { error in - return .just(.failed) - } - .observe(on: MainScheduler.instance) - } else { - return UserApi.shared.rx.loginWithKakaoAccount(prompts: [.Login]) - .map { [weak self] response in - self?._signInState.accept(AccountSignInStateInfo(snsType: .kakao)) - return .success - } - .catch { error in - return .just(.failed) - } - .observe(on: MainScheduler.instance) - } - } - - func signOut() { - UserApi.shared.rx.logout() - .subscribe(onCompleted: { - // Token 제거시 확인 - - debugPrint("Kakao logout completed!") - }, onError: { error in - debugPrint("Kakao logout error!") - }) - .disposed(by: self.disposeBag) - } -} diff --git a/14th-team5-iOS/Domain/Sources/OAuth/Entities/AuthResultEntity.swift b/14th-team5-iOS/Domain/Sources/OAuth/Entities/AuthResultEntity.swift new file mode 100644 index 000000000..1bc68dd64 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/OAuth/Entities/AuthResultEntity.swift @@ -0,0 +1,20 @@ +// +// AuthResultEntity.swift +// Domain +// +// Created by 김건우 on 6/6/24. +// + +import Foundation + +public struct AuthResultEntity { + public var refreshToken: String + public var accessToken: String + public var isTemporaryToken: Bool + + public init(refreshToken: String, accessToken: String, isTemporaryToken: Bool) { + self.refreshToken = refreshToken + self.accessToken = accessToken + self.isTemporaryToken = isTemporaryToken + } +} diff --git a/14th-team5-iOS/Domain/Sources/OAuth/Entities/DefaultResponseEntity.swift b/14th-team5-iOS/Domain/Sources/OAuth/Entities/DefaultResponseEntity.swift new file mode 100644 index 000000000..aac7ba0a8 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/OAuth/Entities/DefaultResponseEntity.swift @@ -0,0 +1,16 @@ +// +// DefaultResponseEntity.swift +// Domain +// +// Created by 김건우 on 6/6/24. +// + +import Foundation + +public struct DefaultResponseEntity { + public var success: Bool + + public init(success: Bool) { + self.success = success + } +} diff --git a/14th-team5-iOS/Domain/Sources/OAuth/Interfaces/Repository/OAuthRepository.swift b/14th-team5-iOS/Domain/Sources/OAuth/Interfaces/Repository/OAuthRepository.swift new file mode 100644 index 000000000..602d7da04 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/OAuth/Interfaces/Repository/OAuthRepository.swift @@ -0,0 +1,8 @@ +// +// OAuthRepository.swift +// Domain +// +// Created by 김건우 on 6/6/24. +// + +import Foundation From 41607a30dcfcea8d09c7d7a3fa5de205ba2ae472 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Thu, 6 Jun 2024 14:12:55 +0900 Subject: [PATCH 084/263] =?UTF-8?q?refactor:=20SignIn=20=EA=B4=80=EB=A0=A8?= =?UTF-8?q?=20Helper=20=EC=BD=94=EB=93=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81=20(#555)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SignIn/Repository/SignInRepository.swift | 8 ++ .../Helpers/Apple/AppleSignIn+Rx.swift | 117 ++++++++++++++++++ .../Helpers/Apple/AppleSignInHelper.swift | 71 +++++++++++ .../Helpers/Kakao/KakaoSignIn+Rx.swift | 38 ++++++ .../Helpers/Kakao/KakaoSignInHelper.swift | 79 ++++++++++++ .../SignInAPI/Helpers/SignInHelperType.swift | 19 +++ .../SignIn/SignInAPI/SignInAPIConfig.swift | 18 +++ .../SignIn/SignInAPI/SignInAPIWorker.swift | 8 ++ .../APIs/SignIn/SignInAPI/SignInHelper.swift | 101 +++++++++++++++ .../{Helpers => SignInAPI}/SignInType.swift | 0 .../AccountRepository/AccountRepository.swift | 2 +- .../Repositories/PrivacyViewRepository.swift | 2 +- .../xcschemes/Bibbi-Workspace.xcscheme | 2 +- 13 files changed, 462 insertions(+), 3 deletions(-) create mode 100644 14th-team5-iOS/Data/Sources/APIs/SignIn/Repository/SignInRepository.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/Apple/AppleSignIn+Rx.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/Apple/AppleSignInHelper.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/Kakao/KakaoSignIn+Rx.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/Kakao/KakaoSignInHelper.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/SignInHelperType.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/SignInAPIConfig.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/SignInAPIWorker.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/SignInHelper.swift rename 14th-team5-iOS/Data/Sources/APIs/SignIn/{Helpers => SignInAPI}/SignInType.swift (100%) diff --git a/14th-team5-iOS/Data/Sources/APIs/SignIn/Repository/SignInRepository.swift b/14th-team5-iOS/Data/Sources/APIs/SignIn/Repository/SignInRepository.swift new file mode 100644 index 000000000..3d7ae9e4c --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/SignIn/Repository/SignInRepository.swift @@ -0,0 +1,8 @@ +// +// SignInRepository.swift +// Data +// +// Created by 김건우 on 6/6/24. +// + +import Foundation diff --git a/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/Apple/AppleSignIn+Rx.swift b/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/Apple/AppleSignIn+Rx.swift new file mode 100644 index 000000000..02b63c036 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/Apple/AppleSignIn+Rx.swift @@ -0,0 +1,117 @@ +// +// AppleService+Rx.swift +// Data +// +// Created by geonhui Yu on 12/20/23. +// + +import UIKit +import Domain + +import AuthenticationServices +import RxCocoa +import RxSwift + +class RxASAuthorizationControllerDelegateProxy: DelegateProxy, DelegateProxyType, ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding { + + private let disposeBag = DisposeBag() + + var presentationWindow: UIWindow = UIWindow() + + public init(controller: ASAuthorizationController) { + super.init(parentObject: controller, delegateProxy: RxASAuthorizationControllerDelegateProxy.self) + } + + static func registerKnownImplementations() { + register { RxASAuthorizationControllerDelegateProxy(controller: $0) } + } + + internal lazy var didComplete = PublishSubject() + + func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { + return presentationWindow + } + + func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { + + switch authorization.credential { + case let appleIDCredential as ASAuthorizationAppleIDCredential: + if let identityToken = appleIDCredential.identityToken, + let tokenString = String(data: identityToken, encoding: .utf8) { + let state = AccountSignInStateInfo(snsType: .apple, snsToken: tokenString) + + self.didComplete.onNext(state) + self.didComplete.onCompleted() + } + + default: + return + } + } + + func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) { + + guard let err = error as? ASAuthorizationError else { + self.didComplete.onNext(AccountSignInStateInfo(snsType: .apple)) + self.didComplete.onCompleted() + return + } + + switch err.code { + case .canceled: + debugPrint("Apple auth canceled!!!") + case .failed: + debugPrint("Apple auth failed!!!") + case .invalidResponse: + debugPrint("Apple auth invalid response!!!") + case .notHandled: + debugPrint("Apple auth not handled!!!") + case .unknown: + debugPrint("Apple auth unknown!!!") + + ASAuthorizationAppleIDProvider().rx.signIn(on: self.presentationWindow) + .withUnretained(self) + .subscribe(onNext: { _self, result in + _self.didComplete.onNext(result) + _self.didComplete.onCompleted() + + }, onError: { [weak self] err in + self?.didComplete.onNext(AccountSignInStateInfo(snsType: .apple)) + self?.didComplete.onCompleted() + }) + .disposed(by: self.disposeBag) + return + default: + debugPrint("???") // ? + } + + self.didComplete.onNext(AccountSignInStateInfo(snsType: .apple)) + self.didComplete.onCompleted() + } + + // MARK: Completed + deinit { + self.didComplete.onCompleted() + } +} + +extension ASAuthorizationController: HasDelegate { + public typealias Delegate = ASAuthorizationControllerDelegate +} + +extension Reactive where Base: ASAuthorizationAppleIDProvider { + + func signIn(on window: UIWindow) -> Observable { + let req = base.createRequest() + req.requestedScopes = [.fullName, .email] + + let ctrl = ASAuthorizationController(authorizationRequests: [req]) + let proxy = RxASAuthorizationControllerDelegateProxy.proxy(for: ctrl) + proxy.presentationWindow = window + + ctrl.presentationContextProvider = proxy + ctrl.performRequests() + + return proxy.didComplete + } +} diff --git a/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/Apple/AppleSignInHelper.swift b/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/Apple/AppleSignInHelper.swift new file mode 100644 index 000000000..66a6dfb4a --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/Apple/AppleSignInHelper.swift @@ -0,0 +1,71 @@ +// +// AppleSignInHelper.swift +// Data +// +// Created by geonhui Yu on 12/20/23. +// + +import Core +import UIKit +import Domain +import AuthenticationServices + +import RxSwift +import RxCocoa + +final class AppleSignInHelper: SignInHelperType { + + // MARK: - Properties + + private var disposeBag = DisposeBag() + + private let _signInState = PublishRelay() // ? + var signInState: Observable { + self._signInState.asObservable() + } // ? + + + // MARK: - Sign In + + func signIn(on window: UIWindow) -> Observable { // 그냥 바로 AccessToken 리턴하게 만들기 + guard #available(iOS 13.0, *) else { + self._signInState.accept(AccountSignInStateInfo(snsType: .apple)) + return Observable.just(.failed) + } + + return Observable.create { observer in + ASAuthorizationAppleIDProvider().rx.signIn(on: window) + .asSingle() + .observe(on: RxSchedulers.utility) + .subscribe( + onSuccess: { [weak self] response in + self?._signInState.accept(response) + observer.onNext(.success) + observer.onCompleted() + }, + onFailure: { [weak self] error in + self?._signInState.accept(AccountSignInStateInfo(snsType: .apple)) + observer.onNext(.failed) + observer.onCompleted() + } + ) + .disposed(by: self.disposeBag) + + return Disposables.create() + } + + } + + + // MARK: - Sign Out + + func signOut() { + debugPrint("Apple AuthenticationServices does not support signOut!") + } + + + // MARK: - Deinitializer + + deinit { self.disposeBag = DisposeBag() } + +} diff --git a/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/Kakao/KakaoSignIn+Rx.swift b/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/Kakao/KakaoSignIn+Rx.swift new file mode 100644 index 000000000..c00915690 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/Kakao/KakaoSignIn+Rx.swift @@ -0,0 +1,38 @@ +// +// KakaoService+Rx.swift +// Data +// +// Created by geonhui Yu on 12/18/23. +// + +import Domain + +import RxSwift +import RxCocoa +import KakaoSDKAuth +import KakaoSDKUser +import RxKakaoSDKUser + +extension Reactive where Base: UserApi { + func signIn() -> Observable { + var loginObservable: Observable? = nil + if UserApi.isKakaoTalkLoginAvailable() { + loginObservable = base.rx.loginWithKakaoTalk() + } else { + loginObservable = base.rx.loginWithKakaoAccount(prompts: [.Login]) + } + + guard let observable = loginObservable else { + return Observable.just(AccountSignInStateInfo(snsType: .kakao)) + } + + return observable + .do(onError: { error in + debugPrint("Kakao SignIn Error: \(error)") + }) + .map { oauthToken -> AccountSignInStateInfo in + return AccountSignInStateInfo(snsType: .kakao, snsToken: oauthToken.accessToken) + } + .catchAndReturn(AccountSignInStateInfo(snsType: .kakao)) + } +} diff --git a/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/Kakao/KakaoSignInHelper.swift b/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/Kakao/KakaoSignInHelper.swift new file mode 100644 index 000000000..f690f733d --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/Kakao/KakaoSignInHelper.swift @@ -0,0 +1,79 @@ +// +// KakaoSignInHelper.swift +// Data +// +// Created by geonhui Yu on 12/6/23. +// + +import Core +import Domain +import UIKit + +import RxCocoa +import RxSwift +import RxKakaoSDKAuth +import RxKakaoSDKCommon +import RxKakaoSDKUser +import KakaoSDKAuth +import KakaoSDKUser + +final class KakaoSignInHelper: SignInHelperType { + + // MARK: - Properties + + private var disposeBag = DisposeBag() + + private let _signInState = PublishRelay() // ? + var signInState: Observable { + self._signInState.asObservable() + } // ? + + + // MARK: - Sign In + + func signIn(on window: UIWindow) -> Observable { // 그냥 바로 AccessToken 리턴하게 만들기 + if UserApi.isKakaoTalkLoginAvailable() { + return UserApi.shared.rx.loginWithKakaoTalk(launchMethod: .CustomScheme) + .map { [weak self] response in + self?._signInState.accept(AccountSignInStateInfo(snsType: .kakao, snsToken: response.accessToken)) + return .success + } + .catch { error in + return .just(.failed) + } + .observe(on: MainScheduler.instance) + } else { + return UserApi.shared.rx.loginWithKakaoAccount(prompts: [.Login]) + .map { [weak self] response in + self?._signInState.accept(AccountSignInStateInfo(snsType: .kakao)) + return .success + } + .catch { error in + return .just(.failed) + } + .observe(on: MainScheduler.instance) + } + } + + + // MARK: - Sign Out + + func signOut() { + UserApi.shared.rx.logout() + .subscribe(onCompleted: { + // Token 제거시 확인 + + debugPrint("Kakao logout completed!") + }, onError: { error in + debugPrint("Kakao logout error!") + }) + .disposed(by: self.disposeBag) + } + + + + + // MARK: - Deinitalizer + + deinit { self.disposeBag = DisposeBag() } +} diff --git a/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/SignInHelperType.swift b/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/SignInHelperType.swift new file mode 100644 index 000000000..47738c65c --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/SignInHelperType.swift @@ -0,0 +1,19 @@ +// +// AccountSignInHelperType.swift +// Domain +// +// Created by geonhui Yu on 12/6/23. +// + +import Core +import Domain +import UIKit + +import RxSwift + +public protocol SignInHelperType: AnyObject { + var signInState: Observable { get } // ? + + func signIn(on window: UIWindow) -> Observable + func signOut() +} diff --git a/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/SignInAPIConfig.swift b/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/SignInAPIConfig.swift new file mode 100644 index 000000000..b4e0759b1 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/SignInAPIConfig.swift @@ -0,0 +1,18 @@ +// +// AccountSignInHelperConfig.swift +// Domain +// +// Created by geonhui Yu on 12/18/23. +// + +import Domain +import Foundation + +struct SignInAPIConfig { + let apple = SignInType.apple.rawValue + let kakao = SignInType.kakao.rawValue + + var helpers: [String: SignInHelperType] { + [apple: AppleSignInHelper(), kakao: KakaoSignInHelper()] + } +} diff --git a/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/SignInAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/SignInAPIWorker.swift new file mode 100644 index 000000000..6a94423bd --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/SignInAPIWorker.swift @@ -0,0 +1,8 @@ +// +// SignInAPIWorker.swift +// Data +// +// Created by 김건우 on 6/6/24. +// + +import Foundation diff --git a/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/SignInHelper.swift b/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/SignInHelper.swift new file mode 100644 index 000000000..576b5ddb1 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/SignInHelper.swift @@ -0,0 +1,101 @@ +// +// AccountSignInHelper.swift +// Data +// +// Created by geonhui Yu on 12/18/23. +// + +import Core +import Domain +import Foundation + +import Alamofire +import AuthenticationServices +import RxCocoa +import RxSwift + + +// TODO: - SignInAPIWorker로 코드 이동, Deprecated 처리 + +final class SignInHelper: NSObject { + + // MARK: - Properties + private(set) var disposeBag = DisposeBag() + + private let _config = SignInAPIConfig() + private lazy var helpers: [String: SignInHelperType] = { + _config.helpers + }() + + private let apiWorker = AccountAPIWorker() + let snsSignInResult = PublishRelay<(APIResult, AccountSignInStateInfo)>() + + func bind() { + Observable.from(helpers.values.map { $0.signInState }).merge() + .withUnretained(self) + .flatMap { (_self, state) -> Single<(APIResult, AccountSignInStateInfo)> in + + guard let token = state.snsToken else { + return Single.just((.failed, state)) + } + + return _self.signInWith(snsType: state.snsType, snsToken: token) + .map { res -> (APIResult, AccountSignInStateInfo) in + return (res, state) + } + } + .withUnretained(self) + .bind(onNext: { $0.snsSignInResult.accept($1) }) + .disposed(by: self.disposeBag) + + snsSignInResult.map { $0.1 } + .withUnretained(self) + .bind(onNext: { UserDefaults.standard.snsType = $0.1.snsType.rawValue }) + .disposed(by: disposeBag) + } +} + +// MARK: SignIn Functions +extension SignInHelper { + func trySignInWith(sns: SNS, window: UIWindow?) -> Observable { + guard let helper = helpers[sns.rawValue], let window = window else { + return Observable.just(.failed) + } + return helper.signIn(on: window) + } + + // 로그인 버튼 클릭 -> SignInReactor -> SignInUseCase -> SignInRepo & API -> SignInUseCase -> SignInReactor + // -> OAuthUseCase -> OAuthRepo & API -> OAuthUseCase -> SignInReactor -> 화면 전환으로 로직이 흐르게 만들기 + func signInWith(snsType: SNS, snsToken: String) -> Single { + return apiWorker.signInWith(snsType: snsType, snsToken: snsToken) + .flatMap { + + let accessToken = $0?.accessToken + let refreshToken = $0?.refreshToken + let isTemporaryToken = $0?.isTemporaryToken + + let token = AccessToken(accessToken: accessToken, refreshToken: refreshToken, isTemporaryToken: isTemporaryToken) + + return Single.just(token) + } + .map { (token: AccessToken?) -> APIResult in + guard let token = token else { + return .failed + } + App.Repository.token.accessToken.accept(token) // 토큰 저장은 Repository에서 + + return .success + } + } + + public func signOut(sns: String) -> Observable { + return Observable.create { [weak self] observer in + guard let signOut = self?.helpers[sns]?.signOut() else { return Disposables.create() } + observer.onNext(signOut) + + App.Repository.token.clearAccessToken() // 토큰 삭제도 Repository에서 + + return Disposables.create() + } + } +} diff --git a/14th-team5-iOS/Data/Sources/APIs/SignIn/Helpers/SignInType.swift b/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/SignInType.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/APIs/SignIn/Helpers/SignInType.swift rename to 14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/SignInType.swift diff --git a/14th-team5-iOS/Data/Sources/Account/AccountRepository/AccountRepository.swift b/14th-team5-iOS/Data/Sources/Account/AccountRepository/AccountRepository.swift index 430a46f85..64aaf53e0 100644 --- a/14th-team5-iOS/Data/Sources/Account/AccountRepository/AccountRepository.swift +++ b/14th-team5-iOS/Data/Sources/Account/AccountRepository/AccountRepository.swift @@ -35,7 +35,7 @@ public final class AccountRepository: AccountImpl { private let accessToken: String = App.Repository.token.accessToken.value?.accessToken ?? "" private let keychain = KeychainWrapper(serviceName: "Bibbi", accessGroup: "P9P4WJ623F.com.5ing.bibbi") - let signInHelper = AccountSignInHelper() + let signInHelper = SignInHelper() private let apiWorker = AccountAPIWorker() private let profileWorker = ProfileAPIWorker() private let meApiWorekr = MeAPIWorker() diff --git a/14th-team5-iOS/Data/Sources/Privacy/Repositories/PrivacyViewRepository.swift b/14th-team5-iOS/Data/Sources/Privacy/Repositories/PrivacyViewRepository.swift index 3618bd475..a805a6761 100644 --- a/14th-team5-iOS/Data/Sources/Privacy/Repositories/PrivacyViewRepository.swift +++ b/14th-team5-iOS/Data/Sources/Privacy/Repositories/PrivacyViewRepository.swift @@ -20,7 +20,7 @@ public final class PrivacyViewRepository { public init() { } private let privacyAPIWorker: PrivacyAPIWorker = PrivacyAPIWorker() - private let signInHelper: AccountSignInHelper = AccountSignInHelper() + private let signInHelper: SignInHelper = SignInHelper() private let accessToken: String = App.Repository.token.accessToken.value?.accessToken ?? "" public var disposeBag: DisposeBag = DisposeBag() diff --git a/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme b/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme index 5f307e6b4..dfbc89c1f 100644 --- a/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme +++ b/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme @@ -1,6 +1,6 @@ Date: Thu, 6 Jun 2024 14:47:25 +0900 Subject: [PATCH 085/263] =?UTF-8?q?feat:=20=EC=A3=BC=EC=84=9D=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#555)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SignInAPI/Helpers/Apple/AppleSignIn+Rx.swift | 11 +++++++++++ .../SignInAPI/Helpers/Apple/AppleSignInHelper.swift | 2 ++ .../Sources/APIs/SignIn/SignInAPI/SignInHelper.swift | 10 +++++++++- 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/Apple/AppleSignIn+Rx.swift b/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/Apple/AppleSignIn+Rx.swift index 02b63c036..7381681d0 100644 --- a/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/Apple/AppleSignIn+Rx.swift +++ b/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/Apple/AppleSignIn+Rx.swift @@ -12,10 +12,13 @@ import AuthenticationServices import RxCocoa import RxSwift +// 참조: https://gist.github.com/iamchiwon/20aa57d4e8f6110bc3f79742c2fb6cc5 + class RxASAuthorizationControllerDelegateProxy: DelegateProxy, DelegateProxyType, ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding { private let disposeBag = DisposeBag() + // 애플 로그인 창을 띄울 윈도우 var presentationWindow: UIWindow = UIWindow() public init(controller: ASAuthorizationController) { @@ -26,12 +29,17 @@ class RxASAuthorizationControllerDelegateProxy: DelegateProxy() + + func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { return presentationWindow } + + // 로그인이 정상적으로 끝내면 Apple IDToken값을 알려주는 델리게이트 메서드 func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { switch authorization.credential { @@ -49,6 +57,8 @@ class RxASAuthorizationControllerDelegateProxy: DelegateProxy() // ? var signInState: Observable { self._signInState.asObservable() @@ -27,6 +28,7 @@ final class AppleSignInHelper: SignInHelperType { // MARK: - Sign In + // Apple 로그인 결과로 IdToken을 리턴하는 코드 func signIn(on window: UIWindow) -> Observable { // 그냥 바로 AccessToken 리턴하게 만들기 guard #available(iOS 13.0, *) else { self._signInState.accept(AccountSignInStateInfo(snsType: .apple)) diff --git a/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/SignInHelper.swift b/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/SignInHelper.swift index 576b5ddb1..f2d526843 100644 --- a/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/SignInHelper.swift +++ b/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/SignInHelper.swift @@ -31,6 +31,7 @@ final class SignInHelper: NSObject { let snsSignInResult = PublishRelay<(APIResult, AccountSignInStateInfo)>() func bind() { + // SignInState 스트림으로 토큰 받아오지 말고 다른 방안 강구하기 Observable.from(helpers.values.map { $0.signInState }).merge() .withUnretained(self) .flatMap { (_self, state) -> Single<(APIResult, AccountSignInStateInfo)> in @@ -48,6 +49,7 @@ final class SignInHelper: NSObject { .bind(onNext: { $0.snsSignInResult.accept($1) }) .disposed(by: self.disposeBag) + // 토큰 및 SNS Type 저장은 Repository에서 하기 snsSignInResult.map { $0.1 } .withUnretained(self) .bind(onNext: { UserDefaults.standard.snsType = $0.1.snsType.rawValue }) @@ -57,6 +59,8 @@ final class SignInHelper: NSObject { // MARK: SignIn Functions extension SignInHelper { + + // 소셜 로그인을 호출해서 소셜 Token을 받아오는 코드 func trySignInWith(sns: SNS, window: UIWindow?) -> Observable { guard let helper = helpers[sns.rawValue], let window = window else { return Observable.just(.failed) @@ -66,6 +70,8 @@ extension SignInHelper { // 로그인 버튼 클릭 -> SignInReactor -> SignInUseCase -> SignInRepo & API -> SignInUseCase -> SignInReactor // -> OAuthUseCase -> OAuthRepo & API -> OAuthUseCase -> SignInReactor -> 화면 전환으로 로직이 흐르게 만들기 + + // 우리 서버와 호출해서 실제로 쓰일 AccessToken과 RefreshToken을 받아오는 코드 func signInWith(snsType: SNS, snsToken: String) -> Single { return apiWorker.signInWith(snsType: snsType, snsToken: snsToken) .flatMap { @@ -87,7 +93,9 @@ extension SignInHelper { return .success } } - + + + // 소셜에 로그아웃 사실을 알리고, 우리가 가진 토큰을 삭제하는 코드 public func signOut(sns: String) -> Observable { return Observable.create { [weak self] observer in guard let signOut = self?.helpers[sns]?.signOut() else { return Disposables.create() } From a16cc3743422c3014594ec321d616a51596565c8 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Thu, 6 Jun 2024 15:45:46 +0900 Subject: [PATCH 086/263] =?UTF-8?q?feat:=20TokenKeychain,=20OAuthRepositor?= =?UTF-8?q?y=20=EA=B5=AC=ED=98=84=20(#555)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../KeychainWrapper/KeychainWrapperKey.swift | 5 + .../APIs/OAuth/OAuthAPI/OAuthAPIWorker.swift | 12 +- .../APIs/OAuth/OAuthAPI/OAuthAPIs.swift | 1 + .../OAuthAPI/Repository/OAuthRepository.swift | 107 ++++++++++++++++++ .../Storages/Keychain/Keychain+Token.swift | 49 +++++++- .../OAuth/Entities/AddFCMTokenRequest.swift | 16 +++ .../Entities/CreateNewMemberRequest.swift | 24 ++++ .../Entities/NativeSocialLoginRequest.swift | 16 +++ .../Entities/RefreshAccessTokenRequest.swift | 12 ++ .../Sources/OAuth/Entities}/SignInType.swift | 5 +- .../Repository/OAuthRepository.swift | 12 ++ .../UseCase/RegisterNewMemberUseCase.swift | 8 ++ 12 files changed, 255 insertions(+), 12 deletions(-) create mode 100644 14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/Repository/OAuthRepository.swift create mode 100644 14th-team5-iOS/Domain/Sources/OAuth/Entities/AddFCMTokenRequest.swift create mode 100644 14th-team5-iOS/Domain/Sources/OAuth/Entities/CreateNewMemberRequest.swift create mode 100644 14th-team5-iOS/Domain/Sources/OAuth/Entities/NativeSocialLoginRequest.swift create mode 100644 14th-team5-iOS/Domain/Sources/OAuth/Entities/RefreshAccessTokenRequest.swift rename 14th-team5-iOS/{Data/Sources/APIs/SignIn/SignInAPI => Domain/Sources/OAuth/Entities}/SignInType.swift (54%) create mode 100644 14th-team5-iOS/Domain/Sources/OAuth/UseCase/RegisterNewMemberUseCase.swift diff --git a/14th-team5-iOS/Core/Sources/BBStorages/KeychainWrapper/KeychainWrapperKey.swift b/14th-team5-iOS/Core/Sources/BBStorages/KeychainWrapper/KeychainWrapperKey.swift index 98e901ec9..70a8c4905 100644 --- a/14th-team5-iOS/Core/Sources/BBStorages/KeychainWrapper/KeychainWrapperKey.swift +++ b/14th-team5-iOS/Core/Sources/BBStorages/KeychainWrapper/KeychainWrapperKey.swift @@ -9,6 +9,11 @@ import Foundation public extension KeychainWrapper.Key { + // MARK: - SignIn + static let signInType: Self = "signInType" + static let idToken: Self = "idToken" // 소셜 로그인의 AccessToken + + // MARK: - OAuth static let accessToken: Self = "accessToken" static let refreshToken: Self = "refreshToken" static let fcmToken: Self = "fcmToken" diff --git a/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/OAuthAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/OAuthAPIWorker.swift index fe793745f..383042fd7 100644 --- a/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/OAuthAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/OAuthAPIWorker.swift @@ -11,9 +11,9 @@ import Foundation import RxSwift -typealias OAuthAPIWorker = OAuthAPIs.Worker +public typealias OAuthAPIWorker = OAuthAPIs.Worker extension OAuthAPIs { - final class Worker: APIWorker { + final public class Worker: APIWorker { static let queue = { ConcurrentDispatchQueueScheduler(queue: DispatchQueue(label: "OAuthAPIQueue", qos: .utility)) }() @@ -27,7 +27,7 @@ extension OAuthAPIs { // MARK: - Extensions -extension CalendarAPIWorker { +extension OAuthAPIWorker { // MARK: - Refresh Access Token @@ -69,8 +69,8 @@ extension CalendarAPIWorker { // MARK: - Sign In With SNS - public func signIn(_ with: SignInType, body: NativeSocialLoginRequestDTO) -> Single { - let spec = OAuthAPIs.signIn(with).spec + public func signIn(_ type: SignInType, body: NativeSocialLoginRequestDTO) -> Single { + let spec = OAuthAPIs.signIn(type).spec return request(spec: spec, jsonEncodable: body) .subscribe(on: Self.queue) @@ -87,7 +87,7 @@ extension CalendarAPIWorker { - // MARK: - Regieter FCM Token + // MARK: - Register FCM Token public func registerNewFCMToken(body: AddFCMTokenRequestDTO) -> Single { let spec = OAuthAPIs.registerFCMToken.spec diff --git a/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/OAuthAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/OAuthAPIs.swift index 98b4e82f7..abdb96967 100644 --- a/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/OAuthAPIs.swift +++ b/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/OAuthAPIs.swift @@ -6,6 +6,7 @@ // import Core +import Domain import Foundation public enum OAuthAPIs: API { 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 new file mode 100644 index 000000000..f9b90b0ec --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/Repository/OAuthRepository.swift @@ -0,0 +1,107 @@ +// +// OAuthRepository.swift +// Data +// +// Created by 김건우 on 6/6/24. +// + +import Core +import Domain + +import RxSwift +import RxCocoa + +public final class OAuthRepository: OAuthRepositoryProtocol { + + // MARK: - Properties + public var disposeBag = DisposeBag() + + public var oAuthApiWorker = OAuthAPIWorker() + public var tokenKeychainStorage = TokenKeychain() + + public init() { } + +} + +extension OAuthRepository { + + // MARK: - Refresh Access Token + + public func refreshAccessToken(body: RefreshAccessTokenRequest) -> Observable { + let body = RefreshAccessTokenRequestDTO( + refreshToken: body.refreshToken + ) + return oAuthApiWorker.refreshAccessToken(body: body) + .observe(on: RxSchedulers.main) + .do(onSuccess: { [weak self] in + guard + let keychain = self?.tokenKeychainStorage + else { return } + keychain.saveAccessToken($0?.accessToken) + keychain.saveRefreshToken($0?.refreshToken) + }) + .asObservable() + } + + + // MARK: - Register New Member + + public func registerNewMember(body: CreateNewMemberRequest) -> Observable { + let body = CreateNewMemberRequestDTO( + memberName: body.memberName, + dayOfBirth: body.dayOfBirth, + profileImageUrl: body.profileImageUrl + ) + return oAuthApiWorker.registerNewMember(body: body) + .observe(on: RxSchedulers.main) + .do(onSuccess: { [weak self] in + guard + let keychain = self?.tokenKeychainStorage + else { return } + keychain.saveAccessToken($0?.accessToken) + keychain.saveRefreshToken($0?.refreshToken) + }) + .asObservable() + } + + + // MARK: - Sign In With SNS + + public func signIn(_ type: SignInType, body: NativeSocialLoginRequest) -> Observable { + let body = NativeSocialLoginRequestDTO( + accessToken: body.accessToken + ) + return oAuthApiWorker.signIn(type, body: body) + .observe(on: RxSchedulers.main) + .do(onSuccess: { [weak self] in + guard + let keychain = self?.tokenKeychainStorage + else { return } + keychain.saveAccessToken($0?.accessToken) + keychain.saveRefreshToken($0?.refreshToken) + }) + .asObservable() + } + + + // MARK: - Register FCM Token + + public func registerNewFCMToken(body: AddFCMTokenRequest) -> Observable { + let body = AddFCMTokenRequestDTO( + fcmToken: body.fcmToken + ) + return oAuthApiWorker.registerNewFCMToken(body: body) + .observe(on: RxSchedulers.main) + .asObservable() + } + + + // MARK: - Delete FCM Token + + public func deleteFCMToken(fcmToken token: String) -> Observable { + return oAuthApiWorker.deleteFCMToken(fcmToken: token) + .observe(on: RxSchedulers.main) + .asObservable() + } + +} diff --git a/14th-team5-iOS/Data/Sources/Storages/Keychain/Keychain+Token.swift b/14th-team5-iOS/Data/Sources/Storages/Keychain/Keychain+Token.swift index c382f4b0c..635b84b81 100644 --- a/14th-team5-iOS/Data/Sources/Storages/Keychain/Keychain+Token.swift +++ b/14th-team5-iOS/Data/Sources/Storages/Keychain/Keychain+Token.swift @@ -8,17 +8,38 @@ import Core import Foundation +public protocol TokenKeychainType: KeychainType { + func saveIdToken(_ idToken: String?) + func loadIdToken() -> String? + + func saveAccessToken(_ accessToken: String?) + func loadAccessToken() -> String? + + func saveRefreshToken(_ refreshToken: String?) + func loadRefreshToken() -> String? + + func saveFCMToken(_ fcmToken: String?) + func loadFCMToken() -> String? +} -// NOTE: - 예시 코드 - -final public class TokenKeychain: KeychainType { +final public class TokenKeychain: TokenKeychainType { // MARK: - Intializer public init() { } + // MARK: - IdToken + public func saveIdToken(_ idToken: String?) { + keychain[.idToken] = idToken + } + + public func loadIdToken() -> String? { + keychain[.idToken] + } + + // MARK: - AccessToken - public func saveAccessToken(_ accessToken: String) { + public func saveAccessToken(_ accessToken: String?) { keychain[.accessToken] = accessToken } @@ -26,6 +47,24 @@ final public class TokenKeychain: KeychainType { keychain[.accessToken] } - // ... + + // MARK: - RefreshToken + public func saveRefreshToken(_ refreshToken: String?) { + keychain[.refreshToken] = refreshToken + } + + public func loadRefreshToken() -> String? { + keychain[.refreshToken] + } + + + // MARK: - FCM Token + public func saveFCMToken(_ fcmToken: String?) { + keychain[.fcmToken] = fcmToken + } + + public func loadFCMToken() -> String? { + keychain[.fcmToken] + } } diff --git a/14th-team5-iOS/Domain/Sources/OAuth/Entities/AddFCMTokenRequest.swift b/14th-team5-iOS/Domain/Sources/OAuth/Entities/AddFCMTokenRequest.swift new file mode 100644 index 000000000..83f4f5554 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/OAuth/Entities/AddFCMTokenRequest.swift @@ -0,0 +1,16 @@ +// +// AddFCMTokenRequest.swift +// Domain +// +// Created by 김건우 on 6/6/24. +// + +import Foundation + +public struct AddFCMTokenRequest { + public var fcmToken: String + + public init(fcmToken: String) { + self.fcmToken = fcmToken + } +} diff --git a/14th-team5-iOS/Domain/Sources/OAuth/Entities/CreateNewMemberRequest.swift b/14th-team5-iOS/Domain/Sources/OAuth/Entities/CreateNewMemberRequest.swift new file mode 100644 index 000000000..5875dc15b --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/OAuth/Entities/CreateNewMemberRequest.swift @@ -0,0 +1,24 @@ +// +// CreateNewMemberRequest.swift +// Domain +// +// Created by 김건우 on 6/6/24. +// + +import Foundation + +public struct CreateNewMemberRequest { + public var memberName: String + public var dayOfBirth: String + public var profileImageUrl: String + + public init( + memberName: String, + dayOfBirth: String, + profileImageUrl: String + ) { + self.memberName = memberName + self.dayOfBirth = dayOfBirth + self.profileImageUrl = profileImageUrl + } +} diff --git a/14th-team5-iOS/Domain/Sources/OAuth/Entities/NativeSocialLoginRequest.swift b/14th-team5-iOS/Domain/Sources/OAuth/Entities/NativeSocialLoginRequest.swift new file mode 100644 index 000000000..489780ca5 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/OAuth/Entities/NativeSocialLoginRequest.swift @@ -0,0 +1,16 @@ +// +// NativeSocialLoginRequest.swift +// Domain +// +// Created by 김건우 on 6/6/24. +// + +import Foundation + +public struct NativeSocialLoginRequest { + public var accessToken: String + + public init(accessToken: String) { + self.accessToken = accessToken + } +} diff --git a/14th-team5-iOS/Domain/Sources/OAuth/Entities/RefreshAccessTokenRequest.swift b/14th-team5-iOS/Domain/Sources/OAuth/Entities/RefreshAccessTokenRequest.swift new file mode 100644 index 000000000..88742fe27 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/OAuth/Entities/RefreshAccessTokenRequest.swift @@ -0,0 +1,12 @@ +// +// RefreshAccessTokenRequest.swift +// Domain +// +// Created by 김건우 on 6/6/24. +// + +import Foundation + +public struct RefreshAccessTokenRequest { + public var refreshToken: String +} diff --git a/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/SignInType.swift b/14th-team5-iOS/Domain/Sources/OAuth/Entities/SignInType.swift similarity index 54% rename from 14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/SignInType.swift rename to 14th-team5-iOS/Domain/Sources/OAuth/Entities/SignInType.swift index 442ffe430..495d96b0b 100644 --- a/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/SignInType.swift +++ b/14th-team5-iOS/Domain/Sources/OAuth/Entities/SignInType.swift @@ -1,12 +1,15 @@ // // SignInType.swift -// Data +// Domain // // Created by 김건우 on 6/6/24. // import Foundation +// NOTE: - App, Data 모듈에 모두 필요해 +// Domain 모듈에 두는데 어디에 어떻게 두면 좋을지 모르겠어요. + public enum SignInType: String { case kakao = "KAKAO" case apple = "APPLE" diff --git a/14th-team5-iOS/Domain/Sources/OAuth/Interfaces/Repository/OAuthRepository.swift b/14th-team5-iOS/Domain/Sources/OAuth/Interfaces/Repository/OAuthRepository.swift index 602d7da04..de9243d63 100644 --- a/14th-team5-iOS/Domain/Sources/OAuth/Interfaces/Repository/OAuthRepository.swift +++ b/14th-team5-iOS/Domain/Sources/OAuth/Interfaces/Repository/OAuthRepository.swift @@ -6,3 +6,15 @@ // import Foundation + +import RxSwift + +public protocol OAuthRepositoryProtocol { + var disposeBag: DisposeBag { get } + + func refreshAccessToken(body: RefreshAccessTokenRequest) -> Observable + func registerNewMember(body: CreateNewMemberRequest) -> Observable + func signIn(_ type: SignInType, body: NativeSocialLoginRequest) -> Observable + func registerNewFCMToken(body: AddFCMTokenRequest) -> Observable + func deleteFCMToken(fcmToken token: String) -> Observable +} diff --git a/14th-team5-iOS/Domain/Sources/OAuth/UseCase/RegisterNewMemberUseCase.swift b/14th-team5-iOS/Domain/Sources/OAuth/UseCase/RegisterNewMemberUseCase.swift new file mode 100644 index 000000000..13799e591 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/OAuth/UseCase/RegisterNewMemberUseCase.swift @@ -0,0 +1,8 @@ +// +// RegisterNewMemberUseCase.swift +// Domain +// +// Created by 김건우 on 6/6/24. +// + +import Foundation From 86e054cfe5729730450f78c71c08619793b38cc3 Mon Sep 17 00:00:00 2001 From: Do-hyun-Kim Date: Thu, 6 Jun 2024 18:30:15 +0900 Subject: [PATCH 087/263] =?UTF-8?q?fix:=20MissionContentResponseDTO,=20Get?= =?UTF-8?q?TodayMissionResponseDTO=EB=A1=9C=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EB=84=A4=EC=9D=B4=EB=B0=8D=20=EC=88=98=EC=A0=95=20-=20MissionA?= =?UTF-8?q?PIWorker=20MissionContentResponseDTO,=20GetTodayMissionResponse?= =?UTF-8?q?DTO=20=EB=B0=98=ED=99=98=20=EA=B0=92=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ssionDTO.swift => GetTodayMissionResponseDTO.swift} | 6 +++--- ...ontentDTO.swift => MissionContentResponseDTO.swift} | 10 ++++------ .../Sources/Mission/MissionAPI/MissionAPIWorker.swift | 4 ++-- 3 files changed, 9 insertions(+), 11 deletions(-) rename 14th-team5-iOS/Data/Sources/Mission/DataMapping/{TodayMissionDTO.swift => GetTodayMissionResponseDTO.swift} (65%) rename 14th-team5-iOS/Data/Sources/Mission/DataMapping/{MissionContentDTO.swift => MissionContentResponseDTO.swift} (72%) diff --git a/14th-team5-iOS/Data/Sources/Mission/DataMapping/TodayMissionDTO.swift b/14th-team5-iOS/Data/Sources/Mission/DataMapping/GetTodayMissionResponseDTO.swift similarity index 65% rename from 14th-team5-iOS/Data/Sources/Mission/DataMapping/TodayMissionDTO.swift rename to 14th-team5-iOS/Data/Sources/Mission/DataMapping/GetTodayMissionResponseDTO.swift index 62d83f758..acfd41b4c 100644 --- a/14th-team5-iOS/Data/Sources/Mission/DataMapping/TodayMissionDTO.swift +++ b/14th-team5-iOS/Data/Sources/Mission/DataMapping/GetTodayMissionResponseDTO.swift @@ -1,15 +1,15 @@ // -// TodayMissionRequestDTO.swift +// GetTodayMissionResponseDTO.swift // Data // -// Created by 마경미 on 21.04.24. +// Created by Kim dohyun on 6/6/24. // import Foundation import Domain -struct GetTodayMissionResponse: Codable { +struct GetTodayMissionResponseDTO: Codable { let date: String let id: String let content: String diff --git a/14th-team5-iOS/Data/Sources/Mission/DataMapping/MissionContentDTO.swift b/14th-team5-iOS/Data/Sources/Mission/DataMapping/MissionContentResponseDTO.swift similarity index 72% rename from 14th-team5-iOS/Data/Sources/Mission/DataMapping/MissionContentDTO.swift rename to 14th-team5-iOS/Data/Sources/Mission/DataMapping/MissionContentResponseDTO.swift index 2db8216e1..b2b2e9280 100644 --- a/14th-team5-iOS/Data/Sources/Mission/DataMapping/MissionContentDTO.swift +++ b/14th-team5-iOS/Data/Sources/Mission/DataMapping/MissionContentResponseDTO.swift @@ -1,16 +1,15 @@ // -// MissionContentDTO.swift +// MissionContentResponseDTO.swift // Data // -// Created by Kim dohyun on 5/8/24. +// Created by Kim dohyun on 6/6/24. // import Foundation import Domain - -struct MissionContentDTO: Decodable { +struct MissionContentResponseDTO: Decodable { let missionId: String let missionContent: String @@ -21,8 +20,7 @@ struct MissionContentDTO: Decodable { } } - -extension MissionContentDTO { +extension MissionContentResponseDTO { func toDomain() -> MissionContentResponse { return .init( missionId: missionId, diff --git a/14th-team5-iOS/Data/Sources/Mission/MissionAPI/MissionAPIWorker.swift b/14th-team5-iOS/Data/Sources/Mission/MissionAPI/MissionAPIWorker.swift index 698ef8d3f..821754e78 100644 --- a/14th-team5-iOS/Data/Sources/Mission/MissionAPI/MissionAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Mission/MissionAPI/MissionAPIWorker.swift @@ -37,7 +37,7 @@ extension MissionAPIWorker { debugPrint("Join Family Result: \(str)") } } - .map(GetTodayMissionResponse.self) + .map(GetTodayMissionResponseDTO.self) .catchAndReturn(nil) .map { $0?.toDomain() } .asSingle() @@ -61,7 +61,7 @@ extension MissionAPIWorker { debugPrint("Mission Content Result: \(str)") } } - .map(MissionContentDTO.self) + .map(MissionContentResponseDTO.self) .catchAndReturn(nil) .map { $0?.toDomain() } .asSingle() From 8d2db1fc09cfd838e27bfdd4cb4813ff9d19fbb9 Mon Sep 17 00:00:00 2001 From: Do-hyun-Kim Date: Thu, 6 Jun 2024 18:57:54 +0900 Subject: [PATCH 088/263] =?UTF-8?q?refactor:=20PostRequestDTO,=20PostReque?= =?UTF-8?q?stListDTO=20=ED=8C=8C=EC=9D=BC=20=EC=B6=94=EA=B0=80=20-=20PostL?= =?UTF-8?q?istResponseDTO=20=EB=AA=A8=EB=93=88=EB=82=B4=EC=97=90=20?= =?UTF-8?q?=EC=9E=88=EB=8A=94=20PostRequestDTO=20=ED=83=80=EC=9E=85=20Post?= =?UTF-8?q?RequestDTO=20=ED=8C=8C=EC=9D=BC=EB=A1=9C=20=EC=9D=B4=EC=A0=84?= =?UTF-8?q?=20-=20PostListDTO=20=EB=84=A4=EC=9D=B4=EB=B0=8D=EC=9D=84=20Pos?= =?UTF-8?q?tListResultsDTO=EB=A1=9C=20=ED=83=80=EC=9E=85=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DataMapping/PostListRequestDTO.swift | 18 +++ .../PostListResponseDTO.swift} | 22 ++-- .../PostList/DataMapping/PostRequestDTO.swift | 20 ++++ .../PostResponseDTO.swift} | 14 +-- .../xcschemes/Bibbi-Workspace.xcscheme | 106 +++++++++--------- 5 files changed, 100 insertions(+), 80 deletions(-) create mode 100644 14th-team5-iOS/Data/Sources/Post/PostList/DataMapping/PostListRequestDTO.swift rename 14th-team5-iOS/Data/Sources/Post/PostList/{DTO/PostListDTO.swift => DataMapping/PostListResponseDTO.swift} (73%) create mode 100644 14th-team5-iOS/Data/Sources/Post/PostList/DataMapping/PostRequestDTO.swift rename 14th-team5-iOS/Data/Sources/Post/PostList/{DTO/PostDTO.swift => DataMapping/PostResponseDTO.swift} (69%) diff --git a/14th-team5-iOS/Data/Sources/Post/PostList/DataMapping/PostListRequestDTO.swift b/14th-team5-iOS/Data/Sources/Post/PostList/DataMapping/PostListRequestDTO.swift new file mode 100644 index 000000000..bf875b4ba --- /dev/null +++ b/14th-team5-iOS/Data/Sources/Post/PostList/DataMapping/PostListRequestDTO.swift @@ -0,0 +1,18 @@ +// +// PostListRequestDTO.swift +// Data +// +// Created by Kim dohyun on 6/6/24. +// + +import Foundation + + +struct PostListRequestDTO: Codable { + let page: Int + let size: Int + let date: String + let memberId: String? + let sort: String + let type: String +} diff --git a/14th-team5-iOS/Data/Sources/Post/PostList/DTO/PostListDTO.swift b/14th-team5-iOS/Data/Sources/Post/PostList/DataMapping/PostListResponseDTO.swift similarity index 73% rename from 14th-team5-iOS/Data/Sources/Post/PostList/DTO/PostListDTO.swift rename to 14th-team5-iOS/Data/Sources/Post/PostList/DataMapping/PostListResponseDTO.swift index 0b55d2b4e..2a7f5eea0 100644 --- a/14th-team5-iOS/Data/Sources/Post/PostList/DTO/PostListDTO.swift +++ b/14th-team5-iOS/Data/Sources/Post/PostList/DataMapping/PostListResponseDTO.swift @@ -1,23 +1,15 @@ // -// PostListDTO.swift +// PostListResponseDTO.swift // Data // -// Created by 마경미 on 25.12.23. +// Created by Kim dohyun on 6/6/24. // import Foundation -import Domain -public struct PostListRequestDTO: Codable { - let page: Int - let size: Int - let date: String - let memberId: String? - let sort: String - let type: String -} +import Domain -struct PostListDTO: Codable { +struct PostListResultsDTO: Codable { let postId: String let authorId: String let type: String @@ -29,11 +21,11 @@ struct PostListDTO: Codable { let createdAt: String } -extension PostListDTO { +extension PostListResultsDTO { func toDomain() -> PostListData { let author = FamilyUserDefaults.load(memberId: authorId) return .init( - postId: postId, + postId: postId, missionId: missionId, missionType: type, author: author, @@ -51,7 +43,7 @@ struct PostListResponseDTO: Codable { let totalPage: Int let itemPerPage: Int let hasNext: Bool - let results: [PostListDTO] + let results: [PostListResultsDTO] } extension PostListResponseDTO { diff --git a/14th-team5-iOS/Data/Sources/Post/PostList/DataMapping/PostRequestDTO.swift b/14th-team5-iOS/Data/Sources/Post/PostList/DataMapping/PostRequestDTO.swift new file mode 100644 index 000000000..716a52d20 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/Post/PostList/DataMapping/PostRequestDTO.swift @@ -0,0 +1,20 @@ +// +// PostRequestDTO.swift +// Data +// +// Created by Kim dohyun on 6/6/24. +// + +import Foundation + +import Domain + +public struct PostRequestDTO: Codable { + let postId: String +} + +extension PostRequestDTO { + func toDomain() -> PostQuery { + return .init(postId: postId) + } +} diff --git a/14th-team5-iOS/Data/Sources/Post/PostList/DTO/PostDTO.swift b/14th-team5-iOS/Data/Sources/Post/PostList/DataMapping/PostResponseDTO.swift similarity index 69% rename from 14th-team5-iOS/Data/Sources/Post/PostList/DTO/PostDTO.swift rename to 14th-team5-iOS/Data/Sources/Post/PostList/DataMapping/PostResponseDTO.swift index 9755f339d..e4a0f1cf1 100644 --- a/14th-team5-iOS/Data/Sources/Post/PostList/DTO/PostDTO.swift +++ b/14th-team5-iOS/Data/Sources/Post/PostList/DataMapping/PostResponseDTO.swift @@ -1,23 +1,13 @@ // -// PostDTO.swift +// PostResponseDTO.swift // Data // -// Created by 마경미 on 30.12.23. +// Created by Kim dohyun on 6/6/24. // import Foundation import Domain -public struct PostRequestDTO: Codable { - let postId: String -} - -extension PostRequestDTO { - func toDomain() -> PostQuery { - return .init(postId: postId) - } -} - struct PostResponseDTO: Codable { let postId: String let authorId: String diff --git a/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme b/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme index 5f307e6b4..7023994af 100644 --- a/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme +++ b/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme @@ -280,52 +280,10 @@ buildForAnalyzing = "YES"> - - - - - - - - - - - - + BlueprintIdentifier = "4160F70E94342FB432F5ABF1" + BuildableName = "GoogleAppMeasurementTarget.framework" + BlueprintName = "GoogleAppMeasurementTarget" + ReferencedContainer = "container:Tuist/Dependencies/SwiftPackageManager/.build/checkouts/GoogleAppMeasurement/GoogleAppMeasurement.xcodeproj"> + BlueprintIdentifier = "08D12528663D0C8DD1A99BCB" + BuildableName = "GoogleDataTransport.framework" + BlueprintName = "GoogleDataTransport" + ReferencedContainer = "container:Tuist/Dependencies/SwiftPackageManager/.build/checkouts/GoogleDataTransport/GoogleDataTransport.xcodeproj"> @@ -468,6 +426,34 @@ ReferencedContainer = "container:Tuist/Dependencies/SwiftPackageManager/.build/checkouts/GoogleUtilities/GoogleUtilities.xcodeproj"> + + + + + + + + + + + + Date: Thu, 6 Jun 2024 21:53:20 +0900 Subject: [PATCH 089/263] =?UTF-8?q?refactor:=20PostRepository=20Class=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EB=B0=8F=20PostRepositoryProtocol=20?= =?UTF-8?q?=EC=B1=84=ED=83=9D=20-=20=EA=B0=81=20DIContainer=20PostReposito?= =?UTF-8?q?ry=20=EC=9D=B8=EC=8A=A4=ED=84=B4=EC=8A=A4=20=EB=B0=98=ED=99=98?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../App/Sources/Manager/DeepLinkManager.swift | 2 +- .../Dependency/MainPostViewDIContainer.swift | 2 +- .../Dependency/PostListsDIContainer.swift | 2 +- .../Dependency/ProfileFeedDIContainer.swift | 2 +- .../PostListAPI/PostListAPIWorker.swift | 2 +- .../PostList/Repository/PostRepository.swift | 27 +++++++++++++++++++ 6 files changed, 32 insertions(+), 5 deletions(-) create mode 100644 14th-team5-iOS/Data/Sources/Post/PostList/Repository/PostRepository.swift diff --git a/14th-team5-iOS/App/Sources/Manager/DeepLinkManager.swift b/14th-team5-iOS/App/Sources/Manager/DeepLinkManager.swift index e99804f56..f37c01264 100644 --- a/14th-team5-iOS/App/Sources/Manager/DeepLinkManager.swift +++ b/14th-team5-iOS/App/Sources/Manager/DeepLinkManager.swift @@ -31,7 +31,7 @@ final class DeepLinkManager { // 이번 3차 끝나고, postdetailviewcontroller에서 post 불러오는 형태로 바꿔보겠습니다. let disposeBag: DisposeBag = DisposeBag() - let postRepository: PostListRepositoryProtocol = PostListAPIs.Worker() + let postRepository: PostListRepositoryProtocol = PostRepository() lazy var postUseCase: PostListUseCaseProtocol = PostListUseCase(postListRepository: postRepository) private init() {} diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Dependency/MainPostViewDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Dependency/MainPostViewDIContainer.swift index dde7af3be..bc9b8c393 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Dependency/MainPostViewDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Dependency/MainPostViewDIContainer.swift @@ -31,7 +31,7 @@ final class MainPostViewDIContainer { extension MainPostViewDIContainer { private func makePostRepository() -> PostListRepositoryProtocol { - return PostListAPIs.Worker() + return PostRepository() } func makeUploadPostRepository() -> UploadPostRepositoryProtocol { diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/PostListsDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/PostListsDIContainer.swift index 63b04b372..8ba694eb6 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/PostListsDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/PostListsDIContainer.swift @@ -37,7 +37,7 @@ final class PostListsDIContainer { } func makePostRepository() -> PostListRepositoryProtocol { - return PostListAPIs.Worker() + return PostRepository() } func makeUploadPostRepository() -> UploadPostRepositoryProtocol { diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/Dependency/ProfileFeedDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/Dependency/ProfileFeedDIContainer.swift index 2a70f168e..0f90512e5 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/Dependency/ProfileFeedDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/Dependency/ProfileFeedDIContainer.swift @@ -40,7 +40,7 @@ final class ProfileFeedDIContainer { } func makeRepository() -> Repository { - return PostListAPIs.Worker() + return PostRepository() } diff --git a/14th-team5-iOS/Data/Sources/Post/PostList/PostListAPI/PostListAPIWorker.swift b/14th-team5-iOS/Data/Sources/Post/PostList/PostListAPI/PostListAPIWorker.swift index a5b853256..aee5d0000 100644 --- a/14th-team5-iOS/Data/Sources/Post/PostList/PostListAPI/PostListAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Post/PostList/PostListAPI/PostListAPIWorker.swift @@ -27,7 +27,7 @@ extension PostListAPIs { } } -extension PostListAPIWorker: PostListRepositoryProtocol { +extension PostListAPIWorker { public func fetchPostDetail(query: PostQuery) -> Single { return Observable.just(()) .withLatestFrom(self._headers) diff --git a/14th-team5-iOS/Data/Sources/Post/PostList/Repository/PostRepository.swift b/14th-team5-iOS/Data/Sources/Post/PostList/Repository/PostRepository.swift new file mode 100644 index 000000000..27cb13676 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/Post/PostList/Repository/PostRepository.swift @@ -0,0 +1,27 @@ +// +// PostRepository.swift +// Data +// +// Created by Kim dohyun on 6/6/24. +// + +import Foundation + +import Domain +import RxSwift + +public final class PostRepository: PostListRepositoryProtocol { + + private let disposeBag: DisposeBag = DisposeBag() + private let postListsAPIWorker: PostListAPIWorker = PostListAPIWorker() + + public init() { } + + public func fetchPostDetail(query: Domain.PostQuery) -> RxSwift.Single { + return postListsAPIWorker.fetchPostDetail(query: query) + } + + public func fetchTodayPostList(query: Domain.PostListQuery) -> RxSwift.Single { + return postListsAPIWorker.fetchTodayPostList(query: query) + } +} From 460b27f95436b306eaeb031dd6f424ded5646246 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Fri, 7 Jun 2024 12:54:38 +0900 Subject: [PATCH 090/263] =?UTF-8?q?fix:=20=EB=82=B4=EB=B9=84=EA=B2=8C?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EB=B0=94=20=EA=B8=B0=EB=B3=B8=20=EB=86=92?= =?UTF-8?q?=EC=9D=B4=20=EC=88=98=EC=A0=95=20(#546)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BBNavigationBarView.swift | 18 +++++++++--------- .../Base/BBNavigationViewController.swift | 8 ++++---- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/14th-team5-iOS/Core/Sources/BBCommons/BBNavigationBarView/BBNavigationBarView.swift b/14th-team5-iOS/Core/Sources/BBCommons/BBNavigationBarView/BBNavigationBarView.swift index 1b535783a..3c296c972 100644 --- a/14th-team5-iOS/Core/Sources/BBCommons/BBNavigationBarView/BBNavigationBarView.swift +++ b/14th-team5-iOS/Core/Sources/BBCommons/BBNavigationBarView/BBNavigationBarView.swift @@ -63,7 +63,7 @@ public final class BBNavigationBarView: UIView { } } - // 오른쪽 버튼의 스타일을 설정합니다. + /// 오른쪽 버튼의 스타일을 설정합니다. public var rightBarButtonItem: TopBarButtonStyle? { didSet { rightBarButton.setImage( @@ -74,49 +74,49 @@ public final class BBNavigationBarView: UIView { } } - // Navigation Image의 크기를 설정합니다. 기본값은 1.0입니다. + /// Navigation Image의 크기를 설정합니다. 기본값은 1.0입니다. public var navigationImageScale: CGFloat = 1.0 { didSet { setupNavigationImageScale(navigationImageScale) } } - // 왼쪽 버튼 이미지의 크기를 설정합니다. 기본값은 1.0입니다. + /// 왼쪽 버튼 이미지의 크기를 설정합니다. 기본값은 1.0입니다. public var leftBarButtonItemScale: CGFloat = 1.0 { didSet { setupLeftButtonImageScale(leftBarButtonItemScale) } } - // 오른쪽 버튼 이미지의 크기를 설정합니다. 기본값은 1.0입니다. + /// 오른쪽 버튼 이미지의 크기를 설정합니다. 기본값은 1.0입니다. public var rightBarButtonItemScale: CGFloat = 1.0 { didSet { setupRightButtonImageScale(rightBarButtonItemScale) } } - // Navigation Title의 색상을 설정합니다. + /// Navigation Title의 색상을 설정합니다. public var navigationTitleTextColor: UIColor = UIColor.gray200 { didSet { navigationTitleLabel.textColor = navigationTitleTextColor } } - // 왼쪽 버튼의 강조 색상을 설정합니다. + /// 왼쪽 버튼의 강조 색상을 설정합니다. public var leftBarButtonItemTintColor: UIColor = UIColor.gray300 { didSet { leftBarButton.tintColor = leftBarButtonItemTintColor } } - // 오른쪽 버튼의 강조 색상을 설정합니다. + /// 오른쪽 버튼의 강조 색상을 설정합니다. public var rightBarButtonItemTintColor: UIColor = UIColor.gray300 { didSet { rightBarButton.tintColor = rightBarButtonItemTintColor } } - // 왼쪽 버튼이 leading으로부터 얼마나 떨어져 있는지 설정합니다. + /// 왼쪽 버튼이 leading으로부터 얼마나 떨어져 있는지 설정합니다. public var leftBarButtonItemYOffset: CGFloat = 0.0 { didSet { leftBarButton.snp.updateConstraints { @@ -125,7 +125,7 @@ public final class BBNavigationBarView: UIView { } } - // 오른쪽 버튼이 leading으로부터 얼마나 떨어져 있는지 설정합니다. + /// 오른쪽 버튼이 leading으로부터 얼마나 떨어져 있는지 설정합니다. public var rightBarButtonItemYOffset: CGFloat = 0.0 { didSet { rightBarButton.snp.updateConstraints { diff --git a/14th-team5-iOS/Core/Sources/Base/BBNavigationViewController.swift b/14th-team5-iOS/Core/Sources/Base/BBNavigationViewController.swift index f7dd57013..91ac5f41a 100644 --- a/14th-team5-iOS/Core/Sources/Base/BBNavigationViewController.swift +++ b/14th-team5-iOS/Core/Sources/Base/BBNavigationViewController.swift @@ -17,8 +17,8 @@ import ReactorKit /// 이 ViewController를 상속하는 ViewController는 `View`가 아닌 `ContentView`에 UI를 배치해야 합니다. /// `ContentView`는 NavigationBar 영역을 제외한 나머지 공간을 차지하는 View입니다. 이 View는 NavigationBar의 높이에 따라 동적으로 변합니다. /// -/// 삐삐 스타일의 NavigationBar의 UI나 스타일을 변경해야 한다면, 직접 해당 NavigationBar에 정의되어 있는 메서드나 프로퍼티를 통해 변경할 수 있습니다. -/// 가령, NavigationBar의 높이를 바꿔야 한다면. `setHeight(_:)` 메서드를 호출하면 됩니다. +/// 삐삐 스타일의 NavigationBar의 UI나 스타일을 변경해야 한다면, 직접 관련 메서드나 프로퍼티를 통해 변경할 수 있습니다. +/// 가령, NavigationBar의 높이를 바꿔야 한다면. `setNavigationBarHeight(_:)` 메서드를 호출하면 됩니다. /// /// LeftBarButton의 타입이 .arrowLeft나 .xmark라면 popViewController() 구현이 기본으로 제공됩니다. /// 왼쪽, 오른쪽 버튼의 동작을 정의하고 싶다면 `navigationBarView.rx.didTapRightButton`과 같이 정의하면 됩니다. @@ -76,7 +76,7 @@ open class BBNavigationViewController: ReactorViewController where R: Reac navigationBarView.snp.makeConstraints { $0.top.equalTo(view.safeAreaLayoutGuide) $0.horizontalEdges.equalToSuperview() - $0.height.equalTo(52) + $0.height.equalTo(42) // 내비게이션 바 기본 높이 42 } contentView.snp.makeConstraints { @@ -99,7 +99,7 @@ open class BBNavigationViewController: ReactorViewController where R: Reac extension BBNavigationViewController { /// NavigationBar의 높이를 바꿉니다. - public func setHeight(_ height: CGFloat) { + public func setNavigationBarHeight(_ height: CGFloat) { navigationBarView.snp.updateConstraints { $0.top.equalTo(view.safeAreaLayoutGuide) $0.horizontalEdges.equalToSuperview() From 4005a72df94f521290e5e6228077540cedeb2b89 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Fri, 7 Jun 2024 13:18:53 +0900 Subject: [PATCH 091/263] =?UTF-8?q?feat:=20=EA=B5=AC=20SignIn=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=9D=B4=EB=A6=84=EC=9D=84=20Account=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20(#555)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Helpers/Apple/AppleSignInHelper.swift | 69 +--------- .../Helpers/Kakao/KakaoSignInHelper.swift | 75 +--------- .../SignInAPI/Helpers/SignInHelperType.swift | 14 +- .../SignIn/SignInAPI/SignInAPIConfig.swift | 16 +-- .../AccountAPI/AccountSignInAPIConfig.swift | 18 +++ .../AccountAPI/AccountSignInHelper.swift} | 4 +- .../Helpers/AccountSignInHelperType.swift | 19 +++ .../Helpers/Apple/AccountAppleSignIn+Rx.swift | 128 ++++++++++++++++++ .../Apple/AccountAppleSignInHelper.swift | 73 ++++++++++ .../Helpers/Kakao/AccountKakaoSignIn+Rx.swift | 38 ++++++ .../Kakao/AccountKakaoSignInHelper.swift | 79 +++++++++++ .../AccountRepository/AccountRepository.swift | 2 +- .../Repositories/PrivacyViewRepository.swift | 2 +- .../DataMapping/TokenResultEntity.swift | 8 ++ 14 files changed, 379 insertions(+), 166 deletions(-) create mode 100644 14th-team5-iOS/Data/Sources/Account/AccountAPI/AccountSignInAPIConfig.swift rename 14th-team5-iOS/Data/Sources/{APIs/SignIn/SignInAPI/SignInHelper.swift => Account/AccountAPI/AccountSignInHelper.swift} (98%) create mode 100644 14th-team5-iOS/Data/Sources/Account/AccountAPI/Helpers/AccountSignInHelperType.swift create mode 100644 14th-team5-iOS/Data/Sources/Account/AccountAPI/Helpers/Apple/AccountAppleSignIn+Rx.swift create mode 100644 14th-team5-iOS/Data/Sources/Account/AccountAPI/Helpers/Apple/AccountAppleSignInHelper.swift create mode 100644 14th-team5-iOS/Data/Sources/Account/AccountAPI/Helpers/Kakao/AccountKakaoSignIn+Rx.swift create mode 100644 14th-team5-iOS/Data/Sources/Account/AccountAPI/Helpers/Kakao/AccountKakaoSignInHelper.swift create mode 100644 14th-team5-iOS/Domain/Sources/SignIn/DataMapping/TokenResultEntity.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/Apple/AppleSignInHelper.swift b/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/Apple/AppleSignInHelper.swift index 5bb131c62..c263b0cae 100644 --- a/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/Apple/AppleSignInHelper.swift +++ b/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/Apple/AppleSignInHelper.swift @@ -2,72 +2,7 @@ // AppleSignInHelper.swift // Data // -// Created by geonhui Yu on 12/20/23. +// Created by 김건우 on 6/7/24. // -import Core -import UIKit -import Domain -import AuthenticationServices - -import RxSwift -import RxCocoa - -final class AppleSignInHelper: SignInHelperType { - - // MARK: - Properties - - private var disposeBag = DisposeBag() - - // 간접적으로 스트림 넘기지 말고, 그냥 메서드에서 토큰값 반환하기 - private let _signInState = PublishRelay() // ? - var signInState: Observable { - self._signInState.asObservable() - } // ? - - - // MARK: - Sign In - - // Apple 로그인 결과로 IdToken을 리턴하는 코드 - func signIn(on window: UIWindow) -> Observable { // 그냥 바로 AccessToken 리턴하게 만들기 - guard #available(iOS 13.0, *) else { - self._signInState.accept(AccountSignInStateInfo(snsType: .apple)) - return Observable.just(.failed) - } - - return Observable.create { observer in - ASAuthorizationAppleIDProvider().rx.signIn(on: window) - .asSingle() - .observe(on: RxSchedulers.utility) - .subscribe( - onSuccess: { [weak self] response in - self?._signInState.accept(response) - observer.onNext(.success) - observer.onCompleted() - }, - onFailure: { [weak self] error in - self?._signInState.accept(AccountSignInStateInfo(snsType: .apple)) - observer.onNext(.failed) - observer.onCompleted() - } - ) - .disposed(by: self.disposeBag) - - return Disposables.create() - } - - } - - - // MARK: - Sign Out - - func signOut() { - debugPrint("Apple AuthenticationServices does not support signOut!") - } - - - // MARK: - Deinitializer - - deinit { self.disposeBag = DisposeBag() } - -} +import Foundation diff --git a/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/Kakao/KakaoSignInHelper.swift b/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/Kakao/KakaoSignInHelper.swift index f690f733d..7cec8535d 100644 --- a/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/Kakao/KakaoSignInHelper.swift +++ b/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/Kakao/KakaoSignInHelper.swift @@ -2,78 +2,7 @@ // KakaoSignInHelper.swift // Data // -// Created by geonhui Yu on 12/6/23. +// Created by 김건우 on 6/7/24. // -import Core -import Domain -import UIKit - -import RxCocoa -import RxSwift -import RxKakaoSDKAuth -import RxKakaoSDKCommon -import RxKakaoSDKUser -import KakaoSDKAuth -import KakaoSDKUser - -final class KakaoSignInHelper: SignInHelperType { - - // MARK: - Properties - - private var disposeBag = DisposeBag() - - private let _signInState = PublishRelay() // ? - var signInState: Observable { - self._signInState.asObservable() - } // ? - - - // MARK: - Sign In - - func signIn(on window: UIWindow) -> Observable { // 그냥 바로 AccessToken 리턴하게 만들기 - if UserApi.isKakaoTalkLoginAvailable() { - return UserApi.shared.rx.loginWithKakaoTalk(launchMethod: .CustomScheme) - .map { [weak self] response in - self?._signInState.accept(AccountSignInStateInfo(snsType: .kakao, snsToken: response.accessToken)) - return .success - } - .catch { error in - return .just(.failed) - } - .observe(on: MainScheduler.instance) - } else { - return UserApi.shared.rx.loginWithKakaoAccount(prompts: [.Login]) - .map { [weak self] response in - self?._signInState.accept(AccountSignInStateInfo(snsType: .kakao)) - return .success - } - .catch { error in - return .just(.failed) - } - .observe(on: MainScheduler.instance) - } - } - - - // MARK: - Sign Out - - func signOut() { - UserApi.shared.rx.logout() - .subscribe(onCompleted: { - // Token 제거시 확인 - - debugPrint("Kakao logout completed!") - }, onError: { error in - debugPrint("Kakao logout error!") - }) - .disposed(by: self.disposeBag) - } - - - - - // MARK: - Deinitalizer - - deinit { self.disposeBag = DisposeBag() } -} +import Foundation diff --git a/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/SignInHelperType.swift b/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/SignInHelperType.swift index 47738c65c..baf1bf7d8 100644 --- a/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/SignInHelperType.swift +++ b/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/SignInHelperType.swift @@ -1,19 +1,15 @@ // -// AccountSignInHelperType.swift -// Domain +// SignInHelperType.swift +// Data // -// Created by geonhui Yu on 12/6/23. +// Created by 김건우 on 6/7/24. // -import Core -import Domain import UIKit import RxSwift -public protocol SignInHelperType: AnyObject { - var signInState: Observable { get } // ? - - func signIn(on window: UIWindow) -> Observable +protocol SignInHelperType { + func signIn(on window: UIWindow) -> Single<> func signOut() } diff --git a/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/SignInAPIConfig.swift b/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/SignInAPIConfig.swift index b4e0759b1..92d1e8004 100644 --- a/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/SignInAPIConfig.swift +++ b/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/SignInAPIConfig.swift @@ -1,18 +1,8 @@ // -// AccountSignInHelperConfig.swift -// Domain +// SignInAPIConfig.swift +// Data // -// Created by geonhui Yu on 12/18/23. +// Created by 김건우 on 6/7/24. // -import Domain import Foundation - -struct SignInAPIConfig { - let apple = SignInType.apple.rawValue - let kakao = SignInType.kakao.rawValue - - var helpers: [String: SignInHelperType] { - [apple: AppleSignInHelper(), kakao: KakaoSignInHelper()] - } -} diff --git a/14th-team5-iOS/Data/Sources/Account/AccountAPI/AccountSignInAPIConfig.swift b/14th-team5-iOS/Data/Sources/Account/AccountAPI/AccountSignInAPIConfig.swift new file mode 100644 index 000000000..626ce98cf --- /dev/null +++ b/14th-team5-iOS/Data/Sources/Account/AccountAPI/AccountSignInAPIConfig.swift @@ -0,0 +1,18 @@ +// +// AccountSignInHelperConfig.swift +// Domain +// +// Created by geonhui Yu on 12/18/23. +// + +import Domain +import Foundation + +struct AccountSignInAPIConfig { + let apple = SignInType.apple.rawValue + let kakao = SignInType.kakao.rawValue + + var helpers: [String: AccountSignInHelperType] { + [apple: AccountAppleSignInHelper(), kakao: AccountKakaoSignInHelper()] + } +} diff --git a/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/SignInHelper.swift b/14th-team5-iOS/Data/Sources/Account/AccountAPI/AccountSignInHelper.swift similarity index 98% rename from 14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/SignInHelper.swift rename to 14th-team5-iOS/Data/Sources/Account/AccountAPI/AccountSignInHelper.swift index f2d526843..49bcaa2e2 100644 --- a/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/SignInHelper.swift +++ b/14th-team5-iOS/Data/Sources/Account/AccountAPI/AccountSignInHelper.swift @@ -17,7 +17,7 @@ import RxSwift // TODO: - SignInAPIWorker로 코드 이동, Deprecated 처리 -final class SignInHelper: NSObject { +final class AccountSignInHelper: NSObject { // MARK: - Properties private(set) var disposeBag = DisposeBag() @@ -58,7 +58,7 @@ final class SignInHelper: NSObject { } // MARK: SignIn Functions -extension SignInHelper { +extension AccountSignInHelper { // 소셜 로그인을 호출해서 소셜 Token을 받아오는 코드 func trySignInWith(sns: SNS, window: UIWindow?) -> Observable { diff --git a/14th-team5-iOS/Data/Sources/Account/AccountAPI/Helpers/AccountSignInHelperType.swift b/14th-team5-iOS/Data/Sources/Account/AccountAPI/Helpers/AccountSignInHelperType.swift new file mode 100644 index 000000000..47738c65c --- /dev/null +++ b/14th-team5-iOS/Data/Sources/Account/AccountAPI/Helpers/AccountSignInHelperType.swift @@ -0,0 +1,19 @@ +// +// AccountSignInHelperType.swift +// Domain +// +// Created by geonhui Yu on 12/6/23. +// + +import Core +import Domain +import UIKit + +import RxSwift + +public protocol SignInHelperType: AnyObject { + var signInState: Observable { get } // ? + + func signIn(on window: UIWindow) -> Observable + func signOut() +} diff --git a/14th-team5-iOS/Data/Sources/Account/AccountAPI/Helpers/Apple/AccountAppleSignIn+Rx.swift b/14th-team5-iOS/Data/Sources/Account/AccountAPI/Helpers/Apple/AccountAppleSignIn+Rx.swift new file mode 100644 index 000000000..7381681d0 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/Account/AccountAPI/Helpers/Apple/AccountAppleSignIn+Rx.swift @@ -0,0 +1,128 @@ +// +// AppleService+Rx.swift +// Data +// +// Created by geonhui Yu on 12/20/23. +// + +import UIKit +import Domain + +import AuthenticationServices +import RxCocoa +import RxSwift + +// 참조: https://gist.github.com/iamchiwon/20aa57d4e8f6110bc3f79742c2fb6cc5 + +class RxASAuthorizationControllerDelegateProxy: DelegateProxy, DelegateProxyType, ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding { + + private let disposeBag = DisposeBag() + + // 애플 로그인 창을 띄울 윈도우 + var presentationWindow: UIWindow = UIWindow() + + public init(controller: ASAuthorizationController) { + super.init(parentObject: controller, delegateProxy: RxASAuthorizationControllerDelegateProxy.self) + } + + static func registerKnownImplementations() { + register { RxASAuthorizationControllerDelegateProxy(controller: $0) } + } + + // 로그인이 끝나면 SNSType과 토큰을 담아 스트림 흘려보내는 용도 + internal lazy var didComplete = PublishSubject() + + + + func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { + return presentationWindow + } + + + // 로그인이 정상적으로 끝내면 Apple IDToken값을 알려주는 델리게이트 메서드 + func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { + + switch authorization.credential { + case let appleIDCredential as ASAuthorizationAppleIDCredential: + if let identityToken = appleIDCredential.identityToken, + let tokenString = String(data: identityToken, encoding: .utf8) { + let state = AccountSignInStateInfo(snsType: .apple, snsToken: tokenString) + + self.didComplete.onNext(state) + self.didComplete.onCompleted() + } + + default: + return + } + } + + + // 로그인 중 에러 발생시 호출되는 델리게이트 메서드 + func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) { + + guard let err = error as? ASAuthorizationError else { + self.didComplete.onNext(AccountSignInStateInfo(snsType: .apple)) + self.didComplete.onCompleted() + return + } + + switch err.code { + case .canceled: + debugPrint("Apple auth canceled!!!") + case .failed: + debugPrint("Apple auth failed!!!") + case .invalidResponse: + debugPrint("Apple auth invalid response!!!") + case .notHandled: + debugPrint("Apple auth not handled!!!") + case .unknown: + debugPrint("Apple auth unknown!!!") + + // 무슨 코드인지 확인 필요 + ASAuthorizationAppleIDProvider().rx.signIn(on: self.presentationWindow) + .withUnretained(self) + .subscribe(onNext: { _self, result in + _self.didComplete.onNext(result) + _self.didComplete.onCompleted() + + }, onError: { [weak self] err in + self?.didComplete.onNext(AccountSignInStateInfo(snsType: .apple)) + self?.didComplete.onCompleted() + }) + .disposed(by: self.disposeBag) + return + default: + debugPrint("???") // ? + } + + self.didComplete.onNext(AccountSignInStateInfo(snsType: .apple)) + self.didComplete.onCompleted() + } + + // MARK: Completed + deinit { + self.didComplete.onCompleted() + } +} + +extension ASAuthorizationController: HasDelegate { + public typealias Delegate = ASAuthorizationControllerDelegate +} + +extension Reactive where Base: ASAuthorizationAppleIDProvider { + + func signIn(on window: UIWindow) -> Observable { + let req = base.createRequest() + req.requestedScopes = [.fullName, .email] + + let ctrl = ASAuthorizationController(authorizationRequests: [req]) + let proxy = RxASAuthorizationControllerDelegateProxy.proxy(for: ctrl) + proxy.presentationWindow = window + + ctrl.presentationContextProvider = proxy + ctrl.performRequests() + + return proxy.didComplete + } +} diff --git a/14th-team5-iOS/Data/Sources/Account/AccountAPI/Helpers/Apple/AccountAppleSignInHelper.swift b/14th-team5-iOS/Data/Sources/Account/AccountAPI/Helpers/Apple/AccountAppleSignInHelper.swift new file mode 100644 index 000000000..febb4755d --- /dev/null +++ b/14th-team5-iOS/Data/Sources/Account/AccountAPI/Helpers/Apple/AccountAppleSignInHelper.swift @@ -0,0 +1,73 @@ +// +// AppleSignInHelper.swift +// Data +// +// Created by geonhui Yu on 12/20/23. +// + +import Core +import UIKit +import Domain +import AuthenticationServices + +import RxSwift +import RxCocoa + +final class AccountAppleSignInHelper: SignInHelperType { + + // MARK: - Properties + + private var disposeBag = DisposeBag() + + // 간접적으로 스트림 넘기지 말고, 그냥 메서드에서 토큰값 반환하기 + private let _signInState = PublishRelay() // ? + var signInState: Observable { + self._signInState.asObservable() + } // ? + + + // MARK: - Sign In + + // Apple 로그인 결과로 IdToken을 리턴하는 코드 + func signIn(on window: UIWindow) -> Observable { // 그냥 바로 AccessToken 리턴하게 만들기 + guard #available(iOS 13.0, *) else { + self._signInState.accept(AccountSignInStateInfo(snsType: .apple)) + return Observable.just(.failed) + } + + return Observable.create { observer in + ASAuthorizationAppleIDProvider().rx.signIn(on: window) + .asSingle() + .observe(on: RxSchedulers.utility) + .subscribe( + onSuccess: { [weak self] response in + self?._signInState.accept(response) + observer.onNext(.success) + observer.onCompleted() + }, + onFailure: { [weak self] error in + self?._signInState.accept(AccountSignInStateInfo(snsType: .apple)) + observer.onNext(.failed) + observer.onCompleted() + } + ) + .disposed(by: self.disposeBag) + + return Disposables.create() + } + + } + + + // MARK: - Sign Out + + func signOut() { + debugPrint("Apple AuthenticationServices does not support signOut!") + } + + + // MARK: - Deinitializer + + deinit { self.disposeBag = DisposeBag() } + +} diff --git a/14th-team5-iOS/Data/Sources/Account/AccountAPI/Helpers/Kakao/AccountKakaoSignIn+Rx.swift b/14th-team5-iOS/Data/Sources/Account/AccountAPI/Helpers/Kakao/AccountKakaoSignIn+Rx.swift new file mode 100644 index 000000000..c00915690 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/Account/AccountAPI/Helpers/Kakao/AccountKakaoSignIn+Rx.swift @@ -0,0 +1,38 @@ +// +// KakaoService+Rx.swift +// Data +// +// Created by geonhui Yu on 12/18/23. +// + +import Domain + +import RxSwift +import RxCocoa +import KakaoSDKAuth +import KakaoSDKUser +import RxKakaoSDKUser + +extension Reactive where Base: UserApi { + func signIn() -> Observable { + var loginObservable: Observable? = nil + if UserApi.isKakaoTalkLoginAvailable() { + loginObservable = base.rx.loginWithKakaoTalk() + } else { + loginObservable = base.rx.loginWithKakaoAccount(prompts: [.Login]) + } + + guard let observable = loginObservable else { + return Observable.just(AccountSignInStateInfo(snsType: .kakao)) + } + + return observable + .do(onError: { error in + debugPrint("Kakao SignIn Error: \(error)") + }) + .map { oauthToken -> AccountSignInStateInfo in + return AccountSignInStateInfo(snsType: .kakao, snsToken: oauthToken.accessToken) + } + .catchAndReturn(AccountSignInStateInfo(snsType: .kakao)) + } +} diff --git a/14th-team5-iOS/Data/Sources/Account/AccountAPI/Helpers/Kakao/AccountKakaoSignInHelper.swift b/14th-team5-iOS/Data/Sources/Account/AccountAPI/Helpers/Kakao/AccountKakaoSignInHelper.swift new file mode 100644 index 000000000..2776da4d3 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/Account/AccountAPI/Helpers/Kakao/AccountKakaoSignInHelper.swift @@ -0,0 +1,79 @@ +// +// KakaoSignInHelper.swift +// Data +// +// Created by geonhui Yu on 12/6/23. +// + +import Core +import Domain +import UIKit + +import RxCocoa +import RxSwift +import RxKakaoSDKAuth +import RxKakaoSDKCommon +import RxKakaoSDKUser +import KakaoSDKAuth +import KakaoSDKUser + +final class AccountKakaoSignInHelper: SignInHelperType { + + // MARK: - Properties + + private var disposeBag = DisposeBag() + + private let _signInState = PublishRelay() // ? + var signInState: Observable { + self._signInState.asObservable() + } // ? + + + // MARK: - Sign In + + func signIn(on window: UIWindow) -> Observable { // 그냥 바로 AccessToken 리턴하게 만들기 + if UserApi.isKakaoTalkLoginAvailable() { + return UserApi.shared.rx.loginWithKakaoTalk(launchMethod: .CustomScheme) + .map { [weak self] response in + self?._signInState.accept(AccountSignInStateInfo(snsType: .kakao, snsToken: response.accessToken)) + return .success + } + .catch { error in + return .just(.failed) + } + .observe(on: MainScheduler.instance) + } else { + return UserApi.shared.rx.loginWithKakaoAccount(prompts: [.Login]) + .map { [weak self] response in + self?._signInState.accept(AccountSignInStateInfo(snsType: .kakao)) + return .success + } + .catch { error in + return .just(.failed) + } + .observe(on: MainScheduler.instance) + } + } + + + // MARK: - Sign Out + + func signOut() { + UserApi.shared.rx.logout() + .subscribe(onCompleted: { + // Token 제거시 확인 + + debugPrint("Kakao logout completed!") + }, onError: { error in + debugPrint("Kakao logout error!") + }) + .disposed(by: self.disposeBag) + } + + + + + // MARK: - Deinitalizer + + deinit { self.disposeBag = DisposeBag() } +} diff --git a/14th-team5-iOS/Data/Sources/Account/AccountRepository/AccountRepository.swift b/14th-team5-iOS/Data/Sources/Account/AccountRepository/AccountRepository.swift index 64aaf53e0..430a46f85 100644 --- a/14th-team5-iOS/Data/Sources/Account/AccountRepository/AccountRepository.swift +++ b/14th-team5-iOS/Data/Sources/Account/AccountRepository/AccountRepository.swift @@ -35,7 +35,7 @@ public final class AccountRepository: AccountImpl { private let accessToken: String = App.Repository.token.accessToken.value?.accessToken ?? "" private let keychain = KeychainWrapper(serviceName: "Bibbi", accessGroup: "P9P4WJ623F.com.5ing.bibbi") - let signInHelper = SignInHelper() + let signInHelper = AccountSignInHelper() private let apiWorker = AccountAPIWorker() private let profileWorker = ProfileAPIWorker() private let meApiWorekr = MeAPIWorker() diff --git a/14th-team5-iOS/Data/Sources/Privacy/Repositories/PrivacyViewRepository.swift b/14th-team5-iOS/Data/Sources/Privacy/Repositories/PrivacyViewRepository.swift index a805a6761..3618bd475 100644 --- a/14th-team5-iOS/Data/Sources/Privacy/Repositories/PrivacyViewRepository.swift +++ b/14th-team5-iOS/Data/Sources/Privacy/Repositories/PrivacyViewRepository.swift @@ -20,7 +20,7 @@ public final class PrivacyViewRepository { public init() { } private let privacyAPIWorker: PrivacyAPIWorker = PrivacyAPIWorker() - private let signInHelper: SignInHelper = SignInHelper() + private let signInHelper: AccountSignInHelper = AccountSignInHelper() private let accessToken: String = App.Repository.token.accessToken.value?.accessToken ?? "" public var disposeBag: DisposeBag = DisposeBag() diff --git a/14th-team5-iOS/Domain/Sources/SignIn/DataMapping/TokenResultEntity.swift b/14th-team5-iOS/Domain/Sources/SignIn/DataMapping/TokenResultEntity.swift new file mode 100644 index 000000000..6e5a6c91d --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/SignIn/DataMapping/TokenResultEntity.swift @@ -0,0 +1,8 @@ +// +// DataMapping.swift +// Domain +// +// Created by 김건우 on 6/7/24. +// + +import Foundation From b70a708453e15c5a42586b1e882e222043fac9ce Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Fri, 7 Jun 2024 14:14:58 +0900 Subject: [PATCH 092/263] =?UTF-8?q?feat:=20SignInHelper=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84,=20TokenKeychain=EC=9D=B4=20SignInType=EC=9D=84=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#555)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SignIn/Repository/SignInRepository.swift | 47 +++++- .../Helpers/Apple/AppleSignIn+Rx.swift | 154 +++++++----------- .../Helpers/Apple/AppleSignInHelper.swift | 22 ++- .../Helpers/Kakao/KakaoSignIn+Rx.swift | 38 ----- .../Helpers/Kakao/KakaoSignInHelper.swift | 46 +++++- .../SignInAPI/Helpers/SignInHelperType.swift | 5 +- .../SignIn/SignInAPI/SignInAPIConfig.swift | 10 ++ .../SignIn/SignInAPI/SignInAPIWorker.swift | 45 ++++- .../AccountAPI/AccountSignInHelper.swift | 4 +- .../Helpers/AccountSignInHelperType.swift | 2 +- .../Apple/AccountAppleSignInHelper.swift | 2 +- .../Kakao/AccountKakaoSignInHelper.swift | 2 +- .../Storages/Keychain/Keychain+Token.swift | 18 ++ .../DataMapping/TokenResultEntity.swift | 10 +- .../xcschemes/Bibbi-Workspace.xcscheme | 2 +- 15 files changed, 264 insertions(+), 143 deletions(-) delete mode 100644 14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/Kakao/KakaoSignIn+Rx.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/SignIn/Repository/SignInRepository.swift b/14th-team5-iOS/Data/Sources/APIs/SignIn/Repository/SignInRepository.swift index 3d7ae9e4c..1b8f6f0a7 100644 --- a/14th-team5-iOS/Data/Sources/APIs/SignIn/Repository/SignInRepository.swift +++ b/14th-team5-iOS/Data/Sources/APIs/SignIn/Repository/SignInRepository.swift @@ -5,4 +5,49 @@ // Created by 김건우 on 6/6/24. // -import Foundation +import Core +import Domain +import UIKit + +import RxSwift + +final class SignInRepository { + + // MARK: - Properties + public var disposeBag = DisposeBag() + + public var signInApiWorker = SignInAPIWorker() + public var tokenKeychainStorage = TokenKeychain() + + public init() { } + +} + +extension SignInRepository { + + // MARK: - Sign In + + public func signIn( + with type: SignInType, + on window: UIWindow? + ) -> Single { + signInApiWorker.signIn(with: type, on: window) + .observe(on: RxSchedulers.main) + .do { [weak self] in + self?.tokenKeychainStorage.saveIdToken($0?.idToken) + self?.tokenKeychainStorage.saveSignInType(type) + } + } + + + // MARK: - Sign Out + + public func signOut( + with type: SignInType + ) -> Completable { + signInApiWorker.signOut(with: type) + .observe(on: RxSchedulers.main) + } + + +} diff --git a/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/Apple/AppleSignIn+Rx.swift b/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/Apple/AppleSignIn+Rx.swift index 7381681d0..c81e751d9 100644 --- a/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/Apple/AppleSignIn+Rx.swift +++ b/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/Apple/AppleSignIn+Rx.swift @@ -1,128 +1,98 @@ // -// AppleService+Rx.swift +// AppleSignIn+Rx.swift // Data // -// Created by geonhui Yu on 12/20/23. +// Created by 김건우 on 6/7/24. // -import UIKit -import Domain - import AuthenticationServices +import Domain import RxCocoa import RxSwift +import UIKit -// 참조: https://gist.github.com/iamchiwon/20aa57d4e8f6110bc3f79742c2fb6cc5 - -class RxASAuthorizationControllerDelegateProxy: DelegateProxy, DelegateProxyType, ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding { - - private let disposeBag = DisposeBag() +final class RxSIWAAuthorizationControllerDelegateProxy: DelegateProxy, DelegateProxyType, ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding { - // 애플 로그인 창을 띄울 윈도우 - var presentationWindow: UIWindow = UIWindow() + // MARK: - Window + var window: UIWindow = UIWindow() + // MARK: - Intializer public init(controller: ASAuthorizationController) { - super.init(parentObject: controller, delegateProxy: RxASAuthorizationControllerDelegateProxy.self) + super.init(parentObject: controller, delegateProxy: RxSIWAAuthorizationControllerDelegateProxy.self) } - static func registerKnownImplementations() { - register { RxASAuthorizationControllerDelegateProxy(controller: $0) } + // MARK: - DelegateProxyType + + public static func registerKnownImplementations() { + register { RxSIWAAuthorizationControllerDelegateProxy(controller: $0) } } - // 로그인이 끝나면 SNSType과 토큰을 담아 스트림 흘려보내는 용도 - internal lazy var didComplete = PublishSubject() + // MARK: - Proxy Subject + lazy var didComplete = PublishSubject() + // MARK: - ASAuthorizationControllerDelegate - func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { - return presentationWindow + func authorizationController( + controller: ASAuthorizationController, + didCompleteWithAuthorization authorization: ASAuthorization + ) { + guard + let credential = authorization.credential as? ASAuthorizationAppleIDCredential, + let identityToken = credential.identityToken, + let idToken = String(data: identityToken, encoding: .utf8) + else { return } + + didComplete.onNext(TokenResultEntity(idToken: idToken)) + didComplete.onCompleted() } - - // 로그인이 정상적으로 끝내면 Apple IDToken값을 알려주는 델리게이트 메서드 - func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { - - switch authorization.credential { - case let appleIDCredential as ASAuthorizationAppleIDCredential: - if let identityToken = appleIDCredential.identityToken, - let tokenString = String(data: identityToken, encoding: .utf8) { - let state = AccountSignInStateInfo(snsType: .apple, snsToken: tokenString) - - self.didComplete.onNext(state) - self.didComplete.onCompleted() - } - - default: - return - } + func authorizationController( + controller: ASAuthorizationController, + didCompleteWithError error: any Error + ) { + didComplete.onNext(nil) + didComplete.onCompleted() } - // 로그인 중 에러 발생시 호출되는 델리게이트 메서드 - func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) { - - guard let err = error as? ASAuthorizationError else { - self.didComplete.onNext(AccountSignInStateInfo(snsType: .apple)) - self.didComplete.onCompleted() - return - } - - switch err.code { - case .canceled: - debugPrint("Apple auth canceled!!!") - case .failed: - debugPrint("Apple auth failed!!!") - case .invalidResponse: - debugPrint("Apple auth invalid response!!!") - case .notHandled: - debugPrint("Apple auth not handled!!!") - case .unknown: - debugPrint("Apple auth unknown!!!") - - // 무슨 코드인지 확인 필요 - ASAuthorizationAppleIDProvider().rx.signIn(on: self.presentationWindow) - .withUnretained(self) - .subscribe(onNext: { _self, result in - _self.didComplete.onNext(result) - _self.didComplete.onCompleted() - - }, onError: { [weak self] err in - self?.didComplete.onNext(AccountSignInStateInfo(snsType: .apple)) - self?.didComplete.onCompleted() - }) - .disposed(by: self.disposeBag) - return - default: - debugPrint("???") // ? - } - - self.didComplete.onNext(AccountSignInStateInfo(snsType: .apple)) - self.didComplete.onCompleted() - } + // MARK: - ASAuthorizationControllerPresentationContextProviding - // MARK: Completed - deinit { - self.didComplete.onCompleted() + func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { + return window } + + + // MARK: - Deinitalizer + + deinit { didComplete.onCompleted() } } -extension ASAuthorizationController: HasDelegate { - public typealias Delegate = ASAuthorizationControllerDelegate -} + +// MARK: - Extensions extension Reactive where Base: ASAuthorizationAppleIDProvider { - func signIn(on window: UIWindow) -> Observable { - let req = base.createRequest() - req.requestedScopes = [.fullName, .email] + public func signIn( + scope: [ASAuthorization.Scope]? = nil, + on window: UIWindow? + ) -> Single { + let request = base.createRequest() + request.requestedScopes = scope + + let controller = ASAuthorizationController(authorizationRequests: [request]) - let ctrl = ASAuthorizationController(authorizationRequests: [req]) - let proxy = RxASAuthorizationControllerDelegateProxy.proxy(for: ctrl) - proxy.presentationWindow = window + let proxy = RxSIWAAuthorizationControllerDelegateProxy(controller: controller) + proxy.window = window! - ctrl.presentationContextProvider = proxy - ctrl.performRequests() + controller.presentationContextProvider = proxy + controller.performRequests() - return proxy.didComplete + return proxy.didComplete.asSingle() } + } + + + +// NOTE: - SIWA: Sign In With Apple의 줄임말 diff --git a/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/Apple/AppleSignInHelper.swift b/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/Apple/AppleSignInHelper.swift index c263b0cae..47a416293 100644 --- a/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/Apple/AppleSignInHelper.swift +++ b/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/Apple/AppleSignInHelper.swift @@ -5,4 +5,24 @@ // Created by 김건우 on 6/7/24. // -import Foundation +import AuthenticationServices +import Domain +import UIKit + +import RxSwift + +public final class AppleSignInHelper: SignInHelperType { + + public func signIn(on window: UIWindow?) -> Single { + ASAuthorizationAppleIDProvider().rx.signIn(on: window) + } + + public func signOut() -> Completable { + Observable.create { observer in + observer.onCompleted() + return Disposables.create() + } + .asCompletable() + } + +} diff --git a/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/Kakao/KakaoSignIn+Rx.swift b/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/Kakao/KakaoSignIn+Rx.swift deleted file mode 100644 index c00915690..000000000 --- a/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/Kakao/KakaoSignIn+Rx.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// KakaoService+Rx.swift -// Data -// -// Created by geonhui Yu on 12/18/23. -// - -import Domain - -import RxSwift -import RxCocoa -import KakaoSDKAuth -import KakaoSDKUser -import RxKakaoSDKUser - -extension Reactive where Base: UserApi { - func signIn() -> Observable { - var loginObservable: Observable? = nil - if UserApi.isKakaoTalkLoginAvailable() { - loginObservable = base.rx.loginWithKakaoTalk() - } else { - loginObservable = base.rx.loginWithKakaoAccount(prompts: [.Login]) - } - - guard let observable = loginObservable else { - return Observable.just(AccountSignInStateInfo(snsType: .kakao)) - } - - return observable - .do(onError: { error in - debugPrint("Kakao SignIn Error: \(error)") - }) - .map { oauthToken -> AccountSignInStateInfo in - return AccountSignInStateInfo(snsType: .kakao, snsToken: oauthToken.accessToken) - } - .catchAndReturn(AccountSignInStateInfo(snsType: .kakao)) - } -} diff --git a/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/Kakao/KakaoSignInHelper.swift b/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/Kakao/KakaoSignInHelper.swift index 7cec8535d..3a322f588 100644 --- a/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/Kakao/KakaoSignInHelper.swift +++ b/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/Kakao/KakaoSignInHelper.swift @@ -5,4 +5,48 @@ // Created by 김건우 on 6/7/24. // -import Foundation +import Domain +import UIKit + +import RxCocoa +import RxSwift +import RxKakaoSDKAuth +import RxKakaoSDKCommon +import RxKakaoSDKUser +import KakaoSDKAuth +import KakaoSDKUser + +public final class KakaoSignInHelper: SignInHelperType { + + // MARK: - Sign In + + public func signIn(on window: UIWindow?) -> Single { + if UserApi.isKakaoTalkLoginAvailable() { + return signInWithKakaoTalk() + } else { + return signInWithKakaoAccount() + } + } + + private func signInWithKakaoTalk() -> Single { + UserApi.shared.rx.loginWithKakaoTalk(launchMethod: .CustomScheme) + .map { TokenResultEntity(idToken: $0.accessToken) } + .catchAndReturn(nil) + .asSingle() + } + + private func signInWithKakaoAccount() -> Single { + UserApi.shared.rx.loginWithKakaoAccount(prompts: [.Login]) + .map { TokenResultEntity(idToken: $0.accessToken) } + .catchAndReturn(nil) + .asSingle() + } + + + // MARK: - Sign Out + + public func signOut() -> Completable { + UserApi.shared.rx.logout() + } + +} diff --git a/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/SignInHelperType.swift b/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/SignInHelperType.swift index baf1bf7d8..582cce0d7 100644 --- a/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/SignInHelperType.swift +++ b/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/SignInHelperType.swift @@ -6,10 +6,11 @@ // import UIKit +import Domain import RxSwift protocol SignInHelperType { - func signIn(on window: UIWindow) -> Single<> - func signOut() + func signIn(on window: UIWindow?) -> Single + func signOut() -> Completable } diff --git a/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/SignInAPIConfig.swift b/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/SignInAPIConfig.swift index 92d1e8004..f52544290 100644 --- a/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/SignInAPIConfig.swift +++ b/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/SignInAPIConfig.swift @@ -5,4 +5,14 @@ // Created by 김건우 on 6/7/24. // +import Domain import Foundation + +struct SignInAPIConfig { + let apple = SignInType.apple.rawValue + let kakao = SignInType.kakao.rawValue + + var helpers: [String: SignInHelperType] { + [apple: AppleSignInHelper(), kakao: KakaoSignInHelper()] + } +} diff --git a/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/SignInAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/SignInAPIWorker.swift index 6a94423bd..c15fc228e 100644 --- a/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/SignInAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/SignInAPIWorker.swift @@ -5,4 +5,47 @@ // Created by 김건우 on 6/6/24. // -import Foundation +import Domain +import UIKit + +import RxSwift + +public class SignInAPIWorker { + + // MARK: - Properties + private var disposeBag = DisposeBag() + + private var _config = SignInAPIConfig() + private var helpers: [String: SignInHelperType] { + _config.helpers + } + +} + +extension SignInAPIWorker { + + // MARK: - Sign In + + public func signIn( + with type: SignInType, + on window: UIWindow? + ) -> Single { + return getHelper(type).signIn(on: window) + } + + + // MARK: - Sign Out + + public func signOut(with type: SignInType) -> Completable { + return getHelper(type).signOut() + } + +} + +extension SignInAPIWorker { + + private func getHelper(_ type: SignInType) -> SignInHelperType { + return helpers[type.rawValue]! + } + +} diff --git a/14th-team5-iOS/Data/Sources/Account/AccountAPI/AccountSignInHelper.swift b/14th-team5-iOS/Data/Sources/Account/AccountAPI/AccountSignInHelper.swift index 49bcaa2e2..952a29869 100644 --- a/14th-team5-iOS/Data/Sources/Account/AccountAPI/AccountSignInHelper.swift +++ b/14th-team5-iOS/Data/Sources/Account/AccountAPI/AccountSignInHelper.swift @@ -22,8 +22,8 @@ final class AccountSignInHelper: NSObject { // MARK: - Properties private(set) var disposeBag = DisposeBag() - private let _config = SignInAPIConfig() - private lazy var helpers: [String: SignInHelperType] = { + private let _config = AccountSignInAPIConfig() + private lazy var helpers: [String: AccountSignInHelperType] = { _config.helpers }() diff --git a/14th-team5-iOS/Data/Sources/Account/AccountAPI/Helpers/AccountSignInHelperType.swift b/14th-team5-iOS/Data/Sources/Account/AccountAPI/Helpers/AccountSignInHelperType.swift index 47738c65c..ca71cc5e1 100644 --- a/14th-team5-iOS/Data/Sources/Account/AccountAPI/Helpers/AccountSignInHelperType.swift +++ b/14th-team5-iOS/Data/Sources/Account/AccountAPI/Helpers/AccountSignInHelperType.swift @@ -11,7 +11,7 @@ import UIKit import RxSwift -public protocol SignInHelperType: AnyObject { +public protocol AccountSignInHelperType: AnyObject { var signInState: Observable { get } // ? func signIn(on window: UIWindow) -> Observable diff --git a/14th-team5-iOS/Data/Sources/Account/AccountAPI/Helpers/Apple/AccountAppleSignInHelper.swift b/14th-team5-iOS/Data/Sources/Account/AccountAPI/Helpers/Apple/AccountAppleSignInHelper.swift index febb4755d..6859f0bc2 100644 --- a/14th-team5-iOS/Data/Sources/Account/AccountAPI/Helpers/Apple/AccountAppleSignInHelper.swift +++ b/14th-team5-iOS/Data/Sources/Account/AccountAPI/Helpers/Apple/AccountAppleSignInHelper.swift @@ -13,7 +13,7 @@ import AuthenticationServices import RxSwift import RxCocoa -final class AccountAppleSignInHelper: SignInHelperType { +final class AccountAppleSignInHelper: AccountSignInHelperType { // MARK: - Properties diff --git a/14th-team5-iOS/Data/Sources/Account/AccountAPI/Helpers/Kakao/AccountKakaoSignInHelper.swift b/14th-team5-iOS/Data/Sources/Account/AccountAPI/Helpers/Kakao/AccountKakaoSignInHelper.swift index 2776da4d3..5dd6d5eba 100644 --- a/14th-team5-iOS/Data/Sources/Account/AccountAPI/Helpers/Kakao/AccountKakaoSignInHelper.swift +++ b/14th-team5-iOS/Data/Sources/Account/AccountAPI/Helpers/Kakao/AccountKakaoSignInHelper.swift @@ -17,7 +17,7 @@ import RxKakaoSDKUser import KakaoSDKAuth import KakaoSDKUser -final class AccountKakaoSignInHelper: SignInHelperType { +final class AccountKakaoSignInHelper: AccountSignInHelperType { // MARK: - Properties diff --git a/14th-team5-iOS/Data/Sources/Storages/Keychain/Keychain+Token.swift b/14th-team5-iOS/Data/Sources/Storages/Keychain/Keychain+Token.swift index 635b84b81..a558df100 100644 --- a/14th-team5-iOS/Data/Sources/Storages/Keychain/Keychain+Token.swift +++ b/14th-team5-iOS/Data/Sources/Storages/Keychain/Keychain+Token.swift @@ -6,12 +6,16 @@ // import Core +import Domain import Foundation public protocol TokenKeychainType: KeychainType { func saveIdToken(_ idToken: String?) func loadIdToken() -> String? + func saveSignInType(_ type: SignInType?) + func loadSignInType() -> SignInType? + func saveAccessToken(_ accessToken: String?) func loadAccessToken() -> String? @@ -38,6 +42,20 @@ final public class TokenKeychain: TokenKeychainType { } + // MARK: - SignInType + public func saveSignInType(_ type: SignInType?) { + keychain[.signInType] = type?.rawValue + } + + public func loadSignInType() -> SignInType? { + guard + let sign: String = keychain[.signInType], + let type = SignInType(rawValue: sign) + else { return nil } + return type + } + + // MARK: - AccessToken public func saveAccessToken(_ accessToken: String?) { keychain[.accessToken] = accessToken diff --git a/14th-team5-iOS/Domain/Sources/SignIn/DataMapping/TokenResultEntity.swift b/14th-team5-iOS/Domain/Sources/SignIn/DataMapping/TokenResultEntity.swift index 6e5a6c91d..d763b6135 100644 --- a/14th-team5-iOS/Domain/Sources/SignIn/DataMapping/TokenResultEntity.swift +++ b/14th-team5-iOS/Domain/Sources/SignIn/DataMapping/TokenResultEntity.swift @@ -1,8 +1,16 @@ // -// DataMapping.swift +// TokenResultEntity.swift // Domain // // Created by 김건우 on 6/7/24. // import Foundation + +public struct TokenResultEntity { + public var idToken: String // 소셜 로그인용 AccessToken + + public init(idToken: String) { + self.idToken = idToken + } +} diff --git a/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme b/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme index dfbc89c1f..5f307e6b4 100644 --- a/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme +++ b/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme @@ -1,6 +1,6 @@ Date: Fri, 7 Jun 2024 14:27:11 +0900 Subject: [PATCH 093/263] =?UTF-8?q?feat:=20SignInRepositoryProtocol=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#555)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../APIs/SignIn/Repository/SignInRepository.swift | 13 ++++++++----- .../TokenResultEntity.swift | 0 .../Interfaces/Repository/SignInRepository.swift | 15 +++++++++++++++ .../Sources/SignIn/UseCases/SignInUseCase.swift | 8 ++++++++ 4 files changed, 31 insertions(+), 5 deletions(-) rename 14th-team5-iOS/Domain/Sources/SignIn/{DataMapping => Entities}/TokenResultEntity.swift (100%) create mode 100644 14th-team5-iOS/Domain/Sources/SignIn/Interfaces/Repository/SignInRepository.swift create mode 100644 14th-team5-iOS/Domain/Sources/SignIn/UseCases/SignInUseCase.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/SignIn/Repository/SignInRepository.swift b/14th-team5-iOS/Data/Sources/APIs/SignIn/Repository/SignInRepository.swift index 1b8f6f0a7..57adf3607 100644 --- a/14th-team5-iOS/Data/Sources/APIs/SignIn/Repository/SignInRepository.swift +++ b/14th-team5-iOS/Data/Sources/APIs/SignIn/Repository/SignInRepository.swift @@ -11,7 +11,7 @@ import UIKit import RxSwift -final class SignInRepository { +final class SignInRepository: SignInRepositoryProtocol { // MARK: - Properties public var disposeBag = DisposeBag() @@ -42,11 +42,14 @@ extension SignInRepository { // MARK: - Sign Out - public func signOut( - with type: SignInType - ) -> Completable { - signInApiWorker.signOut(with: type) + public func signOut() -> Completable { + guard + let type = tokenKeychainStorage.loadSignInType() + else { return .error(NSError()) } // TODO: - Error 타입 정의하기 + + return signInApiWorker.signOut(with: type) .observe(on: RxSchedulers.main) + .do(onCompleted: { KeychainWrapper.standard.removeAllKeys() }) } diff --git a/14th-team5-iOS/Domain/Sources/SignIn/DataMapping/TokenResultEntity.swift b/14th-team5-iOS/Domain/Sources/SignIn/Entities/TokenResultEntity.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/SignIn/DataMapping/TokenResultEntity.swift rename to 14th-team5-iOS/Domain/Sources/SignIn/Entities/TokenResultEntity.swift diff --git a/14th-team5-iOS/Domain/Sources/SignIn/Interfaces/Repository/SignInRepository.swift b/14th-team5-iOS/Domain/Sources/SignIn/Interfaces/Repository/SignInRepository.swift new file mode 100644 index 000000000..9f89b0a13 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/SignIn/Interfaces/Repository/SignInRepository.swift @@ -0,0 +1,15 @@ +// +// SignInRepository.swift +// Domain +// +// Created by 김건우 on 6/7/24. +// + +import UIKit + +import RxSwift + +public protocol SignInRepositoryProtocol { + func signIn(with type: SignInType, on window: UIWindow?) -> Single + func signOut() -> Completable +} diff --git a/14th-team5-iOS/Domain/Sources/SignIn/UseCases/SignInUseCase.swift b/14th-team5-iOS/Domain/Sources/SignIn/UseCases/SignInUseCase.swift new file mode 100644 index 000000000..cb8ae5c7f --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/SignIn/UseCases/SignInUseCase.swift @@ -0,0 +1,8 @@ +// +// SignInUseCase.swift +// Domain +// +// Created by 김건우 on 6/7/24. +// + +import Foundation From 06e184b9815702971738ca5c30e9a40f99983d85 Mon Sep 17 00:00:00 2001 From: Do-hyun-Kim Date: Fri, 7 Jun 2024 16:48:02 +0900 Subject: [PATCH 094/263] =?UTF-8?q?refactor:=20CameraRepository=20File=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20-=20CameraViewRepository,=20CameraDisplayV?= =?UTF-8?q?iewRepository=20=ED=8C=8C=EC=9D=BC=20=EC=A0=9C=EA=B1=B0=20?= =?UTF-8?q?=EB=B0=8F=20CameraRepository=EB=A1=9C=20=ED=86=B5=ED=95=A9=20-?= =?UTF-8?q?=20Camera=20=EA=B4=80=EB=A0=A8=20DTO=20=EB=84=A4=EC=9D=B4?= =?UTF-8?q?=EB=B0=8D=20ResponseDTO=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Camera/Dependency/CameraDIContainer.swift | 6 +- .../Dependency/CameraDisplayDIContainer.swift | 4 +- .../Reactor/CameraDisplayViewReactor.swift | 9 +-- .../Camera/CameraAPI/CameraAPIWorker.swift | 34 +++++------ ...=> CameraCreateRealEmojiResponseDTO.swift} | 10 +-- ...ft => CameraDisplayImageResponseDTO.swift} | 11 ++-- ...ift => CameraDisplayPostResponseDTO.swift} | 10 +-- ...CameraRealEmojiImageItemResponseDTO.swift} | 16 ++--- ...CameraRealEmojiPreSignedResponseDTO.swift} | 12 ++-- ...ft => CameraTodayMissionResponseDTO.swift} | 11 ++-- ...=> CameraUpdateRealEmojiResponseDTO.swift} | 8 +-- .../CameraDisplayViewRepository.swift | 61 ------------------- .../CameraRepository.swift} | 25 +++++--- .../MembersProfileResponseDTO.swift | 4 +- .../Members/MemberAPI/MembersAPIWorker.swift | 4 +- ...e.swift => CameraRepositoryProtocol.swift} | 24 +++++--- .../Interfaces/CameraViewInterface.swift | 28 --------- .../UseCases/CameraDisplayViewUseCase.swift | 11 +--- .../Camera/UseCases/CameraViewUseCase.swift | 26 ++++---- 19 files changed, 119 insertions(+), 195 deletions(-) rename 14th-team5-iOS/Data/Sources/Camera/DataMapping/{CameraCreateRealEmojiDTO.swift => CameraCreateRealEmojiResponseDTO.swift} (75%) rename 14th-team5-iOS/Data/Sources/Camera/DataMapping/{CameraDisplayImageDTO.swift => CameraDisplayImageResponseDTO.swift} (61%) rename 14th-team5-iOS/Data/Sources/Camera/DataMapping/{CameraDisplayPostDTO.swift => CameraDisplayPostResponseDTO.swift} (81%) rename 14th-team5-iOS/Data/Sources/Camera/DataMapping/{CameraRealEmojiImageItemDTO.swift => CameraRealEmojiImageItemResponseDTO.swift} (72%) rename 14th-team5-iOS/Data/Sources/Camera/DataMapping/{CameraRealEmojiPreSignedDTO.swift => CameraRealEmojiPreSignedResponseDTO.swift} (57%) rename 14th-team5-iOS/Data/Sources/Camera/DataMapping/{CameraTodayMissionDTO.swift => CameraTodayMissionResponseDTO.swift} (74%) rename 14th-team5-iOS/Data/Sources/Camera/DataMapping/{CameraUpdateRealEmojiDTO.swift => CameraUpdateRealEmojiResponseDTO.swift} (76%) delete mode 100644 14th-team5-iOS/Data/Sources/Camera/Repositories/CameraDisplayViewRepository.swift rename 14th-team5-iOS/Data/Sources/Camera/{Repositories/CameraViewRepository.swift => Repository/CameraRepository.swift} (83%) rename 14th-team5-iOS/Domain/Sources/Camera/Interfaces/{CameraDisplayViewInterface.swift => CameraRepositoryProtocol.swift} (54%) delete mode 100644 14th-team5-iOS/Domain/Sources/Camera/Interfaces/CameraViewInterface.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Camera/Dependency/CameraDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/Camera/Dependency/CameraDIContainer.swift index a1faeeb9d..815ed414c 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Camera/Dependency/CameraDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Camera/Dependency/CameraDIContainer.swift @@ -15,7 +15,7 @@ import UIKit public final class CameraDIContainer: BaseDIContainer { public typealias ViewContrller = CameraViewController - public typealias Repository = CameraViewInterface + public typealias Repository = CameraRepositoryProtocol public typealias Reactor = CameraViewReactor public typealias UseCase = CameraViewUseCaseProtocol @@ -43,12 +43,12 @@ public final class CameraDIContainer: BaseDIContainer { } public func makeUseCase() -> UseCase { - return CameraViewUseCase(cameraViewRepository: makeRepository()) + return CameraViewUseCase(cameraRepository: makeRepository()) } public func makeRepository() -> Repository { - return CameraViewRepository() + return CameraRepository() } public func makeReactor() -> Reactor { diff --git a/14th-team5-iOS/App/Sources/Presentation/Camera/Dependency/CameraDisplayDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/Camera/Dependency/CameraDisplayDIContainer.swift index 7e3e30183..e57cc4de4 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Camera/Dependency/CameraDisplayDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Camera/Dependency/CameraDisplayDIContainer.swift @@ -14,7 +14,7 @@ import Domain public final class CameraDisplayDIContainer: BaseDIContainer { public typealias ViewContrller = CameraDisplayViewController - public typealias Repository = CameraDisplayViewInterface + public typealias Repository = CameraRepositoryProtocol public typealias Reactor = CameraDisplayViewReactor public typealias UseCase = CameraDisplayViewUseCaseProtocol @@ -40,7 +40,7 @@ public final class CameraDisplayDIContainer: BaseDIContainer { } public func makeRepository() -> Repository { - return CameraDisplayViewRepository() + return CameraRepository() } public func makeUseCase() -> UseCase { diff --git a/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraDisplayViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraDisplayViewReactor.swift index 849ebe498..3a1bac5c3 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraDisplayViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraDisplayViewReactor.swift @@ -119,15 +119,16 @@ public final class CameraDisplayViewReactor: Reactor { ) case let .fetchDisplayImage(description): return .concat( - cameraDisplayUseCase.executeDescrptionItems(with: description) - .asObservable() + Observable.of(Array(description)) + .map { String($0) } .flatMap { items -> Observable in var sectionItem: [DisplayEditItemModel] = [] + items.forEach { - sectionItem.append(.fetchDisplayItem(DisplayEditCellReactor(title: $0, radius: 8, font: .head1))) + sectionItem.append(.fetchDisplayItem(DisplayEditCellReactor(title: String($0), radius: 8, font: .head1))) } - return Observable.concat( + return .concat( .just(.setDisplayEditSection(sectionItem)), .just(.setDescription(description)) ) diff --git a/14th-team5-iOS/Data/Sources/Camera/CameraAPI/CameraAPIWorker.swift b/14th-team5-iOS/Data/Sources/Camera/CameraAPI/CameraAPIWorker.swift index 79f0add24..066006513 100644 --- a/14th-team5-iOS/Data/Sources/Camera/CameraAPI/CameraAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Camera/CameraAPI/CameraAPIWorker.swift @@ -32,7 +32,7 @@ extension CameraAPIs { extension CameraAPIWorker { - public func createProfilePresignedURL(accessToken: String, parameters: Encodable) -> Single { + public func createProfilePresignedURL(accessToken: String, parameters: Encodable) -> Single { let spec = CameraAPIs.uploadProfileImageURL.spec @@ -43,14 +43,14 @@ extension CameraAPIWorker { debugPrint("upload Profile Image URL Fetch Reuslt: \(str)") } } - .map(CameraDisplayImageDTO.self) + .map(CameraDisplayImageResponseDTO.self) .catchAndReturn(nil) .asSingle() } - public func createFeedPresignedURL(accessToken: String, parameters: Encodable) -> Single { + public func createFeedPresignedURL(accessToken: String, parameters: Encodable) -> Single { let spec = CameraAPIs.uploadImageURL.spec @@ -61,7 +61,7 @@ extension CameraAPIWorker { debugPrint("uploadImage Fetch Reuslt: \(str)") } } - .map(CameraDisplayImageDTO.self) + .map(CameraDisplayImageResponseDTO.self) .catchAndReturn(nil) .asSingle() } @@ -89,7 +89,7 @@ extension CameraAPIWorker { .map { $0 } } - public func combineWithTextImageUpload(accessToken: String, parameters: Encodable, query: CameraMissionFeedQuery) -> Single { + public func combineWithTextImageUpload(accessToken: String, parameters: Encodable, query: CameraMissionFeedQuery) -> Single { let spec = CameraAPIs.updateImage(query).spec return request(spec: spec, headers: [BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.xAuthToken(accessToken)], jsonEncodable: parameters) @@ -99,7 +99,7 @@ extension CameraAPIWorker { debugPrint("editProfile Image Upload Result: \(str)") } } - .map(CameraDisplayPostDTO.self) + .map(CameraDisplayPostResponseDTO.self) .map { dto in guard let dto = dto else { return nil } let repository = PostUserDefaultsRepository() @@ -111,9 +111,7 @@ extension CameraAPIWorker { } - - // TODO: Real Emoji API 추가 - public func createRealEmojiPresignedURL(accessToken: String, memberId: String, parameters: Encodable) -> Single { + public func createRealEmojiPresignedURL(accessToken: String, memberId: String, parameters: Encodable) -> Single { let spec = CameraAPIs.uploadRealEmojiURL(memberId).spec return request(spec: spec, headers: [BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.xAuthToken(accessToken)], jsonEncodable: parameters) @@ -123,13 +121,13 @@ extension CameraAPIWorker { debugPrint("RealEmoji Image Presigned URL Reuslt: \(str)") } } - .map(CameraRealEmojiPreSignedDTO.self) + .map(CameraRealEmojiPreSignedResponseDTO.self) .catchAndReturn(nil) .asSingle() } - public func uploadRealEmojiImageToS3(accessToken: String, memberId: String, parameters: Encodable) -> Single { + public func uploadRealEmojiImageToS3(accessToken: String, memberId: String, parameters: Encodable) -> Single { let spec = CameraAPIs.updateRealEmojiImage(memberId).spec return request(spec: spec, headers: [BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.xAuthToken(accessToken)], jsonEncodable: parameters) @@ -139,12 +137,12 @@ extension CameraAPIWorker { debugPrint("Real Image upload to S3 Result: \(str)") } } - .map(CameraCreateRealEmojiDTO.self) + .map(CameraCreateRealEmojiResponseDTO.self) .catchAndReturn(nil) .asSingle() } - public func loadRealEmojiImage(accessToken: String, memberId: String) -> Single { + public func loadRealEmojiImage(accessToken: String, memberId: String) -> Single { let spec = CameraAPIs.reloadRealEmoji(memberId).spec return request(spec: spec, headers: [BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.xAuthToken(accessToken)]) @@ -154,12 +152,12 @@ extension CameraAPIWorker { debugPrint("Real Emoji Items Result: \(str)") } } - .map(CameraRealEmojiImageItemDTO.self) + .map(CameraRealEmojiImageItemResponseDTO.self) .catchAndReturn(nil) .asSingle() } - public func updateRealEmojiImage(accessToken: String, memberId: String, realEmojiId: String, parameters: Encodable) -> Single { + public func updateRealEmojiImage(accessToken: String, memberId: String, realEmojiId: String, parameters: Encodable) -> Single { let spec = CameraAPIs.modifyRealEmojiImage(memberId, realEmojiId).spec return request(spec: spec, headers: [BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.xAuthToken(accessToken)], jsonEncodable: parameters) .subscribe(on: Self.queue) @@ -168,12 +166,12 @@ extension CameraAPIWorker { debugPrint("Real Emoji Modify Result: \(str)") } } - .map(CameraUpdateRealEmojiDTO.self) + .map(CameraUpdateRealEmojiResponseDTO.self) .catchAndReturn(nil) .asSingle() } - public func fetchMissionItems(accessToken: String) -> Single { + public func fetchMissionItems(accessToken: String) -> Single { let spec = CameraAPIs.fetchMissionToday.spec return request(spec: spec, headers: [BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.xAuthToken(accessToken)]) .subscribe(on: Self.queue) @@ -182,7 +180,7 @@ extension CameraAPIWorker { debugPrint("Fetch Today Mission Items : \(str)") } } - .map(CameraTodayMissionDTO.self) + .map(CameraTodayMissionResponseDTO.self) .catchAndReturn(nil) .asSingle() } diff --git a/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraCreateRealEmojiDTO.swift b/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraCreateRealEmojiResponseDTO.swift similarity index 75% rename from 14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraCreateRealEmojiDTO.swift rename to 14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraCreateRealEmojiResponseDTO.swift index 72aebe120..5070d8d18 100644 --- a/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraCreateRealEmojiDTO.swift +++ b/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraCreateRealEmojiResponseDTO.swift @@ -1,8 +1,8 @@ // -// CameraCreateRealEmojiDTO.swift -// Domain +// CameraCreateRealEmojiResponseDTO.swift +// Data // -// Created by Kim dohyun on 1/22/24. +// Created by Kim dohyun on 6/7/24. // import Foundation @@ -10,7 +10,7 @@ import Foundation import Domain -public struct CameraCreateRealEmojiDTO: Decodable { +public struct CameraCreateRealEmojiResponseDTO: Decodable { public var realEmojiId: String public var realEmojiType: String @@ -25,7 +25,7 @@ public struct CameraCreateRealEmojiDTO: Decodable { } -extension CameraCreateRealEmojiDTO { +extension CameraCreateRealEmojiResponseDTO { public func toDomain() -> CameraCreateRealEmojiResponse { return .init( realEmojiId: realEmojiId, diff --git a/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraDisplayImageDTO.swift b/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraDisplayImageResponseDTO.swift similarity index 61% rename from 14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraDisplayImageDTO.swift rename to 14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraDisplayImageResponseDTO.swift index eab0adc07..c025b00d0 100644 --- a/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraDisplayImageDTO.swift +++ b/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraDisplayImageResponseDTO.swift @@ -1,16 +1,17 @@ // -// CameraDisplayImageDTO.swift -// Domain +// CameraDisplayImageResponseDTO.swift +// Data // -// Created by Kim dohyun on 12/21/23. +// Created by Kim dohyun on 6/7/24. // import Foundation + import Domain -public struct CameraDisplayImageDTO: Decodable { +public struct CameraDisplayImageResponseDTO: Decodable { public var imageURL: String? public enum CodingKeys: String, CodingKey { @@ -19,7 +20,7 @@ public struct CameraDisplayImageDTO: Decodable { } -extension CameraDisplayImageDTO { +extension CameraDisplayImageResponseDTO { public func toDomain() -> CameraDisplayImageResponse? { return .init(imageURL: imageURL ?? "") diff --git a/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraDisplayPostDTO.swift b/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraDisplayPostResponseDTO.swift similarity index 81% rename from 14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraDisplayPostDTO.swift rename to 14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraDisplayPostResponseDTO.swift index 305e03ba3..ba6c9fb10 100644 --- a/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraDisplayPostDTO.swift +++ b/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraDisplayPostResponseDTO.swift @@ -1,15 +1,15 @@ // -// CameraDisplayPostDTO.swift -// Domain +// CameraDisplayPostResponseDTO.swift +// Data // -// Created by Kim dohyun on 12/22/23. +// Created by Kim dohyun on 6/7/24. // import Foundation import Domain -public struct CameraDisplayPostDTO: Decodable { +public struct CameraDisplayPostResponseDTO: Decodable { public var postId: String? public var authorId: String? @@ -24,7 +24,7 @@ public struct CameraDisplayPostDTO: Decodable { } -extension CameraDisplayPostDTO { +extension CameraDisplayPostResponseDTO { public func toDomain() -> CameraDisplayPostResponse { diff --git a/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraRealEmojiImageItemDTO.swift b/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraRealEmojiImageItemResponseDTO.swift similarity index 72% rename from 14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraRealEmojiImageItemDTO.swift rename to 14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraRealEmojiImageItemResponseDTO.swift index 4c6c5d832..894317fb3 100644 --- a/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraRealEmojiImageItemDTO.swift +++ b/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraRealEmojiImageItemResponseDTO.swift @@ -1,8 +1,8 @@ // -// CameraRealEmojiImageItemDTO.swift +// CameraRealEmojiImageItemResponseDTO.swift // Data // -// Created by Kim dohyun on 1/22/24. +// Created by Kim dohyun on 6/7/24. // import Foundation @@ -11,8 +11,8 @@ import Core import Domain -public struct CameraRealEmojiImageItemDTO: Decodable { - public var realEmojiItems: [CameraRealEmojiInfoDTO] +public struct CameraRealEmojiImageItemResponseDTO: Decodable { + public var realEmojiItems: [CameraRealEmojiInfoResponseDTO] public enum CodingKeys: String, CodingKey { case realEmojiItems = "myRealEmojiList" @@ -21,8 +21,8 @@ public struct CameraRealEmojiImageItemDTO: Decodable { } -extension CameraRealEmojiImageItemDTO { - public struct CameraRealEmojiInfoDTO: Decodable { +extension CameraRealEmojiImageItemResponseDTO { + public struct CameraRealEmojiInfoResponseDTO: Decodable { public var realEmojiId: String public var realEmojiType: String public var realEmojiImageURL: String @@ -36,7 +36,7 @@ extension CameraRealEmojiImageItemDTO { } -extension CameraRealEmojiImageItemDTO { +extension CameraRealEmojiImageItemResponseDTO { func toDomain() -> [CameraRealEmojiImageItemResponse?] { var items: [CameraRealEmojiImageItemResponse?] = Array(repeating: nil, count: 5) @@ -50,7 +50,7 @@ extension CameraRealEmojiImageItemDTO { } } -extension CameraRealEmojiImageItemDTO.CameraRealEmojiInfoDTO { +extension CameraRealEmojiImageItemResponseDTO.CameraRealEmojiInfoResponseDTO { func toDomain() -> CameraRealEmojiImageItemResponse { return .init( realEmojiId: realEmojiId, diff --git a/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraRealEmojiPreSignedDTO.swift b/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraRealEmojiPreSignedResponseDTO.swift similarity index 57% rename from 14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraRealEmojiPreSignedDTO.swift rename to 14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraRealEmojiPreSignedResponseDTO.swift index f4c305446..01ede5f02 100644 --- a/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraRealEmojiPreSignedDTO.swift +++ b/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraRealEmojiPreSignedResponseDTO.swift @@ -1,15 +1,17 @@ // -// CameraRealEmojiPreSignedDTO.swift -// Domain +// CameraRealEmojiPreSignedResponseDTO.swift +// Data // -// Created by Kim dohyun on 1/22/24. +// Created by Kim dohyun on 6/7/24. // import Foundation +import Core import Domain -public struct CameraRealEmojiPreSignedDTO: Decodable { + +public struct CameraRealEmojiPreSignedResponseDTO: Decodable { public var imageURL: String public enum CodingKeys: String, CodingKey { @@ -18,7 +20,7 @@ public struct CameraRealEmojiPreSignedDTO: Decodable { } -extension CameraRealEmojiPreSignedDTO { +extension CameraRealEmojiPreSignedResponseDTO { public func toDomain() -> CameraRealEmojiPreSignedResponse { return .init(imageURL: imageURL) diff --git a/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraTodayMissionDTO.swift b/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraTodayMissionResponseDTO.swift similarity index 74% rename from 14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraTodayMissionDTO.swift rename to 14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraTodayMissionResponseDTO.swift index 464c27fb8..2953b92a2 100644 --- a/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraTodayMissionDTO.swift +++ b/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraTodayMissionResponseDTO.swift @@ -1,15 +1,15 @@ // -// CameraTodayMissionDTO.swift +// CameraTodayMissionResponseDTO.swift // Data // -// Created by Kim dohyun on 4/30/24. +// Created by Kim dohyun on 6/7/24. // import Foundation -import Domain +import Domain -public struct CameraTodayMissionDTO: Decodable { +public struct CameraTodayMissionResponseDTO: Decodable { public var missionDate: String public var missionId: String @@ -24,7 +24,7 @@ public struct CameraTodayMissionDTO: Decodable { } -extension CameraTodayMissionDTO { +extension CameraTodayMissionResponseDTO { func toDomain() -> CameraTodayMissionResponse { return .init( missionDate: missionDate.toDate(with: "yyyy-MM-dd"), @@ -34,3 +34,4 @@ extension CameraTodayMissionDTO { } } + diff --git a/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraUpdateRealEmojiDTO.swift b/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraUpdateRealEmojiResponseDTO.swift similarity index 76% rename from 14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraUpdateRealEmojiDTO.swift rename to 14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraUpdateRealEmojiResponseDTO.swift index 8f62cd9b3..d44590741 100644 --- a/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraUpdateRealEmojiDTO.swift +++ b/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraUpdateRealEmojiResponseDTO.swift @@ -1,15 +1,15 @@ // -// CameraUpdateRealEmojiDTO.swift +// CameraUpdateRealEmojiResponseDTO.swift // Data // -// Created by Kim dohyun on 1/24/24. +// Created by Kim dohyun on 6/7/24. // import Foundation import Domain -public struct CameraUpdateRealEmojiDTO: Decodable { +public struct CameraUpdateRealEmojiResponseDTO: Decodable { public var realEmojiId: String public var realEmojiType: String @@ -24,7 +24,7 @@ public struct CameraUpdateRealEmojiDTO: Decodable { } -extension CameraUpdateRealEmojiDTO { +extension CameraUpdateRealEmojiResponseDTO { public func toDomain() -> CameraUpdateRealEmojiResponse { return .init( realEmojiId: realEmojiId, diff --git a/14th-team5-iOS/Data/Sources/Camera/Repositories/CameraDisplayViewRepository.swift b/14th-team5-iOS/Data/Sources/Camera/Repositories/CameraDisplayViewRepository.swift deleted file mode 100644 index c7b44bec5..000000000 --- a/14th-team5-iOS/Data/Sources/Camera/Repositories/CameraDisplayViewRepository.swift +++ /dev/null @@ -1,61 +0,0 @@ -// -// CameraDisplayViewRepository.swift -// Data -// -// Created by Kim dohyun on 12/11/23. -// - -import Foundation - -import Core -import Domain -import RxSwift -import RxCocoa -import ReactorKit - - - -public final class CameraDisplayViewRepository { - public init() { } - private let cameraDisplayAPIWorker: CameraAPIWorker = CameraAPIWorker() - - public var disposeBag: DisposeBag = DisposeBag() - public var accessToken: String = App.Repository.token.accessToken.value?.accessToken ?? "" - -} - - -extension CameraDisplayViewRepository: CameraDisplayViewInterface { - - public func generateDescrption(with keyword: String) -> Observable> { - - let item: Array = Array(keyword).map { String($0) } - return Observable.create { observer in - observer.onNext(item) - - return Disposables.create() - } - } - - public func fetchFeedImageURL(parameters: CameraDisplayImageParameters) -> Observable { - return cameraDisplayAPIWorker.createFeedPresignedURL(accessToken: accessToken, parameters: parameters) - .catchAndReturn(nil) - .map { $0?.toDomain() ?? nil } - .asObservable() - - } - - public func uploadImageToS3(toURL url: String, imageData: Data) -> Observable { - return cameraDisplayAPIWorker.uploadImageToPresignedURL(accessToken: accessToken, toURL: url, withImageData: imageData) - .asObservable() - } - - - public func combineWithTextImage(parameters: CameraDisplayPostParameters, query:CameraMissionFeedQuery) -> Observable { - return cameraDisplayAPIWorker.combineWithTextImageUpload(accessToken: accessToken, parameters: parameters, query: query) - .map { $0?.toDomain() } - .catchAndReturn(nil) - .asObservable() - } - -} diff --git a/14th-team5-iOS/Data/Sources/Camera/Repositories/CameraViewRepository.swift b/14th-team5-iOS/Data/Sources/Camera/Repository/CameraRepository.swift similarity index 83% rename from 14th-team5-iOS/Data/Sources/Camera/Repositories/CameraViewRepository.swift rename to 14th-team5-iOS/Data/Sources/Camera/Repository/CameraRepository.swift index 823bb56df..295abf277 100644 --- a/14th-team5-iOS/Data/Sources/Camera/Repositories/CameraViewRepository.swift +++ b/14th-team5-iOS/Data/Sources/Camera/Repository/CameraRepository.swift @@ -1,8 +1,8 @@ // -// CameraViewRepository.swift +// CameraRepository.swift // Data // -// Created by Kim dohyun on 12/6/23. +// Created by Kim dohyun on 6/7/24. // import Foundation @@ -13,9 +13,7 @@ import RxCocoa import RxSwift - - -public final class CameraViewRepository { +public final class CameraRepository { public var disposeBag: DisposeBag = DisposeBag() private let cameraAPIWorker: CameraAPIWorker = CameraAPIWorker() @@ -26,9 +24,8 @@ public final class CameraViewRepository { } - -extension CameraViewRepository: CameraViewInterface { - +extension CameraRepository: CameraRepositoryProtocol { + public func toggleCameraPosition(_ isState: Bool) -> Observable { return Observable .create { observer in @@ -47,13 +44,20 @@ extension CameraViewRepository: CameraViewInterface { } } + public func combineWithTextImage(parameters: CameraDisplayPostParameters, query:CameraMissionFeedQuery) -> Observable { + return cameraAPIWorker.combineWithTextImageUpload(accessToken: accessToken, parameters: parameters, query: query) + .map { $0?.toDomain() } + .catchAndReturn(nil) + .asObservable() + } - public func fetchProfileImageURL(parameters: CameraDisplayImageParameters) -> Observable { - + public func fetchPresignedeImageURL(parameters: CameraDisplayImageParameters) -> Observable { return cameraAPIWorker.createProfilePresignedURL(accessToken: accessToken, parameters: parameters) .compactMap { $0?.toDomain() } .asObservable() } + + public func uploadImageToS3(toURL url: String, imageData: Data) -> Observable { return cameraAPIWorker.uploadImageToPresignedURL(accessToken: accessToken, toURL: url, withImageData: imageData) @@ -99,4 +103,5 @@ extension CameraViewRepository: CameraViewInterface { .compactMap { $0?.toDomain() } .asObservable() } + } diff --git a/14th-team5-iOS/Data/Sources/Members/DataMapping/MembersProfileResponseDTO.swift b/14th-team5-iOS/Data/Sources/Members/DataMapping/MembersProfileResponseDTO.swift index dd71cb004..ba5d305a5 100644 --- a/14th-team5-iOS/Data/Sources/Members/DataMapping/MembersProfileResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/Members/DataMapping/MembersProfileResponseDTO.swift @@ -13,7 +13,7 @@ import Domain struct MembersProfileResponseDTO: Decodable { let memberId: String let name: String - let imageUrl: String + let imageUrl: String? let dayOfBirth: String let familyJoinAt: String } @@ -25,7 +25,7 @@ extension MembersProfileResponseDTO { return .init( memberId: memberId, memberName: name, - memberImage: URL(string: imageUrl) ?? URL(fileURLWithPath: ""), + memberImage: URL(string: imageUrl ?? "") ?? URL(fileURLWithPath: ""), dayOfBirth: dayOfBirth.toDate(), familyJoinAt: familyJoinAt.toDate(with: "yyyy-MM-dd").realativeFormatterYYMM() ) diff --git a/14th-team5-iOS/Data/Sources/Members/MemberAPI/MembersAPIWorker.swift b/14th-team5-iOS/Data/Sources/Members/MemberAPI/MembersAPIWorker.swift index 124452271..50b0dd6c9 100644 --- a/14th-team5-iOS/Data/Sources/Members/MemberAPI/MembersAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Members/MemberAPI/MembersAPIWorker.swift @@ -49,7 +49,7 @@ extension MembersAPIWorker { } - public func createProfileImagePresingedURL(accessToken: String, parameters: Encodable) -> Single { + public func createProfileImagePresingedURL(accessToken: String, parameters: Encodable) -> Single { let spec = MembersAPIs.profileAlbumUploadImageURL.spec return request(spec: spec, headers: [BibbiAPI.Header.xAppVersion, BibbiAPI.Header.xUserPlatform, BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.xAuthToken(accessToken)], jsonEncodable: parameters) @@ -59,7 +59,7 @@ extension MembersAPIWorker { debugPrint("createPresinged URL \(str)") } } - .map(CameraDisplayImageDTO.self) + .map(CameraDisplayImageResponseDTO.self) .catchAndReturn(nil) .asSingle() } diff --git a/14th-team5-iOS/Domain/Sources/Camera/Interfaces/CameraDisplayViewInterface.swift b/14th-team5-iOS/Domain/Sources/Camera/Interfaces/CameraRepositoryProtocol.swift similarity index 54% rename from 14th-team5-iOS/Domain/Sources/Camera/Interfaces/CameraDisplayViewInterface.swift rename to 14th-team5-iOS/Domain/Sources/Camera/Interfaces/CameraRepositoryProtocol.swift index f3178222a..a44fc1e30 100644 --- a/14th-team5-iOS/Domain/Sources/Camera/Interfaces/CameraDisplayViewInterface.swift +++ b/14th-team5-iOS/Domain/Sources/Camera/Interfaces/CameraRepositoryProtocol.swift @@ -1,14 +1,14 @@ // -// CameraDisplayViewInterface.swift +// CameraRepositoryProtocol.swift // Domain // -// Created by Kim dohyun on 12/26/23. +// Created by Kim dohyun on 6/7/24. // import Foundation -import RxCocoa import RxSwift +import RxCocoa public enum UploadLocation { case survival @@ -63,11 +63,21 @@ public enum UploadLocation { } -public protocol CameraDisplayViewInterface: AnyObject { + +public protocol CameraRepositoryProtocol { var disposeBag: DisposeBag { get } - func generateDescrption(with keyword: String) -> Observable> - func fetchFeedImageURL(parameters: CameraDisplayImageParameters) -> Observable + + var accessToken: String { get } + + func toggleCameraPosition(_ isState: Bool) -> Observable + func toggleCameraFlash(_ isState: Bool) -> Observable + func fetchPresignedeImageURL(parameters: CameraDisplayImageParameters) -> Observable func uploadImageToS3(toURL url: String, imageData: Data) -> Observable + func editProfleImageToS3(memberId: String, parameter: ProfileImageEditParameter) -> Observable + func fetchRealEmojiImageURL(memberId: String, parameters: CameraRealEmojiParameters) -> Observable + func uploadRealEmojiImageToS3(memberId: String, parameters: CameraCreateRealEmojiParameters) -> Observable + func fetchRealEmojiItems(memberId: String) -> Observable<[CameraRealEmojiImageItemResponse?]> + func updateRealEmojiImage(memberId: String, realEmojiId: String, parameters: CameraUpdateRealEmojiParameters) -> Observable + func fetchTodayMissionItem() -> Observable func combineWithTextImage(parameters: CameraDisplayPostParameters, query: CameraMissionFeedQuery) -> Observable - } diff --git a/14th-team5-iOS/Domain/Sources/Camera/Interfaces/CameraViewInterface.swift b/14th-team5-iOS/Domain/Sources/Camera/Interfaces/CameraViewInterface.swift deleted file mode 100644 index 483db38f0..000000000 --- a/14th-team5-iOS/Domain/Sources/Camera/Interfaces/CameraViewInterface.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// CameraViewInterface.swift -// Domain -// -// Created by Kim dohyun on 12/26/23. -// - -import Foundation - -import RxCocoa -import RxSwift - -public protocol CameraViewInterface: AnyObject { - var disposeBag: DisposeBag { get } - - var accessToken: String { get } - - func toggleCameraPosition(_ isState: Bool) -> Observable - func toggleCameraFlash(_ isState: Bool) -> Observable - func fetchProfileImageURL(parameters: CameraDisplayImageParameters) -> Observable - func uploadImageToS3(toURL url: String, imageData: Data) -> Observable - func editProfleImageToS3(memberId: String, parameter: ProfileImageEditParameter) -> Observable - func fetchRealEmojiImageURL(memberId: String, parameters: CameraRealEmojiParameters) -> Observable - func uploadRealEmojiImageToS3(memberId: String, parameters: CameraCreateRealEmojiParameters) -> Observable - func fetchRealEmojiItems(memberId: String) -> Observable<[CameraRealEmojiImageItemResponse?]> - func updateRealEmojiImage(memberId: String, realEmojiId: String, parameters: CameraUpdateRealEmojiParameters) -> Observable - func fetchTodayMissionItem() -> Observable -} diff --git a/14th-team5-iOS/Domain/Sources/Camera/UseCases/CameraDisplayViewUseCase.swift b/14th-team5-iOS/Domain/Sources/Camera/UseCases/CameraDisplayViewUseCase.swift index 99adb216c..989726189 100644 --- a/14th-team5-iOS/Domain/Sources/Camera/UseCases/CameraDisplayViewUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/Camera/UseCases/CameraDisplayViewUseCase.swift @@ -12,7 +12,6 @@ import RxCocoa public protocol CameraDisplayViewUseCaseProtocol { - func executeDescrptionItems(with keyword: String) -> Observable> func executeDisplayImageURL(parameters: CameraDisplayImageParameters) -> Observable func executeUploadToS3(toURL url: String, imageData: Data) -> Observable func executeCombineWithTextImage(parameters: CameraDisplayPostParameters, query: CameraMissionFeedQuery) -> Observable @@ -22,18 +21,14 @@ public protocol CameraDisplayViewUseCaseProtocol { public final class CameraDisplayViewUseCase: CameraDisplayViewUseCaseProtocol { - private let cameraDisplayViewRepository: CameraDisplayViewInterface + private let cameraDisplayViewRepository: CameraRepositoryProtocol - public init(cameraDisplayViewRepository: CameraDisplayViewInterface) { + public init(cameraDisplayViewRepository: CameraRepositoryProtocol) { self.cameraDisplayViewRepository = cameraDisplayViewRepository } - public func executeDescrptionItems(with keyword: String) -> Observable> { - return cameraDisplayViewRepository.generateDescrption(with: keyword) - } - public func executeDisplayImageURL(parameters: CameraDisplayImageParameters) -> Observable { - return cameraDisplayViewRepository.fetchFeedImageURL(parameters: parameters) + return cameraDisplayViewRepository.fetchPresignedeImageURL(parameters: parameters) } public func executeUploadToS3(toURL url: String, imageData: Data) -> Observable { diff --git a/14th-team5-iOS/Domain/Sources/Camera/UseCases/CameraViewUseCase.swift b/14th-team5-iOS/Domain/Sources/Camera/UseCases/CameraViewUseCase.swift index e126c4275..91c5f798e 100644 --- a/14th-team5-iOS/Domain/Sources/Camera/UseCases/CameraViewUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/Camera/UseCases/CameraViewUseCase.swift @@ -28,51 +28,51 @@ public protocol CameraViewUseCaseProtocol { public final class CameraViewUseCase: CameraViewUseCaseProtocol { - private let cameraViewRepository: CameraViewInterface + private let cameraRepository: CameraRepositoryProtocol - public init(cameraViewRepository: CameraViewInterface) { - self.cameraViewRepository = cameraViewRepository + public init(cameraRepository: CameraRepositoryProtocol) { + self.cameraRepository = cameraRepository } public func executeToggleCameraPosition(_ isState: Bool) -> Observable { - return cameraViewRepository.toggleCameraPosition(isState) + return cameraRepository.toggleCameraPosition(isState) } public func executeToggleCameraFlash(_ isState: Bool) -> Observable { - return cameraViewRepository.toggleCameraFlash(isState) + return cameraRepository.toggleCameraFlash(isState) } public func executeProfileImageURL(parameter: CameraDisplayImageParameters) -> Observable { - return cameraViewRepository.fetchProfileImageURL(parameters: parameter) + return cameraRepository.fetchPresignedeImageURL(parameters: parameter) } public func executeUploadToS3(toURL url: String, imageData: Data) -> Observable { - return cameraViewRepository.uploadImageToS3(toURL: url, imageData: imageData) + return cameraRepository.uploadImageToS3(toURL: url, imageData: imageData) } public func executeEditProfileImage(memberId: String, parameter: ProfileImageEditParameter) -> Observable { - return cameraViewRepository.editProfleImageToS3(memberId: memberId, parameter: parameter) + return cameraRepository.editProfleImageToS3(memberId: memberId, parameter: parameter) } public func executeRealEmojiImageURL(memberId: String, parameter: CameraRealEmojiParameters) -> Observable { - return cameraViewRepository.fetchRealEmojiImageURL(memberId: memberId, parameters: parameter) + return cameraRepository.fetchRealEmojiImageURL(memberId: memberId, parameters: parameter) } public func executeRealEmojiUploadToS3(memberId: String, parameter: CameraCreateRealEmojiParameters) -> Observable { - return cameraViewRepository.uploadRealEmojiImageToS3(memberId: memberId, parameters: parameter) + return cameraRepository.uploadRealEmojiImageToS3(memberId: memberId, parameters: parameter) } public func executeRealEmojiItems(memberId: String) -> Observable<[CameraRealEmojiImageItemResponse?]> { - return cameraViewRepository.fetchRealEmojiItems(memberId: memberId) + return cameraRepository.fetchRealEmojiItems(memberId: memberId) } public func executeUpdateRealEmojiImage(memberId: String, realEmojiId: String, parameter: CameraUpdateRealEmojiParameters) -> Observable { - return cameraViewRepository.updateRealEmojiImage(memberId: memberId, realEmojiId: realEmojiId, parameters: parameter) + return cameraRepository.updateRealEmojiImage(memberId: memberId, realEmojiId: realEmojiId, parameters: parameter) } public func executeTodayMission() -> Observable { - return cameraViewRepository.fetchTodayMissionItem() + return cameraRepository.fetchTodayMissionItem() } } From 98b39ac11a12d5969a1f69449e0bf146849d49fa Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Fri, 7 Jun 2024 16:45:17 +0900 Subject: [PATCH 095/263] =?UTF-8?q?feat:=20=EA=B3=B5=ED=86=B5=20=ED=97=A4?= =?UTF-8?q?=EB=8D=94=20=EC=82=BD=EC=9E=85=20=EC=BD=94=EB=93=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#555)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Core/Sources/BBNetwork/API/API.swift | 28 +++++++++++++------ .../Data/Sources/APIs/APIWorker.swift | 5 +++- .../Sources/APIs/NetworkIntercepter.swift | 10 ++++++- .../Account/AccountAPI/AccountAPIWorker.swift | 2 +- 4 files changed, 33 insertions(+), 12 deletions(-) diff --git a/14th-team5-iOS/Core/Sources/BBNetwork/API/API.swift b/14th-team5-iOS/Core/Sources/BBNetwork/API/API.swift index 25e6ae46e..f7dc9e557 100644 --- a/14th-team5-iOS/Core/Sources/BBNetwork/API/API.swift +++ b/14th-team5-iOS/Core/Sources/BBNetwork/API/API.swift @@ -50,27 +50,37 @@ public enum BibbiAPI { public var value: String { switch self { - case .auth(let value): return "Bearer \(value)" - case .xAppKey: return "7c5aaa36-570e-491f-b18a-26a1a0b72959" - case .xAuthToken(let value): return "\(value)" + case let .auth(token): return "Bearer \(token)" + case .xAppKey: return "7c5aaa36-570e-491f-b18a-26a1a0b72959" // TODO: - 번들에서 가져오기 + case let .xAuthToken(token): return "\(token)" case .contentForm: return "application/x-www-form-urlencoded" case .contentJson: return "application/json" case .contentMulti: return "multipart/form-data; boundary=\(APIConst.boundary)" case .acceptJson: return "application/json" case .xUserPlatform: return "iOS" case .xAppVersion: return "\(Bundle.main.appVersion)" - case .xUserID: return "\(App.Repository.member.memberID.value ?? "송영민짱")" + case .xUserID: return "\(App.Repository.member.memberID.value ?? "송영민짱")" // TODO: - 연관값 Value를 받도록 바꾸기 } } - public static func commonHeaders(_ accessToken: String) -> [Self] { - return [ + public static func commonHeaders( + userId: String? = nil, + accessToken: String? = nil + ) -> [Self] { + var header: [BibbiHeader] = [ .xAppKey, .xAppVersion, - .xUserPlatform, - .xUserID, - .xAuthToken(accessToken) + .xUserPlatform ] + if let userId = userId { + header.append(.xUserID) + } else { + header.append(.xUserID) + } + if let accessToken = accessToken { + header.append(.xAuthToken(accessToken)) + } + return header } @available(*, deprecated, renamed: "commonHeaders(_:)") diff --git a/14th-team5-iOS/Data/Sources/APIs/APIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/APIWorker.swift index 145f366e9..10917e524 100644 --- a/14th-team5-iOS/Data/Sources/APIs/APIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/APIWorker.swift @@ -90,7 +90,10 @@ public class APIWorker: NSObject { request.httpBody = jsonData print("interCepter call with name \(url)") - return AF.rx.request(urlRequest: request) + return AF.rx.request( + urlRequest: request, + interceptor: NetworkInterceptor() + ) .retry(5) .validate(statusCode: 200..<300) .responseData() diff --git a/14th-team5-iOS/Data/Sources/APIs/NetworkIntercepter.swift b/14th-team5-iOS/Data/Sources/APIs/NetworkIntercepter.swift index 0747ba0ad..f3b06e425 100644 --- a/14th-team5-iOS/Data/Sources/APIs/NetworkIntercepter.swift +++ b/14th-team5-iOS/Data/Sources/APIs/NetworkIntercepter.swift @@ -51,11 +51,19 @@ public final class NetworkInterceptor: RequestInterceptor { guard let accessToken = App.Repository.token.accessToken.value?.accessToken else { + urlRequest.setHeaders(BibbiHeader.commonHeaders()) completion(.success(urlRequest)) return } - urlRequest.setHeaders(BibbiHeader.commonHeaders(accessToken)) + // TODO: - MemberID를 UserDefaults에서 가져오기 + + urlRequest.setHeaders( + BibbiHeader.commonHeaders( + // userId: <#T##String?#>, + accessToken: accessToken + ) + ) completion(.success(urlRequest)) } diff --git a/14th-team5-iOS/Data/Sources/Account/AccountAPI/AccountAPIWorker.swift b/14th-team5-iOS/Data/Sources/Account/AccountAPI/AccountAPIWorker.swift index 7336b928e..bdecdb21f 100644 --- a/14th-team5-iOS/Data/Sources/Account/AccountAPI/AccountAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Account/AccountAPI/AccountAPIWorker.swift @@ -32,7 +32,7 @@ extension AccountAPIs { // MARK: SignIn extension AccountAPIWorker { private func signInWith(spec: APISpec, jsonEncodable: Encodable) -> Single { - return request(spec: spec, headers: [BibbiAPI.Header.xAppKey], jsonEncodable: jsonEncodable) + return request(spec: spec/*, headers: [BibbiAPI.Header.xAppKey]*/, jsonEncodable: jsonEncodable) .subscribe(on: Self.queue) .do(onNext: { if let str = String(data: $0.1, encoding: .utf8) { From 60060e1588705071db57680058e01f3302d95ed7 Mon Sep 17 00:00:00 2001 From: Do-hyun-Kim Date: Fri, 7 Jun 2024 17:01:07 +0900 Subject: [PATCH 096/263] =?UTF-8?q?refactor:=20PostAPIs,=20PostAPIWorker?= =?UTF-8?q?=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20=EC=88=98=EC=A0=95=20-=20withL?= =?UTF-8?q?atestFrom=20Header=20=EC=82=BD=EC=9E=85=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PostAPIs.swift} | 4 +-- .../PostListAPIWorker.swift | 36 ++++++------------- .../PostList/Repository/PostRepository.swift | 6 ++-- 3 files changed, 15 insertions(+), 31 deletions(-) rename 14th-team5-iOS/Data/Sources/Post/PostList/{PostListAPI/PostListAPIs.swift => PostAPI/PostAPIs.swift} (90%) rename 14th-team5-iOS/Data/Sources/Post/PostList/{PostListAPI => PostAPI}/PostListAPIWorker.swift (53%) diff --git a/14th-team5-iOS/Data/Sources/Post/PostList/PostListAPI/PostListAPIs.swift b/14th-team5-iOS/Data/Sources/Post/PostList/PostAPI/PostAPIs.swift similarity index 90% rename from 14th-team5-iOS/Data/Sources/Post/PostList/PostListAPI/PostListAPIs.swift rename to 14th-team5-iOS/Data/Sources/Post/PostList/PostAPI/PostAPIs.swift index b0c80c6fc..adfa8eb13 100644 --- a/14th-team5-iOS/Data/Sources/Post/PostList/PostListAPI/PostListAPIs.swift +++ b/14th-team5-iOS/Data/Sources/Post/PostList/PostAPI/PostAPIs.swift @@ -1,5 +1,5 @@ // -// PostListAPIs.swift +// PostAPIs.swift // Data // // Created by 마경미 on 25.12.23. @@ -8,7 +8,7 @@ import Core import Foundation -public enum PostListAPIs: API { +public enum PostAPIs: API { case fetchPostList case fetchPostDetail(PostRequestDTO) diff --git a/14th-team5-iOS/Data/Sources/Post/PostList/PostListAPI/PostListAPIWorker.swift b/14th-team5-iOS/Data/Sources/Post/PostList/PostAPI/PostListAPIWorker.swift similarity index 53% rename from 14th-team5-iOS/Data/Sources/Post/PostList/PostListAPI/PostListAPIWorker.swift rename to 14th-team5-iOS/Data/Sources/Post/PostList/PostAPI/PostListAPIWorker.swift index aee5d0000..53599b96c 100644 --- a/14th-team5-iOS/Data/Sources/Post/PostList/PostListAPI/PostListAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Post/PostList/PostAPI/PostListAPIWorker.swift @@ -1,5 +1,5 @@ // -// PostListAPIWorker.swift +// PostAPIWorker.swift // Data // // Created by 마경미 on 25.12.23. @@ -13,8 +13,8 @@ import Domain import Alamofire import RxSwift -public typealias PostListAPIWorker = PostListAPIs.Worker -extension PostListAPIs { +public typealias PostAPIWorker = PostAPIs.Worker +extension PostAPIs { public final class Worker: APIWorker { static let queue = { ConcurrentDispatchQueueScheduler(queue: DispatchQueue(label: "PostListAPIQueue", qos: .utility)) @@ -27,19 +27,11 @@ extension PostListAPIs { } } -extension PostListAPIWorker { - public func fetchPostDetail(query: PostQuery) -> Single { - return Observable.just(()) - .withLatestFrom(self._headers) - .withUnretained(self) - .flatMap { $0.0.fetchPostDetail(headers: $0.1, query: query) } - .asSingle() - } - - private func fetchPostDetail(headers: [APIHeader]?, query: Domain.PostQuery) -> RxSwift.Single { +extension PostAPIWorker { + public func fetchPostDetail(query: Domain.PostQuery) -> RxSwift.Single { let requestDTO = PostRequestDTO(postId: query.postId) - let spec = PostListAPIs.fetchPostDetail(requestDTO).spec - return request(spec: spec, headers: headers) + let spec = PostAPIs.fetchPostDetail(requestDTO).spec + return request(spec: spec) .subscribe(on: Self.queue) .do { if let str = String(data: $0.1, encoding: .utf8) { @@ -52,18 +44,10 @@ extension PostListAPIWorker { .asSingle() } - public func fetchTodayPostList(query: PostListQuery) -> Single { - return Observable.just(()) - .withLatestFrom(self._headers) - .withUnretained(self) - .flatMap { $0.0.fetchTodayPostList(headers: $0.1, query: query) } - .asSingle() - } - - private func fetchTodayPostList(headers: [APIHeader]?, query: Domain.PostListQuery) -> RxSwift.Single { + public func fetchTodayPostList(query: Domain.PostListQuery) -> RxSwift.Single { let requestDTO = PostListRequestDTO(page: query.page, size: query.size, date: query.date, memberId: query.memberId, sort: query.sort, type: query.type.rawValue) - let spec = PostListAPIs.fetchPostList.spec - return request(spec: spec, headers: headers, parameters: requestDTO) + let spec = PostAPIs.fetchPostList.spec + return request(spec: spec, parameters: requestDTO) .subscribe(on: Self.queue) .do { if let str = String(data: $0.1, encoding: .utf8) { diff --git a/14th-team5-iOS/Data/Sources/Post/PostList/Repository/PostRepository.swift b/14th-team5-iOS/Data/Sources/Post/PostList/Repository/PostRepository.swift index 27cb13676..d7a610efc 100644 --- a/14th-team5-iOS/Data/Sources/Post/PostList/Repository/PostRepository.swift +++ b/14th-team5-iOS/Data/Sources/Post/PostList/Repository/PostRepository.swift @@ -13,15 +13,15 @@ import RxSwift public final class PostRepository: PostListRepositoryProtocol { private let disposeBag: DisposeBag = DisposeBag() - private let postListsAPIWorker: PostListAPIWorker = PostListAPIWorker() + private let postAPIWorker: PostAPIWorker = PostAPIWorker() public init() { } public func fetchPostDetail(query: Domain.PostQuery) -> RxSwift.Single { - return postListsAPIWorker.fetchPostDetail(query: query) + return postAPIWorker.fetchPostDetail(query: query) } public func fetchTodayPostList(query: Domain.PostListQuery) -> RxSwift.Single { - return postListsAPIWorker.fetchTodayPostList(query: query) + return postAPIWorker.fetchTodayPostList(query: query) } } From 8802421d8ba163272fcfc037a63f266155faf99d Mon Sep 17 00:00:00 2001 From: Kyoungmi <1mmgm.dev@gmail.com> Date: Tue, 4 Jun 2024 18:59:25 +0900 Subject: [PATCH 097/263] [refactor]: apiworker naming(#543) --- .../Home/Dependency/MainViewDIContainer.swift | 4 +- .../Dependency/PostListsDIContainer.swift | 6 +- .../Dependency/ReactionDIContainer.swift | 6 +- .../SelectableEmojiDIContainer.swift | 6 +- .../DataMapping}/MainNightResponseDTO.swift | 0 .../DataMapping}/MainRequestDTO.swift | 0 .../DataMapping}/MainResponseDTO.swift | 0 .../MainViewAPI/MainViewAPIWorker.swift} | 28 +---- .../MainView/MainViewAPI/MainViewAPIs.swift} | 2 +- .../Repository/MainViewRepository.swift} | 4 +- .../DataMapping/AddReactionRequestDTO.swift | 12 ++ .../DataMapping/AddReactionResponseDTO.swift} | 6 +- .../DataMapping/FetchReactionRequestDTO.swift | 12 ++ .../FetchReactionResponseDTO.swift} | 10 +- .../RemoveReactionRequestDTO.swift | 12 ++ .../RemoveReactionResponseDTO.swift} | 6 +- .../ReactionAPI/ReactionAPIWorker.swift | 79 ++++++++++++++ .../Reaction/ReactionAPI/ReactionAPIs.swift} | 4 +- .../Repository/ReactionRepository.swift | 32 ++++++ .../DataMapping/AddRealEmojiRequestDTO.swift | 16 +++ .../AddRealEmojiResponseDTO.swift} | 12 +- .../FetchRealEmojiListRequestDTO.swift | 12 ++ .../FetchRealEmojiListResponseDTO.swift} | 20 ++-- .../DataMapping/MyRealEmojiResponseDTO.swift} | 26 ++--- .../RemoveRealEmojiRequestDTO.swift | 13 +++ .../RemoveRealEmojiResponseDTO.swift} | 7 +- .../RealEmojiAPI/RealEmojiAPIS.swift | 0 .../RealEmojiAPI/RealEmojiAPIWorker.swift | 54 ++------- .../Repository/RealEmojiRepository.swift | 38 +++++++ .../Emoji/EmojiAPI/EmojiAPIWorker.swift | 103 ------------------ .../Emoji/Interfaces/EmojiRepository.swift | 16 --- .../ReactionRepositoryProtocol.swift | 16 +++ .../Sources/Emoji/UseCases/EmojiUseCase.swift | 8 +- ...wift => RealEmojiRepositoryProtocol.swift} | 4 +- .../RealEmoji/UseCases/RealEmojiUseCase.swift | 6 +- 35 files changed, 312 insertions(+), 268 deletions(-) rename 14th-team5-iOS/Data/Sources/{View/Main/DTO => APIs/MainView/MainViewAPI/DataMapping}/MainNightResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/{View/Main/DTO => APIs/MainView/MainViewAPI/DataMapping}/MainRequestDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/{View/Main/DTO => APIs/MainView/MainViewAPI/DataMapping}/MainResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/{View/Main/Worker/MainAPIWorker.swift => APIs/MainView/MainViewAPI/MainViewAPIWorker.swift} (61%) rename 14th-team5-iOS/Data/Sources/{View/Main/Worker/MainAPIs.swift => APIs/MainView/MainViewAPI/MainViewAPIs.swift} (95%) rename 14th-team5-iOS/Data/Sources/{View/Main/Repository/MainRepository.swift => APIs/MainView/Repository/MainViewRepository.swift} (85%) create mode 100644 14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/DataMapping/AddReactionRequestDTO.swift rename 14th-team5-iOS/Data/Sources/{Emoji/DTO/AddEmojiDTO.swift => APIs/Reaction/ReactionAPI/DataMapping/AddReactionResponseDTO.swift} (64%) create mode 100644 14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/DataMapping/FetchReactionRequestDTO.swift rename 14th-team5-iOS/Data/Sources/{Emoji/DTO/FetchEmojiDTO.swift => APIs/Reaction/ReactionAPI/DataMapping/FetchReactionResponseDTO.swift} (89%) create mode 100644 14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/DataMapping/RemoveReactionRequestDTO.swift rename 14th-team5-iOS/Data/Sources/{Emoji/DTO/RemoveEmojiDTO.swift => APIs/Reaction/ReactionAPI/DataMapping/RemoveReactionResponseDTO.swift} (63%) create mode 100644 14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/ReactionAPIWorker.swift rename 14th-team5-iOS/Data/Sources/{Emoji/EmojiAPI/EmojiAPIs.swift => APIs/Reaction/ReactionAPI/ReactionAPIs.swift} (90%) create mode 100644 14th-team5-iOS/Data/Sources/APIs/Reaction/Repository/ReactionRepository.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/DataMapping/AddRealEmojiRequestDTO.swift rename 14th-team5-iOS/Data/Sources/{RealEmoji/DTO/AddRealEmojiDTO.swift => APIs/RealEmoji/RealEmojiAPI/DataMapping/AddRealEmojiResponseDTO.swift} (58%) create mode 100644 14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/DataMapping/FetchRealEmojiListRequestDTO.swift rename 14th-team5-iOS/Data/Sources/{RealEmoji/DTO/FetchRealEmojiDTO.swift => APIs/RealEmoji/RealEmojiAPI/DataMapping/FetchRealEmojiListResponseDTO.swift} (88%) rename 14th-team5-iOS/Data/Sources/{RealEmoji/DTO/LoadMyRealEmojiDTO.swift => APIs/RealEmoji/RealEmojiAPI/DataMapping/MyRealEmojiResponseDTO.swift} (86%) create mode 100644 14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/DataMapping/RemoveRealEmojiRequestDTO.swift rename 14th-team5-iOS/Data/Sources/{RealEmoji/DTO/RemoveRealEmojiDTO.swift => APIs/RealEmoji/RealEmojiAPI/DataMapping/RemoveRealEmojiResponseDTO.swift} (54%) rename 14th-team5-iOS/Data/Sources/{ => APIs}/RealEmoji/RealEmojiAPI/RealEmojiAPIS.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => APIs}/RealEmoji/RealEmojiAPI/RealEmojiAPIWorker.swift (56%) create mode 100644 14th-team5-iOS/Data/Sources/APIs/RealEmoji/Repository/RealEmojiRepository.swift delete mode 100644 14th-team5-iOS/Data/Sources/Emoji/EmojiAPI/EmojiAPIWorker.swift delete mode 100644 14th-team5-iOS/Domain/Sources/Emoji/Interfaces/EmojiRepository.swift create mode 100644 14th-team5-iOS/Domain/Sources/Emoji/Interfaces/ReactionRepositoryProtocol.swift rename 14th-team5-iOS/Domain/Sources/RealEmoji/Interfaces/{RealEmojiRepository.swift => RealEmojiRepositoryProtocol.swift} (78%) diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Dependency/MainViewDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Dependency/MainViewDIContainer.swift index 58b981583..3c26cf9b8 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Dependency/MainViewDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Dependency/MainViewDIContainer.swift @@ -45,8 +45,8 @@ extension MainViewDIContainer { return PickUseCase(pickRepository: makePickReposiotry()) } - private func makeMainRepository() -> MainRepository { - return MainRepository() + private func makeMainRepository() -> MainViewRepository { + return MainViewRepository() } private func makeMissionUserDefaultsRepository() -> MissionUserDefaultsRepository { diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/PostListsDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/PostListsDIContainer.swift index 63b04b372..fdb8bfb4f 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/PostListsDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/PostListsDIContainer.swift @@ -48,11 +48,11 @@ final class PostListsDIContainer { return PostListUseCase(postListRepository: makePostRepository()) } - func makeEmojiRepository() -> EmojiRepository { - return EmojiAPIs.Worker() + func makeEmojiRepository() -> ReactionRepositoryProtocol { + return ReactionAPIs.Worker() } - func makeRealEmojiRepository() -> RealEmojiRepository { + func makeRealEmojiRepository() -> RealEmojiRepositoryProtocol { return RealEmojiAPIS.Worker() } diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/ReactionDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/ReactionDIContainer.swift index 325a65494..4e15dc0db 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/ReactionDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/ReactionDIContainer.swift @@ -39,7 +39,7 @@ final class ReactionDIContainer { } extension ReactionDIContainer { - private func makeRealEmojiRepository() -> RealEmojiRepository { + private func makeRealEmojiRepository() -> RealEmojiRepositoryProtocol { return RealEmojiAPIWorker() } @@ -49,8 +49,8 @@ extension ReactionDIContainer { } extension ReactionDIContainer { - private func makeEmojiRepository() -> EmojiRepository { - return EmojiAPIWorker() + private func makeEmojiRepository() -> ReactionRepositoryProtocol { + return ReactionAPIWorker() } private func makeEmojiUseCase() -> EmojiUseCaseProtocol { diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/SelectableEmojiDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/SelectableEmojiDIContainer.swift index 0eeece0e3..81e0f4651 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/SelectableEmojiDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/SelectableEmojiDIContainer.swift @@ -24,7 +24,7 @@ final class SelectableEmojiDIContainer { } extension SelectableEmojiDIContainer { - private func makeRealEmojiRepository() -> RealEmojiRepository { + private func makeRealEmojiRepository() -> RealEmojiRepositoryProtocol { return RealEmojiAPIWorker() } @@ -34,8 +34,8 @@ extension SelectableEmojiDIContainer { } extension SelectableEmojiDIContainer { - private func makeEmojiRepository() -> EmojiRepository { - return EmojiAPIWorker() + private func makeEmojiRepository() -> ReactionRepositoryProtocol { + return ReactionAPIWorker() } private func makeEmojiUseCase() -> EmojiUseCaseProtocol { diff --git a/14th-team5-iOS/Data/Sources/View/Main/DTO/MainNightResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/DataMapping/MainNightResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/View/Main/DTO/MainNightResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/DataMapping/MainNightResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/View/Main/DTO/MainRequestDTO.swift b/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/DataMapping/MainRequestDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/View/Main/DTO/MainRequestDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/DataMapping/MainRequestDTO.swift diff --git a/14th-team5-iOS/Data/Sources/View/Main/DTO/MainResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/DataMapping/MainResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/View/Main/DTO/MainResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/DataMapping/MainResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/View/Main/Worker/MainAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/MainViewAPIWorker.swift similarity index 61% rename from 14th-team5-iOS/Data/Sources/View/Main/Worker/MainAPIWorker.swift rename to 14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/MainViewAPIWorker.swift index 63b8f32ce..88d37096f 100644 --- a/14th-team5-iOS/Data/Sources/View/Main/Worker/MainAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/MainViewAPIWorker.swift @@ -12,8 +12,8 @@ import Domain import RxSwift -typealias MainAPIWorker = MainAPIs.Worker -extension MainAPIs { +typealias MainAPIWorker = MainViewAPIs.Worker +extension MainViewAPIs { final class Worker: APIWorker { static let queue = { ConcurrentDispatchQueueScheduler(queue: DispatchQueue(label: "MainAPIWorker", qos: .utility)) @@ -28,16 +28,8 @@ extension MainAPIs { extension MainAPIWorker { func fetchMain() -> Single { - return Observable.just(()) - .withLatestFrom(self._headers) - .withUnretained(self) - .flatMap { $0.0.fetchMain(headers: $0.1) } - .asSingle() - } - - private func fetchMain(headers: [APIHeader]?) -> Single { - let spec = MainAPIs.fetchMain.spec - return request(spec: spec, headers: headers) + let spec = MainViewAPIs.fetchMain.spec + return request(spec: spec) .subscribe(on: Self.queue) .do { if let str = String(data: $0.1, encoding: .utf8) { @@ -51,16 +43,8 @@ extension MainAPIWorker { } func fetchMainNight() -> Single { - return Observable.just(()) - .withLatestFrom(self._headers) - .withUnretained(self) - .flatMap { $0.0.fetchMainNight(headers: $0.1) } - .asSingle() - } - - private func fetchMainNight(headers: [APIHeader]?) -> Single { - let spec = MainAPIs.fetchMainNight.spec - return request(spec: spec, headers: headers) + let spec = MainViewAPIs.fetchMainNight.spec + return request(spec: spec) .subscribe(on: Self.queue) .do { if let str = String(data: $0.1, encoding: .utf8) { diff --git a/14th-team5-iOS/Data/Sources/View/Main/Worker/MainAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/MainViewAPIs.swift similarity index 95% rename from 14th-team5-iOS/Data/Sources/View/Main/Worker/MainAPIs.swift rename to 14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/MainViewAPIs.swift index 6dd7974f3..ba2346705 100644 --- a/14th-team5-iOS/Data/Sources/View/Main/Worker/MainAPIs.swift +++ b/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/MainViewAPIs.swift @@ -8,7 +8,7 @@ import Core import Foundation -enum MainAPIs: API { +enum MainViewAPIs: API { case fetchMain case fetchMainNight diff --git a/14th-team5-iOS/Data/Sources/View/Main/Repository/MainRepository.swift b/14th-team5-iOS/Data/Sources/APIs/MainView/Repository/MainViewRepository.swift similarity index 85% rename from 14th-team5-iOS/Data/Sources/View/Main/Repository/MainRepository.swift rename to 14th-team5-iOS/Data/Sources/APIs/MainView/Repository/MainViewRepository.swift index f54384654..0f99f17dc 100644 --- a/14th-team5-iOS/Data/Sources/View/Main/Repository/MainRepository.swift +++ b/14th-team5-iOS/Data/Sources/APIs/MainView/Repository/MainViewRepository.swift @@ -11,7 +11,7 @@ import Domain import RxSwift -public final class MainRepository: MainRepositoryProtocol { +public final class MainViewRepository: MainRepositoryProtocol { public let disposeBag: DisposeBag = DisposeBag() private let mainApiWorker: MainAPIWorker = MainAPIWorker() @@ -19,7 +19,7 @@ public final class MainRepository: MainRepositoryProtocol { public init() { } } -extension MainRepository { +extension MainViewRepository { public func fetchMain() -> Observable { return mainApiWorker.fetchMain() .asObservable() diff --git a/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/DataMapping/AddReactionRequestDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/DataMapping/AddReactionRequestDTO.swift new file mode 100644 index 000000000..e5957d320 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/DataMapping/AddReactionRequestDTO.swift @@ -0,0 +1,12 @@ +// +// AddReactionRequestDTO.swift +// Data +// +// Created by 마경미 on 04.06.24. +// + +import Foundation + +struct AddReactionRequestDTO: Codable { + let content: String +} diff --git a/14th-team5-iOS/Data/Sources/Emoji/DTO/AddEmojiDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/DataMapping/AddReactionResponseDTO.swift similarity index 64% rename from 14th-team5-iOS/Data/Sources/Emoji/DTO/AddEmojiDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/DataMapping/AddReactionResponseDTO.swift index 9d9ff434e..be8224555 100644 --- a/14th-team5-iOS/Data/Sources/Emoji/DTO/AddEmojiDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/DataMapping/AddReactionResponseDTO.swift @@ -8,11 +8,7 @@ import Foundation import Domain -public struct AddEmojiRequestDTO: Codable { - let content: String -} - -struct AddEmojiResponseDTO: Codable { +struct AddReactionResponseDTO: Codable { let success: Bool func toDomain() -> Void { diff --git a/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/DataMapping/FetchReactionRequestDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/DataMapping/FetchReactionRequestDTO.swift new file mode 100644 index 000000000..f155aa321 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/DataMapping/FetchReactionRequestDTO.swift @@ -0,0 +1,12 @@ +// +// FetchReactionRequestDTO.swift +// Data +// +// Created by 마경미 on 04.06.24. +// + +import Foundation + +struct FetchReactionRequestDTO: Codable { + let postId: String +} diff --git a/14th-team5-iOS/Data/Sources/Emoji/DTO/FetchEmojiDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/DataMapping/FetchReactionResponseDTO.swift similarity index 89% rename from 14th-team5-iOS/Data/Sources/Emoji/DTO/FetchEmojiDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/DataMapping/FetchReactionResponseDTO.swift index 6ac836f67..88fe136f9 100644 --- a/14th-team5-iOS/Data/Sources/Emoji/DTO/FetchEmojiDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/DataMapping/FetchReactionResponseDTO.swift @@ -10,11 +10,7 @@ import Foundation import Core import Domain -public struct FetchEmojiRequestDTO: Codable { - let postId: String -} - -struct FetchEmojiResponse: Codable { +struct FetchReactionResult: Codable { let reactionId: String let postId: String let memberId: String @@ -26,8 +22,8 @@ struct FetchEmojiResponse: Codable { } } -struct FetchEmojiResponseDTO: Codable { - let results: [FetchEmojiResponse] +struct FetchReactionResponseDTO: Codable { + let results: [FetchReactionResult] func toDomain() -> [FetchedEmojiData] { let myMemberId = FamilyUserDefaults.returnMyMemberId() diff --git a/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/DataMapping/RemoveReactionRequestDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/DataMapping/RemoveReactionRequestDTO.swift new file mode 100644 index 000000000..6a0b9c5fe --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/DataMapping/RemoveReactionRequestDTO.swift @@ -0,0 +1,12 @@ +// +// RemoveReactionRequestDTO.swift +// Data +// +// Created by 마경미 on 04.06.24. +// + +import Foundation + +struct RemoveReactionRequestDTO: Codable { + let content: String +} diff --git a/14th-team5-iOS/Data/Sources/Emoji/DTO/RemoveEmojiDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/DataMapping/RemoveReactionResponseDTO.swift similarity index 63% rename from 14th-team5-iOS/Data/Sources/Emoji/DTO/RemoveEmojiDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/DataMapping/RemoveReactionResponseDTO.swift index 1635f9479..c96c454cb 100644 --- a/14th-team5-iOS/Data/Sources/Emoji/DTO/RemoveEmojiDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/DataMapping/RemoveReactionResponseDTO.swift @@ -8,11 +8,7 @@ import Foundation import Domain -public struct RemoveEmojiRequestDTO: Codable { - let content: String -} - -struct RemoveEmojiResponseDTO: Codable { +struct RemoveReactionResponseDTO: Codable { let success: Bool func toDomain() -> Void { diff --git a/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/ReactionAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/ReactionAPIWorker.swift new file mode 100644 index 000000000..102b36b5e --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/ReactionAPIWorker.swift @@ -0,0 +1,79 @@ +// +// EmojiAPIWorker.swift +// Data +// +// Created by 마경미 on 01.01.24. +// + +import Foundation + +import Core +import Domain + +import RxSwift + +public typealias ReactionAPIWorker = ReactionAPIs.Worker +extension ReactionAPIs { + public final class Worker: APIWorker { + static let queue = { + ConcurrentDispatchQueueScheduler(queue: DispatchQueue(label: "ReactionAPIQueue", qos: .utility)) + }() + + public override init() { + super.init() + self.id = "ReactionAPIWorker" + } + } +} + +extension ReactionAPIWorker { + func fetchReaction(query: Domain.FetchEmojiQuery) -> RxSwift.Single<[FetchedEmojiData]?> { + let query = FetchReactionRequestDTO(postId: query.postId) + let spec = ReactionAPIs.fetchReactions(query).spec + return request(spec: spec) + .subscribe(on: Self.queue) + .do { + if let str = String(data: $0.1, encoding: .utf8) { + debugPrint("Fetch Reaction Result: \(str)") + } + } + .map(FetchReactionResponseDTO.self) + .catchAndReturn(nil) + .map { + $0?.toDomain() + } + .asSingle() + } + + func addReaction(query: Domain.AddEmojiQuery, body: Domain.AddEmojiBody) -> RxSwift.Single { + let requestDTO = AddReactionRequestDTO(content: body.emojiId) + let spec = ReactionAPIs.addReactions(query.postId).spec + return request(spec: spec, jsonEncodable: requestDTO) + .subscribe(on: Self.queue) + .do { + if let str = String(data: $0.1, encoding: .utf8) { + debugPrint("Add Reaction Result: \(str)") + } + } + .map(AddReactionResponseDTO.self) + .catchAndReturn(nil) + .map { $0?.toDomain() } + .asSingle() + } + + func removeReaction(query: Domain.RemoveEmojiQuery, body: Domain.RemoveEmojiBody) -> RxSwift.Single { + let requestDTO = RemoveReactionRequestDTO(content: body.content.emojiString) + let spec = ReactionAPIs.removeReactions(query.postId).spec + return request(spec: spec, jsonEncodable: requestDTO) + .subscribe(on: Self.queue) + .do { + if let str = String(data: $0.1, encoding: .utf8) { + debugPrint("Remove Reaction Result: \(str)") + } + } + .map(RemoveReactionResponseDTO.self) + .catchAndReturn(nil) + .map { $0?.toDomain() } + .asSingle() + } +} diff --git a/14th-team5-iOS/Data/Sources/Emoji/EmojiAPI/EmojiAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/ReactionAPIs.swift similarity index 90% rename from 14th-team5-iOS/Data/Sources/Emoji/EmojiAPI/EmojiAPIs.swift rename to 14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/ReactionAPIs.swift index ca8708178..91cffbb00 100644 --- a/14th-team5-iOS/Data/Sources/Emoji/EmojiAPI/EmojiAPIs.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/ReactionAPIs.swift @@ -8,10 +8,10 @@ import Core import Foundation -public enum EmojiAPIs: API { +public enum ReactionAPIs: API { case addReactions(String) case removeReactions(String) - case fetchReactions(FetchEmojiRequestDTO) + case fetchReactions(FetchReactionRequestDTO) public var spec: APISpec { switch self { diff --git a/14th-team5-iOS/Data/Sources/APIs/Reaction/Repository/ReactionRepository.swift b/14th-team5-iOS/Data/Sources/APIs/Reaction/Repository/ReactionRepository.swift new file mode 100644 index 000000000..b74782571 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/Reaction/Repository/ReactionRepository.swift @@ -0,0 +1,32 @@ +// +// ReactionRepository.swift +// Data +// +// Created by 마경미 on 04.06.24. +// + +import Foundation + +import Domain + +import RxSwift + +public final class ReactionRepository: ReactionRepositoryProtocol { + + public let disposeBag: DisposeBag = DisposeBag() + private let reactionAPIWorker: ReactionAPIWorker = ReactionAPIWorker() + + public init () { } + + public func addReaction(query: Domain.AddEmojiQuery, body: Domain.AddEmojiBody) -> RxSwift.Single { + reactionAPIWorker.addReaction(query: query, body: body) + } + + public func removeReaction(query: Domain.RemoveEmojiQuery, body: Domain.RemoveEmojiBody) -> RxSwift.Single { + reactionAPIWorker.removeReaction(query: query, body: body) + } + + public func fetchReaction(query: Domain.FetchEmojiQuery) -> RxSwift.Single<[Domain.FetchedEmojiData]?> { + reactionAPIWorker.fetchReaction(query: query) + } +} diff --git a/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/DataMapping/AddRealEmojiRequestDTO.swift b/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/DataMapping/AddRealEmojiRequestDTO.swift new file mode 100644 index 000000000..44b65e49a --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/DataMapping/AddRealEmojiRequestDTO.swift @@ -0,0 +1,16 @@ +// +// AddRealEmojiDTO.swift +// Data +// +// Created by 마경미 on 28.01.24. +// + +import Foundation + +struct AddRealEmojiParameters: Codable { + let postId: String +} + +struct AddRealEmojiRequestDTO: Codable { + let realEmojiId: String +} diff --git a/14th-team5-iOS/Data/Sources/RealEmoji/DTO/AddRealEmojiDTO.swift b/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/DataMapping/AddRealEmojiResponseDTO.swift similarity index 58% rename from 14th-team5-iOS/Data/Sources/RealEmoji/DTO/AddRealEmojiDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/DataMapping/AddRealEmojiResponseDTO.swift index 6ccfcfed6..f941021a3 100644 --- a/14th-team5-iOS/Data/Sources/RealEmoji/DTO/AddRealEmojiDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/DataMapping/AddRealEmojiResponseDTO.swift @@ -1,20 +1,12 @@ // -// AddRealEmojiDTO.swift +// AddRealEmojiResponseDTO.swift // Data // -// Created by 마경미 on 28.01.24. +// Created by 마경미 on 04.06.24. // import Foundation -public struct AddRealEmojiParameters: Codable { - let postId: String -} - -public struct AddRealEmojiRequestDTO: Codable { - let realEmojiId: String -} - struct AddRealEmojiResponseDTO: Codable { let postRealEmojiId: String let postId: String diff --git a/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/DataMapping/FetchRealEmojiListRequestDTO.swift b/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/DataMapping/FetchRealEmojiListRequestDTO.swift new file mode 100644 index 000000000..85deda90c --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/DataMapping/FetchRealEmojiListRequestDTO.swift @@ -0,0 +1,12 @@ +// +// FetchRealEmojiRequestDTO.swift +// Data +// +// Created by 마경미 on 04.06.24. +// + +import Foundation + +struct FetchRealEmojiListParameter: Codable { + let postId: String +} diff --git a/14th-team5-iOS/Data/Sources/RealEmoji/DTO/FetchRealEmojiDTO.swift b/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/DataMapping/FetchRealEmojiListResponseDTO.swift similarity index 88% rename from 14th-team5-iOS/Data/Sources/RealEmoji/DTO/FetchRealEmojiDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/DataMapping/FetchRealEmojiListResponseDTO.swift index 8bf83b349..3430ba855 100644 --- a/14th-team5-iOS/Data/Sources/RealEmoji/DTO/FetchRealEmojiDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/DataMapping/FetchRealEmojiListResponseDTO.swift @@ -10,12 +10,17 @@ import Foundation import Core import Domain -public struct FetchRealEmojiListParameter: Codable { +struct RealEmojiListResult: Codable { + let postRealEmojiId: String let postId: String + let memberId: String + let realEmojiId: String + let emojiImageUrl: String + let emojiType: String } -public struct FetchRealEmojiListResponseDTO: Codable { - let results: [RealEmojiDTO] +struct FetchRealEmojiListResponseDTO: Codable { + let results: [RealEmojiListResult] func toDomain() -> [FetchedEmojiData]? { let myMemberId = FamilyUserDefaults.returnMyMemberId() @@ -44,12 +49,3 @@ public struct FetchRealEmojiListResponseDTO: Codable { return fetchedEmojiDataArray } } - -public struct RealEmojiDTO: Codable { - let postRealEmojiId: String - let postId: String - let memberId: String - let realEmojiId: String - let emojiImageUrl: String - let emojiType: String -} diff --git a/14th-team5-iOS/Data/Sources/RealEmoji/DTO/LoadMyRealEmojiDTO.swift b/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/DataMapping/MyRealEmojiResponseDTO.swift similarity index 86% rename from 14th-team5-iOS/Data/Sources/RealEmoji/DTO/LoadMyRealEmojiDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/DataMapping/MyRealEmojiResponseDTO.swift index 926167057..8c13398c8 100644 --- a/14th-team5-iOS/Data/Sources/RealEmoji/DTO/LoadMyRealEmojiDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/DataMapping/MyRealEmojiResponseDTO.swift @@ -10,11 +10,19 @@ import Foundation import Core import Domain -public struct MyRealEmojiListResponse: Codable { - let myRealEmojiList: [MyRealEmojiList] +struct MyRealEmojiList: Codable { + let realEmojiId: String + let type: String + let imageUrl: String + + func toDomain() -> MyRealEmoji { + return .init(realEmojiId: realEmojiId, type: Emojis.emoji(forString: type), imageUrl: imageUrl) + } } -extension MyRealEmojiListResponse { +struct MyRealEmojiResponseDTO: Codable { + let myRealEmojiList: [MyRealEmojiList] + func toDomain() -> [MyRealEmoji?] { var arr: [MyRealEmoji?] = [nil, nil, nil, nil, nil] myRealEmojiList.forEach { @@ -26,15 +34,3 @@ extension MyRealEmojiListResponse { return arr } } - -struct MyRealEmojiList: Codable { - let realEmojiId: String - let type: String - let imageUrl: String -} - -extension MyRealEmojiList { - func toDomain() -> MyRealEmoji { - return .init(realEmojiId: realEmojiId, type: Emojis.emoji(forString: type), imageUrl: imageUrl) - } -} diff --git a/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/DataMapping/RemoveRealEmojiRequestDTO.swift b/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/DataMapping/RemoveRealEmojiRequestDTO.swift new file mode 100644 index 000000000..c36cae406 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/DataMapping/RemoveRealEmojiRequestDTO.swift @@ -0,0 +1,13 @@ +// +// RemoveRealEmojiRequestDTO.swift +// Data +// +// Created by 마경미 on 04.06.24. +// + +import Foundation + +struct RemoveRealEmojiParameters: Codable { + let postId: String + let realEmojiId: String +} diff --git a/14th-team5-iOS/Data/Sources/RealEmoji/DTO/RemoveRealEmojiDTO.swift b/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/DataMapping/RemoveRealEmojiResponseDTO.swift similarity index 54% rename from 14th-team5-iOS/Data/Sources/RealEmoji/DTO/RemoveRealEmojiDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/DataMapping/RemoveRealEmojiResponseDTO.swift index f2c109291..f4fc2cbbe 100644 --- a/14th-team5-iOS/Data/Sources/RealEmoji/DTO/RemoveRealEmojiDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/DataMapping/RemoveRealEmojiResponseDTO.swift @@ -7,12 +7,7 @@ import Foundation -public struct RemoveRealEmojiParameters: Codable { - let postId: String - let realEmojiId: String -} - -public struct RemoveRealEmojiResponse: Codable { +public struct RemoveRealEmojiResponseDTO: Codable { let success: Bool func toDomain() -> Void { diff --git a/14th-team5-iOS/Data/Sources/RealEmoji/RealEmojiAPI/RealEmojiAPIS.swift b/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/RealEmojiAPIS.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/RealEmoji/RealEmojiAPI/RealEmojiAPIS.swift rename to 14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/RealEmojiAPIS.swift diff --git a/14th-team5-iOS/Data/Sources/RealEmoji/RealEmojiAPI/RealEmojiAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/RealEmojiAPIWorker.swift similarity index 56% rename from 14th-team5-iOS/Data/Sources/RealEmoji/RealEmojiAPI/RealEmojiAPIWorker.swift rename to 14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/RealEmojiAPIWorker.swift index 715c1e23c..7e3119663 100644 --- a/14th-team5-iOS/Data/Sources/RealEmoji/RealEmojiAPI/RealEmojiAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/RealEmojiAPIWorker.swift @@ -27,19 +27,13 @@ extension RealEmojiAPIS { } } -extension RealEmojiAPIWorker: RealEmojiRepository { - public func fetchRealEmoji(query: FetchRealEmojiQuery) -> Single<[FetchedEmojiData]?> { - return Observable.just(()) - .withLatestFrom(self._headers) - .withUnretained(self) - .flatMap { $0.0.fetchRealEmoji(headers: $0.1, query: query)} - .asSingle() - } +extension RealEmojiAPIWorker { - private func fetchRealEmoji(headers: [APIHeader]?, query: FetchRealEmojiQuery) -> Single<[FetchedEmojiData]?> { + func fetchRealEmoji(query: FetchRealEmojiQuery) -> Single<[FetchedEmojiData]?> { let query = FetchRealEmojiListParameter(postId: query.postId) let spec = RealEmojiAPIS.fetchRealEmojiList(query).spec - return request(spec: spec, headers: headers) + + return request(spec: spec) .subscribe(on: Self.queue) .do { if let str = String(data: $0.1, encoding: .utf8) { @@ -54,25 +48,17 @@ extension RealEmojiAPIWorker: RealEmojiRepository { .asSingle() } - public func loadMyRealEmoji() -> Single<[MyRealEmoji?]> { - return Observable.just(()) - .withLatestFrom(self._headers) - .withUnretained(self) - .flatMap { $0.0.loadMyRealEmoji(headers: $0.1)} - .asSingle() - } - - private func loadMyRealEmoji(headers: [APIHeader]?) -> Single<[MyRealEmoji?]> { + func loadMyRealEmoji() -> Single<[MyRealEmoji?]> { let spec = RealEmojiAPIS.loadMyRealEmoji.spec - return request(spec: spec, headers: headers) + return request(spec: spec) .subscribe(on: Self.queue) .do { if let str = String(data: $0.1, encoding: .utf8) { debugPrint("Real Emoji Items Result: \(str)") } } - .map(MyRealEmojiListResponse.self) + .map(MyRealEmojiResponseDTO.self) .catchAndReturn(nil) .map { return $0?.toDomain() ?? Array(repeating: nil, count: 5) @@ -80,19 +66,11 @@ extension RealEmojiAPIWorker: RealEmojiRepository { .asSingle() } - public func addRealEmoji(query: AddEmojiQuery, body: AddEmojiBody) -> Single { - return Observable.just(()) - .withLatestFrom(self._headers) - .withUnretained(self) - .flatMap { $0.0.addRealEmoji(headers: $0.1, query: query, body: body)} - .asSingle() - } - - private func addRealEmoji(headers: [APIHeader]?, query: AddEmojiQuery, body: AddEmojiBody) -> Single { + func addRealEmoji(query: AddEmojiQuery, body: AddEmojiBody) -> Single { let spec = RealEmojiAPIS.addRealEmoji(.init(postId: query.postId)).spec let body = AddRealEmojiRequestDTO(realEmojiId: body.emojiId) - return request(spec: spec, headers: headers, jsonEncodable: body) + return request(spec: spec, jsonEncodable: body) .subscribe(on: Self.queue) .do { if let str = String(data: $0.1, encoding: .utf8) { @@ -107,25 +85,17 @@ extension RealEmojiAPIWorker: RealEmojiRepository { .asSingle() } - public func removeRealEmoji(query: RemoveRealEmojiQuery) -> Single { - return Observable.just(()) - .withLatestFrom(self._headers) - .withUnretained(self) - .flatMap { $0.0.removeRealEmoji(headers: $0.1, query: query)} - .asSingle() - } - - private func removeRealEmoji(headers: [APIHeader]?, query: RemoveRealEmojiQuery) -> Single { + func removeRealEmoji(query: RemoveRealEmojiQuery) -> Single { let spec = RealEmojiAPIS.removeRealEmoji(.init(postId: query.postId, realEmojiId: query.realEmojiId)).spec - return request(spec: spec, headers: headers) + return request(spec: spec) .subscribe(on: Self.queue) .do { if let str = String(data: $0.1, encoding: .utf8) { debugPrint("Remove Real Emoji Result: \(str)") } } - .map(RemoveRealEmojiResponse.self) + .map(RemoveRealEmojiResponseDTO.self) .catchAndReturn(nil) .map { return $0?.toDomain() diff --git a/14th-team5-iOS/Data/Sources/APIs/RealEmoji/Repository/RealEmojiRepository.swift b/14th-team5-iOS/Data/Sources/APIs/RealEmoji/Repository/RealEmojiRepository.swift new file mode 100644 index 000000000..fcc5bedfd --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/RealEmoji/Repository/RealEmojiRepository.swift @@ -0,0 +1,38 @@ +// +// RealEmojiRepository.swift +// Data +// +// Created by 마경미 on 04.06.24. +// + +import Foundation + +import Domain + +import RxSwift + +public final class RealEmojiRepository: RealEmojiRepositoryProtocol { + + public let disposeBag: DisposeBag = DisposeBag() + private let realEmojiAPIWorker: RealEmojiAPIWorker = RealEmojiAPIWorker() + + public init () { } + + public func fetchMyRealEmoji() -> RxSwift.Single<[Domain.MyRealEmoji?]> { + realEmojiAPIWorker.loadMyRealEmoji() + } + + public func addRealEmoji(query: Domain.AddEmojiQuery, body: Domain.AddEmojiBody) -> RxSwift.Single { + realEmojiAPIWorker.addRealEmoji(query: query, body: body) + } + + public func fetchRealEmoji(query: Domain.FetchRealEmojiQuery) -> RxSwift.Single<[Domain.FetchedEmojiData]?> { + realEmojiAPIWorker.fetchRealEmoji(query: query) + } + + public func removeRealEmoji(query: Domain.RemoveRealEmojiQuery) -> RxSwift.Single { + realEmojiAPIWorker.removeRealEmoji(query: query) + } + + +} diff --git a/14th-team5-iOS/Data/Sources/Emoji/EmojiAPI/EmojiAPIWorker.swift b/14th-team5-iOS/Data/Sources/Emoji/EmojiAPI/EmojiAPIWorker.swift deleted file mode 100644 index ff6f26136..000000000 --- a/14th-team5-iOS/Data/Sources/Emoji/EmojiAPI/EmojiAPIWorker.swift +++ /dev/null @@ -1,103 +0,0 @@ -// -// EmojiAPIWorker.swift -// Data -// -// Created by 마경미 on 01.01.24. -// - -import Foundation - -import Core -import Domain - -import RxSwift - -public typealias EmojiAPIWorker = EmojiAPIs.Worker -extension EmojiAPIs { - public final class Worker: APIWorker { - static let queue = { - ConcurrentDispatchQueueScheduler(queue: DispatchQueue(label: "EmojiAPIQueue", qos: .utility)) - }() - - public override init() { - super.init() - self.id = "EmojiAPIWorker" - } - } -} - -extension EmojiAPIWorker: EmojiRepository { - public func fetchEmoji(query: FetchEmojiQuery) -> Single<[FetchedEmojiData]?> { - return Observable.just(()) - .withLatestFrom(self._headers) - .withUnretained(self) - .flatMap { $0.0.fetchEmoji(headers: $0.1, query: query) } - .asSingle() - } - - private func fetchEmoji(headers: [APIHeader]?, query: Domain.FetchEmojiQuery) -> RxSwift.Single<[FetchedEmojiData]?> { - let query = FetchEmojiRequestDTO(postId: query.postId) - let spec = EmojiAPIs.fetchReactions(query).spec - return request(spec: spec, headers: headers) - .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("FetchEmoji Fetch Result: \(str)") - } - } - .map(FetchEmojiResponseDTO.self) - .catchAndReturn(nil) - .map { - $0?.toDomain() - } - .asSingle() - } - - public func addEmoji(query: AddEmojiQuery, body: AddEmojiBody) -> Single { - return Observable.just(()) - .withLatestFrom(self._headers) - .withUnretained(self) - .flatMap { $0.0.addEmoji(headers: $0.1, query: query, body: body) } - .asSingle() - } - - private func addEmoji(headers: [APIHeader]?, query: Domain.AddEmojiQuery, body: Domain.AddEmojiBody) -> RxSwift.Single { - let requestDTO = AddEmojiRequestDTO(content: body.emojiId) - let spec = EmojiAPIs.addReactions(query.postId).spec - return request(spec: spec, headers: headers, jsonEncodable: requestDTO) - .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("AddEmoji Fetch Result: \(str)") - } - } - .map(AddEmojiResponseDTO.self) - .catchAndReturn(nil) - .map { $0?.toDomain() } - .asSingle() - } - - public func removeEmoji(query: RemoveEmojiQuery, body: RemoveEmojiBody) -> Single { - return Observable.just(()) - .withLatestFrom(self._headers) - .withUnretained(self) - .flatMap { $0.0.removeEmoji(headers: $0.1, query: query, body: body) } - .asSingle() - } - - private func removeEmoji(headers: [APIHeader]?, query: Domain.RemoveEmojiQuery, body: Domain.RemoveEmojiBody) -> RxSwift.Single { - let requestDTO = RemoveEmojiRequestDTO(content: body.content.emojiString) - let spec = EmojiAPIs.removeReactions(query.postId).spec - return request(spec: spec, headers: headers, jsonEncodable: requestDTO) - .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("Remove Fetch Result: \(str)") - } - } - .map(RemoveEmojiResponseDTO.self) - .catchAndReturn(nil) - .map { $0?.toDomain() } - .asSingle() - } -} diff --git a/14th-team5-iOS/Domain/Sources/Emoji/Interfaces/EmojiRepository.swift b/14th-team5-iOS/Domain/Sources/Emoji/Interfaces/EmojiRepository.swift deleted file mode 100644 index 532e8b6e3..000000000 --- a/14th-team5-iOS/Domain/Sources/Emoji/Interfaces/EmojiRepository.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// EmojiRepository.swift -// Domain -// -// Created by 마경미 on 01.01.24. -// - -import Foundation - -import RxSwift - -public protocol EmojiRepository { - func addEmoji(query: AddEmojiQuery, body: AddEmojiBody) -> Single - func removeEmoji(query: RemoveEmojiQuery, body: RemoveEmojiBody) -> Single - func fetchEmoji(query: FetchEmojiQuery) -> Single<[FetchedEmojiData]?> -} diff --git a/14th-team5-iOS/Domain/Sources/Emoji/Interfaces/ReactionRepositoryProtocol.swift b/14th-team5-iOS/Domain/Sources/Emoji/Interfaces/ReactionRepositoryProtocol.swift new file mode 100644 index 000000000..21f974e75 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Emoji/Interfaces/ReactionRepositoryProtocol.swift @@ -0,0 +1,16 @@ +// +// EmojiRepository.swift +// Domain +// +// Created by 마경미 on 01.01.24. +// + +import Foundation + +import RxSwift + +public protocol ReactionRepositoryProtocol { + func addReaction(query: AddEmojiQuery, body: AddEmojiBody) -> Single + func removeReaction(query: RemoveEmojiQuery, body: RemoveEmojiBody) -> Single + func fetchReaction(query: FetchEmojiQuery) -> Single<[FetchedEmojiData]?> +} diff --git a/14th-team5-iOS/Domain/Sources/Emoji/UseCases/EmojiUseCase.swift b/14th-team5-iOS/Domain/Sources/Emoji/UseCases/EmojiUseCase.swift index 65e9e15b1..d2a78e8e1 100644 --- a/14th-team5-iOS/Domain/Sources/Emoji/UseCases/EmojiUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/Emoji/UseCases/EmojiUseCase.swift @@ -23,14 +23,14 @@ public protocol EmojiUseCaseProtocol { } public class EmojiUseCase: EmojiUseCaseProtocol { - private let emojiRepository: EmojiRepository + private let emojiRepository: ReactionRepositoryProtocol - public init(emojiRepository: EmojiRepository) { + public init(emojiRepository: ReactionRepositoryProtocol) { self.emojiRepository = emojiRepository } public func executeAddEmoji(query: AddEmojiQuery, body: AddEmojiBody) -> Single { - return emojiRepository.addEmoji(query: query, body: body) + return emojiRepository.addReaction(query: query, body: body) } public func excuteRemoveEmoji(query: RemoveEmojiQuery, body: RemoveEmojiBody) -> Single { @@ -38,6 +38,6 @@ public class EmojiUseCase: EmojiUseCaseProtocol { } public func execute(query: FetchEmojiQuery) -> Single<[FetchedEmojiData]?> { - return emojiRepository.fetchEmoji(query: query) + return emojiRepository.fetchReaction(query: query) } } diff --git a/14th-team5-iOS/Domain/Sources/RealEmoji/Interfaces/RealEmojiRepository.swift b/14th-team5-iOS/Domain/Sources/RealEmoji/Interfaces/RealEmojiRepositoryProtocol.swift similarity index 78% rename from 14th-team5-iOS/Domain/Sources/RealEmoji/Interfaces/RealEmojiRepository.swift rename to 14th-team5-iOS/Domain/Sources/RealEmoji/Interfaces/RealEmojiRepositoryProtocol.swift index 51aa5d6e2..f272251a9 100644 --- a/14th-team5-iOS/Domain/Sources/RealEmoji/Interfaces/RealEmojiRepository.swift +++ b/14th-team5-iOS/Domain/Sources/RealEmoji/Interfaces/RealEmojiRepositoryProtocol.swift @@ -9,8 +9,8 @@ import Foundation import RxSwift -public protocol RealEmojiRepository { - func loadMyRealEmoji() -> Single<[MyRealEmoji?]> +public protocol RealEmojiRepositoryProtocol { + func fetchMyRealEmoji() -> Single<[MyRealEmoji?]> func addRealEmoji(query: AddEmojiQuery, body: AddEmojiBody) -> Single func fetchRealEmoji(query: FetchRealEmojiQuery) -> Single<[FetchedEmojiData]?> func removeRealEmoji(query: RemoveRealEmojiQuery) -> Single diff --git a/14th-team5-iOS/Domain/Sources/RealEmoji/UseCases/RealEmojiUseCase.swift b/14th-team5-iOS/Domain/Sources/RealEmoji/UseCases/RealEmojiUseCase.swift index 86b8f09ea..389e74665 100644 --- a/14th-team5-iOS/Domain/Sources/RealEmoji/UseCases/RealEmojiUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/RealEmoji/UseCases/RealEmojiUseCase.swift @@ -21,14 +21,14 @@ public protocol RealEmojiUseCaseProtocol { public class RealEmojiUseCase: RealEmojiUseCaseProtocol { - private let realEmojiRepository: RealEmojiRepository + private let realEmojiRepository: RealEmojiRepositoryProtocol - public init(realEmojiRepository: RealEmojiRepository) { + public init(realEmojiRepository: RealEmojiRepositoryProtocol) { self.realEmojiRepository = realEmojiRepository } public func execute() -> Single<[MyRealEmoji?]> { - return realEmojiRepository.loadMyRealEmoji() + return realEmojiRepository.fetchMyRealEmoji() } public func execute(query: FetchRealEmojiQuery) -> Single<[FetchedEmojiData]?> { From 9087a47396647ed30733443bbf15bfdd9694a588 Mon Sep 17 00:00:00 2001 From: Kyoungmi <1mmgm.dev@gmail.com> Date: Tue, 4 Jun 2024 21:05:19 +0900 Subject: [PATCH 098/263] [refactor]: APIHeader(#543) --- .../Dependency/PostListsDIContainer.swift | 8 ++-- .../Dependency/ReactionDIContainer.swift | 10 ++--- .../SelectableEmojiDIContainer.swift | 10 ++--- .../PostDetail/Reactor/PostReactor.swift | 4 +- .../Reactor/ReactionViewReactor.swift | 4 +- .../Reactor/SelectableEmojiReactor.swift | 5 ++- .../MainViewAPI/MainViewAPIWorker.swift | 16 ++++++- .../ReactionAPI/ReactionAPIWorker.swift | 42 ++++++++++++------- .../Reaction/ReactionAPI/ReactionAPIs.swift | 4 +- .../Repository/ReactionRepository.swift | 8 ++-- .../RealEmojiAPI/RealEmojiAPIS.swift | 6 +-- .../RealEmojiAPI/RealEmojiAPIWorker.swift | 26 ++++++++---- .../Repository/RealEmojiRepository.swift | 4 +- ...ojiUseCase.swift => ReactionUseCase.swift} | 16 +++---- 14 files changed, 99 insertions(+), 64 deletions(-) rename 14th-team5-iOS/Domain/Sources/Emoji/UseCases/{EmojiUseCase.swift => ReactionUseCase.swift} (61%) diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/PostListsDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/PostListsDIContainer.swift index fdb8bfb4f..2244a71d5 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/PostListsDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/PostListsDIContainer.swift @@ -49,11 +49,11 @@ final class PostListsDIContainer { } func makeEmojiRepository() -> ReactionRepositoryProtocol { - return ReactionAPIs.Worker() + return ReactionRepository() } func makeRealEmojiRepository() -> RealEmojiRepositoryProtocol { - return RealEmojiAPIS.Worker() + return RealEmojiRepository() } func makeMissionRepository() -> MissionRepositoryProtocol { @@ -64,8 +64,8 @@ final class PostListsDIContainer { return MissionContentUseCase(missionContentRepository: makeMissionRepository()) } - func makeEmojiUseCase() -> EmojiUseCaseProtocol { - return EmojiUseCase(emojiRepository: makeEmojiRepository()) + func makeEmojiUseCase() -> ReactionUseCaseProtocol { + return ReactionUseCase(reactionRepository: makeEmojiRepository()) } func makeRealEmojiUseCase() -> RealEmojiUseCaseProtocol { diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/ReactionDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/ReactionDIContainer.swift index 4e15dc0db..d032aa43e 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/ReactionDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/ReactionDIContainer.swift @@ -40,7 +40,7 @@ final class ReactionDIContainer { extension ReactionDIContainer { private func makeRealEmojiRepository() -> RealEmojiRepositoryProtocol { - return RealEmojiAPIWorker() + return RealEmojiRepository() } private func makeRealEmojiUseCase() -> RealEmojiUseCaseProtocol { @@ -49,11 +49,11 @@ extension ReactionDIContainer { } extension ReactionDIContainer { - private func makeEmojiRepository() -> ReactionRepositoryProtocol { - return ReactionAPIWorker() + private func makeReactionRepository() -> ReactionRepositoryProtocol { + return ReactionRepository() } - private func makeEmojiUseCase() -> EmojiUseCaseProtocol { - return EmojiUseCase(emojiRepository: makeEmojiRepository()) + private func makeEmojiUseCase() -> ReactionUseCaseProtocol { + return ReactionUseCase(reactionRepository: makeReactionRepository()) } } diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/SelectableEmojiDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/SelectableEmojiDIContainer.swift index 81e0f4651..0048f18a9 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/SelectableEmojiDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/SelectableEmojiDIContainer.swift @@ -25,7 +25,7 @@ final class SelectableEmojiDIContainer { extension SelectableEmojiDIContainer { private func makeRealEmojiRepository() -> RealEmojiRepositoryProtocol { - return RealEmojiAPIWorker() + return RealEmojiRepository() } private func makeRealEmojiUseCase() -> RealEmojiUseCaseProtocol { @@ -34,11 +34,11 @@ extension SelectableEmojiDIContainer { } extension SelectableEmojiDIContainer { - private func makeEmojiRepository() -> ReactionRepositoryProtocol { - return ReactionAPIWorker() + private func makeReactionRepository() -> ReactionRepositoryProtocol { + return ReactionRepository() } - private func makeEmojiUseCase() -> EmojiUseCaseProtocol { - return EmojiUseCase(emojiRepository: makeEmojiRepository()) + private func makeEmojiUseCase() -> ReactionUseCaseProtocol { + return ReactionUseCase(reactionRepository: makeReactionRepository()) } } diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostReactor.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostReactor.swift index 8c099a3ff..bcd55f062 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostReactor.swift @@ -43,7 +43,7 @@ final class PostReactor: Reactor { let initialState: State let realEmojiRepository: RealEmojiUseCaseProtocol - let emojiRepository: EmojiUseCaseProtocol + let emojiRepository: ReactionUseCaseProtocol let missionUseCase: MissionContentUseCaseProtocol let provider: GlobalStateProviderProtocol @@ -51,7 +51,7 @@ final class PostReactor: Reactor { init( provider: GlobalStateProviderProtocol, realEmojiRepository: RealEmojiUseCaseProtocol, - emojiRepository: EmojiUseCaseProtocol, + emojiRepository: ReactionUseCaseProtocol, missionUseCase: MissionContentUseCaseProtocol, initialState: State ) { diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/ReactionViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/ReactionViewReactor.swift index fe2038554..788596c8d 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/ReactionViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/ReactionViewReactor.swift @@ -55,13 +55,13 @@ final class ReactionViewReactor: Reactor { let initialState: State let provider: GlobalStateProviderProtocol - let emojiRepository: EmojiUseCaseProtocol + let emojiRepository: ReactionUseCaseProtocol let realEmojiRepository: RealEmojiUseCaseProtocol init( provider: GlobalStateProviderProtocol, initialState: State, - emojiRepository: EmojiUseCaseProtocol, + emojiRepository: ReactionUseCaseProtocol, realEmojiRepository: RealEmojiUseCaseProtocol ) { self.initialState = initialState diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/SelectableEmojiReactor.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/SelectableEmojiReactor.swift index 19097437b..ad59344ea 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/SelectableEmojiReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/SelectableEmojiReactor.swift @@ -36,11 +36,11 @@ final class SelectableEmojiReactor: Reactor { } let postId: String - let emojiRepository: EmojiUseCaseProtocol + let emojiRepository: ReactionUseCaseProtocol let realEmojiRepository: RealEmojiUseCaseProtocol var initialState: State = State() - init(postId: String, emojiRepository: EmojiUseCaseProtocol, realEmojiRepository: RealEmojiUseCaseProtocol) { + init(postId: String, emojiRepository: ReactionUseCaseProtocol, realEmojiRepository: RealEmojiUseCaseProtocol) { self.postId = postId self.emojiRepository = emojiRepository self.realEmojiRepository = realEmojiRepository @@ -56,6 +56,7 @@ extension SelectableEmojiReactor { case let .selectStandard(emoji): let query = AddEmojiQuery(postId: self.postId) let body = AddEmojiBody(content: emoji.emojiString) + return emojiRepository.executeAddEmoji(query: query, body: body) .asObservable() .flatMap {_ in diff --git a/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/MainViewAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/MainViewAPIWorker.swift index 88d37096f..1a31f844b 100644 --- a/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/MainViewAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/MainViewAPIWorker.swift @@ -23,13 +23,25 @@ extension MainViewAPIs { super.init() self.id = "MainAPIWorker" } + + var headers: [APIHeader] { + var headers: [any APIHeader] = [] + + _headers.subscribe(onNext: { result in + if let unwrappedHeaders = result { + headers = unwrappedHeaders + } + }).dispose() + + return headers + } } } extension MainAPIWorker { func fetchMain() -> Single { let spec = MainViewAPIs.fetchMain.spec - return request(spec: spec) + return request(spec: spec, headers: headers) .subscribe(on: Self.queue) .do { if let str = String(data: $0.1, encoding: .utf8) { @@ -44,7 +56,7 @@ extension MainAPIWorker { func fetchMainNight() -> Single { let spec = MainViewAPIs.fetchMainNight.spec - return request(spec: spec) + return request(spec: spec, headers: headers) .subscribe(on: Self.queue) .do { if let str = String(data: $0.1, encoding: .utf8) { diff --git a/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/ReactionAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/ReactionAPIWorker.swift index 102b36b5e..e472ad81c 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/ReactionAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/ReactionAPIWorker.swift @@ -12,7 +12,7 @@ import Domain import RxSwift -public typealias ReactionAPIWorker = ReactionAPIs.Worker +typealias ReactionAPIWorker = ReactionAPIs.Worker extension ReactionAPIs { public final class Worker: APIWorker { static let queue = { @@ -23,6 +23,18 @@ extension ReactionAPIs { super.init() self.id = "ReactionAPIWorker" } + + var headers: [APIHeader] { + var headers: [any APIHeader] = [] + + _headers.subscribe(onNext: { result in + if let unwrappedHeaders = result { + headers = unwrappedHeaders + } + }).dispose() + + return headers + } } } @@ -30,25 +42,25 @@ extension ReactionAPIWorker { func fetchReaction(query: Domain.FetchEmojiQuery) -> RxSwift.Single<[FetchedEmojiData]?> { let query = FetchReactionRequestDTO(postId: query.postId) let spec = ReactionAPIs.fetchReactions(query).spec - return request(spec: spec) - .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("Fetch Reaction Result: \(str)") + return request(spec: spec, headers: headers) + .subscribe(on: Self.queue) + .do { + if let str = String(data: $0.1, encoding: .utf8) { + debugPrint("Fetch Reaction Result: \(str)") + } } - } - .map(FetchReactionResponseDTO.self) - .catchAndReturn(nil) - .map { - $0?.toDomain() - } - .asSingle() + .map(FetchReactionResponseDTO.self) + .catchAndReturn(nil) + .map { + $0?.toDomain() + } + .asSingle() } func addReaction(query: Domain.AddEmojiQuery, body: Domain.AddEmojiBody) -> RxSwift.Single { let requestDTO = AddReactionRequestDTO(content: body.emojiId) let spec = ReactionAPIs.addReactions(query.postId).spec - return request(spec: spec, jsonEncodable: requestDTO) + return request(spec: spec, headers: headers, jsonEncodable: requestDTO) .subscribe(on: Self.queue) .do { if let str = String(data: $0.1, encoding: .utf8) { @@ -64,7 +76,7 @@ extension ReactionAPIWorker { func removeReaction(query: Domain.RemoveEmojiQuery, body: Domain.RemoveEmojiBody) -> RxSwift.Single { let requestDTO = RemoveReactionRequestDTO(content: body.content.emojiString) let spec = ReactionAPIs.removeReactions(query.postId).spec - return request(spec: spec, jsonEncodable: requestDTO) + return request(spec: spec, headers: headers, jsonEncodable: requestDTO) .subscribe(on: Self.queue) .do { if let str = String(data: $0.1, encoding: .utf8) { diff --git a/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/ReactionAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/ReactionAPIs.swift index 91cffbb00..f181432f6 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/ReactionAPIs.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/ReactionAPIs.swift @@ -8,12 +8,12 @@ import Core import Foundation -public enum ReactionAPIs: API { +enum ReactionAPIs: API { case addReactions(String) case removeReactions(String) case fetchReactions(FetchReactionRequestDTO) - public var spec: APISpec { + var spec: APISpec { switch self { case let .addReactions(postId): let urlString = "\(BibbiAPI.hostApi)/posts/\(postId)/reactions" diff --git a/14th-team5-iOS/Data/Sources/APIs/Reaction/Repository/ReactionRepository.swift b/14th-team5-iOS/Data/Sources/APIs/Reaction/Repository/ReactionRepository.swift index b74782571..88bdf2027 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Reaction/Repository/ReactionRepository.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Reaction/Repository/ReactionRepository.swift @@ -13,20 +13,20 @@ import RxSwift public final class ReactionRepository: ReactionRepositoryProtocol { - public let disposeBag: DisposeBag = DisposeBag() + private let disposeBag: DisposeBag = DisposeBag() private let reactionAPIWorker: ReactionAPIWorker = ReactionAPIWorker() public init () { } public func addReaction(query: Domain.AddEmojiQuery, body: Domain.AddEmojiBody) -> RxSwift.Single { - reactionAPIWorker.addReaction(query: query, body: body) + return reactionAPIWorker.addReaction(query: query, body: body) } public func removeReaction(query: Domain.RemoveEmojiQuery, body: Domain.RemoveEmojiBody) -> RxSwift.Single { - reactionAPIWorker.removeReaction(query: query, body: body) + return reactionAPIWorker.removeReaction(query: query, body: body) } public func fetchReaction(query: Domain.FetchEmojiQuery) -> RxSwift.Single<[Domain.FetchedEmojiData]?> { - reactionAPIWorker.fetchReaction(query: query) + return reactionAPIWorker.fetchReaction(query: query) } } diff --git a/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/RealEmojiAPIS.swift b/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/RealEmojiAPIS.swift index 5373e4472..120b5dce3 100644 --- a/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/RealEmojiAPIS.swift +++ b/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/RealEmojiAPIS.swift @@ -8,9 +8,9 @@ import Core import Foundation -public enum RealEmojiAPIS: API { +enum RealEmojiAPIS: API { case fetchRealEmojiList(FetchRealEmojiListParameter) - case loadMyRealEmoji + case fetchMyRealEmoji case addRealEmoji(AddRealEmojiParameters) case removeRealEmoji(RemoveRealEmojiParameters) @@ -22,7 +22,7 @@ public enum RealEmojiAPIS: API { case .fetchRealEmojiList(let parameter): let urlString = "\(BibbiAPI.hostApi)/posts/\(parameter.postId)/real-emoji" return APISpec(method: .get, url: urlString) - case .loadMyRealEmoji: + case .fetchMyRealEmoji: let memberId = App.Repository.member.memberID.value let urlString = "\(BibbiAPI.hostApi)/members/\(memberId ?? "")/real-emoji" return APISpec(method: .get, url: urlString) diff --git a/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/RealEmojiAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/RealEmojiAPIWorker.swift index 7e3119663..bfa3111db 100644 --- a/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/RealEmojiAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/RealEmojiAPIWorker.swift @@ -13,7 +13,7 @@ import Domain import Alamofire import RxSwift -public typealias RealEmojiAPIWorker = RealEmojiAPIS.Worker +typealias RealEmojiAPIWorker = RealEmojiAPIS.Worker extension RealEmojiAPIS { public final class Worker: APIWorker { static let queue = { @@ -24,6 +24,18 @@ extension RealEmojiAPIS { super.init() self.id = "RealEmojiAPIWorker" } + + var headers: [APIHeader] { + var headers: [any APIHeader] = [] + + _headers.subscribe(onNext: { result in + if let unwrappedHeaders = result { + headers = unwrappedHeaders + } + }).dispose() + + return headers + } } } @@ -33,7 +45,7 @@ extension RealEmojiAPIWorker { let query = FetchRealEmojiListParameter(postId: query.postId) let spec = RealEmojiAPIS.fetchRealEmojiList(query).spec - return request(spec: spec) + return request(spec: spec, headers: headers) .subscribe(on: Self.queue) .do { if let str = String(data: $0.1, encoding: .utf8) { @@ -48,10 +60,10 @@ extension RealEmojiAPIWorker { .asSingle() } - func loadMyRealEmoji() -> Single<[MyRealEmoji?]> { - let spec = RealEmojiAPIS.loadMyRealEmoji.spec + func fetchMyRealEmoji() -> Single<[MyRealEmoji?]> { + let spec = RealEmojiAPIS.fetchMyRealEmoji.spec - return request(spec: spec) + return request(spec: spec, headers: headers) .subscribe(on: Self.queue) .do { if let str = String(data: $0.1, encoding: .utf8) { @@ -70,7 +82,7 @@ extension RealEmojiAPIWorker { let spec = RealEmojiAPIS.addRealEmoji(.init(postId: query.postId)).spec let body = AddRealEmojiRequestDTO(realEmojiId: body.emojiId) - return request(spec: spec, jsonEncodable: body) + return request(spec: spec, headers: headers, jsonEncodable: body) .subscribe(on: Self.queue) .do { if let str = String(data: $0.1, encoding: .utf8) { @@ -88,7 +100,7 @@ extension RealEmojiAPIWorker { func removeRealEmoji(query: RemoveRealEmojiQuery) -> Single { let spec = RealEmojiAPIS.removeRealEmoji(.init(postId: query.postId, realEmojiId: query.realEmojiId)).spec - return request(spec: spec) + return request(spec: spec, headers: headers) .subscribe(on: Self.queue) .do { if let str = String(data: $0.1, encoding: .utf8) { diff --git a/14th-team5-iOS/Data/Sources/APIs/RealEmoji/Repository/RealEmojiRepository.swift b/14th-team5-iOS/Data/Sources/APIs/RealEmoji/Repository/RealEmojiRepository.swift index fcc5bedfd..0f51839dd 100644 --- a/14th-team5-iOS/Data/Sources/APIs/RealEmoji/Repository/RealEmojiRepository.swift +++ b/14th-team5-iOS/Data/Sources/APIs/RealEmoji/Repository/RealEmojiRepository.swift @@ -19,7 +19,7 @@ public final class RealEmojiRepository: RealEmojiRepositoryProtocol { public init () { } public func fetchMyRealEmoji() -> RxSwift.Single<[Domain.MyRealEmoji?]> { - realEmojiAPIWorker.loadMyRealEmoji() + realEmojiAPIWorker.fetchMyRealEmoji() } public func addRealEmoji(query: Domain.AddEmojiQuery, body: Domain.AddEmojiBody) -> RxSwift.Single { @@ -33,6 +33,4 @@ public final class RealEmojiRepository: RealEmojiRepositoryProtocol { public func removeRealEmoji(query: Domain.RemoveRealEmojiQuery) -> RxSwift.Single { realEmojiAPIWorker.removeRealEmoji(query: query) } - - } diff --git a/14th-team5-iOS/Domain/Sources/Emoji/UseCases/EmojiUseCase.swift b/14th-team5-iOS/Domain/Sources/Emoji/UseCases/ReactionUseCase.swift similarity index 61% rename from 14th-team5-iOS/Domain/Sources/Emoji/UseCases/EmojiUseCase.swift rename to 14th-team5-iOS/Domain/Sources/Emoji/UseCases/ReactionUseCase.swift index d2a78e8e1..0b22c91bc 100644 --- a/14th-team5-iOS/Domain/Sources/Emoji/UseCases/EmojiUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/Emoji/UseCases/ReactionUseCase.swift @@ -13,7 +13,7 @@ protocol EmojiProtocol { } -public protocol EmojiUseCaseProtocol { +public protocol ReactionUseCaseProtocol { /// Add Reactions func executeAddEmoji(query: AddEmojiQuery, body: AddEmojiBody) -> Single /// Remove Reactions @@ -22,22 +22,22 @@ public protocol EmojiUseCaseProtocol { func execute(query: FetchEmojiQuery) -> Single<[FetchedEmojiData]?> } -public class EmojiUseCase: EmojiUseCaseProtocol { - private let emojiRepository: ReactionRepositoryProtocol +public class ReactionUseCase: ReactionUseCaseProtocol { + private let reactionRepository: ReactionRepositoryProtocol - public init(emojiRepository: ReactionRepositoryProtocol) { - self.emojiRepository = emojiRepository + public init(reactionRepository: ReactionRepositoryProtocol) { + self.reactionRepository = reactionRepository } public func executeAddEmoji(query: AddEmojiQuery, body: AddEmojiBody) -> Single { - return emojiRepository.addReaction(query: query, body: body) + return reactionRepository.addReaction(query: query, body: body) } public func excuteRemoveEmoji(query: RemoveEmojiQuery, body: RemoveEmojiBody) -> Single { - return emojiRepository.removeEmoji(query: query, body: body) + return reactionRepository.removeReaction(query: query, body: body) } public func execute(query: FetchEmojiQuery) -> Single<[FetchedEmojiData]?> { - return emojiRepository.fetchReaction(query: query) + return reactionRepository.fetchReaction(query: query) } } From 735a7446c3586ba3cd13ef4f8895290f8d07c678 Mon Sep 17 00:00:00 2001 From: Kyoungmi <1mmgm.dev@gmail.com> Date: Wed, 5 Jun 2024 18:31:50 +0900 Subject: [PATCH 099/263] feat: add widget usecase(#543) --- .../Sources/FamilyWidget/Family.swift | 18 ++-- .../Sources/FamilyWidget/FamilyService.swift | 96 +++++++++---------- .../Sources/FamilyWidget/FamilyWidget.swift | 2 +- .../FamilyWidgetDIContainer.swift | 27 ++++++ .../FamilyWidget/FamilyWidgetEntry.swift | 3 +- .../FamilyWidgetTimelineProvider.swift | 13 ++- .../FamilyWidget/FamilyWidgetView.swift | 3 +- .../Widget/Repository/WidgetRepository.swift | 33 +++++++ .../RecentFamilyPostRequestDTO.swift | 12 +++ .../RecentFamilyPostResponseDTO.swift | 26 +++++ .../Widget/WidgetAPI/WidgetAPIWorker.swift | 56 +++++++++++ .../APIs/Widget/WidgetAPI/WidgetAPIs.swift | 22 +++++ .../Entities/RecentFamilyPostData.swift | 29 ++++++ .../Interfaces/WidgetRepositoryProtocol.swift | 12 +++ .../FetchRecentFamilyPostUseCase.swift | 24 +++++ .../ModuleType+Templates.swift | 4 +- 16 files changed, 317 insertions(+), 63 deletions(-) create mode 100644 14th-team5-iOS/App/WidgetExtension/Sources/FamilyWidget/FamilyWidgetDIContainer.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/Widget/Repository/WidgetRepository.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/Widget/WidgetAPI/DataMaaping/RecentFamilyPostRequestDTO.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/Widget/WidgetAPI/DataMaaping/RecentFamilyPostResponseDTO.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/Widget/WidgetAPI/WidgetAPIWorker.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/Widget/WidgetAPI/WidgetAPIs.swift create mode 100644 14th-team5-iOS/Domain/Sources/Widget/Entities/RecentFamilyPostData.swift create mode 100644 14th-team5-iOS/Domain/Sources/Widget/Interfaces/WidgetRepositoryProtocol.swift create mode 100644 14th-team5-iOS/Domain/Sources/Widget/UseCases/FetchRecentFamilyPostUseCase.swift diff --git a/14th-team5-iOS/App/WidgetExtension/Sources/FamilyWidget/Family.swift b/14th-team5-iOS/App/WidgetExtension/Sources/FamilyWidget/Family.swift index 02f2da36f..46017ce95 100644 --- a/14th-team5-iOS/App/WidgetExtension/Sources/FamilyWidget/Family.swift +++ b/14th-team5-iOS/App/WidgetExtension/Sources/FamilyWidget/Family.swift @@ -5,12 +5,12 @@ // Created by geonhui Yu on 12/11/23. // -import Foundation - -struct Family: Codable { - var authorName: String - var authorProfileImageUrl: String? - var postId: String? - var postImageUrl: String? - var postContent: String? -} +//import Foundation +// +//struct Family: Codable { +// var authorName: String +// var authorProfileImageUrl: String? +// var postId: String? +// var postImageUrl: String? +// var postContent: String? +//} diff --git a/14th-team5-iOS/App/WidgetExtension/Sources/FamilyWidget/FamilyService.swift b/14th-team5-iOS/App/WidgetExtension/Sources/FamilyWidget/FamilyService.swift index 3ed948e07..f27395ce0 100644 --- a/14th-team5-iOS/App/WidgetExtension/Sources/FamilyWidget/FamilyService.swift +++ b/14th-team5-iOS/App/WidgetExtension/Sources/FamilyWidget/FamilyService.swift @@ -4,51 +4,51 @@ // // Created by geonhui Yu on 12/11/23. // - -import Foundation -import UIKit -import Core - -struct FamilyService { - func fetchInfo(completion: @escaping (Result) -> Void) { - - let token = App.Repository.token.keychain.string(forKey: "accessToken") - let appKey = "7c5aaa36-570e-491f-b18a-26a1a0b72959" -#if PRD - var hostApi: String = "https://api.no5ing.kr/v1" -#else - var hostApi: String = "https://dev.api.no5ing.kr/v1" -#endif - let urlString = "\(hostApi)/widgets/single-recent-family-post" - - var request = URLRequest(url: URL(string: urlString)!) - request.addValue("application/json", forHTTPHeaderField: "accept") - request.addValue(token ?? "", forHTTPHeaderField: "X-AUTH-TOKEN") - request.addValue(appKey, forHTTPHeaderField: "X-APP-KEY") - request.httpMethod = "GET" - - let task = URLSession.shared.dataTask(with: request) { (data, response, error) in - if let error = error { - completion(.failure(error)) - return - } - - guard let data = data else { - let noDataError = NSError(domain: "NoDataError", code: 0, userInfo: nil) - completion(.failure(noDataError)) - return - } - - do { - let decoder = JSONDecoder() - let family = try decoder.decode(Family.self, from: data) - completion(.success(family)) - } catch { - completion(.failure(error)) - } - } - task.resume() - } -} - - +// +//import Foundation +//import UIKit +//import Core +// +//struct FamilyService { +// func fetchInfo(completion: @escaping (Result) -> Void) { +// +// let token = App.Repository.token.keychain.string(forKey: "accessToken") +// let appKey = "7c5aaa36-570e-491f-b18a-26a1a0b72959" +//#if PRD +// var hostApi: String = "https://api.no5ing.kr/v1" +//#else +// var hostApi: String = "https://dev.api.no5ing.kr/v1" +//#endif +// let urlString = "\(hostApi)/widgets/single-recent-family-post" +// +// var request = URLRequest(url: URL(string: urlString)!) +// request.addValue("application/json", forHTTPHeaderField: "accept") +// request.addValue(token ?? "", forHTTPHeaderField: "X-AUTH-TOKEN") +// request.addValue(appKey, forHTTPHeaderField: "X-APP-KEY") +// request.httpMethod = "GET" +// +// let task = URLSession.shared.dataTask(with: request) { (data, response, error) in +// if let error = error { +// completion(.failure(error)) +// return +// } +// +// guard let data = data else { +// let noDataError = NSError(domain: "NoDataError", code: 0, userInfo: nil) +// completion(.failure(noDataError)) +// return +// } +// +// do { +// let decoder = JSONDecoder() +// let family = try decoder.decode(Family.self, from: data) +// completion(.success(family)) +// } catch { +// completion(.failure(error)) +// } +// } +// task.resume() +// } +//} +// +// diff --git a/14th-team5-iOS/App/WidgetExtension/Sources/FamilyWidget/FamilyWidget.swift b/14th-team5-iOS/App/WidgetExtension/Sources/FamilyWidget/FamilyWidget.swift index 18e92d4ba..e533d948c 100644 --- a/14th-team5-iOS/App/WidgetExtension/Sources/FamilyWidget/FamilyWidget.swift +++ b/14th-team5-iOS/App/WidgetExtension/Sources/FamilyWidget/FamilyWidget.swift @@ -14,7 +14,7 @@ struct FamilyWidget: Widget { let kind: String = "FamilyWidget" var body: some WidgetConfiguration { - StaticConfiguration(kind: kind, provider: FamilyWidgetTimelineProvider()) { entry in + StaticConfiguration(kind: kind, provider: FamilyWidgetDIContainer().makeProvider()) { entry in if #available(iOSApplicationExtension 17.0, *) { FamilyWidgetView(entry: entry) .containerBackground(for: .widget) {} diff --git a/14th-team5-iOS/App/WidgetExtension/Sources/FamilyWidget/FamilyWidgetDIContainer.swift b/14th-team5-iOS/App/WidgetExtension/Sources/FamilyWidget/FamilyWidgetDIContainer.swift new file mode 100644 index 000000000..2f01e9fa5 --- /dev/null +++ b/14th-team5-iOS/App/WidgetExtension/Sources/FamilyWidget/FamilyWidgetDIContainer.swift @@ -0,0 +1,27 @@ +// +// FamilyWidgetDIContainer.swift +// WidgetExtension +// +// Created by 마경미 on 05.06.24. +// + +import Foundation + +import Core +import Data +import Domain + +final class FamilyWidgetDIContainer { + + func makeRepository() -> WidgetRepositoryProtocol { + return WidgetRepository() + } + + func makeUseCase() -> FetchRecentFamilyPostUseCaseProtocol { + return FetchRecentFamilyPostUseCase(widgetRepository: makeRepository()) + } + + func makeProvider() -> FamilyWidgetTimelineProvider { + return .init(repository: makeRepository()) + } +} diff --git a/14th-team5-iOS/App/WidgetExtension/Sources/FamilyWidget/FamilyWidgetEntry.swift b/14th-team5-iOS/App/WidgetExtension/Sources/FamilyWidget/FamilyWidgetEntry.swift index 5927c39a4..fdc2dbb54 100644 --- a/14th-team5-iOS/App/WidgetExtension/Sources/FamilyWidget/FamilyWidgetEntry.swift +++ b/14th-team5-iOS/App/WidgetExtension/Sources/FamilyWidget/FamilyWidgetEntry.swift @@ -6,8 +6,9 @@ // import WidgetKit +import Domain struct FamilyWidgetEntry: TimelineEntry { let date: Date - let family: Family? + let family: RecentFamilyPostData? } diff --git a/14th-team5-iOS/App/WidgetExtension/Sources/FamilyWidget/FamilyWidgetTimelineProvider.swift b/14th-team5-iOS/App/WidgetExtension/Sources/FamilyWidget/FamilyWidgetTimelineProvider.swift index 685d719b3..824ca5bba 100644 --- a/14th-team5-iOS/App/WidgetExtension/Sources/FamilyWidget/FamilyWidgetTimelineProvider.swift +++ b/14th-team5-iOS/App/WidgetExtension/Sources/FamilyWidget/FamilyWidgetTimelineProvider.swift @@ -8,9 +8,17 @@ import WidgetKit import UIKit -struct FamilyWidgetTimelineProvider: TimelineProvider { +import Domain + +final class FamilyWidgetTimelineProvider: TimelineProvider { typealias Entry = FamilyWidgetEntry + let repository: WidgetRepositoryProtocol + + init(repository: WidgetRepositoryProtocol) { + self.repository = repository + } + func placeholder(in context: Context) -> FamilyWidgetEntry { return FamilyWidgetEntry(date: Date(), family: nil) } @@ -21,7 +29,8 @@ struct FamilyWidgetTimelineProvider: TimelineProvider { let currentDate = Date() let refreshDate = Calendar.current.date(byAdding: .minute, value: 5, to: currentDate)! - FamilyService().fetchInfo { result in + + repository.fetchRecentFamilyPost { result in var entry: FamilyWidgetEntry switch result { case .success(let family): diff --git a/14th-team5-iOS/App/WidgetExtension/Sources/FamilyWidget/FamilyWidgetView.swift b/14th-team5-iOS/App/WidgetExtension/Sources/FamilyWidget/FamilyWidgetView.swift index 8fda109d0..6e0292a29 100644 --- a/14th-team5-iOS/App/WidgetExtension/Sources/FamilyWidget/FamilyWidgetView.swift +++ b/14th-team5-iOS/App/WidgetExtension/Sources/FamilyWidget/FamilyWidgetView.swift @@ -9,6 +9,7 @@ import SwiftUI import DesignSystem import WidgetKit import Core +import Domain struct FamilyWidgetView: View { @Environment(\.widgetFamily) var family: WidgetFamily @@ -152,7 +153,7 @@ struct FamilyWidgetView: View { } // MARK: 가족중 일부가 사진을 올렸을 때 뷰 - private func getPhotoView(info: Family) -> some View { + private func getPhotoView(info: RecentFamilyPostData) -> some View { ZStack { if let postImageUrl = info.postImageUrl { diff --git a/14th-team5-iOS/Data/Sources/APIs/Widget/Repository/WidgetRepository.swift b/14th-team5-iOS/Data/Sources/APIs/Widget/Repository/WidgetRepository.swift new file mode 100644 index 000000000..84d4bb126 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/Widget/Repository/WidgetRepository.swift @@ -0,0 +1,33 @@ +// +// WidgetRepository.swift +// Data +// +// Created by 마경미 on 05.06.24. +// + +import Foundation + +import Domain + +import RxSwift + +public final class WidgetRepository: WidgetRepositoryProtocol { + + public let disposeBag: DisposeBag = DisposeBag() + private let widgetAPIWorker: WidgetAPIWorker = WidgetAPIWorker() + + public init () { } + + public func fetchRecentFamilyPost(completion: @escaping (Result) -> Void) { + widgetAPIWorker.fetchRecentFamilyPost() + .subscribe( + onNext: { result in + completion(.success(result)) + }, + onError: { error in + completion(.failure(error)) + } + ) + .disposed(by: disposeBag) + } +} diff --git a/14th-team5-iOS/Data/Sources/APIs/Widget/WidgetAPI/DataMaaping/RecentFamilyPostRequestDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Widget/WidgetAPI/DataMaaping/RecentFamilyPostRequestDTO.swift new file mode 100644 index 000000000..dba032785 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/Widget/WidgetAPI/DataMaaping/RecentFamilyPostRequestDTO.swift @@ -0,0 +1,12 @@ +// +// RecentFamilyPostRequestDTO.swift +// Data +// +// Created by 마경미 on 05.06.24. +// + +import Foundation + +struct RecentFamilyPostParameter: Codable { + let date: String = Date().toFormatString(with: "yyyy-MM-dd") +} diff --git a/14th-team5-iOS/Data/Sources/APIs/Widget/WidgetAPI/DataMaaping/RecentFamilyPostResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Widget/WidgetAPI/DataMaaping/RecentFamilyPostResponseDTO.swift new file mode 100644 index 000000000..70ddf29e9 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/Widget/WidgetAPI/DataMaaping/RecentFamilyPostResponseDTO.swift @@ -0,0 +1,26 @@ +// +// RecentFamilyPostResponseDTO.swift +// Data +// +// Created by 마경미 on 05.06.24. +// + +import Foundation + +import Domain + +struct RecentFamilyPostResponseDTO: Codable { + let authorName: String + let authorProfileImageUrl: String? + let postId: String? + let postImageUrl: String? + let postContent: String? + + func toDomain() -> RecentFamilyPostData { + return .init(authorName: authorName, + authorProfileImageUrl: authorProfileImageUrl, + postId: postId, + postImageUrl: postImageUrl, + postContent: postContent) + } +} diff --git a/14th-team5-iOS/Data/Sources/APIs/Widget/WidgetAPI/WidgetAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/Widget/WidgetAPI/WidgetAPIWorker.swift new file mode 100644 index 000000000..f60ca358c --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/Widget/WidgetAPI/WidgetAPIWorker.swift @@ -0,0 +1,56 @@ +// +// WidgetAPIWorker.swift +// Data +// +// Created by 마경미 on 05.06.24. +// + +import Foundation + +import Core +import Domain + +import RxSwift + +typealias WidgetAPIWorker = WidgetAPIs.Worker +extension WidgetAPIs { + public final class Worker: APIWorker { + static let queue = { + ConcurrentDispatchQueueScheduler(queue: DispatchQueue(label: "WidgetAPIQueue", qos: .utility)) + }() + + public override init() { + super.init() + self.id = "WidgetAPIWorker" + } + + var headers: [APIHeader] { + guard let token = App.Repository.token.keychain.string(forKey: "accessToken") else { + return [] + } + + let headers = [BibbiAPI.Header.xAppKey, BibbiAPI.Header.xAuthToken(token), BibbiAPI.Header.acceptJson] + return headers + } + } +} + +extension WidgetAPIWorker { + func fetchRecentFamilyPost() -> Observable { + + let spec = WidgetAPIs.fetchRecentFamilyPost.spec + let parameters = RecentFamilyPostParameter() + + return request(spec: spec, headers: headers, parameters: parameters) + .subscribe(on: Self.queue) + .do { + if let str = String(data: $0.1, encoding: .utf8) { + debugPrint("Fetch Recent Family Post Result: \(str)") + } + } + .map(RecentFamilyPostResponseDTO.self) + .catchAndReturn(nil) + .map { $0?.toDomain() } + .asObservable() + } +} diff --git a/14th-team5-iOS/Data/Sources/APIs/Widget/WidgetAPI/WidgetAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/Widget/WidgetAPI/WidgetAPIs.swift new file mode 100644 index 000000000..ae13870c9 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/Widget/WidgetAPI/WidgetAPIs.swift @@ -0,0 +1,22 @@ +// +// WidgetAPIs.swift +// Data +// +// Created by 마경미 on 05.06.24. +// + +import Foundation + +import Core + +enum WidgetAPIs: API { + case fetchRecentFamilyPost + + public var spec: APISpec { + switch self { + case .fetchRecentFamilyPost: + let urlString = "\(BibbiAPI.hostApi)/widgets/single-recent-family-post" + return APISpec(method: .get, url: urlString) + } + } +} diff --git a/14th-team5-iOS/Domain/Sources/Widget/Entities/RecentFamilyPostData.swift b/14th-team5-iOS/Domain/Sources/Widget/Entities/RecentFamilyPostData.swift new file mode 100644 index 000000000..468e2185b --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Widget/Entities/RecentFamilyPostData.swift @@ -0,0 +1,29 @@ +// +// RecentFamilyPostData.swift +// Domain +// +// Created by 마경미 on 05.06.24. +// + +import Foundation + +public struct RecentFamilyPostData: Codable { + public var authorName: String + public var authorProfileImageUrl: String? + public var postId: String? + public var postImageUrl: String? + public var postContent: String? + + public init( + authorName: String, + authorProfileImageUrl: String? = nil, + postId: String? = nil, + postImageUrl: String? = nil, + postContent: String? = nil) { + self.authorName = authorName + self.authorProfileImageUrl = authorProfileImageUrl + self.postId = postId + self.postImageUrl = postImageUrl + self.postContent = postContent + } +} diff --git a/14th-team5-iOS/Domain/Sources/Widget/Interfaces/WidgetRepositoryProtocol.swift b/14th-team5-iOS/Domain/Sources/Widget/Interfaces/WidgetRepositoryProtocol.swift new file mode 100644 index 000000000..b35e063f3 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Widget/Interfaces/WidgetRepositoryProtocol.swift @@ -0,0 +1,12 @@ +// +// WidgetRepositoryProtocol.swift +// Domain +// +// Created by 마경미 on 05.06.24. +// + +import Foundation + +public protocol WidgetRepositoryProtocol { + func fetchRecentFamilyPost(completion: @escaping (Result) -> Void) +} diff --git a/14th-team5-iOS/Domain/Sources/Widget/UseCases/FetchRecentFamilyPostUseCase.swift b/14th-team5-iOS/Domain/Sources/Widget/UseCases/FetchRecentFamilyPostUseCase.swift new file mode 100644 index 000000000..3536804a1 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Widget/UseCases/FetchRecentFamilyPostUseCase.swift @@ -0,0 +1,24 @@ +// +// FetchRecentFamilyPostUseCase.swift +// Domain +// +// Created by 마경미 on 05.06.24. +// + +import Foundation + +public protocol FetchRecentFamilyPostUseCaseProtocol { + func excute(completion: @escaping (Result) -> Void) +} + +public class FetchRecentFamilyPostUseCase: FetchRecentFamilyPostUseCaseProtocol { + private let widgetRepository: WidgetRepositoryProtocol + + public init(widgetRepository: WidgetRepositoryProtocol) { + self.widgetRepository = widgetRepository + } + + public func excute(completion: @escaping (Result) -> Void) { + return widgetRepository.fetchRecentFamilyPost(completion: completion) + } +} diff --git a/Tuist/ProjectDescriptionHelpers/ModuleType+Templates.swift b/Tuist/ProjectDescriptionHelpers/ModuleType+Templates.swift index 092db5d58..5fbf6ebe0 100644 --- a/Tuist/ProjectDescriptionHelpers/ModuleType+Templates.swift +++ b/Tuist/ProjectDescriptionHelpers/ModuleType+Templates.swift @@ -18,7 +18,9 @@ public enum ExtensionsLayer: String, ModuleType { switch self { case .Widget: return [ - .with(.Core) + .with(.Core), + .with(.Domain), + .with(.Data) ] } } From 4edce5c64bda1f18a1d53ca0527f77513111d004 Mon Sep 17 00:00:00 2001 From: Kyoungmi <1mmgm.dev@gmail.com> Date: Wed, 5 Jun 2024 18:33:06 +0900 Subject: [PATCH 100/263] refactor: realEMoji rename(#543) --- .../RealEmoji/RealEmojiAPI/RealEmojiAPIWorker.swift | 12 ++++++------ .../{RealEmojiAPIS.swift => RealEmojiAPIs.swift} | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) rename 14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/{RealEmojiAPIS.swift => RealEmojiAPIs.swift} (97%) diff --git a/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/RealEmojiAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/RealEmojiAPIWorker.swift index bfa3111db..c949f36ce 100644 --- a/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/RealEmojiAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/RealEmojiAPIWorker.swift @@ -13,8 +13,8 @@ import Domain import Alamofire import RxSwift -typealias RealEmojiAPIWorker = RealEmojiAPIS.Worker -extension RealEmojiAPIS { +typealias RealEmojiAPIWorker = RealEmojiAPIs.Worker +extension RealEmojiAPIs { public final class Worker: APIWorker { static let queue = { ConcurrentDispatchQueueScheduler(queue: DispatchQueue(label: "RealEmojiAPIQueue", qos: .utility)) @@ -43,7 +43,7 @@ extension RealEmojiAPIWorker { func fetchRealEmoji(query: FetchRealEmojiQuery) -> Single<[FetchedEmojiData]?> { let query = FetchRealEmojiListParameter(postId: query.postId) - let spec = RealEmojiAPIS.fetchRealEmojiList(query).spec + let spec = RealEmojiAPIs.fetchRealEmojiList(query).spec return request(spec: spec, headers: headers) .subscribe(on: Self.queue) @@ -61,7 +61,7 @@ extension RealEmojiAPIWorker { } func fetchMyRealEmoji() -> Single<[MyRealEmoji?]> { - let spec = RealEmojiAPIS.fetchMyRealEmoji.spec + let spec = RealEmojiAPIs.fetchMyRealEmoji.spec return request(spec: spec, headers: headers) .subscribe(on: Self.queue) @@ -79,7 +79,7 @@ extension RealEmojiAPIWorker { } func addRealEmoji(query: AddEmojiQuery, body: AddEmojiBody) -> Single { - let spec = RealEmojiAPIS.addRealEmoji(.init(postId: query.postId)).spec + let spec = RealEmojiAPIs.addRealEmoji(.init(postId: query.postId)).spec let body = AddRealEmojiRequestDTO(realEmojiId: body.emojiId) return request(spec: spec, headers: headers, jsonEncodable: body) @@ -98,7 +98,7 @@ extension RealEmojiAPIWorker { } func removeRealEmoji(query: RemoveRealEmojiQuery) -> Single { - let spec = RealEmojiAPIS.removeRealEmoji(.init(postId: query.postId, realEmojiId: query.realEmojiId)).spec + let spec = RealEmojiAPIs.removeRealEmoji(.init(postId: query.postId, realEmojiId: query.realEmojiId)).spec return request(spec: spec, headers: headers) .subscribe(on: Self.queue) diff --git a/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/RealEmojiAPIS.swift b/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/RealEmojiAPIs.swift similarity index 97% rename from 14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/RealEmojiAPIS.swift rename to 14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/RealEmojiAPIs.swift index 120b5dce3..f7af176af 100644 --- a/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/RealEmojiAPIS.swift +++ b/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/RealEmojiAPIs.swift @@ -8,7 +8,7 @@ import Core import Foundation -enum RealEmojiAPIS: API { +enum RealEmojiAPIs: API { case fetchRealEmojiList(FetchRealEmojiListParameter) case fetchMyRealEmoji case addRealEmoji(AddRealEmojiParameters) From 97c942511b981b78686df61c51e935269dfb336a Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Fri, 14 Jun 2024 15:09:19 +0900 Subject: [PATCH 101/263] =?UTF-8?q?feat:=20BaseContainer,=20BaseWrapper=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EA=B5=AC=ED=98=84=20(#538)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../App/Sources/Application/AppDelegate.swift | 3 +- .../DIContainer/AppDIContainer.swift | 24 +++++++++ .../DIContainer/CalendarDIContainer.swift | 45 +++++----------- .../Application/DIContainer/DIContainer.swift | 53 ------------------- .../DIContainer/Wrapper/BaseWrapper.swift | 21 -------- ...MonthlyCalendarViewControllerWrapper.swift | 47 ---------------- .../CalendarCellDIContainer.swift | 0 .../CalendarImageCellDIContainer.swift | 0 .../CalendarPostCellDIContainer.swift | 0 .../CalendarPostDIContainer.swift | 0 .../MonthlyCalendarDIConatainer.swift | 6 +-- ...eactor.swift => CalendarViewReactor.swift} | 16 ++---- .../MonthlyCalendarViewController.swift | 8 +-- .../CalendarViewControllerWrapper.swift | 32 +++++++++++ .../Core/Sources/Base/BaseContainer.swift | 22 ++++++++ .../Core/Sources/Base/BaseDIContainer.swift | 2 +- .../Core/Sources/Base/BaseWrapper.swift | 23 ++++++++ .../Repository/CalendarRepository.swift | 8 +-- 18 files changed, 129 insertions(+), 181 deletions(-) create mode 100644 14th-team5-iOS/App/Sources/Application/DIContainer/AppDIContainer.swift delete mode 100644 14th-team5-iOS/App/Sources/Application/DIContainer/DIContainer.swift delete mode 100644 14th-team5-iOS/App/Sources/Application/DIContainer/Wrapper/BaseWrapper.swift delete mode 100644 14th-team5-iOS/App/Sources/Application/DIContainer/Wrapper/Calendar/MonthlyCalendarViewControllerWrapper.swift rename 14th-team5-iOS/App/Sources/Presentation/Calendar/{Dependency => DIContainer}/CalendarCellDIContainer.swift (100%) rename 14th-team5-iOS/App/Sources/Presentation/Calendar/{Dependency => DIContainer}/CalendarImageCellDIContainer.swift (100%) rename 14th-team5-iOS/App/Sources/Presentation/Calendar/{Dependency => DIContainer}/CalendarPostCellDIContainer.swift (100%) rename 14th-team5-iOS/App/Sources/Presentation/Calendar/{Dependency => DIContainer}/CalendarPostDIContainer.swift (100%) rename 14th-team5-iOS/App/Sources/Presentation/Calendar/{Dependency => DIContainer}/MonthlyCalendarDIConatainer.swift (85%) rename 14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/{MonthlyCalendarViewReactor.swift => CalendarViewReactor.swift} (89%) create mode 100644 14th-team5-iOS/App/Sources/Presentation/Calendar/Wrapper/CalendarViewControllerWrapper.swift create mode 100644 14th-team5-iOS/Core/Sources/Base/BaseContainer.swift create mode 100644 14th-team5-iOS/Core/Sources/Base/BaseWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/AppDelegate.swift b/14th-team5-iOS/App/Sources/Application/AppDelegate.swift index e1b422c7e..b66095b53 100644 --- a/14th-team5-iOS/App/Sources/Application/AppDelegate.swift +++ b/14th-team5-iOS/App/Sources/Application/AppDelegate.swift @@ -52,7 +52,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { App.indicator.bind() // TODO: - Data 지우기 - let containers: [DIContainer] = [ + let containers: [BaseContainer] = [ + AppDIContainer(), CalendarDIContainer() ] containers.forEach { diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/AppDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/AppDIContainer.swift new file mode 100644 index 000000000..2b0fe1f5b --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/AppDIContainer.swift @@ -0,0 +1,24 @@ +// +// DIContainer.swift +// App +// +// Created by 김건우 on 6/4/24. +// + +import Core +import Data +import Foundation + +final class AppDIContainer: BaseContainer { + + // MARK: - Register + func registerDependencies() { + + // ServiceProvider 등록 + container.register(type: GlobalStateProviderProtocol.self) { _ in + return GlobalStateProvider() + } + + } + +} diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/CalendarDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/CalendarDIContainer.swift index 915510338..27e899eec 100644 --- a/14th-team5-iOS/App/Sources/Application/DIContainer/CalendarDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/CalendarDIContainer.swift @@ -9,12 +9,7 @@ import Core import Data import Domain - -// NOTE: -// - APIWorker를 나눈대로 DIContainer도 분리합니다. -// - CalendarDIContainer는 유지보수 용이를 위한 컨테어너의 역할만 수행합니다. - -final class CalendarDIContainer: DIContainer { +final class CalendarDIContainer: BaseContainer { // MARK: - Make UseCase @@ -42,11 +37,9 @@ final class CalendarDIContainer: DIContainer { ) } - // NOTE: - 추후 UseCase 리팩토링하면 make() 메서드가 더 많아지겠죠? - - - func makeUseCase() -> CalendarUseCaseProtocol { - return CalendarUseCase( + // Deprecated + func makeOldCalendarUseCase() -> CalendarUseCaseProtocol { + CalendarUseCase( calendarRepository: makeCalendarRepository() ) } @@ -56,45 +49,33 @@ final class CalendarDIContainer: DIContainer { // MARK: - Make Repository func makeCalendarRepository() -> CalendarRepositoryProtocol { - return CalendarRepository( - // calendarApiWorker: ... - keychain: Storage.Keychain.token, - userDefaults: Storage.UserDefaults.member - ) - - // NOTE: - 궁극적인 목표는 DIContainer 안에서 APIWorker, Storage 등 모든 의존성을 주입하게 만들어 유지보수를 용이하게 하는 겁니다. + return CalendarRepository() } // MARK: - Register - override func registerDependencies() { - super.registerDependencies() - - // NOTE: - 등록하면 @Injected로 편하게 의존성을 받아올 수 있습니다. - // - (MonthlyCalendarViewReactor.swift 참조) - - container.register(type: FetchCalendarBannerUseCaseProtocol.self) { [unowned self] _ in + func registerDependencies() { + container.register(type: FetchCalendarBannerUseCaseProtocol.self) { _ in self.makeFetchCalendarBannerUseCase() } - container.register(type: FetchStatisticsSummaryUseCaseProtocol.self) { [unowned self] _ in + container.register(type: FetchStatisticsSummaryUseCaseProtocol.self) { _ in self.makeFetchStatisticsSummaryUseCase() } - container.register(type: FetchDailyCalendarUseCaseProtocol.self) { [unowned self] _ in + container.register(type: FetchDailyCalendarUseCaseProtocol.self) {_ in self.makeFetchDailyCalendarUseCase() } - container.register(type: FetchMonthlyCalendarUseCaseProtocol.self) { [unowned self] _ in + container.register(type: FetchMonthlyCalendarUseCaseProtocol.self) { _ in self.makeFetchMonthlyCalendarUseCase() } - // ... - - container.register(type: CalendarUseCaseProtocol.self) { [unowned self] _ in - self.makeUseCase() + // Deprecated + container.register(type: CalendarUseCaseProtocol.self) { _ in + self.makeOldCalendarUseCase() } } diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/DIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/DIContainer.swift deleted file mode 100644 index 80dd64d20..000000000 --- a/14th-team5-iOS/App/Sources/Application/DIContainer/DIContainer.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// DIContainer.swift -// App -// -// Created by 김건우 on 6/4/24. -// - -import Core -import Data -import Foundation - -class DIContainer { - - // MARK: - Properties - var container: Container = { - Container.standard - }() - - // MARK: - API Workers - enum API { - static let calendar = CalendarAPIWorker() - } - - // MARK: - Persistent Storage - enum Storage { - enum Keychain { - static let token = TokenKeychain() - } - - enum UserDefaults { - static let member = MemberUserDefaults() - } - - enum InMemory { - // NOTE: - 사실 CleanArchitecture 잘 준수만 하면 인-메모리 DB는 사용할 일이 딱히 없어 보여요. - // 곧바로 Repository에서 Keychain이나 UserDefaults에 접근하면 되니까요. - } - } - - // MARK: - Register - func registerDependencies() { - - // ServiceProvider 등록 - container.register(type: GlobalStateProviderProtocol.self) { _ in - return GlobalStateProvider() - } - - // NOTE: - 앞으로 @Injected var provider: ServiceProviderProtocol처럼 작성해야 합니다. - // 직접 Service를 주입하면 항목(Item) 전달이 제대로 되지 않아요. - - } - -} diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/Wrapper/BaseWrapper.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/Wrapper/BaseWrapper.swift deleted file mode 100644 index 29479daed..000000000 --- a/14th-team5-iOS/App/Sources/Application/DIContainer/Wrapper/BaseWrapper.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// BaseWrapper.swift -// App -// -// Created by 김건우 on 6/4/24. -// - -import Core -import UIKit - -import ReactorKit - -// NOTE: - Bibbi-Package에 이미 포함되어 있어 삭제할 코드 - -protocol BaseWrapper { - - associatedtype R: Reactor - associatedtype V: ReactorKit.View - - func makeReactor() -> R -} diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/Wrapper/Calendar/MonthlyCalendarViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/Wrapper/Calendar/MonthlyCalendarViewControllerWrapper.swift deleted file mode 100644 index 87f5ef7a4..000000000 --- a/14th-team5-iOS/App/Sources/Application/DIContainer/Wrapper/Calendar/MonthlyCalendarViewControllerWrapper.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// MonthlyCalendarViewControllerWrapper.swift -// App -// -// Created by 김건우 on 6/4/24. -// - -import Foundation - - -// MARK: - Wrapper - -final class MonthlyCalendarViewControllerWrapper { - - // NOTE: - 기존 DIContainer의 역할은 Wrapper가 대신하게 됩니다. - // - 파일 위치는 의논해봐야 합니다만, 한 부류의 화면으로만 이동하는 게 아니니 전역에 위치하는 게 좋아보입니다. - // - @Wrapper 매크로가 도입되면 코드가 많이 단축됩니다. - - func makeReactor() -> R { - return MonthlyCalendarViewReactor() - } - - - // Begin expansion of "@Wrapper" - - typealias R = MonthlyCalendarViewReactor - typealias V = MonthlyCalendarViewController - - func makeViewController() -> V { - return MonthlyCalendarViewController(reactor: makeReactor()) - } - - var viewController: V { - makeViewController() - } - - var reactor: R { - makeReactor() - } - - // End expansion of "@Wrapper" - -} - -// Begin expansion of "@Wrapper" -extension MonthlyCalendarViewControllerWrapper: BaseWrapper { } -// End expansion of "@Wrapper" diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/CalendarCellDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/DIContainer/CalendarCellDIContainer.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/CalendarCellDIContainer.swift rename to 14th-team5-iOS/App/Sources/Presentation/Calendar/DIContainer/CalendarCellDIContainer.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/CalendarImageCellDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/DIContainer/CalendarImageCellDIContainer.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/CalendarImageCellDIContainer.swift rename to 14th-team5-iOS/App/Sources/Presentation/Calendar/DIContainer/CalendarImageCellDIContainer.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/CalendarPostCellDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/DIContainer/CalendarPostCellDIContainer.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/CalendarPostCellDIContainer.swift rename to 14th-team5-iOS/App/Sources/Presentation/Calendar/DIContainer/CalendarPostCellDIContainer.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/CalendarPostDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/DIContainer/CalendarPostDIContainer.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/CalendarPostDIContainer.swift rename to 14th-team5-iOS/App/Sources/Presentation/Calendar/DIContainer/CalendarPostDIContainer.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/MonthlyCalendarDIConatainer.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/DIContainer/MonthlyCalendarDIConatainer.swift similarity index 85% rename from 14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/MonthlyCalendarDIConatainer.swift rename to 14th-team5-iOS/App/Sources/Presentation/Calendar/DIContainer/MonthlyCalendarDIConatainer.swift index 6d7a37161..1525605a8 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/Dependency/MonthlyCalendarDIConatainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/DIContainer/MonthlyCalendarDIConatainer.swift @@ -33,9 +33,9 @@ public final class MonthlyCalendarDIConatainer { return CalendarRepository() } - public func makeReactor() -> MonthlyCalendarViewReactor { - return MonthlyCalendarViewReactor( -// calendarUseCase: makeCalendarUseCase(), + public func makeReactor() -> CalendarViewReactor { + return CalendarViewReactor( +// calendarUseCase: makeCalendarUseCase() // provider: globalState ) } diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/MonthlyCalendarViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarViewReactor.swift similarity index 89% rename from 14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/MonthlyCalendarViewReactor.swift rename to 14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarViewReactor.swift index 4e3274d5b..83b987998 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/MonthlyCalendarViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarViewReactor.swift @@ -13,7 +13,7 @@ import Domain import ReactorKit import RxSwift -public final class MonthlyCalendarViewReactor: Reactor { +public final class CalendarViewReactor: Reactor { // MARK: - Action public enum Action { case popViewController @@ -36,27 +36,19 @@ public final class MonthlyCalendarViewReactor: Reactor { @Pulse var displayCalendar: [MonthlyCalendarSectionModel] } - @Injected var provider: GlobalStateProviderProtocol - @Injected var calendarUseCase: CalendarUseCaseProtocol // MARK: - Properties public var initialState: State -// public let provider: GlobalStateProviderProtocol -// private let calendarUseCase: CalendarUseCaseProtocol + @Injected var provider: GlobalStateProviderProtocol + @Injected var calendarUseCase: CalendarUseCaseProtocol // MARK: - Intializer - init( -// calendarUseCase: CalendarUseCaseProtocol, -// provider: GlobalStateProviderProtocol - ) { + init() { self.initialState = State( shouldPopViewController: false, displayCalendar: [.init(model: (), items: [])] ) - -// self.calendarUseCase = calendarUseCase -// self.provider = provider } // MARK: - Transform diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/MonthlyCalendarViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/MonthlyCalendarViewController.swift index f87b35029..cb079484f 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/MonthlyCalendarViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/MonthlyCalendarViewController.swift @@ -17,7 +17,7 @@ import SnapKit import Then fileprivate typealias _Str = CalendarStrings -public final class MonthlyCalendarViewController: BaseViewController { +public final class MonthlyCalendarViewController: BaseViewController { // MARK: - Views private lazy var calendarCollectionView: UICollectionView = UICollectionView( frame: .zero, @@ -33,13 +33,13 @@ public final class MonthlyCalendarViewController: BaseViewController.just(()) .delay(RxConst.milliseconds100Interval, scheduler: RxSchedulers.main) .bind(with: self) { owner, _ in @@ -73,7 +73,7 @@ public final class MonthlyCalendarViewController: BaseViewController V { + return MonthlyCalendarViewController(reactor: makeReactor()) + } + + func makeReactor() -> R { + return CalendarViewReactor() + } + + var viewController: V { + makeViewController() + } + + var reactor: R { + makeReactor() + } + +} diff --git a/14th-team5-iOS/Core/Sources/Base/BaseContainer.swift b/14th-team5-iOS/Core/Sources/Base/BaseContainer.swift new file mode 100644 index 000000000..8464c5c74 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Base/BaseContainer.swift @@ -0,0 +1,22 @@ +// +// BaseContainer.swift +// Core +// +// Created by 김건우 on 6/14/24. +// + +import Foundation + +public protocol BaseContainer { + + func registerDependencies() + +} + +public extension BaseContainer { + + var container: Container { + Container.standard + } + +} diff --git a/14th-team5-iOS/Core/Sources/Base/BaseDIContainer.swift b/14th-team5-iOS/Core/Sources/Base/BaseDIContainer.swift index 0b5bd1f1b..b947e84e6 100644 --- a/14th-team5-iOS/Core/Sources/Base/BaseDIContainer.swift +++ b/14th-team5-iOS/Core/Sources/Base/BaseDIContainer.swift @@ -7,7 +7,7 @@ import Foundation - +@available(*, deprecated, renamed: "BaseContainer") public protocol BaseDIContainer { associatedtype ViewContrller diff --git a/14th-team5-iOS/Core/Sources/Base/BaseWrapper.swift b/14th-team5-iOS/Core/Sources/Base/BaseWrapper.swift new file mode 100644 index 000000000..4fefbd592 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Base/BaseWrapper.swift @@ -0,0 +1,23 @@ +// +// BaseWrapper.swift +// Core +// +// Created by 김건우 on 6/14/24. +// + +import UIKit + +import ReactorKit + +public protocol BaseWrapper { + + associatedtype R: Reactor + associatedtype V: ReactorKit.View + + func makeReactor() -> R + func makeViewController() -> V + + var reactor: R { get } + var viewController: V { get } + +} diff --git a/14th-team5-iOS/Data/Sources/APIs/Calendar/Repository/CalendarRepository.swift b/14th-team5-iOS/Data/Sources/APIs/Calendar/Repository/CalendarRepository.swift index 16c1c8504..276571f96 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Calendar/Repository/CalendarRepository.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Calendar/Repository/CalendarRepository.swift @@ -22,13 +22,7 @@ public final class CalendarRepository: CalendarRepositoryProtocol { // MARK: - Persistent Storage // MARK: - Intializer - public init( - keychain: KeychainType? = nil, - userDefaults: UserDefaultsType? = nil, - inMemory: InMemoryType? = nil - ) { - - } + public init() { } } // MARK: - Extensions From be18b484f953f92fe6c8e76ddad17152ca38ebc3 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Fri, 14 Jun 2024 15:24:46 +0900 Subject: [PATCH 102/263] =?UTF-8?q?feat:=20=EC=BD=94=EB=93=9C=20=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EB=B0=98=EC=98=81=20(#555)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/APIs/SignIn/Repository/SignInRepository.swift | 5 ++--- .../SignIn/SignInAPI/Helpers/Apple/AppleSignIn+Rx.swift | 7 ++++--- .../SignIn/SignInAPI/Helpers/Apple/AppleSignInHelper.swift | 2 +- .../SignIn/SignInAPI/Helpers/Kakao/KakaoSignInHelper.swift | 2 +- .../APIs/SignIn/SignInAPI/Helpers/SignInHelperType.swift | 3 +-- .../Sources/APIs/SignIn/SignInAPI/SignInAPIWorker.swift | 3 +-- .../SignIn/Interfaces/Repository/SignInRepository.swift | 2 +- 7 files changed, 11 insertions(+), 13 deletions(-) diff --git a/14th-team5-iOS/Data/Sources/APIs/SignIn/Repository/SignInRepository.swift b/14th-team5-iOS/Data/Sources/APIs/SignIn/Repository/SignInRepository.swift index 57adf3607..0a823ecee 100644 --- a/14th-team5-iOS/Data/Sources/APIs/SignIn/Repository/SignInRepository.swift +++ b/14th-team5-iOS/Data/Sources/APIs/SignIn/Repository/SignInRepository.swift @@ -7,7 +7,6 @@ import Core import Domain -import UIKit import RxSwift @@ -29,7 +28,7 @@ extension SignInRepository { public func signIn( with type: SignInType, - on window: UIWindow? + on window: AnyObject? ) -> Single { signInApiWorker.signIn(with: type, on: window) .observe(on: RxSchedulers.main) @@ -45,7 +44,7 @@ extension SignInRepository { public func signOut() -> Completable { guard let type = tokenKeychainStorage.loadSignInType() - else { return .error(NSError()) } // TODO: - Error 타입 정의하기 + else { return .error(RxError.unknown) } // TODO: - Error 타입 정의하기 return signInApiWorker.signOut(with: type) .observe(on: RxSchedulers.main) diff --git a/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/Apple/AppleSignIn+Rx.swift b/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/Apple/AppleSignIn+Rx.swift index c81e751d9..c884e906c 100644 --- a/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/Apple/AppleSignIn+Rx.swift +++ b/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/Apple/AppleSignIn+Rx.swift @@ -7,9 +7,10 @@ import AuthenticationServices import Domain +import UIKit + import RxCocoa import RxSwift -import UIKit final class RxSIWAAuthorizationControllerDelegateProxy: DelegateProxy, DelegateProxyType, ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding { @@ -75,7 +76,7 @@ extension Reactive where Base: ASAuthorizationAppleIDProvider { public func signIn( scope: [ASAuthorization.Scope]? = nil, - on window: UIWindow? + on window: AnyObject? ) -> Single { let request = base.createRequest() request.requestedScopes = scope @@ -83,7 +84,7 @@ extension Reactive where Base: ASAuthorizationAppleIDProvider { let controller = ASAuthorizationController(authorizationRequests: [request]) let proxy = RxSIWAAuthorizationControllerDelegateProxy(controller: controller) - proxy.window = window! + proxy.window = window as! UIWindow // 형변환 실패 시 앱 죽이기 controller.presentationContextProvider = proxy controller.performRequests() diff --git a/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/Apple/AppleSignInHelper.swift b/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/Apple/AppleSignInHelper.swift index 47a416293..9f7c54168 100644 --- a/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/Apple/AppleSignInHelper.swift +++ b/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/Apple/AppleSignInHelper.swift @@ -13,7 +13,7 @@ import RxSwift public final class AppleSignInHelper: SignInHelperType { - public func signIn(on window: UIWindow?) -> Single { + public func signIn(on window: AnyObject?) -> Single { ASAuthorizationAppleIDProvider().rx.signIn(on: window) } diff --git a/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/Kakao/KakaoSignInHelper.swift b/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/Kakao/KakaoSignInHelper.swift index 3a322f588..76c629577 100644 --- a/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/Kakao/KakaoSignInHelper.swift +++ b/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/Kakao/KakaoSignInHelper.swift @@ -20,7 +20,7 @@ public final class KakaoSignInHelper: SignInHelperType { // MARK: - Sign In - public func signIn(on window: UIWindow?) -> Single { + public func signIn(on window: AnyObject?) -> Single { if UserApi.isKakaoTalkLoginAvailable() { return signInWithKakaoTalk() } else { diff --git a/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/SignInHelperType.swift b/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/SignInHelperType.swift index 582cce0d7..5e8c5d982 100644 --- a/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/SignInHelperType.swift +++ b/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/Helpers/SignInHelperType.swift @@ -5,12 +5,11 @@ // Created by 김건우 on 6/7/24. // -import UIKit import Domain import RxSwift protocol SignInHelperType { - func signIn(on window: UIWindow?) -> Single + func signIn(on window: AnyObject?) -> Single func signOut() -> Completable } diff --git a/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/SignInAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/SignInAPIWorker.swift index c15fc228e..0f684e84b 100644 --- a/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/SignInAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/SignIn/SignInAPI/SignInAPIWorker.swift @@ -6,7 +6,6 @@ // import Domain -import UIKit import RxSwift @@ -28,7 +27,7 @@ extension SignInAPIWorker { public func signIn( with type: SignInType, - on window: UIWindow? + on window: AnyObject? ) -> Single { return getHelper(type).signIn(on: window) } diff --git a/14th-team5-iOS/Domain/Sources/SignIn/Interfaces/Repository/SignInRepository.swift b/14th-team5-iOS/Domain/Sources/SignIn/Interfaces/Repository/SignInRepository.swift index 9f89b0a13..183c15779 100644 --- a/14th-team5-iOS/Domain/Sources/SignIn/Interfaces/Repository/SignInRepository.swift +++ b/14th-team5-iOS/Domain/Sources/SignIn/Interfaces/Repository/SignInRepository.swift @@ -10,6 +10,6 @@ import UIKit import RxSwift public protocol SignInRepositoryProtocol { - func signIn(with type: SignInType, on window: UIWindow?) -> Single + func signIn(with type: SignInType, on window: AnyObject?) -> Single func signOut() -> Completable } From ae331f0b20bcdc1b710681a36b7f073c9e36ccbf Mon Sep 17 00:00:00 2001 From: Kyoungmi <1mmgm.dev@gmail.com> Date: Wed, 12 Jun 2024 22:59:07 +0900 Subject: [PATCH 103/263] [refactor]: mainView usecase(#560) --- .../Home/Dependency/MainViewDIContainer.swift | 4 +- .../Home/Reactor/MainViewReactor.swift | 10 +-- .../DataMapping/MainNightResponseDTO.swift | 2 +- .../DataMapping/MainResponseDTO.swift | 2 +- .../MainViewAPI/MainViewAPIWorker.swift | 4 +- .../Repository/MainViewRepository.swift | 6 +- .../MainView/MainViewEntity.swift} | 2 +- .../MainView/NightMainViewEntity.swift} | 2 +- .../Entities/PostList/PostEntity.swift | 42 +++++++++++++ .../Entities/PostList/PostListQuery.swift | 63 +++++++++++++++++++ .../Reaction/CreateReactionQuery.swift | 16 +++++ .../Reaction/CreateReactionRequest.swift | 17 +++++ .../Reaction/FetchReactionQuery.swift | 16 +++++ .../Reaction/RemoveEmojiRequest.swift | 17 +++++ .../Reaction/RemoveReactionQuery.swift | 16 +++++ .../RealEmoji/FetchRealEmojiQuery.swift | 16 +++++ .../RealEmoji/MyRealEmojiEntity.swift | 22 +++++++ .../Entities/RealEmoji/RealEmojiEntity.swift | 31 +++++++++ .../RealEmoji/RemoveRealEmojiQuery.swift | 18 ++++++ .../Sources/Main/Entities/FamilySection.swift | 37 ----------- .../FetchMainNightUseCaseProtocol.swift | 14 ----- .../FetchMainUsecaseProtocol.swift | 14 ----- .../Repository/MainRepositoryProtocol.swift | 15 ----- .../MainViewRepositoryProtocol.swift | 15 +++++ .../PostListRepositoryProtocol.swift | 13 ++++ .../ReactionRepositoryProtocol.swift | 16 +++++ .../RealEmojiRepositoryProtocol.swift | 17 +++++ .../MainView}/FetchMainUseCase.swift | 6 +- .../MainView/FetchNightMainViewUsecase.swift} | 11 ++-- .../UseCases/Post/FetchPostListUseCase.swift | 25 ++++++++ .../Reaction/CreateReactionUseCase.swift | 26 ++++++++ .../Reaction/FetchReactionListUseCase.swift | 26 ++++++++ .../Reaction/RemoveReactionUseCase.swift | 26 ++++++++ .../RealEmoji/CreateRealEmojiUseCase.swift | 27 ++++++++ .../RealEmoji/FetchMyRealEmojiUseCase.swift | 27 ++++++++ .../RealEmoji/FetchRealEmojiListUseCase.swift | 27 ++++++++ .../UseCases/RealEmoji/RealEmojiUseCase.swift | 45 +++++++++++++ .../RealEmoji/RemoveRealEmojiUseCase.swift | 27 ++++++++ 38 files changed, 619 insertions(+), 101 deletions(-) rename 14th-team5-iOS/Domain/Sources/{Main/Entities/MainData.swift => Entities/MainView/MainViewEntity.swift} (98%) rename 14th-team5-iOS/Domain/Sources/{Main/Entities/MainNightData.swift => Entities/MainView/NightMainViewEntity.swift} (98%) create mode 100644 14th-team5-iOS/Domain/Sources/Entities/PostList/PostEntity.swift create mode 100644 14th-team5-iOS/Domain/Sources/Entities/PostList/PostListQuery.swift create mode 100644 14th-team5-iOS/Domain/Sources/Entities/Reaction/CreateReactionQuery.swift create mode 100644 14th-team5-iOS/Domain/Sources/Entities/Reaction/CreateReactionRequest.swift create mode 100644 14th-team5-iOS/Domain/Sources/Entities/Reaction/FetchReactionQuery.swift create mode 100644 14th-team5-iOS/Domain/Sources/Entities/Reaction/RemoveEmojiRequest.swift create mode 100644 14th-team5-iOS/Domain/Sources/Entities/Reaction/RemoveReactionQuery.swift create mode 100644 14th-team5-iOS/Domain/Sources/Entities/RealEmoji/FetchRealEmojiQuery.swift create mode 100644 14th-team5-iOS/Domain/Sources/Entities/RealEmoji/MyRealEmojiEntity.swift create mode 100644 14th-team5-iOS/Domain/Sources/Entities/RealEmoji/RealEmojiEntity.swift create mode 100644 14th-team5-iOS/Domain/Sources/Entities/RealEmoji/RemoveRealEmojiQuery.swift delete mode 100644 14th-team5-iOS/Domain/Sources/Main/Entities/FamilySection.swift delete mode 100644 14th-team5-iOS/Domain/Sources/Main/FetchMainUseCase/FetchMainNightUseCaseProtocol.swift delete mode 100644 14th-team5-iOS/Domain/Sources/Main/FetchMainUseCase/FetchMainUsecaseProtocol.swift delete mode 100644 14th-team5-iOS/Domain/Sources/Main/Repository/MainRepositoryProtocol.swift create mode 100644 14th-team5-iOS/Domain/Sources/Repositories/MainViewRepositoryProtocol.swift create mode 100644 14th-team5-iOS/Domain/Sources/Repositories/PostListRepositoryProtocol.swift create mode 100644 14th-team5-iOS/Domain/Sources/Repositories/ReactionRepositoryProtocol.swift create mode 100644 14th-team5-iOS/Domain/Sources/Repositories/RealEmojiRepositoryProtocol.swift rename 14th-team5-iOS/Domain/Sources/{Main/FetchMainUseCase => UseCases/MainView}/FetchMainUseCase.swift (72%) rename 14th-team5-iOS/Domain/Sources/{Main/FetchMainUseCase/FetchMainNightUseCase.swift => UseCases/MainView/FetchNightMainViewUsecase.swift} (52%) create mode 100644 14th-team5-iOS/Domain/Sources/UseCases/Post/FetchPostListUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/UseCases/Reaction/CreateReactionUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/UseCases/Reaction/FetchReactionListUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/UseCases/Reaction/RemoveReactionUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/UseCases/RealEmoji/CreateRealEmojiUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/UseCases/RealEmoji/FetchMyRealEmojiUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/UseCases/RealEmoji/FetchRealEmojiListUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/UseCases/RealEmoji/RealEmojiUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/UseCases/RealEmoji/RemoveRealEmojiUseCase.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Dependency/MainViewDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Dependency/MainViewDIContainer.swift index 3c26cf9b8..067af9f3a 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Dependency/MainViewDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Dependency/MainViewDIContainer.swift @@ -57,8 +57,8 @@ extension MainViewDIContainer { return FetchMainUseCase(mainRepository: makeMainRepository()) } - private func makeFetchMainNightUseCase() -> FetchMainNightUseCaseProtocol { - return FetchMainNightUseCase(mainRepository: makeMainRepository()) + private func makeFetchMainNightUseCase() -> FetchNightMainViewUseCaseProtocol { + return FetchNightMainViewUseCase(mainRepository: makeMainRepository()) } private func makeCheckMissionAlertShowUseCase() -> CheckMissionAlertShowUseCaseProtocol { diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift index 04728cf55..3b4f25cb5 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift @@ -46,8 +46,8 @@ final class MainViewReactor: Reactor { } enum Mutation { - case updateMainData(MainData) - case updateMainNight(MainNightData) + case updateMainData(MainViewEntity) + case updateMainNight(NightMainViewEntity) case setInTime(Bool) case setPageIndex(Int) @@ -91,13 +91,13 @@ final class MainViewReactor: Reactor { let initialState: State = State() private let fetchMainUseCase: FetchMainUseCaseProtocol - private let fetchMainNightUseCase: FetchMainNightUseCaseProtocol + private let fetchMainNightUseCase: FetchNightMainViewUseCaseProtocol private let pickUseCase: PickUseCaseProtocol private let checkMissionAlertShowUseCase: CheckMissionAlertShowUseCaseProtocol private let provider: GlobalStateProviderProtocol init(fetchMainUseCase: FetchMainUseCaseProtocol, - fetchMainNightUseCase: FetchMainNightUseCaseProtocol, + fetchMainNightUseCase: FetchNightMainViewUseCaseProtocol, pickUseCase: PickUseCaseProtocol, checkMissionAlertShowUseCase: CheckMissionAlertShowUseCaseProtocol, provider: GlobalStateProviderProtocol) { @@ -291,7 +291,7 @@ extension MainViewReactor { } extension MainViewReactor { - private func updateMainData(_ state: State, _ data: MainData) -> State { + private func updateMainData(_ state: State, _ data: MainViewEntity) -> State { var newState = state newState.isMissionUnlocked = data.isMissionUnlocked newState.isMeSurvivalUploadedToday = data.isMeSurvivalUploadedToday diff --git a/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/DataMapping/MainNightResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/DataMapping/MainNightResponseDTO.swift index 118241787..543277117 100644 --- a/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/DataMapping/MainNightResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/DataMapping/MainNightResponseDTO.swift @@ -40,7 +40,7 @@ struct MainNightResponseDTO: Codable { let topBarElements: [TopBarElement] let familyMemberMonthlyRanking: FamilyMemberMonthlyRanking - func toDomain() -> MainNightData { + func toDomain() -> NightMainViewEntity { return .init(mainFamilyProfileDatas: topBarElements.map { $0.toDomain() }, familyRankData: familyMemberMonthlyRanking.toDomain()) } diff --git a/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/DataMapping/MainResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/DataMapping/MainResponseDTO.swift index 656c9f8ed..d2d8af5a4 100644 --- a/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/DataMapping/MainResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/DataMapping/MainResponseDTO.swift @@ -66,7 +66,7 @@ struct MainResponseDTO: Codable { let missionFeeds: [MissionFeed] let pickers: [Picker] - func toDomain() -> MainData { + func toDomain() -> MainViewEntity { return .init( mainFamilyProfileDatas: topBarElements.map { $0.toDomain() }, leftUploadCountUntilMissionUnlock: leftUploadCountUntilMissionUnlock, diff --git a/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/MainViewAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/MainViewAPIWorker.swift index 1a31f844b..d9d71cfac 100644 --- a/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/MainViewAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/MainViewAPIWorker.swift @@ -39,7 +39,7 @@ extension MainViewAPIs { } extension MainAPIWorker { - func fetchMain() -> Single { + func fetchMain() -> Single { let spec = MainViewAPIs.fetchMain.spec return request(spec: spec, headers: headers) .subscribe(on: Self.queue) @@ -54,7 +54,7 @@ extension MainAPIWorker { .asSingle() } - func fetchMainNight() -> Single { + func fetchMainNight() -> Single { let spec = MainViewAPIs.fetchMainNight.spec return request(spec: spec, headers: headers) .subscribe(on: Self.queue) diff --git a/14th-team5-iOS/Data/Sources/APIs/MainView/Repository/MainViewRepository.swift b/14th-team5-iOS/Data/Sources/APIs/MainView/Repository/MainViewRepository.swift index 0f99f17dc..9ef56c19a 100644 --- a/14th-team5-iOS/Data/Sources/APIs/MainView/Repository/MainViewRepository.swift +++ b/14th-team5-iOS/Data/Sources/APIs/MainView/Repository/MainViewRepository.swift @@ -11,7 +11,7 @@ import Domain import RxSwift -public final class MainViewRepository: MainRepositoryProtocol { +public final class MainViewRepository: MainViewRepositoryProtocol { public let disposeBag: DisposeBag = DisposeBag() private let mainApiWorker: MainAPIWorker = MainAPIWorker() @@ -20,13 +20,13 @@ public final class MainViewRepository: MainRepositoryProtocol { } extension MainViewRepository { - public func fetchMain() -> Observable { + public func fetchMain() -> Observable { return mainApiWorker.fetchMain() .asObservable() } - public func fetchMainNight() -> Observable { + public func fetchMainNight() -> Observable { return mainApiWorker.fetchMainNight() .asObservable() } diff --git a/14th-team5-iOS/Domain/Sources/Main/Entities/MainData.swift b/14th-team5-iOS/Domain/Sources/Entities/MainView/MainViewEntity.swift similarity index 98% rename from 14th-team5-iOS/Domain/Sources/Main/Entities/MainData.swift rename to 14th-team5-iOS/Domain/Sources/Entities/MainView/MainViewEntity.swift index dba16bd42..a1cf67294 100644 --- a/14th-team5-iOS/Domain/Sources/Main/Entities/MainData.swift +++ b/14th-team5-iOS/Domain/Sources/Entities/MainView/MainViewEntity.swift @@ -17,7 +17,7 @@ public struct Picker { } } -public struct MainData { +public struct MainViewEntity { public let mainFamilyProfileDatas: [ProfileData] public let leftUploadCountUntilMissionUnlock: Int public let isFamilySurvivalUploadedToday: Bool diff --git a/14th-team5-iOS/Domain/Sources/Main/Entities/MainNightData.swift b/14th-team5-iOS/Domain/Sources/Entities/MainView/NightMainViewEntity.swift similarity index 98% rename from 14th-team5-iOS/Domain/Sources/Main/Entities/MainNightData.swift rename to 14th-team5-iOS/Domain/Sources/Entities/MainView/NightMainViewEntity.swift index 7c5cff92d..aaf90050d 100644 --- a/14th-team5-iOS/Domain/Sources/Main/Entities/MainNightData.swift +++ b/14th-team5-iOS/Domain/Sources/Entities/MainView/NightMainViewEntity.swift @@ -77,7 +77,7 @@ extension FamilyRankData { } } -public struct MainNightData { +public struct NightMainViewEntity { public let mainFamilyProfileDatas: [ProfileData] public let familyRankData: FamilyRankData diff --git a/14th-team5-iOS/Domain/Sources/Entities/PostList/PostEntity.swift b/14th-team5-iOS/Domain/Sources/Entities/PostList/PostEntity.swift new file mode 100644 index 000000000..43ef1bd09 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Entities/PostList/PostEntity.swift @@ -0,0 +1,42 @@ +// +// DailyPostData.swift +// Domain +// +// Created by 마경미 on 25.12.23. +// + +import Foundation + +public struct PostEntity: Equatable, Hashable { + public let postId: String + public let author: ProfileData? + public var commentCount: Int + public let missionId: String? + public let missionType: String? + public let emojiCount: Int + public let imageURL: String + public let content: String? + public let time: String + + public init(postId: String, missionId: String? = nil, missionType: String? = nil, author: ProfileData?, commentCount: Int, emojiCount: Int, imageURL: String, content: String?, time: String) { + self.postId = postId + self.missionId = missionId + self.missionType = missionType + self.author = author + self.commentCount = commentCount + self.emojiCount = emojiCount + self.imageURL = imageURL + self.content = content + self.time = time + } +} + +public struct PostListPage: Equatable { + public let isLast: Bool + public let postLists: [PostEntity] + + public init(isLast: Bool, postLists: [PostEntity]) { + self.isLast = isLast + self.postLists = postLists + } +} diff --git a/14th-team5-iOS/Domain/Sources/Entities/PostList/PostListQuery.swift b/14th-team5-iOS/Domain/Sources/Entities/PostList/PostListQuery.swift new file mode 100644 index 000000000..1196057e7 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Entities/PostList/PostListQuery.swift @@ -0,0 +1,63 @@ +// +// PostListQuery.swift +// Domain +// +// Created by 마경미 on 25.12.23. +// + +import Foundation + +public enum Sort: String { + case asc = "ASC" + case desc = "DESC" +} + +public enum PostType: String { + case survival = "SURVIVAL" + case mission = "MISSION" + + public func getIndex() -> Int { + switch self { + case .survival: + return 0 + case .mission: + return 1 + } + } + + public static func getPostType(index: Int) -> PostType { + switch index { + case 0: + return .survival + case 1: + return .mission + default: + fatalError("index Out of range") + } + } +} + +public struct PostListQuery { + public var page: Int + public let size: Int + public let date: String + public let memberId: String? + public let type: PostType + public let sort: String + + public init( + page: Int = 1, + size: Int = 256, + date: String, + memberId: String? = nil, + type: PostType = .survival, + sort: Sort = .desc + ) { + self.page = page + self.size = size + self.date = date + self.memberId = memberId + self.type = type + self.sort = sort.rawValue + } +} diff --git a/14th-team5-iOS/Domain/Sources/Entities/Reaction/CreateReactionQuery.swift b/14th-team5-iOS/Domain/Sources/Entities/Reaction/CreateReactionQuery.swift new file mode 100644 index 000000000..a586e91c2 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Entities/Reaction/CreateReactionQuery.swift @@ -0,0 +1,16 @@ +// +// AddReactionQuery.swift +// Domain +// +// Created by 마경미 on 11.06.24. +// + +import Foundation + +public struct CreateReactionQuery { + public let postId: String + + public init(postId: String) { + self.postId = postId + } +} diff --git a/14th-team5-iOS/Domain/Sources/Entities/Reaction/CreateReactionRequest.swift b/14th-team5-iOS/Domain/Sources/Entities/Reaction/CreateReactionRequest.swift new file mode 100644 index 000000000..b433b8e94 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Entities/Reaction/CreateReactionRequest.swift @@ -0,0 +1,17 @@ +// +// AddEmojiRequest.swift +// Domain +// +// Created by 마경미 on 01.01.24. +// + +import Foundation +import Core + +public struct CreateReactionRequest { + public let emojiId: String + + public init(content: String) { + self.emojiId = content + } +} diff --git a/14th-team5-iOS/Domain/Sources/Entities/Reaction/FetchReactionQuery.swift b/14th-team5-iOS/Domain/Sources/Entities/Reaction/FetchReactionQuery.swift new file mode 100644 index 000000000..b296a60da --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Entities/Reaction/FetchReactionQuery.swift @@ -0,0 +1,16 @@ +// +// FetchEmojiQuery.swift +// Domain +// +// Created by 마경미 on 03.01.24. +// + +import Foundation + +public struct FetchReactionQuery { + public let postId: String + + public init(postId: String) { + self.postId = postId + } +} diff --git a/14th-team5-iOS/Domain/Sources/Entities/Reaction/RemoveEmojiRequest.swift b/14th-team5-iOS/Domain/Sources/Entities/Reaction/RemoveEmojiRequest.swift new file mode 100644 index 000000000..9491b0745 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Entities/Reaction/RemoveEmojiRequest.swift @@ -0,0 +1,17 @@ +// +// RemoveEmojiRequest.swift +// Domain +// +// Created by 마경미 on 01.01.24. +// + +import Foundation +import Core + +public struct RemoveReactionRequest { + public let content: Emojis + + public init(content: Emojis) { + self.content = content + } +} diff --git a/14th-team5-iOS/Domain/Sources/Entities/Reaction/RemoveReactionQuery.swift b/14th-team5-iOS/Domain/Sources/Entities/Reaction/RemoveReactionQuery.swift new file mode 100644 index 000000000..da7a54243 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Entities/Reaction/RemoveReactionQuery.swift @@ -0,0 +1,16 @@ +// +// RemoveReactionQuery.swift +// Domain +// +// Created by 마경미 on 11.06.24. +// + +import Foundation + +public struct RemoveReactionQuery { + public let postId: String + + public init(postId: String) { + self.postId = postId + } +} diff --git a/14th-team5-iOS/Domain/Sources/Entities/RealEmoji/FetchRealEmojiQuery.swift b/14th-team5-iOS/Domain/Sources/Entities/RealEmoji/FetchRealEmojiQuery.swift new file mode 100644 index 000000000..e17de0720 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Entities/RealEmoji/FetchRealEmojiQuery.swift @@ -0,0 +1,16 @@ +// +// FetchRealEmojiQuery.swift +// Domain +// +// Created by 마경미 on 22.01.24. +// + +import Foundation + +public struct FetchRealEmojiQuery { + public let postId: String + + public init(postId: String) { + self.postId = postId + } +} diff --git a/14th-team5-iOS/Domain/Sources/Entities/RealEmoji/MyRealEmojiEntity.swift b/14th-team5-iOS/Domain/Sources/Entities/RealEmoji/MyRealEmojiEntity.swift new file mode 100644 index 000000000..490daa0e5 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Entities/RealEmoji/MyRealEmojiEntity.swift @@ -0,0 +1,22 @@ +// +// MyRealEmoji.swift +// Domain +// +// Created by 마경미 on 27.01.24. +// + +import Foundation + +import Core + +public struct MyRealEmojiEntity: Hashable { + public let realEmojiId: String + public let type: Emojis + public let imageUrl: String + + public init(realEmojiId: String, type: Emojis, imageUrl: String) { + self.realEmojiId = realEmojiId + self.type = type + self.imageUrl = imageUrl + } +} diff --git a/14th-team5-iOS/Domain/Sources/Entities/RealEmoji/RealEmojiEntity.swift b/14th-team5-iOS/Domain/Sources/Entities/RealEmoji/RealEmojiEntity.swift new file mode 100644 index 000000000..154dd8dd5 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Entities/RealEmoji/RealEmojiEntity.swift @@ -0,0 +1,31 @@ +// +// FetchRealEmojiData.swift +// Domain +// +// Created by 마경미 on 22.01.24. +// + +import Foundation +import Core + +public struct RealEmojiEntity { + public let isStandard: Bool + public var isSelfSelected: Bool + public let postEmojiId: String + public let emojiType: Emojis + public var count: Int + public let realEmojiId: String + public let realEmojiImageURL: String + public var memberIds: [String] + + public init(isStandard: Bool, isSelfSelected: Bool, postEmojiId: String, emojiType: Emojis, count: Int, realEmojiId: String, realEmojiImageURL: String, memberIds: [String]) { + self.isStandard = isStandard + self.isSelfSelected = isSelfSelected + self.postEmojiId = postEmojiId + self.emojiType = emojiType + self.count = count + self.realEmojiId = realEmojiId + self.realEmojiImageURL = realEmojiImageURL + self.memberIds = memberIds + } +} diff --git a/14th-team5-iOS/Domain/Sources/Entities/RealEmoji/RemoveRealEmojiQuery.swift b/14th-team5-iOS/Domain/Sources/Entities/RealEmoji/RemoveRealEmojiQuery.swift new file mode 100644 index 000000000..0c5566534 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Entities/RealEmoji/RemoveRealEmojiQuery.swift @@ -0,0 +1,18 @@ +// +// RemoveRealEmojiQuery.swift +// Domain +// +// Created by 마경미 on 29.01.24. +// + +import Foundation + +public struct RemoveRealEmojiQuery { + public let postId: String + public let realEmojiId: String + + public init(postId: String, realEmojiId: String) { + self.postId = postId + self.realEmojiId = realEmojiId + } +} diff --git a/14th-team5-iOS/Domain/Sources/Main/Entities/FamilySection.swift b/14th-team5-iOS/Domain/Sources/Main/Entities/FamilySection.swift deleted file mode 100644 index 1ddc6d1e5..000000000 --- a/14th-team5-iOS/Domain/Sources/Main/Entities/FamilySection.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// FamilySection.swift -// Domain -// -// Created by 마경미 on 29.04.24. -// - -import Foundation -import RxDataSources - -//public struct FamilySection { -// public typealias Model = SectionModel -// -// public enum Item { -// case main(ProfileData) -// } -//} -// -//extension FamilySection.Item: Equatable, Hashable { -// public static func == (lhs: FamilySection.Item, rhs: FamilySection.Item) -> Bool { -// switch (lhs, rhs) { -// case (.main(let leftFamily), .main(let rightFamily)): -// return leftFamily == rightFamily -// default: -// return false -// } -// } -// -// public func hash(into hasher: inout Hasher) { -// switch self { -// case .main(let family): -// hasher.combine(family) -// default: -// break -// } -// } -//} diff --git a/14th-team5-iOS/Domain/Sources/Main/FetchMainUseCase/FetchMainNightUseCaseProtocol.swift b/14th-team5-iOS/Domain/Sources/Main/FetchMainUseCase/FetchMainNightUseCaseProtocol.swift deleted file mode 100644 index 4af46a313..000000000 --- a/14th-team5-iOS/Domain/Sources/Main/FetchMainUseCase/FetchMainNightUseCaseProtocol.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// FetchMainNightUseCaseProtocol.swift -// Domain -// -// Created by 마경미 on 07.05.24. -// - -import Foundation - -import RxSwift - -public protocol FetchMainNightUseCaseProtocol { - func execute() -> Observable -} diff --git a/14th-team5-iOS/Domain/Sources/Main/FetchMainUseCase/FetchMainUsecaseProtocol.swift b/14th-team5-iOS/Domain/Sources/Main/FetchMainUseCase/FetchMainUsecaseProtocol.swift deleted file mode 100644 index 2ab4cb145..000000000 --- a/14th-team5-iOS/Domain/Sources/Main/FetchMainUseCase/FetchMainUsecaseProtocol.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// MainUseCaseProtocol.swift -// Domain -// -// Created by 마경미 on 20.04.24. -// - -import Foundation - -import RxSwift - -public protocol FetchMainUseCaseProtocol { - func execute() -> Observable -} diff --git a/14th-team5-iOS/Domain/Sources/Main/Repository/MainRepositoryProtocol.swift b/14th-team5-iOS/Domain/Sources/Main/Repository/MainRepositoryProtocol.swift deleted file mode 100644 index 8896215e9..000000000 --- a/14th-team5-iOS/Domain/Sources/Main/Repository/MainRepositoryProtocol.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// MainRepository.swift -// Domain -// -// Created by 마경미 on 20.04.24. -// - -import Foundation - -import RxSwift - -public protocol MainRepositoryProtocol { - func fetchMain() -> Observable - func fetchMainNight() -> Observable -} diff --git a/14th-team5-iOS/Domain/Sources/Repositories/MainViewRepositoryProtocol.swift b/14th-team5-iOS/Domain/Sources/Repositories/MainViewRepositoryProtocol.swift new file mode 100644 index 000000000..06f5a37bd --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Repositories/MainViewRepositoryProtocol.swift @@ -0,0 +1,15 @@ +// +// MainRepository.swift +// Domain +// +// Created by 마경미 on 20.04.24. +// + +import Foundation + +import RxSwift + +public protocol MainViewRepositoryProtocol { + func fetchMain() -> Observable + func fetchMainNight() -> Observable +} diff --git a/14th-team5-iOS/Domain/Sources/Repositories/PostListRepositoryProtocol.swift b/14th-team5-iOS/Domain/Sources/Repositories/PostListRepositoryProtocol.swift new file mode 100644 index 000000000..7de282731 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Repositories/PostListRepositoryProtocol.swift @@ -0,0 +1,13 @@ +// +// PostListRepository.swift +// Domain +// +// Created by 마경미 on 25.12.23. +// + +import Foundation +import RxSwift + +public protocol PostListRepositoryProtocol { + func fetchTodayPostList(query: PostListQuery) -> Single +} diff --git a/14th-team5-iOS/Domain/Sources/Repositories/ReactionRepositoryProtocol.swift b/14th-team5-iOS/Domain/Sources/Repositories/ReactionRepositoryProtocol.swift new file mode 100644 index 000000000..718260087 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Repositories/ReactionRepositoryProtocol.swift @@ -0,0 +1,16 @@ +// +// EmojiRepository.swift +// Domain +// +// Created by 마경미 on 01.01.24. +// + +import Foundation + +import RxSwift + +public protocol ReactionRepositoryProtocol { + func addReaction(query: CreateReactionQuery, body: CreateReactionRequest) -> Single + func removeReaction(query: RemoveReactionQuery, body: RemoveReactionRequest) -> Single + func fetchReaction(query: FetchReactionQuery) -> Single<[RealEmojiEntity]?> +} diff --git a/14th-team5-iOS/Domain/Sources/Repositories/RealEmojiRepositoryProtocol.swift b/14th-team5-iOS/Domain/Sources/Repositories/RealEmojiRepositoryProtocol.swift new file mode 100644 index 000000000..17bb12662 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Repositories/RealEmojiRepositoryProtocol.swift @@ -0,0 +1,17 @@ +// +// RealEmojiRepository.swift +// Domain +// +// Created by 마경미 on 22.01.24. +// + +import Foundation + +import RxSwift + +public protocol RealEmojiRepositoryProtocol { + func fetchMyRealEmoji() -> Single<[MyRealEmojiEntity?]> + func addRealEmoji(query: CreateReactionQuery, body: CreateReactionRequest) -> Single + func fetchRealEmoji(query: FetchRealEmojiQuery) -> Single<[RealEmojiEntity]?> + func removeRealEmoji(query: RemoveRealEmojiQuery) -> Single +} diff --git a/14th-team5-iOS/Domain/Sources/Main/FetchMainUseCase/FetchMainUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/MainView/FetchMainUseCase.swift similarity index 72% rename from 14th-team5-iOS/Domain/Sources/Main/FetchMainUseCase/FetchMainUseCase.swift rename to 14th-team5-iOS/Domain/Sources/UseCases/MainView/FetchMainUseCase.swift index f168cb599..5c6b1d398 100644 --- a/14th-team5-iOS/Domain/Sources/Main/FetchMainUseCase/FetchMainUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/MainView/FetchMainUseCase.swift @@ -9,6 +9,10 @@ import Foundation import RxSwift +public protocol FetchMainUseCaseProtocol { + func execute() -> Observable +} + public final class FetchMainUseCase: FetchMainUseCaseProtocol { private let mainRepository: MainRepositoryProtocol @@ -16,7 +20,7 @@ public final class FetchMainUseCase: FetchMainUseCaseProtocol { self.mainRepository = mainRepository } - public func execute() -> Observable { + public func execute() -> Observable { return mainRepository.fetchMain() } } diff --git a/14th-team5-iOS/Domain/Sources/Main/FetchMainUseCase/FetchMainNightUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/MainView/FetchNightMainViewUsecase.swift similarity index 52% rename from 14th-team5-iOS/Domain/Sources/Main/FetchMainUseCase/FetchMainNightUseCase.swift rename to 14th-team5-iOS/Domain/Sources/UseCases/MainView/FetchNightMainViewUsecase.swift index a5c51129b..d5751b982 100644 --- a/14th-team5-iOS/Domain/Sources/Main/FetchMainUseCase/FetchMainNightUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/MainView/FetchNightMainViewUsecase.swift @@ -1,5 +1,5 @@ // -// FetchMainNightUseCase.swift +// FetchMainNightUseCaseProtocol.swift // Domain // // Created by 마경미 on 07.05.24. @@ -9,15 +9,18 @@ import Foundation import RxSwift -public final class FetchMainNightUseCase: FetchMainNightUseCaseProtocol { +public protocol FetchNightMainViewUseCaseProtocol { + func execute() -> Observable +} + +public final class FetchNightMainViewUseCase: FetchNightMainViewUseCaseProtocol { private let mainRepository: MainRepositoryProtocol public init(mainRepository: MainRepositoryProtocol) { self.mainRepository = mainRepository } - public func execute() -> Observable { + public func execute() -> Observable { return mainRepository.fetchMainNight() } } - diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Post/FetchPostListUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Post/FetchPostListUseCase.swift new file mode 100644 index 000000000..fd42c85f0 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/UseCases/Post/FetchPostListUseCase.swift @@ -0,0 +1,25 @@ +// +// PostListUseCase.swift +// Domain +// +// Created by 마경미 on 25.12.23. +// + +import Foundation +import RxSwift + +public protocol FetchPostListUseCaseProtocol { + func excute(query: PostListQuery) -> Single +} + +public class FetchPostListUseCase: FetchPostListUseCaseProtocol { + private let postListRepository: PostListRepositoryProtocol + + public init(postListRepository: PostListRepositoryProtocol) { + self.postListRepository = postListRepository + } + + public func excute(query: PostListQuery) -> Single { + return postListRepository.fetchTodayPostList(query: query) + } +} diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Reaction/CreateReactionUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Reaction/CreateReactionUseCase.swift new file mode 100644 index 000000000..979c20772 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/UseCases/Reaction/CreateReactionUseCase.swift @@ -0,0 +1,26 @@ +// +// CreateReactionUseCase.swift +// Domain +// +// Created by 마경미 on 12.06.24. +// + +import Foundation + +import RxSwift + +public protocol CreateReactionUseCaseProtocol { + func execute(query: CreateReactionQuery, body: CreateReactionRequest) -> Single +} + +public final class CreateReactionUseCase: CreateReactionUseCaseProtocol { + private let reactionRepository: ReactionRepositoryProtocol + + public init(reactionRepository: ReactionRepositoryProtocol) { + self.reactionRepository = reactionRepository + } + + public func execute(query: CreateReactionQuery, body: CreateReactionRequest) -> Single { + return reactionRepository.addReaction(query: query, body: body) + } +} diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Reaction/FetchReactionListUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Reaction/FetchReactionListUseCase.swift new file mode 100644 index 000000000..0d4bbb683 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/UseCases/Reaction/FetchReactionListUseCase.swift @@ -0,0 +1,26 @@ +// +// FetchReactionListUseCase.swift +// Domain +// +// Created by 마경미 on 12.06.24. +// + +import Foundation + +import RxSwift + +public protocol FetchReactionListUseCaseProtocol { + func execute(query: FetchReactionQuery) -> Single<[RealEmojiEntity]?> +} + +public final class FetchReactionListUseCase: FetchReactionListUseCaseProtocol { + private let reactionRepository: ReactionRepositoryProtocol + + public init(reactionRepository: ReactionRepositoryProtocol) { + self.reactionRepository = reactionRepository + } + + public func execute(query: FetchReactionQuery) -> Single<[RealEmojiEntity]?> { + return reactionRepository.fetchReaction(query: query) + } +} diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Reaction/RemoveReactionUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Reaction/RemoveReactionUseCase.swift new file mode 100644 index 000000000..a2b78cb86 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/UseCases/Reaction/RemoveReactionUseCase.swift @@ -0,0 +1,26 @@ +// +// RemoveReactionUseCase.swift +// Domain +// +// Created by 마경미 on 12.06.24. +// + +import Foundation + +import RxSwift + +public protocol RemoveReactionUseCaseProtocol { + func execute(query: RemoveReactionQuery, body: RemoveReactionRequest) -> Single +} + +public final class RemoveReactionUseCase: RemoveReactionUseCaseProtocol { + private let reactionRepository: ReactionRepositoryProtocol + + public init(reactionRepository: ReactionRepositoryProtocol) { + self.reactionRepository = reactionRepository + } + + public func execute(query: RemoveReactionQuery, body: RemoveReactionRequest) -> Single { + return reactionRepository.removeReaction(query: query, body: body) + } +} diff --git a/14th-team5-iOS/Domain/Sources/UseCases/RealEmoji/CreateRealEmojiUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/RealEmoji/CreateRealEmojiUseCase.swift new file mode 100644 index 000000000..344aff3c4 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/UseCases/RealEmoji/CreateRealEmojiUseCase.swift @@ -0,0 +1,27 @@ +// +// CreateRealEmojiUseCase.swift +// Domain +// +// Created by 마경미 on 12.06.24. +// + +import Foundation + +import RxSwift + +public protocol CreateRealEmojiUseCaseProtocol { + func execute(query: CreateReactionQuery, body: CreateReactionRequest) -> Single +} + +public class CreateRealEmojiUseCase: CreateRealEmojiUseCaseProtocol { + + private let realEmojiRepository: RealEmojiRepositoryProtocol + + public init(realEmojiRepository: RealEmojiRepositoryProtocol) { + self.realEmojiRepository = realEmojiRepository + } + + public func execute(query: CreateReactionQuery, body: CreateReactionRequest) -> Single { + return realEmojiRepository.addRealEmoji(query: query, body: body) + } +} diff --git a/14th-team5-iOS/Domain/Sources/UseCases/RealEmoji/FetchMyRealEmojiUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/RealEmoji/FetchMyRealEmojiUseCase.swift new file mode 100644 index 000000000..18e962ddd --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/UseCases/RealEmoji/FetchMyRealEmojiUseCase.swift @@ -0,0 +1,27 @@ +// +// FetchMyRealEmojiUseCase.swift +// Domain +// +// Created by 마경미 on 12.06.24. +// + +import Foundation + +import RxSwift + +public protocol FetchMyRealEmojiUseCaseProtocol { + func execute() -> Single<[MyRealEmojiEntity?]> +} + +public class FetchMyRealEmojiUseCase: FetchMyRealEmojiUseCaseProtocol { + + private let realEmojiRepository: RealEmojiRepositoryProtocol + + public init(realEmojiRepository: RealEmojiRepositoryProtocol) { + self.realEmojiRepository = realEmojiRepository + } + + public func execute() -> Single<[MyRealEmojiEntity?]> { + return realEmojiRepository.fetchMyRealEmoji() + } +} diff --git a/14th-team5-iOS/Domain/Sources/UseCases/RealEmoji/FetchRealEmojiListUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/RealEmoji/FetchRealEmojiListUseCase.swift new file mode 100644 index 000000000..932a52eb1 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/UseCases/RealEmoji/FetchRealEmojiListUseCase.swift @@ -0,0 +1,27 @@ +// +// FEtchRealEmojiListUseCase.swift +// Domain +// +// Created by 마경미 on 12.06.24. +// + +import Foundation + +import RxSwift + +public protocol FetchRealEmojiListUseCaseProtocol { + func execute(query: FetchRealEmojiQuery) -> Single<[RealEmojiEntity]?> +} + +public class FetchRealEmojiListUseCase: FetchRealEmojiListUseCaseProtocol { + + private let realEmojiRepository: RealEmojiRepositoryProtocol + + public init(realEmojiRepository: RealEmojiRepositoryProtocol) { + self.realEmojiRepository = realEmojiRepository + } + + public func execute(query: FetchRealEmojiQuery) -> Single<[RealEmojiEntity]?> { + return realEmojiRepository.fetchRealEmoji(query: query) + } +} diff --git a/14th-team5-iOS/Domain/Sources/UseCases/RealEmoji/RealEmojiUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/RealEmoji/RealEmojiUseCase.swift new file mode 100644 index 000000000..d46902305 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/UseCases/RealEmoji/RealEmojiUseCase.swift @@ -0,0 +1,45 @@ +//// +//// RealEmojiUseCase.swift +//// Domain +//// +//// Created by 마경미 on 22.01.24. +//// +// +//import Foundation +//import RxSwift +// +//public protocol RealEmojiUseCaseProtocol { +// /// load my real emoji +// func execute() -> Single<[MyRealEmoji?]> +// /// add real emoji +// func execute(query: CreateReactionQuery, body: CreateReactionRequest) -> Single +// /// fetch real emoji list at post +// func execute(query: FetchRealEmojiQuery) -> Single<[RealEmojiEntity]?> +// /// remove real emoji +// func execute(query: RemoveRealEmojiQuery) -> Single +//} +// +//public class RealEmojiUseCase: RealEmojiUseCaseProtocol { +// +// private let realEmojiRepository: RealEmojiRepositoryProtocol +// +// public init(realEmojiRepository: RealEmojiRepositoryProtocol) { +// self.realEmojiRepository = realEmojiRepository +// } +// +// public func execute() -> Single<[MyRealEmoji?]> { +// return realEmojiRepository.fetchMyRealEmoji() +// } +// +// public func execute(query: FetchRealEmojiQuery) -> Single<[RealEmojiEntity]?> { +// return realEmojiRepository.fetchRealEmoji(query: query) +// } +// +// public func execute(query: CreateReactionQuery, body: CreateReactionRequest) -> Single { +// return realEmojiRepository.addRealEmoji(query: query, body: body) +// } +// +// public func execute(query: RemoveRealEmojiQuery) -> Single { +// return realEmojiRepository.removeRealEmoji(query: query) +// } +//} diff --git a/14th-team5-iOS/Domain/Sources/UseCases/RealEmoji/RemoveRealEmojiUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/RealEmoji/RemoveRealEmojiUseCase.swift new file mode 100644 index 000000000..1b7de13ef --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/UseCases/RealEmoji/RemoveRealEmojiUseCase.swift @@ -0,0 +1,27 @@ +// +// RemoveRealEmojiUseCase.swift +// Domain +// +// Created by 마경미 on 12.06.24. +// + +import Foundation + +import RxSwift + +public protocol RemoveRealEmojiUseCaseProtocol { + func execute(query: RemoveRealEmojiQuery) -> Single +} + +public class RemoveRealEmojiUseCase: RemoveRealEmojiUseCaseProtocol { + + private let realEmojiRepository: RealEmojiRepositoryProtocol + + public init(realEmojiRepository: RealEmojiRepositoryProtocol) { + self.realEmojiRepository = realEmojiRepository + } + + public func execute(query: RemoveRealEmojiQuery) -> Single { + return realEmojiRepository.removeRealEmoji(query: query) + } +} From 0152c93e4ab7fb00bb46c1f89e83f1ad2ecf6bf2 Mon Sep 17 00:00:00 2001 From: Kyoungmi <1mmgm.dev@gmail.com> Date: Wed, 12 Jun 2024 23:00:49 +0900 Subject: [PATCH 104/263] refactor: reaction(#560) --- .../FetchReactionResponseDTO.swift | 6 +-- .../ReactionAPI/ReactionAPIWorker.swift | 6 +-- .../Repository/ReactionRepository.swift | 6 +-- .../Emoji/Entities/AddEmojiRequest.swift | 25 ----------- .../Emoji/Entities/FetchEmojiData.swift | 29 ------------- .../Emoji/Entities/FetchEmojiQuery.swift | 16 ------- .../Emoji/Entities/RemoveEmojiData.swift | 14 ------ .../Emoji/Entities/RemoveEmojiRequest.swift | 25 ----------- .../ReactionRepositoryProtocol.swift | 16 ------- .../Emoji/UseCases/ReactionUseCase.swift | 43 ------------------- 10 files changed, 9 insertions(+), 177 deletions(-) delete mode 100644 14th-team5-iOS/Domain/Sources/Emoji/Entities/AddEmojiRequest.swift delete mode 100644 14th-team5-iOS/Domain/Sources/Emoji/Entities/FetchEmojiData.swift delete mode 100644 14th-team5-iOS/Domain/Sources/Emoji/Entities/FetchEmojiQuery.swift delete mode 100644 14th-team5-iOS/Domain/Sources/Emoji/Entities/RemoveEmojiData.swift delete mode 100644 14th-team5-iOS/Domain/Sources/Emoji/Entities/RemoveEmojiRequest.swift delete mode 100644 14th-team5-iOS/Domain/Sources/Emoji/Interfaces/ReactionRepositoryProtocol.swift delete mode 100644 14th-team5-iOS/Domain/Sources/Emoji/UseCases/ReactionUseCase.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/DataMapping/FetchReactionResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/DataMapping/FetchReactionResponseDTO.swift index 88fe136f9..ad673b6a3 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/DataMapping/FetchReactionResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/DataMapping/FetchReactionResponseDTO.swift @@ -25,19 +25,19 @@ struct FetchReactionResult: Codable { struct FetchReactionResponseDTO: Codable { let results: [FetchReactionResult] - func toDomain() -> [FetchedEmojiData] { + func toDomain() -> [RealEmojiEntity] { let myMemberId = FamilyUserDefaults.returnMyMemberId() let groupedByEmojiType = Dictionary(grouping: results, by: { $0.emojiType }) let fetchedEmojiDataArray = groupedByEmojiType.map { (emojiType, responses) in guard let minReactionIdResponse = responses.min(by: { $0.reactionId < $1.reactionId }) else { - return FetchedEmojiData(isStandard: true, isSelfSelected: false, postEmojiId: "", emojiType: .emoji1, count: 0, realEmojiId: "", realEmojiImageURL: "", memberIds: []) + return RealEmojiEntity(isStandard: true, isSelfSelected: false, postEmojiId: "", emojiType: .emoji1, count: 0, realEmojiId: "", realEmojiImageURL: "", memberIds: []) } let selfSelected = responses.contains { $0.memberId == myMemberId } let count = responses.count - return FetchedEmojiData( + return RealEmojiEntity( isStandard: true, isSelfSelected: selfSelected, postEmojiId: minReactionIdResponse.reactionId, diff --git a/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/ReactionAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/ReactionAPIWorker.swift index e472ad81c..85712ce0b 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/ReactionAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/ReactionAPIWorker.swift @@ -39,7 +39,7 @@ extension ReactionAPIs { } extension ReactionAPIWorker { - func fetchReaction(query: Domain.FetchEmojiQuery) -> RxSwift.Single<[FetchedEmojiData]?> { + func fetchReaction(query: Domain.FetchReactionQuery) -> RxSwift.Single<[RealEmojiEntity]?> { let query = FetchReactionRequestDTO(postId: query.postId) let spec = ReactionAPIs.fetchReactions(query).spec return request(spec: spec, headers: headers) @@ -57,7 +57,7 @@ extension ReactionAPIWorker { .asSingle() } - func addReaction(query: Domain.AddEmojiQuery, body: Domain.AddEmojiBody) -> RxSwift.Single { + func addReaction(query: Domain.CreateReactionQuery, body: Domain.CreateReactionRequest) -> RxSwift.Single { let requestDTO = AddReactionRequestDTO(content: body.emojiId) let spec = ReactionAPIs.addReactions(query.postId).spec return request(spec: spec, headers: headers, jsonEncodable: requestDTO) @@ -73,7 +73,7 @@ extension ReactionAPIWorker { .asSingle() } - func removeReaction(query: Domain.RemoveEmojiQuery, body: Domain.RemoveEmojiBody) -> RxSwift.Single { + func removeReaction(query: Domain.RemoveReactionQuery, body: Domain.RemoveReactionRequest) -> RxSwift.Single { let requestDTO = RemoveReactionRequestDTO(content: body.content.emojiString) let spec = ReactionAPIs.removeReactions(query.postId).spec return request(spec: spec, headers: headers, jsonEncodable: requestDTO) diff --git a/14th-team5-iOS/Data/Sources/APIs/Reaction/Repository/ReactionRepository.swift b/14th-team5-iOS/Data/Sources/APIs/Reaction/Repository/ReactionRepository.swift index 88bdf2027..53de285a2 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Reaction/Repository/ReactionRepository.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Reaction/Repository/ReactionRepository.swift @@ -18,15 +18,15 @@ public final class ReactionRepository: ReactionRepositoryProtocol { public init () { } - public func addReaction(query: Domain.AddEmojiQuery, body: Domain.AddEmojiBody) -> RxSwift.Single { + public func addReaction(query: Domain.CreateReactionQuery, body: Domain.CreateReactionRequest) -> RxSwift.Single { return reactionAPIWorker.addReaction(query: query, body: body) } - public func removeReaction(query: Domain.RemoveEmojiQuery, body: Domain.RemoveEmojiBody) -> RxSwift.Single { + public func removeReaction(query: Domain.RemoveReactionQuery, body: Domain.RemoveReactionRequest) -> RxSwift.Single { return reactionAPIWorker.removeReaction(query: query, body: body) } - public func fetchReaction(query: Domain.FetchEmojiQuery) -> RxSwift.Single<[Domain.FetchedEmojiData]?> { + public func fetchReaction(query: Domain.FetchReactionQuery) -> RxSwift.Single<[Domain.RealEmojiEntity]?> { return reactionAPIWorker.fetchReaction(query: query) } } diff --git a/14th-team5-iOS/Domain/Sources/Emoji/Entities/AddEmojiRequest.swift b/14th-team5-iOS/Domain/Sources/Emoji/Entities/AddEmojiRequest.swift deleted file mode 100644 index 92592edb5..000000000 --- a/14th-team5-iOS/Domain/Sources/Emoji/Entities/AddEmojiRequest.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// AddEmojiRequest.swift -// Domain -// -// Created by 마경미 on 01.01.24. -// - -import Foundation -import Core - -public struct AddEmojiQuery { - public let postId: String - - public init(postId: String) { - self.postId = postId - } -} - -public struct AddEmojiBody { - public let emojiId: String - - public init(content: String) { - self.emojiId = content - } -} diff --git a/14th-team5-iOS/Domain/Sources/Emoji/Entities/FetchEmojiData.swift b/14th-team5-iOS/Domain/Sources/Emoji/Entities/FetchEmojiData.swift deleted file mode 100644 index f9997d44a..000000000 --- a/14th-team5-iOS/Domain/Sources/Emoji/Entities/FetchEmojiData.swift +++ /dev/null @@ -1,29 +0,0 @@ -//// -//// FetchEmojiData.swift -//// Domain -//// -//// Created by 마경미 on 03.01.24. -//// -// -//import Foundation -//import Core -// -//public struct FetchEmojiData: Equatable { -// public var isSelfSelected: Bool -// public var count: Int -// public var memberIds: [String] -// -// public init(isSelfSelected: Bool, count: Int, memberIds: [String]) { -// self.isSelfSelected = isSelfSelected -// self.count = count -// self.memberIds = memberIds -// } -//} -// -//public struct FetchEmojiDataList { -// public let emojis_memberIds: [FetchEmojiData] -// -// public init(emojis_memberIds: [FetchEmojiData]) { -// self.emojis_memberIds = emojis_memberIds -// } -//} diff --git a/14th-team5-iOS/Domain/Sources/Emoji/Entities/FetchEmojiQuery.swift b/14th-team5-iOS/Domain/Sources/Emoji/Entities/FetchEmojiQuery.swift deleted file mode 100644 index 505774c0f..000000000 --- a/14th-team5-iOS/Domain/Sources/Emoji/Entities/FetchEmojiQuery.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// FetchEmojiQuery.swift -// Domain -// -// Created by 마경미 on 03.01.24. -// - -import Foundation - -public struct FetchEmojiQuery { - public let postId: String - - public init(postId: String) { - self.postId = postId - } -} diff --git a/14th-team5-iOS/Domain/Sources/Emoji/Entities/RemoveEmojiData.swift b/14th-team5-iOS/Domain/Sources/Emoji/Entities/RemoveEmojiData.swift deleted file mode 100644 index 952a511bf..000000000 --- a/14th-team5-iOS/Domain/Sources/Emoji/Entities/RemoveEmojiData.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// RemoveEmojiData.swift -// Domain -// -// Created by 마경미 on 01.01.24. -// - -import Foundation - -public struct RemoveEmojiData { - public init() { - - } -} diff --git a/14th-team5-iOS/Domain/Sources/Emoji/Entities/RemoveEmojiRequest.swift b/14th-team5-iOS/Domain/Sources/Emoji/Entities/RemoveEmojiRequest.swift deleted file mode 100644 index 6a808c918..000000000 --- a/14th-team5-iOS/Domain/Sources/Emoji/Entities/RemoveEmojiRequest.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// RemoveEmojiRequest.swift -// Domain -// -// Created by 마경미 on 01.01.24. -// - -import Foundation -import Core - -public struct RemoveEmojiQuery { - public let postId: String - - public init(postId: String) { - self.postId = postId - } -} - -public struct RemoveEmojiBody { - public let content: Emojis - - public init(content: Emojis) { - self.content = content - } -} diff --git a/14th-team5-iOS/Domain/Sources/Emoji/Interfaces/ReactionRepositoryProtocol.swift b/14th-team5-iOS/Domain/Sources/Emoji/Interfaces/ReactionRepositoryProtocol.swift deleted file mode 100644 index 21f974e75..000000000 --- a/14th-team5-iOS/Domain/Sources/Emoji/Interfaces/ReactionRepositoryProtocol.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// EmojiRepository.swift -// Domain -// -// Created by 마경미 on 01.01.24. -// - -import Foundation - -import RxSwift - -public protocol ReactionRepositoryProtocol { - func addReaction(query: AddEmojiQuery, body: AddEmojiBody) -> Single - func removeReaction(query: RemoveEmojiQuery, body: RemoveEmojiBody) -> Single - func fetchReaction(query: FetchEmojiQuery) -> Single<[FetchedEmojiData]?> -} diff --git a/14th-team5-iOS/Domain/Sources/Emoji/UseCases/ReactionUseCase.swift b/14th-team5-iOS/Domain/Sources/Emoji/UseCases/ReactionUseCase.swift deleted file mode 100644 index 0b22c91bc..000000000 --- a/14th-team5-iOS/Domain/Sources/Emoji/UseCases/ReactionUseCase.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// EmojiUseCase.swift -// Domain -// -// Created by 마경미 on 01.01.24. -// - -import Foundation -import RxSwift - -protocol EmojiProtocol { - -} - - -public protocol ReactionUseCaseProtocol { - /// Add Reactions - func executeAddEmoji(query: AddEmojiQuery, body: AddEmojiBody) -> Single - /// Remove Reactions - func excuteRemoveEmoji(query: RemoveEmojiQuery, body: RemoveEmojiBody) -> Single - /// fetch Reaction List - func execute(query: FetchEmojiQuery) -> Single<[FetchedEmojiData]?> -} - -public class ReactionUseCase: ReactionUseCaseProtocol { - private let reactionRepository: ReactionRepositoryProtocol - - public init(reactionRepository: ReactionRepositoryProtocol) { - self.reactionRepository = reactionRepository - } - - public func executeAddEmoji(query: AddEmojiQuery, body: AddEmojiBody) -> Single { - return reactionRepository.addReaction(query: query, body: body) - } - - public func excuteRemoveEmoji(query: RemoveEmojiQuery, body: RemoveEmojiBody) -> Single { - return reactionRepository.removeReaction(query: query, body: body) - } - - public func execute(query: FetchEmojiQuery) -> Single<[FetchedEmojiData]?> { - return reactionRepository.fetchReaction(query: query) - } -} From cb5617869a7821294f818c0a57c80831148e9e40 Mon Sep 17 00:00:00 2001 From: Kyoungmi <1mmgm.dev@gmail.com> Date: Wed, 12 Jun 2024 23:01:32 +0900 Subject: [PATCH 105/263] refacotr: realEmoji usecase(#560) --- .../FetchRealEmojiListResponseDTO.swift | 6 +-- .../DataMapping/MyRealEmojiResponseDTO.swift | 6 +-- .../RealEmojiAPI/RealEmojiAPIWorker.swift | 6 +-- .../Repository/RealEmojiRepository.swift | 6 +-- .../Entities/FetchRealEmojiData.swift | 31 ------------- .../Entities/FetchRealEmojiQuery.swift | 16 ------- .../RealEmoji/Entities/MyRealEmoji.swift | 22 --------- .../Entities/RemoveRealEmojiQuery.swift | 18 -------- .../RealEmojiRepositoryProtocol.swift | 17 ------- .../RealEmoji/UseCases/RealEmojiUseCase.swift | 45 ------------------- 10 files changed, 12 insertions(+), 161 deletions(-) delete mode 100644 14th-team5-iOS/Domain/Sources/RealEmoji/Entities/FetchRealEmojiData.swift delete mode 100644 14th-team5-iOS/Domain/Sources/RealEmoji/Entities/FetchRealEmojiQuery.swift delete mode 100644 14th-team5-iOS/Domain/Sources/RealEmoji/Entities/MyRealEmoji.swift delete mode 100644 14th-team5-iOS/Domain/Sources/RealEmoji/Entities/RemoveRealEmojiQuery.swift delete mode 100644 14th-team5-iOS/Domain/Sources/RealEmoji/Interfaces/RealEmojiRepositoryProtocol.swift delete mode 100644 14th-team5-iOS/Domain/Sources/RealEmoji/UseCases/RealEmojiUseCase.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/DataMapping/FetchRealEmojiListResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/DataMapping/FetchRealEmojiListResponseDTO.swift index 3430ba855..7c54d0a32 100644 --- a/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/DataMapping/FetchRealEmojiListResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/DataMapping/FetchRealEmojiListResponseDTO.swift @@ -22,19 +22,19 @@ struct RealEmojiListResult: Codable { struct FetchRealEmojiListResponseDTO: Codable { let results: [RealEmojiListResult] - func toDomain() -> [FetchedEmojiData]? { + func toDomain() -> [RealEmojiEntity]? { let myMemberId = FamilyUserDefaults.returnMyMemberId() let groupedByEmojiType = Dictionary(grouping: results, by: { $0.realEmojiId }) let fetchedEmojiDataArray = groupedByEmojiType.map { (emojiType, responses) in guard let minReactionIdResponse = responses.min(by: { $0.postRealEmojiId < $1.postRealEmojiId }) else { - return FetchedEmojiData(isStandard: false, isSelfSelected: false, postEmojiId: "", emojiType: .emoji1, count: 0, realEmojiId: "", realEmojiImageURL: "", memberIds: []) + return RealEmojiEntity(isStandard: false, isSelfSelected: false, postEmojiId: "", emojiType: .emoji1, count: 0, realEmojiId: "", realEmojiImageURL: "", memberIds: []) } let selfSelected = responses.contains { $0.memberId == myMemberId } let count = responses.count - return FetchedEmojiData( + return RealEmojiEntity( isStandard: false, isSelfSelected: selfSelected, postEmojiId: minReactionIdResponse.postRealEmojiId, diff --git a/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/DataMapping/MyRealEmojiResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/DataMapping/MyRealEmojiResponseDTO.swift index 8c13398c8..bc087bedf 100644 --- a/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/DataMapping/MyRealEmojiResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/DataMapping/MyRealEmojiResponseDTO.swift @@ -15,7 +15,7 @@ struct MyRealEmojiList: Codable { let type: String let imageUrl: String - func toDomain() -> MyRealEmoji { + func toDomain() -> MyRealEmojiEntity { return .init(realEmojiId: realEmojiId, type: Emojis.emoji(forString: type), imageUrl: imageUrl) } } @@ -23,8 +23,8 @@ struct MyRealEmojiList: Codable { struct MyRealEmojiResponseDTO: Codable { let myRealEmojiList: [MyRealEmojiList] - func toDomain() -> [MyRealEmoji?] { - var arr: [MyRealEmoji?] = [nil, nil, nil, nil, nil] + func toDomain() -> [MyRealEmojiEntity?] { + var arr: [MyRealEmojiEntity?] = [nil, nil, nil, nil, nil] myRealEmojiList.forEach { guard let chr = $0.type.last else { return diff --git a/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/RealEmojiAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/RealEmojiAPIWorker.swift index c949f36ce..f1bfd955f 100644 --- a/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/RealEmojiAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/RealEmojiAPIWorker.swift @@ -41,7 +41,7 @@ extension RealEmojiAPIs { extension RealEmojiAPIWorker { - func fetchRealEmoji(query: FetchRealEmojiQuery) -> Single<[FetchedEmojiData]?> { + func fetchRealEmoji(query: FetchRealEmojiQuery) -> Single<[RealEmojiEntity]?> { let query = FetchRealEmojiListParameter(postId: query.postId) let spec = RealEmojiAPIs.fetchRealEmojiList(query).spec @@ -60,7 +60,7 @@ extension RealEmojiAPIWorker { .asSingle() } - func fetchMyRealEmoji() -> Single<[MyRealEmoji?]> { + func fetchMyRealEmoji() -> Single<[MyRealEmojiEntity?]> { let spec = RealEmojiAPIs.fetchMyRealEmoji.spec return request(spec: spec, headers: headers) @@ -78,7 +78,7 @@ extension RealEmojiAPIWorker { .asSingle() } - func addRealEmoji(query: AddEmojiQuery, body: AddEmojiBody) -> Single { + func addRealEmoji(query: CreateReactionQuery, body: CreateReactionRequest) -> Single { let spec = RealEmojiAPIs.addRealEmoji(.init(postId: query.postId)).spec let body = AddRealEmojiRequestDTO(realEmojiId: body.emojiId) diff --git a/14th-team5-iOS/Data/Sources/APIs/RealEmoji/Repository/RealEmojiRepository.swift b/14th-team5-iOS/Data/Sources/APIs/RealEmoji/Repository/RealEmojiRepository.swift index 0f51839dd..0ed3191c3 100644 --- a/14th-team5-iOS/Data/Sources/APIs/RealEmoji/Repository/RealEmojiRepository.swift +++ b/14th-team5-iOS/Data/Sources/APIs/RealEmoji/Repository/RealEmojiRepository.swift @@ -18,15 +18,15 @@ public final class RealEmojiRepository: RealEmojiRepositoryProtocol { public init () { } - public func fetchMyRealEmoji() -> RxSwift.Single<[Domain.MyRealEmoji?]> { + public func fetchMyRealEmoji() -> RxSwift.Single<[Domain.MyRealEmojiEntity?]> { realEmojiAPIWorker.fetchMyRealEmoji() } - public func addRealEmoji(query: Domain.AddEmojiQuery, body: Domain.AddEmojiBody) -> RxSwift.Single { + public func addRealEmoji(query: Domain.CreateReactionQuery, body: Domain.CreateReactionRequest) -> RxSwift.Single { realEmojiAPIWorker.addRealEmoji(query: query, body: body) } - public func fetchRealEmoji(query: Domain.FetchRealEmojiQuery) -> RxSwift.Single<[Domain.FetchedEmojiData]?> { + public func fetchRealEmoji(query: Domain.FetchRealEmojiQuery) -> RxSwift.Single<[Domain.RealEmojiEntity]?> { realEmojiAPIWorker.fetchRealEmoji(query: query) } diff --git a/14th-team5-iOS/Domain/Sources/RealEmoji/Entities/FetchRealEmojiData.swift b/14th-team5-iOS/Domain/Sources/RealEmoji/Entities/FetchRealEmojiData.swift deleted file mode 100644 index 87dc6fd78..000000000 --- a/14th-team5-iOS/Domain/Sources/RealEmoji/Entities/FetchRealEmojiData.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// FetchRealEmojiData.swift -// Domain -// -// Created by 마경미 on 22.01.24. -// - -import Foundation -import Core - -public struct FetchedEmojiData { - public let isStandard: Bool - public var isSelfSelected: Bool - public let postEmojiId: String - public let emojiType: Emojis - public var count: Int - public let realEmojiId: String - public let realEmojiImageURL: String - public var memberIds: [String] - - public init(isStandard: Bool, isSelfSelected: Bool, postEmojiId: String, emojiType: Emojis, count: Int, realEmojiId: String, realEmojiImageURL: String, memberIds: [String]) { - self.isStandard = isStandard - self.isSelfSelected = isSelfSelected - self.postEmojiId = postEmojiId - self.emojiType = emojiType - self.count = count - self.realEmojiId = realEmojiId - self.realEmojiImageURL = realEmojiImageURL - self.memberIds = memberIds - } -} diff --git a/14th-team5-iOS/Domain/Sources/RealEmoji/Entities/FetchRealEmojiQuery.swift b/14th-team5-iOS/Domain/Sources/RealEmoji/Entities/FetchRealEmojiQuery.swift deleted file mode 100644 index e17de0720..000000000 --- a/14th-team5-iOS/Domain/Sources/RealEmoji/Entities/FetchRealEmojiQuery.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// FetchRealEmojiQuery.swift -// Domain -// -// Created by 마경미 on 22.01.24. -// - -import Foundation - -public struct FetchRealEmojiQuery { - public let postId: String - - public init(postId: String) { - self.postId = postId - } -} diff --git a/14th-team5-iOS/Domain/Sources/RealEmoji/Entities/MyRealEmoji.swift b/14th-team5-iOS/Domain/Sources/RealEmoji/Entities/MyRealEmoji.swift deleted file mode 100644 index fb8e8f466..000000000 --- a/14th-team5-iOS/Domain/Sources/RealEmoji/Entities/MyRealEmoji.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// MyRealEmoji.swift -// Domain -// -// Created by 마경미 on 27.01.24. -// - -import Foundation - -import Core - -public struct MyRealEmoji: Hashable { - public let realEmojiId: String - public let type: Emojis - public let imageUrl: String - - public init(realEmojiId: String, type: Emojis, imageUrl: String) { - self.realEmojiId = realEmojiId - self.type = type - self.imageUrl = imageUrl - } -} diff --git a/14th-team5-iOS/Domain/Sources/RealEmoji/Entities/RemoveRealEmojiQuery.swift b/14th-team5-iOS/Domain/Sources/RealEmoji/Entities/RemoveRealEmojiQuery.swift deleted file mode 100644 index 0c5566534..000000000 --- a/14th-team5-iOS/Domain/Sources/RealEmoji/Entities/RemoveRealEmojiQuery.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// RemoveRealEmojiQuery.swift -// Domain -// -// Created by 마경미 on 29.01.24. -// - -import Foundation - -public struct RemoveRealEmojiQuery { - public let postId: String - public let realEmojiId: String - - public init(postId: String, realEmojiId: String) { - self.postId = postId - self.realEmojiId = realEmojiId - } -} diff --git a/14th-team5-iOS/Domain/Sources/RealEmoji/Interfaces/RealEmojiRepositoryProtocol.swift b/14th-team5-iOS/Domain/Sources/RealEmoji/Interfaces/RealEmojiRepositoryProtocol.swift deleted file mode 100644 index f272251a9..000000000 --- a/14th-team5-iOS/Domain/Sources/RealEmoji/Interfaces/RealEmojiRepositoryProtocol.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// RealEmojiRepository.swift -// Domain -// -// Created by 마경미 on 22.01.24. -// - -import Foundation - -import RxSwift - -public protocol RealEmojiRepositoryProtocol { - func fetchMyRealEmoji() -> Single<[MyRealEmoji?]> - func addRealEmoji(query: AddEmojiQuery, body: AddEmojiBody) -> Single - func fetchRealEmoji(query: FetchRealEmojiQuery) -> Single<[FetchedEmojiData]?> - func removeRealEmoji(query: RemoveRealEmojiQuery) -> Single -} diff --git a/14th-team5-iOS/Domain/Sources/RealEmoji/UseCases/RealEmojiUseCase.swift b/14th-team5-iOS/Domain/Sources/RealEmoji/UseCases/RealEmojiUseCase.swift deleted file mode 100644 index 389e74665..000000000 --- a/14th-team5-iOS/Domain/Sources/RealEmoji/UseCases/RealEmojiUseCase.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// RealEmojiUseCase.swift -// Domain -// -// Created by 마경미 on 22.01.24. -// - -import Foundation -import RxSwift - -public protocol RealEmojiUseCaseProtocol { - /// load my real emoji - func execute() -> Single<[MyRealEmoji?]> - /// add real emoji - func execute(query: AddEmojiQuery, body: AddEmojiBody) -> Single - /// fetch real emoji list at post - func execute(query: FetchRealEmojiQuery) -> Single<[FetchedEmojiData]?> - /// remove real emoji - func execute(query: RemoveRealEmojiQuery) -> Single -} - -public class RealEmojiUseCase: RealEmojiUseCaseProtocol { - - private let realEmojiRepository: RealEmojiRepositoryProtocol - - public init(realEmojiRepository: RealEmojiRepositoryProtocol) { - self.realEmojiRepository = realEmojiRepository - } - - public func execute() -> Single<[MyRealEmoji?]> { - return realEmojiRepository.fetchMyRealEmoji() - } - - public func execute(query: FetchRealEmojiQuery) -> Single<[FetchedEmojiData]?> { - return realEmojiRepository.fetchRealEmoji(query: query) - } - - public func execute(query: AddEmojiQuery, body: AddEmojiBody) -> Single { - return realEmojiRepository.addRealEmoji(query: query, body: body) - } - - public func execute(query: RemoveRealEmojiQuery) -> Single { - return realEmojiRepository.removeRealEmoji(query: query) - } -} From a4d7408938be1ab1ec474d5ec731022741aeac9e Mon Sep 17 00:00:00 2001 From: Kyoungmi <1mmgm.dev@gmail.com> Date: Wed, 12 Jun 2024 23:02:21 +0900 Subject: [PATCH 106/263] refactor: post usecase(#560) --- .../DataMapping/PostListResponseDTO.swift | 2 +- .../PostList/DataMapping/PostRequestDTO.swift | 10 +-- .../DataMapping/PostResponseDTO.swift | 14 ++--- .../PostList/PostAPI/PostListAPIWorker.swift | 16 ----- .../PostList/Repository/PostRepository.swift | 4 -- .../UserDefaults/PostUserDefaults.swift | 2 +- .../Sources/Post/Entities/EmojiData.swift | 19 ------ .../Sources/Post/Entities/PostData.swift | 24 ------- .../Sources/Post/Entities/PostListData.swift | 42 ------------- .../Sources/Post/Entities/PostListQuery.swift | 63 ------------------- .../Sources/Post/Entities/PostQuery.swift | 16 ----- .../PostListRepositoryProtocol.swift | 14 ----- .../UploadPostRepositoryProtocol.swift | 14 ----- .../Post/UseCases/PostListUseCase.swift | 38 ----------- 14 files changed, 14 insertions(+), 264 deletions(-) delete mode 100644 14th-team5-iOS/Domain/Sources/Post/Entities/EmojiData.swift delete mode 100644 14th-team5-iOS/Domain/Sources/Post/Entities/PostData.swift delete mode 100644 14th-team5-iOS/Domain/Sources/Post/Entities/PostListData.swift delete mode 100644 14th-team5-iOS/Domain/Sources/Post/Entities/PostListQuery.swift delete mode 100644 14th-team5-iOS/Domain/Sources/Post/Entities/PostQuery.swift delete mode 100644 14th-team5-iOS/Domain/Sources/Post/Interfaces/PostListRepositoryProtocol.swift delete mode 100644 14th-team5-iOS/Domain/Sources/Post/Interfaces/UploadPostRepositoryProtocol.swift delete mode 100644 14th-team5-iOS/Domain/Sources/Post/UseCases/PostListUseCase.swift diff --git a/14th-team5-iOS/Data/Sources/Post/PostList/DataMapping/PostListResponseDTO.swift b/14th-team5-iOS/Data/Sources/Post/PostList/DataMapping/PostListResponseDTO.swift index 2a7f5eea0..194466c27 100644 --- a/14th-team5-iOS/Data/Sources/Post/PostList/DataMapping/PostListResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/Post/PostList/DataMapping/PostListResponseDTO.swift @@ -22,7 +22,7 @@ struct PostListResultsDTO: Codable { } extension PostListResultsDTO { - func toDomain() -> PostListData { + func toDomain() -> PostEntity { let author = FamilyUserDefaults.load(memberId: authorId) return .init( postId: postId, diff --git a/14th-team5-iOS/Data/Sources/Post/PostList/DataMapping/PostRequestDTO.swift b/14th-team5-iOS/Data/Sources/Post/PostList/DataMapping/PostRequestDTO.swift index 716a52d20..2bd9a0bf6 100644 --- a/14th-team5-iOS/Data/Sources/Post/PostList/DataMapping/PostRequestDTO.swift +++ b/14th-team5-iOS/Data/Sources/Post/PostList/DataMapping/PostRequestDTO.swift @@ -13,8 +13,8 @@ public struct PostRequestDTO: Codable { let postId: String } -extension PostRequestDTO { - func toDomain() -> PostQuery { - return .init(postId: postId) - } -} +//extension PostRequestDTO { +// func toDomain() -> BrieftPostQuery { +// return .init(postId: postId) +// } +//} diff --git a/14th-team5-iOS/Data/Sources/Post/PostList/DataMapping/PostResponseDTO.swift b/14th-team5-iOS/Data/Sources/Post/PostList/DataMapping/PostResponseDTO.swift index e4a0f1cf1..5b59fe3cb 100644 --- a/14th-team5-iOS/Data/Sources/Post/PostList/DataMapping/PostResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/Post/PostList/DataMapping/PostResponseDTO.swift @@ -18,10 +18,10 @@ struct PostResponseDTO: Codable { let createdAt: String } -extension PostResponseDTO { - func toDomain() -> PostData { - let writer = FamilyUserDefaults.load(memberId: authorId) - - return .init(writer: writer, time: createdAt, imageURL: imageUrl, imageText: content, emojis: []) - } -} +//extension PostResponseDTO { +// func toDomain() -> BriefPostEntity { +// let writer = FamilyUserDefaults.load(memberId: authorId) +// +// return .init(writer: writer, time: createdAt, imageURL: imageUrl, imageText: content, emojis: []) +// } +//} diff --git a/14th-team5-iOS/Data/Sources/Post/PostList/PostAPI/PostListAPIWorker.swift b/14th-team5-iOS/Data/Sources/Post/PostList/PostAPI/PostListAPIWorker.swift index 53599b96c..0cb3928b1 100644 --- a/14th-team5-iOS/Data/Sources/Post/PostList/PostAPI/PostListAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Post/PostList/PostAPI/PostListAPIWorker.swift @@ -28,22 +28,6 @@ extension PostAPIs { } extension PostAPIWorker { - public func fetchPostDetail(query: Domain.PostQuery) -> RxSwift.Single { - let requestDTO = PostRequestDTO(postId: query.postId) - let spec = PostAPIs.fetchPostDetail(requestDTO).spec - return request(spec: spec) - .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("PostDetail Fetch Result: \(str)") - } - } - .map(PostResponseDTO.self) - .catchAndReturn(nil) - .map { $0?.toDomain() } - .asSingle() - } - public func fetchTodayPostList(query: Domain.PostListQuery) -> RxSwift.Single { let requestDTO = PostListRequestDTO(page: query.page, size: query.size, date: query.date, memberId: query.memberId, sort: query.sort, type: query.type.rawValue) let spec = PostAPIs.fetchPostList.spec diff --git a/14th-team5-iOS/Data/Sources/Post/PostList/Repository/PostRepository.swift b/14th-team5-iOS/Data/Sources/Post/PostList/Repository/PostRepository.swift index d7a610efc..58433c087 100644 --- a/14th-team5-iOS/Data/Sources/Post/PostList/Repository/PostRepository.swift +++ b/14th-team5-iOS/Data/Sources/Post/PostList/Repository/PostRepository.swift @@ -17,10 +17,6 @@ public final class PostRepository: PostListRepositoryProtocol { public init() { } - public func fetchPostDetail(query: Domain.PostQuery) -> RxSwift.Single { - return postAPIWorker.fetchPostDetail(query: query) - } - public func fetchTodayPostList(query: Domain.PostListQuery) -> RxSwift.Single { return postAPIWorker.fetchTodayPostList(query: query) } diff --git a/14th-team5-iOS/Data/Sources/UserDefaults/PostUserDefaults.swift b/14th-team5-iOS/Data/Sources/UserDefaults/PostUserDefaults.swift index 78576bb1c..8a78fa943 100644 --- a/14th-team5-iOS/Data/Sources/UserDefaults/PostUserDefaults.swift +++ b/14th-team5-iOS/Data/Sources/UserDefaults/PostUserDefaults.swift @@ -11,7 +11,7 @@ import Domain import RxSwift -public final class PostUserDefaultsRepository: UploadPostRepositoryProtocol { +public final class PostUserDefaultsRepository { private let lastPostUploadDateId = "lastPostUploadDateId" public init() { diff --git a/14th-team5-iOS/Domain/Sources/Post/Entities/EmojiData.swift b/14th-team5-iOS/Domain/Sources/Post/Entities/EmojiData.swift deleted file mode 100644 index 14645d5fd..000000000 --- a/14th-team5-iOS/Domain/Sources/Post/Entities/EmojiData.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// EmojiData.swift -// Domain -// -// Created by 마경미 on 30.12.23. -// - -import Foundation -import Core - -public struct EmojiData { - public let emoji: Emojis - public var count: Int - - public init(emoji: Emojis, count: Int) { - self.emoji = emoji - self.count = count - } -} diff --git a/14th-team5-iOS/Domain/Sources/Post/Entities/PostData.swift b/14th-team5-iOS/Domain/Sources/Post/Entities/PostData.swift deleted file mode 100644 index 4c6339a64..000000000 --- a/14th-team5-iOS/Domain/Sources/Post/Entities/PostData.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// PostData.swift -// Domain -// -// Created by 마경미 on 30.12.23. -// - -import Foundation - -public struct PostData { - let writer: ProfileData? - let time: String - public let imageURL: String - let imageText: String - let emojis: [EmojiData] - - public init(writer: ProfileData?, time: String, imageURL: String, imageText: String, emojis: [EmojiData]) { - self.writer = writer - self.time = time - self.imageURL = imageURL - self.imageText = imageText - self.emojis = emojis - } -} diff --git a/14th-team5-iOS/Domain/Sources/Post/Entities/PostListData.swift b/14th-team5-iOS/Domain/Sources/Post/Entities/PostListData.swift deleted file mode 100644 index 339edfc96..000000000 --- a/14th-team5-iOS/Domain/Sources/Post/Entities/PostListData.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// DailyPostData.swift -// Domain -// -// Created by 마경미 on 25.12.23. -// - -import Foundation - -public struct PostListData: Equatable, Hashable { - public let postId: String - public let author: ProfileData? - public var commentCount: Int - public let missionId: String? - public let missionType: String? - public let emojiCount: Int - public let imageURL: String - public let content: String? - public let time: String - - public init(postId: String, missionId: String? = nil, missionType: String? = nil, author: ProfileData?, commentCount: Int, emojiCount: Int, imageURL: String, content: String?, time: String) { - self.postId = postId - self.missionId = missionId - self.missionType = missionType - self.author = author - self.commentCount = commentCount - self.emojiCount = emojiCount - self.imageURL = imageURL - self.content = content - self.time = time - } -} - -public struct PostListPage: Equatable { - public let isLast: Bool - public let postLists: [PostListData] - - public init(isLast: Bool, postLists: [PostListData]) { - self.isLast = isLast - self.postLists = postLists - } -} diff --git a/14th-team5-iOS/Domain/Sources/Post/Entities/PostListQuery.swift b/14th-team5-iOS/Domain/Sources/Post/Entities/PostListQuery.swift deleted file mode 100644 index 1196057e7..000000000 --- a/14th-team5-iOS/Domain/Sources/Post/Entities/PostListQuery.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// PostListQuery.swift -// Domain -// -// Created by 마경미 on 25.12.23. -// - -import Foundation - -public enum Sort: String { - case asc = "ASC" - case desc = "DESC" -} - -public enum PostType: String { - case survival = "SURVIVAL" - case mission = "MISSION" - - public func getIndex() -> Int { - switch self { - case .survival: - return 0 - case .mission: - return 1 - } - } - - public static func getPostType(index: Int) -> PostType { - switch index { - case 0: - return .survival - case 1: - return .mission - default: - fatalError("index Out of range") - } - } -} - -public struct PostListQuery { - public var page: Int - public let size: Int - public let date: String - public let memberId: String? - public let type: PostType - public let sort: String - - public init( - page: Int = 1, - size: Int = 256, - date: String, - memberId: String? = nil, - type: PostType = .survival, - sort: Sort = .desc - ) { - self.page = page - self.size = size - self.date = date - self.memberId = memberId - self.type = type - self.sort = sort.rawValue - } -} diff --git a/14th-team5-iOS/Domain/Sources/Post/Entities/PostQuery.swift b/14th-team5-iOS/Domain/Sources/Post/Entities/PostQuery.swift deleted file mode 100644 index 45879cff2..000000000 --- a/14th-team5-iOS/Domain/Sources/Post/Entities/PostQuery.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// PostQuery.swift -// Domain -// -// Created by 마경미 on 30.12.23. -// - -import Foundation - -public struct PostQuery { - public let postId: String - - public init(postId: String) { - self.postId = postId - } -} diff --git a/14th-team5-iOS/Domain/Sources/Post/Interfaces/PostListRepositoryProtocol.swift b/14th-team5-iOS/Domain/Sources/Post/Interfaces/PostListRepositoryProtocol.swift deleted file mode 100644 index fb6426791..000000000 --- a/14th-team5-iOS/Domain/Sources/Post/Interfaces/PostListRepositoryProtocol.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// PostListRepository.swift -// Domain -// -// Created by 마경미 on 25.12.23. -// - -import Foundation -import RxSwift - -public protocol PostListRepositoryProtocol { - func fetchPostDetail(query: PostQuery) -> Single - func fetchTodayPostList(query: PostListQuery) -> Single -} diff --git a/14th-team5-iOS/Domain/Sources/Post/Interfaces/UploadPostRepositoryProtocol.swift b/14th-team5-iOS/Domain/Sources/Post/Interfaces/UploadPostRepositoryProtocol.swift deleted file mode 100644 index 4af517e92..000000000 --- a/14th-team5-iOS/Domain/Sources/Post/Interfaces/UploadPostRepositoryProtocol.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// UploadPostRepositoryProtocol.swift -// Domain -// -// Created by 마경미 on 30.03.24. -// - -import Foundation - -import RxSwift - -public protocol UploadPostRepositoryProtocol { - func checkPostUploadedToday() -> Single -} diff --git a/14th-team5-iOS/Domain/Sources/Post/UseCases/PostListUseCase.swift b/14th-team5-iOS/Domain/Sources/Post/UseCases/PostListUseCase.swift deleted file mode 100644 index 172b3492d..000000000 --- a/14th-team5-iOS/Domain/Sources/Post/UseCases/PostListUseCase.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// PostListUseCase.swift -// Domain -// -// Created by 마경미 on 25.12.23. -// - -import Foundation -import RxSwift - -public protocol PostListUseCaseProtocol { - func executePost(query: PostQuery) -> Single - func excute(query: PostListQuery) -> Single - func excute() -> Single -} - -public class PostListUseCase: PostListUseCaseProtocol { - private let postListRepository: PostListRepositoryProtocol - private let uploadPostRepository: UploadPostRepositoryProtocol? - - public init(postListRepository: PostListRepositoryProtocol, uploadePostRepository: UploadPostRepositoryProtocol? = nil) { - self.postListRepository = postListRepository - self.uploadPostRepository = uploadePostRepository - } - - public func excute(query: PostListQuery) -> Single { - return postListRepository.fetchTodayPostList(query: query) - } - - public func executePost(query: PostQuery) -> Single { - return postListRepository.fetchPostDetail(query: query) - } - - public func excute() -> RxSwift.Single { - guard let uploadPostRepository = uploadPostRepository else { return .just(false)} - return uploadPostRepository.checkPostUploadedToday() - } -} From 7a691bd010ef32250cca53278fa86668ce5adf7b Mon Sep 17 00:00:00 2001 From: Kyoungmi <1mmgm.dev@gmail.com> Date: Wed, 12 Jun 2024 23:02:56 +0900 Subject: [PATCH 107/263] refactor: rename usecase and entities at presentation(#560) --- .../App/Sources/Manager/DeepLinkManager.swift | 2 +- .../DailyCalendarViewController.swift | 2 +- .../Dependency/MainPostViewDIContainer.swift | 8 +-- .../Presentation/Home/Model/PostSection.swift | 2 +- .../Reactor/Cell/MainPostCellReactor.swift | 2 +- .../Home/Reactor/MainPostViewReactor.swift | 4 +- .../Views/MainPostCollectionViewCell.swift | 2 +- .../PostDetailCellDIContainer.swift | 2 +- .../Dependency/PostListsDIContainer.swift | 18 +----- .../Dependency/ReactionDIContainer.swift | 38 ++++++++++--- .../ReactionMemberDIContainer.swift | 4 +- .../SelectableEmojiDIContainer.swift | 18 ++++-- .../PostDetail/Model/ReactionSection.swift | 4 +- .../Model/SectionOfPostDetail.swift | 36 ++++++------ .../Reactor/PostDetailViewReactor.swift | 2 +- .../PostDetail/Reactor/PostReactor.swift | 9 +-- .../Reactor/ReactionMemberViewReactor.swift | 2 +- .../Reactor/ReactionViewReactor.swift | 57 +++++++++++-------- .../Reactor/SelectableEmojiReactor.swift | 41 +++++++------ .../PostDetail/Reactor/TempCellReactor.swift | 6 +- .../ViewControllers/PostViewController.swift | 2 +- .../ReactionViewController.swift | 2 +- .../Views/PostDetailCollectionViewCell.swift | 4 +- .../PostDetail/Views/PostNavigationView.swift | 2 +- .../Views/ReactionCollectionViewCell.swift | 2 +- .../SeletableEmojiCollectionViewCell.swift | 2 +- .../Reactor/ProfileFeedViewReactor.swift | 10 ++-- 27 files changed, 152 insertions(+), 131 deletions(-) diff --git a/14th-team5-iOS/App/Sources/Manager/DeepLinkManager.swift b/14th-team5-iOS/App/Sources/Manager/DeepLinkManager.swift index f37c01264..e0463b8e3 100644 --- a/14th-team5-iOS/App/Sources/Manager/DeepLinkManager.swift +++ b/14th-team5-iOS/App/Sources/Manager/DeepLinkManager.swift @@ -32,7 +32,7 @@ final class DeepLinkManager { // 이번 3차 끝나고, postdetailviewcontroller에서 post 불러오는 형태로 바꿔보겠습니다. let disposeBag: DisposeBag = DisposeBag() let postRepository: PostListRepositoryProtocol = PostRepository() - lazy var postUseCase: PostListUseCaseProtocol = PostListUseCase(postListRepository: postRepository) + lazy var postUseCase: FetchPostListUseCaseProtocol = FetchPostListUseCase(postListRepository: postRepository) private init() {} diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/DailyCalendarViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/DailyCalendarViewController.swift index 5d6178a7f..71357d870 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/DailyCalendarViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/DailyCalendarViewController.swift @@ -193,7 +193,7 @@ public final class DailyCalendarViewController: BBNavigationViewController PostListRepositoryProtocol { return PostRepository() } - - func makeUploadPostRepository() -> UploadPostRepositoryProtocol { - return PostUserDefaultsRepository() - } - func makePostUseCase() -> PostListUseCaseProtocol { - return PostListUseCase(postListRepository: makePostRepository(), uploadePostRepository: makeUploadPostRepository()) + func makePostUseCase() -> FetchPostListUseCaseProtocol { + return FetchPostListUseCase(postListRepository: makePostRepository()) } } diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Model/PostSection.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Model/PostSection.swift index 7da1f98c0..be06b67db 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Model/PostSection.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Model/PostSection.swift @@ -12,7 +12,7 @@ public struct PostSection { public typealias Model = SectionModel public enum Item { - case main(PostListData) + case main(PostEntity) } } diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/Cell/MainPostCellReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/Cell/MainPostCellReactor.swift index d011b8fa0..88bc293e7 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/Cell/MainPostCellReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/Cell/MainPostCellReactor.swift @@ -23,7 +23,7 @@ final class MainPostCellReactor: Reactor { } struct State { - let postListData: PostListData + let postListData: PostEntity var fetchedDisplayContent: [DisplayEditSectionModel] = [.displayKeyword([])] } diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainPostViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainPostViewReactor.swift index 13150db4d..7b9edb383 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainPostViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainPostViewReactor.swift @@ -35,9 +35,9 @@ final class MainPostViewReactor: Reactor { let initialState: State private let provider: GlobalStateProviderProtocol - private let postUseCase: PostListUseCaseProtocol + private let postUseCase: FetchPostListUseCaseProtocol - init(initialState: State, provider: GlobalStateProviderProtocol, postUseCase: PostListUseCaseProtocol) { + init(initialState: State, provider: GlobalStateProviderProtocol, postUseCase: FetchPostListUseCaseProtocol) { self.initialState = initialState self.provider = provider self.postUseCase = postUseCase diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Views/MainPostCollectionViewCell.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Views/MainPostCollectionViewCell.swift index 3f1811bd6..6369057d3 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Views/MainPostCollectionViewCell.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Views/MainPostCollectionViewCell.swift @@ -92,7 +92,7 @@ extension MainPostCollectionViewCell { } extension MainPostCollectionViewCell { - private func setCell(_ data: PostListData) { + private func setCell(_ data: PostEntity) { if let url = URL(string: data.imageURL ) { imageView.kf.setImage(with: url) indicator.stopAnimating() diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/PostDetailCellDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/PostDetailCellDIContainer.swift index aaba683b7..8fb7551ea 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/PostDetailCellDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/PostDetailCellDIContainer.swift @@ -29,7 +29,7 @@ final class PostDetailCellDIContainer { return MemberUseCase(memberRepository: makeMemberRepository()) } - func makeReactor(type: PostDetailViewReactor.CellType = .home, post: PostListData) -> PostDetailViewReactor { + func makeReactor(type: PostDetailViewReactor.CellType = .home, post: PostEntity) -> PostDetailViewReactor { return PostDetailViewReactor( provider: globalState, memberUserCase: makeMemberUseCase(), diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/PostListsDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/PostListsDIContainer.swift index bd57dadbf..8ccfd4941 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/PostListsDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/PostListsDIContainer.swift @@ -40,12 +40,8 @@ final class PostListsDIContainer { return PostRepository() } - func makeUploadPostRepository() -> UploadPostRepositoryProtocol { - return PostUserDefaultsRepository() - } - - func makePostUseCase() -> PostListUseCaseProtocol { - return PostListUseCase(postListRepository: makePostRepository()) + func makePostUseCase() -> FetchPostListUseCaseProtocol { + return FetchPostListUseCase(postListRepository: makePostRepository()) } func makeEmojiRepository() -> ReactionRepositoryProtocol { @@ -64,14 +60,6 @@ final class PostListsDIContainer { return MissionContentUseCase(missionContentRepository: makeMissionRepository()) } - func makeEmojiUseCase() -> ReactionUseCaseProtocol { - return ReactionUseCase(reactionRepository: makeEmojiRepository()) - } - - func makeRealEmojiUseCase() -> RealEmojiUseCaseProtocol { - return RealEmojiUseCase(realEmojiRepository: makeRealEmojiRepository()) - } - func makeReactor( postLists: PostSection.Model, selectedIndex: Int, @@ -79,8 +67,6 @@ final class PostListsDIContainer { ) -> Reactor { return PostReactor( provider: globalState, - realEmojiRepository: makeRealEmojiUseCase(), - emojiRepository: makeEmojiUseCase(), missionUseCase: makeMissionUseCase(), initialState: PostReactor.State( selectedIndex: selectedIndex, diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/ReactionDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/ReactionDIContainer.swift index d032aa43e..4804d8b34 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/ReactionDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/ReactionDIContainer.swift @@ -29,11 +29,19 @@ final class ReactionDIContainer { return appDelegate.globalStateProvider } - private func makeReactor(post: PostListData) -> ReactionViewReactor { - return ReactionViewReactor(provider: globalState, initialState: .init(type: type, postListData: post), emojiRepository: makeEmojiUseCase(), realEmojiRepository: makeRealEmojiUseCase()) + private func makeReactor(post: PostEntity) -> ReactionViewReactor { + return ReactionViewReactor( + initialState: .init(type: type, postListData: post), + provider: globalState, + fetchReactionUseCase: makeFetchReactionListUseCase(), + createReactionUseCase: makeCreateReactionUseCase(), + removeReactionUseCase: makeRemoveReactionUseCase(), + fetchRealEmojiListUseCase: makeFetchRealEmojiListUseCase(), + createRealEmojiUseCase: makeCreateRealEmojiUseCase(), + removeRealEmojiUseCase: makeRemoveRealEmojiUseCase()) } - func makeViewController(post: PostListData) -> ReactionViewController { + func makeViewController(post: PostEntity) -> ReactionViewController { return ReactionViewController(reactor: makeReactor(post: post)) } } @@ -43,8 +51,16 @@ extension ReactionDIContainer { return RealEmojiRepository() } - private func makeRealEmojiUseCase() -> RealEmojiUseCaseProtocol { - return RealEmojiUseCase(realEmojiRepository: makeRealEmojiRepository()) + private func makeCreateRealEmojiUseCase() -> CreateRealEmojiUseCaseProtocol { + return CreateRealEmojiUseCase(realEmojiRepository: makeRealEmojiRepository()) + } + + private func makeRemoveRealEmojiUseCase() -> RemoveRealEmojiUseCaseProtocol { + return RemoveRealEmojiUseCase(realEmojiRepository: makeRealEmojiRepository()) + } + + private func makeFetchRealEmojiListUseCase() -> FetchRealEmojiListUseCaseProtocol { + return FetchRealEmojiListUseCase(realEmojiRepository: makeRealEmojiRepository()) } } @@ -53,7 +69,15 @@ extension ReactionDIContainer { return ReactionRepository() } - private func makeEmojiUseCase() -> ReactionUseCaseProtocol { - return ReactionUseCase(reactionRepository: makeReactionRepository()) + private func makeCreateReactionUseCase() -> CreateReactionUseCaseProtocol { + return CreateReactionUseCase(reactionRepository: makeReactionRepository()) + } + + private func makeRemoveReactionUseCase() -> RemoveReactionUseCaseProtocol { + return RemoveReactionUseCase(reactionRepository: makeReactionRepository()) + } + + private func makeFetchReactionListUseCase() -> FetchReactionListUseCaseProtocol { + return FetchReactionListUseCase(reactionRepository: makeReactionRepository()) } } diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/ReactionMemberDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/ReactionMemberDIContainer.swift index ad424fb11..f28ceec6b 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/ReactionMemberDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/ReactionMemberDIContainer.swift @@ -22,11 +22,11 @@ final class ReactionMemberDIContainer { return FamilyUseCase(familyRepository: makeFamilyRepository()) } - func makeReactionMemberReactor(emojiData: FetchedEmojiData) -> ReactionMemberViewReactor { + func makeReactionMemberReactor(emojiData: RealEmojiEntity) -> ReactionMemberViewReactor { return ReactionMemberViewReactor(initialState: .init(emojiData: emojiData), familyUseCase: makeFamilyUseCase()) } - func makeViewController(emojiData: FetchedEmojiData) -> ReactionMembersViewController { + func makeViewController(emojiData: RealEmojiEntity) -> ReactionMembersViewController { return ReactionMembersViewController(reactor: makeReactionMemberReactor(emojiData: emojiData)) } } diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/SelectableEmojiDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/SelectableEmojiDIContainer.swift index 0048f18a9..200b2e2ac 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/SelectableEmojiDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/SelectableEmojiDIContainer.swift @@ -15,7 +15,11 @@ import RxSwift final class SelectableEmojiDIContainer { private func makeReactor(postId: String) -> SelectableEmojiReactor { - return SelectableEmojiReactor(postId: postId, emojiRepository: makeEmojiUseCase(), realEmojiRepository: makeRealEmojiUseCase()) + return SelectableEmojiReactor( + postId: postId, + createReactionUseCase: makeCreateReactionUseCase(), + createRealEmojiUseCase: makeCreateRealEmojiUseCase(), + fetchMyRealEmojiUseCase: makeFetchMyRealEmojiUseCase()) } func makeViewController(postId: String, subject: PublishSubject) -> SelectableEmojiViewController { @@ -28,8 +32,12 @@ extension SelectableEmojiDIContainer { return RealEmojiRepository() } - private func makeRealEmojiUseCase() -> RealEmojiUseCaseProtocol { - return RealEmojiUseCase(realEmojiRepository: makeRealEmojiRepository()) + private func makeCreateRealEmojiUseCase() -> CreateRealEmojiUseCaseProtocol { + return CreateRealEmojiUseCase(realEmojiRepository: makeRealEmojiRepository()) + } + + private func makeFetchMyRealEmojiUseCase() -> FetchMyRealEmojiUseCaseProtocol { + return FetchMyRealEmojiUseCase(realEmojiRepository: makeRealEmojiRepository()) } } @@ -38,7 +46,7 @@ extension SelectableEmojiDIContainer { return ReactionRepository() } - private func makeEmojiUseCase() -> ReactionUseCaseProtocol { - return ReactionUseCase(reactionRepository: makeReactionRepository()) + private func makeCreateReactionUseCase() -> CreateReactionUseCaseProtocol { + return CreateReactionUseCase(reactionRepository: makeReactionRepository()) } } diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Model/ReactionSection.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Model/ReactionSection.swift index 54288cd80..dad21c137 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Model/ReactionSection.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Model/ReactionSection.swift @@ -18,7 +18,7 @@ struct ReactionSection { enum Item { case addComment(Int) case addReaction - case main(FetchedEmojiData) + case main(RealEmojiEntity) } } @@ -33,7 +33,7 @@ struct SelectableReactionSection { enum Item { case standard(Emojis) - case realEmoji(MyRealEmoji?) + case realEmoji(MyRealEmojiEntity?) } } diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Model/SectionOfPostDetail.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Model/SectionOfPostDetail.swift index 33a91f87e..927f4072c 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Model/SectionOfPostDetail.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Model/SectionOfPostDetail.swift @@ -9,21 +9,21 @@ import Domain import RxDataSources -typealias PostListSectionModel = SectionModel - -struct SectionOfPostDetail { - var items: [PostData] - - init(items: [PostData]) { - self.items = items - } -} - -extension SectionOfPostDetail: SectionModelType { - typealias Item = PostData - - init(original: SectionOfPostDetail, items: [PostData]) { - self = original - self.items = items - } -} +typealias PostListSectionModel = SectionModel +// +//struct SectionOfPostDetail { +// var items: [BriefPostEntity] +// +// init(items: [BriefPostEntity]) { +// self.items = items +// } +//} +// +//extension SectionOfPostDetail: SectionModelType { +// typealias Item = BriefPostEntity +// +// init(original: SectionOfPostDetail, items: [BriefPostEntity]) { +// self = original +// self.items = items +// } +//} diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostDetailViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostDetailViewReactor.swift index 87721d6df..dd6f9b692 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostDetailViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostDetailViewReactor.swift @@ -29,7 +29,7 @@ final class PostDetailViewReactor: Reactor { struct State { let type: CellType - let post: PostListData + let post: PostEntity var missionContent: String = "" diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostReactor.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostReactor.swift index 623836d7c..8d97a757a 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostReactor.swift @@ -30,10 +30,9 @@ final class PostReactor: Reactor { let originPostLists: PostSection.Model var isPop: Bool = false - var selectedPost: PostListData = .init(postId: "", author: .init(memberId: "", profileImageURL: "", name: ""), commentCount: 0, emojiCount: 0, imageURL: "", content: "", time: "") + var selectedPost: PostEntity = .init(postId: "", author: .init(memberId: "", profileImageURL: "", name: ""), commentCount: 0, emojiCount: 0, imageURL: "", content: "", time: "") @Pulse var missionContent: MissionContentResponse? = nil - @Pulse var fetchedPost: PostData? = nil @Pulse var reactionMemberIds: [String] = [] @Pulse var shouldPushProfileViewController: String? @@ -42,22 +41,16 @@ final class PostReactor: Reactor { let initialState: State - let realEmojiRepository: RealEmojiUseCaseProtocol - let emojiRepository: ReactionUseCaseProtocol let missionUseCase: MissionContentUseCaseProtocol let provider: GlobalStateProviderProtocol init( provider: GlobalStateProviderProtocol, - realEmojiRepository: RealEmojiUseCaseProtocol, - emojiRepository: ReactionUseCaseProtocol, missionUseCase: MissionContentUseCaseProtocol, initialState: State ) { self.provider = provider - self.realEmojiRepository = realEmojiRepository - self.emojiRepository = emojiRepository self.missionUseCase = missionUseCase self.initialState = initialState } diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/ReactionMemberViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/ReactionMemberViewReactor.swift index 29f2c51c3..6499e44eb 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/ReactionMemberViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/ReactionMemberViewReactor.swift @@ -22,7 +22,7 @@ final class ReactionMemberViewReactor: Reactor { } struct State { - let emojiData: FetchedEmojiData + let emojiData: RealEmojiEntity var memberDataSource: [FamilyMemberProfileSectionModel] = [] } diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/ReactionViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/ReactionViewReactor.swift index 788596c8d..f5bc454f7 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/ReactionViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/ReactionViewReactor.swift @@ -18,19 +18,19 @@ final class ReactionViewReactor: Reactor { case tapComment case tapAddEmoji case longPressEmoji(IndexPath) - case selectCell(IndexPath, FetchedEmojiData) - case acceptPostListData(PostListData) + case selectCell(IndexPath, RealEmojiEntity) + case acceptPostListData(PostEntity) case fetchReactionList(String) } enum Mutation { - case setSpecificCell(IndexPath, FetchedEmojiData) + case setSpecificCell(IndexPath, RealEmojiEntity) case setSelectedReactionIndices([Int]) case setCommentSheet case setEmojiSheet - case setReactionMemberSheetEmoji(FetchedEmojiData) + case setReactionMemberSheetEmoji(RealEmojiEntity) case updateDataSource([ReactionSection.Item]) - case setPost(PostListData) + case setPost(PostEntity) case setPostCommentCount(Int) case setInitialDataSource case setSelectionHapticFeedback @@ -38,7 +38,7 @@ final class ReactionViewReactor: Reactor { struct State { let type: ReactionType - var postListData: PostListData + var postListData: PostEntity var deSelectedReactionIndicies: [Int] = [] var selectedReactionIndicies: [Int] = [] @@ -47,7 +47,7 @@ final class ReactionViewReactor: Reactor { .addReaction, ]) - @Pulse var reactionMemberSheetEmoji: FetchedEmojiData? = nil + @Pulse var reactionMemberSheetEmoji: RealEmojiEntity? = nil @Pulse var isShowingCommentSheet: Bool = false @Pulse var isShowingEmojiSheet: Bool = false @Pulse var selectionHapticFeedback: Bool = false @@ -55,19 +55,28 @@ final class ReactionViewReactor: Reactor { let initialState: State let provider: GlobalStateProviderProtocol - let emojiRepository: ReactionUseCaseProtocol - let realEmojiRepository: RealEmojiUseCaseProtocol + let fetchReactionListUseCase: FetchReactionListUseCaseProtocol + let createReactionUseCase: CreateReactionUseCaseProtocol + let removeReactionUseCase: RemoveReactionUseCaseProtocol + let fetchRealEmojiListUseCase: FetchRealEmojiListUseCaseProtocol + let createRealEmojiUseCase: CreateRealEmojiUseCaseProtocol + let removeRealEmojiUseCase: RemoveRealEmojiUseCaseProtocol - init( - provider: GlobalStateProviderProtocol, - initialState: State, - emojiRepository: ReactionUseCaseProtocol, - realEmojiRepository: RealEmojiUseCaseProtocol - ) { + init(initialState: State, provider: GlobalStateProviderProtocol, + fetchReactionUseCase: FetchReactionListUseCaseProtocol, + createReactionUseCase: CreateReactionUseCaseProtocol, + removeReactionUseCase: RemoveReactionUseCaseProtocol, + fetchRealEmojiListUseCase: FetchRealEmojiListUseCaseProtocol, + createRealEmojiUseCase: CreateRealEmojiUseCaseProtocol, + removeRealEmojiUseCase: RemoveRealEmojiUseCaseProtocol) { self.initialState = initialState self.provider = provider - self.emojiRepository = emojiRepository - self.realEmojiRepository = realEmojiRepository + self.fetchReactionListUseCase = fetchReactionUseCase + self.createReactionUseCase = createReactionUseCase + self.removeReactionUseCase = removeReactionUseCase + self.fetchRealEmojiListUseCase = fetchRealEmojiListUseCase + self.createRealEmojiUseCase = createRealEmojiUseCase + self.removeRealEmojiUseCase = removeRealEmojiUseCase } } @@ -75,8 +84,8 @@ extension ReactionViewReactor { func mutate(action: Action) -> Observable { switch action { case .fetchReactionList(let postId): - let repository1 = emojiRepository.execute(query: .init(postId: postId)).asObservable() - let repository2 = realEmojiRepository.execute(query: .init(postId: postId)).asObservable() + let repository1 = fetchReactionListUseCase.execute(query: .init(postId: postId)).asObservable() + let repository2 = fetchRealEmojiListUseCase.execute(query: .init(postId: postId)).asObservable() return Observable.combineLatest(repository1, repository2) .flatMap { response1, response2 in @@ -169,20 +178,20 @@ extension ReactionViewReactor { } extension ReactionViewReactor { - func handleSelectCell(postId: String, indexPath: IndexPath, data: FetchedEmojiData, isAdd: Bool) -> Observable { + func handleSelectCell(postId: String, indexPath: IndexPath, data: RealEmojiEntity, isAdd: Bool) -> Observable { let executeObservable: Observable if data.isStandard { if data.isSelfSelected { - executeObservable = emojiRepository.excuteRemoveEmoji(query: .init(postId: postId), body: .init(content: data.emojiType)).asObservable() + executeObservable = removeReactionUseCase.execute(query: .init(postId: postId), body: .init(content: data.emojiType)).asObservable() } else { - executeObservable = emojiRepository.executeAddEmoji(query: .init(postId: postId), body: .init(content: data.emojiType.emojiString)).asObservable() + executeObservable = createReactionUseCase.execute(query: .init(postId: postId), body: .init(content: data.emojiType.emojiString)).asObservable() } } else { if data.isSelfSelected { - executeObservable = realEmojiRepository.execute(query: .init(postId: postId, realEmojiId: data.realEmojiId)).asObservable() + executeObservable = removeRealEmojiUseCase.execute(query: .init(postId: postId, realEmojiId: data.realEmojiId)).asObservable() } else { - executeObservable = realEmojiRepository.execute(query: .init(postId: postId), body: .init(content: data.realEmojiId)).asObservable() + executeObservable = createRealEmojiUseCase.execute(query: .init(postId: postId), body: .init(content: data.realEmojiId)).asObservable() } } diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/SelectableEmojiReactor.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/SelectableEmojiReactor.swift index ad59344ea..6cf008418 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/SelectableEmojiReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/SelectableEmojiReactor.swift @@ -16,34 +16,39 @@ final class SelectableEmojiReactor: Reactor { enum Action { case loadMyRealEmoji case selectStandard(Emojis) - case selectRealEmoji(IndexPath, MyRealEmoji?) + case selectRealEmoji(IndexPath, MyRealEmojiEntity?) } enum Mutation { case setSelectedStandard(Emojis) - case setSelectedRealEmoji(MyRealEmoji) + case setSelectedRealEmoji(MyRealEmojiEntity) case setStandardSection([Emojis]) - case setRealEmojiSection([MyRealEmoji?]) + case setRealEmojiSection([MyRealEmojiEntity?]) case showCamera(Int) } struct State { var reactionSections: [SelectableReactionSection.Model] = [] var selectedStandard: Set = [] - var selectedRealEmoji: Set = [] + var selectedRealEmoji: Set = [] @Pulse var isShowingCamera: Int = 0 } let postId: String - let emojiRepository: ReactionUseCaseProtocol - let realEmojiRepository: RealEmojiUseCaseProtocol - var initialState: State = State() + var initialState: State + let createReactionUseCase: CreateReactionUseCaseProtocol + let createRealEmojiUseCase: CreateRealEmojiUseCaseProtocol + let fetchMyRealEmojiUseCase: FetchMyRealEmojiUseCaseProtocol - init(postId: String, emojiRepository: ReactionUseCaseProtocol, realEmojiRepository: RealEmojiUseCaseProtocol) { + init(postId: String, + createReactionUseCase: CreateReactionUseCaseProtocol, + createRealEmojiUseCase: CreateRealEmojiUseCaseProtocol, + fetchMyRealEmojiUseCase: FetchMyRealEmojiUseCaseProtocol) { self.postId = postId - self.emojiRepository = emojiRepository - self.realEmojiRepository = realEmojiRepository + self.createReactionUseCase = createReactionUseCase + self.createRealEmojiUseCase = createRealEmojiUseCase + self.fetchMyRealEmojiUseCase = fetchMyRealEmojiUseCase let section1 = SelectableReactionSection.Model(model: 0, items: []) let section2 = SelectableReactionSection.Model(model: 1, items: []) self.initialState = State(reactionSections: [section1, section2]) @@ -54,29 +59,29 @@ extension SelectableEmojiReactor { func mutate(action: Action) -> Observable { switch action { case let .selectStandard(emoji): - let query = AddEmojiQuery(postId: self.postId) - let body = AddEmojiBody(content: emoji.emojiString) + let query = CreateReactionQuery(postId: self.postId) + let body = CreateReactionRequest(content: emoji.emojiString) - return emojiRepository.executeAddEmoji(query: query, body: body) + return createReactionUseCase.execute(query: query, body: body) .asObservable() .flatMap {_ in return Observable.just(Mutation.setSelectedStandard(emoji)) } case let .selectRealEmoji(indexPath, emoji): guard let emoji else { return Observable.just(Mutation.showCamera(indexPath.row)) } - let query = AddEmojiQuery(postId: self.postId) - let body = AddEmojiBody(content: emoji.realEmojiId) - return realEmojiRepository.execute(query: query, body: body) + let query = CreateReactionQuery(postId: self.postId) + let body = CreateReactionRequest(content: emoji.realEmojiId) + return createRealEmojiUseCase.execute(query: query, body: body) .asObservable() .flatMap {_ in return Observable.just(Mutation.setSelectedRealEmoji(emoji)) } case .loadMyRealEmoji: let standardData: [Emojis] = Emojis.allEmojis - return realEmojiRepository.execute() + return fetchMyRealEmojiUseCase.execute() .asObservable() .flatMap { response in - let realEmojiData: [MyRealEmoji?] = response + let realEmojiData: [MyRealEmojiEntity?] = response return Observable.concat([ Observable.just(Mutation.setStandardSection(standardData)), Observable.just(Mutation.setRealEmojiSection(realEmojiData)) diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/TempCellReactor.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/TempCellReactor.swift index 1fc2b67cd..3b1adef1c 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/TempCellReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/TempCellReactor.swift @@ -24,13 +24,13 @@ final class TempCellReactor: Reactor { } struct State { - var cellData: FetchedEmojiData? + var cellData: RealEmojiEntity? } let initialState: State = State() - let items: FetchedEmojiData + let items: RealEmojiEntity - init(items: FetchedEmojiData) { + init(items: RealEmojiEntity) { self.items = items } } diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/PostViewController.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/PostViewController.swift index 4a3b94c12..131f6e297 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/PostViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/PostViewController.swift @@ -217,7 +217,7 @@ final class PostViewController: BaseViewController { } extension PostViewController { - private func setBackgroundView(data: PostListData) { + private func setBackgroundView(data: PostEntity) { guard let url = URL(string: data.imageURL) else { return } diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/ReactionViewController.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/ReactionViewController.swift index da8b7d8f2..1d081eba0 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/ReactionViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/ReactionViewController.swift @@ -15,7 +15,7 @@ import RxCocoa import RxDataSources final class ReactionViewController: BaseViewController, UICollectionViewDelegateFlowLayout { - let postListData: BehaviorRelay = BehaviorRelay(value: .init(postId: "", author: nil, commentCount: 0, emojiCount: 0, imageURL: "", content: nil, time: "")) + let postListData: BehaviorRelay = BehaviorRelay(value: .init(postId: "", author: nil, commentCount: 0, emojiCount: 0, imageURL: "", content: nil, time: "")) private let selectedReactionSubject: PublishSubject = PublishSubject() diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/PostDetailCollectionViewCell.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/PostDetailCollectionViewCell.swift index 8e00f4a76..4cf3b1e2e 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/PostDetailCollectionViewCell.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/PostDetailCollectionViewCell.swift @@ -226,7 +226,7 @@ extension PostDetailCollectionViewCell { } extension PostDetailCollectionViewCell { - private func setupProfileNameAndImage(post: PostListData) { + private func setupProfileNameAndImage(post: PostEntity) { postImageView.kf.setImage( with: URL(string: post.imageURL), options: [ @@ -283,7 +283,7 @@ extension PostDetailCollectionViewCell: UICollectionViewDelegateFlowLayout { extension PostDetailCollectionViewCell { - func setCell(data: PostListData) { + func setCell(data: PostEntity) { postImageView.kf.setImage(with: URL(string: data.imageURL)) } } diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/PostNavigationView.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/PostNavigationView.swift index 4249a74ad..32032a3ed 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/PostNavigationView.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/PostNavigationView.swift @@ -100,7 +100,7 @@ final class PostNavigationView: BaseView { } extension PostNavigationView { - private func setData(data: PostListData) { + private func setData(data: PostEntity) { if let author = data.author, let profileImageURL = author.profileImageURL, let url = URL(string: profileImageURL), !profileImageURL.isEmpty { diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/ReactionCollectionViewCell.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/ReactionCollectionViewCell.swift index 7a1c6453f..adf146474 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/ReactionCollectionViewCell.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/ReactionCollectionViewCell.swift @@ -95,7 +95,7 @@ extension ReactionCollectionViewCell { .disposed(by: disposeBag) } - private func setCell(data: FetchedEmojiData) { + private func setCell(data: RealEmojiEntity) { if data.count == 0 { return } diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/SeletableEmojiCollectionViewCell.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/SeletableEmojiCollectionViewCell.swift index 3ff3b10c5..484fd346f 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/SeletableEmojiCollectionViewCell.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/SeletableEmojiCollectionViewCell.swift @@ -56,7 +56,7 @@ extension SelectableEmojiCollectionViewCell { imageView.image = data.emojiImage } - func setData(index: Int, data: MyRealEmoji?) { + func setData(index: Int, data: MyRealEmojiEntity?) { if let data { badgeView.isHidden = false badgeView.image = data.type.emojiBadgeImage diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileFeedViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileFeedViewReactor.swift index c499b792f..33f80f7c7 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileFeedViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileFeedViewReactor.swift @@ -20,13 +20,13 @@ final class ProfileFeedViewReactor: Reactor { enum Action { case reloadFeedItems case fetchMoreFeedItems - case didTapProfileFeedItem(IndexPath, [PostListData]) + case didTapProfileFeedItem(IndexPath, [PostEntity]) } enum Mutation { case setFeedSectionItems([ProfileFeedSectionItem]) case setFeedItemPage(Int) - case setFeedPaginationItems([PostListData]) + case setFeedPaginationItems([PostEntity]) case setFeedItems(PostListPage) case setFeedDetailItem(PostSection.Model, IndexPath) } @@ -35,7 +35,7 @@ final class ProfileFeedViewReactor: Reactor { var memberId: String @Pulse var selectedIndex: IndexPath? @Pulse var feedDetailItem: PostSection.Model - @Pulse var feedPaginationItems: [PostListData] + @Pulse var feedPaginationItems: [PostEntity] @Pulse var feedPage: Int @Pulse var type: PostType @Pulse var feedItems: PostListPage? @@ -110,7 +110,7 @@ final class ProfileFeedViewReactor: Reactor { var sectionItem: [ProfileFeedSectionItem] = [] guard let entity = entity else { return .empty() } - var feedItems: [PostListData] = owner.currentState.feedPaginationItems + var feedItems: [PostEntity] = owner.currentState.feedPaginationItems feedItems.append(contentsOf: entity.postLists) feedItems.forEach { @@ -139,7 +139,7 @@ final class ProfileFeedViewReactor: Reactor { feedItems.forEach { feedDetailSection.items.append( - .main(PostListData( + .main(PostEntity( postId: $0.postId, missionId: $0.missionId, missionType: $0.missionType, From 393d795d9c65c486de066333501694bec857e19d Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Sat, 15 Jun 2024 14:36:24 +0900 Subject: [PATCH 108/263] =?UTF-8?q?refactor:=20CalendarUseCase=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20(#553)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CalendarAPI/CalendarAPIWorker.swift | 84 +++----------- ...rrayResponseDailyCalendarResponseDTO.swift | 2 +- .../Repository/CalendarRepository.swift | 5 + .../ArrayResponseDailyCalendarEntity.swift | 0 .../ArrayResponseMonthlyCalendarEntity.swift | 0 .../Calendar}/BannerEntity.swift | 0 .../FamilyMonthlyStatisticsEntity.swift | 0 .../Repositories/CalendarRepository.swift | 1 + .../FetchCalendarBannerUseCase.swift | 0 .../Calendar}/FetchDailyCalendarUseCase.swift | 0 .../FetchMonthlyCalendarUseCase.swift | 0 .../FetchStatisticsSummaryUseCase.swift | 0 .../UseCases/MainView/FetchMainUseCase.swift | 4 +- .../MainView/FetchNightMainViewUsecase.swift | 4 +- .../xcschemes/Bibbi-Workspace.xcscheme | 106 +++++++++--------- 15 files changed, 79 insertions(+), 127 deletions(-) rename 14th-team5-iOS/Domain/Sources/{Calendar/Entities => Entities/Calendar}/ArrayResponseDailyCalendarEntity.swift (100%) rename 14th-team5-iOS/Domain/Sources/{Calendar/Entities => Entities/Calendar}/ArrayResponseMonthlyCalendarEntity.swift (100%) rename 14th-team5-iOS/Domain/Sources/{Calendar/Entities => Entities/Calendar}/BannerEntity.swift (100%) rename 14th-team5-iOS/Domain/Sources/{Calendar/Entities => Entities/Calendar}/FamilyMonthlyStatisticsEntity.swift (100%) rename 14th-team5-iOS/Domain/Sources/{Calendar/Interfaces => }/Repositories/CalendarRepository.swift (90%) rename 14th-team5-iOS/Domain/Sources/{Calendar/UseCases => UseCases/Calendar}/FetchCalendarBannerUseCase.swift (100%) rename 14th-team5-iOS/Domain/Sources/{Calendar/UseCases => UseCases/Calendar}/FetchDailyCalendarUseCase.swift (100%) rename 14th-team5-iOS/Domain/Sources/{Calendar/UseCases => UseCases/Calendar}/FetchMonthlyCalendarUseCase.swift (100%) rename 14th-team5-iOS/Domain/Sources/{Calendar/UseCases => UseCases/Calendar}/FetchStatisticsSummaryUseCase.swift (100%) diff --git a/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/CalendarAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/CalendarAPIWorker.swift index 8654ddbd1..b8d52cde9 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/CalendarAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/CalendarAPIWorker.swift @@ -31,10 +31,12 @@ extension CalendarAPIWorker { // MARK: - Fetch Calendar - + @available(*, deprecated) - private func fetchCalendarResponse(spec: APISpec, headers: [APIHeader]?) -> Single { - return request(spec: spec, headers: headers) + public func fetchCalendarResponse(yearMonth: String) -> Single { + let spec = CalendarAPIs.calendarResponse(yearMonth).spec + + return request(spec: spec) .subscribe(on: Self.queue) .do { if let str = String(data: $0.1, encoding: .utf8) { @@ -43,19 +45,6 @@ extension CalendarAPIWorker { } .map(ArrayResponseCalendarResponseDTO.self) .catchAndReturn(nil) - .map { $0?.toDomain() } - .asSingle() - } - - @available(*, deprecated) - public func fetchCalendarResponse(yearMonth: String) -> Single { - let spec = CalendarAPIs.calendarResponse(yearMonth).spec - - return Observable.just(()) - .withLatestFrom(self._headers) - .observe(on: Self.queue) - .withUnretained(self) - .flatMap { $0.0.fetchCalendarResponse(spec: spec, headers: $0.1) } .asSingle() } @@ -63,19 +52,10 @@ extension CalendarAPIWorker { // MARK: - Fetch Statistics Summary - public func fetchStatisticsSummary(yearMonth: String) -> Single { + public func fetchStatisticsSummary(yearMonth: String) -> Single { let spec = CalendarAPIs.fetchStatisticsSummary(yearMonth).spec - return Observable.just(()) - .withLatestFrom(self._headers) - .observe(on: Self.queue) - .withUnretained(self) - .flatMap { $0.0.fetchStatisticsSummary(spec: spec, headers: $0.1) } - .asSingle() - } - - private func fetchStatisticsSummary(spec: APISpec, headers: [APIHeader]?) -> Single { - return request(spec: spec, headers: headers) + return request(spec: spec) .subscribe(on: Self.queue) .do { if let str = String(data: $0.1, encoding: .utf8) { @@ -84,7 +64,6 @@ extension CalendarAPIWorker { } .map(FamilyMonthlyStatisticsResponseDTO.self) .catchAndReturn(nil) - .map { $0?.toDomain() } .asSingle() } @@ -92,20 +71,10 @@ extension CalendarAPIWorker { // MARK: - Fetch Monthly Calendar - public func fetchMonthlyCalendar(yearMonth: String) -> Single { + public func fetchMonthlyCalendar(yearMonth: String) -> Single { let spec = CalendarAPIs.fetchMonthlyCalendar(yearMonth).spec - return Observable.just(()) - .withLatestFrom(self._headers) - .observe(on: Self.queue) - .withUnretained(self) - .flatMap { $0.0.fetchMonthlyCalendar(spec: spec, headers: $0.1) } - .asSingle() - } - - - private func fetchMonthlyCalendar(spec: APISpec, headers: [APIHeader]?) -> Single { - return request(spec: spec, headers: headers) + return request(spec: spec) .subscribe(on: Self.queue) .do { if let str = String(data: $0.1, encoding: .utf8) { @@ -114,7 +83,6 @@ extension CalendarAPIWorker { } .map(ArrayResponseMonthlyCalendarResponseDTO.self) .catchAndReturn(nil) - .map { $0?.toDomain() } .asSingle() } @@ -122,20 +90,10 @@ extension CalendarAPIWorker { // MARK: - Fetch Daily Calendar - public func fetchDailyCalendar(yearMonthDay: String) -> Single { + public func fetchDailyCalendar(yearMonthDay: String) -> Single { let spec = CalendarAPIs.fetchDailyCalendar(yearMonthDay).spec - return Observable.just(()) - .withLatestFrom(self._headers) - .observe(on: Self.queue) - .withUnretained(self) - .flatMap { $0.0.fetchDailyCalendar(spec: spec, headers: $0.1) } - .asSingle() - } - - - private func fetchDailyCalendar(spec: APISpec, headers: [APIHeader]?) -> Single { - return request(spec: spec, headers: headers) + return request(spec: spec) .subscribe(on: Self.queue) .do { if let str = String(data: $0.1, encoding: .utf8) { @@ -144,7 +102,6 @@ extension CalendarAPIWorker { } .map(ArrayResponseDailyCalendarResponseDTO.self) .catchAndReturn(nil) - .map { $0?.toDomain() } .asSingle() } @@ -153,8 +110,10 @@ extension CalendarAPIWorker { // MARK: - Fetch Banner - private func fetchCalendarBanner(spec: APISpec, headers: [APIHeader]?) -> Single { - return request(spec: spec, headers: headers) + public func fetchCalendarBanner(yearMonth: String) -> Single { + let spec = CalendarAPIs.fetchBanner(yearMonth).spec + + return request(spec: spec) .subscribe(on: Self.queue) .do { if let str = String(data: $0.1, encoding: .utf8) { @@ -163,20 +122,7 @@ extension CalendarAPIWorker { } .map(BannerResponseDTO.self) .catchAndReturn(nil) - .map { $0?.toDomain() } .asSingle() } - public func fetchCalendarBanner(yearMonth: String) -> Single { - let spec = CalendarAPIs.fetchBanner(yearMonth).spec - - return Observable.just(()) - .withLatestFrom(self._headers) - .observe(on: Self.queue) - .withUnretained(self) - .flatMap { $0.0.fetchCalendarBanner(spec: spec, headers: $0.1) } - .asSingle() - } - - } diff --git a/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/DataMapping/ArrayResponseDailyCalendarResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/DataMapping/ArrayResponseDailyCalendarResponseDTO.swift index 300423404..a7da95b46 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/DataMapping/ArrayResponseDailyCalendarResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/DataMapping/ArrayResponseDailyCalendarResponseDTO.swift @@ -8,7 +8,7 @@ import Domain import Foundation -struct ArrayResponseDailyCalendarResponseDTO: Decodable { +public struct ArrayResponseDailyCalendarResponseDTO: Decodable { enum CodingKeys: String, CodingKey { case results } diff --git a/14th-team5-iOS/Data/Sources/APIs/Calendar/Repository/CalendarRepository.swift b/14th-team5-iOS/Data/Sources/APIs/Calendar/Repository/CalendarRepository.swift index 276571f96..8e81011a6 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Calendar/Repository/CalendarRepository.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Calendar/Repository/CalendarRepository.swift @@ -31,27 +31,32 @@ extension CalendarRepository { @available(*, deprecated) public func fetchCalendarResponse(yearMonth: String) -> Observable { return calendarApiWorker.fetchCalendarResponse(yearMonth: yearMonth) + .map { $0?.toDomain() } .asObservable() } public func fetchMonthyCalendarResponse(yearMonth: String) -> Observable { return calendarApiWorker.fetchMonthlyCalendar(yearMonth: yearMonth) + .map { $0?.toDomain() } .asObservable() } public func fetchDailyCalendarResponse(yearMonthDay: String) -> Observable { return calendarApiWorker.fetchDailyCalendar(yearMonthDay: yearMonthDay) + .map { $0?.toDomain() } .asObservable() } public func fetchStatisticsSummary(yearMonth: String) -> Observable { return calendarApiWorker.fetchStatisticsSummary(yearMonth: yearMonth) + .map { $0?.toDomain() } .asObservable() } public func fetchCalendarBanner(yearMonth: String) -> Observable { return calendarApiWorker.fetchCalendarBanner(yearMonth: yearMonth) + .map { $0?.toDomain() } .asObservable() } diff --git a/14th-team5-iOS/Domain/Sources/Calendar/Entities/ArrayResponseDailyCalendarEntity.swift b/14th-team5-iOS/Domain/Sources/Entities/Calendar/ArrayResponseDailyCalendarEntity.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Calendar/Entities/ArrayResponseDailyCalendarEntity.swift rename to 14th-team5-iOS/Domain/Sources/Entities/Calendar/ArrayResponseDailyCalendarEntity.swift diff --git a/14th-team5-iOS/Domain/Sources/Calendar/Entities/ArrayResponseMonthlyCalendarEntity.swift b/14th-team5-iOS/Domain/Sources/Entities/Calendar/ArrayResponseMonthlyCalendarEntity.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Calendar/Entities/ArrayResponseMonthlyCalendarEntity.swift rename to 14th-team5-iOS/Domain/Sources/Entities/Calendar/ArrayResponseMonthlyCalendarEntity.swift diff --git a/14th-team5-iOS/Domain/Sources/Calendar/Entities/BannerEntity.swift b/14th-team5-iOS/Domain/Sources/Entities/Calendar/BannerEntity.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Calendar/Entities/BannerEntity.swift rename to 14th-team5-iOS/Domain/Sources/Entities/Calendar/BannerEntity.swift diff --git a/14th-team5-iOS/Domain/Sources/Calendar/Entities/FamilyMonthlyStatisticsEntity.swift b/14th-team5-iOS/Domain/Sources/Entities/Calendar/FamilyMonthlyStatisticsEntity.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Calendar/Entities/FamilyMonthlyStatisticsEntity.swift rename to 14th-team5-iOS/Domain/Sources/Entities/Calendar/FamilyMonthlyStatisticsEntity.swift diff --git a/14th-team5-iOS/Domain/Sources/Calendar/Interfaces/Repositories/CalendarRepository.swift b/14th-team5-iOS/Domain/Sources/Repositories/CalendarRepository.swift similarity index 90% rename from 14th-team5-iOS/Domain/Sources/Calendar/Interfaces/Repositories/CalendarRepository.swift rename to 14th-team5-iOS/Domain/Sources/Repositories/CalendarRepository.swift index 5fe518744..9feabe19e 100644 --- a/14th-team5-iOS/Domain/Sources/Calendar/Interfaces/Repositories/CalendarRepository.swift +++ b/14th-team5-iOS/Domain/Sources/Repositories/CalendarRepository.swift @@ -12,6 +12,7 @@ import RxSwift public protocol CalendarRepositoryProtocol { var disposeBag: DisposeBag { get } + @available(*, deprecated, renamed: "fetchMonthlyCalendarResponse") func fetchCalendarResponse(yearMonth: String) -> Observable func fetchMonthyCalendarResponse(yearMonth: String) -> Observable diff --git a/14th-team5-iOS/Domain/Sources/Calendar/UseCases/FetchCalendarBannerUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Calendar/FetchCalendarBannerUseCase.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Calendar/UseCases/FetchCalendarBannerUseCase.swift rename to 14th-team5-iOS/Domain/Sources/UseCases/Calendar/FetchCalendarBannerUseCase.swift diff --git a/14th-team5-iOS/Domain/Sources/Calendar/UseCases/FetchDailyCalendarUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Calendar/FetchDailyCalendarUseCase.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Calendar/UseCases/FetchDailyCalendarUseCase.swift rename to 14th-team5-iOS/Domain/Sources/UseCases/Calendar/FetchDailyCalendarUseCase.swift diff --git a/14th-team5-iOS/Domain/Sources/Calendar/UseCases/FetchMonthlyCalendarUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Calendar/FetchMonthlyCalendarUseCase.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Calendar/UseCases/FetchMonthlyCalendarUseCase.swift rename to 14th-team5-iOS/Domain/Sources/UseCases/Calendar/FetchMonthlyCalendarUseCase.swift diff --git a/14th-team5-iOS/Domain/Sources/Calendar/UseCases/FetchStatisticsSummaryUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Calendar/FetchStatisticsSummaryUseCase.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Calendar/UseCases/FetchStatisticsSummaryUseCase.swift rename to 14th-team5-iOS/Domain/Sources/UseCases/Calendar/FetchStatisticsSummaryUseCase.swift diff --git a/14th-team5-iOS/Domain/Sources/UseCases/MainView/FetchMainUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/MainView/FetchMainUseCase.swift index 5c6b1d398..3e881c69a 100644 --- a/14th-team5-iOS/Domain/Sources/UseCases/MainView/FetchMainUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/MainView/FetchMainUseCase.swift @@ -14,9 +14,9 @@ public protocol FetchMainUseCaseProtocol { } public final class FetchMainUseCase: FetchMainUseCaseProtocol { - private let mainRepository: MainRepositoryProtocol + private let mainRepository: MainViewRepositoryProtocol - public init(mainRepository: MainRepositoryProtocol) { + public init(mainRepository: MainViewRepositoryProtocol) { self.mainRepository = mainRepository } diff --git a/14th-team5-iOS/Domain/Sources/UseCases/MainView/FetchNightMainViewUsecase.swift b/14th-team5-iOS/Domain/Sources/UseCases/MainView/FetchNightMainViewUsecase.swift index d5751b982..2ed78b6d8 100644 --- a/14th-team5-iOS/Domain/Sources/UseCases/MainView/FetchNightMainViewUsecase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/MainView/FetchNightMainViewUsecase.swift @@ -14,9 +14,9 @@ public protocol FetchNightMainViewUseCaseProtocol { } public final class FetchNightMainViewUseCase: FetchNightMainViewUseCaseProtocol { - private let mainRepository: MainRepositoryProtocol + private let mainRepository: MainViewRepositoryProtocol - public init(mainRepository: MainRepositoryProtocol) { + public init(mainRepository: MainViewRepositoryProtocol) { self.mainRepository = mainRepository } diff --git a/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme b/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme index 7023994af..5f307e6b4 100644 --- a/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme +++ b/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme @@ -280,10 +280,10 @@ buildForAnalyzing = "YES"> + BlueprintIdentifier = "7669601E3503153E790457BC" + BuildableName = "Firebase_FirebaseCore.bundle" + BlueprintName = "Firebase_FirebaseCore" + ReferencedContainer = "container:Tuist/Dependencies/SwiftPackageManager/.build/checkouts/firebase-ios-sdk/Firebase.xcodeproj"> + BlueprintIdentifier = "C241B5678162C4E6FDA163B6" + BuildableName = "Firebase_FirebaseCoreInternal.bundle" + BlueprintName = "Firebase_FirebaseCoreInternal" + ReferencedContainer = "container:Tuist/Dependencies/SwiftPackageManager/.build/checkouts/firebase-ios-sdk/Firebase.xcodeproj"> + + + + + + + + + + + + @@ -426,34 +468,6 @@ ReferencedContainer = "container:Tuist/Dependencies/SwiftPackageManager/.build/checkouts/GoogleUtilities/GoogleUtilities.xcodeproj"> - - - - - - - - - - - - Date: Sat, 15 Jun 2024 15:25:37 +0900 Subject: [PATCH 109/263] =?UTF-8?q?refactor:=20FamilyUseCase=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20(#553)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DailyCalendarViewController.swift | 2 +- .../FamilyMemberProfileCellReactor.swift | 2 +- .../Reactor/Cell/MainFamilyCellReactor.swift | 4 +- .../MainFamilyViewController.swift | 2 +- .../Reactor/InputFamilyLinkReactor.swift | 2 +- .../Reactor/JoinFamilyReactor.swift | 2 +- .../Reactor/ReactionMemberViewReactor.swift | 6 +- .../Reactor/ProfileFeedViewReactor.swift | 2 +- .../DataMapping/CreateFamilyResponseDTO.swift | 2 +- .../FamilyCreatedAtResponseDTO.swift | 2 +- .../FamilyInvitationLinkResponseDTO.swift | 2 +- .../FamilyMemberProfileResponseDTO.swift | 4 +- .../DataMapping/JoinFamilyResponseDTO.swift | 4 +- .../Family/FamilyAPI/FamilyAPIWorker.swift | 115 ++++-------------- .../APIs/Family/FamilyAPI/FamilyAPIs.swift | 4 +- .../Family/Repository/FamilyRepository.swift | 58 ++++----- .../DataMapping/MainResponseDTO.swift | 2 +- .../DataMapping/DefaultResponseDTO.swift | 2 +- .../APIs/OAuth/OAuthAPI/OAuthAPIWorker.swift | 4 +- .../OAuthAPI/Repository/OAuthRepository.swift | 4 +- .../AccountRepository/AccountRepository.swift | 2 +- .../Sources/Account/MeAPI/MeAPIWorker.swift | 4 +- .../MembersProfileResponseDTO.swift | 2 +- .../UserDefaults/FamilyUserDefautls.swift | 14 +-- .../ArrayResponseCalendarEntity.swift | 0 .../Family/CreateFamilyEntity.swift} | 2 +- .../Family/FamilyCreatedAtEntity.swift} | 2 +- .../Family/FamilyInvitationLinkEntity.swift} | 2 +- .../Family/FamilyMemberProfileEntity.swift} | 8 +- .../Family}/FamilyPaginationQuery.swift | 0 .../Family/JoinFamilyEntity.swift} | 2 +- .../Family}/JoinFamilyRequest.swift | 0 .../Entities/MainView/MainViewEntity.swift | 4 +- .../MainView/NightMainViewEntity.swift | 4 +- .../Entities/PostList/PostEntity.swift | 4 +- .../Repositories/FamilyRepository.swift | 23 ---- .../Repositories/JoinFamilyRepository.swift | 2 +- .../Family/UseCases/FamilyUseCase.swift | 35 +++--- ...sponseEntity.swift => DefaultEntity.swift} | 2 +- .../Repository/OAuthRepository.swift | 4 +- .../Repositories/FamilyRepository.swift | 22 ++++ .../UseCases/Family/CreateFamilyUseCase.swift | 30 +++++ .../Family/FetchFamilyCreatedAtUseCase.swift | 30 +++++ ...FetchFamilyMembersFromStorageUseCase.swift | 31 +++++ .../Family/FetchFamilyMembersUseCase.swift | 30 +++++ .../Family/FetchInvitationLinkUseCase.swift | 30 +++++ .../UseCases/Family/JoinFamilyUseCase.swift | 30 +++++ .../UseCases/Family/ResignFamilyUseCase.swift | 30 +++++ 48 files changed, 359 insertions(+), 215 deletions(-) rename 14th-team5-iOS/Domain/Sources/{Calendar/Entities => Entities/Calendar}/ArrayResponseCalendarEntity.swift (100%) rename 14th-team5-iOS/Domain/Sources/{Family/Entities/CreateFamilyResponse.swift => Entities/Family/CreateFamilyEntity.swift} (88%) rename 14th-team5-iOS/Domain/Sources/{Family/Entities/FamilyCreatedAtResponse.swift => Entities/Family/FamilyCreatedAtEntity.swift} (84%) rename 14th-team5-iOS/Domain/Sources/{Family/Entities/FamilyInvitationLinkResponse.swift => Entities/Family/FamilyInvitationLinkEntity.swift} (81%) rename 14th-team5-iOS/Domain/Sources/{Family/Entities/FamilyMemberProfileResponse.swift => Entities/Family/FamilyMemberProfileEntity.swift} (79%) rename 14th-team5-iOS/Domain/Sources/{Family/Entities => Entities/Family}/FamilyPaginationQuery.swift (100%) rename 14th-team5-iOS/Domain/Sources/{Family/Entities/JoinFamilyResponse.swift => Entities/Family/JoinFamilyEntity.swift} (89%) rename 14th-team5-iOS/Domain/Sources/{Family/Entities => Entities/Family}/JoinFamilyRequest.swift (100%) delete mode 100644 14th-team5-iOS/Domain/Sources/Family/Interfaces/Repositories/FamilyRepository.swift rename 14th-team5-iOS/Domain/Sources/OAuth/Entities/{DefaultResponseEntity.swift => DefaultEntity.swift} (84%) create mode 100644 14th-team5-iOS/Domain/Sources/Repositories/FamilyRepository.swift create mode 100644 14th-team5-iOS/Domain/Sources/UseCases/Family/CreateFamilyUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/UseCases/Family/FetchFamilyCreatedAtUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/UseCases/Family/FetchFamilyMembersFromStorageUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/UseCases/Family/FetchFamilyMembersUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/UseCases/Family/FetchInvitationLinkUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/UseCases/Family/JoinFamilyUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/UseCases/Family/ResignFamilyUseCase.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/DailyCalendarViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/DailyCalendarViewController.swift index 71357d870..1dfb3bfe0 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/DailyCalendarViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/DailyCalendarViewController.swift @@ -195,7 +195,7 @@ public final class DailyCalendarViewController: BBNavigationViewController ProfileData? in + .compactMap { item -> FamilyMemberProfileEntity? in switch item { case let .main(reactor): return reactor.currentState.profileData } diff --git a/14th-team5-iOS/App/Sources/Presentation/JoinFamily/Reactor/InputFamilyLinkReactor.swift b/14th-team5-iOS/App/Sources/Presentation/JoinFamily/Reactor/InputFamilyLinkReactor.swift index eb6e1700e..78cf701ec 100644 --- a/14th-team5-iOS/App/Sources/Presentation/JoinFamily/Reactor/InputFamilyLinkReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/JoinFamily/Reactor/InputFamilyLinkReactor.swift @@ -60,7 +60,7 @@ extension InputFamilyLinkReactor { let code = currentState.linkString let body = JoinFamilyRequest(inviteCode: String(code)) - let commonLogic: (JoinFamilyResponse) -> Observable = { joinFamilyData in + let commonLogic: (JoinFamilyEntity) -> Observable = { joinFamilyData in // App.Repository.member.familyId.accept(joinFamilyData.familyId) // App.Repository.member.familyCreatedAt.accept(joinFamilyData.createdAt) return Observable.just(Mutation.setShowHome(true)) diff --git a/14th-team5-iOS/App/Sources/Presentation/JoinFamily/Reactor/JoinFamilyReactor.swift b/14th-team5-iOS/App/Sources/Presentation/JoinFamily/Reactor/JoinFamilyReactor.swift index 849bb5a97..119caed41 100644 --- a/14th-team5-iOS/App/Sources/Presentation/JoinFamily/Reactor/JoinFamilyReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/JoinFamily/Reactor/JoinFamilyReactor.swift @@ -49,7 +49,7 @@ extension JoinFamilyReactor { case .makeFamily: return familyUseCase.executeCreateFamily() .flatMap { - guard let familyResponse: CreateFamilyResponse = $0 else { + guard let familyResponse: CreateFamilyEntity = $0 else { return Observable.just(Mutation.setShowHome(false)) } // App.Repository.member.familyCreatedAt.accept(familyResponse.createdAt) diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/ReactionMemberViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/ReactionMemberViewReactor.swift index 6499e44eb..fd3fb32e9 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/ReactionMemberViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/ReactionMemberViewReactor.swift @@ -39,11 +39,11 @@ extension ReactionMemberViewReactor { func mutate(action: Action) -> Observable { switch action { case .makeDataSource: - let profiles: [ProfileData] = familyUseCase.executeFetchPaginationFamilyMembers(memberIds: currentState.emojiData.memberIds) + let profiles: [FamilyMemberProfileEntity] = familyUseCase.executeFetchPaginationFamilyMembers(memberIds: currentState.emojiData.memberIds) var items: [FamilyMemberProfileCellReactor] = [] profiles.forEach { - let member = ProfileData( + let member = FamilyMemberProfileEntity( memberId: $0.memberId, profileImageURL: $0.profileImageURL, name: $0.name @@ -54,7 +54,7 @@ extension ReactionMemberViewReactor { if profiles.count != currentState.emojiData.memberIds.count { let len = currentState.emojiData.memberIds.count - profiles.count for _ in 0...(len - 1) { - let member = ProfileData( + let member = FamilyMemberProfileEntity( memberId: .none, name: .unknown ) diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileFeedViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileFeedViewReactor.swift index 33f80f7c7..78759820e 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileFeedViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileFeedViewReactor.swift @@ -143,7 +143,7 @@ final class ProfileFeedViewReactor: Reactor { postId: $0.postId, missionId: $0.missionId, missionType: $0.missionType, - author: ProfileData( + author: FamilyMemberProfileEntity( memberId: currentState.memberId, profileImageURL: $0.author?.profileImageURL, name: $0.author?.name ?? ""), diff --git a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/CreateFamilyResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/CreateFamilyResponseDTO.swift index e58024670..fd07d5c2d 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/CreateFamilyResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/CreateFamilyResponseDTO.swift @@ -18,7 +18,7 @@ public struct CreateFamilyResponseDTO: Decodable { } extension CreateFamilyResponseDTO { - func toDomain() -> CreateFamilyResponse { + func toDomain() -> CreateFamilyEntity { return .init( familyId: familyId, createdAt: createdAt.iso8601ToDate() diff --git a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/FamilyCreatedAtResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/FamilyCreatedAtResponseDTO.swift index 131960c63..79ad09d43 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/FamilyCreatedAtResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/FamilyCreatedAtResponseDTO.swift @@ -18,7 +18,7 @@ public struct FamilyCreatedAtResponseDTO: Decodable { } extension FamilyCreatedAtResponseDTO { - func toDomain() -> FamilyCreatedAtResponse { + func toDomain() -> FamilyCreatedAtEntity { return .init( createdAt: createdAt.iso8601ToDate() ) diff --git a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/FamilyInvitationLinkResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/FamilyInvitationLinkResponseDTO.swift index 73c152c7e..0f6c00013 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/FamilyInvitationLinkResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/FamilyInvitationLinkResponseDTO.swift @@ -29,7 +29,7 @@ extension FamilyInvitationLinkResponseDTO { } extension FamilyInvitationLinkResponseDTO { - func toDomain() -> FamilyInvitationLinkResponse { + func toDomain() -> FamilyInvitationLinkEntity { return .init(url: url) } } diff --git a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/FamilyMemberProfileResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/FamilyMemberProfileResponseDTO.swift index 0b64e2101..325389b96 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/FamilyMemberProfileResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/FamilyMemberProfileResponseDTO.swift @@ -39,7 +39,7 @@ extension PaginationResponseFamilyMemberProfileDTO { } extension PaginationResponseFamilyMemberProfileDTO { - func toDomain() -> PaginationResponseFamilyMemberProfile { + func toDomain() -> PaginationResponseFamilyMemberProfileEntity { return .init( results: results.map { $0.toDomain() } ) @@ -47,7 +47,7 @@ extension PaginationResponseFamilyMemberProfileDTO { } extension PaginationResponseFamilyMemberProfileDTO.FamilyMemberProfileResponseDTO { - func toDomain() -> ProfileData { + func toDomain() -> FamilyMemberProfileEntity { return .init( memberId: memberId, profileImageURL: imageUrl, diff --git a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/JoinFamilyResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/JoinFamilyResponseDTO.swift index 7fec90381..4043e261c 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/JoinFamilyResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/JoinFamilyResponseDTO.swift @@ -8,13 +8,13 @@ import Domain import Foundation -struct JoinFamilyResponseDTO: Decodable { +public struct JoinFamilyResponseDTO: Decodable { let familyId: String let createdAt: String } extension JoinFamilyResponseDTO { - func toDomain() -> JoinFamilyResponse { + func toDomain() -> JoinFamilyEntity { return .init( familyId: familyId, createdAt: createdAt.iso8601ToDate() diff --git a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIWorker.swift index 0d3bb8bab..c2fb3353b 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIWorker.swift @@ -5,10 +5,9 @@ // Created by 김건우 on 12/20/23. // -import Foundation - import Core import Domain +import Foundation import RxSwift @@ -34,10 +33,10 @@ extension FamilyAPIWorker { // MARK: - Join Family - private func joinFamily(headers: [APIHeader]?, jsonEncodable body: JoinFamilyRequestDTO) -> Single { + public func joinFamily(body: JoinFamilyRequestDTO) -> Single { let spec = MeAPIs.joinFamily.spec - return request(spec: spec, headers: headers, jsonEncodable: body) + return request(spec: spec, jsonEncodable: body) .subscribe(on: Self.queue) .do { if let str = String(data: $0.1, encoding: .utf8) { @@ -46,52 +45,34 @@ extension FamilyAPIWorker { } .map(JoinFamilyResponseDTO.self) .catchAndReturn(nil) - .map { $0?.toDomain() } - .asSingle() - } - - public func joinFamily(body: JoinFamilyRequestDTO) -> Single { - return Observable.just(()) - .withLatestFrom(self._headers) - .observe(on: Self.queue) - .withUnretained(self) - .flatMap { $0.0.joinFamily(headers: $0.1, jsonEncodable: body) } .asSingle() } - // MARK: - ResignFamily - private func resignFamily(spec: APISpec, headers: [APIHeader]?) -> Single { - return request(spec: spec, headers: headers) + public func resignFamily() -> Single { + let spec = FamilyAPIs.resignFamily.spec + + return request(spec: spec) .subscribe(on: Self.queue) .do(onNext: { if let str = String(data: $0.1, encoding: .utf8) { debugPrint("Resign Family result: \(str)") } }) - .map(AccountFamilyResignResponse.self) + .map(DefaultResponseDTO.self) .catchAndReturn(nil) .asSingle() } - public func resignFamily() -> Single { - let spec = FamilyAPIs.resignFamily.spec - - return Observable.just(()) - .withLatestFrom(self._headers) - .withUnretained(self) - .flatMap { $0.0.resignFamily(spec: spec, headers: $0.1) } - .asSingle() - } - - // MARK: - CreateFamily - private func createFamily(spec: APISpec, headers: [APIHeader]?) -> Single { - return request(spec: spec, headers: headers) + public func createFamily() -> Single { + let spec = FamilyAPIs.createFamily.spec + + return request(spec: spec) .subscribe(on: Self.queue) .do { if let str = String(data: $0.1, encoding: .utf8) { @@ -100,27 +81,16 @@ extension FamilyAPIWorker { } .map(CreateFamilyResponseDTO.self) .catchAndReturn(nil) - .map { $0?.toDomain() } .asSingle() } - public func createFamily() -> Single { - let spec: APISpec = FamilyAPIs.createFamily.spec - - return Observable.just(()) - .withLatestFrom(self._headers) - .observe(on: Self.queue) - .withUnretained(self) - .flatMap { $0.0.createFamily(spec: spec, headers: $0.1) } - .asSingle() - } - - // MARK: - Fetch Invititaion URL - private func fetchInvitationUrl(spec: APISpec, headers: [APIHeader]?) -> Single { - return request(spec: spec, headers: headers) + public func fetchInvitationLink(familyId: String) -> Single { + let spec = FamilyAPIs.fetchInvitationLink(familyId).spec + + return request(spec: spec) .subscribe(on: Self.queue) .do { if let str = String(data: $0.1, encoding: .utf8) { @@ -129,27 +99,16 @@ extension FamilyAPIWorker { } .map(FamilyInvitationLinkResponseDTO.self) .catchAndReturn(nil) - .map { $0?.toDomain() } - .asSingle() - } - - public func fetchInvitationUrl(familyId: String) -> Single { - let spec: APISpec = FamilyAPIs.fetchInvitationUrl(familyId).spec - - return Observable.just(()) - .withLatestFrom(self._headers) - .observe(on: Self.queue) - .withUnretained(self) - .flatMap { $0.0.fetchInvitationUrl(spec: spec, headers: $0.1) } .asSingle() } - // MARK: - Fetch FamilyCreatedAt - private func fetchFamilyCreatedAt(spec: APISpec, headers: [APIHeader]?) -> Single { - return request(spec: spec, headers: headers) + public func fetchFamilyCreatedAt(familyId: String) -> Single { + let spec = FamilyAPIs.fetchFamilyCreatedAt(familyId).spec + + return request(spec: spec) .subscribe(on: Self.queue) .do { if let str = String(data: $0.1, encoding: .utf8) { @@ -158,18 +117,6 @@ extension FamilyAPIWorker { } .map(FamilyCreatedAtResponseDTO.self) .catchAndReturn(nil) - .map { $0?.toDomain() } - .asSingle() - } - - public func fetchFamilyCreatedAt(familyId: String) -> Single { - let spec = FamilyAPIs.fetchFamilyCreatedAt(familyId).spec - - return Observable.just(()) - .withLatestFrom(self._headers) - .observe(on: Self.queue) - .withUnretained(self) - .flatMap { $0.0.fetchFamilyCreatedAt(spec: spec, headers: $0.1) } .asSingle() } @@ -177,8 +124,12 @@ extension FamilyAPIWorker { // MARK: - Fetch Family Member - private func fetchPaginationFamilyMember(spec: APISpec, headers: [APIHeader]?) -> Single { - return request(spec: spec, headers: headers) + public func fetchPaginationFamilyMember(familyId: String, query: FamilyPaginationQuery) -> Single { + let page = query.page + let size = query.size + let spec = FamilyAPIs.fetchPaginationFamilyMembers(page, size).spec + + return request(spec: spec) .subscribe(on: Self.queue) .do { if let str = String(data: $0.1, encoding: .utf8) { @@ -187,20 +138,6 @@ extension FamilyAPIWorker { } .map(PaginationResponseFamilyMemberProfileDTO.self) .catchAndReturn(nil) - .map { $0?.toDomain() } - .asSingle() - } - - public func fetchPaginationFamilyMember(familyId: String, query: FamilyPaginationQuery) -> Single { - let page = query.page - let size = query.size - let spec = FamilyAPIs.fetchPaginationFamilyMembers(page, size).spec - - return Observable.just(()) - .withLatestFrom(self._headers) - .observe(on: Self.queue) - .withUnretained(self) - .flatMap { $0.0.fetchPaginationFamilyMember(spec: spec, headers: $0.1) } .asSingle() } diff --git a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIs.swift index b407f0e1f..118436624 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIs.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIs.swift @@ -12,7 +12,7 @@ enum FamilyAPIs: API { case joinFamily case createFamily case resignFamily - case fetchInvitationUrl(String) + case fetchInvitationLink(String) case fetchFamilyCreatedAt(String) case fetchPaginationFamilyMembers(Int, Int) @@ -24,7 +24,7 @@ enum FamilyAPIs: API { return APISpec(method: .post, url: "\(BibbiAPI.hostApi)/me/quit-family") case .createFamily: return APISpec(method: .post, url: "\(BibbiAPI.hostApi)/me/create-family") - case let .fetchInvitationUrl(familyId): + case let .fetchInvitationLink(familyId): return APISpec(method: .post, url: "\(BibbiAPI.hostApi)/links/family/\(familyId)") case let .fetchFamilyCreatedAt(familyId): return APISpec(method: .get, url: "\(BibbiAPI.hostApi)/families/\(familyId)/created-at") diff --git a/14th-team5-iOS/Data/Sources/APIs/Family/Repository/FamilyRepository.swift b/14th-team5-iOS/Data/Sources/APIs/Family/Repository/FamilyRepository.swift index 01c94fde7..430d25c4a 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Family/Repository/FamilyRepository.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Family/Repository/FamilyRepository.swift @@ -16,71 +16,73 @@ public final class FamilyRepository: FamilyRepositoryProtocol { private let familyApiWorker: FamilyAPIWorker = FamilyAPIWorker() + // TODO: - UserDefaults로 바꾸기 private var familyId: String = App.Repository.member.familyId.value ?? "" - - // NOTE: - 예시 코드 - private var inMemory = InMemoryWrapper.standard - private var userDefaults = UserDefaultsWrapper.standard - private var keychain = KeychainWrapper.standard - - - - public init() { } } extension FamilyRepository { - public func joinFamily(body: JoinFamilyRequest) -> Observable { + + // MARK: - Join Family + + public func joinFamily(body: JoinFamilyRequest) -> Observable { let body = JoinFamilyRequestDTO(inviteCode: body.inviteCode) + return familyApiWorker.joinFamily(body: body) + .map { $0?.toDomain() } .do(onSuccess: { [weak self] response in guard let self else { return } - App.Repository.member.familyId.accept(response?.familyId) + App.Repository.member.familyId.accept(response?.familyId) // TODO: - UserDefaults로 바꾸기 App.Repository.member.familyCreatedAt.accept(response?.createdAt) - fetchPaginationFamilyMembers(query: .init()) + fetchPaginationFamilyMembers(query: .init()) // TODO: - 로직 분리하기 }) .asObservable() } - public func resignFamily() -> Observable { + public func resignFamily() -> Observable { return familyApiWorker.resignFamily() + .map { $0?.toDomain() } .asObservable() } - public func createFamily() -> Observable { + public func createFamily() -> Observable { return familyApiWorker.createFamily() + .map { $0?.toDomain() } .do(onSuccess: { - App.Repository.member.familyId.accept($0?.familyId) + App.Repository.member.familyId.accept($0?.familyId) // TODO: - UserDefaults로 바꾸기 App.Repository.member.familyCreatedAt.accept($0?.createdAt) }) .asObservable() } - public func fetchFamilyCreatedAt() -> Observable { + public func fetchFamilyCreatedAt() -> Observable { return familyApiWorker.fetchFamilyCreatedAt(familyId: familyId) - .do(onSuccess: { App.Repository.member.familyCreatedAt.accept($0?.createdAt) }) - .asObservable() - } - - public func fetchFamilyCreatedAt(_ familyId: String) -> Observable { - return familyApiWorker.fetchFamilyCreatedAt(familyId: familyId) - .do(onSuccess: { App.Repository.member.familyCreatedAt.accept($0?.createdAt) }) + .map { $0?.toDomain() } + .do(onSuccess: { + App.Repository.member.familyCreatedAt.accept($0?.createdAt) // TODO: - UserDefaults로 바꾸기 + }) .asObservable() } - public func fetchInvitationUrl() -> Observable { - return familyApiWorker.fetchInvitationUrl(familyId: familyId) + public func fetchInvitationLink() -> Observable { + return familyApiWorker.fetchInvitationLink(familyId: familyId) + .map { $0?.toDomain() } .asObservable() } - public func fetchPaginationFamilyMembers(query: FamilyPaginationQuery) -> Observable { + // TODO: - 반환 타입 확인하기 + public func fetchPaginationFamilyMembers(query: FamilyPaginationQuery) -> Observable { return familyApiWorker.fetchPaginationFamilyMember(familyId: familyId, query: query) - .do(onSuccess: { FamilyUserDefaults.saveFamilyMembers($0?.results ?? []) }) + .map { $0?.toDomain() } + .do(onSuccess: { + FamilyUserDefaults.saveFamilyMembers($0?.results ?? []) // TODO: - UserDefaults로 바꾸기 + }) .asObservable() } - public func fetchPaginationFamilyMembers(memberIds: [String]) -> [ProfileData] { + public func fetchPaginationFamilyMembers(memberIds: [String]) -> [FamilyMemberProfileEntity] { // TODO: - 반환 타입 바꾸기 + // TODO: - 리팩토링된 UserDefaults로 바꾸기 return FamilyUserDefaults.loadMembersFromUserDefaults(memberIds: memberIds) } } diff --git a/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/DataMapping/MainResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/DataMapping/MainResponseDTO.swift index d2d8af5a4..616271083 100644 --- a/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/DataMapping/MainResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/DataMapping/MainResponseDTO.swift @@ -18,7 +18,7 @@ struct TopBarElement: Codable { let shouldShowBirthdayMark: Bool let shouldShowPickIcon: Bool - func toDomain() -> ProfileData { + func toDomain() -> FamilyMemberProfileEntity { return .init( memberId: memberId, profileImageURL: imageUrl, diff --git a/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/DataMapping/DefaultResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/DataMapping/DefaultResponseDTO.swift index 31123199a..a14e6d2b7 100644 --- a/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/DataMapping/DefaultResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/DataMapping/DefaultResponseDTO.swift @@ -16,7 +16,7 @@ public struct DefaultResponseDTO: Decodable { } extension DefaultResponseDTO { - func toDomain() -> DefaultResponseEntity { + func toDomain() -> DefaultEntity { return .init(success: self.success) } } diff --git a/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/OAuthAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/OAuthAPIWorker.swift index 383042fd7..e4d9112ea 100644 --- a/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/OAuthAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/OAuthAPIWorker.swift @@ -89,7 +89,7 @@ extension OAuthAPIWorker { // MARK: - Register FCM Token - public func registerNewFCMToken(body: AddFCMTokenRequestDTO) -> Single { + public func registerNewFCMToken(body: AddFCMTokenRequestDTO) -> Single { let spec = OAuthAPIs.registerFCMToken.spec return request(spec: spec, jsonEncodable: body) @@ -108,7 +108,7 @@ extension OAuthAPIWorker { // MARK: - Delete FCM Token - public func deleteFCMToken(fcmToken token: String) -> Single { + public func deleteFCMToken(fcmToken token: String) -> Single { let spec = OAuthAPIs.deleteFCMToken(token).spec return request(spec: spec) 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 f9b90b0ec..b6c0e3ead 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 @@ -86,7 +86,7 @@ extension OAuthRepository { // MARK: - Register FCM Token - public func registerNewFCMToken(body: AddFCMTokenRequest) -> Observable { + public func registerNewFCMToken(body: AddFCMTokenRequest) -> Observable { let body = AddFCMTokenRequestDTO( fcmToken: body.fcmToken ) @@ -98,7 +98,7 @@ extension OAuthRepository { // MARK: - Delete FCM Token - public func deleteFCMToken(fcmToken token: String) -> Observable { + public func deleteFCMToken(fcmToken token: String) -> Observable { return oAuthApiWorker.deleteFCMToken(fcmToken: token) .observe(on: RxSchedulers.main) .asObservable() diff --git a/14th-team5-iOS/Data/Sources/Account/AccountRepository/AccountRepository.swift b/14th-team5-iOS/Data/Sources/Account/AccountRepository/AccountRepository.swift index e1bfb6a1d..1dfbaa1d0 100644 --- a/14th-team5-iOS/Data/Sources/Account/AccountRepository/AccountRepository.swift +++ b/14th-team5-iOS/Data/Sources/Account/AccountRepository/AccountRepository.swift @@ -143,7 +143,7 @@ public final class AccountRepository: AccountImpl { App.Repository.member.familyId.accept(memberInfo.familyId) App.Repository.member.nickname.accept(memberInfo.name) - let member: ProfileData = ProfileData(memberId: memberInfo.memberId, profileImageURL: memberInfo.imageUrl, name: memberInfo.name) + let member: FamilyMemberProfileEntity = FamilyMemberProfileEntity(memberId: memberInfo.memberId, profileImageURL: memberInfo.imageUrl, name: memberInfo.name) FamilyUserDefaults.saveMyMemberId(memberId: memberInfo.memberId) FamilyUserDefaults.saveMemberToUserDefaults(familyMember: member) } diff --git a/14th-team5-iOS/Data/Sources/Account/MeAPI/MeAPIWorker.swift b/14th-team5-iOS/Data/Sources/Account/MeAPI/MeAPIWorker.swift index e7230e575..1779a2f35 100644 --- a/14th-team5-iOS/Data/Sources/Account/MeAPI/MeAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Account/MeAPI/MeAPIWorker.swift @@ -89,7 +89,7 @@ extension MeAPIWorker: MeRepositoryProtocol, JoinFamilyRepository, FCMRepository } @available(*, deprecated, renamed: "joinFamily") - private func joinFamily(headers: [APIHeader]?, body: Domain.JoinFamilyRequest) -> RxSwift.Single { + private func joinFamily(headers: [APIHeader]?, body: Domain.JoinFamilyRequest) -> RxSwift.Single { let spec = MeAPIs.joinFamily.spec let requestDTO: JoinFamilyRequestDTO = JoinFamilyRequestDTO(inviteCode: body.inviteCode) @@ -172,7 +172,7 @@ extension MeAPIWorker { } @available(*, deprecated, renamed: "joinFamily") - public func joinFamily(body: JoinFamilyRequest) -> Single { + public func joinFamily(body: JoinFamilyRequest) -> Single { return Observable.just(()) .withLatestFrom(self._headers) .withUnretained(self) diff --git a/14th-team5-iOS/Data/Sources/Members/DataMapping/MembersProfileResponseDTO.swift b/14th-team5-iOS/Data/Sources/Members/DataMapping/MembersProfileResponseDTO.swift index ba5d305a5..3fe53de3a 100644 --- a/14th-team5-iOS/Data/Sources/Members/DataMapping/MembersProfileResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/Members/DataMapping/MembersProfileResponseDTO.swift @@ -32,7 +32,7 @@ extension MembersProfileResponseDTO { } //MARK: 프로빌 정보 공유 데이터 Entity - func toProfileEntity() -> ProfileData { + func toProfileEntity() -> FamilyMemberProfileEntity { return .init( memberId: memberId, profileImageURL: imageUrl, diff --git a/14th-team5-iOS/Data/Sources/UserDefaults/FamilyUserDefautls.swift b/14th-team5-iOS/Data/Sources/UserDefaults/FamilyUserDefautls.swift index 7f26c7444..8562e63d7 100644 --- a/14th-team5-iOS/Data/Sources/UserDefaults/FamilyUserDefautls.swift +++ b/14th-team5-iOS/Data/Sources/UserDefaults/FamilyUserDefautls.swift @@ -63,18 +63,18 @@ public class FamilyUserDefaults { return UserDefaults.standard.stringArray(forKey: myMemberIdKey)?.count ?? 0 } - public static func saveFamilyMembers(_ familyMembers: [ProfileData]) { + public static func saveFamilyMembers(_ familyMembers: [FamilyMemberProfileEntity]) { removeFamilyMembers() saveMemberIdToUserDefaults(memberIds: familyMembers.map { $0.memberId }) saveDayOfBirths(dateOfBirths: familyMembers.map { $0.dayOfBirth ?? Date() }) familyMembers.forEach { saveMemberToUserDefaults(familyMember: $0) } } - public static func load(memberId: String) -> ProfileData? { + public static func load(memberId: String) -> FamilyMemberProfileEntity? { if let data = UserDefaults.standard.data(forKey: memberId) { do { let decoder = JSONDecoder() - let person = try decoder.decode(ProfileData.self, from: data) + let person = try decoder.decode(FamilyMemberProfileEntity.self, from: data) return person } catch { print("Error decoding person: \(error.localizedDescription)") @@ -89,7 +89,7 @@ public class FamilyUserDefaults { } extension FamilyUserDefaults { - public static func saveMemberToUserDefaults(familyMember: ProfileData) { + public static func saveMemberToUserDefaults(familyMember: FamilyMemberProfileEntity) { do { let encoder = JSONEncoder() let data = try encoder.encode(familyMember) @@ -108,13 +108,13 @@ extension FamilyUserDefaults { userDefaults.setValue(dateOfBirths, forKey: self.dayOfBirths) } - static func loadMembersFromUserDefaults(memberIds: [String]) -> [ProfileData] { - var datas: [ProfileData] = [] + static func loadMembersFromUserDefaults(memberIds: [String]) -> [FamilyMemberProfileEntity] { + var datas: [FamilyMemberProfileEntity] = [] memberIds.forEach { if let data = UserDefaults.standard.data(forKey: $0) { do { let decoder = JSONDecoder() - let data = try decoder.decode(ProfileData.self, from: data) + let data = try decoder.decode(FamilyMemberProfileEntity.self, from: data) return datas.append(data) } catch { print("Error decoding person: \(error.localizedDescription)") diff --git a/14th-team5-iOS/Domain/Sources/Calendar/Entities/ArrayResponseCalendarEntity.swift b/14th-team5-iOS/Domain/Sources/Entities/Calendar/ArrayResponseCalendarEntity.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Calendar/Entities/ArrayResponseCalendarEntity.swift rename to 14th-team5-iOS/Domain/Sources/Entities/Calendar/ArrayResponseCalendarEntity.swift diff --git a/14th-team5-iOS/Domain/Sources/Family/Entities/CreateFamilyResponse.swift b/14th-team5-iOS/Domain/Sources/Entities/Family/CreateFamilyEntity.swift similarity index 88% rename from 14th-team5-iOS/Domain/Sources/Family/Entities/CreateFamilyResponse.swift rename to 14th-team5-iOS/Domain/Sources/Entities/Family/CreateFamilyEntity.swift index f6010cc0f..8edd71217 100644 --- a/14th-team5-iOS/Domain/Sources/Family/Entities/CreateFamilyResponse.swift +++ b/14th-team5-iOS/Domain/Sources/Entities/Family/CreateFamilyEntity.swift @@ -7,7 +7,7 @@ import Foundation -public struct CreateFamilyResponse { +public struct CreateFamilyEntity { public var familyId: String public var createdAt: Date diff --git a/14th-team5-iOS/Domain/Sources/Family/Entities/FamilyCreatedAtResponse.swift b/14th-team5-iOS/Domain/Sources/Entities/Family/FamilyCreatedAtEntity.swift similarity index 84% rename from 14th-team5-iOS/Domain/Sources/Family/Entities/FamilyCreatedAtResponse.swift rename to 14th-team5-iOS/Domain/Sources/Entities/Family/FamilyCreatedAtEntity.swift index 8f33d6bbb..30ed889c1 100644 --- a/14th-team5-iOS/Domain/Sources/Family/Entities/FamilyCreatedAtResponse.swift +++ b/14th-team5-iOS/Domain/Sources/Entities/Family/FamilyCreatedAtEntity.swift @@ -7,7 +7,7 @@ import Foundation -public struct FamilyCreatedAtResponse { +public struct FamilyCreatedAtEntity { public var createdAt: Date public init(createdAt: Date) { diff --git a/14th-team5-iOS/Domain/Sources/Family/Entities/FamilyInvitationLinkResponse.swift b/14th-team5-iOS/Domain/Sources/Entities/Family/FamilyInvitationLinkEntity.swift similarity index 81% rename from 14th-team5-iOS/Domain/Sources/Family/Entities/FamilyInvitationLinkResponse.swift rename to 14th-team5-iOS/Domain/Sources/Entities/Family/FamilyInvitationLinkEntity.swift index 21c308cfd..bc4975796 100644 --- a/14th-team5-iOS/Domain/Sources/Family/Entities/FamilyInvitationLinkResponse.swift +++ b/14th-team5-iOS/Domain/Sources/Entities/Family/FamilyInvitationLinkEntity.swift @@ -7,7 +7,7 @@ import Foundation -public struct FamilyInvitationLinkResponse { +public struct FamilyInvitationLinkEntity { public var url: String public init(url: String) { diff --git a/14th-team5-iOS/Domain/Sources/Family/Entities/FamilyMemberProfileResponse.swift b/14th-team5-iOS/Domain/Sources/Entities/Family/FamilyMemberProfileEntity.swift similarity index 79% rename from 14th-team5-iOS/Domain/Sources/Family/Entities/FamilyMemberProfileResponse.swift rename to 14th-team5-iOS/Domain/Sources/Entities/Family/FamilyMemberProfileEntity.swift index 3b8a28fdc..4daa69bb2 100644 --- a/14th-team5-iOS/Domain/Sources/Family/Entities/FamilyMemberProfileResponse.swift +++ b/14th-team5-iOS/Domain/Sources/Entities/Family/FamilyMemberProfileEntity.swift @@ -7,15 +7,15 @@ import Foundation -public struct PaginationResponseFamilyMemberProfile { - public var results: [ProfileData] +public struct PaginationResponseFamilyMemberProfileEntity { + public var results: [FamilyMemberProfileEntity] - public init(results: [ProfileData]) { + public init(results: [FamilyMemberProfileEntity]) { self.results = results } } -public struct ProfileData: Equatable, Hashable, Codable { +public struct FamilyMemberProfileEntity: Equatable, Hashable, Codable { public let memberId: String public let profileImageURL: String? public let name: String diff --git a/14th-team5-iOS/Domain/Sources/Family/Entities/FamilyPaginationQuery.swift b/14th-team5-iOS/Domain/Sources/Entities/Family/FamilyPaginationQuery.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Family/Entities/FamilyPaginationQuery.swift rename to 14th-team5-iOS/Domain/Sources/Entities/Family/FamilyPaginationQuery.swift diff --git a/14th-team5-iOS/Domain/Sources/Family/Entities/JoinFamilyResponse.swift b/14th-team5-iOS/Domain/Sources/Entities/Family/JoinFamilyEntity.swift similarity index 89% rename from 14th-team5-iOS/Domain/Sources/Family/Entities/JoinFamilyResponse.swift rename to 14th-team5-iOS/Domain/Sources/Entities/Family/JoinFamilyEntity.swift index 8f36548f7..a89b31261 100644 --- a/14th-team5-iOS/Domain/Sources/Family/Entities/JoinFamilyResponse.swift +++ b/14th-team5-iOS/Domain/Sources/Entities/Family/JoinFamilyEntity.swift @@ -7,7 +7,7 @@ import Foundation -public struct JoinFamilyResponse { +public struct JoinFamilyEntity { public var familyId: String public var createdAt: Date diff --git a/14th-team5-iOS/Domain/Sources/Family/Entities/JoinFamilyRequest.swift b/14th-team5-iOS/Domain/Sources/Entities/Family/JoinFamilyRequest.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Family/Entities/JoinFamilyRequest.swift rename to 14th-team5-iOS/Domain/Sources/Entities/Family/JoinFamilyRequest.swift diff --git a/14th-team5-iOS/Domain/Sources/Entities/MainView/MainViewEntity.swift b/14th-team5-iOS/Domain/Sources/Entities/MainView/MainViewEntity.swift index a1cf67294..cf54dac6d 100644 --- a/14th-team5-iOS/Domain/Sources/Entities/MainView/MainViewEntity.swift +++ b/14th-team5-iOS/Domain/Sources/Entities/MainView/MainViewEntity.swift @@ -18,7 +18,7 @@ public struct Picker { } public struct MainViewEntity { - public let mainFamilyProfileDatas: [ProfileData] + public let mainFamilyProfileDatas: [FamilyMemberProfileEntity] public let leftUploadCountUntilMissionUnlock: Int public let isFamilySurvivalUploadedToday: Bool public let isFamilyMissionUploadedToday: Bool @@ -29,7 +29,7 @@ public struct MainViewEntity { public let pickers: [Picker] public init( - mainFamilyProfileDatas: [ProfileData], + mainFamilyProfileDatas: [FamilyMemberProfileEntity], leftUploadCountUntilMissionUnlock: Int, isMissionUnlocked: Bool, isMeSurvivalUploadedToday: Bool, diff --git a/14th-team5-iOS/Domain/Sources/Entities/MainView/NightMainViewEntity.swift b/14th-team5-iOS/Domain/Sources/Entities/MainView/NightMainViewEntity.swift index aaf90050d..fa25cd74b 100644 --- a/14th-team5-iOS/Domain/Sources/Entities/MainView/NightMainViewEntity.swift +++ b/14th-team5-iOS/Domain/Sources/Entities/MainView/NightMainViewEntity.swift @@ -78,10 +78,10 @@ extension FamilyRankData { } public struct NightMainViewEntity { - public let mainFamilyProfileDatas: [ProfileData] + public let mainFamilyProfileDatas: [FamilyMemberProfileEntity] public let familyRankData: FamilyRankData - public init(mainFamilyProfileDatas: [ProfileData], familyRankData: FamilyRankData) { + public init(mainFamilyProfileDatas: [FamilyMemberProfileEntity], familyRankData: FamilyRankData) { self.mainFamilyProfileDatas = mainFamilyProfileDatas self.familyRankData = familyRankData } diff --git a/14th-team5-iOS/Domain/Sources/Entities/PostList/PostEntity.swift b/14th-team5-iOS/Domain/Sources/Entities/PostList/PostEntity.swift index 43ef1bd09..5aa05bde8 100644 --- a/14th-team5-iOS/Domain/Sources/Entities/PostList/PostEntity.swift +++ b/14th-team5-iOS/Domain/Sources/Entities/PostList/PostEntity.swift @@ -9,7 +9,7 @@ import Foundation public struct PostEntity: Equatable, Hashable { public let postId: String - public let author: ProfileData? + public let author: FamilyMemberProfileEntity? public var commentCount: Int public let missionId: String? public let missionType: String? @@ -18,7 +18,7 @@ public struct PostEntity: Equatable, Hashable { public let content: String? public let time: String - public init(postId: String, missionId: String? = nil, missionType: String? = nil, author: ProfileData?, commentCount: Int, emojiCount: Int, imageURL: String, content: String?, time: String) { + public init(postId: String, missionId: String? = nil, missionType: String? = nil, author: FamilyMemberProfileEntity?, commentCount: Int, emojiCount: Int, imageURL: String, content: String?, time: String) { self.postId = postId self.missionId = missionId self.missionType = missionType diff --git a/14th-team5-iOS/Domain/Sources/Family/Interfaces/Repositories/FamilyRepository.swift b/14th-team5-iOS/Domain/Sources/Family/Interfaces/Repositories/FamilyRepository.swift deleted file mode 100644 index 39f150ca2..000000000 --- a/14th-team5-iOS/Domain/Sources/Family/Interfaces/Repositories/FamilyRepository.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// FamilyRepository.swift -// Domain -// -// Created by 김건우 on 12/29/23. -// - -import Foundation - -import RxSwift - -public protocol FamilyRepositoryProtocol { - var disposeBag: DisposeBag { get } - - func joinFamily(body: JoinFamilyRequest) -> Observable - func resignFamily() -> Observable - func createFamily() -> Observable - func fetchFamilyCreatedAt() -> Observable - func fetchFamilyCreatedAt(_ familyId: String) -> Observable - func fetchInvitationUrl() -> Observable - func fetchPaginationFamilyMembers(query: FamilyPaginationQuery) -> Observable - func fetchPaginationFamilyMembers(memberIds: [String]) -> [ProfileData] -} diff --git a/14th-team5-iOS/Domain/Sources/Family/Interfaces/Repositories/JoinFamilyRepository.swift b/14th-team5-iOS/Domain/Sources/Family/Interfaces/Repositories/JoinFamilyRepository.swift index 33ed2b652..5ea04bed0 100644 --- a/14th-team5-iOS/Domain/Sources/Family/Interfaces/Repositories/JoinFamilyRepository.swift +++ b/14th-team5-iOS/Domain/Sources/Family/Interfaces/Repositories/JoinFamilyRepository.swift @@ -10,6 +10,6 @@ import RxSwift @available(*, deprecated, renamed: "FamilyRepository") public protocol JoinFamilyRepository { - func joinFamily(body: JoinFamilyRequest) -> Single + func joinFamily(body: JoinFamilyRequest) -> Single func resignFamily() -> Single } diff --git a/14th-team5-iOS/Domain/Sources/Family/UseCases/FamilyUseCase.swift b/14th-team5-iOS/Domain/Sources/Family/UseCases/FamilyUseCase.swift index 696ef4e9b..3f3d41084 100644 --- a/14th-team5-iOS/Domain/Sources/Family/UseCases/FamilyUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/Family/UseCases/FamilyUseCase.swift @@ -10,14 +10,13 @@ import Foundation import RxSwift public protocol FamilyUseCaseProtocol { - func executeJoinFamily(body: JoinFamilyRequest) -> Observable - func executeResignFamily() -> Observable - func executeCreateFamily() -> Observable - func executeFetchCreatedAtFamily() -> Observable - func executeFetchCreatedAtFamily(_ familyId: String) -> Observable - func executeFetchInvitationUrl() -> Observable - func executeFetchPaginationFamilyMembers(query: FamilyPaginationQuery) -> Observable - func executeFetchPaginationFamilyMembers(memberIds: [String]) -> [ProfileData] + func executeJoinFamily(body: JoinFamilyRequest) -> Observable + func executeResignFamily() -> Observable + func executeCreateFamily() -> Observable + func executeFetchCreatedAtFamily() -> Observable + func executeFetchInvitationUrl() -> Observable + func executeFetchPaginationFamilyMembers(query: FamilyPaginationQuery) -> Observable + func executeFetchPaginationFamilyMembers(memberIds: [String]) -> [FamilyMemberProfileEntity] } public final class FamilyUseCase: FamilyUseCaseProtocol { @@ -27,35 +26,31 @@ public final class FamilyUseCase: FamilyUseCaseProtocol { self.familyRepository = familyRepository } - public func executeJoinFamily(body: JoinFamilyRequest) -> Observable { + public func executeJoinFamily(body: JoinFamilyRequest) -> Observable { return familyRepository.joinFamily(body: body) } - public func executeResignFamily() -> Observable { + public func executeResignFamily() -> Observable { return familyRepository.resignFamily() } - public func executeCreateFamily() -> Observable { + public func executeCreateFamily() -> Observable { return familyRepository.createFamily() } - public func executeFetchCreatedAtFamily() -> Observable { + public func executeFetchCreatedAtFamily() -> Observable { return familyRepository.fetchFamilyCreatedAt() } - public func executeFetchCreatedAtFamily(_ familyId: String) -> Observable { - return familyRepository.fetchFamilyCreatedAt(familyId) + public func executeFetchInvitationUrl() -> Observable { + return familyRepository.fetchInvitationLink() } - public func executeFetchInvitationUrl() -> Observable { - return familyRepository.fetchInvitationUrl() - } - - public func executeFetchPaginationFamilyMembers(query: FamilyPaginationQuery) -> Observable { + public func executeFetchPaginationFamilyMembers(query: FamilyPaginationQuery) -> Observable { return familyRepository.fetchPaginationFamilyMembers(query: query) } - public func executeFetchPaginationFamilyMembers(memberIds: [String]) -> [ProfileData] { + public func executeFetchPaginationFamilyMembers(memberIds: [String]) -> [FamilyMemberProfileEntity] { return familyRepository.fetchPaginationFamilyMembers(memberIds: memberIds) } } diff --git a/14th-team5-iOS/Domain/Sources/OAuth/Entities/DefaultResponseEntity.swift b/14th-team5-iOS/Domain/Sources/OAuth/Entities/DefaultEntity.swift similarity index 84% rename from 14th-team5-iOS/Domain/Sources/OAuth/Entities/DefaultResponseEntity.swift rename to 14th-team5-iOS/Domain/Sources/OAuth/Entities/DefaultEntity.swift index aac7ba0a8..267e47774 100644 --- a/14th-team5-iOS/Domain/Sources/OAuth/Entities/DefaultResponseEntity.swift +++ b/14th-team5-iOS/Domain/Sources/OAuth/Entities/DefaultEntity.swift @@ -7,7 +7,7 @@ import Foundation -public struct DefaultResponseEntity { +public struct DefaultEntity { public var success: Bool public init(success: Bool) { diff --git a/14th-team5-iOS/Domain/Sources/OAuth/Interfaces/Repository/OAuthRepository.swift b/14th-team5-iOS/Domain/Sources/OAuth/Interfaces/Repository/OAuthRepository.swift index de9243d63..cbb7f6cb3 100644 --- a/14th-team5-iOS/Domain/Sources/OAuth/Interfaces/Repository/OAuthRepository.swift +++ b/14th-team5-iOS/Domain/Sources/OAuth/Interfaces/Repository/OAuthRepository.swift @@ -15,6 +15,6 @@ public protocol OAuthRepositoryProtocol { func refreshAccessToken(body: RefreshAccessTokenRequest) -> Observable func registerNewMember(body: CreateNewMemberRequest) -> Observable func signIn(_ type: SignInType, body: NativeSocialLoginRequest) -> Observable - func registerNewFCMToken(body: AddFCMTokenRequest) -> Observable - func deleteFCMToken(fcmToken token: String) -> Observable + func registerNewFCMToken(body: AddFCMTokenRequest) -> Observable + func deleteFCMToken(fcmToken token: String) -> Observable } diff --git a/14th-team5-iOS/Domain/Sources/Repositories/FamilyRepository.swift b/14th-team5-iOS/Domain/Sources/Repositories/FamilyRepository.swift new file mode 100644 index 000000000..6cbcc6c0e --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Repositories/FamilyRepository.swift @@ -0,0 +1,22 @@ +// +// FamilyRepository.swift +// Domain +// +// Created by 김건우 on 12/29/23. +// + +import Foundation + +import RxSwift + +public protocol FamilyRepositoryProtocol { + var disposeBag: DisposeBag { get } + + func joinFamily(body: JoinFamilyRequest) -> Observable + func resignFamily() -> Observable + func createFamily() -> Observable + func fetchFamilyCreatedAt() -> Observable + func fetchInvitationLink() -> Observable + func fetchPaginationFamilyMembers(query: FamilyPaginationQuery) -> Observable + func fetchPaginationFamilyMembers(memberIds: [String]) -> [FamilyMemberProfileEntity] +} diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Family/CreateFamilyUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Family/CreateFamilyUseCase.swift new file mode 100644 index 000000000..094f896a6 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/UseCases/Family/CreateFamilyUseCase.swift @@ -0,0 +1,30 @@ +// +// CreateFamilyUseCase.swift +// Domain +// +// Created by 김건우 on 6/15/24. +// + +import Foundation + +import RxSwift + +public protocol CreateFamilyUseCaseProtocol { + func execute() -> Observable +} + +public class CreateFamilyUseCase: CreateFamilyUseCaseProtocol { + + // MARK: - Repositories + private var familyRepository: FamilyRepositoryProtocol + + // MARK: - Intializer + public init(familyRepository: FamilyRepositoryProtocol) { + self.familyRepository = familyRepository + } + + // MARK: - Execute + public func execute() -> Observable { + return familyRepository.createFamily() + } +} diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Family/FetchFamilyCreatedAtUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Family/FetchFamilyCreatedAtUseCase.swift new file mode 100644 index 000000000..638e52e6c --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/UseCases/Family/FetchFamilyCreatedAtUseCase.swift @@ -0,0 +1,30 @@ +// +// FetchFamilyCreatedAtUseCase.swift +// Domain +// +// Created by 김건우 on 6/15/24. +// + +import Foundation + +import RxSwift + +public protocol FetchFamilyCreatedAtUseCaseProtocol { + func execute() -> Observable +} + +public class FetchFamilyCreatedAtUseCase: FetchFamilyCreatedAtUseCaseProtocol { + + // MARK: - Repositories + private var familyRepository: FamilyRepositoryProtocol + + // MARK: - Intializer + public init(familyRepository: FamilyRepositoryProtocol) { + self.familyRepository = familyRepository + } + + // MARK: - Execute + public func execute() -> Observable { + return familyRepository.fetchFamilyCreatedAt() + } +} diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Family/FetchFamilyMembersFromStorageUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Family/FetchFamilyMembersFromStorageUseCase.swift new file mode 100644 index 000000000..d27bb0e47 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/UseCases/Family/FetchFamilyMembersFromStorageUseCase.swift @@ -0,0 +1,31 @@ +// +// FetchFamilyMembersFromStorageUseCase.swift +// Domain +// +// Created by 김건우 on 6/15/24. +// + +import Foundation + +import RxSwift + +public protocol FetchFamilyMembersUseCaseFromStorageProtocol { + func execute(memberIds: [String]) -> [FamilyMemberProfileEntity] +} + +public class FetchFamilyMembersFromStoragUseCase: FetchFamilyMembersUseCaseFromStorageProtocol { + + // MARK: - Repositories + private var familyRepository: FamilyRepositoryProtocol + + // MARK: - Intializer + public init(familyRepository: FamilyRepositoryProtocol) { + self.familyRepository = familyRepository + } + + // MARK: - Execute + public func execute(memberIds: [String]) -> [FamilyMemberProfileEntity] { + return familyRepository.fetchPaginationFamilyMembers(memberIds: memberIds) + } +} + diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Family/FetchFamilyMembersUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Family/FetchFamilyMembersUseCase.swift new file mode 100644 index 000000000..8476660d8 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/UseCases/Family/FetchFamilyMembersUseCase.swift @@ -0,0 +1,30 @@ +// +// FetchFamilyMembersUseCase.swift +// Domain +// +// Created by 김건우 on 6/15/24. +// + +import Foundation + +import RxSwift + +public protocol FetchFamilyMembersUseCaseProtocol { + func execute(query: FamilyPaginationQuery) -> Observable +} + +public class FetchFamilyMembersUseCase: FetchFamilyMembersUseCaseProtocol { + + // MARK: - Repositories + private var familyRepository: FamilyRepositoryProtocol + + // MARK: - Intializer + public init(familyRepository: FamilyRepositoryProtocol) { + self.familyRepository = familyRepository + } + + // MARK: - Execute + public func execute(query: FamilyPaginationQuery) -> Observable { + return familyRepository.fetchPaginationFamilyMembers(query: query) + } +} diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Family/FetchInvitationLinkUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Family/FetchInvitationLinkUseCase.swift new file mode 100644 index 000000000..18bc252cd --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/UseCases/Family/FetchInvitationLinkUseCase.swift @@ -0,0 +1,30 @@ +// +// FetchInvitationUrlUseCase.swift +// Domain +// +// Created by 김건우 on 6/15/24. +// + +import Foundation + +import RxSwift + +public protocol FetchInvitationLinkUseCaseProtocol { + func execute() -> Observable +} + +public class FetchInvitationUrlUseCase: FetchInvitationLinkUseCaseProtocol { + + // MARK: - Repositories + private var familyRepository: FamilyRepositoryProtocol + + // MARK: - Intializer + public init(familyRepository: FamilyRepositoryProtocol) { + self.familyRepository = familyRepository + } + + // MARK: - Execute + public func execute() -> Observable { + return familyRepository.fetchInvitationLink() + } +} diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Family/JoinFamilyUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Family/JoinFamilyUseCase.swift new file mode 100644 index 000000000..65fe7d27c --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/UseCases/Family/JoinFamilyUseCase.swift @@ -0,0 +1,30 @@ +// +// JoinFamilyUseCase.swift +// Domain +// +// Created by 김건우 on 6/15/24. +// + +import Foundation + +import RxSwift + +public protocol JoinFamilyUseCaseProtocol { + func execute(body: JoinFamilyRequest) -> Observable +} + +public class JoinFamilyUseCase: JoinFamilyUseCaseProtocol { + + // MARK: - Repositories + private var familyRepository: FamilyRepositoryProtocol + + // MARK: - Intializer + public init(familyRepository: FamilyRepositoryProtocol) { + self.familyRepository = familyRepository + } + + // MARK: - Execute + public func execute(body: JoinFamilyRequest) -> Observable { + return familyRepository.joinFamily(body: body) + } +} diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Family/ResignFamilyUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Family/ResignFamilyUseCase.swift new file mode 100644 index 000000000..39d71614b --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/UseCases/Family/ResignFamilyUseCase.swift @@ -0,0 +1,30 @@ +// +// ResignFamily.swift +// Domain +// +// Created by 김건우 on 6/15/24. +// + +import Foundation + +import RxSwift + +public protocol ResignFamilyUseCaseProtocol { + func execute() -> Observable +} + +public class ResignFamilyUseCase: ResignFamilyUseCaseProtocol { + + // MARK: - Repositories + private var familyRepository: FamilyRepositoryProtocol + + // MARK: - Intializer + public init(familyRepository: FamilyRepositoryProtocol) { + self.familyRepository = familyRepository + } + + // MARK: - Execute + public func execute() -> Observable { + return familyRepository.resignFamily() + } +} From 641b84d33caf0a710b19df9af870079e2bc024cd Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Sat, 15 Jun 2024 15:52:54 +0900 Subject: [PATCH 110/263] =?UTF-8?q?refactor:=20OAuthUseCase=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20(#553)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../OAuthAPI/Repository/OAuthRepository.swift | 13 ++++++-- .../OAuth}/AddFCMTokenRequest.swift | 0 .../OAuth}/AuthResultEntity.swift | 0 .../OAuth}/CreateNewMemberRequest.swift | 0 .../OAuth}/DefaultEntity.swift | 0 .../OAuth}/NativeSocialLoginRequest.swift | 0 .../OAuth}/RefreshAccessTokenRequest.swift | 0 .../OAuth}/SignInType.swift | 3 -- .../UseCase/RegisterNewMemberUseCase.swift | 8 ----- .../Repositories/CalendarRepository.swift | 2 -- .../Repositories/FamilyRepository.swift | 2 -- .../OAuthRepository.swift | 4 +-- .../SignIn/UseCases/SignInUseCase.swift | 8 ----- .../OAuth/DeleteFCMTokenUseCase.swift | 31 +++++++++++++++++ .../OAuth/RefreshAccessTokenUseCase.swift | 30 +++++++++++++++++ .../OAuth/RegisterNewFCMTokenUseCase.swift | 31 +++++++++++++++++ .../OAuth/RegisterNewMemberUseCase.swift | 30 +++++++++++++++++ .../UseCases/OAuth/SignInUseCase.swift | 33 +++++++++++++++++++ 18 files changed, 167 insertions(+), 28 deletions(-) rename 14th-team5-iOS/Domain/Sources/{OAuth/Entities => Entities/OAuth}/AddFCMTokenRequest.swift (100%) rename 14th-team5-iOS/Domain/Sources/{OAuth/Entities => Entities/OAuth}/AuthResultEntity.swift (100%) rename 14th-team5-iOS/Domain/Sources/{OAuth/Entities => Entities/OAuth}/CreateNewMemberRequest.swift (100%) rename 14th-team5-iOS/Domain/Sources/{OAuth/Entities => Entities/OAuth}/DefaultEntity.swift (100%) rename 14th-team5-iOS/Domain/Sources/{OAuth/Entities => Entities/OAuth}/NativeSocialLoginRequest.swift (100%) rename 14th-team5-iOS/Domain/Sources/{OAuth/Entities => Entities/OAuth}/RefreshAccessTokenRequest.swift (100%) rename 14th-team5-iOS/Domain/Sources/{OAuth/Entities => Entities/OAuth}/SignInType.swift (57%) delete mode 100644 14th-team5-iOS/Domain/Sources/OAuth/UseCase/RegisterNewMemberUseCase.swift rename 14th-team5-iOS/Domain/Sources/{OAuth/Interfaces/Repository => Repositories}/OAuthRepository.swift (81%) delete mode 100644 14th-team5-iOS/Domain/Sources/SignIn/UseCases/SignInUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/UseCases/OAuth/DeleteFCMTokenUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/UseCases/OAuth/RefreshAccessTokenUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/UseCases/OAuth/RegisterNewFCMTokenUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/UseCases/OAuth/RegisterNewMemberUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/UseCases/OAuth/SignInUseCase.swift 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 b6c0e3ead..916d4f0e3 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 @@ -19,6 +19,7 @@ public final class OAuthRepository: OAuthRepositoryProtocol { public var oAuthApiWorker = OAuthAPIWorker() public var tokenKeychainStorage = TokenKeychain() + // MARK: - Intializer public init() { } } @@ -98,10 +99,18 @@ extension OAuthRepository { // MARK: - Delete FCM Token - public func deleteFCMToken(fcmToken token: String) -> Observable { - return oAuthApiWorker.deleteFCMToken(fcmToken: token) + public func deleteFCMToken() -> Observable { + + guard + let fcmToken = tokenKeychainStorage.loadFCMToken() + else { + return Observable.just(DefaultEntity(success: false)) + } + + return oAuthApiWorker.deleteFCMToken(fcmToken: fcmToken) .observe(on: RxSchedulers.main) .asObservable() + } } diff --git a/14th-team5-iOS/Domain/Sources/OAuth/Entities/AddFCMTokenRequest.swift b/14th-team5-iOS/Domain/Sources/Entities/OAuth/AddFCMTokenRequest.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/OAuth/Entities/AddFCMTokenRequest.swift rename to 14th-team5-iOS/Domain/Sources/Entities/OAuth/AddFCMTokenRequest.swift diff --git a/14th-team5-iOS/Domain/Sources/OAuth/Entities/AuthResultEntity.swift b/14th-team5-iOS/Domain/Sources/Entities/OAuth/AuthResultEntity.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/OAuth/Entities/AuthResultEntity.swift rename to 14th-team5-iOS/Domain/Sources/Entities/OAuth/AuthResultEntity.swift diff --git a/14th-team5-iOS/Domain/Sources/OAuth/Entities/CreateNewMemberRequest.swift b/14th-team5-iOS/Domain/Sources/Entities/OAuth/CreateNewMemberRequest.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/OAuth/Entities/CreateNewMemberRequest.swift rename to 14th-team5-iOS/Domain/Sources/Entities/OAuth/CreateNewMemberRequest.swift diff --git a/14th-team5-iOS/Domain/Sources/OAuth/Entities/DefaultEntity.swift b/14th-team5-iOS/Domain/Sources/Entities/OAuth/DefaultEntity.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/OAuth/Entities/DefaultEntity.swift rename to 14th-team5-iOS/Domain/Sources/Entities/OAuth/DefaultEntity.swift diff --git a/14th-team5-iOS/Domain/Sources/OAuth/Entities/NativeSocialLoginRequest.swift b/14th-team5-iOS/Domain/Sources/Entities/OAuth/NativeSocialLoginRequest.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/OAuth/Entities/NativeSocialLoginRequest.swift rename to 14th-team5-iOS/Domain/Sources/Entities/OAuth/NativeSocialLoginRequest.swift diff --git a/14th-team5-iOS/Domain/Sources/OAuth/Entities/RefreshAccessTokenRequest.swift b/14th-team5-iOS/Domain/Sources/Entities/OAuth/RefreshAccessTokenRequest.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/OAuth/Entities/RefreshAccessTokenRequest.swift rename to 14th-team5-iOS/Domain/Sources/Entities/OAuth/RefreshAccessTokenRequest.swift diff --git a/14th-team5-iOS/Domain/Sources/OAuth/Entities/SignInType.swift b/14th-team5-iOS/Domain/Sources/Entities/OAuth/SignInType.swift similarity index 57% rename from 14th-team5-iOS/Domain/Sources/OAuth/Entities/SignInType.swift rename to 14th-team5-iOS/Domain/Sources/Entities/OAuth/SignInType.swift index 495d96b0b..4b58ec61e 100644 --- a/14th-team5-iOS/Domain/Sources/OAuth/Entities/SignInType.swift +++ b/14th-team5-iOS/Domain/Sources/Entities/OAuth/SignInType.swift @@ -7,9 +7,6 @@ import Foundation -// NOTE: - App, Data 모듈에 모두 필요해 -// Domain 모듈에 두는데 어디에 어떻게 두면 좋을지 모르겠어요. - public enum SignInType: String { case kakao = "KAKAO" case apple = "APPLE" diff --git a/14th-team5-iOS/Domain/Sources/OAuth/UseCase/RegisterNewMemberUseCase.swift b/14th-team5-iOS/Domain/Sources/OAuth/UseCase/RegisterNewMemberUseCase.swift deleted file mode 100644 index 13799e591..000000000 --- a/14th-team5-iOS/Domain/Sources/OAuth/UseCase/RegisterNewMemberUseCase.swift +++ /dev/null @@ -1,8 +0,0 @@ -// -// RegisterNewMemberUseCase.swift -// Domain -// -// Created by 김건우 on 6/6/24. -// - -import Foundation diff --git a/14th-team5-iOS/Domain/Sources/Repositories/CalendarRepository.swift b/14th-team5-iOS/Domain/Sources/Repositories/CalendarRepository.swift index 9feabe19e..19410247b 100644 --- a/14th-team5-iOS/Domain/Sources/Repositories/CalendarRepository.swift +++ b/14th-team5-iOS/Domain/Sources/Repositories/CalendarRepository.swift @@ -10,8 +10,6 @@ import Foundation import RxSwift public protocol CalendarRepositoryProtocol { - var disposeBag: DisposeBag { get } - @available(*, deprecated, renamed: "fetchMonthlyCalendarResponse") func fetchCalendarResponse(yearMonth: String) -> Observable diff --git a/14th-team5-iOS/Domain/Sources/Repositories/FamilyRepository.swift b/14th-team5-iOS/Domain/Sources/Repositories/FamilyRepository.swift index 6cbcc6c0e..014b392ac 100644 --- a/14th-team5-iOS/Domain/Sources/Repositories/FamilyRepository.swift +++ b/14th-team5-iOS/Domain/Sources/Repositories/FamilyRepository.swift @@ -10,8 +10,6 @@ import Foundation import RxSwift public protocol FamilyRepositoryProtocol { - var disposeBag: DisposeBag { get } - func joinFamily(body: JoinFamilyRequest) -> Observable func resignFamily() -> Observable func createFamily() -> Observable diff --git a/14th-team5-iOS/Domain/Sources/OAuth/Interfaces/Repository/OAuthRepository.swift b/14th-team5-iOS/Domain/Sources/Repositories/OAuthRepository.swift similarity index 81% rename from 14th-team5-iOS/Domain/Sources/OAuth/Interfaces/Repository/OAuthRepository.swift rename to 14th-team5-iOS/Domain/Sources/Repositories/OAuthRepository.swift index cbb7f6cb3..a4e39f66f 100644 --- a/14th-team5-iOS/Domain/Sources/OAuth/Interfaces/Repository/OAuthRepository.swift +++ b/14th-team5-iOS/Domain/Sources/Repositories/OAuthRepository.swift @@ -10,11 +10,9 @@ import Foundation import RxSwift public protocol OAuthRepositoryProtocol { - var disposeBag: DisposeBag { get } - func refreshAccessToken(body: RefreshAccessTokenRequest) -> Observable func registerNewMember(body: CreateNewMemberRequest) -> Observable func signIn(_ type: SignInType, body: NativeSocialLoginRequest) -> Observable func registerNewFCMToken(body: AddFCMTokenRequest) -> Observable - func deleteFCMToken(fcmToken token: String) -> Observable + func deleteFCMToken() -> Observable } diff --git a/14th-team5-iOS/Domain/Sources/SignIn/UseCases/SignInUseCase.swift b/14th-team5-iOS/Domain/Sources/SignIn/UseCases/SignInUseCase.swift deleted file mode 100644 index cb8ae5c7f..000000000 --- a/14th-team5-iOS/Domain/Sources/SignIn/UseCases/SignInUseCase.swift +++ /dev/null @@ -1,8 +0,0 @@ -// -// SignInUseCase.swift -// Domain -// -// Created by 김건우 on 6/7/24. -// - -import Foundation diff --git a/14th-team5-iOS/Domain/Sources/UseCases/OAuth/DeleteFCMTokenUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/OAuth/DeleteFCMTokenUseCase.swift new file mode 100644 index 000000000..c92be7b0a --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/UseCases/OAuth/DeleteFCMTokenUseCase.swift @@ -0,0 +1,31 @@ +// +// DeleteFCMTokenUseCase.swift +// Domain +// +// Created by 김건우 on 6/15/24. +// + +import Foundation + +import RxSwift + +public protocol DeleteFCMTokenUseCaseProtocol { + func execute() -> Observable +} + +public class DeleteFCMTokenUseCase: DeleteFCMTokenUseCaseProtocol { + + // MARK: - Repositories + private var oauthRepository: OAuthRepositoryProtocol + + // MARK: - Intializer + public init(oauthRepository: OAuthRepositoryProtocol) { + self.oauthRepository = oauthRepository + } + + // MARK: - Execute + public func execute() -> Observable { + return oauthRepository.deleteFCMToken() + } +} + diff --git a/14th-team5-iOS/Domain/Sources/UseCases/OAuth/RefreshAccessTokenUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/OAuth/RefreshAccessTokenUseCase.swift new file mode 100644 index 000000000..79237880d --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/UseCases/OAuth/RefreshAccessTokenUseCase.swift @@ -0,0 +1,30 @@ +// +// RefreshAccessTokenUseCase.swift +// Domain +// +// Created by 김건우 on 6/15/24. +// + +import Foundation + +import RxSwift + +public protocol RefreshAccessTokenUseCaseProtocol { + func execute(body: RefreshAccessTokenRequest) -> Observable +} + +public class RefreshAccessTokenUseCase: RefreshAccessTokenUseCaseProtocol { + + // MARK: - Repositories + private var oauthRepository: OAuthRepositoryProtocol + + // MARK: - Intializer + public init(oauthRepository: OAuthRepositoryProtocol) { + self.oauthRepository = oauthRepository + } + + // MARK: - Execute + public func execute(body: RefreshAccessTokenRequest) -> Observable { + return oauthRepository.refreshAccessToken(body: body) + } +} diff --git a/14th-team5-iOS/Domain/Sources/UseCases/OAuth/RegisterNewFCMTokenUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/OAuth/RegisterNewFCMTokenUseCase.swift new file mode 100644 index 000000000..eff633b07 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/UseCases/OAuth/RegisterNewFCMTokenUseCase.swift @@ -0,0 +1,31 @@ +// +// RegisterNewFCMTokenUseCase.swift +// Domain +// +// Created by 김건우 on 6/15/24. +// + +import Foundation + +import RxSwift + +public protocol RegisterNewFCMTokenUseCaseProtocol { + func execute(body: AddFCMTokenRequest) -> Observable +} + +public class RegisterNewFCMTokenUseCase: RegisterNewFCMTokenUseCaseProtocol { + + // MARK: - Repositories + private var oauthRepository: OAuthRepositoryProtocol + + // MARK: - Intializer + public init(oauthRepository: OAuthRepositoryProtocol) { + self.oauthRepository = oauthRepository + } + + // MARK: - Execute + public func execute(body: AddFCMTokenRequest) -> Observable { + return oauthRepository.registerNewFCMToken(body: body) + } +} + diff --git a/14th-team5-iOS/Domain/Sources/UseCases/OAuth/RegisterNewMemberUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/OAuth/RegisterNewMemberUseCase.swift new file mode 100644 index 000000000..567c1aed1 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/UseCases/OAuth/RegisterNewMemberUseCase.swift @@ -0,0 +1,30 @@ +// +// RegisterNewMemberUseCase.swift +// Domain +// +// Created by 김건우 on 6/15/24. +// + +import Foundation + +import RxSwift + +public protocol RegisterNewMemberUseCaseProtocol { + func execute(body: CreateNewMemberRequest) -> Observable +} + +public class RegisterNewMemberUseCase: RegisterNewMemberUseCaseProtocol { + + // MARK: - Repositories + private var oauthRepository: OAuthRepositoryProtocol + + // MARK: - Intializer + public init(oauthRepository: OAuthRepositoryProtocol) { + self.oauthRepository = oauthRepository + } + + // MARK: - Execute + public func execute(body: CreateNewMemberRequest) -> Observable { + return oauthRepository.registerNewMember(body: body) + } +} diff --git a/14th-team5-iOS/Domain/Sources/UseCases/OAuth/SignInUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/OAuth/SignInUseCase.swift new file mode 100644 index 000000000..c867945e3 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/UseCases/OAuth/SignInUseCase.swift @@ -0,0 +1,33 @@ +// +// SignInUseCase.swift +// Domain +// +// Created by 김건우 on 6/15/24. +// + +import Foundation + +import RxSwift + +public protocol SignInUseCaseProtocol { + func execute(_ type: SignInType, body: NativeSocialLoginRequest) -> Observable +} + +public class SignInUseCase: SignInUseCaseProtocol { + + // MARK: - Repositories + private var oauthRepository: OAuthRepositoryProtocol + + // MARK: - Intializer + public init(oauthRepository: OAuthRepositoryProtocol) { + self.oauthRepository = oauthRepository + } + + // MARK: - Execute + public func execute( + _ type: SignInType, + body: NativeSocialLoginRequest + ) -> Observable { + return oauthRepository.signIn(type, body: body) + } +} From b5b2a43d78f61a1b3ff581895ef8628048eb9e98 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Sat, 15 Jun 2024 16:04:00 +0900 Subject: [PATCH 111/263] =?UTF-8?q?refactor:=20SignInUseCase=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20(#553)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SignIn/Repository/SignInRepository.swift | 9 ++--- .../SignIn}/TokenResultEntity.swift | 0 .../SignInRepository.swift | 4 +-- .../OAuth/RefreshAccessTokenUseCase.swift | 2 +- .../UseCases/SignIn/SocialSignInUseCase.swift | 34 +++++++++++++++++++ .../SignIn/SocialSignOutUseCase.swift | 32 +++++++++++++++++ 6 files changed, 73 insertions(+), 8 deletions(-) rename 14th-team5-iOS/Domain/Sources/{SignIn/Entities => Entities/SignIn}/TokenResultEntity.swift (100%) rename 14th-team5-iOS/Domain/Sources/{SignIn/Interfaces/Repository => Repositories}/SignInRepository.swift (85%) create mode 100644 14th-team5-iOS/Domain/Sources/UseCases/SignIn/SocialSignInUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/UseCases/SignIn/SocialSignOutUseCase.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/SignIn/Repository/SignInRepository.swift b/14th-team5-iOS/Data/Sources/APIs/SignIn/Repository/SignInRepository.swift index 0a823ecee..6f8b1bac2 100644 --- a/14th-team5-iOS/Data/Sources/APIs/SignIn/Repository/SignInRepository.swift +++ b/14th-team5-iOS/Data/Sources/APIs/SignIn/Repository/SignInRepository.swift @@ -29,13 +29,13 @@ extension SignInRepository { public func signIn( with type: SignInType, on window: AnyObject? - ) -> Single { + ) -> Observable { signInApiWorker.signIn(with: type, on: window) - .observe(on: RxSchedulers.main) .do { [weak self] in self?.tokenKeychainStorage.saveIdToken($0?.idToken) self?.tokenKeychainStorage.saveSignInType(type) } + .asObservable() } @@ -47,8 +47,9 @@ extension SignInRepository { else { return .error(RxError.unknown) } // TODO: - Error 타입 정의하기 return signInApiWorker.signOut(with: type) - .observe(on: RxSchedulers.main) - .do(onCompleted: { KeychainWrapper.standard.removeAllKeys() }) + .do(onCompleted: { + KeychainWrapper.standard.removeAllKeys() + }) } diff --git a/14th-team5-iOS/Domain/Sources/SignIn/Entities/TokenResultEntity.swift b/14th-team5-iOS/Domain/Sources/Entities/SignIn/TokenResultEntity.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/SignIn/Entities/TokenResultEntity.swift rename to 14th-team5-iOS/Domain/Sources/Entities/SignIn/TokenResultEntity.swift diff --git a/14th-team5-iOS/Domain/Sources/SignIn/Interfaces/Repository/SignInRepository.swift b/14th-team5-iOS/Domain/Sources/Repositories/SignInRepository.swift similarity index 85% rename from 14th-team5-iOS/Domain/Sources/SignIn/Interfaces/Repository/SignInRepository.swift rename to 14th-team5-iOS/Domain/Sources/Repositories/SignInRepository.swift index 183c15779..04fc5b0ae 100644 --- a/14th-team5-iOS/Domain/Sources/SignIn/Interfaces/Repository/SignInRepository.swift +++ b/14th-team5-iOS/Domain/Sources/Repositories/SignInRepository.swift @@ -5,11 +5,9 @@ // Created by 김건우 on 6/7/24. // -import UIKit - import RxSwift public protocol SignInRepositoryProtocol { - func signIn(with type: SignInType, on window: AnyObject?) -> Single + func signIn(with type: SignInType, on window: AnyObject?) -> Observable func signOut() -> Completable } diff --git a/14th-team5-iOS/Domain/Sources/UseCases/OAuth/RefreshAccessTokenUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/OAuth/RefreshAccessTokenUseCase.swift index 79237880d..9c0a65d76 100644 --- a/14th-team5-iOS/Domain/Sources/UseCases/OAuth/RefreshAccessTokenUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/OAuth/RefreshAccessTokenUseCase.swift @@ -13,7 +13,7 @@ public protocol RefreshAccessTokenUseCaseProtocol { func execute(body: RefreshAccessTokenRequest) -> Observable } -public class RefreshAccessTokenUseCase: RefreshAccessTokenUseCaseProtocol { +public final class RefreshAccessTokenUseCase: RefreshAccessTokenUseCaseProtocol { // MARK: - Repositories private var oauthRepository: OAuthRepositoryProtocol diff --git a/14th-team5-iOS/Domain/Sources/UseCases/SignIn/SocialSignInUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/SignIn/SocialSignInUseCase.swift new file mode 100644 index 000000000..2932090ea --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/UseCases/SignIn/SocialSignInUseCase.swift @@ -0,0 +1,34 @@ +// +// SocialSignInUsecase.swift +// Domain +// +// Created by 김건우 on 6/15/24. +// + +import Foundation + +import RxSwift + +public protocol SocialSignInUseCaseProtocol { + func execute(with type: SignInType, on window: AnyObject?) -> Observable +} + +public final class SocialSignInUseCase: SocialSignInUseCaseProtocol { + + // MARK: - Repositories + private var signInRepository: SignInRepositoryProtocol + + // MARK: - Intializer + public init(signInRepository: SignInRepositoryProtocol) { + self.signInRepository = signInRepository + } + + // MARK: - Execute + public func execute( + with type: SignInType, + on window: AnyObject? + ) -> Observable { + return signInRepository.signIn(with: type, on: window) + } + +} diff --git a/14th-team5-iOS/Domain/Sources/UseCases/SignIn/SocialSignOutUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/SignIn/SocialSignOutUseCase.swift new file mode 100644 index 000000000..b6dc65cf5 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/UseCases/SignIn/SocialSignOutUseCase.swift @@ -0,0 +1,32 @@ +// +// SocialSignOutUseCase.swift +// Domain +// +// Created by 김건우 on 6/15/24. +// + +import Foundation + +import RxSwift + +public protocol SocialSignOutUseCaseProtocol { + func execute() -> Completable +} + +public final class SocialSignOutUseCase: SocialSignOutUseCaseProtocol { + + // MARK: - Repositories + private var signInRepository: SignInRepositoryProtocol + + // MARK: - Intializer + public init(signInRepository: SignInRepositoryProtocol) { + self.signInRepository = signInRepository + } + + // MARK: - Execute + public func execute() -> Completable { + return signInRepository.signOut() + } + +} + From 5fbf562a122c5d8ca02131cd57865996d0cb27da Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Sat, 15 Jun 2024 16:15:59 +0900 Subject: [PATCH 112/263] =?UTF-8?q?refactor:=20PickUseCase=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20(#553)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PickMemberListResponseDTO.swift | 4 +-- .../PickAPI/DataMapping/PickResponseDTO.swift | 2 +- .../APIs/Pick/PickAPI/PickAPIWorker.swift | 12 +++---- .../APIs/Pick/Repository/PickRepository.swift | 6 ++-- .../Pick/PickEntity.swift} | 4 +-- .../Pick/PickMemberListEntity.swift} | 8 ++--- .../Sources/Pick/UseCases/PickUseCase.swift | 12 +++---- .../PickRepository.swift} | 10 +++--- .../UseCases/Pick/PickMemberUseCase.swift | 31 +++++++++++++++++ .../Pick/WhoDidIPickMemberUseCase.swift | 32 ++++++++++++++++++ .../UseCases/Pick/WhoPickedMeUseCase.swift | 33 +++++++++++++++++++ 11 files changed, 124 insertions(+), 30 deletions(-) rename 14th-team5-iOS/Domain/Sources/{Pick/Entities/PickResponse.swift => Entities/Pick/PickEntity.swift} (77%) rename 14th-team5-iOS/Domain/Sources/{Pick/Entities/PickMemberListResponse.swift => Entities/Pick/PickMemberListEntity.swift} (82%) rename 14th-team5-iOS/Domain/Sources/{Pick/Interfaces/Repositories/PickRepositoryProtocol.swift => Repositories/PickRepository.swift} (58%) create mode 100644 14th-team5-iOS/Domain/Sources/UseCases/Pick/PickMemberUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/UseCases/Pick/WhoDidIPickMemberUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/UseCases/Pick/WhoPickedMeUseCase.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/DataMapping/PickMemberListResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/DataMapping/PickMemberListResponseDTO.swift index 7b1cd94e8..217ca268c 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/DataMapping/PickMemberListResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/DataMapping/PickMemberListResponseDTO.swift @@ -35,13 +35,13 @@ extension PickMemberListResponseDTO { } extension PickMemberListResponseDTO { - func toDomain() -> PickMemberListResponse { + func toDomain() -> PickMemberListEntity { return .init(results: results.map { $0.toDomain() }) } } extension PickMemberListResponseDTO.PickMemberDTO { - func toDomain() -> PickMember { + func toDomain() -> PickMemberEntity { return .init( memberId: memberId, name: name, diff --git a/14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/DataMapping/PickResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/DataMapping/PickResponseDTO.swift index b3e0848e2..9f49ef6f4 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/DataMapping/PickResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/DataMapping/PickResponseDTO.swift @@ -16,7 +16,7 @@ public struct PickResponseDTO: Decodable { } extension PickResponseDTO { - func toDomain() -> PickResponse { + func toDomain() -> PickEntity { return .init(success: success) } } diff --git a/14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/PickAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/PickAPIWorker.swift index 78058a1e7..cb628af35 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/PickAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/PickAPIWorker.swift @@ -33,7 +33,7 @@ extension PickAPIWorker { // MARK: - Pick Member - private func pickMember(spec: APISpec, headers: [APIHeader]?) -> Single { + private func pickMember(spec: APISpec, headers: [APIHeader]?) -> Single { return request(spec: spec, headers: headers) .subscribe(on: Self.queue) .do { @@ -47,7 +47,7 @@ extension PickAPIWorker { .asSingle() } - public func pickMember(memberId: String) -> Single { + public func pickMember(memberId: String) -> Single { let spec = PickAPIs.pick(memberId).spec return Observable.just(()) @@ -62,7 +62,7 @@ extension PickAPIWorker { // MARK: - Who Did I Pick? - private func fetchWhoDidIPickMember(spec: APISpec, headers: [APIHeader]?) -> Single { + private func fetchWhoDidIPickMember(spec: APISpec, headers: [APIHeader]?) -> Single { return request(spec: spec, headers: headers) .subscribe(on: Self.queue) .do { @@ -76,7 +76,7 @@ extension PickAPIWorker { .asSingle() } - public func fetchWhoDidIPickMember(memberId: String) -> Single { + public func fetchWhoDidIPickMember(memberId: String) -> Single { let spec = PickAPIs.whoDidIPick(memberId).spec return Observable.just(()) @@ -91,7 +91,7 @@ extension PickAPIWorker { // MARK: - Who Picked Me? - private func fetchWhoPickedMeMember(spec: APISpec, headers: [APIHeader]?) -> Single { + private func fetchWhoPickedMeMember(spec: APISpec, headers: [APIHeader]?) -> Single { return request(spec: spec, headers: headers) .subscribe(on: Self.queue) .do { @@ -105,7 +105,7 @@ extension PickAPIWorker { .asSingle() } - public func fetchWhoPickedMeMember(memberId: String) -> Single { + public func fetchWhoPickedMeMember(memberId: String) -> Single { let spec = PickAPIs.whoPickedMe(memberId).spec return Observable.just(()) diff --git a/14th-team5-iOS/Data/Sources/APIs/Pick/Repository/PickRepository.swift b/14th-team5-iOS/Data/Sources/APIs/Pick/Repository/PickRepository.swift index ff35506bb..8d42c0ddf 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Pick/Repository/PickRepository.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Pick/Repository/PickRepository.swift @@ -20,17 +20,17 @@ public final class PickRepository: PickRepositoryProtocol { } extension PickRepository { - public func pickMember(memberId: String) -> Observable { + public func pickMember(memberId: String) -> Observable { return pickApiWorker.pickMember(memberId: memberId) .asObservable() } - public func whoDidIPickMember(memberId: String) -> Observable { + public func whoDidIPickMember(memberId: String) -> Observable { return pickApiWorker.fetchWhoDidIPickMember(memberId: memberId) .asObservable() } - public func whoPickedMeMember(memberId: String) -> Observable { + public func whoPickedMeMember(memberId: String) -> Observable { return pickApiWorker.fetchWhoPickedMeMember(memberId: memberId) .asObservable() } diff --git a/14th-team5-iOS/Domain/Sources/Pick/Entities/PickResponse.swift b/14th-team5-iOS/Domain/Sources/Entities/Pick/PickEntity.swift similarity index 77% rename from 14th-team5-iOS/Domain/Sources/Pick/Entities/PickResponse.swift rename to 14th-team5-iOS/Domain/Sources/Entities/Pick/PickEntity.swift index ec74f921e..b43be1710 100644 --- a/14th-team5-iOS/Domain/Sources/Pick/Entities/PickResponse.swift +++ b/14th-team5-iOS/Domain/Sources/Entities/Pick/PickEntity.swift @@ -1,5 +1,5 @@ // -// PickResponse.swift +// PickEntity.swift // Domain // // Created by 김건우 on 4/15/24. @@ -7,7 +7,7 @@ import Foundation -public struct PickResponse { +public struct PickEntity { public var success: Bool public init(success: Bool) { diff --git a/14th-team5-iOS/Domain/Sources/Pick/Entities/PickMemberListResponse.swift b/14th-team5-iOS/Domain/Sources/Entities/Pick/PickMemberListEntity.swift similarity index 82% rename from 14th-team5-iOS/Domain/Sources/Pick/Entities/PickMemberListResponse.swift rename to 14th-team5-iOS/Domain/Sources/Entities/Pick/PickMemberListEntity.swift index f880527c2..9c2c2e5bc 100644 --- a/14th-team5-iOS/Domain/Sources/Pick/Entities/PickMemberListResponse.swift +++ b/14th-team5-iOS/Domain/Sources/Entities/Pick/PickMemberListEntity.swift @@ -7,15 +7,15 @@ import Foundation -public struct PickMemberListResponse { - public var results: [PickMember] +public struct PickMemberListEntity { + public var results: [PickMemberEntity] - public init(results: [PickMember]) { + public init(results: [PickMemberEntity]) { self.results = results } } -public struct PickMember { +public struct PickMemberEntity { public var memberId: String public var name: String public var imageUrl: String diff --git a/14th-team5-iOS/Domain/Sources/Pick/UseCases/PickUseCase.swift b/14th-team5-iOS/Domain/Sources/Pick/UseCases/PickUseCase.swift index da1f51d73..f859dc831 100644 --- a/14th-team5-iOS/Domain/Sources/Pick/UseCases/PickUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/Pick/UseCases/PickUseCase.swift @@ -10,9 +10,9 @@ import Foundation import RxSwift public protocol PickUseCaseProtocol { - func executePickMember(memberId: String) -> Observable - func executeFetchWhoDidIPickMember(memberId: String) -> Observable - func executeFetchWhoPickedMeMember(memberId: String) -> Observable + func executePickMember(memberId: String) -> Observable + func executeFetchWhoDidIPickMember(memberId: String) -> Observable + func executeFetchWhoPickedMeMember(memberId: String) -> Observable } public final class PickUseCase: PickUseCaseProtocol { @@ -22,15 +22,15 @@ public final class PickUseCase: PickUseCaseProtocol { self.pickRepository = pickRepository } - public func executePickMember(memberId: String) -> Observable { + public func executePickMember(memberId: String) -> Observable { return pickRepository.pickMember(memberId: memberId) } - public func executeFetchWhoDidIPickMember(memberId: String) -> Observable { + public func executeFetchWhoDidIPickMember(memberId: String) -> Observable { return pickRepository.whoDidIPickMember(memberId: memberId) } - public func executeFetchWhoPickedMeMember(memberId: String) -> Observable { + public func executeFetchWhoPickedMeMember(memberId: String) -> Observable { return pickRepository.whoPickedMeMember(memberId: memberId) } } diff --git a/14th-team5-iOS/Domain/Sources/Pick/Interfaces/Repositories/PickRepositoryProtocol.swift b/14th-team5-iOS/Domain/Sources/Repositories/PickRepository.swift similarity index 58% rename from 14th-team5-iOS/Domain/Sources/Pick/Interfaces/Repositories/PickRepositoryProtocol.swift rename to 14th-team5-iOS/Domain/Sources/Repositories/PickRepository.swift index 725a399c8..edc70819c 100644 --- a/14th-team5-iOS/Domain/Sources/Pick/Interfaces/Repositories/PickRepositoryProtocol.swift +++ b/14th-team5-iOS/Domain/Sources/Repositories/PickRepository.swift @@ -1,5 +1,5 @@ // -// PickRepositoryProtocol.swift +// PickRepository.swift // Domain // // Created by 김건우 on 4/15/24. @@ -10,9 +10,7 @@ import Foundation import RxSwift public protocol PickRepositoryProtocol { - var disposeBag: DisposeBag { get } - - func pickMember(memberId: String) -> Observable - func whoDidIPickMember(memberId: String) -> Observable - func whoPickedMeMember(memberId: String) -> Observable + func pickMember(memberId: String) -> Observable + func whoDidIPickMember(memberId: String) -> Observable + func whoPickedMeMember(memberId: String) -> Observable } diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Pick/PickMemberUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Pick/PickMemberUseCase.swift new file mode 100644 index 000000000..cd74b977a --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/UseCases/Pick/PickMemberUseCase.swift @@ -0,0 +1,31 @@ +// +// PickMemberUseCase.swift +// Domain +// +// Created by 김건우 on 6/15/24. +// + +import Foundation + +import RxSwift + +public protocol PickMemberUseCaseProtocol { + func execute(memberId: String) -> Observable +} + +public final class PickMemberUseCase: PickMemberUseCaseProtocol { + + // MARK: - Repositories + private var pickRepository: PickRepositoryProtocol + + // MARK: - Intializer + public init(pickRepository: PickRepositoryProtocol) { + self.pickRepository = pickRepository + } + + // MARK: - Execute + public func execute(memberId: String) -> Observable { + return pickRepository.pickMember(memberId: memberId) + } + +} diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Pick/WhoDidIPickMemberUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Pick/WhoDidIPickMemberUseCase.swift new file mode 100644 index 000000000..f3526ca22 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/UseCases/Pick/WhoDidIPickMemberUseCase.swift @@ -0,0 +1,32 @@ +// +// WhoDidIPickMemberUseCase.swift +// Domain +// +// Created by 김건우 on 6/15/24. +// + +import Foundation + +import RxSwift + +public protocol WhoDidIPickMemberUseCaseProtocol { + func execute(memberId: String) -> Observable +} + +public final class WhoDidIPickMemberUseCase: WhoDidIPickMemberUseCaseProtocol { + + // MARK: - Repositories + private var pickRepository: PickRepositoryProtocol + + // MARK: - Intializer + public init(pickRepository: PickRepositoryProtocol) { + self.pickRepository = pickRepository + } + + // MARK: - Execute + public func execute(memberId: String) -> Observable { + return pickRepository.whoDidIPickMember(memberId: memberId) + } + +} + diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Pick/WhoPickedMeUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Pick/WhoPickedMeUseCase.swift new file mode 100644 index 000000000..869ba1a91 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/UseCases/Pick/WhoPickedMeUseCase.swift @@ -0,0 +1,33 @@ +// +// WhoPickedMeUseCase.swift +// Domain +// +// Created by 김건우 on 6/15/24. +// + +import Foundation + +import RxSwift + +public protocol WhoPickedMeMemberUseCaseProtocol { + func execute(memberId: String) -> Observable +} + +public final class WhoPickedMeUseCase: WhoPickedMeMemberUseCaseProtocol { + + // MARK: - Repositories + private var pickRepository: PickRepositoryProtocol + + // MARK: - Intializer + public init(pickRepository: PickRepositoryProtocol) { + self.pickRepository = pickRepository + } + + // MARK: - Execute + public func execute(memberId: String) -> Observable { + return pickRepository.whoPickedMeMember (memberId: memberId) + } + +} + + From 97bfc0a34cd7d82da4d49cf5b0ecddffb1163c71 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Sat, 15 Jun 2024 17:00:52 +0900 Subject: [PATCH 113/263] =?UTF-8?q?refactor:=20CommentUseCase=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20(#553)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Reactor/CommentCellReactor.swift | 2 +- .../Comment/CommentAPI/CommentAPIWorker.swift | 76 ++++++------------- ...nationResponsePostCommentResponseDTO.swift | 4 +- .../PostCommentDeleteResponseDTO.swift | 2 +- .../DataMapping/PostCommentResponseDTO.swift | 2 +- .../Repository/CommentRepository.swift | 24 +++++- .../Comment}/CreatePostCommentRequest.swift | 0 ...PaginationResponsePostCommentEntity.swift} | 8 +- .../Comment/PostCommentDeleteEntity.swift} | 2 +- .../Comment}/PostCommentPaginationQuery.swift | 0 .../Comment}/UpdatePostCommentRequest.swift | 0 .../Family/UseCases/FamilyUseCase.swift | 2 + .../Sources/Pick/UseCases/PickUseCase.swift | 2 + .../UseCase/PostCommentUseCase.swift | 15 ++-- .../Repositories/CommentRepository.swift | 10 +-- .../Comment/CreateCommentUseCase.swift | 34 +++++++++ .../Comment/DeleteCommentUseCase.swift | 35 +++++++++ .../Comment/FetchCommentUseCase.swift | 34 +++++++++ .../Comment/UpdateCommentUseCase.swift | 36 +++++++++ 19 files changed, 208 insertions(+), 80 deletions(-) rename 14th-team5-iOS/Domain/Sources/{PostComment/Entities => Entities/Comment}/CreatePostCommentRequest.swift (100%) rename 14th-team5-iOS/Domain/Sources/{PostComment/Entities/PaginationResponsePostCommentResponse.swift => Entities/Comment/PaginationResponsePostCommentEntity.swift} (85%) rename 14th-team5-iOS/Domain/Sources/{PostComment/Entities/PostCommentDeleteResponse.swift => Entities/Comment/PostCommentDeleteEntity.swift} (83%) rename 14th-team5-iOS/Domain/Sources/{PostComment/Entities => Entities/Comment}/PostCommentPaginationQuery.swift (100%) rename 14th-team5-iOS/Domain/Sources/{PostComment/Entities => Entities/Comment}/UpdatePostCommentRequest.swift (100%) rename 14th-team5-iOS/Domain/Sources/{PostComment/Interfaces => }/Repositories/CommentRepository.swift (62%) create mode 100644 14th-team5-iOS/Domain/Sources/UseCases/Comment/CreateCommentUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/UseCases/Comment/DeleteCommentUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/UseCases/Comment/FetchCommentUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/UseCases/Comment/UpdateCommentUseCase.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/PostComment/Reactor/CommentCellReactor.swift b/14th-team5-iOS/App/Sources/Presentation/PostComment/Reactor/CommentCellReactor.swift index 142594a5a..c252ec1e8 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostComment/Reactor/CommentCellReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostComment/Reactor/CommentCellReactor.swift @@ -48,7 +48,7 @@ final public class CommentCellReactor: Reactor { // MARK: - Intializer public init( - _ commentResponse: PostCommentResponse, + _ commentResponse: PostCommentEntity, memberUseCase: MemberUseCaseProtocol, postCommentUseCase: PostCommentUseCaseProtocol, provider: GlobalStateProviderProtocol 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 5971a8770..ad71f37ca 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/CommentAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/CommentAPIWorker.swift @@ -33,22 +33,16 @@ extension CommentAPIWorker { // MARK: - Fetch Comment - public func fetchComment(postId: String, query: PostCommentPaginationQuery) -> Single { + public func fetchComment( + postId: String, + query: PostCommentPaginationQuery + ) -> Single { let page = query.page let size = query.size let sort = query.sort.rawValue let spec = CommentAPIs.fetchPostComment(postId, page, size, sort).spec - return Observable.just(()) - .withLatestFrom(self._headers) - .observe(on: Self.queue) - .withUnretained(self) - .flatMap { $0.0.fetchComment(spec: spec, headers: $0.1) } - .asSingle() - } - - private func fetchComment(spec: APISpec, headers: [APIHeader]?) -> Single { - return request(spec: spec, headers: headers) + return request(spec: spec) .subscribe(on: Self.queue) .do { if let str = String(data: $0.1, encoding: .utf8) { @@ -57,27 +51,19 @@ extension CommentAPIWorker { } .map(PaginationResponsePostCommentResponseDTO.self) .catchAndReturn(nil) - .map { $0?.toDomain() } .asSingle() } - // MARK: - Create Comment - public func createComment(postId: String, body: CreatePostCommentReqeustDTO) -> Single { + public func createComment( + postId: String, + body: CreatePostCommentReqeustDTO + ) -> Single { let spec = CommentAPIs.createPostComment(postId).spec - return Observable.just(()) - .withLatestFrom(self._headers) - .observe(on: Self.queue) - .withUnretained(self) - .flatMap { $0.0.createComment(spec: spec, headers: $0.1, jsonEncodable: body) } - .asSingle() - } - - private func createComment(spec: APISpec, headers: [APIHeader]?, jsonEncodable body: Encodable) -> Single { - return request(spec: spec, headers: headers, jsonEncodable: body) + return request(spec: spec, jsonEncodable: body) .subscribe(on: Self.queue) .do { if let str = String(data: $0.1, encoding: .utf8) { @@ -86,27 +72,20 @@ extension CommentAPIWorker { } .map(PostCommentResponseDTO.self) .catchAndReturn(nil) - .map { $0?.toDomain() } .asSingle() } - // MARK: - Update Comment - public func updateComment(postId: String, commentId: String, body: UpdatePostCommentReqeustDTO) -> Single { + public func updateComment( + postId: String, + commentId: String, + body: UpdatePostCommentReqeustDTO + ) -> Single { let spec = CommentAPIs.updatePostComment(postId, commentId).spec - return Observable.just(()) - .withLatestFrom(self._headers) - .observe(on: Self.queue) - .withUnretained(self) - .flatMap { $0.0.updateComment(spec: spec, headers: $0.1, jsonEncodable: body) } - .asSingle() - } - - private func updateComment(spec: APISpec, headers: [APIHeader]?, jsonEncodable body: Encodable) -> Single { - return request(spec: spec, headers: headers, jsonEncodable: body) + return request(spec: spec, jsonEncodable: body) .subscribe(on: Self.queue) .do { if let str = String(data: $0.1, encoding: .utf8) { @@ -115,28 +94,19 @@ extension CommentAPIWorker { } .map(PostCommentResponseDTO.self) .catchAndReturn(nil) - .map { $0?.toDomain() } .asSingle() } + // MARK: - Delete Comment - - // MARK: - Delete Commen - - public func deleteComment(postId: String, commentId: String) -> Single { + public func deleteComment( + postId: String, + commentId: String + ) -> Single { let spec = CommentAPIs.deletePostComment(postId, commentId).spec - return Observable.just(()) - .withLatestFrom(self._headers) - .observe(on: Self.queue) - .withUnretained(self) - .flatMap { $0.0.deleteComment(spec: spec, headers: $0.1) } - .asSingle() - } - - private func deleteComment(spec: APISpec, headers: [APIHeader]?) -> Single { - return request(spec: spec, headers: headers) + return request(spec: spec) .subscribe(on: Self.queue) .do { if let str = String(data: $0.1, encoding: .utf8) { @@ -145,7 +115,7 @@ extension CommentAPIWorker { } .map(PostCommentDeleteResponseDTO.self) .catchAndReturn(nil) - .map { $0?.toDomain() } .asSingle() } + } diff --git a/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/PaginationResponsePostCommentResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/PaginationResponsePostCommentResponseDTO.swift index 9f3302afc..b1e7d3d31 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/PaginationResponsePostCommentResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/PaginationResponsePostCommentResponseDTO.swift @@ -41,7 +41,7 @@ extension PaginationResponsePostCommentResponseDTO { } extension PaginationResponsePostCommentResponseDTO { - func toDomain() -> PaginationResponsePostCommentResponse { + func toDomain() -> PaginationResponsePostCommentEntity { return .init( currentPage: currentPage, totalPage: totalPage, @@ -53,7 +53,7 @@ extension PaginationResponsePostCommentResponseDTO { } extension PaginationResponsePostCommentResponseDTO.PostCommentResponseDTO { - func toDomain() -> PostCommentResponse { + func toDomain() -> PostCommentEntity { return .init( commentId: commentId, postId: postId, diff --git a/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/PostCommentDeleteResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/PostCommentDeleteResponseDTO.swift index 53abb27e2..7ce370f22 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/PostCommentDeleteResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/PostCommentDeleteResponseDTO.swift @@ -13,7 +13,7 @@ public struct PostCommentDeleteResponseDTO: Decodable { } extension PostCommentDeleteResponseDTO { - func toDomain() -> PostCommentDeleteResponse { + func toDomain() -> PostCommentDeleteEntity { return .init(success: success) } } diff --git a/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/PostCommentResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/PostCommentResponseDTO.swift index 2ac895ce3..c9221ad1b 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/PostCommentResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/PostCommentResponseDTO.swift @@ -24,7 +24,7 @@ public struct PostCommentResponseDTO: Decodable { } extension PostCommentResponseDTO { - func toDomain() -> PostCommentResponse { + func toDomain() -> PostCommentEntity { return .init( commentId: commentId, postId: postId, 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 e42acc84f..5275cd123 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Comment/Repository/CommentRepository.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Comment/Repository/CommentRepository.swift @@ -19,25 +19,41 @@ public final class CommentRepository: CommentRepositoryProtocol { } extension CommentRepository { - public func fetchPostComment(postId: String, query: PostCommentPaginationQuery) -> Observable { + + // MARK: - Fetch Comment + + public func fetchPostComment(postId: String, query: PostCommentPaginationQuery) -> Observable { return commentApiWorker.fetchComment(postId: postId, query: query) + .map { $0?.toDomain() } .asObservable() } - public func createPostComment(postId: String, body: CreatePostCommentRequest) -> Observable { + + // MARK: - Create Comment + + 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() } - public func updatePostComment(postId: String, commentId: String, body: UpdatePostCommentRequest) -> Observable { + + // MARK: - Update Comment + + 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() } - public func deletePostComment(postId: String, commentId: String) -> Observable { + + // MARK: - Delete Comment + + public func deletePostComment(postId: String, commentId: String) -> Observable { return commentApiWorker.deleteComment(postId: postId, commentId: commentId) + .map { $0?.toDomain() } .asObservable() } } diff --git a/14th-team5-iOS/Domain/Sources/PostComment/Entities/CreatePostCommentRequest.swift b/14th-team5-iOS/Domain/Sources/Entities/Comment/CreatePostCommentRequest.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/PostComment/Entities/CreatePostCommentRequest.swift rename to 14th-team5-iOS/Domain/Sources/Entities/Comment/CreatePostCommentRequest.swift diff --git a/14th-team5-iOS/Domain/Sources/PostComment/Entities/PaginationResponsePostCommentResponse.swift b/14th-team5-iOS/Domain/Sources/Entities/Comment/PaginationResponsePostCommentEntity.swift similarity index 85% rename from 14th-team5-iOS/Domain/Sources/PostComment/Entities/PaginationResponsePostCommentResponse.swift rename to 14th-team5-iOS/Domain/Sources/Entities/Comment/PaginationResponsePostCommentEntity.swift index 9644c9ef7..145fd2f21 100644 --- a/14th-team5-iOS/Domain/Sources/PostComment/Entities/PaginationResponsePostCommentResponse.swift +++ b/14th-team5-iOS/Domain/Sources/Entities/Comment/PaginationResponsePostCommentEntity.swift @@ -7,19 +7,19 @@ import Foundation -public struct PaginationResponsePostCommentResponse { +public struct PaginationResponsePostCommentEntity { public var currentPage: Int public var totalPage: Int public var itemPerPage: Int public var hasNext: Bool - public var results: [PostCommentResponse] + public var results: [PostCommentEntity] public init( currentPage: Int, totalPage: Int, itemPerPage: Int, hasNext: Bool, - results: [PostCommentResponse] + results: [PostCommentEntity] ) { self.currentPage = currentPage self.totalPage = totalPage @@ -29,7 +29,7 @@ public struct PaginationResponsePostCommentResponse { } } -public struct PostCommentResponse { +public struct PostCommentEntity { public var commentId: String public var postId: String public var memberId: String diff --git a/14th-team5-iOS/Domain/Sources/PostComment/Entities/PostCommentDeleteResponse.swift b/14th-team5-iOS/Domain/Sources/Entities/Comment/PostCommentDeleteEntity.swift similarity index 83% rename from 14th-team5-iOS/Domain/Sources/PostComment/Entities/PostCommentDeleteResponse.swift rename to 14th-team5-iOS/Domain/Sources/Entities/Comment/PostCommentDeleteEntity.swift index cc45a9cba..f870d9893 100644 --- a/14th-team5-iOS/Domain/Sources/PostComment/Entities/PostCommentDeleteResponse.swift +++ b/14th-team5-iOS/Domain/Sources/Entities/Comment/PostCommentDeleteEntity.swift @@ -7,7 +7,7 @@ import Foundation -public struct PostCommentDeleteResponse { +public struct PostCommentDeleteEntity { public var success: Bool public init(success: Bool) { diff --git a/14th-team5-iOS/Domain/Sources/PostComment/Entities/PostCommentPaginationQuery.swift b/14th-team5-iOS/Domain/Sources/Entities/Comment/PostCommentPaginationQuery.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/PostComment/Entities/PostCommentPaginationQuery.swift rename to 14th-team5-iOS/Domain/Sources/Entities/Comment/PostCommentPaginationQuery.swift diff --git a/14th-team5-iOS/Domain/Sources/PostComment/Entities/UpdatePostCommentRequest.swift b/14th-team5-iOS/Domain/Sources/Entities/Comment/UpdatePostCommentRequest.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/PostComment/Entities/UpdatePostCommentRequest.swift rename to 14th-team5-iOS/Domain/Sources/Entities/Comment/UpdatePostCommentRequest.swift diff --git a/14th-team5-iOS/Domain/Sources/Family/UseCases/FamilyUseCase.swift b/14th-team5-iOS/Domain/Sources/Family/UseCases/FamilyUseCase.swift index 3f3d41084..cd96c8a81 100644 --- a/14th-team5-iOS/Domain/Sources/Family/UseCases/FamilyUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/Family/UseCases/FamilyUseCase.swift @@ -9,6 +9,7 @@ import Foundation import RxSwift +@available(*, deprecated) public protocol FamilyUseCaseProtocol { func executeJoinFamily(body: JoinFamilyRequest) -> Observable func executeResignFamily() -> Observable @@ -19,6 +20,7 @@ public protocol FamilyUseCaseProtocol { func executeFetchPaginationFamilyMembers(memberIds: [String]) -> [FamilyMemberProfileEntity] } +@available(*, deprecated) public final class FamilyUseCase: FamilyUseCaseProtocol { private let familyRepository: FamilyRepositoryProtocol diff --git a/14th-team5-iOS/Domain/Sources/Pick/UseCases/PickUseCase.swift b/14th-team5-iOS/Domain/Sources/Pick/UseCases/PickUseCase.swift index f859dc831..0c4a0992e 100644 --- a/14th-team5-iOS/Domain/Sources/Pick/UseCases/PickUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/Pick/UseCases/PickUseCase.swift @@ -9,12 +9,14 @@ import Foundation import RxSwift +@available(*, deprecated) public protocol PickUseCaseProtocol { func executePickMember(memberId: String) -> Observable func executeFetchWhoDidIPickMember(memberId: String) -> Observable func executeFetchWhoPickedMeMember(memberId: String) -> Observable } +@available(*, deprecated) public final class PickUseCase: PickUseCaseProtocol { private let pickRepository: PickRepositoryProtocol diff --git a/14th-team5-iOS/Domain/Sources/PostComment/UseCase/PostCommentUseCase.swift b/14th-team5-iOS/Domain/Sources/PostComment/UseCase/PostCommentUseCase.swift index 1c98dd8f6..9c06dc7c4 100644 --- a/14th-team5-iOS/Domain/Sources/PostComment/UseCase/PostCommentUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/PostComment/UseCase/PostCommentUseCase.swift @@ -10,13 +10,14 @@ import Foundation import RxSwift - +@available(*, deprecated) public protocol PostCommentUseCaseProtocol { - func executeFetchPostComment(postId: String, query: PostCommentPaginationQuery) -> Observable - func executeCreatePostComment(postId: String, body: CreatePostCommentRequest) -> Observable - func executeDeletePostComment(postId: String, commentId: String) -> Observable + func executeFetchPostComment(postId: String, query: PostCommentPaginationQuery) -> Observable + func executeCreatePostComment(postId: String, body: CreatePostCommentRequest) -> Observable + func executeDeletePostComment(postId: String, commentId: String) -> Observable } +@available(*, deprecated) public final class PostCommentUseCase: PostCommentUseCaseProtocol { private let commentRepository: CommentRepositoryProtocol @@ -26,15 +27,15 @@ 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) } - public func executeCreatePostComment(postId: String, body: CreatePostCommentRequest) -> Observable { + public func executeCreatePostComment(postId: String, body: CreatePostCommentRequest) -> Observable { return commentRepository.createPostComment(postId: postId, body: body) } - public func executeDeletePostComment(postId: String, commentId: String) -> Observable { + public func executeDeletePostComment(postId: String, commentId: String) -> Observable { return commentRepository.deletePostComment(postId: postId, commentId: commentId) } } diff --git a/14th-team5-iOS/Domain/Sources/PostComment/Interfaces/Repositories/CommentRepository.swift b/14th-team5-iOS/Domain/Sources/Repositories/CommentRepository.swift similarity index 62% rename from 14th-team5-iOS/Domain/Sources/PostComment/Interfaces/Repositories/CommentRepository.swift rename to 14th-team5-iOS/Domain/Sources/Repositories/CommentRepository.swift index ee6751f07..72d38153d 100644 --- a/14th-team5-iOS/Domain/Sources/PostComment/Interfaces/Repositories/CommentRepository.swift +++ b/14th-team5-iOS/Domain/Sources/Repositories/CommentRepository.swift @@ -10,10 +10,8 @@ import Foundation import RxSwift public protocol CommentRepositoryProtocol { - var disposeBag: DisposeBag { get } - - 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 + 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/UseCases/Comment/CreateCommentUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Comment/CreateCommentUseCase.swift new file mode 100644 index 000000000..eb1e2ac5a --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/UseCases/Comment/CreateCommentUseCase.swift @@ -0,0 +1,34 @@ +// +// CreateCommentUseCase.swift +// Domain +// +// Created by 김건우 on 6/15/24. +// + +import Foundation + +import RxSwift + +public protocol CreateCommentUseCaseProtocol { + func execute(postId: String, body: CreatePostCommentRequest) -> Observable +} + +public final class CreateCommentUseCase: CreateCommentUseCaseProtocol { + + // MARK: - Repositories + private let commentRepository: CommentRepositoryProtocol + + // MARK: - Intializer + public init(commentRepository: CommentRepositoryProtocol) { + self.commentRepository = commentRepository + } + + // MARK: - Execute + public func execute( + postId: String, + body: CreatePostCommentRequest + ) -> Observable { + return commentRepository.createPostComment(postId: postId, body: body) + } + +} diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Comment/DeleteCommentUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Comment/DeleteCommentUseCase.swift new file mode 100644 index 000000000..c0ad0580e --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/UseCases/Comment/DeleteCommentUseCase.swift @@ -0,0 +1,35 @@ +// +// DeleteCommentUseCase.swift +// Domain +// +// Created by 김건우 on 6/15/24. +// + +import Foundation + +import RxSwift + +public protocol DeleteCommentUseCaseProtocol { + func execute(postId: String, commentId: String) -> Observable +} + +public final class DeleteCommentUseCase: DeleteCommentUseCaseProtocol { + + // MARK: - Repositories + private let commentRepository: CommentRepositoryProtocol + + // MARK: - Intializer + public init(commentRepository: CommentRepositoryProtocol) { + self.commentRepository = commentRepository + } + + // MARK: - Execute + public func execute( + postId: String, + commentId: String + ) -> Observable { + return commentRepository.deletePostComment(postId: postId, commentId: commentId) + } + +} + diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Comment/FetchCommentUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Comment/FetchCommentUseCase.swift new file mode 100644 index 000000000..f98f8a3b6 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/UseCases/Comment/FetchCommentUseCase.swift @@ -0,0 +1,34 @@ +// +// FetchCommentUseCase.swift +// Domain +// +// Created by 김건우 on 6/15/24. +// + +import Foundation + +import RxSwift + +public protocol FetchCommentUseCaseProtocol { + func execute(postId: String, query: PostCommentPaginationQuery) -> Observable +} + +public final class FetchCommentUseCase: FetchCommentUseCaseProtocol { + + // MARK: - Repositories + private let commentRepository: CommentRepositoryProtocol + + // MARK: - Intializer + public init(commentRepository: CommentRepositoryProtocol) { + self.commentRepository = commentRepository + } + + // MARK: - Execute + public func execute( + postId: String, + query: PostCommentPaginationQuery + ) -> Observable { + return commentRepository.fetchPostComment(postId: postId, query: query) + } + +} diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Comment/UpdateCommentUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Comment/UpdateCommentUseCase.swift new file mode 100644 index 000000000..f571d6c0c --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/UseCases/Comment/UpdateCommentUseCase.swift @@ -0,0 +1,36 @@ +// +// UpdateCommentUseCase.swift +// Domain +// +// Created by 김건우 on 6/15/24. +// + +import Foundation + +import RxSwift + +public protocol UpdateCommentUseCaseProtocol { + func execute(postId: String, commentId: String, body: UpdatePostCommentRequest) -> Observable +} + +public final class UpdateCommentUseCase: UpdateCommentUseCaseProtocol { + + // MARK: - Repositories + private let commentRepository: CommentRepositoryProtocol + + // MARK: - Intializer + public init(commentRepository: CommentRepositoryProtocol) { + self.commentRepository = commentRepository + } + + // MARK: - Execute + public func execute( + postId: String, + commentId: String, + body: UpdatePostCommentRequest + ) -> Observable { + return commentRepository.updatePostComment(postId: postId, commentId: commentId, body: body) + } + +} + From 336ebee7f2d2635cfbad1de133726cba32aeb1c0 Mon Sep 17 00:00:00 2001 From: Do-hyun-Kim Date: Sun, 16 Jun 2024 18:39:15 +0900 Subject: [PATCH 114/263] =?UTF-8?q?refactor:=20Camera=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?UseCase=20=EC=B1=85=EC=9E=84=20=EB=B6=84=EB=A6=AC=20-=20Camera?= =?UTF-8?q?=20Entity=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?-=20Members=20UseCase=20=EC=B1=85=EC=9E=84=20=EB=B6=84=EB=A6=AC?= =?UTF-8?q?=20-=20Members=20Entity=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Camera/Dependency/CameraDIContainer.swift | 15 +-- .../Dependency/CameraDisplayDIContainer.swift | 16 +-- .../Reactor/CameraDisplayViewReactor.swift | 28 +++-- .../Camera/Reactor/CameraViewReactor.swift | 101 ++++++++++++------ .../Dependency/ProfileDIContainer.swift | 21 ++-- .../Profile/Reactor/ProfileViewReactor.swift | 53 ++++++--- .../AccountRepository/AccountRepository.swift | 4 +- .../CameraCreateRealEmojiResponseDTO.swift | 2 +- .../CameraDisplayImageResponseDTO.swift | 2 +- .../CameraDisplayPostResponseDTO.swift | 4 +- .../CameraRealEmojiImageItemResponseDTO.swift | 6 +- .../CameraRealEmojiPreSignedResponseDTO.swift | 2 +- .../CameraTodayMissionResponseDTO.swift | 2 +- .../CameraUpdateRealEmojiResponseDTO.swift | 2 +- .../Camera/Repository/CameraRepository.swift | 58 +++------- .../MembersProfileResponseDTO.swift | 2 +- .../Repositories/MembersRepository.swift | 24 +---- ...wift => CameraCreateRealEmojiEntity.swift} | 15 ++- .../Entity/CameraDisplayImageResponse.swift | 19 ---- ...tResponse.swift => CameraPostEntity.swift} | 26 +++-- .../Camera/Entity/CameraPreSignedEntity.swift | 16 +++ ...t => CameraRealEmojiImageItemEntity.swift} | 14 ++- .../CameraRealEmojiPreSignedEntity.swift | 16 +++ .../CameraRealEmojiPreSignedResponse.swift | 17 --- .../Entity/CameraTodayMissionResponse.swift | 22 ---- .../Entity/CameraTodayMssionEntity.swift | 21 ++++ ...wift => CameraUpdateRealEmojiEntity.swift} | 13 ++- .../Interfaces/CameraRepositoryProtocol.swift | 22 ++-- .../UseCases/CameraDisplayViewUseCase.swift | 47 -------- .../Camera/UseCases/CameraViewUseCase.swift | 78 -------------- .../UseCases/CreateCameraImageUseCase.swift | 29 +++++ .../Camera/UseCases/CreateCameraUseCase.swift | 32 ++++++ .../EditCameraProfileImageUseCase.swift | 36 +++++++ .../FetchCameraRealEmojiListUseCase.swift | 31 ++++++ .../FetchCameraRealEmojiUpdateUseCase.swift | 29 +++++ .../FetchCameraRealEmojiUploadUseCase.swift | 29 +++++ .../FetchCameraRealEmojiUseCase.swift | 34 ++++++ .../FetchCameraTodayMissionUseCase.swift | 32 ++++++ .../FetchCameraUploadImageUseCase.swift | 30 ++++++ ...ponse.swift => MembersProfileEntity.swift} | 17 +-- .../MembersRepositoryProtocol.swift | 8 +- .../DeleteMembersProfileUseCase.swift | 30 ++++++ .../UseCases/FetchMembersProfileUseCase.swift | 31 ++++++ .../Members/UseCases/MembersUseCase.swift | 51 --------- .../UpdateMembersProfileUseCase.swift | 30 ++++++ 45 files changed, 666 insertions(+), 451 deletions(-) rename 14th-team5-iOS/Domain/Sources/Camera/Entity/{CameraUpdateRealEmojiResponse.swift => CameraCreateRealEmojiEntity.swift} (53%) delete mode 100644 14th-team5-iOS/Domain/Sources/Camera/Entity/CameraDisplayImageResponse.swift rename 14th-team5-iOS/Domain/Sources/Camera/Entity/{CameraDisplayPostResponse.swift => CameraPostEntity.swift} (60%) create mode 100644 14th-team5-iOS/Domain/Sources/Camera/Entity/CameraPreSignedEntity.swift rename 14th-team5-iOS/Domain/Sources/Camera/Entity/{CameraCreateRealEmojiResponse.swift => CameraRealEmojiImageItemEntity.swift} (53%) create mode 100644 14th-team5-iOS/Domain/Sources/Camera/Entity/CameraRealEmojiPreSignedEntity.swift delete mode 100644 14th-team5-iOS/Domain/Sources/Camera/Entity/CameraRealEmojiPreSignedResponse.swift delete mode 100644 14th-team5-iOS/Domain/Sources/Camera/Entity/CameraTodayMissionResponse.swift create mode 100644 14th-team5-iOS/Domain/Sources/Camera/Entity/CameraTodayMssionEntity.swift rename 14th-team5-iOS/Domain/Sources/Camera/Entity/{CameraRealEmojiImageItemResponse.swift => CameraUpdateRealEmojiEntity.swift} (51%) delete mode 100644 14th-team5-iOS/Domain/Sources/Camera/UseCases/CameraDisplayViewUseCase.swift delete mode 100644 14th-team5-iOS/Domain/Sources/Camera/UseCases/CameraViewUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/Camera/UseCases/CreateCameraImageUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/Camera/UseCases/CreateCameraUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/Camera/UseCases/EditCameraProfileImageUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/Camera/UseCases/FetchCameraRealEmojiListUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/Camera/UseCases/FetchCameraRealEmojiUpdateUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/Camera/UseCases/FetchCameraRealEmojiUploadUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/Camera/UseCases/FetchCameraRealEmojiUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/Camera/UseCases/FetchCameraTodayMissionUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/Camera/UseCases/FetchCameraUploadImageUseCase.swift rename 14th-team5-iOS/Domain/Sources/Members/Entities/{MemberProfileResponse.swift => MembersProfileEntity.swift} (59%) create mode 100644 14th-team5-iOS/Domain/Sources/Members/UseCases/DeleteMembersProfileUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/Members/UseCases/FetchMembersProfileUseCase.swift delete mode 100644 14th-team5-iOS/Domain/Sources/Members/UseCases/MembersUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/Members/UseCases/UpdateMembersProfileUseCase.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Camera/Dependency/CameraDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/Camera/Dependency/CameraDIContainer.swift index 815ed414c..49ae164c0 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Camera/Dependency/CameraDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Camera/Dependency/CameraDIContainer.swift @@ -17,7 +17,6 @@ public final class CameraDIContainer: BaseDIContainer { public typealias ViewContrller = CameraViewController public typealias Repository = CameraRepositoryProtocol public typealias Reactor = CameraViewReactor - public typealias UseCase = CameraViewUseCaseProtocol private let cameraType: UploadLocation @@ -42,18 +41,22 @@ public final class CameraDIContainer: BaseDIContainer { return CameraViewController(reactor: makeReactor()) } - public func makeUseCase() -> UseCase { - return CameraViewUseCase(cameraRepository: makeRepository()) - } - public func makeRepository() -> Repository { return CameraRepository() } public func makeReactor() -> Reactor { + return CameraViewReactor( - cameraUseCase: makeUseCase(), + createProfileImageUseCase: CreateCameraUseCase(cameraRepository: makeRepository()), + uploadImageUseCase: FetchCameraUploadImageUseCase(cameraRepository: makeRepository()), + fetchMissionUseCase: FetchCameraTodayMissionUseCase(cameraRepository: makeRepository()), + fetchRealEmojiUpdateUseCase: FetchCameraRealEmojiUpdateUseCase(cameraRepostiroy: makeRepository()), + editProfileImageUseCase: EditCameraProfileImageUseCase(cameraRepository: makeRepository()), + fetchRealEmojiCreateUseCase: FetchCameraRealEmojiUploadUseCase(cameraRepository: makeRepository()), + fetchRealEmojiListUseCase: FetchCameraRealEmojiListUseCase(cameraRepository: makeRepository()), + fetchRealEmojiPreSignedUseCase: FetchCameraRealEmojiUseCase(cameraRepository: makeRepository()), provider: globalState, cameraType: cameraType, memberId: memberId, diff --git a/14th-team5-iOS/App/Sources/Presentation/Camera/Dependency/CameraDisplayDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/Camera/Dependency/CameraDisplayDIContainer.swift index e57cc4de4..d1334884e 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Camera/Dependency/CameraDisplayDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Camera/Dependency/CameraDisplayDIContainer.swift @@ -16,7 +16,6 @@ public final class CameraDisplayDIContainer: BaseDIContainer { public typealias ViewContrller = CameraDisplayViewController public typealias Repository = CameraRepositoryProtocol public typealias Reactor = CameraDisplayViewReactor - public typealias UseCase = CameraDisplayViewUseCaseProtocol fileprivate var displayData: Data fileprivate var missionTitle: String @@ -43,12 +42,15 @@ public final class CameraDisplayDIContainer: BaseDIContainer { return CameraRepository() } - public func makeUseCase() -> UseCase { - return CameraDisplayViewUseCase(cameraDisplayViewRepository: makeRepository()) - } - public func makeReactor() -> Reactor { - return CameraDisplayViewReactor(provider: globalState, cameraDisplayUseCase: makeUseCase(), displayData: displayData, missionTitle: missionTitle, cameraType: cameraDisplayType) + return CameraDisplayViewReactor( + provider: globalState, + createPresignedCameraUseCase: CreateCameraUseCase(cameraRepository: makeRepository()), + uploadImageUseCase: FetchCameraUploadImageUseCase(cameraRepository: makeRepository()), + fetchCameraImageUseCase: CreateCameraImageUseCase(cameraRepository: makeRepository()), + displayData: displayData, + missionTitle: missionTitle, + cameraType: cameraDisplayType + ) } - } diff --git a/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraDisplayViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraDisplayViewReactor.swift index 3a1bac5c3..36eb3b9af 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraDisplayViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraDisplayViewReactor.swift @@ -16,7 +16,9 @@ public final class CameraDisplayViewReactor: Reactor { public var initialState: State private let provider: GlobalStateProviderProtocol - private var cameraDisplayUseCase: CameraDisplayViewUseCaseProtocol + private let createPresignedCameraUseCase: CreateCameraUseCaseProtocol + private let uploadImageUseCase: FetchCameraUploadImageUseCaseProtocol + private let fetchCameraImageUseCase: CreateCameraImageUseCaseProtocol public enum Action { case viewDidLoad @@ -33,9 +35,9 @@ public final class CameraDisplayViewReactor: Reactor { case setRenderImage(Data) case saveDeviceimage(Data) case setDescription(String) - case setDisplayEntity(CameraDisplayImageResponse?) + case setDisplayEntity(CameraPreSignedEntity?) case setDisplayOriginalEntity(Bool) - case setPostEntity(CameraDisplayPostResponse?) + case setPostEntity(CameraPostEntity?) } public struct State { @@ -46,22 +48,27 @@ public final class CameraDisplayViewReactor: Reactor { @Pulse var displayData: Data @Pulse var missionTitle: String @Pulse var displaySection: [DisplayEditSectionModel] - @Pulse var displayEntity: CameraDisplayImageResponse? + @Pulse var displayEntity: CameraPreSignedEntity? @Pulse var displayOringalEntity: Bool - @Pulse var displayPostEntity: CameraDisplayPostResponse? + @Pulse var displayPostEntity: CameraPostEntity? } init( provider: GlobalStateProviderProtocol, - cameraDisplayUseCase: CameraDisplayViewUseCaseProtocol, + createPresignedCameraUseCase: CreateCameraUseCaseProtocol, + uploadImageUseCase: FetchCameraUploadImageUseCaseProtocol, + fetchCameraImageUseCase: CreateCameraImageUseCaseProtocol, displayData: Data, missionTitle: String, cameraType: PostType = .survival ) { self.provider = provider - self.cameraDisplayUseCase = cameraDisplayUseCase + self.createPresignedCameraUseCase = createPresignedCameraUseCase + self.uploadImageUseCase = uploadImageUseCase + self.fetchCameraImageUseCase = fetchCameraImageUseCase + self.initialState = State( isLoading: true, displayDescrption: "", @@ -86,7 +93,8 @@ public final class CameraDisplayViewReactor: Reactor { .just(.setLoading(false)), .just(.setError(false)), .just(.setRenderImage(currentState.displayData)), - cameraDisplayUseCase.executeDisplayImageURL(parameters: parameters) + createPresignedCameraUseCase.execute(parameter: parameters) + .asObservable() .withUnretained(self) .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) .asObservable() @@ -97,7 +105,7 @@ public final class CameraDisplayViewReactor: Reactor { .just(.setError(true)) ) } - return owner.cameraDisplayUseCase.executeUploadToS3(toURL: originalURL, imageData: owner.currentState.displayData) + return owner.uploadImageUseCase.execute(to: originalURL, from: owner.currentState.displayData) .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) .asObservable() .flatMap { isSuccess -> Observable in @@ -155,7 +163,7 @@ public final class CameraDisplayViewReactor: Reactor { uploadTime: DateFormatter.yyyyMMddTHHmmssXXX.string(from: Date()) ) - return cameraDisplayUseCase.executeCombineWithTextImage(parameters: parameters, query: cameraQuery) + return fetchCameraImageUseCase.execute(parameter: parameters, query: cameraQuery) .asObservable() .catchAndReturn(nil) .flatMap { entity -> Observable in diff --git a/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraViewReactor.swift index 2d3d06640..9945b4830 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraViewReactor.swift @@ -17,7 +17,20 @@ import ReactorKit public final class CameraViewReactor: Reactor { public var initialState: State - private var cameraUseCase: CameraViewUseCaseProtocol + + + private let createProfileImageUseCase: CreateCameraUseCaseProtocol + private let uploadImageUseCase: FetchCameraUploadImageUseCaseProtocol + private let fetchMissionUseCase: FetchCameraTodayMissionUseCaseProtocol + private let fetchRealEmojiUpdateUseCase: FetchCameraRealEmojiUpdateUseCaseProtocol + private let fetchRealEmojiCreateUseCase: FetchCameraRealEmojiUploadUseCaseProtocol + private let editProfileImageUseCase: EditCameraProfileImageUseCaseProtocol + private let fetchRealEmojiListUseCase: FetchCameraRealEmojiListUseCaseProtocol + private let fetchRealEmojiPreSignedUseCase: FetchCameraRealEmojiUseCaseProtocol + + + + private let provider: GlobalStateProviderProtocol public var cameraType: UploadLocation public var memberId: String @@ -40,13 +53,13 @@ public final class CameraViewReactor: Reactor { case setAccountProfileData(Data) case setPinchZoomScale(CGFloat) case setZoomScale(CGFloat) - case setProfileImageURLResponse(CameraDisplayImageResponse?) - case setProfileMemberResponse(MembersProfileResponse?) - case setRealEmojiImageURLResponse(CameraRealEmojiPreSignedResponse?) - case setRealEmojiImageCreateResponse(CameraCreateRealEmojiResponse?) - case setRealEmojiItems([CameraRealEmojiImageItemResponse?]) + case setProfileImageURLResponse(CameraPreSignedEntity?) + case setProfileMemberResponse(MembersProfileEntity?) + case setRealEmojiImageURLResponse(CameraRealEmojiPreSignedEntity?) + case setRealEmojiImageCreateResponse(CameraCreateRealEmojiEntity?) + case setRealEmojiItems([CameraRealEmojiImageItemEntity?]) case setRealEmojiSection([EmojiSectionItem]) - case setMissionResponse(CameraTodayMissionResponse?) + case setMissionResponse(CameraTodayMssionEntity?) case setErrorAlert(Bool) case setRealEmojiType(Emojis) case setFeedImageData(Data) @@ -57,11 +70,11 @@ public final class CameraViewReactor: Reactor { @Pulse var isLoading: Bool @Pulse var isFlashMode: Bool @Pulse var isSwitchPosition: Bool - @Pulse var profileImageURLEntity: CameraDisplayImageResponse? - @Pulse var realEmojiURLEntity: CameraRealEmojiPreSignedResponse? - @Pulse var realEmojiCreateEntity: CameraCreateRealEmojiResponse? - @Pulse var realEmojiEntity: [CameraRealEmojiImageItemResponse?] - @Pulse var missionEntity: CameraTodayMissionResponse? + @Pulse var profileImageURLEntity: CameraPreSignedEntity? + @Pulse var realEmojiURLEntity: CameraRealEmojiPreSignedEntity? + @Pulse var realEmojiCreateEntity: CameraCreateRealEmojiEntity? + @Pulse var realEmojiEntity: [CameraRealEmojiImageItemEntity?] + @Pulse var missionEntity: CameraTodayMssionEntity? @Pulse var realEmojiSection: [EmojiSectionModel] @Pulse var zoomScale: CGFloat @Pulse var pinchZoomScale: CGFloat @@ -73,17 +86,32 @@ public final class CameraViewReactor: Reactor { var memberId: String var isUpload: Bool @Pulse var isError: Bool - @Pulse var profileMemberEntity: MembersProfileResponse? + @Pulse var profileMemberEntity: MembersProfileEntity? } - init(cameraUseCase: CameraViewUseCaseProtocol, - provider: GlobalStateProviderProtocol, - cameraType: UploadLocation, - memberId: String, - emojiType: Emojis = .emoji(forIndex: 1) + init( + createProfileImageUseCase: CreateCameraUseCaseProtocol, + uploadImageUseCase: FetchCameraUploadImageUseCaseProtocol, + fetchMissionUseCase: FetchCameraTodayMissionUseCaseProtocol, + fetchRealEmojiUpdateUseCase: FetchCameraRealEmojiUpdateUseCaseProtocol, + editProfileImageUseCase: EditCameraProfileImageUseCaseProtocol, + fetchRealEmojiCreateUseCase: FetchCameraRealEmojiUploadUseCaseProtocol, + fetchRealEmojiListUseCase: FetchCameraRealEmojiListUseCaseProtocol, + fetchRealEmojiPreSignedUseCase: FetchCameraRealEmojiUseCaseProtocol, + provider: GlobalStateProviderProtocol, + cameraType: UploadLocation, + memberId: String, + emojiType: Emojis = .emoji(forIndex: 1) ) { self.cameraType = cameraType - self.cameraUseCase = cameraUseCase + self.createProfileImageUseCase = createProfileImageUseCase + self.uploadImageUseCase = uploadImageUseCase + self.fetchMissionUseCase = fetchMissionUseCase + self.fetchRealEmojiUpdateUseCase = fetchRealEmojiUpdateUseCase + self.editProfileImageUseCase = editProfileImageUseCase + self.fetchRealEmojiCreateUseCase = fetchRealEmojiCreateUseCase + self.fetchRealEmojiListUseCase = fetchRealEmojiListUseCase + self.fetchRealEmojiPreSignedUseCase = fetchRealEmojiPreSignedUseCase self.memberId = memberId self.provider = provider self.initialState = State( @@ -116,9 +144,9 @@ public final class CameraViewReactor: Reactor { return viewDidLoadMutation() case .didTapToggleButton: - return cameraUseCase.executeToggleCameraPosition(self.currentState.isSwitchPosition).map { .setPosition($0) } + return Observable.just(.setPosition(!self.currentState.isSwitchPosition)) case .didTapFlashButton: - return cameraUseCase.executeToggleCameraFlash(self.currentState.isFlashMode).map { .setFlashMode($0) } + return Observable.just(.setFlashMode(self.currentState.isFlashMode)) case let .didTapZoomButton(scale): if self.currentState.zoomScale == 2.0 { return .just(.setZoomScale(self.currentState.zoomScale - scale)) @@ -211,8 +239,10 @@ extension CameraViewReactor { private func viewDidLoadMutation() -> Observable { switch cameraType { case .realEmoji: + return .concat( - cameraUseCase.executeRealEmojiItems(memberId: memberId) + fetchRealEmojiListUseCase.execute(memberId: memberId) + .asObservable() .withUnretained(self) .flatMap { owner, entity -> Observable in var sectionItem: [EmojiSectionItem] = [] @@ -244,7 +274,8 @@ extension CameraViewReactor { } ) case .mission: - return cameraUseCase.executeTodayMission() + return fetchMissionUseCase.execute() + .asObservable() .withUnretained(self) .flatMap { owner, entity -> Observable in @@ -278,7 +309,9 @@ extension CameraViewReactor { return .concat( .just(.setLoading(false)), - cameraUseCase.executeProfileImageURL(parameter: profileParameter) + + createProfileImageUseCase.execute(parameter: profileParameter) + .asObservable() .withUnretained(self) .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) .asObservable() @@ -291,7 +324,7 @@ extension CameraViewReactor { ) } - return owner.cameraUseCase.executeUploadToS3(toURL: presingedURL, imageData: imageData) + return owner.uploadImageUseCase.execute(to: presingedURL, from: imageData) .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) .asObservable() .flatMap { isSuccess -> Observable in @@ -309,7 +342,7 @@ extension CameraViewReactor { let profileImageEditParameter = ProfileImageEditParameter(profileImageUrl: originalURL) if isSuccess { - return owner.cameraUseCase.executeEditProfileImage(memberId: owner.memberId, parameter: profileImageEditParameter) + return owner.editProfileImageUseCase.execute(memberId: owner.memberId, parameter: profileImageEditParameter) .asObservable() .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) .flatMap { editEntity -> Observable in @@ -341,27 +374,28 @@ extension CameraViewReactor { if currentState.realEmojiEntity[currentState.emojiType.rawValue - 1] == nil { return .concat( .just(.setLoading(false)), - cameraUseCase.executeRealEmojiImageURL(memberId: memberId, parameter: realEmojiParameter) + fetchRealEmojiPreSignedUseCase.execute(memberId: memberId, parameter: realEmojiParameter) + .asObservable() .withUnretained(self) .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) .asObservable() .flatMap { owner, entity -> Observable in guard let presingedURL = entity?.imageURL else { return .just(.setErrorAlert(true))} - return owner.cameraUseCase.executeUploadToS3(toURL: presingedURL, imageData: imageData) + return owner.uploadImageUseCase.execute(to: presingedURL, from: imageData) .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) .asObservable() .flatMap { isSuccess -> Observable in let originalURL = owner.configureProfileOriginalS3URL(url: presingedURL, with: .realEmoji) let realEmojiCreateParameter = CameraCreateRealEmojiParameters(type: owner.currentState.emojiType.emojiString, imageUrl: originalURL) if isSuccess { - return owner.cameraUseCase.executeRealEmojiUploadToS3(memberId: owner.memberId, parameter: realEmojiCreateParameter) + return owner.fetchRealEmojiCreateUseCase.execute(memberId: owner.memberId, parameter: realEmojiCreateParameter) .asObservable() .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) .flatMap { realEmojiEntity -> Observable in guard let createRealEmojiEntity = realEmojiEntity else { return .just(.setErrorAlert(true))} owner.provider.realEmojiGlobalState.createRealEmojiImage(indexPath: owner.currentState.emojiType.rawValue - 1, image: createRealEmojiEntity.realEmojiImageURL, emojiType: createRealEmojiEntity.realEmojiType) - return owner.cameraUseCase.executeRealEmojiItems(memberId: owner.memberId) + return owner.fetchRealEmojiListUseCase.execute(memberId: owner.memberId) .asObservable() .flatMap { reloadEntity -> Observable in return .concat( @@ -389,7 +423,8 @@ extension CameraViewReactor { let realEmojiParameter = CameraRealEmojiParameters(imageName: realEmojiImage) return .concat( .just(.setLoading(false)), - cameraUseCase.executeRealEmojiImageURL(memberId: memberId, parameter: realEmojiParameter) + fetchRealEmojiPreSignedUseCase.execute(memberId: memberId, parameter: realEmojiParameter) + .asObservable() .withUnretained(self) .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) .asObservable() @@ -397,12 +432,12 @@ extension CameraViewReactor { guard let presingedURL = entity?.imageURL else { return .just(.setErrorAlert(true))} let originalURL = owner.configureProfileOriginalS3URL(url: presingedURL, with: .realEmoji) let updateRealEmojiParameter = CameraUpdateRealEmojiParameters(imageUrl: originalURL) - return owner.cameraUseCase.executeUploadToS3(toURL: presingedURL, imageData: imageData) + return owner.uploadImageUseCase.execute(to: presingedURL, from: imageData) .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) .asObservable() .flatMap { isSuccess -> Observable in if isSuccess { - return owner.cameraUseCase.executeUpdateRealEmojiImage(memberId: owner.memberId, realEmojiId: owner.currentState.realEmojiEntity[owner.currentState.emojiType.rawValue - 1]?.realEmojiId ?? "", parameter: updateRealEmojiParameter) + return owner.fetchRealEmojiUpdateUseCase.execute(memberId: owner.memberId, emojiId: owner.currentState.realEmojiEntity[owner.currentState.emojiType.rawValue - 1]?.realEmojiId ?? "", parameter: updateRealEmojiParameter) .asObservable() .flatMap { updateRealEmojiEntity -> Observable in guard let updateEntity = updateRealEmojiEntity else { return .just(.setErrorAlert(true))} diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/Dependency/ProfileDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/Dependency/ProfileDIContainer.swift index b6fed08af..18712a1eb 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/Dependency/ProfileDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/Dependency/ProfileDIContainer.swift @@ -16,7 +16,6 @@ public final class ProfileDIContainer: BaseDIContainer { public typealias ViewContrller = ProfileViewController public typealias Repository = MembersRepositoryProtocol public typealias Reactor = ProfileViewReactor - public typealias UseCase = MembersUseCaseProtocol private var globalState: GlobalStateProviderProtocol { guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { @@ -37,17 +36,23 @@ public final class ProfileDIContainer: BaseDIContainer { return ProfileViewController(reactor: makeReactor()) } - public func makeUseCase() -> UseCase { - return MembersUseCase(membersRepository: makeRepository()) - } - public func makeRepository() -> Repository { return MembersRepository() } - public func makeReactor() -> ProfileViewReactor { - return ProfileViewReactor(membersUseCase: makeUseCase(), provider: globalState, memberId: memberId, isUser: isUser) + private func makeCameraRepository() -> CameraRepository { + return CameraRepository() } - + public func makeReactor() -> ProfileViewReactor { + return ProfileViewReactor( + fetchMembersProfileUseCase: FetchMembersProfileUseCase(membersRepository: makeRepository()), + createProfilePresignedUseCase: CreateCameraUseCase(cameraRepository: makeCameraRepository()), + uploadProfileImageUseCase: FetchCameraUploadImageUseCase(cameraRepository: makeCameraRepository()), + updateProfileUseCase: UpdateMembersProfileUseCase(membersRepository: makeRepository()), + provider: globalState, + memberId: memberId, + isUser: isUser + ) + } } diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift index fdcbf8a71..27c92fdf5 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift @@ -14,7 +14,14 @@ import ReactorKit public final class ProfileViewReactor: Reactor { public var initialState: State - private let membersUseCase: MembersUseCaseProtocol + + private let fetchMembersProfileUseCase: FetchMembersProfileUseCaseProtocol + private let createProfilePresignedUseCase: CreateCameraUseCaseProtocol + private let uploadProfileImageUseCase: FetchCameraUploadImageUseCaseProtocol + private let updateProfileUseCase: UpdateMembersProfileUseCaseProtocol + + + private let memberId: String private let isUser: Bool private let provider: GlobalStateProviderProtocol @@ -31,8 +38,8 @@ public final class ProfileViewReactor: Reactor { public enum Mutation { case setLoading(Bool) - case setProfilePresingedURL(CameraDisplayImageResponse?) - case setProfileMemberItems(MembersProfileResponse?) + case setProfilePresingedURL(CameraPreSignedEntity?) + case setProfileMemberItems(MembersProfileEntity?) case setProfileFeedType(BibbiFeedType) } @@ -41,12 +48,23 @@ public final class ProfileViewReactor: Reactor { var memberId: String var isUser: Bool var feedType: BibbiFeedType - @Pulse var profileMemberEntity: MembersProfileResponse? - @Pulse var profilePresingedURLEntity: CameraDisplayImageResponse? + @Pulse var profileMemberEntity: MembersProfileEntity? + @Pulse var profilePresingedURLEntity: CameraPreSignedEntity? } - init(membersUseCase: MembersUseCaseProtocol, provider: GlobalStateProviderProtocol, memberId: String, isUser: Bool) { - self.membersUseCase = membersUseCase + init( + fetchMembersProfileUseCase: FetchMembersProfileUseCaseProtocol, + createProfilePresignedUseCase: CreateCameraUseCaseProtocol, + uploadProfileImageUseCase: FetchCameraUploadImageUseCaseProtocol, + updateProfileUseCase: UpdateMembersProfileUseCaseProtocol, + provider: GlobalStateProviderProtocol, + memberId: String, + isUser: Bool + ) { + self.fetchMembersProfileUseCase = fetchMembersProfileUseCase + self.createProfilePresignedUseCase = createProfilePresignedUseCase + self.uploadProfileImageUseCase = uploadProfileImageUseCase + self.updateProfileUseCase = updateProfileUseCase self.memberId = memberId self.isUser = isUser self.initialState = State( @@ -77,7 +95,7 @@ public final class ProfileViewReactor: Reactor { //TODO: Keychain, UserDefaults 추가 switch action { case .viewDidLoad: - return membersUseCase.executeProfileMemberItems(memberId: currentState.memberId) + return fetchMembersProfileUseCase.execute(memberId: currentState.memberId) .asObservable() .flatMap { entity -> Observable in .just(.setProfileMemberItems(entity)) @@ -87,20 +105,21 @@ public final class ProfileViewReactor: Reactor { let nickNameImageEditParameter: CameraDisplayImageParameters = CameraDisplayImageParameters(imageName: nickNameProfileImage) return .concat( .just(.setLoading(false)), - membersUseCase.executeProfileImageURLCreate(parameter: nickNameImageEditParameter) + createProfilePresignedUseCase.execute(parameter: nickNameImageEditParameter) + .asObservable() .withUnretained(self) .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) .asObservable() .flatMap { owner, entity -> Observable in guard let profilePresingedURL = entity?.imageURL else { return .empty() } - return owner.membersUseCase.executeProfileImageToPresingedUpload(to: profilePresingedURL, data: nickNameFileData) + return owner.uploadProfileImageUseCase.execute(to: profilePresingedURL, from: nickNameFileData) .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) .asObservable() .flatMap { isSuccess -> Observable in let originalPath = owner.configureProfileOriginalS3URL(url: profilePresingedURL) let profileEditParameter: ProfileImageEditParameter = ProfileImageEditParameter(profileImageUrl: originalPath) if isSuccess { - return owner.membersUseCase.executeReloadProfileImage(memberId: self.currentState.memberId, parameter: profileEditParameter) + return owner.updateProfileUseCase.execute(memberId: owner.currentState.memberId, parameter: profileEditParameter) .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) .asObservable() .flatMap { memberEntity -> Observable in @@ -119,7 +138,7 @@ public final class ProfileViewReactor: Reactor { }) case .viewWillAppear: - return membersUseCase.executeProfileMemberItems(memberId: currentState.memberId) + return fetchMembersProfileUseCase.execute(memberId: currentState.memberId) .asObservable() .withUnretained(self) .flatMap { owner ,entity -> Observable in @@ -135,20 +154,20 @@ public final class ProfileViewReactor: Reactor { let profileImageEditParameter: CameraDisplayImageParameters = CameraDisplayImageParameters(imageName: profileImage) return .concat( .just(.setLoading(false)), - membersUseCase.executeProfileImageURLCreate(parameter: profileImageEditParameter) + createProfilePresignedUseCase.execute(parameter: profileImageEditParameter) + .asObservable() .withUnretained(self) .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) - .asObservable() .flatMap { owner, entity -> Observable in guard let profilePresingedURL = entity?.imageURL else { return .empty() } - return owner.membersUseCase.executeProfileImageToPresingedUpload(to: profilePresingedURL, data: fileData) + return owner.uploadProfileImageUseCase.execute(to: profilePresingedURL, from: fileData) .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) .asObservable() .flatMap { isSuccess -> Observable in let originalPath = owner.configureProfileOriginalS3URL(url: profilePresingedURL) let profileEditParameter: ProfileImageEditParameter = ProfileImageEditParameter(profileImageUrl: originalPath) if isSuccess { - return owner.membersUseCase.executeReloadProfileImage(memberId: self.currentState.memberId, parameter: profileEditParameter) + return owner.updateProfileUseCase.execute(memberId: owner.currentState.memberId, parameter: profileEditParameter) .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) .asObservable() .flatMap { memberEntity -> Observable in @@ -169,7 +188,7 @@ public final class ProfileViewReactor: Reactor { ) case .didTapInitProfile: - return membersUseCase.executeDeleteProfileImage(memberId: memberId) + return fetchMembersProfileUseCase.execute(memberId: memberId) .asObservable() .flatMap { entity -> Observable in return .concat( diff --git a/14th-team5-iOS/Data/Sources/Account/AccountRepository/AccountRepository.swift b/14th-team5-iOS/Data/Sources/Account/AccountRepository/AccountRepository.swift index e1bfb6a1d..268c8c31e 100644 --- a/14th-team5-iOS/Data/Sources/Account/AccountRepository/AccountRepository.swift +++ b/14th-team5-iOS/Data/Sources/Account/AccountRepository/AccountRepository.swift @@ -25,7 +25,7 @@ public protocol AccountImpl: AnyObject { func appleLogin(with snsType: SNS, vc: UIViewController) -> Observable func executeNicknameUpdate(memberId: String, parameter: AccountNickNameEditParameter) -> Observable func signUp(name: String, date: String, photoURL: String?) -> Observable - func executePresignedImageURLCreate(parameter: CameraDisplayImageParameters) -> Observable + func executePresignedImageURLCreate(parameter: CameraDisplayImageParameters) -> Observable func executeProfileImageUpload(to url: String, data: Data) -> Observable } @@ -124,7 +124,7 @@ public final class AccountRepository: AccountImpl { .asObservable() } - public func executePresignedImageURLCreate(parameter: CameraDisplayImageParameters) -> Observable { + public func executePresignedImageURLCreate(parameter: CameraDisplayImageParameters) -> Observable { return profileWorker.createProfileImagePresingedURL(accessToken: accessToken, parameters: parameter) .compactMap { $0?.toDomain() } .asObservable() diff --git a/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraCreateRealEmojiResponseDTO.swift b/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraCreateRealEmojiResponseDTO.swift index 5070d8d18..36ac19e51 100644 --- a/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraCreateRealEmojiResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraCreateRealEmojiResponseDTO.swift @@ -26,7 +26,7 @@ public struct CameraCreateRealEmojiResponseDTO: Decodable { extension CameraCreateRealEmojiResponseDTO { - public func toDomain() -> CameraCreateRealEmojiResponse { + public func toDomain() -> CameraCreateRealEmojiEntity { return .init( realEmojiId: realEmojiId, realEmojiType: realEmojiType, diff --git a/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraDisplayImageResponseDTO.swift b/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraDisplayImageResponseDTO.swift index c025b00d0..271ba073d 100644 --- a/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraDisplayImageResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraDisplayImageResponseDTO.swift @@ -22,7 +22,7 @@ public struct CameraDisplayImageResponseDTO: Decodable { extension CameraDisplayImageResponseDTO { - public func toDomain() -> CameraDisplayImageResponse? { + public func toDomain() -> CameraPreSignedEntity? { return .init(imageURL: imageURL ?? "") } } diff --git a/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraDisplayPostResponseDTO.swift b/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraDisplayPostResponseDTO.swift index ba6c9fb10..606fe86a1 100644 --- a/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraDisplayPostResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraDisplayPostResponseDTO.swift @@ -26,8 +26,7 @@ public struct CameraDisplayPostResponseDTO: Decodable { extension CameraDisplayPostResponseDTO { - public func toDomain() -> CameraDisplayPostResponse { - + public func toDomain() -> CameraPostEntity { return .init( postId: postId ?? "", authorId: authorId ?? "", @@ -39,6 +38,5 @@ extension CameraDisplayPostResponseDTO { content: content ?? "", createdAt: createdAt ) - } } diff --git a/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraRealEmojiImageItemResponseDTO.swift b/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraRealEmojiImageItemResponseDTO.swift index 894317fb3..35b3e369c 100644 --- a/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraRealEmojiImageItemResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraRealEmojiImageItemResponseDTO.swift @@ -37,8 +37,8 @@ extension CameraRealEmojiImageItemResponseDTO { extension CameraRealEmojiImageItemResponseDTO { - func toDomain() -> [CameraRealEmojiImageItemResponse?] { - var items: [CameraRealEmojiImageItemResponse?] = Array(repeating: nil, count: 5) + func toDomain() -> [CameraRealEmojiImageItemEntity?] { + var items: [CameraRealEmojiImageItemEntity?] = Array(repeating: nil, count: 5) realEmojiItems.forEach { guard let emojiType = $0.realEmojiType.last else { @@ -51,7 +51,7 @@ extension CameraRealEmojiImageItemResponseDTO { } extension CameraRealEmojiImageItemResponseDTO.CameraRealEmojiInfoResponseDTO { - func toDomain() -> CameraRealEmojiImageItemResponse { + func toDomain() -> CameraRealEmojiImageItemEntity { return .init( realEmojiId: realEmojiId, realEmojiType: realEmojiType, diff --git a/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraRealEmojiPreSignedResponseDTO.swift b/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraRealEmojiPreSignedResponseDTO.swift index 01ede5f02..7fbfc2d41 100644 --- a/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraRealEmojiPreSignedResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraRealEmojiPreSignedResponseDTO.swift @@ -22,7 +22,7 @@ public struct CameraRealEmojiPreSignedResponseDTO: Decodable { extension CameraRealEmojiPreSignedResponseDTO { - public func toDomain() -> CameraRealEmojiPreSignedResponse { + public func toDomain() -> CameraRealEmojiPreSignedEntity { return .init(imageURL: imageURL) } } diff --git a/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraTodayMissionResponseDTO.swift b/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraTodayMissionResponseDTO.swift index 2953b92a2..97de51ad7 100644 --- a/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraTodayMissionResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraTodayMissionResponseDTO.swift @@ -25,7 +25,7 @@ public struct CameraTodayMissionResponseDTO: Decodable { extension CameraTodayMissionResponseDTO { - func toDomain() -> CameraTodayMissionResponse { + func toDomain() -> CameraTodayMssionEntity { return .init( missionDate: missionDate.toDate(with: "yyyy-MM-dd"), missionId: missionId, diff --git a/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraUpdateRealEmojiResponseDTO.swift b/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraUpdateRealEmojiResponseDTO.swift index d44590741..dbbc254f9 100644 --- a/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraUpdateRealEmojiResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraUpdateRealEmojiResponseDTO.swift @@ -25,7 +25,7 @@ public struct CameraUpdateRealEmojiResponseDTO: Decodable { } extension CameraUpdateRealEmojiResponseDTO { - public func toDomain() -> CameraUpdateRealEmojiResponse { + public func toDomain() -> CameraUpdateRealEmojiEntity { return .init( realEmojiId: realEmojiId, realEmojiType: realEmojiType, diff --git a/14th-team5-iOS/Data/Sources/Camera/Repository/CameraRepository.swift b/14th-team5-iOS/Data/Sources/Camera/Repository/CameraRepository.swift index 295abf277..74c28d799 100644 --- a/14th-team5-iOS/Data/Sources/Camera/Repository/CameraRepository.swift +++ b/14th-team5-iOS/Data/Sources/Camera/Repository/CameraRepository.swift @@ -26,82 +26,56 @@ public final class CameraRepository { extension CameraRepository: CameraRepositoryProtocol { - public func toggleCameraPosition(_ isState: Bool) -> Observable { - return Observable - .create { observer in - observer.onNext(!isState) - - return Disposables.create() - } - } - - - public func toggleCameraFlash(_ isState: Bool) -> Observable { - return Observable - .create { observer in - observer.onNext(!isState) - return Disposables.create() - } - } - public func combineWithTextImage(parameters: CameraDisplayPostParameters, query:CameraMissionFeedQuery) -> Observable { + public func combineWithTextImage(parameters: CameraDisplayPostParameters, query:CameraMissionFeedQuery) -> Single { return cameraAPIWorker.combineWithTextImageUpload(accessToken: accessToken, parameters: parameters, query: query) .map { $0?.toDomain() } .catchAndReturn(nil) - .asObservable() } - public func fetchPresignedeImageURL(parameters: CameraDisplayImageParameters) -> Observable { + public func addPresignedeImageURL(parameters: CameraDisplayImageParameters) -> Single { return cameraAPIWorker.createProfilePresignedURL(accessToken: accessToken, parameters: parameters) - .compactMap { $0?.toDomain() } - .asObservable() + .map { $0?.toDomain() } } - public func uploadImageToS3(toURL url: String, imageData: Data) -> Observable { + public func uploadImageToS3(to url: String, from imageData: Data) -> Single { return cameraAPIWorker.uploadImageToPresignedURL(accessToken: accessToken, toURL: url, withImageData: imageData) - .asObservable() } - public func editProfleImageToS3(memberId: String, parameter: ProfileImageEditParameter) -> Observable { + public func editProfleImageToS3(memberId: String, parameter: ProfileImageEditParameter) -> Single { return cameraAPIWorker.editProfileImageToS3(accessToken: accessToken, memberId: memberId, parameters: parameter) .do { guard let userEntity = $0?.toProfileEntity() else { return } FamilyUserDefaults.saveMemberToUserDefaults(familyMember: userEntity) } - .compactMap { $0?.toDomain() } - .asObservable() + .map { $0?.toDomain() } } - public func fetchRealEmojiImageURL(memberId: String, parameters: CameraRealEmojiParameters) -> Observable { + public func fetchRealEmojiImageURL(memberId: String, parameters: CameraRealEmojiParameters) -> Single { return cameraAPIWorker.createRealEmojiPresignedURL(accessToken: accessToken, memberId: memberId, parameters: parameters) - .compactMap { $0?.toDomain() } - .asObservable() + .map { $0?.toDomain() } } - public func uploadRealEmojiImageToS3(memberId: String, parameters: CameraCreateRealEmojiParameters) -> Observable { + public func uploadRealEmojiImageToS3(memberId: String, parameters: CameraCreateRealEmojiParameters) -> Single { return cameraAPIWorker.uploadRealEmojiImageToS3(accessToken: accessToken, memberId: memberId, parameters: parameters) - .compactMap { $0?.toDomain() } - .asObservable() + .map { $0?.toDomain() } } - public func fetchRealEmojiItems(memberId: String) -> Observable<[CameraRealEmojiImageItemResponse?]> { + public func fetchRealEmojiItems(memberId: String) -> Single<[CameraRealEmojiImageItemEntity?]> { return cameraAPIWorker.loadRealEmojiImage(accessToken: accessToken, memberId: memberId) - .compactMap { $0?.toDomain() } - .asObservable() + .map { $0?.toDomain() ?? [] } } - public func updateRealEmojiImage(memberId: String, realEmojiId: String, parameters: CameraUpdateRealEmojiParameters) -> Observable { + public func updateRealEmojiImage(memberId: String, realEmojiId: String, parameters: CameraUpdateRealEmojiParameters) -> Single { return cameraAPIWorker.updateRealEmojiImage(accessToken: accessToken, memberId: memberId, realEmojiId: realEmojiId, parameters: parameters) - .compactMap { $0?.toDomain() } - .asObservable() + .map { $0?.toDomain() } } - public func fetchTodayMissionItem() -> Observable { + public func fetchTodayMissionItem() -> Single { return cameraAPIWorker.fetchMissionItems(accessToken: accessToken) - .compactMap { $0?.toDomain() } - .asObservable() + .map { $0?.toDomain() } } } diff --git a/14th-team5-iOS/Data/Sources/Members/DataMapping/MembersProfileResponseDTO.swift b/14th-team5-iOS/Data/Sources/Members/DataMapping/MembersProfileResponseDTO.swift index ba5d305a5..c662434d9 100644 --- a/14th-team5-iOS/Data/Sources/Members/DataMapping/MembersProfileResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/Members/DataMapping/MembersProfileResponseDTO.swift @@ -21,7 +21,7 @@ struct MembersProfileResponseDTO: Decodable { extension MembersProfileResponseDTO { //MARK: 프로필 정보 Entity - func toDomain() -> MembersProfileResponse { + func toDomain() -> MembersProfileEntity { return .init( memberId: memberId, memberName: name, diff --git a/14th-team5-iOS/Data/Sources/Members/Repositories/MembersRepository.swift b/14th-team5-iOS/Data/Sources/Members/Repositories/MembersRepository.swift index f37ad58bf..4ae2a612c 100644 --- a/14th-team5-iOS/Data/Sources/Members/Repositories/MembersRepository.swift +++ b/14th-team5-iOS/Data/Sources/Members/Repositories/MembersRepository.swift @@ -24,28 +24,14 @@ public final class MembersRepository { extension MembersRepository: MembersRepositoryProtocol { - - public func fetchProfileMemberItems(memberId: String) -> Observable { + + public func fetchProfileMemberItems(memberId: String) -> Single { return membersAPIWorker.fetchProfileMember(accessToken: accessToken, memberId: memberId) .map { $0?.toDomain() } .catchAndReturn(nil) - .asObservable() - } - - - public func fetchProfileAlbumImageURL(parameter: CameraDisplayImageParameters) -> Observable { - return membersAPIWorker.createProfileImagePresingedURL(accessToken: accessToken, parameters: parameter) - .compactMap { $0?.toDomain() } - .asObservable() - } - - - public func uploadProfileImageToPresingedURL(to url: String, imageData: Data) -> Observable { - return membersAPIWorker.uploadToProfilePresingedURL(accessToken: accessToken, toURL: url, with: imageData) - .asObservable() } - public func updataProfileImageToS3(memberId: String, parameter: ProfileImageEditParameter) -> Observable { + public func updataProfileImageToS3(memberId: String, parameter: ProfileImageEditParameter) -> Single { return membersAPIWorker.updateProfileAlbumImageToS3(accessToken: accessToken, memberId: memberId, parameter: parameter) .do { guard let userEntity = $0?.toProfileEntity() else { return } @@ -53,10 +39,9 @@ extension MembersRepository: MembersRepositoryProtocol { } .map { $0?.toDomain() } .catchAndReturn(nil) - .asObservable() } - public func deleteProfileImageToS3(memberId: String) -> Observable { + public func deleteProfileImageToS3(memberId: String) -> Single { return membersAPIWorker.deleteProfileImageToS3(accessToken: accessToken, memberId: memberId) .do { guard let userEntity = $0?.toProfileEntity() else { return } @@ -64,7 +49,6 @@ extension MembersRepository: MembersRepositoryProtocol { } .map { $0?.toDomain() } .catchAndReturn(nil) - .asObservable() } } diff --git a/14th-team5-iOS/Domain/Sources/Camera/Entity/CameraUpdateRealEmojiResponse.swift b/14th-team5-iOS/Domain/Sources/Camera/Entity/CameraCreateRealEmojiEntity.swift similarity index 53% rename from 14th-team5-iOS/Domain/Sources/Camera/Entity/CameraUpdateRealEmojiResponse.swift rename to 14th-team5-iOS/Domain/Sources/Camera/Entity/CameraCreateRealEmojiEntity.swift index 3dd1bc954..b5bb2446e 100644 --- a/14th-team5-iOS/Domain/Sources/Camera/Entity/CameraUpdateRealEmojiResponse.swift +++ b/14th-team5-iOS/Domain/Sources/Camera/Entity/CameraCreateRealEmojiEntity.swift @@ -1,23 +1,22 @@ // -// CameraUpdateRealEmojiResponse.swift +// CameraCreateRealEmojiEntity.swift // Domain // -// Created by Kim dohyun on 1/24/24. +// Created by Kim dohyun on 6/14/24. // import Foundation -public struct CameraUpdateRealEmojiResponse { - public var realEmojiId: String - public var realEmojiType: String - public var realEmojiImageURL: URL +public struct CameraCreateRealEmojiEntity { + public let realEmojiId: String + public let realEmojiType: String + public let realEmojiImageURL: URL + public init(realEmojiId: String, realEmojiType: String, realEmojiImageURL: URL) { self.realEmojiId = realEmojiId self.realEmojiType = realEmojiType self.realEmojiImageURL = realEmojiImageURL } - - } diff --git a/14th-team5-iOS/Domain/Sources/Camera/Entity/CameraDisplayImageResponse.swift b/14th-team5-iOS/Domain/Sources/Camera/Entity/CameraDisplayImageResponse.swift deleted file mode 100644 index 58a8a32fc..000000000 --- a/14th-team5-iOS/Domain/Sources/Camera/Entity/CameraDisplayImageResponse.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// CameraDisplayImageResponse.swift -// Domain -// -// Created by Kim dohyun on 12/21/23. -// - -import Foundation - - -public struct CameraDisplayImageResponse { - - public var imageURL: String - - public init(imageURL: String) { - self.imageURL = imageURL - } - -} diff --git a/14th-team5-iOS/Domain/Sources/Camera/Entity/CameraDisplayPostResponse.swift b/14th-team5-iOS/Domain/Sources/Camera/Entity/CameraPostEntity.swift similarity index 60% rename from 14th-team5-iOS/Domain/Sources/Camera/Entity/CameraDisplayPostResponse.swift rename to 14th-team5-iOS/Domain/Sources/Camera/Entity/CameraPostEntity.swift index e4e1b6fcc..1b613a305 100644 --- a/14th-team5-iOS/Domain/Sources/Camera/Entity/CameraDisplayPostResponse.swift +++ b/14th-team5-iOS/Domain/Sources/Camera/Entity/CameraPostEntity.swift @@ -1,23 +1,22 @@ // -// CameraDisplayPostResponse.swift +// CameraPostEntity.swift // Domain // -// Created by Kim dohyun on 12/22/23. +// Created by Kim dohyun on 6/14/24. // import Foundation - -public struct CameraDisplayPostResponse { - public var postId: String - public var authorId: String - public var commentCount: Int - public var missionType: String - public var missionId: String - public var emojiCount: Int - public var imageURL: String - public var content: String - public var createdAt: String +public struct CameraPostEntity { + public let postId: String + public let authorId: String + public let commentCount: Int + public let missionType: String + public let missionId: String + public let emojiCount: Int + public let imageURL: String + public let content: String + public let createdAt: String public init( postId: String, @@ -40,5 +39,4 @@ public struct CameraDisplayPostResponse { self.content = content self.createdAt = createdAt } - } diff --git a/14th-team5-iOS/Domain/Sources/Camera/Entity/CameraPreSignedEntity.swift b/14th-team5-iOS/Domain/Sources/Camera/Entity/CameraPreSignedEntity.swift new file mode 100644 index 000000000..d1f545b63 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Camera/Entity/CameraPreSignedEntity.swift @@ -0,0 +1,16 @@ +// +// CameraPreSignedEntity.swift +// Domain +// +// Created by Kim dohyun on 6/13/24. +// + +import Foundation + +public struct CameraPreSignedEntity { + public let imageURL: String + + public init(imageURL: String) { + self.imageURL = imageURL + } +} diff --git a/14th-team5-iOS/Domain/Sources/Camera/Entity/CameraCreateRealEmojiResponse.swift b/14th-team5-iOS/Domain/Sources/Camera/Entity/CameraRealEmojiImageItemEntity.swift similarity index 53% rename from 14th-team5-iOS/Domain/Sources/Camera/Entity/CameraCreateRealEmojiResponse.swift rename to 14th-team5-iOS/Domain/Sources/Camera/Entity/CameraRealEmojiImageItemEntity.swift index 58507ec2b..2d9abccf3 100644 --- a/14th-team5-iOS/Domain/Sources/Camera/Entity/CameraCreateRealEmojiResponse.swift +++ b/14th-team5-iOS/Domain/Sources/Camera/Entity/CameraRealEmojiImageItemEntity.swift @@ -1,18 +1,17 @@ // -// CameraCreateRealEmojiResponse.swift +// CameraRealEmojiImageItemEntity.swift // Domain // -// Created by Kim dohyun on 1/22/24. +// Created by Kim dohyun on 6/14/24. // import Foundation -public struct CameraCreateRealEmojiResponse { - public var realEmojiId: String - public var realEmojiType: String - public var realEmojiImageURL: URL - +public struct CameraRealEmojiImageItemEntity: Hashable { + public let realEmojiId: String + public let realEmojiType: String + public let realEmojiImageURL: URL public init(realEmojiId: String, realEmojiType: String, realEmojiImageURL: URL) { self.realEmojiId = realEmojiId @@ -20,5 +19,4 @@ public struct CameraCreateRealEmojiResponse { self.realEmojiImageURL = realEmojiImageURL } - } diff --git a/14th-team5-iOS/Domain/Sources/Camera/Entity/CameraRealEmojiPreSignedEntity.swift b/14th-team5-iOS/Domain/Sources/Camera/Entity/CameraRealEmojiPreSignedEntity.swift new file mode 100644 index 000000000..a9ec296e7 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Camera/Entity/CameraRealEmojiPreSignedEntity.swift @@ -0,0 +1,16 @@ +// +// CameraRealEmojiPreSignedEntity.swift +// Domain +// +// Created by Kim dohyun on 6/14/24. +// + +import Foundation + +public struct CameraRealEmojiPreSignedEntity { + public let imageURL: String + + public init(imageURL: String) { + self.imageURL = imageURL + } +} diff --git a/14th-team5-iOS/Domain/Sources/Camera/Entity/CameraRealEmojiPreSignedResponse.swift b/14th-team5-iOS/Domain/Sources/Camera/Entity/CameraRealEmojiPreSignedResponse.swift deleted file mode 100644 index e0b46284c..000000000 --- a/14th-team5-iOS/Domain/Sources/Camera/Entity/CameraRealEmojiPreSignedResponse.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// CameraRealEmojiPreSignedResponse.swift -// Domain -// -// Created by Kim dohyun on 1/22/24. -// - -import Foundation - - -public struct CameraRealEmojiPreSignedResponse { - public var imageURL: String - - public init(imageURL: String) { - self.imageURL = imageURL - } -} diff --git a/14th-team5-iOS/Domain/Sources/Camera/Entity/CameraTodayMissionResponse.swift b/14th-team5-iOS/Domain/Sources/Camera/Entity/CameraTodayMissionResponse.swift deleted file mode 100644 index 8df01d15b..000000000 --- a/14th-team5-iOS/Domain/Sources/Camera/Entity/CameraTodayMissionResponse.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// CameraTodayMissionResponse.swift -// Domain -// -// Created by Kim dohyun on 4/30/24. -// - -import Foundation - - -public struct CameraTodayMissionResponse { - public var missionDate: Date - public var missionId: String - public var missionContent: String - - public init(missionDate: Date, missionId: String, missionContent: String) { - self.missionDate = missionDate - self.missionId = missionId - self.missionContent = missionContent - } - -} diff --git a/14th-team5-iOS/Domain/Sources/Camera/Entity/CameraTodayMssionEntity.swift b/14th-team5-iOS/Domain/Sources/Camera/Entity/CameraTodayMssionEntity.swift new file mode 100644 index 000000000..b93dfa8d8 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Camera/Entity/CameraTodayMssionEntity.swift @@ -0,0 +1,21 @@ +// +// CameraTodayMssionEntity.swift +// Domain +// +// Created by Kim dohyun on 6/14/24. +// + +import Foundation + + +public struct CameraTodayMssionEntity { + public let missionDate: Date + public let missionId: String + public let missionContent: String + + public init(missionDate: Date, missionId: String, missionContent: String) { + self.missionDate = missionDate + self.missionId = missionId + self.missionContent = missionContent + } +} diff --git a/14th-team5-iOS/Domain/Sources/Camera/Entity/CameraRealEmojiImageItemResponse.swift b/14th-team5-iOS/Domain/Sources/Camera/Entity/CameraUpdateRealEmojiEntity.swift similarity index 51% rename from 14th-team5-iOS/Domain/Sources/Camera/Entity/CameraRealEmojiImageItemResponse.swift rename to 14th-team5-iOS/Domain/Sources/Camera/Entity/CameraUpdateRealEmojiEntity.swift index 8f077fed6..644477508 100644 --- a/14th-team5-iOS/Domain/Sources/Camera/Entity/CameraRealEmojiImageItemResponse.swift +++ b/14th-team5-iOS/Domain/Sources/Camera/Entity/CameraUpdateRealEmojiEntity.swift @@ -1,18 +1,17 @@ // -// CameraRealEmojiImageItemResponse.swift +// CameraUpdateRealEmojiEntity.swift // Domain // -// Created by Kim dohyun on 1/22/24. +// Created by Kim dohyun on 6/14/24. // import Foundation -import Core -public struct CameraRealEmojiImageItemResponse: Hashable { - public var realEmojiId: String - public var realEmojiType: String - public var realEmojiImageURL: URL +public struct CameraUpdateRealEmojiEntity { + public let realEmojiId: String + public let realEmojiType: String + public let realEmojiImageURL: URL public init(realEmojiId: String, realEmojiType: String, realEmojiImageURL: URL) { self.realEmojiId = realEmojiId diff --git a/14th-team5-iOS/Domain/Sources/Camera/Interfaces/CameraRepositoryProtocol.swift b/14th-team5-iOS/Domain/Sources/Camera/Interfaces/CameraRepositoryProtocol.swift index a44fc1e30..b2c0082a2 100644 --- a/14th-team5-iOS/Domain/Sources/Camera/Interfaces/CameraRepositoryProtocol.swift +++ b/14th-team5-iOS/Domain/Sources/Camera/Interfaces/CameraRepositoryProtocol.swift @@ -68,16 +68,14 @@ public protocol CameraRepositoryProtocol { var disposeBag: DisposeBag { get } var accessToken: String { get } - - func toggleCameraPosition(_ isState: Bool) -> Observable - func toggleCameraFlash(_ isState: Bool) -> Observable - func fetchPresignedeImageURL(parameters: CameraDisplayImageParameters) -> Observable - func uploadImageToS3(toURL url: String, imageData: Data) -> Observable - func editProfleImageToS3(memberId: String, parameter: ProfileImageEditParameter) -> Observable - func fetchRealEmojiImageURL(memberId: String, parameters: CameraRealEmojiParameters) -> Observable - func uploadRealEmojiImageToS3(memberId: String, parameters: CameraCreateRealEmojiParameters) -> Observable - func fetchRealEmojiItems(memberId: String) -> Observable<[CameraRealEmojiImageItemResponse?]> - func updateRealEmojiImage(memberId: String, realEmojiId: String, parameters: CameraUpdateRealEmojiParameters) -> Observable - func fetchTodayMissionItem() -> Observable - func combineWithTextImage(parameters: CameraDisplayPostParameters, query: CameraMissionFeedQuery) -> Observable + + func addPresignedeImageURL(parameters: CameraDisplayImageParameters) -> Single + func uploadImageToS3(to url: String, from image: Data) -> Single + func editProfleImageToS3(memberId: String, parameter: ProfileImageEditParameter) -> Single + func fetchRealEmojiImageURL(memberId: String, parameters: CameraRealEmojiParameters) -> Single + func uploadRealEmojiImageToS3(memberId: String, parameters: CameraCreateRealEmojiParameters) -> Single + func fetchRealEmojiItems(memberId: String) -> Single<[CameraRealEmojiImageItemEntity?]> + func updateRealEmojiImage(memberId: String, realEmojiId: String, parameters: CameraUpdateRealEmojiParameters) -> Single + func fetchTodayMissionItem() -> Single + func combineWithTextImage(parameters: CameraDisplayPostParameters, query: CameraMissionFeedQuery) -> Single } diff --git a/14th-team5-iOS/Domain/Sources/Camera/UseCases/CameraDisplayViewUseCase.swift b/14th-team5-iOS/Domain/Sources/Camera/UseCases/CameraDisplayViewUseCase.swift deleted file mode 100644 index 989726189..000000000 --- a/14th-team5-iOS/Domain/Sources/Camera/UseCases/CameraDisplayViewUseCase.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// CameraDisplayViewUseCase.swift -// Domain -// -// Created by Kim dohyun on 12/26/23. -// - -import Foundation - -import RxSwift -import RxCocoa - - -public protocol CameraDisplayViewUseCaseProtocol { - func executeDisplayImageURL(parameters: CameraDisplayImageParameters) -> Observable - func executeUploadToS3(toURL url: String, imageData: Data) -> Observable - func executeCombineWithTextImage(parameters: CameraDisplayPostParameters, query: CameraMissionFeedQuery) -> Observable - -} - - -public final class CameraDisplayViewUseCase: CameraDisplayViewUseCaseProtocol { - - private let cameraDisplayViewRepository: CameraRepositoryProtocol - - public init(cameraDisplayViewRepository: CameraRepositoryProtocol) { - self.cameraDisplayViewRepository = cameraDisplayViewRepository - } - - public func executeDisplayImageURL(parameters: CameraDisplayImageParameters) -> Observable { - return cameraDisplayViewRepository.fetchPresignedeImageURL(parameters: parameters) - } - - public func executeUploadToS3(toURL url: String, imageData: Data) -> Observable { - return cameraDisplayViewRepository.uploadImageToS3(toURL: url, imageData: imageData) - } - - public func executeCombineWithTextImage(parameters: CameraDisplayPostParameters, query: CameraMissionFeedQuery) -> Observable { - return cameraDisplayViewRepository.combineWithTextImage(parameters: parameters, query: query) - } - - - - - - -} diff --git a/14th-team5-iOS/Domain/Sources/Camera/UseCases/CameraViewUseCase.swift b/14th-team5-iOS/Domain/Sources/Camera/UseCases/CameraViewUseCase.swift deleted file mode 100644 index 91c5f798e..000000000 --- a/14th-team5-iOS/Domain/Sources/Camera/UseCases/CameraViewUseCase.swift +++ /dev/null @@ -1,78 +0,0 @@ -// -// CameraViewUseCase.swift -// Domain -// -// Created by Kim dohyun on 12/26/23. -// - -import Foundation - -import RxSwift -import RxCocoa - - -public protocol CameraViewUseCaseProtocol { - func executeToggleCameraPosition(_ isState: Bool) -> Observable - func executeToggleCameraFlash(_ isState: Bool) -> Observable - func executeProfileImageURL(parameter: CameraDisplayImageParameters) -> Observable - func executeUploadToS3(toURL url: String, imageData: Data) -> Observable - func executeEditProfileImage(memberId: String, parameter: ProfileImageEditParameter) -> Observable - func executeRealEmojiImageURL(memberId: String, parameter: CameraRealEmojiParameters) -> Observable - func executeRealEmojiUploadToS3(memberId: String, parameter: CameraCreateRealEmojiParameters) -> Observable - func executeRealEmojiItems(memberId: String) -> Observable<[CameraRealEmojiImageItemResponse?]> - func executeUpdateRealEmojiImage(memberId: String, realEmojiId: String ,parameter: CameraUpdateRealEmojiParameters) -> Observable - func executeTodayMission() -> Observable -} - - - -public final class CameraViewUseCase: CameraViewUseCaseProtocol { - - private let cameraRepository: CameraRepositoryProtocol - - public init(cameraRepository: CameraRepositoryProtocol) { - self.cameraRepository = cameraRepository - } - - public func executeToggleCameraPosition(_ isState: Bool) -> Observable { - return cameraRepository.toggleCameraPosition(isState) - } - - public func executeToggleCameraFlash(_ isState: Bool) -> Observable { - return cameraRepository.toggleCameraFlash(isState) - } - - public func executeProfileImageURL(parameter: CameraDisplayImageParameters) -> Observable { - return cameraRepository.fetchPresignedeImageURL(parameters: parameter) - } - - public func executeUploadToS3(toURL url: String, imageData: Data) -> Observable { - return cameraRepository.uploadImageToS3(toURL: url, imageData: imageData) - } - - public func executeEditProfileImage(memberId: String, parameter: ProfileImageEditParameter) -> Observable { - return cameraRepository.editProfleImageToS3(memberId: memberId, parameter: parameter) - } - - public func executeRealEmojiImageURL(memberId: String, parameter: CameraRealEmojiParameters) -> Observable { - return cameraRepository.fetchRealEmojiImageURL(memberId: memberId, parameters: parameter) - } - - public func executeRealEmojiUploadToS3(memberId: String, parameter: CameraCreateRealEmojiParameters) -> Observable { - return cameraRepository.uploadRealEmojiImageToS3(memberId: memberId, parameters: parameter) - } - - - public func executeRealEmojiItems(memberId: String) -> Observable<[CameraRealEmojiImageItemResponse?]> { - return cameraRepository.fetchRealEmojiItems(memberId: memberId) - } - - public func executeUpdateRealEmojiImage(memberId: String, realEmojiId: String, parameter: CameraUpdateRealEmojiParameters) -> Observable { - return cameraRepository.updateRealEmojiImage(memberId: memberId, realEmojiId: realEmojiId, parameters: parameter) - } - - public func executeTodayMission() -> Observable { - return cameraRepository.fetchTodayMissionItem() - } - -} diff --git a/14th-team5-iOS/Domain/Sources/Camera/UseCases/CreateCameraImageUseCase.swift b/14th-team5-iOS/Domain/Sources/Camera/UseCases/CreateCameraImageUseCase.swift new file mode 100644 index 000000000..345f244df --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Camera/UseCases/CreateCameraImageUseCase.swift @@ -0,0 +1,29 @@ +// +// CreateCameraImageUseCase.swift +// Domain +// +// Created by Kim dohyun on 6/14/24. +// + +import Foundation + +import RxSwift +import RxCocoa + + +public protocol CreateCameraImageUseCaseProtocol { + func execute(parameter: CameraDisplayPostParameters, query: CameraMissionFeedQuery) -> Single +} + +public final class CreateCameraImageUseCase: CreateCameraImageUseCaseProtocol { + + private let cameraRepository: any CameraRepositoryProtocol + + public init(cameraRepository: any CameraRepositoryProtocol) { + self.cameraRepository = cameraRepository + } + + public func execute(parameter: CameraDisplayPostParameters, query: CameraMissionFeedQuery) -> Single { + return cameraRepository.combineWithTextImage(parameters: parameter, query: query) + } +} diff --git a/14th-team5-iOS/Domain/Sources/Camera/UseCases/CreateCameraUseCase.swift b/14th-team5-iOS/Domain/Sources/Camera/UseCases/CreateCameraUseCase.swift new file mode 100644 index 000000000..94b63f228 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Camera/UseCases/CreateCameraUseCase.swift @@ -0,0 +1,32 @@ +// +// CreateCameraUseCase.swift +// Domain +// +// Created by Kim dohyun on 6/13/24. +// + +import Foundation + +import RxSwift +import RxCocoa + +public protocol CreateCameraUseCaseProtocol { + func execute(parameter: CameraDisplayImageParameters) -> Single +} + + +// Camera Presigned URL 생성 UseCase +public final class CreateCameraUseCase: CreateCameraUseCaseProtocol { + + private let cameraRepository: any CameraRepositoryProtocol + + public init(cameraRepository: any CameraRepositoryProtocol) { + self.cameraRepository = cameraRepository + } + + public func execute(parameter: CameraDisplayImageParameters) -> RxSwift.Single { + return cameraRepository.addPresignedeImageURL(parameters: parameter) + } +} + + diff --git a/14th-team5-iOS/Domain/Sources/Camera/UseCases/EditCameraProfileImageUseCase.swift b/14th-team5-iOS/Domain/Sources/Camera/UseCases/EditCameraProfileImageUseCase.swift new file mode 100644 index 000000000..84589cf10 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Camera/UseCases/EditCameraProfileImageUseCase.swift @@ -0,0 +1,36 @@ +// +// EditCameraProfileImageUseCase.swift +// Domain +// +// Created by Kim dohyun on 6/14/24. +// + +import Foundation + +import RxSwift +import RxCocoa + +public protocol EditCameraProfileImageUseCaseProtocol { + func execute(memberId: String, parameter: ProfileImageEditParameter) -> Single +} + + +public final class EditCameraProfileImageUseCase: EditCameraProfileImageUseCaseProtocol { + + private let cameraRepository: any CameraRepositoryProtocol + + public init(cameraRepository: any CameraRepositoryProtocol) { + self.cameraRepository = cameraRepository + } + + + public func execute(memberId: String, parameter: ProfileImageEditParameter) -> Single { + return cameraRepository.editProfleImageToS3(memberId: memberId, parameter: parameter) + } + + + +} + + + diff --git a/14th-team5-iOS/Domain/Sources/Camera/UseCases/FetchCameraRealEmojiListUseCase.swift b/14th-team5-iOS/Domain/Sources/Camera/UseCases/FetchCameraRealEmojiListUseCase.swift new file mode 100644 index 000000000..1795ea577 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Camera/UseCases/FetchCameraRealEmojiListUseCase.swift @@ -0,0 +1,31 @@ +// +// FetchCameraRealEmojiListUseCase.swift +// Domain +// +// Created by Kim dohyun on 6/14/24. +// + +import Foundation + +import RxSwift +import RxCocoa + + +public protocol FetchCameraRealEmojiListUseCaseProtocol { + func execute(memberId: String) -> Single<[CameraRealEmojiImageItemEntity?]> +} + + +public final class FetchCameraRealEmojiListUseCase: FetchCameraRealEmojiListUseCaseProtocol { + + private let cameraRepository: any CameraRepositoryProtocol + + public init(cameraRepository: any CameraRepositoryProtocol) { + self.cameraRepository = cameraRepository + } + + public func execute(memberId: String) -> Single<[CameraRealEmojiImageItemEntity?]> { + return cameraRepository.fetchRealEmojiItems(memberId: memberId) + } + +} diff --git a/14th-team5-iOS/Domain/Sources/Camera/UseCases/FetchCameraRealEmojiUpdateUseCase.swift b/14th-team5-iOS/Domain/Sources/Camera/UseCases/FetchCameraRealEmojiUpdateUseCase.swift new file mode 100644 index 000000000..d0ad19af5 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Camera/UseCases/FetchCameraRealEmojiUpdateUseCase.swift @@ -0,0 +1,29 @@ +// +// FetchCameraRealEmojiUpdateUseCase.swift +// Domain +// +// Created by Kim dohyun on 6/14/24. +// + +import Foundation + +import RxSwift +import RxCocoa + +public protocol FetchCameraRealEmojiUpdateUseCaseProtocol { + + func execute(memberId: String, emojiId: String, parameter: CameraUpdateRealEmojiParameters) -> Single +} + + +public final class FetchCameraRealEmojiUpdateUseCase: FetchCameraRealEmojiUpdateUseCaseProtocol { + private let cameraRepostiroy: any CameraRepositoryProtocol + + public init(cameraRepostiroy: any CameraRepositoryProtocol) { + self.cameraRepostiroy = cameraRepostiroy + } + + public func execute(memberId: String, emojiId: String, parameter: CameraUpdateRealEmojiParameters) -> Single { + return cameraRepostiroy.updateRealEmojiImage(memberId: memberId, realEmojiId: emojiId, parameters: parameter) + } +} diff --git a/14th-team5-iOS/Domain/Sources/Camera/UseCases/FetchCameraRealEmojiUploadUseCase.swift b/14th-team5-iOS/Domain/Sources/Camera/UseCases/FetchCameraRealEmojiUploadUseCase.swift new file mode 100644 index 000000000..c24738ee4 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Camera/UseCases/FetchCameraRealEmojiUploadUseCase.swift @@ -0,0 +1,29 @@ +// +// FetchCameraRealEmojiUploadUseCase.swift +// Domain +// +// Created by Kim dohyun on 6/14/24. +// + +import Foundation + +import RxSwift +import RxCocoa + +public protocol FetchCameraRealEmojiUploadUseCaseProtocol { + func execute(memberId: String, parameter: CameraCreateRealEmojiParameters) -> Single +} + +public final class FetchCameraRealEmojiUploadUseCase: FetchCameraRealEmojiUploadUseCaseProtocol { + + private let cameraRepository: any CameraRepositoryProtocol + + public init(cameraRepository: any CameraRepositoryProtocol) { + self.cameraRepository = cameraRepository + } + + public func execute(memberId: String, parameter: CameraCreateRealEmojiParameters) -> Single { + return cameraRepository.uploadRealEmojiImageToS3(memberId: memberId, parameters: parameter) + } +} + diff --git a/14th-team5-iOS/Domain/Sources/Camera/UseCases/FetchCameraRealEmojiUseCase.swift b/14th-team5-iOS/Domain/Sources/Camera/UseCases/FetchCameraRealEmojiUseCase.swift new file mode 100644 index 000000000..b5bc3a91f --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Camera/UseCases/FetchCameraRealEmojiUseCase.swift @@ -0,0 +1,34 @@ +// +// FetchCameraRealEmojiUseCase.swift +// Domain +// +// Created by Kim dohyun on 6/14/24. +// + +import Foundation + +import RxSwift +import RxCocoa + + +public protocol FetchCameraRealEmojiUseCaseProtocol { + func execute(memberId: String, parameter: CameraRealEmojiParameters) -> Single + +} + + + +public final class FetchCameraRealEmojiUseCase: FetchCameraRealEmojiUseCaseProtocol { + + private let cameraRepository: any CameraRepositoryProtocol + + public init(cameraRepository: any CameraRepositoryProtocol) { + self.cameraRepository = cameraRepository + } + + + public func execute(memberId: String, parameter: CameraRealEmojiParameters) -> Single { + return cameraRepository.fetchRealEmojiImageURL(memberId: memberId, parameters: parameter) + } + +} diff --git a/14th-team5-iOS/Domain/Sources/Camera/UseCases/FetchCameraTodayMissionUseCase.swift b/14th-team5-iOS/Domain/Sources/Camera/UseCases/FetchCameraTodayMissionUseCase.swift new file mode 100644 index 000000000..8eb066361 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Camera/UseCases/FetchCameraTodayMissionUseCase.swift @@ -0,0 +1,32 @@ +// +// FetchCameraTodayMissionUseCase.swift +// Domain +// +// Created by Kim dohyun on 6/14/24. +// + +import Foundation + +import RxSwift +import RxCocoa + + +public protocol FetchCameraTodayMissionUseCaseProtocol { + func execute() -> Single +} + + +public final class FetchCameraTodayMissionUseCase: FetchCameraTodayMissionUseCaseProtocol { + + private let cameraRepository: any CameraRepositoryProtocol + + public init(cameraRepository: any CameraRepositoryProtocol) { + self.cameraRepository = cameraRepository + } + + public func execute() -> Single { + return cameraRepository.fetchTodayMissionItem() + } + + +} diff --git a/14th-team5-iOS/Domain/Sources/Camera/UseCases/FetchCameraUploadImageUseCase.swift b/14th-team5-iOS/Domain/Sources/Camera/UseCases/FetchCameraUploadImageUseCase.swift new file mode 100644 index 000000000..913ece64b --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Camera/UseCases/FetchCameraUploadImageUseCase.swift @@ -0,0 +1,30 @@ +// +// FetchCameraUploadImageUseCase.swift +// Domain +// +// Created by Kim dohyun on 6/15/24. +// + +import Foundation + +import RxSwift +import RxCocoa + + +public protocol FetchCameraUploadImageUseCaseProtocol { + func execute(to url: String, from image: Data) -> Single +} + + +public final class FetchCameraUploadImageUseCase: FetchCameraUploadImageUseCaseProtocol { + + private let cameraRepository: any CameraRepositoryProtocol + + public init(cameraRepository: any CameraRepositoryProtocol) { + self.cameraRepository = cameraRepository + } + + public func execute(to url: String, from image: Data) -> Single { + return cameraRepository.uploadImageToS3(to: url, from: image) + } +} diff --git a/14th-team5-iOS/Domain/Sources/Members/Entities/MemberProfileResponse.swift b/14th-team5-iOS/Domain/Sources/Members/Entities/MembersProfileEntity.swift similarity index 59% rename from 14th-team5-iOS/Domain/Sources/Members/Entities/MemberProfileResponse.swift rename to 14th-team5-iOS/Domain/Sources/Members/Entities/MembersProfileEntity.swift index 876a9a22c..cc5f80b0e 100644 --- a/14th-team5-iOS/Domain/Sources/Members/Entities/MemberProfileResponse.swift +++ b/14th-team5-iOS/Domain/Sources/Members/Entities/MembersProfileEntity.swift @@ -1,22 +1,27 @@ // -// MemberProfileResponse.swift +// MembersProfileEntity.swift // Domain // -// Created by Kim dohyun on 6/3/24. +// Created by Kim dohyun on 6/15/24. // import Foundation - -public struct MembersProfileResponse { +public struct MembersProfileEntity { public let memberId: String public let memberName: String public let memberImage: URL public let dayOfBirth: Date public let familyJoinAt: String - - public init(memberId: String, memberName: String, memberImage: URL, dayOfBirth: Date, familyJoinAt: String) { + + public init( + memberId: String, + memberName: String, + memberImage: URL, + dayOfBirth: Date, + familyJoinAt: String + ) { self.memberId = memberId self.memberName = memberName self.memberImage = memberImage diff --git a/14th-team5-iOS/Domain/Sources/Members/Interfaces/MembersRepositoryProtocol.swift b/14th-team5-iOS/Domain/Sources/Members/Interfaces/MembersRepositoryProtocol.swift index c43a0e93b..a6b85ba63 100644 --- a/14th-team5-iOS/Domain/Sources/Members/Interfaces/MembersRepositoryProtocol.swift +++ b/14th-team5-iOS/Domain/Sources/Members/Interfaces/MembersRepositoryProtocol.swift @@ -14,9 +14,7 @@ import RxSwift public protocol MembersRepositoryProtocol { var disposeBag: DisposeBag { get } - func fetchProfileMemberItems(memberId: String) -> Observable - func fetchProfileAlbumImageURL(parameter: CameraDisplayImageParameters) -> Observable - func uploadProfileImageToPresingedURL(to url: String, imageData: Data) -> Observable - func updataProfileImageToS3(memberId: String, parameter: ProfileImageEditParameter) -> Observable - func deleteProfileImageToS3(memberId: String) -> Observable + func fetchProfileMemberItems(memberId: String) -> Single + func updataProfileImageToS3(memberId: String, parameter: ProfileImageEditParameter) -> Single + func deleteProfileImageToS3(memberId: String) -> Single } diff --git a/14th-team5-iOS/Domain/Sources/Members/UseCases/DeleteMembersProfileUseCase.swift b/14th-team5-iOS/Domain/Sources/Members/UseCases/DeleteMembersProfileUseCase.swift new file mode 100644 index 000000000..6145f3d11 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Members/UseCases/DeleteMembersProfileUseCase.swift @@ -0,0 +1,30 @@ +// +// DeleteMembersProfileUseCase.swift +// Domain +// +// Created by Kim dohyun on 6/15/24. +// + +import Foundation + +import RxSwift +import RxCocoa + +public protocol DeleteMembersProfileUseCaseProtocol { + func execute(memberId: String) -> Single +} + + +public final class DeleteMembersProfileUseCase: DeleteMembersProfileUseCaseProtocol { + + private let membersRepository: any MembersRepositoryProtocol + + + public init(membersRepository: any MembersRepositoryProtocol) { + self.membersRepository = membersRepository + } + + public func execute(memberId: String) -> Single { + return membersRepository.deleteProfileImageToS3(memberId: memberId) + } +} diff --git a/14th-team5-iOS/Domain/Sources/Members/UseCases/FetchMembersProfileUseCase.swift b/14th-team5-iOS/Domain/Sources/Members/UseCases/FetchMembersProfileUseCase.swift new file mode 100644 index 000000000..5c79d623e --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Members/UseCases/FetchMembersProfileUseCase.swift @@ -0,0 +1,31 @@ +// +// FetchMembersProfileUseCase.swift +// Domain +// +// Created by Kim dohyun on 6/15/24. +// + +import Foundation + +import RxSwift +import RxCocoa + + +public protocol FetchMembersProfileUseCaseProtocol { + func execute(memberId: String) -> Single +} + + +public final class FetchMembersProfileUseCase: FetchMembersProfileUseCaseProtocol { + + private let membersRepository: any MembersRepositoryProtocol + + + public init(membersRepository: any MembersRepositoryProtocol) { + self.membersRepository = membersRepository + } + + public func execute(memberId: String) -> Single { + return membersRepository.fetchProfileMemberItems(memberId: memberId) + } +} diff --git a/14th-team5-iOS/Domain/Sources/Members/UseCases/MembersUseCase.swift b/14th-team5-iOS/Domain/Sources/Members/UseCases/MembersUseCase.swift deleted file mode 100644 index 84b6c836d..000000000 --- a/14th-team5-iOS/Domain/Sources/Members/UseCases/MembersUseCase.swift +++ /dev/null @@ -1,51 +0,0 @@ -// -// MembersUseCase.swift -// Domain -// -// Created by Kim dohyun on 6/5/24. -// - -import Foundation - -import RxCocoa -import RxSwift - - -public protocol MembersUseCaseProtocol { - func executeProfileMemberItems(memberId: String) -> Observable - func executeProfileImageURLCreate(parameter: CameraDisplayImageParameters) -> Observable - func executeProfileImageToPresingedUpload(to url: String, data: Data) -> Observable - func executeReloadProfileImage(memberId: String, parameter: ProfileImageEditParameter) -> Observable - func executeDeleteProfileImage(memberId: String) -> Observable -} - -public final class MembersUseCase: MembersUseCaseProtocol { - private let membersRepository: MembersRepositoryProtocol - - public init(membersRepository: MembersRepositoryProtocol) { - self.membersRepository = membersRepository - } - - public func executeProfileMemberItems(memberId: String) -> Observable { - return membersRepository.fetchProfileMemberItems(memberId: memberId) - .asObservable() - } - - public func executeProfileImageURLCreate(parameter: CameraDisplayImageParameters) -> Observable { - return membersRepository.fetchProfileAlbumImageURL(parameter: parameter) - } - - - public func executeProfileImageToPresingedUpload(to url: String, data: Data) -> Observable { - return membersRepository.uploadProfileImageToPresingedURL(to: url, imageData: data) - } - - public func executeReloadProfileImage(memberId: String, parameter: ProfileImageEditParameter) -> Observable { - return membersRepository.updataProfileImageToS3(memberId: memberId, parameter: parameter) - } - - public func executeDeleteProfileImage(memberId: String) -> Observable { - return membersRepository.deleteProfileImageToS3(memberId: memberId) - } - -} diff --git a/14th-team5-iOS/Domain/Sources/Members/UseCases/UpdateMembersProfileUseCase.swift b/14th-team5-iOS/Domain/Sources/Members/UseCases/UpdateMembersProfileUseCase.swift new file mode 100644 index 000000000..7e5f98b13 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Members/UseCases/UpdateMembersProfileUseCase.swift @@ -0,0 +1,30 @@ +// +// UpdateMembersProfileUseCase.swift +// Domain +// +// Created by Kim dohyun on 6/15/24. +// + +import Foundation + +import RxSwift +import RxCocoa + +public protocol UpdateMembersProfileUseCaseProtocol { + func execute(memberId: String, parameter: ProfileImageEditParameter) -> Single +} + + +public final class UpdateMembersProfileUseCase: UpdateMembersProfileUseCaseProtocol { + + private let membersRepository: any MembersRepositoryProtocol + + + public init(membersRepository: any MembersRepositoryProtocol) { + self.membersRepository = membersRepository + } + + public func execute(memberId: String, parameter: ProfileImageEditParameter) -> Single { + return membersRepository.updataProfileImageToS3(memberId: memberId, parameter: parameter) + } +} From 23803918b8993d166194fe74eeadb9c01165f172 Mon Sep 17 00:00:00 2001 From: Do-hyun-Kim Date: Sun, 16 Jun 2024 20:06:58 +0900 Subject: [PATCH 115/263] =?UTF-8?q?refactor:=20Mission=20UseCase=20?= =?UTF-8?q?=EA=B4=80=EC=8B=AC=EC=82=AC=20=EB=B6=84=EB=A6=AC=20-=20Mission?= =?UTF-8?q?=20Entity=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?-=20=EC=82=AC=EC=9A=A9=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8A=94?= =?UTF-8?q?=20Mission=20Repository=20Protocol=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=20-=20asObservable=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../App/Sources/Manager/DeepLinkManager.swift | 2 +- .../Camera/Reactor/CameraViewReactor.swift | 3 -- .../Home/Dependency/MainViewDIContainer.swift | 6 +-- .../Dependency/PostListsDIContainer.swift | 6 +-- .../PostDetail/Reactor/PostReactor.swift | 16 ++++--- .../Dependency/ProfileFeedDIContainer.swift | 4 +- .../Reactor/ProfileFeedViewReactor.swift | 12 +++-- .../Profile/Reactor/ProfileViewReactor.swift | 1 - .../GetTodayMissionResponseDTO.swift | 20 -------- .../MissionContentResponseDTO.swift | 2 +- .../Mission/MissionAPI/MissionAPIWorker.swift | 30 +----------- .../Repository/MissionRepository.swift | 31 ++++++++++--- .../DataMapping/PostListResponseDTO.swift | 2 +- .../PostList/PostAPI/PostListAPIWorker.swift | 2 +- .../PostList/Repository/PostRepository.swift | 2 +- .../MissionUserDefaultsRepository.swift | 43 ----------------- .../Entities/PostList/PostEntity.swift | 2 +- .../FetchMembersPostListUseCase.swift | 32 +++++++++++++ .../Members/UseCases/ProfileFeedUseCase.swift | 46 +++++++++---------- ...ponse.swift => MissionContentEntity.swift} | 7 +-- .../Entities/TodayMissionResponse.swift | 18 -------- ...CheckMissionAlertShowUseCaseProtocol.swift | 6 +-- .../GetTodayMissionUseCaseProtocol.swift | 14 ------ .../MissionRepositoryProtocol.swift | 4 +- ...issionUserdefaultsRepositoryProtocol.swift | 6 +-- .../CheckMissionAlertShowUseCase.swift | 13 ++++-- .../UseCases/FetchMissionContentUseCase.swift | 29 ++++++++++++ .../UseCases/MissionContentUseCase.swift | 31 ------------- .../PostListRepositoryProtocol.swift | 2 +- .../UseCases/MainView/FetchMainUseCase.swift | 4 +- .../MainView/FetchNightMainViewUsecase.swift | 4 +- .../UseCases/Post/FetchPostListUseCase.swift | 4 +- 32 files changed, 170 insertions(+), 234 deletions(-) delete mode 100644 14th-team5-iOS/Data/Sources/Mission/DataMapping/GetTodayMissionResponseDTO.swift delete mode 100644 14th-team5-iOS/Data/Sources/UserDefaults/MissionUserDefaultsRepository.swift create mode 100644 14th-team5-iOS/Domain/Sources/Members/UseCases/FetchMembersPostListUseCase.swift rename 14th-team5-iOS/Domain/Sources/Mission/Entities/{MissionContentResponse.swift => MissionContentEntity.swift} (70%) delete mode 100644 14th-team5-iOS/Domain/Sources/Mission/Entities/TodayMissionResponse.swift delete mode 100644 14th-team5-iOS/Domain/Sources/Mission/Interfaces/GetTodayMissionUseCaseProtocol.swift create mode 100644 14th-team5-iOS/Domain/Sources/Mission/UseCases/FetchMissionContentUseCase.swift delete mode 100644 14th-team5-iOS/Domain/Sources/Mission/UseCases/MissionContentUseCase.swift diff --git a/14th-team5-iOS/App/Sources/Manager/DeepLinkManager.swift b/14th-team5-iOS/App/Sources/Manager/DeepLinkManager.swift index e0463b8e3..bf82f36f7 100644 --- a/14th-team5-iOS/App/Sources/Manager/DeepLinkManager.swift +++ b/14th-team5-iOS/App/Sources/Manager/DeepLinkManager.swift @@ -71,7 +71,7 @@ final class DeepLinkManager { } - private func fetchTodayPost(type: PostType, completion: @escaping (PostListPage?) -> Void) { + private func fetchTodayPost(type: PostType, completion: @escaping (PostListPageEntity?) -> Void) { let dateString = Date().toFormatString(with: "yyyy-MM-dd") let query = PostListQuery(date: dateString, type: type) diff --git a/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraViewReactor.swift index 9945b4830..5429c414f 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraViewReactor.swift @@ -314,7 +314,6 @@ extension CameraViewReactor { .asObservable() .withUnretained(self) .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) - .asObservable() .flatMap { owner, entity -> Observable in //TODO: 추후 오류 Alert 추가 guard let presingedURL = entity?.imageURL else { @@ -378,7 +377,6 @@ extension CameraViewReactor { .asObservable() .withUnretained(self) .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) - .asObservable() .flatMap { owner, entity -> Observable in guard let presingedURL = entity?.imageURL else { return .just(.setErrorAlert(true))} @@ -427,7 +425,6 @@ extension CameraViewReactor { .asObservable() .withUnretained(self) .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) - .asObservable() .flatMap { owner, entity -> Observable in guard let presingedURL = entity?.imageURL else { return .just(.setErrorAlert(true))} let originalURL = owner.configureProfileOriginalS3URL(url: presingedURL, with: .realEmoji) diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Dependency/MainViewDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Dependency/MainViewDIContainer.swift index 067af9f3a..c522b9b1e 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Dependency/MainViewDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Dependency/MainViewDIContainer.swift @@ -49,8 +49,8 @@ extension MainViewDIContainer { return MainViewRepository() } - private func makeMissionUserDefaultsRepository() -> MissionUserDefaultsRepository { - return MissionUserDefaultsRepository() + private func makeMissionRepository() -> MissionRepositoryProtocol { + return MissionRepository() } private func makeFetchMainUseCase() -> FetchMainUseCaseProtocol { @@ -62,6 +62,6 @@ extension MainViewDIContainer { } private func makeCheckMissionAlertShowUseCase() -> CheckMissionAlertShowUseCaseProtocol { - return CheckMissionAlertShowUseCase(missionUserDefaultsRepository: makeMissionUserDefaultsRepository()) + return CheckMissionAlertShowUseCase(missionRepository: makeMissionRepository()) } } diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/PostListsDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/PostListsDIContainer.swift index 8ccfd4941..352756059 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/PostListsDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/PostListsDIContainer.swift @@ -56,8 +56,8 @@ final class PostListsDIContainer { return MissionRepository() } - func makeMissionUseCase() -> MissionContentUseCaseProtocol { - return MissionContentUseCase(missionContentRepository: makeMissionRepository()) + func makeMissionUseCase() -> FetchMissionContentUseCaseProtocol { + return FetchMissionContentUseCase(missionRepository: makeMissionRepository()) } func makeReactor( @@ -67,7 +67,7 @@ final class PostListsDIContainer { ) -> Reactor { return PostReactor( provider: globalState, - missionUseCase: makeMissionUseCase(), + fetchMissionUseCase: makeMissionUseCase(), initialState: PostReactor.State( selectedIndex: selectedIndex, originPostLists: postLists, diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostReactor.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostReactor.swift index 8d97a757a..3df3ac5d5 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostReactor.swift @@ -21,7 +21,7 @@ final class PostReactor: Reactor { enum Mutation { case setPop case setSelectedPostIndex(Int) - case setMissionContent(MissionContentResponse) + case setMissionContent(MissionContentEntity) case setPushProfileViewController(String) } @@ -32,7 +32,7 @@ final class PostReactor: Reactor { var isPop: Bool = false var selectedPost: PostEntity = .init(postId: "", author: .init(memberId: "", profileImageURL: "", name: ""), commentCount: 0, emojiCount: 0, imageURL: "", content: "", time: "") - @Pulse var missionContent: MissionContentResponse? = nil + @Pulse var missionContent: MissionContentEntity? = nil @Pulse var reactionMemberIds: [String] = [] @Pulse var shouldPushProfileViewController: String? @@ -41,17 +41,17 @@ final class PostReactor: Reactor { let initialState: State - let missionUseCase: MissionContentUseCaseProtocol + let fetchMissionUseCase: FetchMissionContentUseCaseProtocol let provider: GlobalStateProviderProtocol init( provider: GlobalStateProviderProtocol, - missionUseCase: MissionContentUseCaseProtocol, + fetchMissionUseCase: FetchMissionContentUseCaseProtocol, initialState: State ) { self.provider = provider - self.missionUseCase = missionUseCase + self.fetchMissionUseCase = fetchMissionUseCase self.initialState = initialState } } @@ -76,11 +76,13 @@ extension PostReactor { case let .setPost(index): guard case let .main(postEntity) = currentState.originPostLists.items[index], let missionId = postEntity.missionId else { return Observable.just(.setSelectedPostIndex(index)) } - return missionUseCase.execute(missionId: missionId) + return fetchMissionUseCase.execute(missionId: missionId) + .asObservable() .flatMap { entity -> Observable in + guard let originEntity = entity else { return .empty() } return .concat( .just(.setSelectedPostIndex(index)), - .just(.setMissionContent(entity)) + .just(.setMissionContent(originEntity)) ) } diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/Dependency/ProfileFeedDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/Dependency/ProfileFeedDIContainer.swift index 0f90512e5..ffd601eba 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/Dependency/ProfileFeedDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/Dependency/ProfileFeedDIContainer.swift @@ -16,7 +16,7 @@ final class ProfileFeedDIContainer { typealias ViewController = ProfileFeedViewController typealias Reactor = ProfileFeedViewReactor typealias Repository = PostListRepositoryProtocol - typealias UseCase = ProfileFeedUseCaseProtocol + typealias UseCase = FetchMembersPostListUseCaseProtocol private let postType: PostType private let memberId: String @@ -36,7 +36,7 @@ final class ProfileFeedDIContainer { } func makeUseCase() -> UseCase { - return ProfileFeedUseCase(missionFeedRepository: makeRepository()) + return FetchMembersPostListUseCase(postListRepository: makeRepository()) } func makeRepository() -> Repository { diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileFeedViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileFeedViewReactor.swift index 33f80f7c7..c1c5b9c09 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileFeedViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileFeedViewReactor.swift @@ -15,7 +15,7 @@ import ReactorKit final class ProfileFeedViewReactor: Reactor { var initialState: State - private let feedUseCase: ProfileFeedUseCaseProtocol + private let feedUseCase: FetchMembersPostListUseCaseProtocol enum Action { case reloadFeedItems @@ -27,7 +27,7 @@ final class ProfileFeedViewReactor: Reactor { case setFeedSectionItems([ProfileFeedSectionItem]) case setFeedItemPage(Int) case setFeedPaginationItems([PostEntity]) - case setFeedItems(PostListPage) + case setFeedItems(PostListPageEntity) case setFeedDetailItem(PostSection.Model, IndexPath) } @@ -38,11 +38,15 @@ final class ProfileFeedViewReactor: Reactor { @Pulse var feedPaginationItems: [PostEntity] @Pulse var feedPage: Int @Pulse var type: PostType - @Pulse var feedItems: PostListPage? + @Pulse var feedItems: PostListPageEntity? @Pulse var feedSection: [ProfileFeedSectionModel] } - init(feedUseCase: ProfileFeedUseCaseProtocol, type: PostType, memberId: String) { + init( + feedUseCase: FetchMembersPostListUseCaseProtocol, + type: PostType, + memberId: String + ) { self.feedUseCase = feedUseCase self.initialState = State( memberId: memberId, diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift index 27c92fdf5..b05c52a86 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift @@ -109,7 +109,6 @@ public final class ProfileViewReactor: Reactor { .asObservable() .withUnretained(self) .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) - .asObservable() .flatMap { owner, entity -> Observable in guard let profilePresingedURL = entity?.imageURL else { return .empty() } return owner.uploadProfileImageUseCase.execute(to: profilePresingedURL, from: nickNameFileData) diff --git a/14th-team5-iOS/Data/Sources/Mission/DataMapping/GetTodayMissionResponseDTO.swift b/14th-team5-iOS/Data/Sources/Mission/DataMapping/GetTodayMissionResponseDTO.swift deleted file mode 100644 index acfd41b4c..000000000 --- a/14th-team5-iOS/Data/Sources/Mission/DataMapping/GetTodayMissionResponseDTO.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// GetTodayMissionResponseDTO.swift -// Data -// -// Created by Kim dohyun on 6/6/24. -// - -import Foundation - -import Domain - -struct GetTodayMissionResponseDTO: Codable { - let date: String - let id: String - let content: String - - func toDomain() -> TodayMissionResponse { - return .init(id: id, content: content) - } -} diff --git a/14th-team5-iOS/Data/Sources/Mission/DataMapping/MissionContentResponseDTO.swift b/14th-team5-iOS/Data/Sources/Mission/DataMapping/MissionContentResponseDTO.swift index b2b2e9280..3d255a50f 100644 --- a/14th-team5-iOS/Data/Sources/Mission/DataMapping/MissionContentResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/Mission/DataMapping/MissionContentResponseDTO.swift @@ -21,7 +21,7 @@ struct MissionContentResponseDTO: Decodable { } extension MissionContentResponseDTO { - func toDomain() -> MissionContentResponse { + func toDomain() -> MissionContentEntity { return .init( missionId: missionId, missionContent: missionContent diff --git a/14th-team5-iOS/Data/Sources/Mission/MissionAPI/MissionAPIWorker.swift b/14th-team5-iOS/Data/Sources/Mission/MissionAPI/MissionAPIWorker.swift index 821754e78..d3679fd2b 100644 --- a/14th-team5-iOS/Data/Sources/Mission/MissionAPI/MissionAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Mission/MissionAPI/MissionAPIWorker.swift @@ -27,33 +27,7 @@ extension MissionAPIs { } extension MissionAPIWorker { - private func getTodayMission(headers: [APIHeader]?) -> Single { - let spec = MissionAPIs.getTodayMission.spec - - return request(spec: spec, headers: headers) - .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("Join Family Result: \(str)") - } - } - .map(GetTodayMissionResponseDTO.self) - .catchAndReturn(nil) - .map { $0?.toDomain() } - .asSingle() - } - - func getTodayMission() -> Single { - return Observable.just(()) - .withLatestFrom(self._headers) - .observe(on: Self.queue) - .withUnretained(self) - .flatMap { $0.0.getTodayMission(headers: $0.1) } - .asSingle() - } - - - private func getMissionContent(spec: APISpec, headers: [APIHeader]?) -> Single { + private func getMissionContent(spec: APISpec, headers: [APIHeader]?) -> Single { return request(spec: spec, headers: headers) .subscribe(on: Self.queue) .do { @@ -68,7 +42,7 @@ extension MissionAPIWorker { } - func getMissionContent(missionId: String) -> Single { + func getMissionContent(missionId: String) -> Single { let spec = MissionAPIs.getMissionContent(missionId).spec return Observable.just(()) diff --git a/14th-team5-iOS/Data/Sources/Mission/Repository/MissionRepository.swift b/14th-team5-iOS/Data/Sources/Mission/Repository/MissionRepository.swift index f4eabce61..90d24b30e 100644 --- a/14th-team5-iOS/Data/Sources/Mission/Repository/MissionRepository.swift +++ b/14th-team5-iOS/Data/Sources/Mission/Repository/MissionRepository.swift @@ -15,20 +15,39 @@ import RxSwift public final class MissionRepository: MissionRepositoryProtocol { public let disposeBag: DisposeBag = DisposeBag() + private let lastMissionUploadDateId = "lastMissionUploadDateId" private let missionAPIWorker: MissionAPIWorker = MissionAPIWorker() public init() { } } extension MissionRepository { - public func getTodayMission() -> Observable { - return missionAPIWorker.getTodayMission() - .asObservable() + + public func getMissionContent(missionId: String) -> Single { + return missionAPIWorker.getMissionContent(missionId: missionId) } - public func getMissionContent(missionId: String) -> Observable { - return missionAPIWorker.getMissionContent(missionId: missionId) - .asObservable() + public func isAlreadyShowMissionAlert() -> Observable { + guard let lastDate = UserDefaults.standard.string(forKey: lastMissionUploadDateId) else { + saveMissionUploadDate() + return .just(false) + } + + if lastDate == Date().toFormatString(with: "yyyy-MM-dd") { + return .just(true) + } else { + saveMissionUploadDate() + return .just(false) + } } + private func saveMissionUploadDate() { + let isoDateFormatter = ISO8601DateFormatter() + isoDateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd" + let extractedDate = dateFormatter.string(from: Date()) + UserDefaults.standard.set(extractedDate, forKey: lastMissionUploadDateId) + } } diff --git a/14th-team5-iOS/Data/Sources/Post/PostList/DataMapping/PostListResponseDTO.swift b/14th-team5-iOS/Data/Sources/Post/PostList/DataMapping/PostListResponseDTO.swift index 194466c27..e97e127da 100644 --- a/14th-team5-iOS/Data/Sources/Post/PostList/DataMapping/PostListResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/Post/PostList/DataMapping/PostListResponseDTO.swift @@ -47,7 +47,7 @@ struct PostListResponseDTO: Codable { } extension PostListResponseDTO { - func toDomain() -> PostListPage { + func toDomain() -> PostListPageEntity { return .init(isLast: !hasNext, postLists: results.map { $0.toDomain() }) } } diff --git a/14th-team5-iOS/Data/Sources/Post/PostList/PostAPI/PostListAPIWorker.swift b/14th-team5-iOS/Data/Sources/Post/PostList/PostAPI/PostListAPIWorker.swift index 0cb3928b1..c8a2d4d32 100644 --- a/14th-team5-iOS/Data/Sources/Post/PostList/PostAPI/PostListAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Post/PostList/PostAPI/PostListAPIWorker.swift @@ -28,7 +28,7 @@ extension PostAPIs { } extension PostAPIWorker { - public func fetchTodayPostList(query: Domain.PostListQuery) -> RxSwift.Single { + public func fetchTodayPostList(query: Domain.PostListQuery) -> RxSwift.Single { let requestDTO = PostListRequestDTO(page: query.page, size: query.size, date: query.date, memberId: query.memberId, sort: query.sort, type: query.type.rawValue) let spec = PostAPIs.fetchPostList.spec return request(spec: spec, parameters: requestDTO) diff --git a/14th-team5-iOS/Data/Sources/Post/PostList/Repository/PostRepository.swift b/14th-team5-iOS/Data/Sources/Post/PostList/Repository/PostRepository.swift index 58433c087..da6a4aa15 100644 --- a/14th-team5-iOS/Data/Sources/Post/PostList/Repository/PostRepository.swift +++ b/14th-team5-iOS/Data/Sources/Post/PostList/Repository/PostRepository.swift @@ -17,7 +17,7 @@ public final class PostRepository: PostListRepositoryProtocol { public init() { } - public func fetchTodayPostList(query: Domain.PostListQuery) -> RxSwift.Single { + public func fetchTodayPostList(query: Domain.PostListQuery) -> RxSwift.Single { return postAPIWorker.fetchTodayPostList(query: query) } } diff --git a/14th-team5-iOS/Data/Sources/UserDefaults/MissionUserDefaultsRepository.swift b/14th-team5-iOS/Data/Sources/UserDefaults/MissionUserDefaultsRepository.swift deleted file mode 100644 index 262f41de9..000000000 --- a/14th-team5-iOS/Data/Sources/UserDefaults/MissionUserDefaultsRepository.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// MissionUserDefaultsRepository.swift -// Data -// -// Created by 마경미 on 03.06.24. -// - -import Foundation - -import Domain - -import RxSwift - -public final class MissionUserDefaultsRepository: MissionUserdefaultsRepositoryProtocol { - - private let lastMissionUploadDateId = "lastMissionUploadDateId" - - public init() { } - - public func isAlreadyShowMissionAlert() -> Observable { - guard let lastDate = UserDefaults.standard.string(forKey: lastMissionUploadDateId) else { - saveMissionUploadDate() - return .just(false) - } - - if lastDate == Date().toFormatString(with: "yyyy-MM-dd") { - return .just(true) - } else { - saveMissionUploadDate() - return .just(false) - } - } - - private func saveMissionUploadDate() { - let isoDateFormatter = ISO8601DateFormatter() - isoDateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] - - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd" - let extractedDate = dateFormatter.string(from: Date()) - UserDefaults.standard.set(extractedDate, forKey: lastMissionUploadDateId) - } -} diff --git a/14th-team5-iOS/Domain/Sources/Entities/PostList/PostEntity.swift b/14th-team5-iOS/Domain/Sources/Entities/PostList/PostEntity.swift index 43ef1bd09..ab6434ce6 100644 --- a/14th-team5-iOS/Domain/Sources/Entities/PostList/PostEntity.swift +++ b/14th-team5-iOS/Domain/Sources/Entities/PostList/PostEntity.swift @@ -31,7 +31,7 @@ public struct PostEntity: Equatable, Hashable { } } -public struct PostListPage: Equatable { +public struct PostListPageEntity: Equatable { public let isLast: Bool public let postLists: [PostEntity] diff --git a/14th-team5-iOS/Domain/Sources/Members/UseCases/FetchMembersPostListUseCase.swift b/14th-team5-iOS/Domain/Sources/Members/UseCases/FetchMembersPostListUseCase.swift new file mode 100644 index 000000000..0be3bc34d --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Members/UseCases/FetchMembersPostListUseCase.swift @@ -0,0 +1,32 @@ +// +// FetchMembersPostListUseCase.swift +// Domain +// +// Created by Kim dohyun on 6/16/24. +// + +import Foundation + +import RxSwift +import RxCocoa + +public protocol FetchMembersPostListUseCaseProtocol { + func execute(query: PostListQuery) -> Single +} + + +public final class FetchMembersPostListUseCase: FetchMembersPostListUseCaseProtocol { + + private let postListRepository: any PostListRepositoryProtocol + + + public init(postListRepository: any PostListRepositoryProtocol) { + self.postListRepository = postListRepository + } + + public func execute(query: PostListQuery) -> Single { + return postListRepository.fetchTodayPostList(query: query) + } + + +} diff --git a/14th-team5-iOS/Domain/Sources/Members/UseCases/ProfileFeedUseCase.swift b/14th-team5-iOS/Domain/Sources/Members/UseCases/ProfileFeedUseCase.swift index 819867687..1560414e5 100644 --- a/14th-team5-iOS/Domain/Sources/Members/UseCases/ProfileFeedUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/Members/UseCases/ProfileFeedUseCase.swift @@ -10,26 +10,26 @@ import Foundation import RxSwift import RxCocoa - -public protocol ProfileFeedUseCaseProtocol { - func execute(query: PostListQuery) -> Observable -} - - - -public final class ProfileFeedUseCase: ProfileFeedUseCaseProtocol { - - private let missionFeedRepository: PostListRepositoryProtocol - - public init(missionFeedRepository: PostListRepositoryProtocol) { - self.missionFeedRepository = missionFeedRepository - } - - public func execute(query: PostListQuery) -> RxSwift.Observable { - return missionFeedRepository.fetchTodayPostList(query: query) - .asObservable() - } - - - -} +// +//public protocol ProfileFeedUseCaseProtocol { +// func execute(query: PostListQuery) -> Observable +//} +// +// +// +//public final class ProfileFeedUseCase: ProfileFeedUseCaseProtocol { +// +// private let missionFeedRepository: PostListRepositoryProtocol +// +// public init(missionFeedRepository: PostListRepositoryProtocol) { +// self.missionFeedRepository = missionFeedRepository +// } +// +// public func execute(query: PostListQuery) -> RxSwift.Observable { +// return missionFeedRepository.fetchTodayPostList(query: query) +// .asObservable() +// } +// +// +// +//} diff --git a/14th-team5-iOS/Domain/Sources/Mission/Entities/MissionContentResponse.swift b/14th-team5-iOS/Domain/Sources/Mission/Entities/MissionContentEntity.swift similarity index 70% rename from 14th-team5-iOS/Domain/Sources/Mission/Entities/MissionContentResponse.swift rename to 14th-team5-iOS/Domain/Sources/Mission/Entities/MissionContentEntity.swift index 25ef66092..f395c34a7 100644 --- a/14th-team5-iOS/Domain/Sources/Mission/Entities/MissionContentResponse.swift +++ b/14th-team5-iOS/Domain/Sources/Mission/Entities/MissionContentEntity.swift @@ -1,13 +1,14 @@ // -// MissionContentResponse.swift +// MissionContentEntity.swift // Domain // -// Created by Kim dohyun on 6/5/24. +// Created by Kim dohyun on 6/16/24. // import Foundation -public struct MissionContentResponse { + +public struct MissionContentEntity { public let missionId: String public let missionContent: String diff --git a/14th-team5-iOS/Domain/Sources/Mission/Entities/TodayMissionResponse.swift b/14th-team5-iOS/Domain/Sources/Mission/Entities/TodayMissionResponse.swift deleted file mode 100644 index 9dad60c2b..000000000 --- a/14th-team5-iOS/Domain/Sources/Mission/Entities/TodayMissionResponse.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// TodayMissionResponse.swift -// Domain -// -// Created by Kim dohyun on 6/5/24. -// - -import Foundation - -public struct TodayMissionResponse { - let id: String - public let content: String - - public init(id: String, content: String) { - self.id = id - self.content = content - } -} diff --git a/14th-team5-iOS/Domain/Sources/Mission/Interfaces/CheckMissionAlertShowUseCaseProtocol.swift b/14th-team5-iOS/Domain/Sources/Mission/Interfaces/CheckMissionAlertShowUseCaseProtocol.swift index 825b5734c..e8e15de05 100644 --- a/14th-team5-iOS/Domain/Sources/Mission/Interfaces/CheckMissionAlertShowUseCaseProtocol.swift +++ b/14th-team5-iOS/Domain/Sources/Mission/Interfaces/CheckMissionAlertShowUseCaseProtocol.swift @@ -9,6 +9,6 @@ import Foundation import RxSwift -public protocol CheckMissionAlertShowUseCaseProtocol { - func execute() -> Observable -} +//public protocol CheckMissionAlertShowUseCaseProtocol { +// func execute() -> Observable +//} diff --git a/14th-team5-iOS/Domain/Sources/Mission/Interfaces/GetTodayMissionUseCaseProtocol.swift b/14th-team5-iOS/Domain/Sources/Mission/Interfaces/GetTodayMissionUseCaseProtocol.swift deleted file mode 100644 index d66ee1bbb..000000000 --- a/14th-team5-iOS/Domain/Sources/Mission/Interfaces/GetTodayMissionUseCaseProtocol.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// GetTodayUseCaseProtocol.swift -// Domain -// -// Created by 마경미 on 21.04.24. -// - -import Foundation - -import RxSwift - -public protocol GetTodayMissionUseCaseProtocol { - func execute() -> Observable -} diff --git a/14th-team5-iOS/Domain/Sources/Mission/Interfaces/MissionRepositoryProtocol.swift b/14th-team5-iOS/Domain/Sources/Mission/Interfaces/MissionRepositoryProtocol.swift index 751780dda..af158700d 100644 --- a/14th-team5-iOS/Domain/Sources/Mission/Interfaces/MissionRepositoryProtocol.swift +++ b/14th-team5-iOS/Domain/Sources/Mission/Interfaces/MissionRepositoryProtocol.swift @@ -10,6 +10,6 @@ import Foundation import RxSwift public protocol MissionRepositoryProtocol { - func getTodayMission() -> Observable - func getMissionContent(missionId: String) -> Observable + func getMissionContent(missionId: String) -> Single + func isAlreadyShowMissionAlert() -> Observable } diff --git a/14th-team5-iOS/Domain/Sources/Mission/Interfaces/MissionUserdefaultsRepositoryProtocol.swift b/14th-team5-iOS/Domain/Sources/Mission/Interfaces/MissionUserdefaultsRepositoryProtocol.swift index c26c8cc05..15b894215 100644 --- a/14th-team5-iOS/Domain/Sources/Mission/Interfaces/MissionUserdefaultsRepositoryProtocol.swift +++ b/14th-team5-iOS/Domain/Sources/Mission/Interfaces/MissionUserdefaultsRepositoryProtocol.swift @@ -9,6 +9,6 @@ import Foundation import RxSwift -public protocol MissionUserdefaultsRepositoryProtocol { - func isAlreadyShowMissionAlert() -> Observable -} +//public protocol MissionUserdefaultsRepositoryProtocol { +// func isAlreadyShowMissionAlert() -> Observable +//} diff --git a/14th-team5-iOS/Domain/Sources/Mission/UseCases/CheckMissionAlertShowUseCase.swift b/14th-team5-iOS/Domain/Sources/Mission/UseCases/CheckMissionAlertShowUseCase.swift index 253667e8b..9b0bca0d8 100644 --- a/14th-team5-iOS/Domain/Sources/Mission/UseCases/CheckMissionAlertShowUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/Mission/UseCases/CheckMissionAlertShowUseCase.swift @@ -9,14 +9,19 @@ import Foundation import RxSwift +public protocol CheckMissionAlertShowUseCaseProtocol { + func execute() -> Observable +} + + public class CheckMissionAlertShowUseCase: CheckMissionAlertShowUseCaseProtocol { - private let missionUserDefaultsRepository: MissionUserdefaultsRepositoryProtocol + private let missionRepository: any MissionRepositoryProtocol - public init(missionUserDefaultsRepository: MissionUserdefaultsRepositoryProtocol) { - self.missionUserDefaultsRepository = missionUserDefaultsRepository + public init(missionRepository: any MissionRepositoryProtocol) { + self.missionRepository = missionRepository } public func execute() -> Observable { - return missionUserDefaultsRepository.isAlreadyShowMissionAlert().asObservable() + return missionRepository.isAlreadyShowMissionAlert() } } diff --git a/14th-team5-iOS/Domain/Sources/Mission/UseCases/FetchMissionContentUseCase.swift b/14th-team5-iOS/Domain/Sources/Mission/UseCases/FetchMissionContentUseCase.swift new file mode 100644 index 000000000..a11803642 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Mission/UseCases/FetchMissionContentUseCase.swift @@ -0,0 +1,29 @@ +// +// FetchMissionContentUseCase.swift +// Domain +// +// Created by Kim dohyun on 6/16/24. +// + +import Foundation + +import RxSwift +import RxCocoa + + +public protocol FetchMissionContentUseCaseProtocol { + func execute(missionId: String) -> Single +} + +public final class FetchMissionContentUseCase: FetchMissionContentUseCaseProtocol { + private let missionRepository: any MissionRepositoryProtocol + + public init(missionRepository: any MissionRepositoryProtocol) { + self.missionRepository = missionRepository + } + + public func execute(missionId: String) -> Single { + return missionRepository.getMissionContent(missionId: missionId) + } + +} diff --git a/14th-team5-iOS/Domain/Sources/Mission/UseCases/MissionContentUseCase.swift b/14th-team5-iOS/Domain/Sources/Mission/UseCases/MissionContentUseCase.swift deleted file mode 100644 index 89a4bb8d8..000000000 --- a/14th-team5-iOS/Domain/Sources/Mission/UseCases/MissionContentUseCase.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// MissionContentUseCase.swift -// Domain -// -// Created by Kim dohyun on 5/9/24. -// - -import Foundation - -import RxSwift - -public protocol MissionContentUseCaseProtocol { - func execute(missionId: String) -> Observable -} - - -public class MissionContentUseCase: MissionContentUseCaseProtocol { - - private let missionContentRepository: MissionRepositoryProtocol - - public init(missionContentRepository: MissionRepositoryProtocol) { - self.missionContentRepository = missionContentRepository - } - - public func execute(missionId: String) -> Observable { - return missionContentRepository.getMissionContent(missionId: missionId) - .compactMap { $0 } - .asObservable() - } - -} diff --git a/14th-team5-iOS/Domain/Sources/Repositories/PostListRepositoryProtocol.swift b/14th-team5-iOS/Domain/Sources/Repositories/PostListRepositoryProtocol.swift index 7de282731..e74ba50ac 100644 --- a/14th-team5-iOS/Domain/Sources/Repositories/PostListRepositoryProtocol.swift +++ b/14th-team5-iOS/Domain/Sources/Repositories/PostListRepositoryProtocol.swift @@ -9,5 +9,5 @@ import Foundation import RxSwift public protocol PostListRepositoryProtocol { - func fetchTodayPostList(query: PostListQuery) -> Single + func fetchTodayPostList(query: PostListQuery) -> Single } diff --git a/14th-team5-iOS/Domain/Sources/UseCases/MainView/FetchMainUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/MainView/FetchMainUseCase.swift index 5c6b1d398..3e881c69a 100644 --- a/14th-team5-iOS/Domain/Sources/UseCases/MainView/FetchMainUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/MainView/FetchMainUseCase.swift @@ -14,9 +14,9 @@ public protocol FetchMainUseCaseProtocol { } public final class FetchMainUseCase: FetchMainUseCaseProtocol { - private let mainRepository: MainRepositoryProtocol + private let mainRepository: MainViewRepositoryProtocol - public init(mainRepository: MainRepositoryProtocol) { + public init(mainRepository: MainViewRepositoryProtocol) { self.mainRepository = mainRepository } diff --git a/14th-team5-iOS/Domain/Sources/UseCases/MainView/FetchNightMainViewUsecase.swift b/14th-team5-iOS/Domain/Sources/UseCases/MainView/FetchNightMainViewUsecase.swift index d5751b982..2ed78b6d8 100644 --- a/14th-team5-iOS/Domain/Sources/UseCases/MainView/FetchNightMainViewUsecase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/MainView/FetchNightMainViewUsecase.swift @@ -14,9 +14,9 @@ public protocol FetchNightMainViewUseCaseProtocol { } public final class FetchNightMainViewUseCase: FetchNightMainViewUseCaseProtocol { - private let mainRepository: MainRepositoryProtocol + private let mainRepository: MainViewRepositoryProtocol - public init(mainRepository: MainRepositoryProtocol) { + public init(mainRepository: MainViewRepositoryProtocol) { self.mainRepository = mainRepository } diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Post/FetchPostListUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Post/FetchPostListUseCase.swift index fd42c85f0..a3a67059f 100644 --- a/14th-team5-iOS/Domain/Sources/UseCases/Post/FetchPostListUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/Post/FetchPostListUseCase.swift @@ -9,7 +9,7 @@ import Foundation import RxSwift public protocol FetchPostListUseCaseProtocol { - func excute(query: PostListQuery) -> Single + func excute(query: PostListQuery) -> Single } public class FetchPostListUseCase: FetchPostListUseCaseProtocol { @@ -19,7 +19,7 @@ public class FetchPostListUseCase: FetchPostListUseCaseProtocol { self.postListRepository = postListRepository } - public func excute(query: PostListQuery) -> Single { + public func excute(query: PostListQuery) -> Single { return postListRepository.fetchTodayPostList(query: query) } } From 84158dc03fb55da3ade25b810d9a4331b7c4cefb Mon Sep 17 00:00:00 2001 From: Do-hyun-Kim Date: Sun, 16 Jun 2024 20:15:07 +0900 Subject: [PATCH 116/263] =?UTF-8?q?refactor:=20CheckMissionAlertShowUseCas?= =?UTF-8?q?eProtocol,=20MissionUserDefaultsRepositoryProtocol=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CheckMissionAlertShowUseCaseProtocol.swift | 14 -------------- .../MissionUserdefaultsRepositoryProtocol.swift | 14 -------------- 2 files changed, 28 deletions(-) delete mode 100644 14th-team5-iOS/Domain/Sources/Mission/Interfaces/CheckMissionAlertShowUseCaseProtocol.swift delete mode 100644 14th-team5-iOS/Domain/Sources/Mission/Interfaces/MissionUserdefaultsRepositoryProtocol.swift diff --git a/14th-team5-iOS/Domain/Sources/Mission/Interfaces/CheckMissionAlertShowUseCaseProtocol.swift b/14th-team5-iOS/Domain/Sources/Mission/Interfaces/CheckMissionAlertShowUseCaseProtocol.swift deleted file mode 100644 index e8e15de05..000000000 --- a/14th-team5-iOS/Domain/Sources/Mission/Interfaces/CheckMissionAlertShowUseCaseProtocol.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// MissionAlertUseCaseProtocol.swift -// Domain -// -// Created by 마경미 on 03.06.24. -// - -import Foundation - -import RxSwift - -//public protocol CheckMissionAlertShowUseCaseProtocol { -// func execute() -> Observable -//} diff --git a/14th-team5-iOS/Domain/Sources/Mission/Interfaces/MissionUserdefaultsRepositoryProtocol.swift b/14th-team5-iOS/Domain/Sources/Mission/Interfaces/MissionUserdefaultsRepositoryProtocol.swift deleted file mode 100644 index 15b894215..000000000 --- a/14th-team5-iOS/Domain/Sources/Mission/Interfaces/MissionUserdefaultsRepositoryProtocol.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// MissionUserdefaultsRepositoryProtocol.swift -// Domain -// -// Created by 마경미 on 03.06.24. -// - -import Foundation - -import RxSwift - -//public protocol MissionUserdefaultsRepositoryProtocol { -// func isAlreadyShowMissionAlert() -> Observable -//} From 3dcd59784cede90a06ed7632449b06d02324491d Mon Sep 17 00:00:00 2001 From: Do-hyun-Kim Date: Sun, 16 Jun 2024 20:26:00 +0900 Subject: [PATCH 117/263] =?UTF-8?q?refactor:=20ProfileFeedUseCase=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Members/UseCases/ProfileFeedUseCase.swift | 35 ------------------- 1 file changed, 35 deletions(-) delete mode 100644 14th-team5-iOS/Domain/Sources/Members/UseCases/ProfileFeedUseCase.swift diff --git a/14th-team5-iOS/Domain/Sources/Members/UseCases/ProfileFeedUseCase.swift b/14th-team5-iOS/Domain/Sources/Members/UseCases/ProfileFeedUseCase.swift deleted file mode 100644 index 1560414e5..000000000 --- a/14th-team5-iOS/Domain/Sources/Members/UseCases/ProfileFeedUseCase.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// ProfileFeedUseCase.swift -// Domain -// -// Created by Kim dohyun on 5/4/24. -// - -import Foundation - -import RxSwift -import RxCocoa - -// -//public protocol ProfileFeedUseCaseProtocol { -// func execute(query: PostListQuery) -> Observable -//} -// -// -// -//public final class ProfileFeedUseCase: ProfileFeedUseCaseProtocol { -// -// private let missionFeedRepository: PostListRepositoryProtocol -// -// public init(missionFeedRepository: PostListRepositoryProtocol) { -// self.missionFeedRepository = missionFeedRepository -// } -// -// public func execute(query: PostListQuery) -> RxSwift.Observable { -// return missionFeedRepository.fetchTodayPostList(query: query) -// .asObservable() -// } -// -// -// -//} From d1c56dce9099899def917eb29e7109b04ad06fc0 Mon Sep 17 00:00:00 2001 From: Do-hyun-Kim Date: Sun, 16 Jun 2024 20:39:07 +0900 Subject: [PATCH 118/263] =?UTF-8?q?fix:=20FetchPostListUseCase,=20MainPost?= =?UTF-8?q?ViewReactor,=20DeepLinkManager=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EB=84=A4=EC=9D=B4=EB=B0=8D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 14th-team5-iOS/App/Sources/Manager/DeepLinkManager.swift | 2 +- .../Presentation/Home/Reactor/MainPostViewReactor.swift | 2 +- .../Domain/Sources/UseCases/Post/FetchPostListUseCase.swift | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/14th-team5-iOS/App/Sources/Manager/DeepLinkManager.swift b/14th-team5-iOS/App/Sources/Manager/DeepLinkManager.swift index bf82f36f7..4bae1cf7b 100644 --- a/14th-team5-iOS/App/Sources/Manager/DeepLinkManager.swift +++ b/14th-team5-iOS/App/Sources/Manager/DeepLinkManager.swift @@ -75,7 +75,7 @@ final class DeepLinkManager { let dateString = Date().toFormatString(with: "yyyy-MM-dd") let query = PostListQuery(date: dateString, type: type) - postUseCase.excute(query: query) + postUseCase.execute(query: query) .subscribe(onSuccess: { result in completion(result) }) diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainPostViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainPostViewReactor.swift index 7b9edb383..40307c8df 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainPostViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainPostViewReactor.swift @@ -55,7 +55,7 @@ extension MainPostViewReactor { ]) case .fetchPost: let query = PostListQuery(date: DateFormatter.dashYyyyMMdd.string(from: Date()), type: currentState.type) - return postUseCase.excute(query: query) + return postUseCase.execute(query: query) .asObservable() .flatMap { (postList) -> Observable in guard let postList = postList, diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Post/FetchPostListUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Post/FetchPostListUseCase.swift index a3a67059f..d061a776a 100644 --- a/14th-team5-iOS/Domain/Sources/UseCases/Post/FetchPostListUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/Post/FetchPostListUseCase.swift @@ -9,7 +9,7 @@ import Foundation import RxSwift public protocol FetchPostListUseCaseProtocol { - func excute(query: PostListQuery) -> Single + func execute(query: PostListQuery) -> Single } public class FetchPostListUseCase: FetchPostListUseCaseProtocol { @@ -19,7 +19,7 @@ public class FetchPostListUseCase: FetchPostListUseCaseProtocol { self.postListRepository = postListRepository } - public func excute(query: PostListQuery) -> Single { + public func execute(query: PostListQuery) -> Single { return postListRepository.fetchTodayPostList(query: query) } } From cb69632d47481147f8767ae4920a1c98a7804053 Mon Sep 17 00:00:00 2001 From: Do-hyun-Kim Date: Mon, 17 Jun 2024 19:43:28 +0900 Subject: [PATCH 119/263] =?UTF-8?q?fix:=20Camera,=20Members,=20Mission=20U?= =?UTF-8?q?seCase=20=ED=8F=B4=EB=8D=94=20=EA=B5=AC=EC=A1=B0=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Camera/UseCases/{ => Camera}/CreateCameraImageUseCase.swift | 0 .../Camera/UseCases/{ => Camera}/CreateCameraUseCase.swift | 0 .../UseCases/{ => Camera}/FetchCameraUploadImageUseCase.swift | 0 .../UseCases/{ => Mission}/FetchCameraTodayMissionUseCase.swift | 0 .../UseCases/{ => Profile}/EditCameraProfileImageUseCase.swift | 0 .../{ => RealEmoji}/FetchCameraRealEmojiListUseCase.swift | 0 .../{ => RealEmoji}/FetchCameraRealEmojiUpdateUseCase.swift | 0 .../{ => RealEmoji}/FetchCameraRealEmojiUploadUseCase.swift | 0 .../UseCases/{ => RealEmoji}/FetchCameraRealEmojiUseCase.swift | 0 .../UseCases/{ => Members}/DeleteMembersProfileUseCase.swift | 0 .../UseCases/{ => Members}/FetchMembersPostListUseCase.swift | 0 .../UseCases/{ => Members}/FetchMembersProfileUseCase.swift | 0 .../UseCases/{ => Members}/UpdateMembersProfileUseCase.swift | 0 .../UseCases/{ => Mission}/CheckMissionAlertShowUseCase.swift | 0 .../UseCases/{ => Mission}/FetchMissionContentUseCase.swift | 0 15 files changed, 0 insertions(+), 0 deletions(-) rename 14th-team5-iOS/Domain/Sources/Camera/UseCases/{ => Camera}/CreateCameraImageUseCase.swift (100%) rename 14th-team5-iOS/Domain/Sources/Camera/UseCases/{ => Camera}/CreateCameraUseCase.swift (100%) rename 14th-team5-iOS/Domain/Sources/Camera/UseCases/{ => Camera}/FetchCameraUploadImageUseCase.swift (100%) rename 14th-team5-iOS/Domain/Sources/Camera/UseCases/{ => Mission}/FetchCameraTodayMissionUseCase.swift (100%) rename 14th-team5-iOS/Domain/Sources/Camera/UseCases/{ => Profile}/EditCameraProfileImageUseCase.swift (100%) rename 14th-team5-iOS/Domain/Sources/Camera/UseCases/{ => RealEmoji}/FetchCameraRealEmojiListUseCase.swift (100%) rename 14th-team5-iOS/Domain/Sources/Camera/UseCases/{ => RealEmoji}/FetchCameraRealEmojiUpdateUseCase.swift (100%) rename 14th-team5-iOS/Domain/Sources/Camera/UseCases/{ => RealEmoji}/FetchCameraRealEmojiUploadUseCase.swift (100%) rename 14th-team5-iOS/Domain/Sources/Camera/UseCases/{ => RealEmoji}/FetchCameraRealEmojiUseCase.swift (100%) rename 14th-team5-iOS/Domain/Sources/Members/UseCases/{ => Members}/DeleteMembersProfileUseCase.swift (100%) rename 14th-team5-iOS/Domain/Sources/Members/UseCases/{ => Members}/FetchMembersPostListUseCase.swift (100%) rename 14th-team5-iOS/Domain/Sources/Members/UseCases/{ => Members}/FetchMembersProfileUseCase.swift (100%) rename 14th-team5-iOS/Domain/Sources/Members/UseCases/{ => Members}/UpdateMembersProfileUseCase.swift (100%) rename 14th-team5-iOS/Domain/Sources/Mission/UseCases/{ => Mission}/CheckMissionAlertShowUseCase.swift (100%) rename 14th-team5-iOS/Domain/Sources/Mission/UseCases/{ => Mission}/FetchMissionContentUseCase.swift (100%) diff --git a/14th-team5-iOS/Domain/Sources/Camera/UseCases/CreateCameraImageUseCase.swift b/14th-team5-iOS/Domain/Sources/Camera/UseCases/Camera/CreateCameraImageUseCase.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Camera/UseCases/CreateCameraImageUseCase.swift rename to 14th-team5-iOS/Domain/Sources/Camera/UseCases/Camera/CreateCameraImageUseCase.swift diff --git a/14th-team5-iOS/Domain/Sources/Camera/UseCases/CreateCameraUseCase.swift b/14th-team5-iOS/Domain/Sources/Camera/UseCases/Camera/CreateCameraUseCase.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Camera/UseCases/CreateCameraUseCase.swift rename to 14th-team5-iOS/Domain/Sources/Camera/UseCases/Camera/CreateCameraUseCase.swift diff --git a/14th-team5-iOS/Domain/Sources/Camera/UseCases/FetchCameraUploadImageUseCase.swift b/14th-team5-iOS/Domain/Sources/Camera/UseCases/Camera/FetchCameraUploadImageUseCase.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Camera/UseCases/FetchCameraUploadImageUseCase.swift rename to 14th-team5-iOS/Domain/Sources/Camera/UseCases/Camera/FetchCameraUploadImageUseCase.swift diff --git a/14th-team5-iOS/Domain/Sources/Camera/UseCases/FetchCameraTodayMissionUseCase.swift b/14th-team5-iOS/Domain/Sources/Camera/UseCases/Mission/FetchCameraTodayMissionUseCase.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Camera/UseCases/FetchCameraTodayMissionUseCase.swift rename to 14th-team5-iOS/Domain/Sources/Camera/UseCases/Mission/FetchCameraTodayMissionUseCase.swift diff --git a/14th-team5-iOS/Domain/Sources/Camera/UseCases/EditCameraProfileImageUseCase.swift b/14th-team5-iOS/Domain/Sources/Camera/UseCases/Profile/EditCameraProfileImageUseCase.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Camera/UseCases/EditCameraProfileImageUseCase.swift rename to 14th-team5-iOS/Domain/Sources/Camera/UseCases/Profile/EditCameraProfileImageUseCase.swift diff --git a/14th-team5-iOS/Domain/Sources/Camera/UseCases/FetchCameraRealEmojiListUseCase.swift b/14th-team5-iOS/Domain/Sources/Camera/UseCases/RealEmoji/FetchCameraRealEmojiListUseCase.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Camera/UseCases/FetchCameraRealEmojiListUseCase.swift rename to 14th-team5-iOS/Domain/Sources/Camera/UseCases/RealEmoji/FetchCameraRealEmojiListUseCase.swift diff --git a/14th-team5-iOS/Domain/Sources/Camera/UseCases/FetchCameraRealEmojiUpdateUseCase.swift b/14th-team5-iOS/Domain/Sources/Camera/UseCases/RealEmoji/FetchCameraRealEmojiUpdateUseCase.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Camera/UseCases/FetchCameraRealEmojiUpdateUseCase.swift rename to 14th-team5-iOS/Domain/Sources/Camera/UseCases/RealEmoji/FetchCameraRealEmojiUpdateUseCase.swift diff --git a/14th-team5-iOS/Domain/Sources/Camera/UseCases/FetchCameraRealEmojiUploadUseCase.swift b/14th-team5-iOS/Domain/Sources/Camera/UseCases/RealEmoji/FetchCameraRealEmojiUploadUseCase.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Camera/UseCases/FetchCameraRealEmojiUploadUseCase.swift rename to 14th-team5-iOS/Domain/Sources/Camera/UseCases/RealEmoji/FetchCameraRealEmojiUploadUseCase.swift diff --git a/14th-team5-iOS/Domain/Sources/Camera/UseCases/FetchCameraRealEmojiUseCase.swift b/14th-team5-iOS/Domain/Sources/Camera/UseCases/RealEmoji/FetchCameraRealEmojiUseCase.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Camera/UseCases/FetchCameraRealEmojiUseCase.swift rename to 14th-team5-iOS/Domain/Sources/Camera/UseCases/RealEmoji/FetchCameraRealEmojiUseCase.swift diff --git a/14th-team5-iOS/Domain/Sources/Members/UseCases/DeleteMembersProfileUseCase.swift b/14th-team5-iOS/Domain/Sources/Members/UseCases/Members/DeleteMembersProfileUseCase.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Members/UseCases/DeleteMembersProfileUseCase.swift rename to 14th-team5-iOS/Domain/Sources/Members/UseCases/Members/DeleteMembersProfileUseCase.swift diff --git a/14th-team5-iOS/Domain/Sources/Members/UseCases/FetchMembersPostListUseCase.swift b/14th-team5-iOS/Domain/Sources/Members/UseCases/Members/FetchMembersPostListUseCase.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Members/UseCases/FetchMembersPostListUseCase.swift rename to 14th-team5-iOS/Domain/Sources/Members/UseCases/Members/FetchMembersPostListUseCase.swift diff --git a/14th-team5-iOS/Domain/Sources/Members/UseCases/FetchMembersProfileUseCase.swift b/14th-team5-iOS/Domain/Sources/Members/UseCases/Members/FetchMembersProfileUseCase.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Members/UseCases/FetchMembersProfileUseCase.swift rename to 14th-team5-iOS/Domain/Sources/Members/UseCases/Members/FetchMembersProfileUseCase.swift diff --git a/14th-team5-iOS/Domain/Sources/Members/UseCases/UpdateMembersProfileUseCase.swift b/14th-team5-iOS/Domain/Sources/Members/UseCases/Members/UpdateMembersProfileUseCase.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Members/UseCases/UpdateMembersProfileUseCase.swift rename to 14th-team5-iOS/Domain/Sources/Members/UseCases/Members/UpdateMembersProfileUseCase.swift diff --git a/14th-team5-iOS/Domain/Sources/Mission/UseCases/CheckMissionAlertShowUseCase.swift b/14th-team5-iOS/Domain/Sources/Mission/UseCases/Mission/CheckMissionAlertShowUseCase.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Mission/UseCases/CheckMissionAlertShowUseCase.swift rename to 14th-team5-iOS/Domain/Sources/Mission/UseCases/Mission/CheckMissionAlertShowUseCase.swift diff --git a/14th-team5-iOS/Domain/Sources/Mission/UseCases/FetchMissionContentUseCase.swift b/14th-team5-iOS/Domain/Sources/Mission/UseCases/Mission/FetchMissionContentUseCase.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Mission/UseCases/FetchMissionContentUseCase.swift rename to 14th-team5-iOS/Domain/Sources/Mission/UseCases/Mission/FetchMissionContentUseCase.swift From 35037b497a9f037f1c728bae6f4a2db8862a638c Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Wed, 19 Jun 2024 22:45:56 +0900 Subject: [PATCH 120/263] =?UTF-8?q?fix:=20Keychain=EC=9D=98=20Key=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80,=20RefreshToken=20=EA=B0=B1=EC=8B=A0=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95=20(#564)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../KeychainWrapper/KeychainWrapperKey.swift | 7 +- .../KeychainWrapperSubscript.swift | 12 ++++ .../Data/Sources/APIs/APIWorker.swift | 5 +- .../APIs/OAuth/OAuthAPI/OAuthAPIWorker.swift | 15 ++-- .../OAuthAPI/Repository/OAuthRepository.swift | 29 ++++++-- .../Account/AccountAPI/AccountAPIWorker.swift | 3 +- .../Storages/Keychain/Keychain+Token.swift | 35 ++++++++-- .../Entities/OAuth/AuthResultEntity.swift | 12 ++-- .../xcschemes/Bibbi-Workspace.xcscheme | 70 +++++++++++++++++++ Tuist/Dependencies.swift | 3 +- .../Package+Templates.swift | 1 + 11 files changed, 159 insertions(+), 33 deletions(-) diff --git a/14th-team5-iOS/Core/Sources/BBStorages/KeychainWrapper/KeychainWrapperKey.swift b/14th-team5-iOS/Core/Sources/BBStorages/KeychainWrapper/KeychainWrapperKey.swift index 70a8c4905..b80b8d940 100644 --- a/14th-team5-iOS/Core/Sources/BBStorages/KeychainWrapper/KeychainWrapperKey.swift +++ b/14th-team5-iOS/Core/Sources/BBStorages/KeychainWrapper/KeychainWrapperKey.swift @@ -14,8 +14,13 @@ public extension KeychainWrapper.Key { static let idToken: Self = "idToken" // 소셜 로그인의 AccessToken // MARK: - OAuth + static let newAccessToken: Self = "newAccessToken" + static let newRefreshToken: Self = "newRefreshToken" + static let newFcmToken: Self = "newFcmToken" + + + // MARK: - Old OAuth Key static let accessToken: Self = "accessToken" - static let refreshToken: Self = "refreshToken" static let fcmToken: Self = "fcmToken" } diff --git a/14th-team5-iOS/Core/Sources/BBStorages/KeychainWrapper/KeychainWrapperSubscript.swift b/14th-team5-iOS/Core/Sources/BBStorages/KeychainWrapper/KeychainWrapperSubscript.swift index 694d796ea..c63af1c1b 100644 --- a/14th-team5-iOS/Core/Sources/BBStorages/KeychainWrapper/KeychainWrapperSubscript.swift +++ b/14th-team5-iOS/Core/Sources/BBStorages/KeychainWrapper/KeychainWrapperSubscript.swift @@ -49,6 +49,14 @@ public extension KeychainWrapper { } } + subscript(key: Key) -> NSCoding? { + get { object(forKey: key) } + set { + guard let value = newValue else { return } + set(value, forKey: key.rawValue) + } + } + subscript(key: Key) -> Data? { get { data(forKey: key) } set { @@ -81,6 +89,10 @@ public extension KeychainWrapper { string(forKey: key.rawValue) } + func object(forKey key: Key) -> NSCoding? { + object(forKey: key.rawValue) + } + func data(forKey key: Key) -> Data? { data(forKey: key.rawValue) } diff --git a/14th-team5-iOS/Data/Sources/APIs/APIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/APIWorker.swift index 10917e524..145f366e9 100644 --- a/14th-team5-iOS/Data/Sources/APIs/APIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/APIWorker.swift @@ -90,10 +90,7 @@ public class APIWorker: NSObject { request.httpBody = jsonData print("interCepter call with name \(url)") - return AF.rx.request( - urlRequest: request, - interceptor: NetworkInterceptor() - ) + return AF.rx.request(urlRequest: request) .retry(5) .validate(statusCode: 200..<300) .responseData() diff --git a/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/OAuthAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/OAuthAPIWorker.swift index e4d9112ea..930cb9aec 100644 --- a/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/OAuthAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/OAuthAPIWorker.swift @@ -31,7 +31,7 @@ extension OAuthAPIWorker { // MARK: - Refresh Access Token - public func refreshAccessToken(body: RefreshAccessTokenRequestDTO) -> Single { + public func refreshAccessToken(body: RefreshAccessTokenRequestDTO) -> Single { let spec = OAuthAPIs.refreshToken.spec return request(spec: spec, jsonEncodable: body) @@ -43,14 +43,13 @@ extension OAuthAPIWorker { } .map(AuthResultResponseDTO.self) .catchAndReturn(nil) - .map { $0?.toDomain() } .asSingle() } // MARK: - Register New Member - public func registerNewMember(body: CreateNewMemberRequestDTO) -> Single { + public func registerNewMember(body: CreateNewMemberRequestDTO) -> Single { let spec = OAuthAPIs.registerMember.spec return request(spec: spec, jsonEncodable: body) @@ -62,14 +61,13 @@ extension OAuthAPIWorker { } .map(AuthResultResponseDTO.self) .catchAndReturn(nil) - .map { $0?.toDomain() } .asSingle() } // MARK: - Sign In With SNS - public func signIn(_ type: SignInType, body: NativeSocialLoginRequestDTO) -> Single { + public func signIn(_ type: SignInType, body: NativeSocialLoginRequestDTO) -> Single { let spec = OAuthAPIs.signIn(type).spec return request(spec: spec, jsonEncodable: body) @@ -81,7 +79,6 @@ extension OAuthAPIWorker { } .map(AuthResultResponseDTO.self) .catchAndReturn(nil) - .map { $0?.toDomain() } .asSingle() } @@ -89,7 +86,7 @@ extension OAuthAPIWorker { // MARK: - Register FCM Token - public func registerNewFCMToken(body: AddFCMTokenRequestDTO) -> Single { + public func registerNewFCMToken(body: AddFCMTokenRequestDTO) -> Single { let spec = OAuthAPIs.registerFCMToken.spec return request(spec: spec, jsonEncodable: body) @@ -101,14 +98,13 @@ extension OAuthAPIWorker { } .map(DefaultResponseDTO.self) .catchAndReturn(nil) - .map { $0?.toDomain() } .asSingle() } // MARK: - Delete FCM Token - public func deleteFCMToken(fcmToken token: String) -> Single { + public func deleteFCMToken(fcmToken token: String) -> Single { let spec = OAuthAPIs.deleteFCMToken(token).spec return request(spec: spec) @@ -120,7 +116,6 @@ extension OAuthAPIWorker { } .map(DefaultResponseDTO.self) .catchAndReturn(nil) - .map { $0?.toDomain() } .asSingle() } 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 916d4f0e3..e703d2512 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 @@ -34,12 +34,17 @@ extension OAuthRepository { ) return oAuthApiWorker.refreshAccessToken(body: body) .observe(on: RxSchedulers.main) + .map { $0?.toDomain() } .do(onSuccess: { [weak self] in guard let keychain = self?.tokenKeychainStorage else { return } - keychain.saveAccessToken($0?.accessToken) - keychain.saveRefreshToken($0?.refreshToken) + let accessToken = AccessToken( + accessToken: $0?.accessToken, + refreshToken: $0?.refreshToken, + isTemporaryToken: $0?.isTemporaryToken + ) + keychain.saveOldAccessToken(accessToken) }) .asObservable() } @@ -55,12 +60,17 @@ extension OAuthRepository { ) return oAuthApiWorker.registerNewMember(body: body) .observe(on: RxSchedulers.main) + .map { $0?.toDomain() } .do(onSuccess: { [weak self] in guard let keychain = self?.tokenKeychainStorage else { return } - keychain.saveAccessToken($0?.accessToken) - keychain.saveRefreshToken($0?.refreshToken) + let accessToken = AccessToken( + accessToken: $0?.accessToken, + refreshToken: $0?.refreshToken, + isTemporaryToken: $0?.isTemporaryToken + ) + keychain.saveOldAccessToken(accessToken) }) .asObservable() } @@ -74,12 +84,17 @@ extension OAuthRepository { ) return oAuthApiWorker.signIn(type, body: body) .observe(on: RxSchedulers.main) + .map { $0?.toDomain() } .do(onSuccess: { [weak self] in guard let keychain = self?.tokenKeychainStorage else { return } - keychain.saveAccessToken($0?.accessToken) - keychain.saveRefreshToken($0?.refreshToken) + let accessToken = AccessToken( + accessToken: $0?.accessToken, + refreshToken: $0?.refreshToken, + isTemporaryToken: $0?.isTemporaryToken + ) + keychain.saveOldAccessToken(accessToken) }) .asObservable() } @@ -93,6 +108,7 @@ extension OAuthRepository { ) return oAuthApiWorker.registerNewFCMToken(body: body) .observe(on: RxSchedulers.main) + .map { $0?.toDomain() } .asObservable() } @@ -109,6 +125,7 @@ extension OAuthRepository { return oAuthApiWorker.deleteFCMToken(fcmToken: fcmToken) .observe(on: RxSchedulers.main) + .map { $0?.toDomain() } .asObservable() } diff --git a/14th-team5-iOS/Data/Sources/Account/AccountAPI/AccountAPIWorker.swift b/14th-team5-iOS/Data/Sources/Account/AccountAPI/AccountAPIWorker.swift index bdecdb21f..94266e7e5 100644 --- a/14th-team5-iOS/Data/Sources/Account/AccountAPI/AccountAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Account/AccountAPI/AccountAPIWorker.swift @@ -32,7 +32,7 @@ extension AccountAPIs { // MARK: SignIn extension AccountAPIWorker { private func signInWith(spec: APISpec, jsonEncodable: Encodable) -> Single { - return request(spec: spec/*, headers: [BibbiAPI.Header.xAppKey]*/, jsonEncodable: jsonEncodable) + return request(spec: spec, headers: [BibbiAPI.Header.xAppKey], jsonEncodable: jsonEncodable) .subscribe(on: Self.queue) .do(onNext: { if let str = String(data: $0.1, encoding: .utf8) { @@ -95,6 +95,7 @@ extension AccountAPIWorker { func accountRefreshToken(parameter: Encodable) -> Single { let spec = AccountAPIs.refreshToken.spec + print("RefreshToken: \(parameter)") return request(spec: spec, headers: [BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.contentJson], jsonEncodable: parameter) .subscribe(on: Self.queue) .do { diff --git a/14th-team5-iOS/Data/Sources/Storages/Keychain/Keychain+Token.swift b/14th-team5-iOS/Data/Sources/Storages/Keychain/Keychain+Token.swift index a558df100..a6329a4aa 100644 --- a/14th-team5-iOS/Data/Sources/Storages/Keychain/Keychain+Token.swift +++ b/14th-team5-iOS/Data/Sources/Storages/Keychain/Keychain+Token.swift @@ -19,6 +19,9 @@ public protocol TokenKeychainType: KeychainType { func saveAccessToken(_ accessToken: String?) func loadAccessToken() -> String? + func saveOldAccessToken(_ tokenResult: AccessToken?) + func loadOldAccessToken() -> AccessToken? + func saveRefreshToken(_ refreshToken: String?) func loadRefreshToken() -> String? @@ -58,31 +61,51 @@ final public class TokenKeychain: TokenKeychainType { // MARK: - AccessToken public func saveAccessToken(_ accessToken: String?) { - keychain[.accessToken] = accessToken + keychain[.newAccessToken] = accessToken } public func loadAccessToken() -> String? { - keychain[.accessToken] + keychain[.newAccessToken] + } + + + // MARK: - Old AccessToken + public func saveOldAccessToken(_ tokenResult: AccessToken?) { + // AccessToken은 Core 모듈의 TokenRepository에 정의되어 있음 + guard + let data = try? JSONEncoder().encode(tokenResult), + let str = String(data: data, encoding: .utf8) + else { return } + keychain[.accessToken] = str + } + + 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 } // MARK: - RefreshToken public func saveRefreshToken(_ refreshToken: String?) { - keychain[.refreshToken] = refreshToken + keychain[.newRefreshToken] = refreshToken } public func loadRefreshToken() -> String? { - keychain[.refreshToken] + keychain[.newRefreshToken] } // MARK: - FCM Token public func saveFCMToken(_ fcmToken: String?) { - keychain[.fcmToken] = fcmToken + keychain[.newFcmToken] = fcmToken } public func loadFCMToken() -> String? { - keychain[.fcmToken] + keychain[.newFcmToken] } } diff --git a/14th-team5-iOS/Domain/Sources/Entities/OAuth/AuthResultEntity.swift b/14th-team5-iOS/Domain/Sources/Entities/OAuth/AuthResultEntity.swift index 1bc68dd64..2b36ff308 100644 --- a/14th-team5-iOS/Domain/Sources/Entities/OAuth/AuthResultEntity.swift +++ b/14th-team5-iOS/Domain/Sources/Entities/OAuth/AuthResultEntity.swift @@ -8,11 +8,15 @@ import Foundation public struct AuthResultEntity { - public var refreshToken: String - public var accessToken: String - public var isTemporaryToken: Bool + public var refreshToken: String? + public var accessToken: String? + public var isTemporaryToken: Bool? - public init(refreshToken: String, accessToken: String, isTemporaryToken: Bool) { + public init( + refreshToken: String?, + accessToken: String?, + isTemporaryToken: Bool? + ) { self.refreshToken = refreshToken self.accessToken = accessToken self.isTemporaryToken = isTemporaryToken diff --git a/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme b/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme index 5f307e6b4..defe8e429 100644 --- a/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme +++ b/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme @@ -356,6 +356,20 @@ ReferencedContainer = "container:Tuist/Dependencies/SwiftPackageManager/.build/checkouts/GoogleDataTransport/GoogleDataTransport.xcodeproj"> + + + + + + + + + + + + + + + + + + + + Date: Thu, 20 Jun 2024 11:04:54 +0900 Subject: [PATCH 121/263] =?UTF-8?q?feat:=20CommentDIContainer=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#552)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../App/Sources/Application/AppDelegate.swift | 3 +- .../DIContainer/CommentDIContainer.swift | 81 +++++++++++++++++++ .../DailyCalendarViewController.swift | 2 +- ...ner.swift => PostCommentDIContainer.swift} | 2 +- .../ViewControllers/PostViewController.swift | 2 +- .../ReactionViewController.swift | 2 +- 6 files changed, 87 insertions(+), 5 deletions(-) create mode 100644 14th-team5-iOS/App/Sources/Application/DIContainer/CommentDIContainer.swift rename 14th-team5-iOS/App/Sources/Presentation/PostComment/Dependency/{CommentDIContainer.swift => PostCommentDIContainer.swift} (97%) diff --git a/14th-team5-iOS/App/Sources/Application/AppDelegate.swift b/14th-team5-iOS/App/Sources/Application/AppDelegate.swift index b66095b53..e71e9144f 100644 --- a/14th-team5-iOS/App/Sources/Application/AppDelegate.swift +++ b/14th-team5-iOS/App/Sources/Application/AppDelegate.swift @@ -54,7 +54,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { let containers: [BaseContainer] = [ AppDIContainer(), - CalendarDIContainer() + CalendarDIContainer(), + CommentDIContainer() ] containers.forEach { $0.registerDependencies() diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/CommentDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/CommentDIContainer.swift new file mode 100644 index 000000000..0305e94a1 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/CommentDIContainer.swift @@ -0,0 +1,81 @@ +// +// CommentDIContainer.swift +// App +// +// Created by 김건우 on 6/20/24. +// + +import Core +import Data +import Domain + +final class CommentDIContainer: BaseContainer { + + // MARK: - Make UseCase + + func makeCreateCommentUseCase() -> CreateCommentUseCaseProtocol { + CreateCommentUseCase( + commentRepository: makeCommentRepository() + ) + } + + func makeDeleteCommentUseCase() -> DeleteCommentUseCaseProtocol { + DeleteCommentUseCase( + commentRepository: makeCommentRepository() + ) + } + + func makeFetchCommentUseCase() -> FetchCommentUseCaseProtocol { + FetchCommentUseCase( + commentRepository: makeCommentRepository() + ) + } + + func makeUpdateCommentUseCase() -> UpdateCommentUseCaseProtocol { + UpdateCommentUseCase( + commentRepository: makeCommentRepository() + ) + } + + // Deprecated + func makeCommentUseCase() -> PostCommentUseCaseProtocol { + PostCommentUseCase( + commentRepository: makeCommentRepository() + ) + } + + + // MARK: - Make Repository + + func makeCommentRepository() -> CommentRepositoryProtocol { + return CommentRepository() + } + + + // MARK: - Register + + func registerDependencies() { + container.register(type: CreateCommentUseCaseProtocol.self) { _ in + self.makeCreateCommentUseCase() + } + + container.register(type: DeleteCommentUseCaseProtocol.self) { _ in + self.makeDeleteCommentUseCase() + } + + container.register(type: FetchCommentUseCaseProtocol.self) { _ in + self.makeFetchCommentUseCase() + } + + container.register(type: UpdateCommentUseCaseProtocol.self) { _ in + self.makeUpdateCommentUseCase() + } + + // Deprecated + container.register(type: PostCommentUseCaseProtocol.self) { _ in + self.makeCommentUseCase() + } + } + + +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/DailyCalendarViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/DailyCalendarViewController.swift index 1dfb3bfe0..fa69f5e6d 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/DailyCalendarViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/DailyCalendarViewController.swift @@ -253,7 +253,7 @@ public final class DailyCalendarViewController: BBNavigationViewController { }) { switch postList { case let .main(post): - let postCommentViewController = CommentDIContainer( + let postCommentViewController = PostCommentDIContainer( postId: post.postId ).makeViewController() diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/ReactionViewController.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/ReactionViewController.swift index 1d081eba0..d947a36e1 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/ReactionViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/ReactionViewController.swift @@ -150,7 +150,7 @@ extension ReactionViewController { .withLatestFrom(postListData) .withUnretained(self) { ($0, $1) } .bind(onNext: { - let commentViewController = CommentDIContainer( + let commentViewController = PostCommentDIContainer( postId: $1.postId ).makeViewController() From e9ccf47217e1abd45ac00904e76d594b9378171f Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Thu, 20 Jun 2024 11:15:51 +0900 Subject: [PATCH 122/263] =?UTF-8?q?feat:=20PickDIContainer=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#552)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../App/Sources/Application/AppDelegate.swift | 3 +- .../DIContainer/PickDIContainer.swift | 69 +++++++++++++++++++ .../UseCases/Pick/WhoPickedMeUseCase.swift | 4 +- 3 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 14th-team5-iOS/App/Sources/Application/DIContainer/PickDIContainer.swift diff --git a/14th-team5-iOS/App/Sources/Application/AppDelegate.swift b/14th-team5-iOS/App/Sources/Application/AppDelegate.swift index e71e9144f..2e7e07139 100644 --- a/14th-team5-iOS/App/Sources/Application/AppDelegate.swift +++ b/14th-team5-iOS/App/Sources/Application/AppDelegate.swift @@ -55,7 +55,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { let containers: [BaseContainer] = [ AppDIContainer(), CalendarDIContainer(), - CommentDIContainer() + CommentDIContainer(), + PickDIContainer() ] containers.forEach { $0.registerDependencies() diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/PickDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/PickDIContainer.swift new file mode 100644 index 000000000..e3b9e3289 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/PickDIContainer.swift @@ -0,0 +1,69 @@ +// +// PickDIContainer.swift +// App +// +// Created by 김건우 on 6/20/24. +// + +import Core +import Data +import Domain + +final class PickDIContainer: BaseContainer { + + // MARK: - Make UseCase + + func makePickMemberUseCase() -> PickMemberUseCaseProtocol { + PickMemberUseCase( + pickRepository: makePickRepository() + ) + } + + func makeWhoDidIPickedMemberUseCase() -> WhoDidIPickMemberUseCaseProtocol { + WhoDidIPickMemberUseCase( + pickRepository: makePickRepository() + ) + } + + func makeWhoPickedMeUseCase() -> WhoPickedMeUseCaseProtocol { + WhoPickedMeUseCase( + pickRepository: makePickRepository() + ) + } + + // Deprecated + func makePickUseCase() -> PickUseCaseProtocol { + PickUseCase( + pickRepository: makePickRepository() + ) + } + + + // MARK: - Make Repository + + func makePickRepository() -> PickRepositoryProtocol { + return PickRepository() + } + + // MARK: - Register + + func registerDependencies() { + container.register(type: PickMemberUseCaseProtocol.self) { _ in + makePickMemberUseCase() + } + + container.register(type: WhoDidIPickMemberUseCaseProtocol.self) { _ in + makeWhoDidIPickedMemberUseCase() + } + + container.register(type: WhoPickedMeUseCaseProtocol.self) { _ in + makeWhoPickedMeUseCase() + } + + // Deprecated + container.register(type: PickUseCaseProtocol.self) { _ in + makePickUseCase() + } + } + +} diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Pick/WhoPickedMeUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Pick/WhoPickedMeUseCase.swift index 869ba1a91..e2ca9ac8b 100644 --- a/14th-team5-iOS/Domain/Sources/UseCases/Pick/WhoPickedMeUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/Pick/WhoPickedMeUseCase.swift @@ -9,11 +9,11 @@ import Foundation import RxSwift -public protocol WhoPickedMeMemberUseCaseProtocol { +public protocol WhoPickedMeUseCaseProtocol { func execute(memberId: String) -> Observable } -public final class WhoPickedMeUseCase: WhoPickedMeMemberUseCaseProtocol { +public final class WhoPickedMeUseCase: WhoPickedMeUseCaseProtocol { // MARK: - Repositories private var pickRepository: PickRepositoryProtocol From d26b773ef1e33738f41a984dbe62e55a24684ce2 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Thu, 20 Jun 2024 11:47:43 +0900 Subject: [PATCH 123/263] =?UTF-8?q?feat:=20OAuthDIContainer=20=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#547)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../App/Sources/Application/AppDelegate.swift | 3 +- .../DIContainer/OAuthDIContainer.swift | 78 +++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 14th-team5-iOS/App/Sources/Application/DIContainer/OAuthDIContainer.swift diff --git a/14th-team5-iOS/App/Sources/Application/AppDelegate.swift b/14th-team5-iOS/App/Sources/Application/AppDelegate.swift index b66095b53..d12e6d021 100644 --- a/14th-team5-iOS/App/Sources/Application/AppDelegate.swift +++ b/14th-team5-iOS/App/Sources/Application/AppDelegate.swift @@ -54,7 +54,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { let containers: [BaseContainer] = [ AppDIContainer(), - CalendarDIContainer() + CalendarDIContainer(), + OAuthDIContainer() ] containers.forEach { $0.registerDependencies() diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/OAuthDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/OAuthDIContainer.swift new file mode 100644 index 000000000..d49953ddf --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/OAuthDIContainer.swift @@ -0,0 +1,78 @@ +// +// OAuthDIContainer.swift +// App +// +// Created by 김건우 on 6/20/24. +// + +import Core +import Data +import Domain + +final class OAuthDIContainer: BaseContainer { + + // MARK: - Make UseCase + + func makeRegisterNewFCMTokenUseCase() -> RegisterNewFCMTokenUseCaseProtocol { + RegisterNewFCMTokenUseCase( + oauthRepository: makeOAuthRepository() + ) + } + + func makeDeleteFCMTokenUseCase() -> DeleteFCMTokenUseCaseProtocol { + DeleteFCMTokenUseCase( + oauthRepository: makeOAuthRepository() + ) + } + + func makeRefreshAccessTokenUseCase() -> RefreshAccessTokenUseCaseProtocol { + RefreshAccessTokenUseCase( + oauthRepository: makeOAuthRepository() + ) + } + + func makeRegisterNewMemberUseCase() -> RegisterNewMemberUseCaseProtocol { + RegisterNewMemberUseCase( + oauthRepository: makeOAuthRepository() + ) + } + + func makeSignInUseCase() -> SignInUseCaseProtocol { + SignInUseCase( + oauthRepository: makeOAuthRepository() + ) + } + + + // MARK: - Make Repository + + func makeOAuthRepository() -> OAuthRepositoryProtocol { + return OAuthRepository() + } + + + // MARK: - Register + + func registerDependencies() { + container.register(type: RegisterNewFCMTokenUseCaseProtocol.self) { _ in + self.makeRegisterNewFCMTokenUseCase() + } + + container.register(type: DeleteFCMTokenUseCaseProtocol.self) { _ in + self.makeDeleteFCMTokenUseCase() + } + + container.register(type: RefreshAccessTokenUseCaseProtocol.self) { _ in + self.makeRefreshAccessTokenUseCase() + } + + container.register(type: RegisterNewMemberUseCaseProtocol.self) { _ in + self.makeRegisterNewMemberUseCase() + } + + container.register(type: SignInUseCaseProtocol.self) { _ in + self.makeSignInUseCase() + } + } + +} From 70bfa11192554c4a26efe3f6aff1ef8aff87bedd Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Thu, 20 Jun 2024 11:48:29 +0900 Subject: [PATCH 124/263] =?UTF-8?q?feat:=20SignInDIContainer=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#547)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../App/Sources/Application/AppDelegate.swift | 3 +- .../DIContainer/SignInDIContainer.swift | 48 +++++++++++++++++++ .../SignIn/Repository/SignInRepository.swift | 2 +- 3 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 14th-team5-iOS/App/Sources/Application/DIContainer/SignInDIContainer.swift diff --git a/14th-team5-iOS/App/Sources/Application/AppDelegate.swift b/14th-team5-iOS/App/Sources/Application/AppDelegate.swift index d12e6d021..41294bde9 100644 --- a/14th-team5-iOS/App/Sources/Application/AppDelegate.swift +++ b/14th-team5-iOS/App/Sources/Application/AppDelegate.swift @@ -55,7 +55,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { let containers: [BaseContainer] = [ AppDIContainer(), CalendarDIContainer(), - OAuthDIContainer() + OAuthDIContainer(), + SignInDIContainer() ] containers.forEach { $0.registerDependencies() diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/SignInDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/SignInDIContainer.swift new file mode 100644 index 000000000..359c390b5 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/SignInDIContainer.swift @@ -0,0 +1,48 @@ +// +// SignInDIContainer.swift +// App +// +// Created by 김건우 on 6/20/24. +// + +import Core +import Data +import Domain + +final class SignInDIContainer: BaseContainer { + + // MARK: - Make UseCase + + func makeSocialSignInUseCase() -> SocialSignInUseCaseProtocol { + SocialSignInUseCase( + signInRepository: makeSignInRepository() + ) + } + + func makeSocialSignOutUseCase() -> SocialSignOutUseCaseProtocol { + SocialSignOutUseCase( + signInRepository: makeSignInRepository() + ) + } + + + // MARK: - Make Repository + + func makeSignInRepository() -> SignInRepositoryProtocol { + return SignInRepository() + } + + + // MARK: - Register + + func registerDependencies() { + container.register(type: SocialSignInUseCaseProtocol.self) { _ in + makeSocialSignInUseCase() + } + + container.register(type: SocialSignOutUseCaseProtocol.self) { _ in + makeSocialSignOutUseCase() + } + } + +} diff --git a/14th-team5-iOS/Data/Sources/APIs/SignIn/Repository/SignInRepository.swift b/14th-team5-iOS/Data/Sources/APIs/SignIn/Repository/SignInRepository.swift index 6f8b1bac2..676096ce8 100644 --- a/14th-team5-iOS/Data/Sources/APIs/SignIn/Repository/SignInRepository.swift +++ b/14th-team5-iOS/Data/Sources/APIs/SignIn/Repository/SignInRepository.swift @@ -10,7 +10,7 @@ import Domain import RxSwift -final class SignInRepository: SignInRepositoryProtocol { +public final class SignInRepository: SignInRepositoryProtocol { // MARK: - Properties public var disposeBag = DisposeBag() From 0602a29e2f24dbcf8bca32514b839b878f9e8d8a Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Thu, 20 Jun 2024 12:02:52 +0900 Subject: [PATCH 125/263] =?UTF-8?q?feat:=20FamilyDIContainer=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#547)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../App/Sources/Application/AppDelegate.swift | 3 +- .../DIContainer/FamilyDIContainer.swift | 110 ++++++++++++++++++ 2 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 14th-team5-iOS/App/Sources/Application/DIContainer/FamilyDIContainer.swift diff --git a/14th-team5-iOS/App/Sources/Application/AppDelegate.swift b/14th-team5-iOS/App/Sources/Application/AppDelegate.swift index 41294bde9..17311f3fa 100644 --- a/14th-team5-iOS/App/Sources/Application/AppDelegate.swift +++ b/14th-team5-iOS/App/Sources/Application/AppDelegate.swift @@ -56,7 +56,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { AppDIContainer(), CalendarDIContainer(), OAuthDIContainer(), - SignInDIContainer() + SignInDIContainer(), + FamilyDIContainer() ] containers.forEach { $0.registerDependencies() diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/FamilyDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/FamilyDIContainer.swift new file mode 100644 index 000000000..953d69cc3 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/FamilyDIContainer.swift @@ -0,0 +1,110 @@ +// +// FamilyDIContainer.swift +// App +// +// Created by 김건우 on 6/20/24. +// + +import Core +import Data +import Domain + +final class FamilyDIContainer: BaseContainer { + + // MARK: - Make UseCase + + func makeCreateFamilyUseCase() -> CreateFamilyUseCaseProtocol { + CreateFamilyUseCase( + familyRepository: makeFamilyRepository() + ) + } + + func makeFetchFamilyCreatedAtUseCase() -> FetchFamilyCreatedAtUseCaseProtocol { + FetchFamilyCreatedAtUseCase( + familyRepository: makeFamilyRepository() + ) + } + + func makeFetchFamilyMemberUseCase() -> FetchFamilyMembersUseCaseProtocol { + FetchFamilyMembersUseCase( + familyRepository: makeFamilyRepository() + ) + } + + func makeFetchFamilyMemberFromStorageUseCase() -> FetchFamilyMembersUseCaseFromStorageProtocol { + FetchFamilyMembersFromStoragUseCase( + familyRepository: makeFamilyRepository() + ) + } + + func makeFetchInvitationLinkUseCase() -> FetchInvitationLinkUseCaseProtocol { + FetchInvitationUrlUseCase( + familyRepository: makeFamilyRepository() + ) + } + + func makeJoinFamilyUseCase() -> JoinFamilyUseCaseProtocol { + JoinFamilyUseCase( + familyRepository: makeFamilyRepository() + ) + } + + func makeResignFamilyUseCase() -> ResignFamilyUseCaseProtocol { + ResignFamilyUseCase( + familyRepository: makeFamilyRepository() + ) + } + + // Deprecated + func makeFamilyUseCase() -> FamilyUseCaseProtocol { + FamilyUseCase( + familyRepository: makeFamilyRepository() + ) + } + + + // MARK: - Make Repository + + func makeFamilyRepository() -> FamilyRepositoryProtocol { + return FamilyRepository() + } + + + // MARK: - Register + + func registerDependencies() { + container.register(type: CreateFamilyUseCaseProtocol.self) { _ in + makeCreateFamilyUseCase() + } + + container.register(type: FetchFamilyCreatedAtUseCaseProtocol.self) { _ in + makeFetchFamilyCreatedAtUseCase() + } + + container.register(type: FetchFamilyMembersUseCaseProtocol.self) { _ in + makeFetchFamilyMemberUseCase() + } + + container.register(type: FetchFamilyMembersUseCaseFromStorageProtocol.self) { _ in + makeFetchFamilyMemberFromStorageUseCase() + } + + container.register(type: FetchInvitationLinkUseCaseProtocol.self) { _ in + makeFetchInvitationLinkUseCase() + } + + container.register(type: JoinFamilyUseCaseProtocol.self) { _ in + makeJoinFamilyUseCase() + } + + container.register(type: ResignFamilyUseCaseProtocol.self) { _ in + makeResignFamilyUseCase() + } + + // Deprecated + container.register(type: FamilyUseCaseProtocol.self) { _ in + makeFamilyUseCase() + } + } + +} From 2e1c3ab663f359ddf0fdd739f30519f9bfef47df Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Thu, 20 Jun 2024 23:12:48 +0900 Subject: [PATCH 126/263] =?UTF-8?q?feat:=20BaseWrapperView=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#571)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Core/Sources/Base/BaseWrapperView.swift | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 14th-team5-iOS/Core/Sources/Base/BaseWrapperView.swift diff --git a/14th-team5-iOS/Core/Sources/Base/BaseWrapperView.swift b/14th-team5-iOS/Core/Sources/Base/BaseWrapperView.swift new file mode 100644 index 000000000..ad960b9d1 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Base/BaseWrapperView.swift @@ -0,0 +1,23 @@ +// +// BaseWrapperView.swift +// Core +// +// Created by 김건우 on 6/20/24. +// + +import UIKit + +import ReactorKit + +public protocol BaseWrapperView { + + associatedtype R: Reactor + associatedtype V: ReactorKit.View + + func makeReactor() -> R + func makeView() -> V + + var reactor: R { get } + var view: V { get } + +} From 95bfe245c379b63a238c6c7734a51ba3630051d4 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Sat, 22 Jun 2024 09:44:46 +0900 Subject: [PATCH 127/263] =?UTF-8?q?feat:=20private=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=A0=91=EA=B7=BC=EC=A0=9C=EC=96=B4=EC=9E=90=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#547)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DIContainer/CalendarDIContainer.swift | 12 ++++++------ .../DIContainer/FamilyDIContainer.swift | 18 +++++++++--------- .../DIContainer/OAuthDIContainer.swift | 12 ++++++------ .../DIContainer/SignInDIContainer.swift | 6 +++--- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/CalendarDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/CalendarDIContainer.swift index 27e899eec..d7ed8b11a 100644 --- a/14th-team5-iOS/App/Sources/Application/DIContainer/CalendarDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/CalendarDIContainer.swift @@ -13,32 +13,32 @@ final class CalendarDIContainer: BaseContainer { // MARK: - Make UseCase - func makeFetchCalendarBannerUseCase() -> FetchCalendarBannerUseCaseProtocol { + private func makeFetchCalendarBannerUseCase() -> FetchCalendarBannerUseCaseProtocol { FetchCalendarBannerUseCase( calendarRepository: makeCalendarRepository() ) } - func makeFetchStatisticsSummaryUseCase() -> FetchStatisticsSummaryUseCaseProtocol { + private func makeFetchStatisticsSummaryUseCase() -> FetchStatisticsSummaryUseCaseProtocol { FetchStatisticsSummaryUseCase( calendarRepository: makeCalendarRepository() ) } - func makeFetchDailyCalendarUseCase() -> FetchDailyCalendarUseCaseProtocol { + private func makeFetchDailyCalendarUseCase() -> FetchDailyCalendarUseCaseProtocol { FetchDailyCalendarUseCase( calendarRepository: makeCalendarRepository() ) } - func makeFetchMonthlyCalendarUseCase() -> FetchMonthlyCalendarUseCaseProtocol { + private func makeFetchMonthlyCalendarUseCase() -> FetchMonthlyCalendarUseCaseProtocol { FetchMonthlyCalendarUseCase( calendarRepository: makeCalendarRepository() ) } // Deprecated - func makeOldCalendarUseCase() -> CalendarUseCaseProtocol { + private func makeOldCalendarUseCase() -> CalendarUseCaseProtocol { CalendarUseCase( calendarRepository: makeCalendarRepository() ) @@ -48,7 +48,7 @@ final class CalendarDIContainer: BaseContainer { // MARK: - Make Repository - func makeCalendarRepository() -> CalendarRepositoryProtocol { + private func makeCalendarRepository() -> CalendarRepositoryProtocol { return CalendarRepository() } diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/FamilyDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/FamilyDIContainer.swift index 953d69cc3..5bd206471 100644 --- a/14th-team5-iOS/App/Sources/Application/DIContainer/FamilyDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/FamilyDIContainer.swift @@ -13,50 +13,50 @@ final class FamilyDIContainer: BaseContainer { // MARK: - Make UseCase - func makeCreateFamilyUseCase() -> CreateFamilyUseCaseProtocol { + private func makeCreateFamilyUseCase() -> CreateFamilyUseCaseProtocol { CreateFamilyUseCase( familyRepository: makeFamilyRepository() ) } - func makeFetchFamilyCreatedAtUseCase() -> FetchFamilyCreatedAtUseCaseProtocol { + private func makeFetchFamilyCreatedAtUseCase() -> FetchFamilyCreatedAtUseCaseProtocol { FetchFamilyCreatedAtUseCase( familyRepository: makeFamilyRepository() ) } - func makeFetchFamilyMemberUseCase() -> FetchFamilyMembersUseCaseProtocol { + private func makeFetchFamilyMemberUseCase() -> FetchFamilyMembersUseCaseProtocol { FetchFamilyMembersUseCase( familyRepository: makeFamilyRepository() ) } - func makeFetchFamilyMemberFromStorageUseCase() -> FetchFamilyMembersUseCaseFromStorageProtocol { + private func makeFetchFamilyMemberFromStorageUseCase() -> FetchFamilyMembersUseCaseFromStorageProtocol { FetchFamilyMembersFromStoragUseCase( familyRepository: makeFamilyRepository() ) } - func makeFetchInvitationLinkUseCase() -> FetchInvitationLinkUseCaseProtocol { + private func makeFetchInvitationLinkUseCase() -> FetchInvitationLinkUseCaseProtocol { FetchInvitationUrlUseCase( familyRepository: makeFamilyRepository() ) } - func makeJoinFamilyUseCase() -> JoinFamilyUseCaseProtocol { + private func makeJoinFamilyUseCase() -> JoinFamilyUseCaseProtocol { JoinFamilyUseCase( familyRepository: makeFamilyRepository() ) } - func makeResignFamilyUseCase() -> ResignFamilyUseCaseProtocol { + private func makeResignFamilyUseCase() -> ResignFamilyUseCaseProtocol { ResignFamilyUseCase( familyRepository: makeFamilyRepository() ) } // Deprecated - func makeFamilyUseCase() -> FamilyUseCaseProtocol { + private func makeFamilyUseCase() -> FamilyUseCaseProtocol { FamilyUseCase( familyRepository: makeFamilyRepository() ) @@ -65,7 +65,7 @@ final class FamilyDIContainer: BaseContainer { // MARK: - Make Repository - func makeFamilyRepository() -> FamilyRepositoryProtocol { + private func makeFamilyRepository() -> FamilyRepositoryProtocol { return FamilyRepository() } diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/OAuthDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/OAuthDIContainer.swift index d49953ddf..80cafa06c 100644 --- a/14th-team5-iOS/App/Sources/Application/DIContainer/OAuthDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/OAuthDIContainer.swift @@ -13,31 +13,31 @@ final class OAuthDIContainer: BaseContainer { // MARK: - Make UseCase - func makeRegisterNewFCMTokenUseCase() -> RegisterNewFCMTokenUseCaseProtocol { + private func makeRegisterNewFCMTokenUseCase() -> RegisterNewFCMTokenUseCaseProtocol { RegisterNewFCMTokenUseCase( oauthRepository: makeOAuthRepository() ) } - func makeDeleteFCMTokenUseCase() -> DeleteFCMTokenUseCaseProtocol { + private func makeDeleteFCMTokenUseCase() -> DeleteFCMTokenUseCaseProtocol { DeleteFCMTokenUseCase( oauthRepository: makeOAuthRepository() ) } - func makeRefreshAccessTokenUseCase() -> RefreshAccessTokenUseCaseProtocol { + private func makeRefreshAccessTokenUseCase() -> RefreshAccessTokenUseCaseProtocol { RefreshAccessTokenUseCase( oauthRepository: makeOAuthRepository() ) } - func makeRegisterNewMemberUseCase() -> RegisterNewMemberUseCaseProtocol { + private func makeRegisterNewMemberUseCase() -> RegisterNewMemberUseCaseProtocol { RegisterNewMemberUseCase( oauthRepository: makeOAuthRepository() ) } - func makeSignInUseCase() -> SignInUseCaseProtocol { + private func makeSignInUseCase() -> SignInUseCaseProtocol { SignInUseCase( oauthRepository: makeOAuthRepository() ) @@ -46,7 +46,7 @@ final class OAuthDIContainer: BaseContainer { // MARK: - Make Repository - func makeOAuthRepository() -> OAuthRepositoryProtocol { + private func makeOAuthRepository() -> OAuthRepositoryProtocol { return OAuthRepository() } diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/SignInDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/SignInDIContainer.swift index 359c390b5..1d3fca4a2 100644 --- a/14th-team5-iOS/App/Sources/Application/DIContainer/SignInDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/SignInDIContainer.swift @@ -13,13 +13,13 @@ final class SignInDIContainer: BaseContainer { // MARK: - Make UseCase - func makeSocialSignInUseCase() -> SocialSignInUseCaseProtocol { + private func makeSocialSignInUseCase() -> SocialSignInUseCaseProtocol { SocialSignInUseCase( signInRepository: makeSignInRepository() ) } - func makeSocialSignOutUseCase() -> SocialSignOutUseCaseProtocol { + private func makeSocialSignOutUseCase() -> SocialSignOutUseCaseProtocol { SocialSignOutUseCase( signInRepository: makeSignInRepository() ) @@ -28,7 +28,7 @@ final class SignInDIContainer: BaseContainer { // MARK: - Make Repository - func makeSignInRepository() -> SignInRepositoryProtocol { + private func makeSignInRepository() -> SignInRepositoryProtocol { return SignInRepository() } From 2bc5eb56f60896ce0f9448e2e058a2542c35d707 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Sat, 22 Jun 2024 09:46:17 +0900 Subject: [PATCH 128/263] =?UTF-8?q?feat:=20private=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=A0=91=EA=B7=BC=EC=A0=9C=EC=96=B4=EC=9E=90=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#552)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Application/DIContainer/CommentDIContainer.swift | 12 ++++++------ .../Application/DIContainer/PickDIContainer.swift | 10 +++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/CommentDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/CommentDIContainer.swift index 0305e94a1..0e1111a75 100644 --- a/14th-team5-iOS/App/Sources/Application/DIContainer/CommentDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/CommentDIContainer.swift @@ -13,32 +13,32 @@ final class CommentDIContainer: BaseContainer { // MARK: - Make UseCase - func makeCreateCommentUseCase() -> CreateCommentUseCaseProtocol { + private func makeCreateCommentUseCase() -> CreateCommentUseCaseProtocol { CreateCommentUseCase( commentRepository: makeCommentRepository() ) } - func makeDeleteCommentUseCase() -> DeleteCommentUseCaseProtocol { + private func makeDeleteCommentUseCase() -> DeleteCommentUseCaseProtocol { DeleteCommentUseCase( commentRepository: makeCommentRepository() ) } - func makeFetchCommentUseCase() -> FetchCommentUseCaseProtocol { + private func makeFetchCommentUseCase() -> FetchCommentUseCaseProtocol { FetchCommentUseCase( commentRepository: makeCommentRepository() ) } - func makeUpdateCommentUseCase() -> UpdateCommentUseCaseProtocol { + private func makeUpdateCommentUseCase() -> UpdateCommentUseCaseProtocol { UpdateCommentUseCase( commentRepository: makeCommentRepository() ) } // Deprecated - func makeCommentUseCase() -> PostCommentUseCaseProtocol { + private func makeCommentUseCase() -> PostCommentUseCaseProtocol { PostCommentUseCase( commentRepository: makeCommentRepository() ) @@ -47,7 +47,7 @@ final class CommentDIContainer: BaseContainer { // MARK: - Make Repository - func makeCommentRepository() -> CommentRepositoryProtocol { + private func makeCommentRepository() -> CommentRepositoryProtocol { return CommentRepository() } diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/PickDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/PickDIContainer.swift index e3b9e3289..eacd77895 100644 --- a/14th-team5-iOS/App/Sources/Application/DIContainer/PickDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/PickDIContainer.swift @@ -13,26 +13,26 @@ final class PickDIContainer: BaseContainer { // MARK: - Make UseCase - func makePickMemberUseCase() -> PickMemberUseCaseProtocol { + private func makePickMemberUseCase() -> PickMemberUseCaseProtocol { PickMemberUseCase( pickRepository: makePickRepository() ) } - func makeWhoDidIPickedMemberUseCase() -> WhoDidIPickMemberUseCaseProtocol { + private func makeWhoDidIPickedMemberUseCase() -> WhoDidIPickMemberUseCaseProtocol { WhoDidIPickMemberUseCase( pickRepository: makePickRepository() ) } - func makeWhoPickedMeUseCase() -> WhoPickedMeUseCaseProtocol { + private func makeWhoPickedMeUseCase() -> WhoPickedMeUseCaseProtocol { WhoPickedMeUseCase( pickRepository: makePickRepository() ) } // Deprecated - func makePickUseCase() -> PickUseCaseProtocol { + private func makePickUseCase() -> PickUseCaseProtocol { PickUseCase( pickRepository: makePickRepository() ) @@ -41,7 +41,7 @@ final class PickDIContainer: BaseContainer { // MARK: - Make Repository - func makePickRepository() -> PickRepositoryProtocol { + private func makePickRepository() -> PickRepositoryProtocol { return PickRepository() } From 4b92880c17547d0e5653c1836f1be0dec193f4ca Mon Sep 17 00:00:00 2001 From: Kyoungmi <1mmgm.dev@gmail.com> Date: Thu, 20 Jun 2024 23:33:26 +0900 Subject: [PATCH 129/263] [feat]: add wrapper(#565) --- .../App/Sources/Application/AppDelegate.swift | 8 +++ .../Sources/Application/SceneDelegate.swift | 2 +- .../App/Sources/Manager/DeepLinkManager.swift | 9 ++- .../DailyCalendarViewController.swift | 7 +- .../Dependency/MainCameraDIContainer.swift | 20 ------ .../MainFamilyViewDIContainer.swift | 28 +++----- .../Dependency/MainPostViewDIContainer.swift | 29 +++----- .../Home/Dependency/MainViewDIContainer.swift | 46 ++++++------ .../Home/Reactor/MainFamilyViewReactor.swift | 9 +-- .../Home/Reactor/MainPostViewReactor.swift | 8 +-- .../Home/Reactor/MainViewReactor.swift | 22 ++---- .../MainPostViewController.swift | 10 +-- .../ViewControllers/MainViewController.swift | 4 +- .../SegmentPageViewController.swift | 4 +- .../MainFamilyViewControllerWrapper.swift | 31 ++++++++ .../MainPostViewControllerWrapper.swift | 38 ++++++++++ .../Wrapper/MainViewControllerWrapper.swift | 31 ++++++++ .../InputFamilyLinkViewController.swift | 2 +- .../JoinFamilyViewController.swift | 2 +- .../JoinedFamilyViewController.swift | 2 +- .../OnBoarding/OnBoardingViewController.swift | 2 +- .../PostDetailCellDIContainer.swift | 27 +++---- .../Dependency/PostListsDIContainer.swift | 59 ++-------------- .../Dependency/ReactionDIContainer.swift | 65 ++++++++--------- .../ReactionMemberDIContainer.swift | 20 +++--- .../SelectableEmojiDIContainer.swift | 36 +++++----- .../PostDetail/Model/ReactionSection.swift | 2 +- .../Reactor/PostDetailViewReactor.swift | 8 +-- .../PostDetail/Reactor/PostReactor.swift | 15 +--- .../Reactor/ReactionMemberViewReactor.swift | 7 +- .../Reactor/ReactionViewReactor.swift | 39 ++++------- .../Reactor/SelectableEmojiReactor.swift | 14 ++-- .../PostDetail/Reactor/TempCellReactor.swift | 6 +- .../ViewControllers/PostViewController.swift | 34 +-------- .../ReactionViewController.swift | 4 +- .../Views/ReactionCollectionViewCell.swift | 2 +- .../Wrapper/PostDetailCellWrapper.swift | 25 +++++++ .../PostDetailViewControllerWrapper.swift | 39 +++++++++++ ...ReactionMembersViewControllerWrapper.swift | 38 ++++++++++ .../ReactionViewControllerWrapper.swift | 40 +++++++++++ ...SelectableEmojiViewControllerWrapper.swift | 41 +++++++++++ .../Profile/ProfileFeedViewController.swift | 7 +- .../Splash/SplashViewController.swift | 2 +- .../Sources/DIContainer/DIContainer.swift | 1 - .../FetchReactionResponseDTO.swift | 6 +- .../ReactionAPI/ReactionAPIWorker.swift | 2 +- .../Repository/ReactionRepository.swift | 2 +- .../FetchRealEmojiListResponseDTO.swift | 6 +- .../RealEmojiAPI/RealEmojiAPIWorker.swift | 2 +- .../Repository/RealEmojiRepository.swift | 2 +- .../Entities/PostList/PostEntity.swift | 4 ++ ...ealEmojiEntity.swift => EmojiEntity.swift} | 2 +- .../ReactionRepositoryProtocol.swift | 2 +- .../RealEmojiRepositoryProtocol.swift | 2 +- .../Reaction/FetchReactionListUseCase.swift | 4 +- .../RealEmoji/FetchRealEmojiListUseCase.swift | 4 +- .../xcschemes/Bibbi-Workspace.xcscheme | 70 ------------------- 57 files changed, 496 insertions(+), 457 deletions(-) delete mode 100644 14th-team5-iOS/App/Sources/Presentation/Home/Dependency/MainCameraDIContainer.swift create mode 100644 14th-team5-iOS/App/Sources/Presentation/Home/Wrapper/MainFamilyViewControllerWrapper.swift create mode 100644 14th-team5-iOS/App/Sources/Presentation/Home/Wrapper/MainPostViewControllerWrapper.swift create mode 100644 14th-team5-iOS/App/Sources/Presentation/Home/Wrapper/MainViewControllerWrapper.swift create mode 100644 14th-team5-iOS/App/Sources/Presentation/PostDetail/Wrapper/PostDetailCellWrapper.swift create mode 100644 14th-team5-iOS/App/Sources/Presentation/PostDetail/Wrapper/PostDetailViewControllerWrapper.swift create mode 100644 14th-team5-iOS/App/Sources/Presentation/PostDetail/Wrapper/ReactionMembersViewControllerWrapper.swift create mode 100644 14th-team5-iOS/App/Sources/Presentation/PostDetail/Wrapper/ReactionViewControllerWrapper.swift create mode 100644 14th-team5-iOS/App/Sources/Presentation/PostDetail/Wrapper/SelectableEmojiViewControllerWrapper.swift rename 14th-team5-iOS/Domain/Sources/Entities/RealEmoji/{RealEmojiEntity.swift => EmojiEntity.swift} (96%) diff --git a/14th-team5-iOS/App/Sources/Application/AppDelegate.swift b/14th-team5-iOS/App/Sources/Application/AppDelegate.swift index d3cb1d79b..2f8f4b1c0 100644 --- a/14th-team5-iOS/App/Sources/Application/AppDelegate.swift +++ b/14th-team5-iOS/App/Sources/Application/AppDelegate.swift @@ -60,6 +60,14 @@ class AppDelegate: UIResponder, UIApplicationDelegate { PickDIContainer(), OAuthDIContainer(), SignInDIContainer() + MainFamilyViewDIContainer(), + MainPostViewDIContainer(), + MainViewDIContainer(), + PostDetailCellDIContainer(), + PostListsDIContainer(), + ReactionDIContainer(), + ReactionMemberDIContainer(), + SelectableEmojiDIContainer() ] containers.forEach { $0.registerDependencies() diff --git a/14th-team5-iOS/App/Sources/Application/SceneDelegate.swift b/14th-team5-iOS/App/Sources/Application/SceneDelegate.swift index 7b2ad3d4f..d141e7911 100644 --- a/14th-team5-iOS/App/Sources/Application/SceneDelegate.swift +++ b/14th-team5-iOS/App/Sources/Application/SceneDelegate.swift @@ -32,7 +32,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } self.window = UIWindow(windowScene: scene) - self.window?.rootViewController = UINavigationController(rootViewController: MainViewDIContainer().makeViewController()) + self.window?.rootViewController = UINavigationController(rootViewController: MainViewControllerWrapper().makeViewController()) self.window?.makeKeyAndVisible() } diff --git a/14th-team5-iOS/App/Sources/Manager/DeepLinkManager.swift b/14th-team5-iOS/App/Sources/Manager/DeepLinkManager.swift index 4bae1cf7b..798daa9e6 100644 --- a/14th-team5-iOS/App/Sources/Manager/DeepLinkManager.swift +++ b/14th-team5-iOS/App/Sources/Manager/DeepLinkManager.swift @@ -86,11 +86,10 @@ final class DeepLinkManager { if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, let keyWindow = windowScene.windows.first(where: { $0.isKeyWindow }), let navigationController = keyWindow.rootViewController as? UINavigationController { - let viewController = PostListsDIContainer().makeViewController( - postLists: PostSection.Model(model: 0, items: items), - selectedIndex: index, - notificationDeepLink: data - ) + let viewController = PostDetailViewControllerWrapper( + selectedIndex: index.row, + originPostLists: PostSection.Model(model: 0, items: items) + ).makeViewController() navigationController.pushViewController(viewController, animated: true) } } diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/DailyCalendarViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/DailyCalendarViewController.swift index fa69f5e6d..38bd24772 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/DailyCalendarViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/DailyCalendarViewController.swift @@ -28,12 +28,7 @@ public final class DailyCalendarViewController: BBNavigationViewController MainCameraButtonView { - return MainCameraButtonView(reactor: makeReactor()) - } - - func makeReactor() -> MainCameraReactor { - return MainCameraReactor() - } -} diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Dependency/MainFamilyViewDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Dependency/MainFamilyViewDIContainer.swift index 556873931..f5aa35c1f 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Dependency/MainFamilyViewDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Dependency/MainFamilyViewDIContainer.swift @@ -12,25 +12,7 @@ import Core import Data import Domain -final class MainFamilyViewDIContainer { - private var globalState: GlobalStateProviderProtocol { - guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { - return GlobalStateProvider() - } - return appDelegate.globalStateProvider - } - - - func makeViewController() -> MainFamilyViewController { - return MainFamilyViewController(reactor: makeReactor()) - } - - private func makeReactor() -> MainFamilyViewReactor { - return MainFamilyViewReactor(provider: globalState, familyUseCase: makeInviteFamilyUseCase()) - } -} - -extension MainFamilyViewDIContainer { +final class MainFamilyViewDIContainer: BaseContainer { private func makeInviteFamilyRepository() -> FamilyRepositoryProtocol { return FamilyRepository() } @@ -39,3 +21,11 @@ extension MainFamilyViewDIContainer { return FamilyUseCase(familyRepository: makeInviteFamilyRepository()) } } + +extension MainFamilyViewDIContainer { + func registerDependencies() { + container.register(type: FamilyUseCaseProtocol.self) { _ in + self.makeInviteFamilyUseCase() + } + } +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Dependency/MainPostViewDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Dependency/MainPostViewDIContainer.swift index e5c737594..ca151b6af 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Dependency/MainPostViewDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Dependency/MainPostViewDIContainer.swift @@ -12,29 +12,20 @@ import Data import Domain -final class MainPostViewDIContainer { - private var globalState: GlobalStateProviderProtocol { - guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { - return GlobalStateProvider() - } - return appDelegate.globalStateProvider - } - - func makeViewController(type: PostType) -> MainPostViewController { - return MainPostViewController(reactor: makeReactor(type: type)) - } - - private func makeReactor(type: PostType) -> MainPostViewReactor { - return MainPostViewReactor(initialState: .init(type: type), provider: globalState, postUseCase: makePostUseCase()) - } -} - -extension MainPostViewDIContainer { +final class MainPostViewDIContainer: BaseContainer { private func makePostRepository() -> PostListRepositoryProtocol { return PostRepository() } - func makePostUseCase() -> FetchPostListUseCaseProtocol { + private func makePostUseCase() -> FetchPostListUseCaseProtocol { return FetchPostListUseCase(postListRepository: makePostRepository()) } } + +extension MainPostViewDIContainer { + func registerDependencies() { + container.register(type: FetchPostListUseCaseProtocol.self) { _ in + self.makePostUseCase() + } + } +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Dependency/MainViewDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Dependency/MainViewDIContainer.swift index c522b9b1e..e0a0f95d0 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Dependency/MainViewDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Dependency/MainViewDIContainer.swift @@ -12,31 +12,7 @@ import Domain import Core -final class MainViewDIContainer { - private var globalState: GlobalStateProviderProtocol { - guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { - return GlobalStateProvider() - } - return appDelegate.globalStateProvider - } - - - func makeViewController() -> MainViewController { - return MainViewController(reactor: makeReactor()) - } -} - -extension MainViewDIContainer { - private func makeReactor() -> MainViewReactor { - return MainViewReactor( - fetchMainUseCase: makeFetchMainUseCase(), - fetchMainNightUseCase: makeFetchMainNightUseCase(), - pickUseCase: makePickUseCase(), - checkMissionAlertShowUseCase: makeCheckMissionAlertShowUseCase(), - provider: globalState - ) - } - +final class MainViewDIContainer: BaseContainer { private func makePickReposiotry() -> PickRepositoryProtocol { return PickRepository() } @@ -65,3 +41,23 @@ extension MainViewDIContainer { return CheckMissionAlertShowUseCase(missionRepository: makeMissionRepository()) } } + +extension MainViewDIContainer { + func registerDependencies() { + container.register(type: PickUseCaseProtocol.self) { _ in + self.makePickUseCase() + } + + container.register(type: FetchMainUseCaseProtocol.self) { _ in + self.makeFetchMainUseCase() + } + + container.register(type: FetchNightMainViewUseCaseProtocol.self) { _ in + self.makeFetchMainNightUseCase() + } + + container.register(type: CheckMissionAlertShowUseCaseProtocol.self) {_ in + self.makeCheckMissionAlertShowUseCase() + } + } +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainFamilyViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainFamilyViewReactor.swift index 60dec4726..a7c801c22 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainFamilyViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainFamilyViewReactor.swift @@ -37,13 +37,8 @@ final class MainFamilyViewReactor: Reactor { } let initialState: State = State() - let provider: GlobalStateProviderProtocol - private let familyUseCase: FamilyUseCaseProtocol - - init(provider: GlobalStateProviderProtocol, familyUseCase: FamilyUseCaseProtocol) { - self.provider = provider - self.familyUseCase = familyUseCase - } + @Injected var provider: GlobalStateProviderProtocol + @Injected var familyUseCase: FamilyUseCaseProtocol } extension MainFamilyViewReactor { diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainPostViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainPostViewReactor.swift index 40307c8df..893672ab3 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainPostViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainPostViewReactor.swift @@ -34,13 +34,11 @@ final class MainPostViewReactor: Reactor { } let initialState: State - private let provider: GlobalStateProviderProtocol - private let postUseCase: FetchPostListUseCaseProtocol + @Injected var provider: GlobalStateProviderProtocol + @Injected var postUseCase: FetchPostListUseCaseProtocol - init(initialState: State, provider: GlobalStateProviderProtocol, postUseCase: FetchPostListUseCaseProtocol) { + init(initialState: State) { self.initialState = initialState - self.provider = provider - self.postUseCase = postUseCase } } diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift index 3b4f25cb5..1043d7f3f 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift @@ -90,23 +90,11 @@ final class MainViewReactor: Reactor { } let initialState: State = State() - private let fetchMainUseCase: FetchMainUseCaseProtocol - private let fetchMainNightUseCase: FetchNightMainViewUseCaseProtocol - private let pickUseCase: PickUseCaseProtocol - private let checkMissionAlertShowUseCase: CheckMissionAlertShowUseCaseProtocol - private let provider: GlobalStateProviderProtocol - - init(fetchMainUseCase: FetchMainUseCaseProtocol, - fetchMainNightUseCase: FetchNightMainViewUseCaseProtocol, - pickUseCase: PickUseCaseProtocol, - checkMissionAlertShowUseCase: CheckMissionAlertShowUseCaseProtocol, - provider: GlobalStateProviderProtocol) { - self.fetchMainUseCase = fetchMainUseCase - self.fetchMainNightUseCase = fetchMainNightUseCase - self.pickUseCase = pickUseCase - self.checkMissionAlertShowUseCase = checkMissionAlertShowUseCase - self.provider = provider - } + @Injected var provider: GlobalStateProviderProtocol + @Injected var fetchMainUseCase: FetchMainUseCaseProtocol + @Injected var fetchMainNightUseCase: FetchNightMainViewUseCaseProtocol + @Injected var pickUseCase: PickUseCaseProtocol + @Injected var checkMissionAlertShowUseCase: CheckMissionAlertShowUseCaseProtocol } extension MainViewReactor { diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainPostViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainPostViewController.swift index e76b1e63e..0a98ca8c8 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainPostViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainPostViewController.swift @@ -87,11 +87,11 @@ extension MainPostViewController { .throttle(RxConst.milliseconds300Interval, scheduler: RxSchedulers.main) .withUnretained(self) .bind(onNext: { - $0.0.navigationController?.pushViewController( - PostListsDIContainer().makeViewController( - postLists: reactor.currentState.postSection, - selectedIndex: $0.1 - ), animated: true) + let vc = PostDetailViewControllerWrapper( + selectedIndex: $0.1.row, + originPostLists: reactor.currentState.postSection + ).makeViewController() + $0.0.navigationController?.pushViewController(vc, animated: true) }) .disposed(by: disposeBag) 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 54cbd86b7..061f52cd3 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift @@ -16,7 +16,7 @@ import RxCocoa import RxSwift final class MainViewController: BaseViewController, UICollectionViewDelegateFlowLayout { - private let familyViewController: MainFamilyViewController = MainFamilyViewDIContainer().makeViewController() + private let familyViewController: MainFamilyViewController = MainFamilyViewControllerWrapper().makeViewController() private let timerView: TimerView = TimerView(reactor: TimerReactor()) private let descriptionLabel: BibbiLabel = BibbiLabel(.body2Regular, textAlignment: .center, textColor: .gray300) @@ -26,7 +26,7 @@ final class MainViewController: BaseViewController, UICollectio private let segmentControl: BibbiSegmentedControl = BibbiSegmentedControl() private let pageViewController: SegmentPageViewController = SegmentPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal) - private let cameraButton: MainCameraButtonView = MainCameraDIContainer().makeView() + private let cameraButton: MainCameraButtonView = MainCameraButtonView(reactor: MainCameraReactor()) private let alertConfirmRelay: PublishRelay<(String, String)> = PublishRelay<(String, String)>() override func viewDidLoad() { diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/SegmentPageViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/SegmentPageViewController.swift index 5476df541..e0779ea03 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/SegmentPageViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/SegmentPageViewController.swift @@ -23,8 +23,8 @@ struct PageRelay { } final class SegmentPageViewController: UIPageViewController { - private let survivalViewController: MainPostViewController = MainPostViewDIContainer().makeViewController(type: .survival) - private let missionViewController: MainPostViewController = MainPostViewDIContainer().makeViewController(type: .mission) + private let survivalViewController: MainPostViewController = MainPostViewControllerWrapper(type: .survival).makeViewController() + private let missionViewController: MainPostViewController = MainPostViewControllerWrapper(type: .mission).makeViewController() private let disposeBag = DisposeBag() private lazy var pages: [UIViewController] = [survivalViewController, missionViewController] diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Wrapper/MainFamilyViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Wrapper/MainFamilyViewControllerWrapper.swift new file mode 100644 index 000000000..80a4f2398 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Wrapper/MainFamilyViewControllerWrapper.swift @@ -0,0 +1,31 @@ +// +// MainFamilyViewControllerWrapper.swift +// App +// +// Created by 마경미 on 17.06.24. +// + +import Foundation + +import Core + +final class MainFamilyViewControllerWrapper: BaseWrapper { + typealias R = MainFamilyViewReactor + typealias V = MainFamilyViewController + + func makeViewController() -> V { + return MainFamilyViewController(reactor: makeReactor()) + } + + func makeReactor() -> R { + return MainFamilyViewReactor() + } + + var viewController: V { + makeViewController() + } + + var reactor: R { + makeReactor() + } +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Wrapper/MainPostViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Wrapper/MainPostViewControllerWrapper.swift new file mode 100644 index 000000000..92dee3353 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Wrapper/MainPostViewControllerWrapper.swift @@ -0,0 +1,38 @@ +// +// MainPostViewController.swift +// App +// +// Created by 마경미 on 17.06.24. +// + +import Foundation + +import Core +import Domain + +final class MainPostViewControllerWrapper: BaseWrapper { + typealias R = MainPostViewReactor + typealias V = MainPostViewController + + private let type: PostType + + init(type: PostType) { + self.type = type + } + + func makeViewController() -> V { + return MainPostViewController(reactor: makeReactor()) + } + + func makeReactor() -> R { + return MainPostViewReactor(initialState: .init(type: type)) + } + + var viewController: V { + makeViewController() + } + + var reactor: R { + makeReactor() + } +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Wrapper/MainViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Wrapper/MainViewControllerWrapper.swift new file mode 100644 index 000000000..e57770aca --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Wrapper/MainViewControllerWrapper.swift @@ -0,0 +1,31 @@ +// +// MainViewControllerWrapper.swift +// App +// +// Created by 마경미 on 17.06.24. +// + +import Foundation + +import Core + +final class MainViewControllerWrapper: BaseWrapper { + typealias R = MainViewReactor + typealias V = MainViewController + + func makeViewController() -> V { + return MainViewController(reactor: makeReactor()) + } + + func makeReactor() -> R { + return MainViewReactor() + } + + var viewController: V { + makeViewController() + } + + var reactor: R { + makeReactor() + } +} diff --git a/14th-team5-iOS/App/Sources/Presentation/JoinFamily/ViewController/InputFamilyLinkViewController.swift b/14th-team5-iOS/App/Sources/Presentation/JoinFamily/ViewController/InputFamilyLinkViewController.swift index a0dc55033..da6131e45 100644 --- a/14th-team5-iOS/App/Sources/Presentation/JoinFamily/ViewController/InputFamilyLinkViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/JoinFamily/ViewController/InputFamilyLinkViewController.swift @@ -190,7 +190,7 @@ extension InputFamilyLinkViewController { UserDefaults.standard.clearInviteCode() guard let sceneDelegate = UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate else { return } - sceneDelegate.window?.rootViewController = UINavigationController(rootViewController: MainViewDIContainer().makeViewController()) + sceneDelegate.window?.rootViewController = UINavigationController(rootViewController: MainViewControllerWrapper().makeViewController()) sceneDelegate.window?.makeKeyAndVisible() } } diff --git a/14th-team5-iOS/App/Sources/Presentation/JoinFamily/ViewController/JoinFamilyViewController.swift b/14th-team5-iOS/App/Sources/Presentation/JoinFamily/ViewController/JoinFamilyViewController.swift index e089709ce..31f5590aa 100644 --- a/14th-team5-iOS/App/Sources/Presentation/JoinFamily/ViewController/JoinFamilyViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/JoinFamily/ViewController/JoinFamilyViewController.swift @@ -120,7 +120,7 @@ extension JoinFamilyViewController { UserDefaults.standard.clearInviteCode() guard let sceneDelegate = UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate else { return } - sceneDelegate.window?.rootViewController = UINavigationController(rootViewController: MainViewDIContainer().makeViewController()) + sceneDelegate.window?.rootViewController = UINavigationController(rootViewController: MainViewControllerWrapper().makeViewController()) sceneDelegate.window?.makeKeyAndVisible() } diff --git a/14th-team5-iOS/App/Sources/Presentation/JoinFamily/ViewController/JoinedFamilyViewController.swift b/14th-team5-iOS/App/Sources/Presentation/JoinFamily/ViewController/JoinedFamilyViewController.swift index d4758a935..069c92daa 100644 --- a/14th-team5-iOS/App/Sources/Presentation/JoinFamily/ViewController/JoinedFamilyViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/JoinFamily/ViewController/JoinedFamilyViewController.swift @@ -140,7 +140,7 @@ extension JoinedFamilyViewController { UserDefaults.standard.clearInviteCode() guard let sceneDelegate = UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate else { return } - sceneDelegate.window?.rootViewController = UINavigationController(rootViewController: MainViewDIContainer().makeViewController()) + sceneDelegate.window?.rootViewController = UINavigationController(rootViewController: MainViewControllerWrapper().makeViewController()) sceneDelegate.window?.makeKeyAndVisible() } diff --git a/14th-team5-iOS/App/Sources/Presentation/OnBoarding/OnBoardingViewController.swift b/14th-team5-iOS/App/Sources/Presentation/OnBoarding/OnBoardingViewController.swift index 814466c42..0e55b3e86 100644 --- a/14th-team5-iOS/App/Sources/Presentation/OnBoarding/OnBoardingViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/OnBoarding/OnBoardingViewController.swift @@ -151,7 +151,7 @@ final public class OnBoardingViewController: BaseViewController MemberRepositoryProtocol { +final class PostDetailCellDIContainer: BaseContainer { + private func makeMemberRepository() -> MemberRepositoryProtocol { return MemberRepository() } - func makeMemberUseCase() -> MemberUseCaseProtocol { + private func makeMemberUseCase() -> MemberUseCaseProtocol { return MemberUseCase(memberRepository: makeMemberRepository()) } - - func makeReactor(type: PostDetailViewReactor.CellType = .home, post: PostEntity) -> PostDetailViewReactor { - return PostDetailViewReactor( - provider: globalState, - memberUserCase: makeMemberUseCase(), - initialState: .init(type: type, post: post) - ) +} + +extension PostDetailCellDIContainer { + func registerDependencies() { + container.register(type: MemberUseCaseProtocol.self) { _ in + self.makeMemberUseCase() + } } } diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/PostListsDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/PostListsDIContainer.swift index 352756059..4cd206f16 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/PostListsDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/PostListsDIContainer.swift @@ -13,46 +13,9 @@ import Domain import RxDataSources -final class PostListsDIContainer { - typealias ViewContrller = PostViewController - typealias Reactor = PostReactor +final class PostListsDIContainer: BaseContainer { - private var globalState: GlobalStateProviderProtocol { - guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { - return GlobalStateProvider() - } - return appDelegate.globalStateProvider - } - - func makeViewController( - postLists: PostSection.Model, - selectedIndex: IndexPath, - notificationDeepLink: NotificationDeepLink? = nil - ) -> ViewContrller { - return PostViewController(reactor: makeReactor( - postLists: postLists, - selectedIndex: selectedIndex.row, - notificationDeepLink: notificationDeepLink) - ) - } - - func makePostRepository() -> PostListRepositoryProtocol { - return PostRepository() - } - - func makePostUseCase() -> FetchPostListUseCaseProtocol { - return FetchPostListUseCase(postListRepository: makePostRepository()) - } - - func makeEmojiRepository() -> ReactionRepositoryProtocol { - return ReactionRepository() - } - - func makeRealEmojiRepository() -> RealEmojiRepositoryProtocol { - return RealEmojiRepository() - } - - func makeMissionRepository() -> MissionRepositoryProtocol { + private func makeMissionRepository() -> MissionRepositoryProtocol { return MissionRepository() } @@ -60,19 +23,9 @@ final class PostListsDIContainer { return FetchMissionContentUseCase(missionRepository: makeMissionRepository()) } - func makeReactor( - postLists: PostSection.Model, - selectedIndex: Int, - notificationDeepLink: NotificationDeepLink? - ) -> Reactor { - return PostReactor( - provider: globalState, - fetchMissionUseCase: makeMissionUseCase(), - initialState: PostReactor.State( - selectedIndex: selectedIndex, - originPostLists: postLists, - notificationDeepLink: notificationDeepLink - ) - ) + func registerDependencies() { + container.register(type: FetchMissionContentUseCaseProtocol.self) { _ in + self.makeMissionUseCase() + } } } diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/ReactionDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/ReactionDIContainer.swift index 4804d8b34..4683db913 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/ReactionDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/ReactionDIContainer.swift @@ -15,38 +15,7 @@ enum ReactionType { case calendar } -final class ReactionDIContainer { - var type: ReactionType - - init(type: ReactionType) { - self.type = type - } - - private var globalState: GlobalStateProviderProtocol { - guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { - return GlobalStateProvider() - } - return appDelegate.globalStateProvider - } - - private func makeReactor(post: PostEntity) -> ReactionViewReactor { - return ReactionViewReactor( - initialState: .init(type: type, postListData: post), - provider: globalState, - fetchReactionUseCase: makeFetchReactionListUseCase(), - createReactionUseCase: makeCreateReactionUseCase(), - removeReactionUseCase: makeRemoveReactionUseCase(), - fetchRealEmojiListUseCase: makeFetchRealEmojiListUseCase(), - createRealEmojiUseCase: makeCreateRealEmojiUseCase(), - removeRealEmojiUseCase: makeRemoveRealEmojiUseCase()) - } - - func makeViewController(post: PostEntity) -> ReactionViewController { - return ReactionViewController(reactor: makeReactor(post: post)) - } -} - -extension ReactionDIContainer { +final class ReactionDIContainer: BaseContainer { private func makeRealEmojiRepository() -> RealEmojiRepositoryProtocol { return RealEmojiRepository() } @@ -62,9 +31,7 @@ extension ReactionDIContainer { private func makeFetchRealEmojiListUseCase() -> FetchRealEmojiListUseCaseProtocol { return FetchRealEmojiListUseCase(realEmojiRepository: makeRealEmojiRepository()) } -} - -extension ReactionDIContainer { + private func makeReactionRepository() -> ReactionRepositoryProtocol { return ReactionRepository() } @@ -81,3 +48,31 @@ extension ReactionDIContainer { return FetchReactionListUseCase(reactionRepository: makeReactionRepository()) } } + +extension ReactionDIContainer { + func registerDependencies() { + container.register(type: CreateRealEmojiUseCaseProtocol.self) { _ in + self.makeCreateRealEmojiUseCase() + } + + container.register(type: RemoveRealEmojiUseCaseProtocol.self) { _ in + self.makeRemoveRealEmojiUseCase() + } + + container.register(type: FetchRealEmojiListUseCaseProtocol.self) { _ in + self.makeFetchRealEmojiListUseCase() + } + + container.register(type: CreateReactionUseCaseProtocol.self) { _ in + self.makeCreateReactionUseCase() + } + + container.register(type: RemoveReactionUseCaseProtocol.self) { _ in + self.makeRemoveReactionUseCase() + } + + container.register(type: FetchReactionListUseCaseProtocol.self) { _ in + self.makeFetchReactionListUseCase() + } + } +} diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/ReactionMemberDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/ReactionMemberDIContainer.swift index f28ceec6b..163550f07 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/ReactionMemberDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/ReactionMemberDIContainer.swift @@ -13,20 +13,20 @@ import Domain import RxDataSources -final class ReactionMemberDIContainer { - func makeFamilyRepository() -> FamilyRepositoryProtocol { +final class ReactionMemberDIContainer: BaseContainer { + private func makeFamilyRepository() -> FamilyRepositoryProtocol { return FamilyRepository() } - func makeFamilyUseCase() -> FamilyUseCaseProtocol { + private func makeFamilyUseCase() -> FamilyUseCaseProtocol { return FamilyUseCase(familyRepository: makeFamilyRepository()) } - - func makeReactionMemberReactor(emojiData: RealEmojiEntity) -> ReactionMemberViewReactor { - return ReactionMemberViewReactor(initialState: .init(emojiData: emojiData), familyUseCase: makeFamilyUseCase()) - } - - func makeViewController(emojiData: RealEmojiEntity) -> ReactionMembersViewController { - return ReactionMembersViewController(reactor: makeReactionMemberReactor(emojiData: emojiData)) +} + +extension ReactionMemberDIContainer { + func registerDependencies() { + container.register(type: FamilyUseCaseProtocol.self) { _ in + self.makeFamilyUseCase() + } } } diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/SelectableEmojiDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/SelectableEmojiDIContainer.swift index 200b2e2ac..aff2fa2ba 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/SelectableEmojiDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/SelectableEmojiDIContainer.swift @@ -13,21 +13,7 @@ import Domain import RxSwift -final class SelectableEmojiDIContainer { - private func makeReactor(postId: String) -> SelectableEmojiReactor { - return SelectableEmojiReactor( - postId: postId, - createReactionUseCase: makeCreateReactionUseCase(), - createRealEmojiUseCase: makeCreateRealEmojiUseCase(), - fetchMyRealEmojiUseCase: makeFetchMyRealEmojiUseCase()) - } - - func makeViewController(postId: String, subject: PublishSubject) -> SelectableEmojiViewController { - return SelectableEmojiViewController(reactor: makeReactor(postId: postId), selectedReactionSubject: subject) - } -} - -extension SelectableEmojiDIContainer { +final class SelectableEmojiDIContainer: BaseContainer { private func makeRealEmojiRepository() -> RealEmojiRepositoryProtocol { return RealEmojiRepository() } @@ -39,9 +25,7 @@ extension SelectableEmojiDIContainer { private func makeFetchMyRealEmojiUseCase() -> FetchMyRealEmojiUseCaseProtocol { return FetchMyRealEmojiUseCase(realEmojiRepository: makeRealEmojiRepository()) } -} - -extension SelectableEmojiDIContainer { + private func makeReactionRepository() -> ReactionRepositoryProtocol { return ReactionRepository() } @@ -50,3 +34,19 @@ extension SelectableEmojiDIContainer { return CreateReactionUseCase(reactionRepository: makeReactionRepository()) } } + +extension SelectableEmojiDIContainer { + func registerDependencies() { + container.register(type: CreateRealEmojiUseCaseProtocol.self) { _ in + self.makeCreateRealEmojiUseCase() + } + + container.register(type: FetchMyRealEmojiUseCaseProtocol.self) { _ in + self.makeFetchMyRealEmojiUseCase() + } + + container.register(type: CreateReactionUseCaseProtocol.self) { _ in + self.makeCreateReactionUseCase() + } + } +} diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Model/ReactionSection.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Model/ReactionSection.swift index dad21c137..195906f41 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Model/ReactionSection.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Model/ReactionSection.swift @@ -18,7 +18,7 @@ struct ReactionSection { enum Item { case addComment(Int) case addReaction - case main(RealEmojiEntity) + case main(EmojiEntity) } } diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostDetailViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostDetailViewReactor.swift index dd6f9b692..7c15598ba 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostDetailViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostDetailViewReactor.swift @@ -39,12 +39,10 @@ final class PostDetailViewReactor: Reactor { } let initialState: State - let provider: GlobalStateProviderProtocol - let memberUseCase: MemberUseCaseProtocol + @Injected var provider: GlobalStateProviderProtocol + @Injected var memberUseCase: MemberUseCaseProtocol - init(provider: GlobalStateProviderProtocol, memberUserCase: MemberUseCaseProtocol, initialState: State) { - self.provider = provider - self.memberUseCase = memberUserCase + init(initialState: State) { self.initialState = initialState } diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostReactor.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostReactor.swift index 3df3ac5d5..723f7810c 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostReactor.swift @@ -35,23 +35,14 @@ final class PostReactor: Reactor { @Pulse var missionContent: MissionContentEntity? = nil @Pulse var reactionMemberIds: [String] = [] @Pulse var shouldPushProfileViewController: String? - - var notificationDeepLink: NotificationDeepLink? } let initialState: State - - let fetchMissionUseCase: FetchMissionContentUseCaseProtocol - let provider: GlobalStateProviderProtocol + @Injected var fetchMissionUseCase: FetchMissionContentUseCaseProtocol + @Injected var provider: GlobalStateProviderProtocol - init( - provider: GlobalStateProviderProtocol, - fetchMissionUseCase: FetchMissionContentUseCaseProtocol, - initialState: State - ) { - self.provider = provider - self.fetchMissionUseCase = fetchMissionUseCase + init(initialState: State) { self.initialState = initialState } } diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/ReactionMemberViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/ReactionMemberViewReactor.swift index fd3fb32e9..76aa4009b 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/ReactionMemberViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/ReactionMemberViewReactor.swift @@ -22,16 +22,15 @@ final class ReactionMemberViewReactor: Reactor { } struct State { - let emojiData: RealEmojiEntity + let emojiData: EmojiEntity var memberDataSource: [FamilyMemberProfileSectionModel] = [] } let initialState: State - let familyUseCase: FamilyUseCaseProtocol + @Injected var familyUseCase: FamilyUseCaseProtocol - init(initialState: State, familyUseCase: FamilyUseCaseProtocol) { + init(initialState: State) { self.initialState = initialState - self.familyUseCase = familyUseCase } } diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/ReactionViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/ReactionViewReactor.swift index f5bc454f7..888ac4b54 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/ReactionViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/ReactionViewReactor.swift @@ -18,17 +18,17 @@ final class ReactionViewReactor: Reactor { case tapComment case tapAddEmoji case longPressEmoji(IndexPath) - case selectCell(IndexPath, RealEmojiEntity) + case selectCell(IndexPath, EmojiEntity) case acceptPostListData(PostEntity) case fetchReactionList(String) } enum Mutation { - case setSpecificCell(IndexPath, RealEmojiEntity) + case setSpecificCell(IndexPath, EmojiEntity) case setSelectedReactionIndices([Int]) case setCommentSheet case setEmojiSheet - case setReactionMemberSheetEmoji(RealEmojiEntity) + case setReactionMemberSheetEmoji(EmojiEntity) case updateDataSource([ReactionSection.Item]) case setPost(PostEntity) case setPostCommentCount(Int) @@ -47,36 +47,23 @@ final class ReactionViewReactor: Reactor { .addReaction, ]) - @Pulse var reactionMemberSheetEmoji: RealEmojiEntity? = nil + @Pulse var reactionMemberSheetEmoji: EmojiEntity? = nil @Pulse var isShowingCommentSheet: Bool = false @Pulse var isShowingEmojiSheet: Bool = false @Pulse var selectionHapticFeedback: Bool = false } let initialState: State - let provider: GlobalStateProviderProtocol - let fetchReactionListUseCase: FetchReactionListUseCaseProtocol - let createReactionUseCase: CreateReactionUseCaseProtocol - let removeReactionUseCase: RemoveReactionUseCaseProtocol - let fetchRealEmojiListUseCase: FetchRealEmojiListUseCaseProtocol - let createRealEmojiUseCase: CreateRealEmojiUseCaseProtocol - let removeRealEmojiUseCase: RemoveRealEmojiUseCaseProtocol + @Injected var provider: GlobalStateProviderProtocol + @Injected var fetchReactionListUseCase: FetchReactionListUseCaseProtocol + @Injected var createReactionUseCase: CreateReactionUseCaseProtocol + @Injected var removeReactionUseCase: RemoveReactionUseCaseProtocol + @Injected var fetchRealEmojiListUseCase: FetchRealEmojiListUseCaseProtocol + @Injected var createRealEmojiUseCase: CreateRealEmojiUseCaseProtocol + @Injected var removeRealEmojiUseCase: RemoveRealEmojiUseCaseProtocol - init(initialState: State, provider: GlobalStateProviderProtocol, - fetchReactionUseCase: FetchReactionListUseCaseProtocol, - createReactionUseCase: CreateReactionUseCaseProtocol, - removeReactionUseCase: RemoveReactionUseCaseProtocol, - fetchRealEmojiListUseCase: FetchRealEmojiListUseCaseProtocol, - createRealEmojiUseCase: CreateRealEmojiUseCaseProtocol, - removeRealEmojiUseCase: RemoveRealEmojiUseCaseProtocol) { + init(initialState: State) { self.initialState = initialState - self.provider = provider - self.fetchReactionListUseCase = fetchReactionUseCase - self.createReactionUseCase = createReactionUseCase - self.removeReactionUseCase = removeReactionUseCase - self.fetchRealEmojiListUseCase = fetchRealEmojiListUseCase - self.createRealEmojiUseCase = createRealEmojiUseCase - self.removeRealEmojiUseCase = removeRealEmojiUseCase } } @@ -178,7 +165,7 @@ extension ReactionViewReactor { } extension ReactionViewReactor { - func handleSelectCell(postId: String, indexPath: IndexPath, data: RealEmojiEntity, isAdd: Bool) -> Observable { + func handleSelectCell(postId: String, indexPath: IndexPath, data: EmojiEntity, isAdd: Bool) -> Observable { let executeObservable: Observable if data.isStandard { diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/SelectableEmojiReactor.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/SelectableEmojiReactor.swift index 6cf008418..86ca57102 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/SelectableEmojiReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/SelectableEmojiReactor.swift @@ -37,18 +37,12 @@ final class SelectableEmojiReactor: Reactor { let postId: String var initialState: State - let createReactionUseCase: CreateReactionUseCaseProtocol - let createRealEmojiUseCase: CreateRealEmojiUseCaseProtocol - let fetchMyRealEmojiUseCase: FetchMyRealEmojiUseCaseProtocol + @Injected var createReactionUseCase: CreateReactionUseCaseProtocol + @Injected var createRealEmojiUseCase: CreateRealEmojiUseCaseProtocol + @Injected var fetchMyRealEmojiUseCase: FetchMyRealEmojiUseCaseProtocol - init(postId: String, - createReactionUseCase: CreateReactionUseCaseProtocol, - createRealEmojiUseCase: CreateRealEmojiUseCaseProtocol, - fetchMyRealEmojiUseCase: FetchMyRealEmojiUseCaseProtocol) { + init(postId: String) { self.postId = postId - self.createReactionUseCase = createReactionUseCase - self.createRealEmojiUseCase = createRealEmojiUseCase - self.fetchMyRealEmojiUseCase = fetchMyRealEmojiUseCase let section1 = SelectableReactionSection.Model(model: 0, items: []) let section2 = SelectableReactionSection.Model(model: 1, items: []) self.initialState = State(reactionSections: [section1, section2]) diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/TempCellReactor.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/TempCellReactor.swift index 3b1adef1c..bec8ff588 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/TempCellReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/TempCellReactor.swift @@ -24,13 +24,13 @@ final class TempCellReactor: Reactor { } struct State { - var cellData: RealEmojiEntity? + var cellData: EmojiEntity? } let initialState: State = State() - let items: RealEmojiEntity + let items: EmojiEntity - init(items: RealEmojiEntity) { + init(items: EmojiEntity) { self.items = items } } diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/PostViewController.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/PostViewController.swift index 45f9ce4dc..d420f422a 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/PostViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/PostViewController.swift @@ -19,7 +19,7 @@ final class PostViewController: BaseViewController { private var navigationView: PostNavigationView = PostNavigationView() private let collectionView: UICollectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) private let collectionViewLayout: UICollectionViewFlowLayout = UICollectionViewFlowLayout() - private let reactionViewController: ReactionViewController = ReactionDIContainer(type: .post).makeViewController(post: .init(postId: "", author: nil, commentCount: 0, emojiCount: 0, imageURL: "", content: nil, time: "")) + private let reactionViewController: ReactionViewController = ReactionViewControllerWrapper(type: .post, postListData: .empty).makeViewController() convenience init(reactor: Reactor? = nil) { self.init() @@ -54,7 +54,6 @@ final class PostViewController: BaseViewController { collectionView.rx.didEndDisplayingCell.map { $0.at.item }.distinctUntilChanged(), reactor.state.map { $0.selectedIndex }.distinctUntilChanged() ) - .debug("포스트 뷰 ZIP 옵저버블 :") .filter { $0.0 == $0.1 } .withUnretained(self) .subscribe { owner, indexPath in @@ -115,35 +114,6 @@ final class PostViewController: BaseViewController { .map { Reactor.Action.setPost($0) } .bind(to: reactor.action) .disposed(by: disposeBag) - - // 댓글 노티피케이션 딥링크 코드 - reactor.state.compactMap { $0.notificationDeepLink } - .distinctUntilChanged(at: \.postId) - .filter { $0.openComment } - .bind(with: self) { owner, deepLink in - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - let postList = reactor.initialState.originPostLists.items - if let postList = postList.first(where: { item in - switch item { - case let .main(post): - post.postId == deepLink.postId - } - }) { - switch postList { - case let .main(post): - let postCommentViewController = PostCommentDIContainer( - postId: post.postId - ).makeViewController() - - owner.presentPostCommentSheet( - postCommentViewController, - from: .post - ) - } - } - } - } - .disposed(by: disposeBag) } override func setupUI() { @@ -243,7 +213,7 @@ extension PostViewController { guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: PostDetailCollectionViewCell.id, for: indexPath) as? PostDetailCollectionViewCell else { return UICollectionViewCell() } - cell.reactor = PostDetailCellDIContainer().makeReactor(post: data) + cell.reactor = PostDetailCellWrapper(post: data).makeReactor() cell.setCell(data: data) return cell } diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/ReactionViewController.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/ReactionViewController.swift index d947a36e1..c274236d8 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/ReactionViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/ReactionViewController.swift @@ -126,7 +126,7 @@ extension ReactionViewController { .withUnretained(self) .observe(on: MainScheduler.instance) .bind(onNext: { - let vc = ReactionMemberDIContainer().makeViewController(emojiData: $0.1) + let vc = ReactionMembersViewControllerWrapper(emojiData: $0.1).makeViewController() $0.0.presentCustomSheetViewController(viewController: vc, detentHeightRatio: 0.58) }) .disposed(by: disposeBag) @@ -138,7 +138,7 @@ extension ReactionViewController { .withLatestFrom(postListData) .withUnretained(self) { ($0, $1) } .bind(onNext: { - let vc = SelectableEmojiDIContainer().makeViewController(postId: $0.1.postId, subject: self.selectedReactionSubject) + let vc = SelectableEmojiViewControllerWrapper(subject: self.selectedReactionSubject, postId: $0.1.postId).makeViewController() $0.0.presentCustomSheetViewController(viewController: vc, detentHeightRatio: 0.25) }) .disposed(by: disposeBag) diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/ReactionCollectionViewCell.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/ReactionCollectionViewCell.swift index adf146474..2c25a1b21 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/ReactionCollectionViewCell.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/ReactionCollectionViewCell.swift @@ -95,7 +95,7 @@ extension ReactionCollectionViewCell { .disposed(by: disposeBag) } - private func setCell(data: RealEmojiEntity) { + private func setCell(data: EmojiEntity) { if data.count == 0 { return } diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Wrapper/PostDetailCellWrapper.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Wrapper/PostDetailCellWrapper.swift new file mode 100644 index 000000000..121b4fa49 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Wrapper/PostDetailCellWrapper.swift @@ -0,0 +1,25 @@ +// +// PostDetailCellWrapper.swift +// App +// +// Created by 마경미 on 17.06.24. +// + +import Foundation + +import Domain + +final class PostDetailCellWrapper { + private let type: PostDetailViewReactor.CellType = .home + private let post: PostEntity + + init(post: PostEntity) { + self.post = post + } + + func makeReactor() -> PostDetailViewReactor { + return PostDetailViewReactor( + initialState: .init(type: type, post: post) + ) + } +} diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Wrapper/PostDetailViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Wrapper/PostDetailViewControllerWrapper.swift new file mode 100644 index 000000000..fdb39d305 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Wrapper/PostDetailViewControllerWrapper.swift @@ -0,0 +1,39 @@ +// +// PostDetailViewControllerWrapper.swift +// App +// +// Created by 마경미 on 17.06.24. +// + +import Foundation + +import Core + +final class PostDetailViewControllerWrapper: BaseWrapper { + typealias R = PostReactor + typealias V = PostViewController + + private let selectedIndex: Int + private let originPostLists: PostSection.Model + + init(selectedIndex: Int, originPostLists: PostSection.Model) { + self.selectedIndex = selectedIndex + self.originPostLists = originPostLists + } + + func makeViewController() -> V { + return PostViewController(reactor: makeReactor()) + } + + func makeReactor() -> R { + return PostReactor(initialState: .init(selectedIndex: selectedIndex, originPostLists: originPostLists)) + } + + var viewController: V { + makeViewController() + } + + var reactor: R { + makeReactor() + } +} diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Wrapper/ReactionMembersViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Wrapper/ReactionMembersViewControllerWrapper.swift new file mode 100644 index 000000000..e77fbbeac --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Wrapper/ReactionMembersViewControllerWrapper.swift @@ -0,0 +1,38 @@ +// +// ReactionMembersViewControllerWrapper.swift +// App +// +// Created by 마경미 on 17.06.24. +// + +import Foundation + +import Core +import Domain + +final class ReactionMembersViewControllerWrapper: BaseWrapper { + typealias R = ReactionMemberViewReactor + typealias V = ReactionMembersViewController + + private let emojiData: EmojiEntity + + init(emojiData: EmojiEntity) { + self.emojiData = emojiData + } + + func makeViewController() -> V { + return ReactionMembersViewController(reactor: makeReactor()) + } + + func makeReactor() -> R { + return ReactionMemberViewReactor(initialState: .init(emojiData: emojiData)) + } + + var viewController: V { + makeViewController() + } + + var reactor: R { + makeReactor() + } +} diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Wrapper/ReactionViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Wrapper/ReactionViewControllerWrapper.swift new file mode 100644 index 000000000..df0436c58 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Wrapper/ReactionViewControllerWrapper.swift @@ -0,0 +1,40 @@ +// +// ReactionViewControllerWrapper.swift +// App +// +// Created by 마경미 on 17.06.24. +// + +import Foundation + +import Core +import Domain + +final class ReactionViewControllerWrapper: BaseWrapper { + typealias R = ReactionViewReactor + typealias V = ReactionViewController + + private let type: ReactionType + private let postListData: PostEntity + + init(type: ReactionType, postListData: PostEntity) { + self.type = type + self.postListData = postListData + } + + func makeViewController() -> V { + return ReactionViewController(reactor: makeReactor()) + } + + func makeReactor() -> R { + return ReactionViewReactor(initialState: .init(type: type, postListData: postListData)) + } + + var viewController: V { + makeViewController() + } + + var reactor: R { + makeReactor() + } +} diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Wrapper/SelectableEmojiViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Wrapper/SelectableEmojiViewControllerWrapper.swift new file mode 100644 index 000000000..34345453d --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Wrapper/SelectableEmojiViewControllerWrapper.swift @@ -0,0 +1,41 @@ +// +// SelectableEmojiViewControllerWrapper.swift +// App +// +// Created by 마경미 on 17.06.24. +// + +import Foundation + +import Core + +import RxSwift + +final class SelectableEmojiViewControllerWrapper: BaseWrapper { + typealias R = SelectableEmojiReactor + typealias V = SelectableEmojiViewController + + private let subject: PublishSubject + private let postId: String + + init(subject: PublishSubject, postId: String) { + self.subject = subject + self.postId = postId + } + + func makeViewController() -> V { + return SelectableEmojiViewController(reactor: makeReactor(), selectedReactionSubject: subject) + } + + func makeReactor() -> R { + return SelectableEmojiReactor(postId: postId) + } + + var viewController: V { + makeViewController() + } + + var reactor: R { + makeReactor() + } +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileFeedViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileFeedViewController.swift index 78f54b770..ae4f4b13d 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileFeedViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileFeedViewController.swift @@ -113,8 +113,11 @@ final class ProfileFeedViewController: BaseViewController { if UserDefaults.standard.inviteCode != nil { container = UINavigationController(rootViewController: JoinedFamilyDIContainer().makeViewController()) } else { - container = UINavigationController(rootViewController: MainViewDIContainer().makeViewController()) + container = UINavigationController(rootViewController: MainViewControllerWrapper().makeViewController()) } sceneDelegate.window?.rootViewController = container sceneDelegate.window?.makeKeyAndVisible() diff --git a/14th-team5-iOS/Core/Sources/DIContainer/DIContainer.swift b/14th-team5-iOS/Core/Sources/DIContainer/DIContainer.swift index 0ed50d29b..97df04a8d 100644 --- a/14th-team5-iOS/Core/Sources/DIContainer/DIContainer.swift +++ b/14th-team5-iOS/Core/Sources/DIContainer/DIContainer.swift @@ -87,7 +87,6 @@ public extension Injectable { self.register(.by(type: type, key: key), resolve) } - // MARK: - Remove func remove(_ identifier: InjectIdentifier) { diff --git a/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/DataMapping/FetchReactionResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/DataMapping/FetchReactionResponseDTO.swift index ad673b6a3..46d480f39 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/DataMapping/FetchReactionResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/DataMapping/FetchReactionResponseDTO.swift @@ -25,19 +25,19 @@ struct FetchReactionResult: Codable { struct FetchReactionResponseDTO: Codable { let results: [FetchReactionResult] - func toDomain() -> [RealEmojiEntity] { + func toDomain() -> [EmojiEntity] { let myMemberId = FamilyUserDefaults.returnMyMemberId() let groupedByEmojiType = Dictionary(grouping: results, by: { $0.emojiType }) let fetchedEmojiDataArray = groupedByEmojiType.map { (emojiType, responses) in guard let minReactionIdResponse = responses.min(by: { $0.reactionId < $1.reactionId }) else { - return RealEmojiEntity(isStandard: true, isSelfSelected: false, postEmojiId: "", emojiType: .emoji1, count: 0, realEmojiId: "", realEmojiImageURL: "", memberIds: []) + return EmojiEntity(isStandard: true, isSelfSelected: false, postEmojiId: "", emojiType: .emoji1, count: 0, realEmojiId: "", realEmojiImageURL: "", memberIds: []) } let selfSelected = responses.contains { $0.memberId == myMemberId } let count = responses.count - return RealEmojiEntity( + return EmojiEntity( isStandard: true, isSelfSelected: selfSelected, postEmojiId: minReactionIdResponse.reactionId, diff --git a/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/ReactionAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/ReactionAPIWorker.swift index 85712ce0b..802be2cf3 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/ReactionAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/ReactionAPIWorker.swift @@ -39,7 +39,7 @@ extension ReactionAPIs { } extension ReactionAPIWorker { - func fetchReaction(query: Domain.FetchReactionQuery) -> RxSwift.Single<[RealEmojiEntity]?> { + func fetchReaction(query: Domain.FetchReactionQuery) -> RxSwift.Single<[EmojiEntity]?> { let query = FetchReactionRequestDTO(postId: query.postId) let spec = ReactionAPIs.fetchReactions(query).spec return request(spec: spec, headers: headers) diff --git a/14th-team5-iOS/Data/Sources/APIs/Reaction/Repository/ReactionRepository.swift b/14th-team5-iOS/Data/Sources/APIs/Reaction/Repository/ReactionRepository.swift index 53de285a2..24a3a4ed6 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Reaction/Repository/ReactionRepository.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Reaction/Repository/ReactionRepository.swift @@ -26,7 +26,7 @@ public final class ReactionRepository: ReactionRepositoryProtocol { return reactionAPIWorker.removeReaction(query: query, body: body) } - public func fetchReaction(query: Domain.FetchReactionQuery) -> RxSwift.Single<[Domain.RealEmojiEntity]?> { + public func fetchReaction(query: Domain.FetchReactionQuery) -> RxSwift.Single<[Domain.EmojiEntity]?> { return reactionAPIWorker.fetchReaction(query: query) } } diff --git a/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/DataMapping/FetchRealEmojiListResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/DataMapping/FetchRealEmojiListResponseDTO.swift index 7c54d0a32..0846e3f02 100644 --- a/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/DataMapping/FetchRealEmojiListResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/DataMapping/FetchRealEmojiListResponseDTO.swift @@ -22,19 +22,19 @@ struct RealEmojiListResult: Codable { struct FetchRealEmojiListResponseDTO: Codable { let results: [RealEmojiListResult] - func toDomain() -> [RealEmojiEntity]? { + func toDomain() -> [EmojiEntity]? { let myMemberId = FamilyUserDefaults.returnMyMemberId() let groupedByEmojiType = Dictionary(grouping: results, by: { $0.realEmojiId }) let fetchedEmojiDataArray = groupedByEmojiType.map { (emojiType, responses) in guard let minReactionIdResponse = responses.min(by: { $0.postRealEmojiId < $1.postRealEmojiId }) else { - return RealEmojiEntity(isStandard: false, isSelfSelected: false, postEmojiId: "", emojiType: .emoji1, count: 0, realEmojiId: "", realEmojiImageURL: "", memberIds: []) + return EmojiEntity(isStandard: false, isSelfSelected: false, postEmojiId: "", emojiType: .emoji1, count: 0, realEmojiId: "", realEmojiImageURL: "", memberIds: []) } let selfSelected = responses.contains { $0.memberId == myMemberId } let count = responses.count - return RealEmojiEntity( + return EmojiEntity( isStandard: false, isSelfSelected: selfSelected, postEmojiId: minReactionIdResponse.postRealEmojiId, diff --git a/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/RealEmojiAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/RealEmojiAPIWorker.swift index f1bfd955f..ae20c5367 100644 --- a/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/RealEmojiAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/RealEmojiAPIWorker.swift @@ -41,7 +41,7 @@ extension RealEmojiAPIs { extension RealEmojiAPIWorker { - func fetchRealEmoji(query: FetchRealEmojiQuery) -> Single<[RealEmojiEntity]?> { + func fetchRealEmoji(query: FetchRealEmojiQuery) -> Single<[EmojiEntity]?> { let query = FetchRealEmojiListParameter(postId: query.postId) let spec = RealEmojiAPIs.fetchRealEmojiList(query).spec diff --git a/14th-team5-iOS/Data/Sources/APIs/RealEmoji/Repository/RealEmojiRepository.swift b/14th-team5-iOS/Data/Sources/APIs/RealEmoji/Repository/RealEmojiRepository.swift index 0ed3191c3..951e85b7a 100644 --- a/14th-team5-iOS/Data/Sources/APIs/RealEmoji/Repository/RealEmojiRepository.swift +++ b/14th-team5-iOS/Data/Sources/APIs/RealEmoji/Repository/RealEmojiRepository.swift @@ -26,7 +26,7 @@ public final class RealEmojiRepository: RealEmojiRepositoryProtocol { realEmojiAPIWorker.addRealEmoji(query: query, body: body) } - public func fetchRealEmoji(query: Domain.FetchRealEmojiQuery) -> RxSwift.Single<[Domain.RealEmojiEntity]?> { + public func fetchRealEmoji(query: Domain.FetchRealEmojiQuery) -> RxSwift.Single<[Domain.EmojiEntity]?> { realEmojiAPIWorker.fetchRealEmoji(query: query) } diff --git a/14th-team5-iOS/Domain/Sources/Entities/PostList/PostEntity.swift b/14th-team5-iOS/Domain/Sources/Entities/PostList/PostEntity.swift index 76d3b1dee..695fed8e7 100644 --- a/14th-team5-iOS/Domain/Sources/Entities/PostList/PostEntity.swift +++ b/14th-team5-iOS/Domain/Sources/Entities/PostList/PostEntity.swift @@ -29,6 +29,10 @@ public struct PostEntity: Equatable, Hashable { self.content = content self.time = time } + + public static var empty: PostEntity { + return .init(postId: "", author: nil, commentCount: 0, emojiCount: 0, imageURL: "", content: nil, time: "") + } } public struct PostListPageEntity: Equatable { diff --git a/14th-team5-iOS/Domain/Sources/Entities/RealEmoji/RealEmojiEntity.swift b/14th-team5-iOS/Domain/Sources/Entities/RealEmoji/EmojiEntity.swift similarity index 96% rename from 14th-team5-iOS/Domain/Sources/Entities/RealEmoji/RealEmojiEntity.swift rename to 14th-team5-iOS/Domain/Sources/Entities/RealEmoji/EmojiEntity.swift index 154dd8dd5..83ea0c59a 100644 --- a/14th-team5-iOS/Domain/Sources/Entities/RealEmoji/RealEmojiEntity.swift +++ b/14th-team5-iOS/Domain/Sources/Entities/RealEmoji/EmojiEntity.swift @@ -8,7 +8,7 @@ import Foundation import Core -public struct RealEmojiEntity { +public struct EmojiEntity { public let isStandard: Bool public var isSelfSelected: Bool public let postEmojiId: String diff --git a/14th-team5-iOS/Domain/Sources/Repositories/ReactionRepositoryProtocol.swift b/14th-team5-iOS/Domain/Sources/Repositories/ReactionRepositoryProtocol.swift index 718260087..069b61e82 100644 --- a/14th-team5-iOS/Domain/Sources/Repositories/ReactionRepositoryProtocol.swift +++ b/14th-team5-iOS/Domain/Sources/Repositories/ReactionRepositoryProtocol.swift @@ -12,5 +12,5 @@ import RxSwift public protocol ReactionRepositoryProtocol { func addReaction(query: CreateReactionQuery, body: CreateReactionRequest) -> Single func removeReaction(query: RemoveReactionQuery, body: RemoveReactionRequest) -> Single - func fetchReaction(query: FetchReactionQuery) -> Single<[RealEmojiEntity]?> + func fetchReaction(query: FetchReactionQuery) -> Single<[EmojiEntity]?> } diff --git a/14th-team5-iOS/Domain/Sources/Repositories/RealEmojiRepositoryProtocol.swift b/14th-team5-iOS/Domain/Sources/Repositories/RealEmojiRepositoryProtocol.swift index 17bb12662..247e3a2b9 100644 --- a/14th-team5-iOS/Domain/Sources/Repositories/RealEmojiRepositoryProtocol.swift +++ b/14th-team5-iOS/Domain/Sources/Repositories/RealEmojiRepositoryProtocol.swift @@ -12,6 +12,6 @@ import RxSwift public protocol RealEmojiRepositoryProtocol { func fetchMyRealEmoji() -> Single<[MyRealEmojiEntity?]> func addRealEmoji(query: CreateReactionQuery, body: CreateReactionRequest) -> Single - func fetchRealEmoji(query: FetchRealEmojiQuery) -> Single<[RealEmojiEntity]?> + func fetchRealEmoji(query: FetchRealEmojiQuery) -> Single<[EmojiEntity]?> func removeRealEmoji(query: RemoveRealEmojiQuery) -> Single } diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Reaction/FetchReactionListUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Reaction/FetchReactionListUseCase.swift index 0d4bbb683..cf6370b79 100644 --- a/14th-team5-iOS/Domain/Sources/UseCases/Reaction/FetchReactionListUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/Reaction/FetchReactionListUseCase.swift @@ -10,7 +10,7 @@ import Foundation import RxSwift public protocol FetchReactionListUseCaseProtocol { - func execute(query: FetchReactionQuery) -> Single<[RealEmojiEntity]?> + func execute(query: FetchReactionQuery) -> Single<[EmojiEntity]?> } public final class FetchReactionListUseCase: FetchReactionListUseCaseProtocol { @@ -20,7 +20,7 @@ public final class FetchReactionListUseCase: FetchReactionListUseCaseProtocol { self.reactionRepository = reactionRepository } - public func execute(query: FetchReactionQuery) -> Single<[RealEmojiEntity]?> { + public func execute(query: FetchReactionQuery) -> Single<[EmojiEntity]?> { return reactionRepository.fetchReaction(query: query) } } diff --git a/14th-team5-iOS/Domain/Sources/UseCases/RealEmoji/FetchRealEmojiListUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/RealEmoji/FetchRealEmojiListUseCase.swift index 932a52eb1..7b7549ecf 100644 --- a/14th-team5-iOS/Domain/Sources/UseCases/RealEmoji/FetchRealEmojiListUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/RealEmoji/FetchRealEmojiListUseCase.swift @@ -10,7 +10,7 @@ import Foundation import RxSwift public protocol FetchRealEmojiListUseCaseProtocol { - func execute(query: FetchRealEmojiQuery) -> Single<[RealEmojiEntity]?> + func execute(query: FetchRealEmojiQuery) -> Single<[EmojiEntity]?> } public class FetchRealEmojiListUseCase: FetchRealEmojiListUseCaseProtocol { @@ -21,7 +21,7 @@ public class FetchRealEmojiListUseCase: FetchRealEmojiListUseCaseProtocol { self.realEmojiRepository = realEmojiRepository } - public func execute(query: FetchRealEmojiQuery) -> Single<[RealEmojiEntity]?> { + public func execute(query: FetchRealEmojiQuery) -> Single<[EmojiEntity]?> { return realEmojiRepository.fetchRealEmoji(query: query) } } diff --git a/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme b/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme index defe8e429..5f307e6b4 100644 --- a/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme +++ b/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme @@ -356,20 +356,6 @@ ReferencedContainer = "container:Tuist/Dependencies/SwiftPackageManager/.build/checkouts/GoogleDataTransport/GoogleDataTransport.xcodeproj"> - - - - - - - - - - - - - - - - - - - - Date: Fri, 21 Jun 2024 01:52:08 +0900 Subject: [PATCH 130/263] [feat]: wrapper and dependency folder(#565) --- .../DIContainer}/MainFamilyViewDIContainer.swift | 0 .../DIContainer}/MainPostViewDIContainer.swift | 0 .../DIContainer}/MainViewDIContainer.swift | 0 .../DIContainer}/PostDetailCellDIContainer.swift | 0 .../DIContainer}/PostListsDIContainer.swift | 0 .../DIContainer}/ReactionDIContainer.swift | 0 .../DIContainer}/ReactionMemberDIContainer.swift | 0 .../DIContainer}/SelectableEmojiDIContainer.swift | 0 .../Wrapper/Main}/MainFamilyViewControllerWrapper.swift | 0 .../Wrapper/Main}/MainPostViewControllerWrapper.swift | 0 .../Wrapper/Main}/MainViewControllerWrapper.swift | 0 .../Wrapper/PostDetail}/PostDetailCellWrapper.swift | 0 .../Wrapper/PostDetail}/PostDetailViewControllerWrapper.swift | 0 .../PostDetail}/ReactionMembersViewControllerWrapper.swift | 0 .../Wrapper/PostDetail}/ReactionViewControllerWrapper.swift | 0 .../PostDetail}/SelectableEmojiViewControllerWrapper.swift | 0 16 files changed, 0 insertions(+), 0 deletions(-) rename 14th-team5-iOS/App/Sources/{Presentation/Home/Dependency => Application/DIContainer}/MainFamilyViewDIContainer.swift (100%) rename 14th-team5-iOS/App/Sources/{Presentation/Home/Dependency => Application/DIContainer}/MainPostViewDIContainer.swift (100%) rename 14th-team5-iOS/App/Sources/{Presentation/Home/Dependency => Application/DIContainer}/MainViewDIContainer.swift (100%) rename 14th-team5-iOS/App/Sources/{Presentation/PostDetail/Dependency => Application/DIContainer}/PostDetailCellDIContainer.swift (100%) rename 14th-team5-iOS/App/Sources/{Presentation/PostDetail/Dependency => Application/DIContainer}/PostListsDIContainer.swift (100%) rename 14th-team5-iOS/App/Sources/{Presentation/PostDetail/Dependency => Application/DIContainer}/ReactionDIContainer.swift (100%) rename 14th-team5-iOS/App/Sources/{Presentation/PostDetail/Dependency => Application/DIContainer}/ReactionMemberDIContainer.swift (100%) rename 14th-team5-iOS/App/Sources/{Presentation/PostDetail/Dependency => Application/DIContainer}/SelectableEmojiDIContainer.swift (100%) rename 14th-team5-iOS/App/Sources/{Presentation/Home/Wrapper => Application/Wrapper/Main}/MainFamilyViewControllerWrapper.swift (100%) rename 14th-team5-iOS/App/Sources/{Presentation/Home/Wrapper => Application/Wrapper/Main}/MainPostViewControllerWrapper.swift (100%) rename 14th-team5-iOS/App/Sources/{Presentation/Home/Wrapper => Application/Wrapper/Main}/MainViewControllerWrapper.swift (100%) rename 14th-team5-iOS/App/Sources/{Presentation/PostDetail/Wrapper => Application/Wrapper/PostDetail}/PostDetailCellWrapper.swift (100%) rename 14th-team5-iOS/App/Sources/{Presentation/PostDetail/Wrapper => Application/Wrapper/PostDetail}/PostDetailViewControllerWrapper.swift (100%) rename 14th-team5-iOS/App/Sources/{Presentation/PostDetail/Wrapper => Application/Wrapper/PostDetail}/ReactionMembersViewControllerWrapper.swift (100%) rename 14th-team5-iOS/App/Sources/{Presentation/PostDetail/Wrapper => Application/Wrapper/PostDetail}/ReactionViewControllerWrapper.swift (100%) rename 14th-team5-iOS/App/Sources/{Presentation/PostDetail/Wrapper => Application/Wrapper/PostDetail}/SelectableEmojiViewControllerWrapper.swift (100%) diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Dependency/MainFamilyViewDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/MainFamilyViewDIContainer.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Home/Dependency/MainFamilyViewDIContainer.swift rename to 14th-team5-iOS/App/Sources/Application/DIContainer/MainFamilyViewDIContainer.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Dependency/MainPostViewDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/MainPostViewDIContainer.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Home/Dependency/MainPostViewDIContainer.swift rename to 14th-team5-iOS/App/Sources/Application/DIContainer/MainPostViewDIContainer.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Dependency/MainViewDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/MainViewDIContainer.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Home/Dependency/MainViewDIContainer.swift rename to 14th-team5-iOS/App/Sources/Application/DIContainer/MainViewDIContainer.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/PostDetailCellDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/PostDetailCellDIContainer.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/PostDetailCellDIContainer.swift rename to 14th-team5-iOS/App/Sources/Application/DIContainer/PostDetailCellDIContainer.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/PostListsDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/PostListsDIContainer.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/PostListsDIContainer.swift rename to 14th-team5-iOS/App/Sources/Application/DIContainer/PostListsDIContainer.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/ReactionDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/ReactionDIContainer.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/ReactionDIContainer.swift rename to 14th-team5-iOS/App/Sources/Application/DIContainer/ReactionDIContainer.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/ReactionMemberDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/ReactionMemberDIContainer.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/ReactionMemberDIContainer.swift rename to 14th-team5-iOS/App/Sources/Application/DIContainer/ReactionMemberDIContainer.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/SelectableEmojiDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/SelectableEmojiDIContainer.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/PostDetail/Dependency/SelectableEmojiDIContainer.swift rename to 14th-team5-iOS/App/Sources/Application/DIContainer/SelectableEmojiDIContainer.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Wrapper/MainFamilyViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Wrapper/Main/MainFamilyViewControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Home/Wrapper/MainFamilyViewControllerWrapper.swift rename to 14th-team5-iOS/App/Sources/Application/Wrapper/Main/MainFamilyViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Wrapper/MainPostViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Wrapper/Main/MainPostViewControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Home/Wrapper/MainPostViewControllerWrapper.swift rename to 14th-team5-iOS/App/Sources/Application/Wrapper/Main/MainPostViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Wrapper/MainViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Wrapper/Main/MainViewControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Home/Wrapper/MainViewControllerWrapper.swift rename to 14th-team5-iOS/App/Sources/Application/Wrapper/Main/MainViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Wrapper/PostDetailCellWrapper.swift b/14th-team5-iOS/App/Sources/Application/Wrapper/PostDetail/PostDetailCellWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/PostDetail/Wrapper/PostDetailCellWrapper.swift rename to 14th-team5-iOS/App/Sources/Application/Wrapper/PostDetail/PostDetailCellWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Wrapper/PostDetailViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Wrapper/PostDetail/PostDetailViewControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/PostDetail/Wrapper/PostDetailViewControllerWrapper.swift rename to 14th-team5-iOS/App/Sources/Application/Wrapper/PostDetail/PostDetailViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Wrapper/ReactionMembersViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Wrapper/PostDetail/ReactionMembersViewControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/PostDetail/Wrapper/ReactionMembersViewControllerWrapper.swift rename to 14th-team5-iOS/App/Sources/Application/Wrapper/PostDetail/ReactionMembersViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Wrapper/ReactionViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Wrapper/PostDetail/ReactionViewControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/PostDetail/Wrapper/ReactionViewControllerWrapper.swift rename to 14th-team5-iOS/App/Sources/Application/Wrapper/PostDetail/ReactionViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Wrapper/SelectableEmojiViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Wrapper/PostDetail/SelectableEmojiViewControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/PostDetail/Wrapper/SelectableEmojiViewControllerWrapper.swift rename to 14th-team5-iOS/App/Sources/Application/Wrapper/PostDetail/SelectableEmojiViewControllerWrapper.swift From c43563b2d24a6145518e04a02f57fe0c545a86c8 Mon Sep 17 00:00:00 2001 From: Kyoungmi <1mmgm.dev@gmail.com> Date: Fri, 21 Jun 2024 19:39:37 +0900 Subject: [PATCH 131/263] feat: dicontainer(#565) --- .../App/Sources/Application/AppDelegate.swift | 15 +++--- .../DIContainer/FamilyDIContainer.swift | 53 +++++++------------ .../MainFamilyViewDIContainer.swift | 31 ----------- .../DIContainer/MainPostViewDIContainer.swift | 31 ----------- .../DIContainer/MainViewDIContainer.swift | 32 ++--------- .../DIContainer/MemberDIContainer.swift | 26 +++++++++ .../DIContainer/MissionDIContainer.swift | 33 ++++++++++++ .../DIContainer/PostDIContainer.swift | 28 ++++++++++ .../PostDetailCellDIContainer.swift | 32 ----------- .../DIContainer/PostListsDIContainer.swift | 31 ----------- .../DIContainer/ReactionDIContainer.swift | 38 ++----------- .../ReactionMemberDIContainer.swift | 32 ----------- .../DIContainer/RealEmojiDIContainer.swift | 44 +++++++++++++++ .../SelectableEmojiDIContainer.swift | 52 ------------------ 14 files changed, 163 insertions(+), 315 deletions(-) delete mode 100644 14th-team5-iOS/App/Sources/Application/DIContainer/MainFamilyViewDIContainer.swift delete mode 100644 14th-team5-iOS/App/Sources/Application/DIContainer/MainPostViewDIContainer.swift create mode 100644 14th-team5-iOS/App/Sources/Application/DIContainer/MemberDIContainer.swift create mode 100644 14th-team5-iOS/App/Sources/Application/DIContainer/MissionDIContainer.swift create mode 100644 14th-team5-iOS/App/Sources/Application/DIContainer/PostDIContainer.swift delete mode 100644 14th-team5-iOS/App/Sources/Application/DIContainer/PostDetailCellDIContainer.swift delete mode 100644 14th-team5-iOS/App/Sources/Application/DIContainer/PostListsDIContainer.swift delete mode 100644 14th-team5-iOS/App/Sources/Application/DIContainer/ReactionMemberDIContainer.swift create mode 100644 14th-team5-iOS/App/Sources/Application/DIContainer/RealEmojiDIContainer.swift delete mode 100644 14th-team5-iOS/App/Sources/Application/DIContainer/SelectableEmojiDIContainer.swift diff --git a/14th-team5-iOS/App/Sources/Application/AppDelegate.swift b/14th-team5-iOS/App/Sources/Application/AppDelegate.swift index 2f8f4b1c0..95e2a8bb9 100644 --- a/14th-team5-iOS/App/Sources/Application/AppDelegate.swift +++ b/14th-team5-iOS/App/Sources/Application/AppDelegate.swift @@ -57,17 +57,16 @@ class AppDelegate: UIResponder, UIApplicationDelegate { CalendarDIContainer(), CommentDIContainer(), FamilyDIContainer(), - PickDIContainer(), OAuthDIContainer(), - SignInDIContainer() - MainFamilyViewDIContainer(), - MainPostViewDIContainer(), + SignInDIContainer(), + FamilyDIContainer(), + PostDIContainer(), MainViewDIContainer(), - PostDetailCellDIContainer(), - PostListsDIContainer(), ReactionDIContainer(), - ReactionMemberDIContainer(), - SelectableEmojiDIContainer() + RealEmojiDIContainer(), + PickDIContainer(), + MissionDIContainer(), + MemberDIContainer() ] containers.forEach { $0.registerDependencies() diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/FamilyDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/FamilyDIContainer.swift index 5bd206471..4b2265e25 100644 --- a/14th-team5-iOS/App/Sources/Application/DIContainer/FamilyDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/FamilyDIContainer.swift @@ -10,66 +10,46 @@ import Data import Domain final class FamilyDIContainer: BaseContainer { - - // MARK: - Make UseCase + + private let repository: FamilyRepositoryProtocol = FamilyRepository() private func makeCreateFamilyUseCase() -> CreateFamilyUseCaseProtocol { - CreateFamilyUseCase( - familyRepository: makeFamilyRepository() - ) + CreateFamilyUseCase(familyRepository: repository) } private func makeFetchFamilyCreatedAtUseCase() -> FetchFamilyCreatedAtUseCaseProtocol { - FetchFamilyCreatedAtUseCase( - familyRepository: makeFamilyRepository() - ) + FetchFamilyCreatedAtUseCase(familyRepository: repository) } private func makeFetchFamilyMemberUseCase() -> FetchFamilyMembersUseCaseProtocol { - FetchFamilyMembersUseCase( - familyRepository: makeFamilyRepository() - ) + FetchFamilyMembersUseCase(familyRepository: repository) } private func makeFetchFamilyMemberFromStorageUseCase() -> FetchFamilyMembersUseCaseFromStorageProtocol { - FetchFamilyMembersFromStoragUseCase( - familyRepository: makeFamilyRepository() - ) + FetchFamilyMembersFromStoragUseCase(familyRepository: repository) } private func makeFetchInvitationLinkUseCase() -> FetchInvitationLinkUseCaseProtocol { - FetchInvitationUrlUseCase( - familyRepository: makeFamilyRepository() - ) + FetchInvitationUrlUseCase(familyRepository: repository) } private func makeJoinFamilyUseCase() -> JoinFamilyUseCaseProtocol { - JoinFamilyUseCase( - familyRepository: makeFamilyRepository() - ) + JoinFamilyUseCase(familyRepository: repository) } private func makeResignFamilyUseCase() -> ResignFamilyUseCaseProtocol { - ResignFamilyUseCase( - familyRepository: makeFamilyRepository() - ) + ResignFamilyUseCase(familyRepository: repository) } - // Deprecated - private func makeFamilyUseCase() -> FamilyUseCaseProtocol { - FamilyUseCase( - familyRepository: makeFamilyRepository() - ) + private func makeInviteFamilyUseCase() -> FamilyUseCaseProtocol { + return FamilyUseCase(familyRepository: repository) } - - // MARK: - Make Repository - - private func makeFamilyRepository() -> FamilyRepositoryProtocol { - return FamilyRepository() + // Deprecated + private func makeFamilyUseCase() -> FamilyUseCaseProtocol { + FamilyUseCase(familyRepository: repository) } - // MARK: - Register func registerDependencies() { @@ -101,10 +81,13 @@ final class FamilyDIContainer: BaseContainer { makeResignFamilyUseCase() } + container.register(type: FamilyUseCaseProtocol.self) { _ in + self.makeInviteFamilyUseCase() + } + // Deprecated container.register(type: FamilyUseCaseProtocol.self) { _ in makeFamilyUseCase() } } - } diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/MainFamilyViewDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/MainFamilyViewDIContainer.swift deleted file mode 100644 index f5aa35c1f..000000000 --- a/14th-team5-iOS/App/Sources/Application/DIContainer/MainFamilyViewDIContainer.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// MainFamilyDIContainer.swift -// App -// -// Created by 마경미 on 21.04.24. -// - -import UIKit -import Foundation - -import Core -import Data -import Domain - -final class MainFamilyViewDIContainer: BaseContainer { - private func makeInviteFamilyRepository() -> FamilyRepositoryProtocol { - return FamilyRepository() - } - - private func makeInviteFamilyUseCase() -> FamilyUseCaseProtocol { - return FamilyUseCase(familyRepository: makeInviteFamilyRepository()) - } -} - -extension MainFamilyViewDIContainer { - func registerDependencies() { - container.register(type: FamilyUseCaseProtocol.self) { _ in - self.makeInviteFamilyUseCase() - } - } -} diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/MainPostViewDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/MainPostViewDIContainer.swift deleted file mode 100644 index ca151b6af..000000000 --- a/14th-team5-iOS/App/Sources/Application/DIContainer/MainPostViewDIContainer.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// SurvivalDIContainer.swift -// App -// -// Created by 마경미 on 21.04.24. -// - -import UIKit - -import Core -import Data -import Domain - - -final class MainPostViewDIContainer: BaseContainer { - private func makePostRepository() -> PostListRepositoryProtocol { - return PostRepository() - } - - private func makePostUseCase() -> FetchPostListUseCaseProtocol { - return FetchPostListUseCase(postListRepository: makePostRepository()) - } -} - -extension MainPostViewDIContainer { - func registerDependencies() { - container.register(type: FetchPostListUseCaseProtocol.self) { _ in - self.makePostUseCase() - } - } -} diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/MainViewDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/MainViewDIContainer.swift index e0a0f95d0..007c9e487 100644 --- a/14th-team5-iOS/App/Sources/Application/DIContainer/MainViewDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/MainViewDIContainer.swift @@ -13,41 +13,19 @@ import Core final class MainViewDIContainer: BaseContainer { - private func makePickReposiotry() -> PickRepositoryProtocol { - return PickRepository() - } - - private func makePickUseCase() -> PickUseCaseProtocol { - return PickUseCase(pickRepository: makePickReposiotry()) - } - - private func makeMainRepository() -> MainViewRepository { - return MainViewRepository() - } - - private func makeMissionRepository() -> MissionRepositoryProtocol { - return MissionRepository() - } + private let repository: MainViewRepositoryProtocol = MainViewRepository() private func makeFetchMainUseCase() -> FetchMainUseCaseProtocol { - return FetchMainUseCase(mainRepository: makeMainRepository()) + return FetchMainUseCase(mainRepository: repository) } private func makeFetchMainNightUseCase() -> FetchNightMainViewUseCaseProtocol { - return FetchNightMainViewUseCase(mainRepository: makeMainRepository()) - } - - private func makeCheckMissionAlertShowUseCase() -> CheckMissionAlertShowUseCaseProtocol { - return CheckMissionAlertShowUseCase(missionRepository: makeMissionRepository()) + return FetchNightMainViewUseCase(mainRepository: repository) } } extension MainViewDIContainer { func registerDependencies() { - container.register(type: PickUseCaseProtocol.self) { _ in - self.makePickUseCase() - } - container.register(type: FetchMainUseCaseProtocol.self) { _ in self.makeFetchMainUseCase() } @@ -55,9 +33,5 @@ extension MainViewDIContainer { container.register(type: FetchNightMainViewUseCaseProtocol.self) { _ in self.makeFetchMainNightUseCase() } - - container.register(type: CheckMissionAlertShowUseCaseProtocol.self) {_ in - self.makeCheckMissionAlertShowUseCase() - } } } diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/MemberDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/MemberDIContainer.swift new file mode 100644 index 000000000..1f6958a00 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/MemberDIContainer.swift @@ -0,0 +1,26 @@ +// +// MemberDIContainer.swift +// App +// +// Created by 마경미 on 21.06.24. +// + +import Core +import Data +import Domain + +final class MemberDIContainer: BaseContainer { + private let repository: MemberRepositoryProtocol = MemberRepository() + + private func makeMemberUseCase() -> MemberUseCaseProtocol { + return MemberUseCase(memberRepository: repository) + } +} + +extension MemberDIContainer { + func registerDependencies() { + container.register(type: MemberUseCaseProtocol.self) { _ in + self.makeMemberUseCase() + } + } +} diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/MissionDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/MissionDIContainer.swift new file mode 100644 index 000000000..72e12a7b0 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/MissionDIContainer.swift @@ -0,0 +1,33 @@ +// +// MissionDIContainer.swift +// App +// +// Created by 마경미 on 21.06.24. +// + +import Core +import Data +import Domain + +final class MissionDIContainer: BaseContainer { + private let repository: MissionRepositoryProtocol = MissionRepository() + + func makeMissionUseCase() -> FetchMissionContentUseCaseProtocol { + return FetchMissionContentUseCase(missionRepository: repository) + } + + private func makeCheckMissionAlertShowUseCase() -> CheckMissionAlertShowUseCaseProtocol { + return CheckMissionAlertShowUseCase(missionRepository: repository) + } + + func registerDependencies() { + container.register(type: FetchMissionContentUseCaseProtocol.self) { _ in + self.makeMissionUseCase() + } + + container.register(type: CheckMissionAlertShowUseCaseProtocol.self) { _ in + self.makeCheckMissionAlertShowUseCase() + } + } +} + diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/PostDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/PostDIContainer.swift new file mode 100644 index 000000000..540a3f0bb --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/PostDIContainer.swift @@ -0,0 +1,28 @@ +// +// PostDIContainer.swift +// App +// +// Created by 마경미 on 21.06.24. +// + +import Core +import Data +import Domain + + +final class PostDIContainer: BaseContainer { + private let repository: PostListRepositoryProtocol = PostRepository() + + private func makePostUseCase() -> FetchPostListUseCaseProtocol { + return FetchPostListUseCase(postListRepository: repository) + } +} + +extension PostDIContainer { + func registerDependencies() { + container.register(type: FetchPostListUseCaseProtocol.self) { _ in + self.makePostUseCase() + } + } +} + diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/PostDetailCellDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/PostDetailCellDIContainer.swift deleted file mode 100644 index 7a106f125..000000000 --- a/14th-team5-iOS/App/Sources/Application/DIContainer/PostDetailCellDIContainer.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// PostDetailViewDIContainer.swift -// App -// -// Created by 마경미 on 13.02.24. -// - -import UIKit - -import Core -import Data -import Domain - -import RxDataSources - -final class PostDetailCellDIContainer: BaseContainer { - private func makeMemberRepository() -> MemberRepositoryProtocol { - return MemberRepository() - } - - private func makeMemberUseCase() -> MemberUseCaseProtocol { - return MemberUseCase(memberRepository: makeMemberRepository()) - } -} - -extension PostDetailCellDIContainer { - func registerDependencies() { - container.register(type: MemberUseCaseProtocol.self) { _ in - self.makeMemberUseCase() - } - } -} diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/PostListsDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/PostListsDIContainer.swift deleted file mode 100644 index 4cd206f16..000000000 --- a/14th-team5-iOS/App/Sources/Application/DIContainer/PostListsDIContainer.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// PostListsDIContainer.swift -// App -// -// Created by 마경미 on 30.12.23. -// - -import UIKit - -import Core -import Data -import Domain - -import RxDataSources - -final class PostListsDIContainer: BaseContainer { - - private func makeMissionRepository() -> MissionRepositoryProtocol { - return MissionRepository() - } - - func makeMissionUseCase() -> FetchMissionContentUseCaseProtocol { - return FetchMissionContentUseCase(missionRepository: makeMissionRepository()) - } - - func registerDependencies() { - container.register(type: FetchMissionContentUseCaseProtocol.self) { _ in - self.makeMissionUseCase() - } - } -} diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/ReactionDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/ReactionDIContainer.swift index 4683db913..392881ba8 100644 --- a/14th-team5-iOS/App/Sources/Application/DIContainer/ReactionDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/ReactionDIContainer.swift @@ -16,53 +16,23 @@ enum ReactionType { } final class ReactionDIContainer: BaseContainer { - private func makeRealEmojiRepository() -> RealEmojiRepositoryProtocol { - return RealEmojiRepository() - } - - private func makeCreateRealEmojiUseCase() -> CreateRealEmojiUseCaseProtocol { - return CreateRealEmojiUseCase(realEmojiRepository: makeRealEmojiRepository()) - } - - private func makeRemoveRealEmojiUseCase() -> RemoveRealEmojiUseCaseProtocol { - return RemoveRealEmojiUseCase(realEmojiRepository: makeRealEmojiRepository()) - } - - private func makeFetchRealEmojiListUseCase() -> FetchRealEmojiListUseCaseProtocol { - return FetchRealEmojiListUseCase(realEmojiRepository: makeRealEmojiRepository()) - } - - private func makeReactionRepository() -> ReactionRepositoryProtocol { - return ReactionRepository() - } + private let repository: ReactionRepositoryProtocol = ReactionRepository() private func makeCreateReactionUseCase() -> CreateReactionUseCaseProtocol { - return CreateReactionUseCase(reactionRepository: makeReactionRepository()) + return CreateReactionUseCase(reactionRepository: repository) } private func makeRemoveReactionUseCase() -> RemoveReactionUseCaseProtocol { - return RemoveReactionUseCase(reactionRepository: makeReactionRepository()) + return RemoveReactionUseCase(reactionRepository: repository) } private func makeFetchReactionListUseCase() -> FetchReactionListUseCaseProtocol { - return FetchReactionListUseCase(reactionRepository: makeReactionRepository()) + return FetchReactionListUseCase(reactionRepository: repository) } } extension ReactionDIContainer { func registerDependencies() { - container.register(type: CreateRealEmojiUseCaseProtocol.self) { _ in - self.makeCreateRealEmojiUseCase() - } - - container.register(type: RemoveRealEmojiUseCaseProtocol.self) { _ in - self.makeRemoveRealEmojiUseCase() - } - - container.register(type: FetchRealEmojiListUseCaseProtocol.self) { _ in - self.makeFetchRealEmojiListUseCase() - } - container.register(type: CreateReactionUseCaseProtocol.self) { _ in self.makeCreateReactionUseCase() } diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/ReactionMemberDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/ReactionMemberDIContainer.swift deleted file mode 100644 index 163550f07..000000000 --- a/14th-team5-iOS/App/Sources/Application/DIContainer/ReactionMemberDIContainer.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// ReactionDIContainer.swift -// App -// -// Created by 마경미 on 07.01.24. -// - -import UIKit - -import Core -import Data -import Domain - -import RxDataSources - -final class ReactionMemberDIContainer: BaseContainer { - private func makeFamilyRepository() -> FamilyRepositoryProtocol { - return FamilyRepository() - } - - private func makeFamilyUseCase() -> FamilyUseCaseProtocol { - return FamilyUseCase(familyRepository: makeFamilyRepository()) - } -} - -extension ReactionMemberDIContainer { - func registerDependencies() { - container.register(type: FamilyUseCaseProtocol.self) { _ in - self.makeFamilyUseCase() - } - } -} diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/RealEmojiDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/RealEmojiDIContainer.swift new file mode 100644 index 000000000..92c448c33 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/RealEmojiDIContainer.swift @@ -0,0 +1,44 @@ +// +// RealEmojiDIContainer.swift +// App +// +// Created by 마경미 on 21.06.24. +// + +import Foundation + +import Core +import Data +import Domain + +final class RealEmojiDIContainer: BaseContainer { + private let repository: RealEmojiRepositoryProtocol = RealEmojiRepository() + + private func makeCreateRealEmojiUseCase() -> CreateRealEmojiUseCaseProtocol { + return CreateRealEmojiUseCase(realEmojiRepository: repository) + } + + private func makeRemoveRealEmojiUseCase() -> RemoveRealEmojiUseCaseProtocol { + return RemoveRealEmojiUseCase(realEmojiRepository: repository) + } + + private func makeFetchRealEmojiListUseCase() -> FetchRealEmojiListUseCaseProtocol { + return FetchRealEmojiListUseCase(realEmojiRepository: repository) + } +} + +extension RealEmojiDIContainer { + func registerDependencies() { + container.register(type: CreateRealEmojiUseCaseProtocol.self) { _ in + self.makeCreateRealEmojiUseCase() + } + + container.register(type: RemoveRealEmojiUseCaseProtocol.self) { _ in + self.makeRemoveRealEmojiUseCase() + } + + container.register(type: FetchRealEmojiListUseCaseProtocol.self) { _ in + self.makeFetchRealEmojiListUseCase() + } + } +} diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/SelectableEmojiDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/SelectableEmojiDIContainer.swift deleted file mode 100644 index aff2fa2ba..000000000 --- a/14th-team5-iOS/App/Sources/Application/DIContainer/SelectableEmojiDIContainer.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// ReactionsDIContainer.swift -// App -// -// Created by 마경미 on 06.01.24. -// - -import UIKit - -import Core -import Data -import Domain - -import RxSwift - -final class SelectableEmojiDIContainer: BaseContainer { - private func makeRealEmojiRepository() -> RealEmojiRepositoryProtocol { - return RealEmojiRepository() - } - - private func makeCreateRealEmojiUseCase() -> CreateRealEmojiUseCaseProtocol { - return CreateRealEmojiUseCase(realEmojiRepository: makeRealEmojiRepository()) - } - - private func makeFetchMyRealEmojiUseCase() -> FetchMyRealEmojiUseCaseProtocol { - return FetchMyRealEmojiUseCase(realEmojiRepository: makeRealEmojiRepository()) - } - - private func makeReactionRepository() -> ReactionRepositoryProtocol { - return ReactionRepository() - } - - private func makeCreateReactionUseCase() -> CreateReactionUseCaseProtocol { - return CreateReactionUseCase(reactionRepository: makeReactionRepository()) - } -} - -extension SelectableEmojiDIContainer { - func registerDependencies() { - container.register(type: CreateRealEmojiUseCaseProtocol.self) { _ in - self.makeCreateRealEmojiUseCase() - } - - container.register(type: FetchMyRealEmojiUseCaseProtocol.self) { _ in - self.makeFetchMyRealEmojiUseCase() - } - - container.register(type: CreateReactionUseCaseProtocol.self) { _ in - self.makeCreateReactionUseCase() - } - } -} From 6b37f9ebe61e32cd984efee006e8a634988e3241 Mon Sep 17 00:00:00 2001 From: Kyoungmi <1mmgm.dev@gmail.com> Date: Mon, 24 Jun 2024 00:58:10 +0900 Subject: [PATCH 132/263] rebase --- .../DataMapping/MainNightResponseDTO.swift | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/DataMapping/MainNightResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/DataMapping/MainNightResponseDTO.swift index 543277117..e67bf15a4 100644 --- a/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/DataMapping/MainNightResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/DataMapping/MainNightResponseDTO.swift @@ -14,24 +14,24 @@ struct Ranker: Codable { let name: String let survivalCount: Int - func toDomain() -> RankerData { + func toDomain() -> RankerData? { return .init(imageURL: profileImageUrl, name: name, survivalCount: survivalCount) } } struct FamilyMemberMonthlyRanking: Codable { let month: Int - let firstRanker: Ranker - let secondRanker: Ranker - let thirdRanker: Ranker + let firstRanker: Ranker? + let secondRanker: Ranker? + let thirdRanker: Ranker? let mostRecentSurvivalPostDate: String func toDomain() -> FamilyRankData { return .init(month: month, recentPostDate: mostRecentSurvivalPostDate, - firstRanker: firstRanker.toDomain(), - secondRanker: secondRanker.toDomain(), - thirdRanker: thirdRanker.toDomain() + firstRanker: firstRanker?.toDomain(), + secondRanker: secondRanker?.toDomain(), + thirdRanker: thirdRanker?.toDomain() ) } } From 57f85c167151b78a49de066cf0fb24f18bc35190 Mon Sep 17 00:00:00 2001 From: Do-hyun-Kim Date: Mon, 24 Jun 2024 01:32:57 +0900 Subject: [PATCH 133/263] =?UTF-8?q?feat:=20CameraViewControllerWrapper,=20?= =?UTF-8?q?CameraDisplayViewControllerWrapper=EC=B6=94=EA=B0=80=20-=20Came?= =?UTF-8?q?raDIContainer=20=EC=B6=94=EA=B0=80=20-=20Camera=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20UseCase=EC=97=90=20Injected=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=ED=8D=BC=ED=8B=B0=20=EB=9E=98=ED=8D=BC=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?-=20ProfileViewControllerWrapper,=20ProfileFeedPageViewWrapper,?= =?UTF-8?q?=20ProfileFeedViewControllerWrapper,=20ProfileDetailViewControl?= =?UTF-8?q?lerWrapper=20=EC=B6=94=EA=B0=80=20-=20ProfileDIContainer=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20-=20Profile=20=EA=B4=80=EB=A0=A8=20UseCase?= =?UTF-8?q?=EC=97=90=20Injected=20=ED=94=84=EB=A1=9C=ED=8D=BC=ED=8B=B0=20?= =?UTF-8?q?=EB=9E=98=ED=8D=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../App/Sources/Application/AppDelegate.swift | 4 +- .../DIContainer/CameraDIContainer.swift | 105 ++++++++++++++++++ .../DIContainer/ProfileDIContainer.swift | 57 ++++++++++ .../CameraDisplayViewControllerWrapper.swift | 53 +++++++++ .../Camera/CameraViewControllerWrapper.swift | 52 +++++++++ .../ProfileDetailViewControllerWrapper.swift | 41 +++++++ ...ProfileFeedPageViewControllerWrapper.swift | 38 +++++++ .../ProfileFeedViewControllerWrapper.swift | 43 +++++++ .../ProfileViewControllerWrapper.swift | 42 +++++++ .../AccountProfileViewController.swift | 2 +- .../DailyCalendarViewController.swift | 8 +- .../Camera/CameraViewController.swift | 4 +- .../Camera/Dependency/CameraDIContainer.swift | 68 ------------ .../Dependency/CameraDisplayDIContainer.swift | 56 ---------- .../Reactor/CameraDisplayViewReactor.swift | 17 +-- .../Camera/Reactor/CameraViewReactor.swift | 38 ++----- .../FamilyManagementViewController.swift | 4 +- .../MainFamilyViewController.swift | 2 +- .../ViewControllers/MainViewController.swift | 6 +- .../ViewControllers/PostViewController.swift | 10 +- .../Dependency/ProfileDIContainer.swift | 58 ---------- .../Dependency/ProfileDetailDIContainer.swift | 36 ------ .../Dependency/ProfileFeedDIContainer.swift | 48 -------- .../ProfileFeedPageDIContainer.swift | 39 ------- .../ProfileFeedPageViewController.swift | 4 +- .../Profile/ProfileViewController.swift | 6 +- .../Reactor/ProfileFeedPageViewReactor.swift | 5 +- .../Reactor/ProfileFeedViewReactor.swift | 4 +- .../Profile/Reactor/ProfileViewReactor.swift | 19 +--- .../UseCases/Camera/CreateCameraUseCase.swift | 2 - 30 files changed, 477 insertions(+), 394 deletions(-) create mode 100644 14th-team5-iOS/App/Sources/Application/DIContainer/CameraDIContainer.swift create mode 100644 14th-team5-iOS/App/Sources/Application/DIContainer/ProfileDIContainer.swift create mode 100644 14th-team5-iOS/App/Sources/Application/Wrapper/Camera/CameraDisplayViewControllerWrapper.swift create mode 100644 14th-team5-iOS/App/Sources/Application/Wrapper/Camera/CameraViewControllerWrapper.swift create mode 100644 14th-team5-iOS/App/Sources/Application/Wrapper/Profile/ProfileDetailViewControllerWrapper.swift create mode 100644 14th-team5-iOS/App/Sources/Application/Wrapper/Profile/ProfileFeedPageViewControllerWrapper.swift create mode 100644 14th-team5-iOS/App/Sources/Application/Wrapper/Profile/ProfileFeedViewControllerWrapper.swift create mode 100644 14th-team5-iOS/App/Sources/Application/Wrapper/Profile/ProfileViewControllerWrapper.swift delete mode 100644 14th-team5-iOS/App/Sources/Presentation/Camera/Dependency/CameraDIContainer.swift delete mode 100644 14th-team5-iOS/App/Sources/Presentation/Camera/Dependency/CameraDisplayDIContainer.swift delete mode 100644 14th-team5-iOS/App/Sources/Presentation/Profile/Dependency/ProfileDIContainer.swift delete mode 100644 14th-team5-iOS/App/Sources/Presentation/Profile/Dependency/ProfileDetailDIContainer.swift delete mode 100644 14th-team5-iOS/App/Sources/Presentation/Profile/Dependency/ProfileFeedDIContainer.swift delete mode 100644 14th-team5-iOS/App/Sources/Presentation/Profile/Dependency/ProfileFeedPageDIContainer.swift diff --git a/14th-team5-iOS/App/Sources/Application/AppDelegate.swift b/14th-team5-iOS/App/Sources/Application/AppDelegate.swift index b66095b53..360d88ac3 100644 --- a/14th-team5-iOS/App/Sources/Application/AppDelegate.swift +++ b/14th-team5-iOS/App/Sources/Application/AppDelegate.swift @@ -54,7 +54,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { let containers: [BaseContainer] = [ AppDIContainer(), - CalendarDIContainer() + CalendarDIContainer(), + CameraDIContainer(), + ProfileDIContainer() ] containers.forEach { $0.registerDependencies() diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/CameraDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/CameraDIContainer.swift new file mode 100644 index 000000000..9f6201f55 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/CameraDIContainer.swift @@ -0,0 +1,105 @@ +// +// CameraDIContainer.swift +// App +// +// Created by Kim dohyun on 12/6/23. +// + +import Foundation + +import Core +import Data +import Domain +import UIKit + +final class CameraDIContainer: BaseContainer { + + private var globalState: GlobalStateProviderProtocol { + guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { + return GlobalStateProvider() + } + return appDelegate.globalStateProvider + } + + + public func makeRepository() -> CameraRepositoryProtocol { + return CameraRepository() + } + + + private func makeCreateProfileImageUseCase() -> CreateCameraImageUseCaseProtocol { + return CreateCameraImageUseCase(cameraRepository: makeRepository()) + } + + private func makeCreateCameraUseCase() -> CreateCameraUseCaseProtocol { + return CreateCameraUseCase(cameraRepository: makeRepository()) + } + + private func makeEditCameraProfileImageUseCase() -> EditCameraProfileImageUseCaseProtocol { + return EditCameraProfileImageUseCase(cameraRepository: makeRepository()) + } + + private func makeFetchCameraUploadImageUseCase() -> FetchCameraUploadImageUseCaseProtocol { + return FetchCameraUploadImageUseCase(cameraRepository: makeRepository()) + } + + private func makeFetchCameraTodayMissionUseCase() -> FetchCameraTodayMissionUseCaseProtocol { + return FetchCameraTodayMissionUseCase(cameraRepository: makeRepository()) + } + + private func makeFetchCameraRealEmojiUpdateUseCase() -> FetchCameraRealEmojiUpdateUseCaseProtocol { + return FetchCameraRealEmojiUpdateUseCase(cameraRepostiroy: makeRepository()) + } + + private func makeFetchCameraRealEmojiUploadUseCase() -> FetchCameraRealEmojiUploadUseCaseProtocol { + return FetchCameraRealEmojiUploadUseCase(cameraRepository: makeRepository()) + } + + private func makeFetchCameraRealEmojiListUseCase() -> FetchCameraRealEmojiListUseCaseProtocol { + return FetchCameraRealEmojiListUseCase(cameraRepository: makeRepository()) + } + + private func makeFetchCameraRealEmojiUseCase() -> FetchCameraRealEmojiUseCaseProtocol { + return FetchCameraRealEmojiUseCase(cameraRepository: makeRepository()) + } + + func registerDependencies() { + container.register(type: CreateCameraImageUseCaseProtocol.self) { _ in + self.makeCreateProfileImageUseCase() + } + + container.register(type: CreateCameraUseCaseProtocol.self) { _ in + self.makeCreateCameraUseCase() + } + + container.register(type: EditCameraProfileImageUseCaseProtocol.self) { _ in + self.makeEditCameraProfileImageUseCase() + } + + container.register(type: FetchCameraUploadImageUseCaseProtocol.self) { _ in + self.makeFetchCameraUploadImageUseCase() + } + + container.register(type: FetchCameraTodayMissionUseCaseProtocol.self) { _ in + self.makeFetchCameraTodayMissionUseCase() + } + + container.register(type: FetchCameraRealEmojiUpdateUseCaseProtocol.self) { _ in + self.makeFetchCameraRealEmojiUpdateUseCase() + } + + container.register(type: FetchCameraRealEmojiUploadUseCaseProtocol.self) { _ in + self.makeFetchCameraRealEmojiUploadUseCase() + } + + container.register(type: FetchCameraRealEmojiListUseCaseProtocol.self) { _ in + self.makeFetchCameraRealEmojiListUseCase() + } + + container.register(type: FetchCameraRealEmojiUseCaseProtocol.self) { _ in + self.makeFetchCameraRealEmojiUseCase() + } + } + + +} diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/ProfileDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/ProfileDIContainer.swift new file mode 100644 index 000000000..f218d5244 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/ProfileDIContainer.swift @@ -0,0 +1,57 @@ +// +// ProfileDIContainer.swift +// App +// +// Created by Kim dohyun on 6/21/24. +// + +import Core +import Data +import Domain + +import UIKit + + +final class ProfileDIContainer: BaseContainer { + + private var globalState: GlobalStateProviderProtocol { + guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { + return GlobalStateProvider() + } + return appDelegate.globalStateProvider + } + + public func makeRepository() -> MembersRepositoryProtocol { + return MembersRepository() + } + + private func makeDeleteMembersProfileUseCase() -> DeleteMembersProfileUseCaseProtocol { + return DeleteMembersProfileUseCase(membersRepository: makeRepository()) + } + + private func makeFetchMembersProfileUseCase() -> FetchMembersProfileUseCaseProtocol { + return FetchMembersProfileUseCase(membersRepository: makeRepository()) + } + + private func makeUpdateMembersProfileUseCase() -> UpdateMembersProfileUseCaseProtocol { + return UpdateMembersProfileUseCase(membersRepository: makeRepository()) + } + + //TODO: FetchMembersPostListUseCaseProtocol는 PostDIContainer에 추가하기 + + + func registerDependencies() { + container.register(type: DeleteMembersProfileUseCaseProtocol.self) { _ in + self.makeDeleteMembersProfileUseCase() + } + + container.register(type: FetchMembersProfileUseCaseProtocol.self) { _ in + self.makeFetchMembersProfileUseCase() + } + + container.register(type: UpdateMembersProfileUseCaseProtocol.self) { _ in + self.makeUpdateMembersProfileUseCase() + } + } + +} diff --git a/14th-team5-iOS/App/Sources/Application/Wrapper/Camera/CameraDisplayViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Wrapper/Camera/CameraDisplayViewControllerWrapper.swift new file mode 100644 index 000000000..cd66d23c6 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/Wrapper/Camera/CameraDisplayViewControllerWrapper.swift @@ -0,0 +1,53 @@ +// +// CameraDisplayViewControllerWrapper.swift +// App +// +// Created by Kim dohyun on 6/21/24. +// + +import Core +import Domain + +import Foundation + + +final class CameraDisplayViewControllerWrapper: BaseWrapper { + + typealias R = CameraDisplayViewReactor + typealias V = CameraDisplayViewController + + private let displayData: Data + private let missionTitle: String + private let cameraDisplayType: PostType + + public init( + displayData: Data, + missionTitle: String = "", + cameraDisplayType: PostType = .survival + ) { + self.displayData = displayData + self.missionTitle = missionTitle + self.cameraDisplayType = cameraDisplayType + } + + var reactor: CameraDisplayViewReactor { + return makeReactor() + } + + var viewController: CameraDisplayViewController { + return makeViewController() + } + + + func makeViewController() -> CameraDisplayViewController { + return CameraDisplayViewController(reactor: reactor) + } + + func makeReactor() -> CameraDisplayViewReactor { + return CameraDisplayViewReactor( + displayData: displayData, + missionTitle: missionTitle, + cameraType: cameraDisplayType + ) + } +} diff --git a/14th-team5-iOS/App/Sources/Application/Wrapper/Camera/CameraViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Wrapper/Camera/CameraViewControllerWrapper.swift new file mode 100644 index 000000000..0ac3435fc --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/Wrapper/Camera/CameraViewControllerWrapper.swift @@ -0,0 +1,52 @@ +// +// CameraViewControllerWrapper.swift +// App +// +// Created by Kim dohyun on 6/19/24. +// + +import Core +import Domain + +import Foundation + +final class CameraViewControllerWrapper: BaseWrapper { + + typealias R = CameraViewReactor + typealias V = CameraViewController + + var reactor: CameraViewReactor { + return makeReactor() + } + var viewController: CameraViewController { + return makeViewController() + } + + private let cameraType: UploadLocation + private let memberId: String + private let realEmojiType: Emojis + + public init( + cameraType: UploadLocation, + memberId: String = "", + realEmojiType: Emojis = .emoji(forIndex: 1) + ) { + self.cameraType = cameraType + self.memberId = memberId + self.realEmojiType = realEmojiType + } + + + func makeReactor() -> CameraViewReactor { + return CameraViewReactor( + cameraType: cameraType, + memberId: memberId, + emojiType: realEmojiType + ) + } + + func makeViewController() -> CameraViewController { + return CameraViewController(reactor: reactor) + } + +} diff --git a/14th-team5-iOS/App/Sources/Application/Wrapper/Profile/ProfileDetailViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Wrapper/Profile/ProfileDetailViewControllerWrapper.swift new file mode 100644 index 000000000..b548641d2 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/Wrapper/Profile/ProfileDetailViewControllerWrapper.swift @@ -0,0 +1,41 @@ +// +// ProfileDetailViewControllerWrapper.swift +// App +// +// Created by Kim dohyun on 6/24/24. +// + +import Core + +import Foundation + +final class ProfileDetailViewControllerWrapper: BaseWrapper { + typealias R = ProfileDetailViewReactor + typealias V = ProfileDetailViewController + + var reactor: ProfileDetailViewReactor { + return makeReactor() + } + + var viewController: ProfileDetailViewController { + return makeViewController() + } + + private let profileURL: URL + private let userNickname: String + + init(profileURL: URL, userNickname: String) { + self.profileURL = profileURL + self.userNickname = userNickname + } + + func makeReactor() -> ProfileDetailViewReactor { + return ProfileDetailViewReactor(profileURL: profileURL, userNickname: userNickname) + } + + func makeViewController() -> ProfileDetailViewController { + return ProfileDetailViewController(reactor: reactor) + } + + +} diff --git a/14th-team5-iOS/App/Sources/Application/Wrapper/Profile/ProfileFeedPageViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Wrapper/Profile/ProfileFeedPageViewControllerWrapper.swift new file mode 100644 index 000000000..089a6ad34 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/Wrapper/Profile/ProfileFeedPageViewControllerWrapper.swift @@ -0,0 +1,38 @@ +// +// ProfileFeedPageViewControllerWrapper.swift +// App +// +// Created by Kim dohyun on 6/21/24. +// + +import Core + +import Foundation + +final class ProfileFeedPageViewControllerWrapper: BaseWrapper { + typealias R = ProfileFeedPageViewReactor + typealias V = ProfileFeedPageViewController + + private let memberId: String + + init(memberId: String) { + self.memberId = memberId + } + + var reactor: ProfileFeedPageViewReactor { + return makeReactor() + } + + var viewController: ProfileFeedPageViewController { + return makeViewController() + } + + func makeViewController() -> ProfileFeedPageViewController { + return ProfileFeedPageViewController(reactor: reactor, memberId: memberId) + } + + func makeReactor() -> ProfileFeedPageViewReactor { + return ProfileFeedPageViewReactor() + } + +} diff --git a/14th-team5-iOS/App/Sources/Application/Wrapper/Profile/ProfileFeedViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Wrapper/Profile/ProfileFeedViewControllerWrapper.swift new file mode 100644 index 000000000..f2500d827 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/Wrapper/Profile/ProfileFeedViewControllerWrapper.swift @@ -0,0 +1,43 @@ +// +// ProfileFeedViewControllerWrapper.swift +// App +// +// Created by Kim dohyun on 6/24/24. +// + +import Core +import Domain + +import Foundation + + +final class ProfileFeedViewControllerWrapper: BaseWrapper { + + typealias R = ProfileFeedViewReactor + typealias V = ProfileFeedViewController + + var reactor: ProfileFeedViewReactor { + return makeReactor() + } + + var viewController: ProfileFeedViewController { + return makeViewController() + } + + private let postType: PostType + private let memberId: String + + init(postType: PostType, memberId: String) { + self.postType = postType + self.memberId = memberId + } + + + func makeReactor() -> ProfileFeedViewReactor { + return ProfileFeedViewReactor(type: postType, memberId: memberId) + } + + func makeViewController() -> ProfileFeedViewController { + return ProfileFeedViewController(reactor: reactor) + } +} diff --git a/14th-team5-iOS/App/Sources/Application/Wrapper/Profile/ProfileViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Wrapper/Profile/ProfileViewControllerWrapper.swift new file mode 100644 index 000000000..614fdfa86 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/Wrapper/Profile/ProfileViewControllerWrapper.swift @@ -0,0 +1,42 @@ +// +// ProfileViewControllerWrapper.swift +// App +// +// Created by Kim dohyun on 6/21/24. +// + +import Core + +import Foundation + +final class ProfileViewControllerWrapper: BaseWrapper { + typealias R = ProfileViewReactor + typealias V = ProfileViewController + + private let memberId: String + private let isUser: Bool + + init( + memberId: String = "" + ) { + self.memberId = memberId + self.isUser = memberId == App.Repository.member.memberID.value ? true : false + } + + var reactor: ProfileViewReactor { + return makeReactor() + } + + var viewController: ProfileViewController { + return makeViewController() + } + + + func makeViewController() -> ProfileViewController { + return ProfileViewController(reactor: reactor) + } + + func makeReactor() -> ProfileViewReactor { + ProfileViewReactor(memberId: memberId, isUser: isUser) + } +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountProfileViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountProfileViewController.swift index b78339fb7..1ee8ccf5a 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountProfileViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountProfileViewController.swift @@ -210,7 +210,7 @@ extension AccountProfileViewController { private func createAlertController(owner: AccountProfileViewController) { let alertController: UIAlertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) let presentCameraAction: UIAlertAction = UIAlertAction(title: "카메라", style: .default) { _ in - let cameraViewController = CameraDIContainer(cameraType: .profile).makeViewController() + let cameraViewController = CameraViewControllerWrapper(cameraType: .profile).viewController owner.navigationController?.pushViewController(cameraViewController, animated: true) } let presentAlbumAction: UIAlertAction = UIAlertAction(title: "앨범", style: .default) { _ in diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/DailyCalendarViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/DailyCalendarViewController.swift index 71357d870..af7d352dc 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/DailyCalendarViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/DailyCalendarViewController.swift @@ -436,9 +436,7 @@ extension DailyCalendarViewController { } private func pushCameraViewController(cameraType type: UploadLocation) { - let cameraViewController = CameraDIContainer( - cameraType: type - ).makeViewController() + let cameraViewController = CameraViewControllerWrapper(cameraType: type).viewController navigationController?.pushViewController( cameraViewController, @@ -447,9 +445,9 @@ extension DailyCalendarViewController { } private func pushProfileViewController(memberId: String) { - let profileController = ProfileDIContainer( + let profileController = ProfileViewControllerWrapper( memberId: memberId - ).makeViewController() + ).viewController navigationController?.pushViewController( profileController, diff --git a/14th-team5-iOS/App/Sources/Presentation/Camera/CameraViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Camera/CameraViewController.swift index db7c94a5c..c4670f2b7 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Camera/CameraViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Camera/CameraViewController.swift @@ -238,7 +238,7 @@ public final class CameraViewController: BaseViewController { .filter { $0.1.asPostType == .survival } .withUnretained(self) .bind { - let cameraDisplayViewController = CameraDisplayDIContainer(displayData: $0.1.0).makeViewController() + let cameraDisplayViewController = CameraDisplayViewControllerWrapper(displayData: $0.1.0).viewController $0.0.navigationController?.pushViewController(cameraDisplayViewController, animated: true) }.disposed(by: disposeBag) @@ -251,7 +251,7 @@ public final class CameraViewController: BaseViewController { ) .withUnretained(self) .bind { - let cameraDisplayViewController = CameraDisplayDIContainer(displayData: $0.1.0, missionTitle: $0.1.1, cameraDisplayType: $0.1.2).makeViewController() + let cameraDisplayViewController = CameraDisplayViewControllerWrapper(displayData: $0.1.0, missionTitle: $0.1.1, cameraDisplayType: $0.1.2).viewController $0.0.navigationController?.pushViewController(cameraDisplayViewController, animated: true) }.disposed(by: disposeBag) diff --git a/14th-team5-iOS/App/Sources/Presentation/Camera/Dependency/CameraDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/Camera/Dependency/CameraDIContainer.swift deleted file mode 100644 index 49ae164c0..000000000 --- a/14th-team5-iOS/App/Sources/Presentation/Camera/Dependency/CameraDIContainer.swift +++ /dev/null @@ -1,68 +0,0 @@ -// -// CameraDIContainer.swift -// App -// -// Created by Kim dohyun on 12/6/23. -// - -import Foundation - -import Core -import Data -import Domain -import UIKit - -public final class CameraDIContainer: BaseDIContainer { - - public typealias ViewContrller = CameraViewController - public typealias Repository = CameraRepositoryProtocol - public typealias Reactor = CameraViewReactor - - - private let cameraType: UploadLocation - private let realEmojiType: Emojis - private let memberId: String - - - private var globalState: GlobalStateProviderProtocol { - guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { - return GlobalStateProvider() - } - return appDelegate.globalStateProvider - } - - public init(cameraType: UploadLocation, memberId: String = "", realEmojiType: Emojis = Emojis.emoji(forIndex: 1)) { - self.cameraType = cameraType - self.realEmojiType = realEmojiType - self.memberId = App.Repository.member.memberID.value ?? "" - } - - public func makeViewController() -> CameraViewController { - return CameraViewController(reactor: makeReactor()) - } - - - public func makeRepository() -> Repository { - return CameraRepository() - } - - public func makeReactor() -> Reactor { - - return CameraViewReactor( - createProfileImageUseCase: CreateCameraUseCase(cameraRepository: makeRepository()), - uploadImageUseCase: FetchCameraUploadImageUseCase(cameraRepository: makeRepository()), - fetchMissionUseCase: FetchCameraTodayMissionUseCase(cameraRepository: makeRepository()), - fetchRealEmojiUpdateUseCase: FetchCameraRealEmojiUpdateUseCase(cameraRepostiroy: makeRepository()), - editProfileImageUseCase: EditCameraProfileImageUseCase(cameraRepository: makeRepository()), - fetchRealEmojiCreateUseCase: FetchCameraRealEmojiUploadUseCase(cameraRepository: makeRepository()), - fetchRealEmojiListUseCase: FetchCameraRealEmojiListUseCase(cameraRepository: makeRepository()), - fetchRealEmojiPreSignedUseCase: FetchCameraRealEmojiUseCase(cameraRepository: makeRepository()), - provider: globalState, - cameraType: cameraType, - memberId: memberId, - emojiType: realEmojiType - ) - } - - -} diff --git a/14th-team5-iOS/App/Sources/Presentation/Camera/Dependency/CameraDisplayDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/Camera/Dependency/CameraDisplayDIContainer.swift deleted file mode 100644 index d1334884e..000000000 --- a/14th-team5-iOS/App/Sources/Presentation/Camera/Dependency/CameraDisplayDIContainer.swift +++ /dev/null @@ -1,56 +0,0 @@ -// -// CameraDisplayDIContainer.swift -// App -// -// Created by Kim dohyun on 12/11/23. -// - -import UIKit - -import Core -import Data -import Domain - - -public final class CameraDisplayDIContainer: BaseDIContainer { - public typealias ViewContrller = CameraDisplayViewController - public typealias Repository = CameraRepositoryProtocol - public typealias Reactor = CameraDisplayViewReactor - - fileprivate var displayData: Data - fileprivate var missionTitle: String - fileprivate var cameraDisplayType: PostType - - private var globalState: GlobalStateProviderProtocol { - guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { - return GlobalStateProvider() - } - return appDelegate.globalStateProvider - } - - public init(displayData: Data, missionTitle: String = "", cameraDisplayType: PostType = .survival) { - self.displayData = displayData - self.missionTitle = missionTitle - self.cameraDisplayType = cameraDisplayType - } - - public func makeViewController() -> ViewContrller { - return CameraDisplayViewController(reactor: makeReactor()) - } - - public func makeRepository() -> Repository { - return CameraRepository() - } - - public func makeReactor() -> Reactor { - return CameraDisplayViewReactor( - provider: globalState, - createPresignedCameraUseCase: CreateCameraUseCase(cameraRepository: makeRepository()), - uploadImageUseCase: FetchCameraUploadImageUseCase(cameraRepository: makeRepository()), - fetchCameraImageUseCase: CreateCameraImageUseCase(cameraRepository: makeRepository()), - displayData: displayData, - missionTitle: missionTitle, - cameraType: cameraDisplayType - ) - } -} diff --git a/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraDisplayViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraDisplayViewReactor.swift index 36eb3b9af..615027334 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraDisplayViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraDisplayViewReactor.swift @@ -15,10 +15,10 @@ import Core public final class CameraDisplayViewReactor: Reactor { public var initialState: State - private let provider: GlobalStateProviderProtocol - private let createPresignedCameraUseCase: CreateCameraUseCaseProtocol - private let uploadImageUseCase: FetchCameraUploadImageUseCaseProtocol - private let fetchCameraImageUseCase: CreateCameraImageUseCaseProtocol + @Injected private var provider: GlobalStateProviderProtocol + @Injected private var createPresignedCameraUseCase: CreateCameraUseCaseProtocol + @Injected private var uploadImageUseCase: FetchCameraUploadImageUseCaseProtocol + @Injected private var fetchCameraImageUseCase: CreateCameraImageUseCaseProtocol public enum Action { case viewDidLoad @@ -56,19 +56,10 @@ public final class CameraDisplayViewReactor: Reactor { init( - provider: GlobalStateProviderProtocol, - createPresignedCameraUseCase: CreateCameraUseCaseProtocol, - uploadImageUseCase: FetchCameraUploadImageUseCaseProtocol, - fetchCameraImageUseCase: CreateCameraImageUseCaseProtocol, displayData: Data, missionTitle: String, cameraType: PostType = .survival ) { - self.provider = provider - self.createPresignedCameraUseCase = createPresignedCameraUseCase - self.uploadImageUseCase = uploadImageUseCase - self.fetchCameraImageUseCase = fetchCameraImageUseCase - self.initialState = State( isLoading: true, displayDescrption: "", diff --git a/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraViewReactor.swift index 5429c414f..fa738873f 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraViewReactor.swift @@ -19,19 +19,17 @@ public final class CameraViewReactor: Reactor { public var initialState: State - private let createProfileImageUseCase: CreateCameraUseCaseProtocol - private let uploadImageUseCase: FetchCameraUploadImageUseCaseProtocol - private let fetchMissionUseCase: FetchCameraTodayMissionUseCaseProtocol - private let fetchRealEmojiUpdateUseCase: FetchCameraRealEmojiUpdateUseCaseProtocol - private let fetchRealEmojiCreateUseCase: FetchCameraRealEmojiUploadUseCaseProtocol - private let editProfileImageUseCase: EditCameraProfileImageUseCaseProtocol - private let fetchRealEmojiListUseCase: FetchCameraRealEmojiListUseCaseProtocol - private let fetchRealEmojiPreSignedUseCase: FetchCameraRealEmojiUseCaseProtocol + @Injected private var createProfileImageUseCase: CreateCameraUseCaseProtocol + @Injected private var uploadImageUseCase: FetchCameraUploadImageUseCaseProtocol + @Injected private var fetchMissionUseCase: FetchCameraTodayMissionUseCaseProtocol + @Injected private var fetchRealEmojiUpdateUseCase: FetchCameraRealEmojiUpdateUseCaseProtocol + @Injected private var fetchRealEmojiCreateUseCase: FetchCameraRealEmojiUploadUseCaseProtocol + @Injected private var editProfileImageUseCase: EditCameraProfileImageUseCaseProtocol + @Injected private var fetchRealEmojiListUseCase: FetchCameraRealEmojiListUseCaseProtocol + @Injected private var fetchRealEmojiPreSignedUseCase: FetchCameraRealEmojiUseCaseProtocol + @Injected private var provider: GlobalStateProviderProtocol - - - private let provider: GlobalStateProviderProtocol public var cameraType: UploadLocation public var memberId: String @@ -90,30 +88,12 @@ public final class CameraViewReactor: Reactor { } init( - createProfileImageUseCase: CreateCameraUseCaseProtocol, - uploadImageUseCase: FetchCameraUploadImageUseCaseProtocol, - fetchMissionUseCase: FetchCameraTodayMissionUseCaseProtocol, - fetchRealEmojiUpdateUseCase: FetchCameraRealEmojiUpdateUseCaseProtocol, - editProfileImageUseCase: EditCameraProfileImageUseCaseProtocol, - fetchRealEmojiCreateUseCase: FetchCameraRealEmojiUploadUseCaseProtocol, - fetchRealEmojiListUseCase: FetchCameraRealEmojiListUseCaseProtocol, - fetchRealEmojiPreSignedUseCase: FetchCameraRealEmojiUseCaseProtocol, - provider: GlobalStateProviderProtocol, cameraType: UploadLocation, memberId: String, emojiType: Emojis = .emoji(forIndex: 1) ) { self.cameraType = cameraType - self.createProfileImageUseCase = createProfileImageUseCase - self.uploadImageUseCase = uploadImageUseCase - self.fetchMissionUseCase = fetchMissionUseCase - self.fetchRealEmojiUpdateUseCase = fetchRealEmojiUpdateUseCase - self.editProfileImageUseCase = editProfileImageUseCase - self.fetchRealEmojiCreateUseCase = fetchRealEmojiCreateUseCase - self.fetchRealEmojiListUseCase = fetchRealEmojiListUseCase - self.fetchRealEmojiPreSignedUseCase = fetchRealEmojiPreSignedUseCase self.memberId = memberId - self.provider = provider self.initialState = State( isLoading: true, isFlashMode: false, diff --git a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/ViewController/FamilyManagementViewController.swift b/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/ViewController/FamilyManagementViewController.swift index dd94c5bc5..cd3347ed6 100644 --- a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/ViewController/FamilyManagementViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/ViewController/FamilyManagementViewController.swift @@ -125,8 +125,8 @@ public final class FamilyManagementViewController: BaseViewController { .withUnretained(self) .observe(on: MainScheduler.asyncInstance) .bind { owner, entity in - let cameraViewController = CameraDIContainer(cameraType: .realEmoji, realEmojiType: entity).makeViewController() + let cameraViewController = CameraViewControllerWrapper(cameraType: .realEmoji, realEmojiType: entity).viewController owner.navigationController?.pushViewController(cameraViewController, animated: true) }.disposed(by: disposeBag) @@ -253,9 +253,9 @@ extension PostViewController { extension PostViewController { private func pushCameraViewController(cameraType type: UploadLocation) { - let cameraViewController = CameraDIContainer( + let cameraViewController = CameraViewControllerWrapper( cameraType: type - ).makeViewController() + ).viewController navigationController?.pushViewController( cameraViewController, @@ -264,9 +264,9 @@ extension PostViewController { } private func pushProfileViewController(memberId: String) { - let profileController = ProfileDIContainer( + let profileController = ProfileViewControllerWrapper( memberId: memberId - ).makeViewController() + ).viewController navigationController?.pushViewController( profileController, diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/Dependency/ProfileDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/Dependency/ProfileDIContainer.swift deleted file mode 100644 index 18712a1eb..000000000 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/Dependency/ProfileDIContainer.swift +++ /dev/null @@ -1,58 +0,0 @@ -// -// ProfileDIContainer.swift -// App -// -// Created by Kim dohyun on 12/17/23. -// - -import UIKit - -import Core -import Data -import Domain - - -public final class ProfileDIContainer: BaseDIContainer { - public typealias ViewContrller = ProfileViewController - public typealias Repository = MembersRepositoryProtocol - public typealias Reactor = ProfileViewReactor - - private var globalState: GlobalStateProviderProtocol { - guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { - return GlobalStateProvider() - } - return appDelegate.globalStateProvider - } - - private let memberId: String - private let isUser: Bool - - public init(memberId: String = "") { - self.memberId = memberId - self.isUser = memberId == App.Repository.member.memberID.value ? true : false - } - - public func makeViewController() -> ProfileViewController { - return ProfileViewController(reactor: makeReactor()) - } - - public func makeRepository() -> Repository { - return MembersRepository() - } - - private func makeCameraRepository() -> CameraRepository { - return CameraRepository() - } - - public func makeReactor() -> ProfileViewReactor { - return ProfileViewReactor( - fetchMembersProfileUseCase: FetchMembersProfileUseCase(membersRepository: makeRepository()), - createProfilePresignedUseCase: CreateCameraUseCase(cameraRepository: makeCameraRepository()), - uploadProfileImageUseCase: FetchCameraUploadImageUseCase(cameraRepository: makeCameraRepository()), - updateProfileUseCase: UpdateMembersProfileUseCase(membersRepository: makeRepository()), - provider: globalState, - memberId: memberId, - isUser: isUser - ) - } -} diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/Dependency/ProfileDetailDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/Dependency/ProfileDetailDIContainer.swift deleted file mode 100644 index 292d15476..000000000 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/Dependency/ProfileDetailDIContainer.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// ProfileDetailDIContainer.swift -// App -// -// Created by Kim dohyun on 2/10/24. -// - -import Foundation - -import Core - - -final class ProfileDetailDIContainer { - public typealias ViewContrller = ProfileDetailViewController - public typealias Reactor = ProfileDetailViewReactor - - private let profileURL: URL - private let userNickname: String - - init(profileURL: URL, userNickname: String) { - self.profileURL = profileURL - self.userNickname = userNickname - } - - - public func makeReactor() -> Reactor { - return ProfileDetailViewReactor(profileURL: profileURL, userNickname: userNickname) - } - - public func makeViewController() -> ViewContrller { - return ProfileDetailViewController(reactor: makeReactor()) - } - - - -} diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/Dependency/ProfileFeedDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/Dependency/ProfileFeedDIContainer.swift deleted file mode 100644 index ffd601eba..000000000 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/Dependency/ProfileFeedDIContainer.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// ProfileFeedDIContainer.swift -// App -// -// Created by Kim dohyun on 5/4/24. -// - -import UIKit - -import Core -import Data -import Domain - - -final class ProfileFeedDIContainer { - typealias ViewController = ProfileFeedViewController - typealias Reactor = ProfileFeedViewReactor - typealias Repository = PostListRepositoryProtocol - typealias UseCase = FetchMembersPostListUseCaseProtocol - - private let postType: PostType - private let memberId: String - - - init(postType: PostType, memberId: String) { - self.postType = postType - self.memberId = memberId - } - - func makeViewController() -> ViewController { - return ProfileFeedViewController(reactor: makeReactor()) - } - - func makeReactor() -> Reactor { - return ProfileFeedViewReactor(feedUseCase: makeUseCase(), type: postType, memberId: memberId) - } - - func makeUseCase() -> UseCase { - return FetchMembersPostListUseCase(postListRepository: makeRepository()) - } - - func makeRepository() -> Repository { - return PostRepository() - } - - - -} diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/Dependency/ProfileFeedPageDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/Dependency/ProfileFeedPageDIContainer.swift deleted file mode 100644 index c31d89c4b..000000000 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/Dependency/ProfileFeedPageDIContainer.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// ProfileFeedPageDIContainer.swift -// App -// -// Created by Kim dohyun on 5/16/24. -// - -import UIKit - -import Core - - -final class ProfileFeedPageDIContainer { - typealias ViewController = ProfileFeedPageViewController - typealias Reactor = ProfileFeedPageViewReactor - - private var memberId: String = "" - - init(memberId: String) { - self.memberId = memberId - } - - private var globalState: GlobalStateProviderProtocol { - guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { - return GlobalStateProvider() - } - return appDelegate.globalStateProvider - } - - func makeViewController() -> ViewController { - return ProfileFeedPageViewController(reactor: makeReactor(), memberId: memberId) - } - - func makeReactor() -> Reactor { - return ProfileFeedPageViewReactor(provider: globalState) - } - - -} diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileFeedPageViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileFeedPageViewController.swift index f94b02e4f..0a77b5599 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileFeedPageViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileFeedPageViewController.swift @@ -19,8 +19,8 @@ final class ProfileFeedPageViewController: UIPageViewController { private lazy var feedViewControllers:[UIViewController] = [profileFeedSurivalViewController, profileFeedMissionViewController] public var memberId: String - private lazy var profileFeedSurivalViewController: ProfileFeedViewController = ProfileFeedDIContainer(postType: .survival, memberId: memberId).makeViewController() - private lazy var profileFeedMissionViewController: ProfileFeedViewController = ProfileFeedDIContainer(postType: .mission, memberId: memberId).makeViewController() + private lazy var profileFeedSurivalViewController: ProfileFeedViewController = ProfileFeedViewControllerWrapper(postType: .survival, memberId: memberId).viewController + private lazy var profileFeedMissionViewController: ProfileFeedViewController = ProfileFeedViewControllerWrapper(postType: .mission, memberId: memberId).viewController diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileViewController.swift index e4475e219..cdaacb201 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileViewController.swift @@ -36,7 +36,7 @@ public final class ProfileViewController: BaseViewController private let profileLineView: UIView = UIView() private lazy var profilePickerController: PHPickerViewController = PHPickerViewController(configuration: pickerConfiguration) - private lazy var profileFeedViewController: ProfileFeedPageViewController = ProfileFeedPageDIContainer(memberId: reactor?.currentState.memberId ?? "").makeViewController() + private lazy var profileFeedViewController: ProfileFeedPageViewController = ProfileFeedPageViewControllerWrapper(memberId: reactor?.currentState.memberId ?? "").viewController public override func viewWillAppear(_ animated: Bool) { @@ -249,7 +249,7 @@ public final class ProfileViewController: BaseViewController .withLatestFrom(reactor.state.compactMap { $0.profileMemberEntity } ) .withUnretained(self) .bind { owner, entity in - let profileDetailViewController = ProfileDetailDIContainer(profileURL: entity.memberImage, userNickname: entity.memberName).makeViewController() + let profileDetailViewController = ProfileDetailViewControllerWrapper(profileURL: entity.memberImage, userNickname: entity.memberName).makeViewController() owner.navigationController?.pushViewController(profileDetailViewController, animated: false) }.disposed(by: disposeBag) @@ -299,7 +299,7 @@ extension ProfileViewController { let presentCameraAction: UIAlertAction = UIAlertAction(title: "카메라", style: .default) { _ in guard let profileMemberId = self.reactor?.currentState.profileMemberEntity?.memberId else { return } - let cameraViewController = CameraDIContainer(cameraType: .profile, memberId: profileMemberId).makeViewController() + let cameraViewController = CameraViewControllerWrapper(cameraType: .profile, memberId: profileMemberId).viewController self.navigationController?.pushViewController(cameraViewController, animated: true) } diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileFeedPageViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileFeedPageViewReactor.swift index 3e4871dfb..4feb09dde 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileFeedPageViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileFeedPageViewReactor.swift @@ -14,7 +14,7 @@ final class ProfileFeedPageViewReactor: Reactor { //MAKR: Property public var initialState: State - private var provider: GlobalStateProviderProtocol + @Injected private var provider: GlobalStateProviderProtocol enum Action { case updatePageViewController(Int) @@ -29,9 +29,8 @@ final class ProfileFeedPageViewReactor: Reactor { case didShowPageViewController(BibbiFeedType) } - init(provider: GlobalStateProviderProtocol) { + init() { self.initialState = State(pageType: .survival) - self.provider = provider } func transform(mutation: Observable) -> Observable { diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileFeedViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileFeedViewReactor.swift index c1c5b9c09..6ec400aaa 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileFeedViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileFeedViewReactor.swift @@ -15,7 +15,7 @@ import ReactorKit final class ProfileFeedViewReactor: Reactor { var initialState: State - private let feedUseCase: FetchMembersPostListUseCaseProtocol + @Injected private var feedUseCase: FetchMembersPostListUseCaseProtocol enum Action { case reloadFeedItems @@ -43,11 +43,9 @@ final class ProfileFeedViewReactor: Reactor { } init( - feedUseCase: FetchMembersPostListUseCaseProtocol, type: PostType, memberId: String ) { - self.feedUseCase = feedUseCase self.initialState = State( memberId: memberId, selectedIndex: nil, diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift index b05c52a86..48c6ee2d9 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift @@ -15,16 +15,16 @@ import ReactorKit public final class ProfileViewReactor: Reactor { public var initialState: State - private let fetchMembersProfileUseCase: FetchMembersProfileUseCaseProtocol - private let createProfilePresignedUseCase: CreateCameraUseCaseProtocol - private let uploadProfileImageUseCase: FetchCameraUploadImageUseCaseProtocol - private let updateProfileUseCase: UpdateMembersProfileUseCaseProtocol + @Injected private var fetchMembersProfileUseCase: FetchMembersProfileUseCaseProtocol + @Injected private var createProfilePresignedUseCase: CreateCameraUseCaseProtocol + @Injected private var uploadProfileImageUseCase: FetchCameraUploadImageUseCaseProtocol + @Injected private var updateProfileUseCase: UpdateMembersProfileUseCaseProtocol private let memberId: String private let isUser: Bool - private let provider: GlobalStateProviderProtocol + @Injected private var provider: GlobalStateProviderProtocol public enum Action { case viewDidLoad @@ -53,18 +53,9 @@ public final class ProfileViewReactor: Reactor { } init( - fetchMembersProfileUseCase: FetchMembersProfileUseCaseProtocol, - createProfilePresignedUseCase: CreateCameraUseCaseProtocol, - uploadProfileImageUseCase: FetchCameraUploadImageUseCaseProtocol, - updateProfileUseCase: UpdateMembersProfileUseCaseProtocol, - provider: GlobalStateProviderProtocol, memberId: String, isUser: Bool ) { - self.fetchMembersProfileUseCase = fetchMembersProfileUseCase - self.createProfilePresignedUseCase = createProfilePresignedUseCase - self.uploadProfileImageUseCase = uploadProfileImageUseCase - self.updateProfileUseCase = updateProfileUseCase self.memberId = memberId self.isUser = isUser self.initialState = State( diff --git a/14th-team5-iOS/Domain/Sources/Camera/UseCases/Camera/CreateCameraUseCase.swift b/14th-team5-iOS/Domain/Sources/Camera/UseCases/Camera/CreateCameraUseCase.swift index 94b63f228..588f4af73 100644 --- a/14th-team5-iOS/Domain/Sources/Camera/UseCases/Camera/CreateCameraUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/Camera/UseCases/Camera/CreateCameraUseCase.swift @@ -14,8 +14,6 @@ public protocol CreateCameraUseCaseProtocol { func execute(parameter: CameraDisplayImageParameters) -> Single } - -// Camera Presigned URL 생성 UseCase public final class CreateCameraUseCase: CreateCameraUseCaseProtocol { private let cameraRepository: any CameraRepositoryProtocol From 8cd457a2059008778342c353e35a2b3a68799699 Mon Sep 17 00:00:00 2001 From: Do-hyun-Kim Date: Mon, 24 Jun 2024 16:34:21 +0900 Subject: [PATCH 134/263] =?UTF-8?q?feat:=20PostDIContainer=20makeFetchMemb?= =?UTF-8?q?ersPostListUseCase=20Method=20=EC=B6=94=EA=B0=80=20-=20AppDeleg?= =?UTF-8?q?ate=20ProfileDIContainer=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../App/Sources/Application/AppDelegate.swift | 2 +- .../DIContainer/PostDIContainer.swift | 8 ++ .../xcschemes/Bibbi-Workspace.xcscheme | 106 +++++++++--------- 3 files changed, 62 insertions(+), 54 deletions(-) diff --git a/14th-team5-iOS/App/Sources/Application/AppDelegate.swift b/14th-team5-iOS/App/Sources/Application/AppDelegate.swift index d91aaf5b9..61cb371cd 100644 --- a/14th-team5-iOS/App/Sources/Application/AppDelegate.swift +++ b/14th-team5-iOS/App/Sources/Application/AppDelegate.swift @@ -56,7 +56,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { AppDIContainer(), CalendarDIContainer(), CameraDIContainer(), - ProfileDIContainer() + ProfileDIContainer(), CommentDIContainer(), FamilyDIContainer(), OAuthDIContainer(), diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/PostDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/PostDIContainer.swift index 540a3f0bb..1198cc9c1 100644 --- a/14th-team5-iOS/App/Sources/Application/DIContainer/PostDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/PostDIContainer.swift @@ -16,6 +16,10 @@ final class PostDIContainer: BaseContainer { private func makePostUseCase() -> FetchPostListUseCaseProtocol { return FetchPostListUseCase(postListRepository: repository) } + + private func makeFetchMembersPostListUseCase() -> FetchMembersPostListUseCaseProtocol { + return FetchMembersPostListUseCase(postListRepository: repository) + } } extension PostDIContainer { @@ -23,6 +27,10 @@ extension PostDIContainer { container.register(type: FetchPostListUseCaseProtocol.self) { _ in self.makePostUseCase() } + + container.register(type: FetchMembersPostListUseCaseProtocol.self) { _ in + self.makeFetchMembersPostListUseCase() + } } } diff --git a/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme b/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme index 5f307e6b4..7023994af 100644 --- a/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme +++ b/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme @@ -280,52 +280,10 @@ buildForAnalyzing = "YES"> - - - - - - - - - - - - + BlueprintIdentifier = "4160F70E94342FB432F5ABF1" + BuildableName = "GoogleAppMeasurementTarget.framework" + BlueprintName = "GoogleAppMeasurementTarget" + ReferencedContainer = "container:Tuist/Dependencies/SwiftPackageManager/.build/checkouts/GoogleAppMeasurement/GoogleAppMeasurement.xcodeproj"> + BlueprintIdentifier = "08D12528663D0C8DD1A99BCB" + BuildableName = "GoogleDataTransport.framework" + BlueprintName = "GoogleDataTransport" + ReferencedContainer = "container:Tuist/Dependencies/SwiftPackageManager/.build/checkouts/GoogleDataTransport/GoogleDataTransport.xcodeproj"> @@ -468,6 +426,34 @@ ReferencedContainer = "container:Tuist/Dependencies/SwiftPackageManager/.build/checkouts/GoogleUtilities/GoogleUtilities.xcodeproj"> + + + + + + + + + + + + Date: Mon, 24 Jun 2024 18:37:40 +0900 Subject: [PATCH 135/263] =?UTF-8?q?fix:=20CameraDIContainer,=20ProfileDICo?= =?UTF-8?q?ntainer=20=EA=B8=B0=EC=A1=B4=20=EC=BD=94=EB=93=9C=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Application/DIContainer/CameraDIContainer.swift | 8 -------- .../Application/DIContainer/ProfileDIContainer.swift | 9 +-------- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/CameraDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/CameraDIContainer.swift index 9f6201f55..8bf02943b 100644 --- a/14th-team5-iOS/App/Sources/Application/DIContainer/CameraDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/CameraDIContainer.swift @@ -14,14 +14,6 @@ import UIKit final class CameraDIContainer: BaseContainer { - private var globalState: GlobalStateProviderProtocol { - guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { - return GlobalStateProvider() - } - return appDelegate.globalStateProvider - } - - public func makeRepository() -> CameraRepositoryProtocol { return CameraRepository() } diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/ProfileDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/ProfileDIContainer.swift index f218d5244..b836920da 100644 --- a/14th-team5-iOS/App/Sources/Application/DIContainer/ProfileDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/ProfileDIContainer.swift @@ -13,14 +13,7 @@ import UIKit final class ProfileDIContainer: BaseContainer { - - private var globalState: GlobalStateProviderProtocol { - guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { - return GlobalStateProvider() - } - return appDelegate.globalStateProvider - } - + public func makeRepository() -> MembersRepositoryProtocol { return MembersRepository() } From 0d86ceb906f4471d112e309a58b9d10787987fb6 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Tue, 25 Jun 2024 11:06:03 +0900 Subject: [PATCH 136/263] =?UTF-8?q?feat:=20Calendar=20=EA=B4=80=EB=A0=A8?= =?UTF-8?q?=20Wrapper=20=EA=B5=AC=ED=98=84=20(#570)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DIContainer/FamilyDIContainer.swift | 12 ++-- .../Calendar/DailyCalendarDIContainer.swift | 51 ++++++++++++++ ...onthlyCalendarViewControllerWrapper.swift} | 24 ++++--- .../DIContainer/CalendarCellDIContainer.swift | 10 +-- .../CalendarImageCellDIContainer.swift | 13 +--- .../CalendarPostCellDIContainer.swift | 12 +--- .../DIContainer/CalendarPostDIContainer.swift | 19 ++--- .../MonthlyCalendarDIConatainer.swift | 5 +- .../Reactor/CalendarImageCellReactor.swift | 11 +-- .../Reactor/CalendarPostCellReactor.swift | 10 +-- .../Reactor/DailyCalendarViewReactor.swift | 13 ++-- ...swift => MonthlyCalendarViewReactor.swift} | 2 +- .../MonthlyCalendarViewController.swift | 8 +-- .../xcschemes/Bibbi-Workspace.xcscheme | 70 +++++++++++++++++++ 14 files changed, 174 insertions(+), 86 deletions(-) create mode 100644 14th-team5-iOS/App/Sources/Application/Wrapper/Calendar/DailyCalendarDIContainer.swift rename 14th-team5-iOS/App/Sources/{Presentation/Calendar/Wrapper/CalendarViewControllerWrapper.swift => Application/Wrapper/Calendar/MonthlyCalendarViewControllerWrapper.swift} (66%) rename 14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/{CalendarViewReactor.swift => MonthlyCalendarViewReactor.swift} (98%) diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/FamilyDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/FamilyDIContainer.swift index 4b2265e25..962b8204d 100644 --- a/14th-team5-iOS/App/Sources/Application/DIContainer/FamilyDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/FamilyDIContainer.swift @@ -11,8 +11,12 @@ import Domain final class FamilyDIContainer: BaseContainer { + // MARK: - Repositories + private let repository: FamilyRepositoryProtocol = FamilyRepository() + // MARK: - Make UseCase + private func makeCreateFamilyUseCase() -> CreateFamilyUseCaseProtocol { CreateFamilyUseCase(familyRepository: repository) } @@ -41,10 +45,6 @@ final class FamilyDIContainer: BaseContainer { ResignFamilyUseCase(familyRepository: repository) } - private func makeInviteFamilyUseCase() -> FamilyUseCaseProtocol { - return FamilyUseCase(familyRepository: repository) - } - // Deprecated private func makeFamilyUseCase() -> FamilyUseCaseProtocol { FamilyUseCase(familyRepository: repository) @@ -81,10 +81,6 @@ final class FamilyDIContainer: BaseContainer { makeResignFamilyUseCase() } - container.register(type: FamilyUseCaseProtocol.self) { _ in - self.makeInviteFamilyUseCase() - } - // Deprecated container.register(type: FamilyUseCaseProtocol.self) { _ in makeFamilyUseCase() diff --git a/14th-team5-iOS/App/Sources/Application/Wrapper/Calendar/DailyCalendarDIContainer.swift b/14th-team5-iOS/App/Sources/Application/Wrapper/Calendar/DailyCalendarDIContainer.swift new file mode 100644 index 000000000..a80e367a3 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/Wrapper/Calendar/DailyCalendarDIContainer.swift @@ -0,0 +1,51 @@ +// +// WeeklyCalendarDIContainer.swift +// App +// +// Created by 김건우 on 6/25/24. +// + +import Core +import Foundation + +final class DailyCalendarDIContainer: BaseWrapper { + + // MARK: - Typealias + + typealias R = DailyCalendarViewReactor + typealias V = DailyCalendarViewController + + // MARK: - Properties + + let date: Date + let link: NotificationDeepLink? + + var reactor: DailyCalendarViewReactor { + makeReactor() + } + + var viewController: DailyCalendarViewController { + makeViewController() + } + + // MARK: - Intializer + + init(date: Date, link: NotificationDeepLink? = nil) { + self.date = date + self.link = link + } + + // MARK: - Make + + func makeReactor() -> DailyCalendarViewReactor { + DailyCalendarViewReactor( + date: date, + notificationDeepLink: link + ) + } + + func makeViewController() -> DailyCalendarViewController { + DailyCalendarViewController(reactor: makeReactor()) + } + +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Wrapper/CalendarViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Wrapper/Calendar/MonthlyCalendarViewControllerWrapper.swift similarity index 66% rename from 14th-team5-iOS/App/Sources/Presentation/Calendar/Wrapper/CalendarViewControllerWrapper.swift rename to 14th-team5-iOS/App/Sources/Application/Wrapper/Calendar/MonthlyCalendarViewControllerWrapper.swift index 4032c2941..ece594d4a 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/Wrapper/CalendarViewControllerWrapper.swift +++ b/14th-team5-iOS/App/Sources/Application/Wrapper/Calendar/MonthlyCalendarViewControllerWrapper.swift @@ -8,18 +8,14 @@ import Core import Foundation -final class CalendarViewControllerWrapper: BaseWrapper { +final class MonthlyCalendarViewControllerWrapper: BaseWrapper { - typealias R = CalendarViewReactor - typealias V = MonthlyCalendarViewController + // MARK: - Typealias - func makeViewController() -> V { - return MonthlyCalendarViewController(reactor: makeReactor()) - } + typealias R = MonthlyCalendarViewReactor + typealias V = MonthlyCalendarViewController - func makeReactor() -> R { - return CalendarViewReactor() - } + // MARK: - Properties var viewController: V { makeViewController() @@ -29,4 +25,14 @@ final class CalendarViewControllerWrapper: BaseWrapper { makeReactor() } + // MARK: - Make + + func makeViewController() -> V { + return MonthlyCalendarViewController(reactor: makeReactor()) + } + + func makeReactor() -> R { + return MonthlyCalendarViewReactor() + } + } diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/DIContainer/CalendarCellDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/DIContainer/CalendarCellDIContainer.swift index f1e6a3c53..0aaab6991 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/DIContainer/CalendarCellDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/DIContainer/CalendarCellDIContainer.swift @@ -11,17 +11,11 @@ import Core import Data import Domain +@available(*, deprecated) public final class CalendarCellDIContainer { // MARK: - Properties public let yearMonth: String - private var globalState: GlobalStateProviderProtocol { - guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { - return GlobalStateProvider() - } - return appDelegate.globalStateProvider - } - // MARK: - Intializer public init(yearMonth: String) { self.yearMonth = yearMonth @@ -39,8 +33,6 @@ public final class CalendarCellDIContainer { public func makeReactor() -> CalendarCellReactor { return CalendarCellReactor( yearMonth: yearMonth -// calendarUseCase: makeUseCase(), -// provider: globalState ) } } diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/DIContainer/CalendarImageCellDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/DIContainer/CalendarImageCellDIContainer.swift index 2c8e00dae..bb24b7dbf 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/DIContainer/CalendarImageCellDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/DIContainer/CalendarImageCellDIContainer.swift @@ -11,18 +11,13 @@ import Core import Data import Domain +@available(*, deprecated) final public class CalendarImageCellDIContainer { // MARK: - Properties public let type: CalendarImageCellReactor.CalendarType public let monthlyEntity: CalendarEntity public let isSelected: Bool - - private var globalState: GlobalStateProviderProtocol { - guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { - return GlobalStateProvider() - } - return appDelegate.globalStateProvider - } + // MARK: - Intializer public init( @@ -48,9 +43,7 @@ final public class CalendarImageCellDIContainer { return CalendarImageCellReactor( type: type, monthlyEntity: monthlyEntity, - isSelected: isSelected, - calendarUseCase: makeUseCase(), - provider: globalState + isSelected: isSelected ) } } diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/DIContainer/CalendarPostCellDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/DIContainer/CalendarPostCellDIContainer.swift index 494729540..d4637b9bd 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/DIContainer/CalendarPostCellDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/DIContainer/CalendarPostCellDIContainer.swift @@ -10,17 +10,11 @@ import Data import Domain import UIKit +@available(*, deprecated) public final class CalendarPostCellDIContainer { // MARK: - Properties let post: DailyCalendarEntity - private var globalState: GlobalStateProviderProtocol { - guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { - return GlobalStateProvider() - } - return appDelegate.globalStateProvider - } - init(post: DailyCalendarEntity) { self.post = post } @@ -36,9 +30,7 @@ public final class CalendarPostCellDIContainer { public func makeReactor() -> CalendarPostCellReactor { return CalendarPostCellReactor( - post: post, - meUseCase: makeMeUseCase(), - provider: globalState + post: post ) } } diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/DIContainer/CalendarPostDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/DIContainer/CalendarPostDIContainer.swift index 962859024..a54919d66 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/DIContainer/CalendarPostDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/DIContainer/CalendarPostDIContainer.swift @@ -11,6 +11,7 @@ import Data import Domain import UIKit +@available(*, deprecated, renamed: "DailyCalendarViewControllerWrapper") public final class WeeklyCalendarDIConatainer { // MARK: - Properties let date: Date @@ -26,12 +27,12 @@ public final class WeeklyCalendarDIConatainer { self.deepLink = deepLink } - private var globalState: GlobalStateProviderProtocol { - guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { - return GlobalStateProvider() - } - return appDelegate.globalStateProvider - } +// private var globalState: GlobalStateProviderProtocol { +// guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { +// return GlobalStateProvider() +// } +// return appDelegate.globalStateProvider +// } // MARK: - Make public func makeViewController() -> DailyCalendarViewController { @@ -49,9 +50,9 @@ public final class WeeklyCalendarDIConatainer { public func makeReactor() -> DailyCalendarViewReactor { return DailyCalendarViewReactor( date: date, - notificationDeepLink: deepLink, - calendarUseCase: makeCalendarUseCase(), - provider: globalState + notificationDeepLink: deepLink +// calendarUseCase: makeCalendarUseCase(), +// provider: globalState ) } } diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/DIContainer/MonthlyCalendarDIConatainer.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/DIContainer/MonthlyCalendarDIConatainer.swift index 1525605a8..637794ff9 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/DIContainer/MonthlyCalendarDIConatainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/DIContainer/MonthlyCalendarDIConatainer.swift @@ -11,6 +11,7 @@ import Core import Data import Domain +@available(*, deprecated, renamed: "MonthlyCalendarViewControllerWrapper") public final class MonthlyCalendarDIConatainer { // MARK: - Properties private var globalState: GlobalStateProviderProtocol { @@ -33,8 +34,8 @@ public final class MonthlyCalendarDIConatainer { return CalendarRepository() } - public func makeReactor() -> CalendarViewReactor { - return CalendarViewReactor( + public func makeReactor() -> MonthlyCalendarViewReactor { + return MonthlyCalendarViewReactor( // calendarUseCase: makeCalendarUseCase() // provider: globalState ) diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarImageCellReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarImageCellReactor.swift index 96d6ce45b..ac942ae44 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarImageCellReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarImageCellReactor.swift @@ -40,8 +40,8 @@ final public class CalendarImageCellReactor: Reactor { // MARK: - Properties public var initialState: State - private let calendarUseCase: CalendarUseCaseProtocol - private let provider: GlobalStateProviderProtocol + @Injected var calendarUseCase: CalendarUseCaseProtocol + @Injected var provider: GlobalStateProviderProtocol public let type: CalendarType @@ -49,9 +49,7 @@ final public class CalendarImageCellReactor: Reactor { init( type: CalendarType, monthlyEntity: CalendarEntity, - isSelected: Bool, - calendarUseCase: CalendarUseCaseProtocol, - provider: GlobalStateProviderProtocol + isSelected: Bool ) { self.initialState = State( date: monthlyEntity.date, @@ -62,9 +60,6 @@ final public class CalendarImageCellReactor: Reactor { ) self.type = type - - self.calendarUseCase = calendarUseCase - self.provider = provider } // MARK: - Transform diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPostCellReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPostCellReactor.swift index ea6e8a95b..471ad58b6 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPostCellReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPostCellReactor.swift @@ -39,20 +39,16 @@ public final class CalendarPostCellReactor: Reactor { // MARK: - Properties public var initialState: State - public let meUseCase: MemberUseCaseProtocol - public let provider: GlobalStateProviderProtocol + @Injected var meUseCase: MemberUseCaseProtocol + @Injected var provider: GlobalStateProviderProtocol // MARK: - Intializer public init( - post: DailyCalendarEntity, - meUseCase: MemberUseCaseProtocol, - provider: GlobalStateProviderProtocol + post: DailyCalendarEntity ) { self.initialState = State( post: post ) - self.meUseCase = meUseCase - self.provider = provider } // MARK: - Mutate diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/DailyCalendarViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/DailyCalendarViewReactor.swift index 3d94098dc..257e0c8b9 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/DailyCalendarViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/DailyCalendarViewReactor.swift @@ -59,10 +59,10 @@ public final class DailyCalendarViewReactor: Reactor { } // MARK: - Properties - public var initialState: State + @Injected var provider: GlobalStateProviderProtocol + @Injected var calendarUseCase: CalendarUseCaseProtocol - public let provider: GlobalStateProviderProtocol - private let calendarUseCase: CalendarUseCaseProtocol + public var initialState: State private var hasReceivedPostEvent: Bool = false private var hasReceivedSelectionEvent: Bool = false @@ -72,9 +72,7 @@ public final class DailyCalendarViewReactor: Reactor { // MARK: - Intializer init( date: Date, - notificationDeepLink deepLink: NotificationDeepLink?, - calendarUseCase: CalendarUseCaseProtocol, - provider: GlobalStateProviderProtocol + notificationDeepLink deepLink: NotificationDeepLink? ) { self.initialState = State( date: date, @@ -86,9 +84,6 @@ public final class DailyCalendarViewReactor: Reactor { shouldPopViewController: false, notificationDeepLink: deepLink ) - - self.calendarUseCase = calendarUseCase - self.provider = provider } // MARK: - Transfor diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/MonthlyCalendarViewReactor.swift similarity index 98% rename from 14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarViewReactor.swift rename to 14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/MonthlyCalendarViewReactor.swift index 83b987998..523af4591 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/MonthlyCalendarViewReactor.swift @@ -13,7 +13,7 @@ import Domain import ReactorKit import RxSwift -public final class CalendarViewReactor: Reactor { +public final class MonthlyCalendarViewReactor: Reactor { // MARK: - Action public enum Action { case popViewController diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/MonthlyCalendarViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/MonthlyCalendarViewController.swift index cb079484f..f87b35029 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/MonthlyCalendarViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/MonthlyCalendarViewController.swift @@ -17,7 +17,7 @@ import SnapKit import Then fileprivate typealias _Str = CalendarStrings -public final class MonthlyCalendarViewController: BaseViewController { +public final class MonthlyCalendarViewController: BaseViewController { // MARK: - Views private lazy var calendarCollectionView: UICollectionView = UICollectionView( frame: .zero, @@ -33,13 +33,13 @@ public final class MonthlyCalendarViewController: BaseViewController.just(()) .delay(RxConst.milliseconds100Interval, scheduler: RxSchedulers.main) .bind(with: self) { owner, _ in @@ -73,7 +73,7 @@ public final class MonthlyCalendarViewController: BaseViewController + + + + + + + + + + + + + + + + + + + + Date: Tue, 25 Jun 2024 11:18:09 +0900 Subject: [PATCH 137/263] =?UTF-8?q?feat:=20Management=20=EA=B4=80=EB=A0=A8?= =?UTF-8?q?=20Wrapper=20=EA=B5=AC=ED=98=84=20(#570)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Management/ManagementViewController.swift | 38 +++++++++++++++++++ .../FamilyManagementDIContainer.swift | 16 ++------ .../InvitationUrlContainerDIContainer.swift | 1 + .../Reactor/FamilyManagementViewReactor.swift | 15 ++------ 4 files changed, 46 insertions(+), 24 deletions(-) create mode 100644 14th-team5-iOS/App/Sources/Application/Wrapper/Management/ManagementViewController.swift diff --git a/14th-team5-iOS/App/Sources/Application/Wrapper/Management/ManagementViewController.swift b/14th-team5-iOS/App/Sources/Application/Wrapper/Management/ManagementViewController.swift new file mode 100644 index 000000000..fd1ae6c64 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/Wrapper/Management/ManagementViewController.swift @@ -0,0 +1,38 @@ +// +// ManagementViewController.swift +// App +// +// Created by 김건우 on 6/25/24. +// + +import Core +import Foundation + +final class ManagementViewController: BaseWrapper { + + // MARK: - Typealias + + typealias R = FamilyManagementViewReactor + typealias V = FamilyManagementViewController + + // MARK: - Properties + + var reactor: FamilyManagementViewReactor { + makeReactor() + } + + var viewController: FamilyManagementViewController { + makeViewController() + } + + // MARK: - Make + + func makeReactor() -> FamilyManagementViewReactor { + FamilyManagementViewReactor() + } + + func makeViewController() -> FamilyManagementViewController { + FamilyManagementViewController(reactor: makeReactor()) + } + +} diff --git a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/Dependency/FamilyManagementDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/Dependency/FamilyManagementDIContainer.swift index 3c77ecab5..8850dfba3 100644 --- a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/Dependency/FamilyManagementDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/Dependency/FamilyManagementDIContainer.swift @@ -11,15 +11,9 @@ import Core import Data import Domain +@available(*, deprecated, renamed: "ManagementViewControllerWrapper") public final class FamilyManagementDIContainer { - // MARK: - Properties - private var globalState: GlobalStateProviderProtocol { - guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { - return GlobalStateProvider() - } - return appDelegate.globalStateProvider - } - + // MARK: - Make public func makeViewController() -> FamilyManagementViewController { return FamilyManagementViewController(reactor: makeReactor()) @@ -42,10 +36,6 @@ public final class FamilyManagementDIContainer { } public func makeReactor() -> FamilyManagementViewReactor { - return FamilyManagementViewReactor( - memberUseCase: makeMemberUseCase(), - familyUseCase: makeFamilyUseCase(), - provider: globalState - ) + return FamilyManagementViewReactor() } } diff --git a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/Dependency/InvitationUrlContainerDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/Dependency/InvitationUrlContainerDIContainer.swift index d05a84f2b..b8267ba64 100644 --- a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/Dependency/InvitationUrlContainerDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/Dependency/InvitationUrlContainerDIContainer.swift @@ -9,6 +9,7 @@ import Core import Foundation import UIKit +@available(*, deprecated) public final class InvitationUrlContainerDIContainer { // MARK: - Properties private var globalState: GlobalStateProviderProtocol { diff --git a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/Reactor/FamilyManagementViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/Reactor/FamilyManagementViewReactor.swift index efd79e97b..94b657718 100644 --- a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/Reactor/FamilyManagementViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/Reactor/FamilyManagementViewReactor.swift @@ -56,16 +56,12 @@ public final class FamilyManagementViewReactor: Reactor { // MARK: - Properties public let initialState: State - public let memberUseCase: MemberUseCaseProtocol - public let familyUseCase: FamilyUseCaseProtocol - public let provider: GlobalStateProviderProtocol + @Injected var memberUseCase: MemberUseCaseProtocol + @Injected var familyUseCase: FamilyUseCaseProtocol + @Injected var provider: GlobalStateProviderProtocol // MARK: - Intializer - init( - memberUseCase: MemberUseCaseProtocol, - familyUseCase: FamilyUseCaseProtocol, - provider: GlobalStateProviderProtocol - ) { + init() { self.initialState = State( familyId: nil, familyInvitationUrl: nil, @@ -81,9 +77,6 @@ public final class FamilyManagementViewReactor: Reactor { displayFamilyMemberCount: 0 ) - self.memberUseCase = memberUseCase - self.familyUseCase = familyUseCase - self.provider = provider } // MARK: - Transform From 87228d8732a35c4d2db04916546bdbcb596f89de Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Tue, 25 Jun 2024 13:49:41 +0900 Subject: [PATCH 138/263] =?UTF-8?q?feat:=20Comment=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?Wrapper=20=EA=B5=AC=ED=98=84=20(#570)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CommentViewControllerWrapper.swift | 46 +++++++++++++++++++ .../Dependency/PostCommentDIContainer.swift | 6 +-- .../Reactor/CommentViewReactor.swift | 17 ++----- .../Comment/CommentAPI/CommentAPIWorker.swift | 11 ++++- 4 files changed, 61 insertions(+), 19 deletions(-) create mode 100644 14th-team5-iOS/App/Sources/Application/Wrapper/Comment/CommentViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/Wrapper/Comment/CommentViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Wrapper/Comment/CommentViewControllerWrapper.swift new file mode 100644 index 000000000..d145a51cf --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/Wrapper/Comment/CommentViewControllerWrapper.swift @@ -0,0 +1,46 @@ +// +// CommentViewControllerWrapper.swift +// App +// +// Created by 김건우 on 6/25/24. +// + +import Core +import Foundation + +final class CommentViewControllerWrapper: BaseWrapper { + + // MARK: - Typealias + + typealias R = CommentViewReactor + typealias V = PostCommentViewController + + // MARK: - Properties + + let postId: String + + var reactor: CommentViewReactor { + makeReactor() + } + + var viewController: PostCommentViewController { + makeViewController() + } + + // MARK: - Intializer + + init(postId: String) { + self.postId = postId + } + + // MARK: - Make + + func makeReactor() -> CommentViewReactor { + CommentViewReactor(postId: postId) + } + + func makeViewController() -> PostCommentViewController { + PostCommentViewController(reactor: makeReactor()) + } + +} diff --git a/14th-team5-iOS/App/Sources/Presentation/PostComment/Dependency/PostCommentDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/PostComment/Dependency/PostCommentDIContainer.swift index a4df1ba8a..64c5fce2a 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostComment/Dependency/PostCommentDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostComment/Dependency/PostCommentDIContainer.swift @@ -10,6 +10,7 @@ import Data import Domain import UIKit +@available(*, deprecated, renamed: "CommentViewControllerWrapper") public final class PostCommentDIContainer { // MARK: - Properties private var globalState: GlobalStateProviderProtocol { @@ -49,10 +50,7 @@ public final class PostCommentDIContainer { public func makeReactor() -> CommentViewReactor { return CommentViewReactor( - postId: postId, - memberUseCase: makeMemberUseCase(), - postCommentUseCase: makePostCommentUseCase(), - provider: globalState + postId: postId ) } } diff --git a/14th-team5-iOS/App/Sources/Presentation/PostComment/Reactor/CommentViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/PostComment/Reactor/CommentViewReactor.swift index d06f221c9..087c447d0 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostComment/Reactor/CommentViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostComment/Reactor/CommentViewReactor.swift @@ -70,20 +70,15 @@ final public class CommentViewReactor: Reactor { // MARK: - Properties public var initialState: State - public var memberUseCase: MemberUseCaseProtocol - public var postCommentUseCase: PostCommentUseCaseProtocol - public var provider: GlobalStateProviderProtocol + @Injected var memberUseCase: MemberUseCaseProtocol + @Injected var postCommentUseCase: PostCommentUseCaseProtocol + @Injected var provider: GlobalStateProviderProtocol private var postComentCount: Int = 0 private var hasReceivedInputEvent: Bool = false // MARK: - Intializer - public init( - postId: String, - memberUseCase: MemberUseCaseProtocol, - postCommentUseCase: PostCommentUseCaseProtocol, - provider: GlobalStateProviderProtocol - ) { + public init(postId: String) { self.initialState = State( postId: postId, commentCount: 0, @@ -103,10 +98,6 @@ final public class CommentViewReactor: Reactor { enableCommentTextField: false, tableViewBottomOffset: 0 ) - - self.memberUseCase = memberUseCase - self.postCommentUseCase = postCommentUseCase - self.provider = provider } // MARK: - Transform 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 ad71f37ca..46150179b 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/CommentAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/CommentAPIWorker.swift @@ -62,8 +62,15 @@ extension CommentAPIWorker { body: CreatePostCommentReqeustDTO ) -> Single { let spec = CommentAPIs.createPostComment(postId).spec - - return request(spec: spec, jsonEncodable: body) + 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) .do { if let str = String(data: $0.1, encoding: .utf8) { From 5b9e9b9201e9b4d629907a922a893339cb35f5d4 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Tue, 25 Jun 2024 14:19:14 +0900 Subject: [PATCH 139/263] =?UTF-8?q?move:=20=ED=95=84=EC=9A=94=EC=97=86?= =?UTF-8?q?=EB=8A=94=20=ED=8C=8C=EC=9D=BC=20Trash=EB=A1=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99=20(#576)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/FamilyWidget/FamilyWidgetEntry.swift | 2 +- .../Sources/FamilyWidget/FamilyWidgetView.swift | 2 +- .../BBNavigationBarView/BBNavigationBarView.swift | 10 ++++++++-- .../APIs/Widget/Repository/WidgetRepository.swift | 2 +- .../DataMaaping/RecentFamilyPostResponseDTO.swift | 2 +- .../APIs/Widget/WidgetAPI/WidgetAPIWorker.swift | 2 +- .../Account/AccountAPI/AccountAPIWorker.swift | 0 .../{ => Trash}/Account/AccountAPI/AccountAPIs.swift | 0 .../Account/AccountAPI/AccountSignInAPIConfig.swift | 0 .../Account/AccountAPI/AccountSignInHelper.swift | 0 .../AccountAPI/Helpers/AccountSignInHelperType.swift | 0 .../Helpers/Apple/AccountAppleSignIn+Rx.swift | 0 .../Helpers/Apple/AccountAppleSignInHelper.swift | 0 .../Helpers/Kakao/AccountKakaoSignIn+Rx.swift | 0 .../Helpers/Kakao/AccountKakaoSignInHelper.swift | 0 .../{ => Trash}/Account/AccountAPI/VoidResponse.swift | 0 .../Account/AccountRepository/AccountRepository.swift | 0 .../{ => Trash}/Account/MeAPI/MeAPIWorker.swift | 0 .../Sources/{ => Trash}/Account/MeAPI/MeAPIs.swift | 0 .../{ => Trash}/Camera/CameraAPI/CameraAPIWorker.swift | 0 .../{ => Trash}/Camera/CameraAPI/CameraAPIs.swift | 0 .../DataMapping/CameraCreateRealEmojiResponseDTO.swift | 0 .../DataMapping/CameraDisplayImageResponseDTO.swift | 0 .../DataMapping/CameraDisplayPostResponseDTO.swift | 0 .../CameraRealEmojiImageItemResponseDTO.swift | 0 .../CameraRealEmojiPreSignedResponseDTO.swift | 0 .../DataMapping/CameraTodayMissionResponseDTO.swift | 0 .../DataMapping/CameraUpdateRealEmojiResponseDTO.swift | 0 .../Camera/Repository/CameraRepository.swift | 0 .../{ => Trash}/Keychain/KeychainRepository.swift | 0 .../Member/Repository/MemberRepository.swift | 0 .../DataMapping/MembersProfileResponseDTO.swift | 0 .../Members/MemberAPI/MembersAPIWorker.swift | 0 .../{ => Trash}/Members/MemberAPI/MembersAPIs.swift | 0 .../Members/Repositories/MembersRepository.swift | 0 .../DataMapping/MissionContentResponseDTO.swift | 0 .../Mission/MissionAPI/MissionAPIWorker.swift | 0 .../{ => Trash}/Mission/MissionAPI/MissionAPIs.swift | 0 .../Mission/Repository/MissionRepository.swift | 0 .../Post/PostList/DataMapping/PostListRequestDTO.swift | 0 .../PostList/DataMapping/PostListResponseDTO.swift | 0 .../Post/PostList/DataMapping/PostRequestDTO.swift | 0 .../Post/PostList/DataMapping/PostResponseDTO.swift | 0 .../{ => Trash}/Post/PostList/PostAPI/PostAPIs.swift | 0 .../Post/PostList/PostAPI/PostListAPIWorker.swift | 0 .../Post/PostList/Repository/PostRepository.swift | 0 .../Privacy/DataMapping/BibbiAppInfoDTO.swift | 0 .../Privacy/PrivacyAPI/PrivacyAPIWorker.swift | 0 .../{ => Trash}/Privacy/PrivacyAPI/PrivacyAPIs.swift | 0 .../Privacy/Repositories/PrivacyViewRepository.swift | 0 .../Resign/DataMapping/AccountFamilyResignDTO.swift | 0 .../Resign/DataMapping/AccountFcmResignDTO.swift | 0 .../Resign/DataMapping/AccountResignDTO.swift | 0 .../Repositories/AccountResignViewRepository.swift | 0 .../{ => Trash}/Resign/ResignAPI/ResignAPIWorker.swift | 0 .../{ => Trash}/Resign/ResignAPI/ResignAPIs.swift | 0 .../{ => Trash}/UserDefaults/FamilyUserDefautls.swift | 0 .../{ => Trash}/UserDefaults/PostUserDefaults.swift | 0 .../UserDefaults/UserDefaultsRepository.swift | 0 .../Widget/RecentFamilyPostEntity.swift} | 2 +- .../WidgetRepositoryProtocol.swift | 2 +- .../Sources/{ => Trash}/Account/AccessToken.swift | 0 .../{ => Trash}/Account/AccountSignInStateInfo.swift | 0 .../Account/DataMapping/AccountNickNameEditDTO.swift | 0 .../Account/DataMapping/AccountRefreshDTO.swift | 0 .../Account/Entity/AccountNickNameEditResponse.swift | 0 .../Account/Entity/AccountRefreshResponse.swift | 0 .../Sources/{ => Trash}/Account/FamilyInfo.swift | 0 .../Domain/Sources/{ => Trash}/Account/MeberInfo.swift | 0 .../Parameters/AccountNickNameEditParameter.swift | 0 .../Account/Parameters/AccountRefreshParameter.swift | 0 .../Domain/Sources/{ => Trash}/Account/SNS.swift | 0 .../Calendar/UseCases/CalendarUseCase.swift | 0 .../Camera/Entity/CameraCreateRealEmojiEntity.swift | 0 .../Camera/Entity/CameraMissionFeedQuery.swift | 0 .../{ => Trash}/Camera/Entity/CameraPostEntity.swift | 0 .../Camera/Entity/CameraPreSignedEntity.swift | 0 .../Camera/Entity/CameraRealEmojiImageItemEntity.swift | 0 .../Camera/Entity/CameraRealEmojiItems.swift | 0 .../Camera/Entity/CameraRealEmojiPreSignedEntity.swift | 0 .../Camera/Entity/CameraTodayMssionEntity.swift | 0 .../Camera/Entity/CameraUpdateRealEmojiEntity.swift | 0 .../Camera/Interfaces/CameraRepositoryProtocol.swift | 0 .../Parameters/CameraCreateRealEmojiParameters.swift | 0 .../Parameters/CameraDisplayImageParameters.swift | 0 .../Parameters/CameraDisplayPostParameters.swift | 0 .../Camera/Parameters/CameraRealEmojiParameters.swift | 0 .../Parameters/CameraUpdateRealEmojiParameters.swift | 0 .../UseCases/Camera/CreateCameraImageUseCase.swift | 0 .../Camera/UseCases/Camera/CreateCameraUseCase.swift | 0 .../Camera/FetchCameraUploadImageUseCase.swift | 0 .../Mission/FetchCameraTodayMissionUseCase.swift | 0 .../Profile/EditCameraProfileImageUseCase.swift | 0 .../RealEmoji/FetchCameraRealEmojiListUseCase.swift | 0 .../RealEmoji/FetchCameraRealEmojiUpdateUseCase.swift | 0 .../RealEmoji/FetchCameraRealEmojiUploadUseCase.swift | 0 .../RealEmoji/FetchCameraRealEmojiUseCase.swift | 0 .../Sources/{ => Trash}/FCM/Entities/FCMToken.swift | 0 .../Sources/{ => Trash}/FCM/UseCases/FCMUseCase.swift | 0 .../Interfaces/Repositories/JoinFamilyRepository.swift | 0 .../{ => Trash}/Family/UseCases/FamilyUseCase.swift | 0 .../{ => Trash}/Interfaces/FCMRepositoryProtocol.swift | 0 .../Interfaces/KeychainRepositoryProtocol.swift | 0 .../Interfaces/UserDefaultsRepositoryProtocol.swift | 0 .../Sources/{ => Trash}/Me/MeRepositoryProtocol.swift | 0 .../Domain/Sources/{ => Trash}/Me/MeUseCase.swift | 0 .../Interfaces/Repositories/MemberRepository.swift | 0 .../{ => Trash}/Member/UseCases/MemberUseCase.swift | 0 .../Members/Entities/MembersProfileEntity.swift | 0 .../Members/Interfaces/MembersRepositoryProtocol.swift | 0 .../Members/Parameters/ProfileImageEditParameter.swift | 0 .../UseCases/Members/DeleteMembersProfileUseCase.swift | 0 .../UseCases/Members/FetchMembersPostListUseCase.swift | 0 .../UseCases/Members/FetchMembersProfileUseCase.swift | 0 .../UseCases/Members/UpdateMembersProfileUseCase.swift | 0 .../Mission/Entities/MissionContentEntity.swift | 0 .../Mission/Interfaces/MissionRepositoryProtocol.swift | 0 .../Mission/CheckMissionAlertShowUseCase.swift | 0 .../UseCases/Mission/FetchMissionContentUseCase.swift | 0 .../{ => Trash}/Pick/UseCases/PickUseCase.swift | 0 .../PostComment/UseCase/PostCommentUseCase.swift | 0 .../Privacy/Entity/BibbiAppInfoResponse.swift | 0 .../Privacy/Interfaces/PrivacyViewInterface.swift | 0 .../Privacy/Parameters/BibbiAppInfoParameter.swift | 0 .../Domain/Sources/{ => Trash}/Privacy/Privacy.swift | 0 .../Privacy/UseCases/PrivacyViewUseCase.swift | 0 .../Resign/Entity/AccountFamilyResignResponse.swift | 0 .../Resign/Entity/AccountFcmResignResponse.swift | 0 .../Resign/Entity/AccountResignResponse.swift | 0 .../Resign/Interfaces/AccountResignInterface.swift | 0 .../Resign/UseCases/AccountResignUseCase.swift | 0 .../{ => Trash}/SignOut/UseCases/SignOutUseCase.swift | 0 .../Widget}/FetchRecentFamilyPostUseCase.swift | 4 ++-- 133 files changed, 17 insertions(+), 11 deletions(-) rename 14th-team5-iOS/Data/Sources/{ => Trash}/Account/AccountAPI/AccountAPIWorker.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => Trash}/Account/AccountAPI/AccountAPIs.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => Trash}/Account/AccountAPI/AccountSignInAPIConfig.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => Trash}/Account/AccountAPI/AccountSignInHelper.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => Trash}/Account/AccountAPI/Helpers/AccountSignInHelperType.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => Trash}/Account/AccountAPI/Helpers/Apple/AccountAppleSignIn+Rx.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => Trash}/Account/AccountAPI/Helpers/Apple/AccountAppleSignInHelper.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => Trash}/Account/AccountAPI/Helpers/Kakao/AccountKakaoSignIn+Rx.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => Trash}/Account/AccountAPI/Helpers/Kakao/AccountKakaoSignInHelper.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => Trash}/Account/AccountAPI/VoidResponse.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => Trash}/Account/AccountRepository/AccountRepository.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => Trash}/Account/MeAPI/MeAPIWorker.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => Trash}/Account/MeAPI/MeAPIs.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => Trash}/Camera/CameraAPI/CameraAPIWorker.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => Trash}/Camera/CameraAPI/CameraAPIs.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => Trash}/Camera/DataMapping/CameraCreateRealEmojiResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => Trash}/Camera/DataMapping/CameraDisplayImageResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => Trash}/Camera/DataMapping/CameraDisplayPostResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => Trash}/Camera/DataMapping/CameraRealEmojiImageItemResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => Trash}/Camera/DataMapping/CameraRealEmojiPreSignedResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => Trash}/Camera/DataMapping/CameraTodayMissionResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => Trash}/Camera/DataMapping/CameraUpdateRealEmojiResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => Trash}/Camera/Repository/CameraRepository.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => Trash}/Keychain/KeychainRepository.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => Trash}/Member/Repository/MemberRepository.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => Trash}/Members/DataMapping/MembersProfileResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => Trash}/Members/MemberAPI/MembersAPIWorker.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => Trash}/Members/MemberAPI/MembersAPIs.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => Trash}/Members/Repositories/MembersRepository.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => Trash}/Mission/DataMapping/MissionContentResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => Trash}/Mission/MissionAPI/MissionAPIWorker.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => Trash}/Mission/MissionAPI/MissionAPIs.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => Trash}/Mission/Repository/MissionRepository.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => Trash}/Post/PostList/DataMapping/PostListRequestDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => Trash}/Post/PostList/DataMapping/PostListResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => Trash}/Post/PostList/DataMapping/PostRequestDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => Trash}/Post/PostList/DataMapping/PostResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => Trash}/Post/PostList/PostAPI/PostAPIs.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => Trash}/Post/PostList/PostAPI/PostListAPIWorker.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => Trash}/Post/PostList/Repository/PostRepository.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => Trash}/Privacy/DataMapping/BibbiAppInfoDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => Trash}/Privacy/PrivacyAPI/PrivacyAPIWorker.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => Trash}/Privacy/PrivacyAPI/PrivacyAPIs.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => Trash}/Privacy/Repositories/PrivacyViewRepository.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => Trash}/Resign/DataMapping/AccountFamilyResignDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => Trash}/Resign/DataMapping/AccountFcmResignDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => Trash}/Resign/DataMapping/AccountResignDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => Trash}/Resign/Repositories/AccountResignViewRepository.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => Trash}/Resign/ResignAPI/ResignAPIWorker.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => Trash}/Resign/ResignAPI/ResignAPIs.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => Trash}/UserDefaults/FamilyUserDefautls.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => Trash}/UserDefaults/PostUserDefaults.swift (100%) rename 14th-team5-iOS/Data/Sources/{ => Trash}/UserDefaults/UserDefaultsRepository.swift (100%) rename 14th-team5-iOS/Domain/Sources/{Widget/Entities/RecentFamilyPostData.swift => Entities/Widget/RecentFamilyPostEntity.swift} (93%) rename 14th-team5-iOS/Domain/Sources/{Widget/Interfaces => Repositories}/WidgetRepositoryProtocol.swift (85%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Account/AccessToken.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Account/AccountSignInStateInfo.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Account/DataMapping/AccountNickNameEditDTO.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Account/DataMapping/AccountRefreshDTO.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Account/Entity/AccountNickNameEditResponse.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Account/Entity/AccountRefreshResponse.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Account/FamilyInfo.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Account/MeberInfo.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Account/Parameters/AccountNickNameEditParameter.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Account/Parameters/AccountRefreshParameter.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Account/SNS.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Calendar/UseCases/CalendarUseCase.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Camera/Entity/CameraCreateRealEmojiEntity.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Camera/Entity/CameraMissionFeedQuery.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Camera/Entity/CameraPostEntity.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Camera/Entity/CameraPreSignedEntity.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Camera/Entity/CameraRealEmojiImageItemEntity.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Camera/Entity/CameraRealEmojiItems.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Camera/Entity/CameraRealEmojiPreSignedEntity.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Camera/Entity/CameraTodayMssionEntity.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Camera/Entity/CameraUpdateRealEmojiEntity.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Camera/Interfaces/CameraRepositoryProtocol.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Camera/Parameters/CameraCreateRealEmojiParameters.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Camera/Parameters/CameraDisplayImageParameters.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Camera/Parameters/CameraDisplayPostParameters.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Camera/Parameters/CameraRealEmojiParameters.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Camera/Parameters/CameraUpdateRealEmojiParameters.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Camera/UseCases/Camera/CreateCameraImageUseCase.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Camera/UseCases/Camera/CreateCameraUseCase.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Camera/UseCases/Camera/FetchCameraUploadImageUseCase.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Camera/UseCases/Mission/FetchCameraTodayMissionUseCase.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Camera/UseCases/Profile/EditCameraProfileImageUseCase.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Camera/UseCases/RealEmoji/FetchCameraRealEmojiListUseCase.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Camera/UseCases/RealEmoji/FetchCameraRealEmojiUpdateUseCase.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Camera/UseCases/RealEmoji/FetchCameraRealEmojiUploadUseCase.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Camera/UseCases/RealEmoji/FetchCameraRealEmojiUseCase.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/FCM/Entities/FCMToken.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/FCM/UseCases/FCMUseCase.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Family/Interfaces/Repositories/JoinFamilyRepository.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Family/UseCases/FamilyUseCase.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Interfaces/FCMRepositoryProtocol.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Interfaces/KeychainRepositoryProtocol.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Interfaces/UserDefaultsRepositoryProtocol.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Me/MeRepositoryProtocol.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Me/MeUseCase.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Member/Interfaces/Repositories/MemberRepository.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Member/UseCases/MemberUseCase.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Members/Entities/MembersProfileEntity.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Members/Interfaces/MembersRepositoryProtocol.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Members/Parameters/ProfileImageEditParameter.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Members/UseCases/Members/DeleteMembersProfileUseCase.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Members/UseCases/Members/FetchMembersPostListUseCase.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Members/UseCases/Members/FetchMembersProfileUseCase.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Members/UseCases/Members/UpdateMembersProfileUseCase.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Mission/Entities/MissionContentEntity.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Mission/Interfaces/MissionRepositoryProtocol.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Mission/UseCases/Mission/CheckMissionAlertShowUseCase.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Mission/UseCases/Mission/FetchMissionContentUseCase.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Pick/UseCases/PickUseCase.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/PostComment/UseCase/PostCommentUseCase.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Privacy/Entity/BibbiAppInfoResponse.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Privacy/Interfaces/PrivacyViewInterface.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Privacy/Parameters/BibbiAppInfoParameter.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Privacy/Privacy.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Privacy/UseCases/PrivacyViewUseCase.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Resign/Entity/AccountFamilyResignResponse.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Resign/Entity/AccountFcmResignResponse.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Resign/Entity/AccountResignResponse.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Resign/Interfaces/AccountResignInterface.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/Resign/UseCases/AccountResignUseCase.swift (100%) rename 14th-team5-iOS/Domain/Sources/{ => Trash}/SignOut/UseCases/SignOutUseCase.swift (100%) rename 14th-team5-iOS/Domain/Sources/{Widget/UseCases => UseCases/Widget}/FetchRecentFamilyPostUseCase.swift (83%) diff --git a/14th-team5-iOS/App/WidgetExtension/Sources/FamilyWidget/FamilyWidgetEntry.swift b/14th-team5-iOS/App/WidgetExtension/Sources/FamilyWidget/FamilyWidgetEntry.swift index fdc2dbb54..bccb73f3f 100644 --- a/14th-team5-iOS/App/WidgetExtension/Sources/FamilyWidget/FamilyWidgetEntry.swift +++ b/14th-team5-iOS/App/WidgetExtension/Sources/FamilyWidget/FamilyWidgetEntry.swift @@ -10,5 +10,5 @@ import Domain struct FamilyWidgetEntry: TimelineEntry { let date: Date - let family: RecentFamilyPostData? + let family: RecentFamilyPostEntity? } diff --git a/14th-team5-iOS/App/WidgetExtension/Sources/FamilyWidget/FamilyWidgetView.swift b/14th-team5-iOS/App/WidgetExtension/Sources/FamilyWidget/FamilyWidgetView.swift index 6e0292a29..a3896d0e8 100644 --- a/14th-team5-iOS/App/WidgetExtension/Sources/FamilyWidget/FamilyWidgetView.swift +++ b/14th-team5-iOS/App/WidgetExtension/Sources/FamilyWidget/FamilyWidgetView.swift @@ -153,7 +153,7 @@ struct FamilyWidgetView: View { } // MARK: 가족중 일부가 사진을 올렸을 때 뷰 - private func getPhotoView(info: RecentFamilyPostData) -> some View { + private func getPhotoView(info: RecentFamilyPostEntity) -> some View { ZStack { if let postImageUrl = info.postImageUrl { diff --git a/14th-team5-iOS/Core/Sources/BBCommons/BBNavigationBarView/BBNavigationBarView.swift b/14th-team5-iOS/Core/Sources/BBCommons/BBNavigationBarView/BBNavigationBarView.swift index 3c296c972..f38046ab3 100644 --- a/14th-team5-iOS/Core/Sources/BBCommons/BBNavigationBarView/BBNavigationBarView.swift +++ b/14th-team5-iOS/Core/Sources/BBCommons/BBNavigationBarView/BBNavigationBarView.swift @@ -5,12 +5,11 @@ // Created by 김건우 on 6/5/24. // +import DesignSystem import UIKit -import DesignSystem import Then import SnapKit - import RxSwift import RxCocoa @@ -41,6 +40,13 @@ public final class BBNavigationBarView: UIView { } } + // NavigationBar의 FontStyle을 바꿉니다. + public var navigationTitleFontStyle: BibbiFontStyle = .head2Bold { + didSet { + navigationTitleLabel.fontStyle = navigationTitleFontStyle + } + } + /// NavigationBar의 Image를 바꿉니다. /// Image를 적용하면 Title이 사라집니다. public var navigationImage: TopBarButtonStyle? { diff --git a/14th-team5-iOS/Data/Sources/APIs/Widget/Repository/WidgetRepository.swift b/14th-team5-iOS/Data/Sources/APIs/Widget/Repository/WidgetRepository.swift index 84d4bb126..75dd5ebce 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Widget/Repository/WidgetRepository.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Widget/Repository/WidgetRepository.swift @@ -18,7 +18,7 @@ public final class WidgetRepository: WidgetRepositoryProtocol { public init () { } - public func fetchRecentFamilyPost(completion: @escaping (Result) -> Void) { + public func fetchRecentFamilyPost(completion: @escaping (Result) -> Void) { widgetAPIWorker.fetchRecentFamilyPost() .subscribe( onNext: { result in diff --git a/14th-team5-iOS/Data/Sources/APIs/Widget/WidgetAPI/DataMaaping/RecentFamilyPostResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Widget/WidgetAPI/DataMaaping/RecentFamilyPostResponseDTO.swift index 70ddf29e9..cbd8c5076 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Widget/WidgetAPI/DataMaaping/RecentFamilyPostResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Widget/WidgetAPI/DataMaaping/RecentFamilyPostResponseDTO.swift @@ -16,7 +16,7 @@ struct RecentFamilyPostResponseDTO: Codable { let postImageUrl: String? let postContent: String? - func toDomain() -> RecentFamilyPostData { + func toDomain() -> RecentFamilyPostEntity { return .init(authorName: authorName, authorProfileImageUrl: authorProfileImageUrl, postId: postId, diff --git a/14th-team5-iOS/Data/Sources/APIs/Widget/WidgetAPI/WidgetAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/Widget/WidgetAPI/WidgetAPIWorker.swift index f60ca358c..95a881fe2 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Widget/WidgetAPI/WidgetAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Widget/WidgetAPI/WidgetAPIWorker.swift @@ -36,7 +36,7 @@ extension WidgetAPIs { } extension WidgetAPIWorker { - func fetchRecentFamilyPost() -> Observable { + func fetchRecentFamilyPost() -> Observable { let spec = WidgetAPIs.fetchRecentFamilyPost.spec let parameters = RecentFamilyPostParameter() diff --git a/14th-team5-iOS/Data/Sources/Account/AccountAPI/AccountAPIWorker.swift b/14th-team5-iOS/Data/Sources/Trash/Account/AccountAPI/AccountAPIWorker.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Account/AccountAPI/AccountAPIWorker.swift rename to 14th-team5-iOS/Data/Sources/Trash/Account/AccountAPI/AccountAPIWorker.swift diff --git a/14th-team5-iOS/Data/Sources/Account/AccountAPI/AccountAPIs.swift b/14th-team5-iOS/Data/Sources/Trash/Account/AccountAPI/AccountAPIs.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Account/AccountAPI/AccountAPIs.swift rename to 14th-team5-iOS/Data/Sources/Trash/Account/AccountAPI/AccountAPIs.swift diff --git a/14th-team5-iOS/Data/Sources/Account/AccountAPI/AccountSignInAPIConfig.swift b/14th-team5-iOS/Data/Sources/Trash/Account/AccountAPI/AccountSignInAPIConfig.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Account/AccountAPI/AccountSignInAPIConfig.swift rename to 14th-team5-iOS/Data/Sources/Trash/Account/AccountAPI/AccountSignInAPIConfig.swift diff --git a/14th-team5-iOS/Data/Sources/Account/AccountAPI/AccountSignInHelper.swift b/14th-team5-iOS/Data/Sources/Trash/Account/AccountAPI/AccountSignInHelper.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Account/AccountAPI/AccountSignInHelper.swift rename to 14th-team5-iOS/Data/Sources/Trash/Account/AccountAPI/AccountSignInHelper.swift diff --git a/14th-team5-iOS/Data/Sources/Account/AccountAPI/Helpers/AccountSignInHelperType.swift b/14th-team5-iOS/Data/Sources/Trash/Account/AccountAPI/Helpers/AccountSignInHelperType.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Account/AccountAPI/Helpers/AccountSignInHelperType.swift rename to 14th-team5-iOS/Data/Sources/Trash/Account/AccountAPI/Helpers/AccountSignInHelperType.swift diff --git a/14th-team5-iOS/Data/Sources/Account/AccountAPI/Helpers/Apple/AccountAppleSignIn+Rx.swift b/14th-team5-iOS/Data/Sources/Trash/Account/AccountAPI/Helpers/Apple/AccountAppleSignIn+Rx.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Account/AccountAPI/Helpers/Apple/AccountAppleSignIn+Rx.swift rename to 14th-team5-iOS/Data/Sources/Trash/Account/AccountAPI/Helpers/Apple/AccountAppleSignIn+Rx.swift diff --git a/14th-team5-iOS/Data/Sources/Account/AccountAPI/Helpers/Apple/AccountAppleSignInHelper.swift b/14th-team5-iOS/Data/Sources/Trash/Account/AccountAPI/Helpers/Apple/AccountAppleSignInHelper.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Account/AccountAPI/Helpers/Apple/AccountAppleSignInHelper.swift rename to 14th-team5-iOS/Data/Sources/Trash/Account/AccountAPI/Helpers/Apple/AccountAppleSignInHelper.swift diff --git a/14th-team5-iOS/Data/Sources/Account/AccountAPI/Helpers/Kakao/AccountKakaoSignIn+Rx.swift b/14th-team5-iOS/Data/Sources/Trash/Account/AccountAPI/Helpers/Kakao/AccountKakaoSignIn+Rx.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Account/AccountAPI/Helpers/Kakao/AccountKakaoSignIn+Rx.swift rename to 14th-team5-iOS/Data/Sources/Trash/Account/AccountAPI/Helpers/Kakao/AccountKakaoSignIn+Rx.swift diff --git a/14th-team5-iOS/Data/Sources/Account/AccountAPI/Helpers/Kakao/AccountKakaoSignInHelper.swift b/14th-team5-iOS/Data/Sources/Trash/Account/AccountAPI/Helpers/Kakao/AccountKakaoSignInHelper.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Account/AccountAPI/Helpers/Kakao/AccountKakaoSignInHelper.swift rename to 14th-team5-iOS/Data/Sources/Trash/Account/AccountAPI/Helpers/Kakao/AccountKakaoSignInHelper.swift diff --git a/14th-team5-iOS/Data/Sources/Account/AccountAPI/VoidResponse.swift b/14th-team5-iOS/Data/Sources/Trash/Account/AccountAPI/VoidResponse.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Account/AccountAPI/VoidResponse.swift rename to 14th-team5-iOS/Data/Sources/Trash/Account/AccountAPI/VoidResponse.swift diff --git a/14th-team5-iOS/Data/Sources/Account/AccountRepository/AccountRepository.swift b/14th-team5-iOS/Data/Sources/Trash/Account/AccountRepository/AccountRepository.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Account/AccountRepository/AccountRepository.swift rename to 14th-team5-iOS/Data/Sources/Trash/Account/AccountRepository/AccountRepository.swift diff --git a/14th-team5-iOS/Data/Sources/Account/MeAPI/MeAPIWorker.swift b/14th-team5-iOS/Data/Sources/Trash/Account/MeAPI/MeAPIWorker.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Account/MeAPI/MeAPIWorker.swift rename to 14th-team5-iOS/Data/Sources/Trash/Account/MeAPI/MeAPIWorker.swift diff --git a/14th-team5-iOS/Data/Sources/Account/MeAPI/MeAPIs.swift b/14th-team5-iOS/Data/Sources/Trash/Account/MeAPI/MeAPIs.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Account/MeAPI/MeAPIs.swift rename to 14th-team5-iOS/Data/Sources/Trash/Account/MeAPI/MeAPIs.swift diff --git a/14th-team5-iOS/Data/Sources/Camera/CameraAPI/CameraAPIWorker.swift b/14th-team5-iOS/Data/Sources/Trash/Camera/CameraAPI/CameraAPIWorker.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Camera/CameraAPI/CameraAPIWorker.swift rename to 14th-team5-iOS/Data/Sources/Trash/Camera/CameraAPI/CameraAPIWorker.swift diff --git a/14th-team5-iOS/Data/Sources/Camera/CameraAPI/CameraAPIs.swift b/14th-team5-iOS/Data/Sources/Trash/Camera/CameraAPI/CameraAPIs.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Camera/CameraAPI/CameraAPIs.swift rename to 14th-team5-iOS/Data/Sources/Trash/Camera/CameraAPI/CameraAPIs.swift diff --git a/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraCreateRealEmojiResponseDTO.swift b/14th-team5-iOS/Data/Sources/Trash/Camera/DataMapping/CameraCreateRealEmojiResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraCreateRealEmojiResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/Trash/Camera/DataMapping/CameraCreateRealEmojiResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraDisplayImageResponseDTO.swift b/14th-team5-iOS/Data/Sources/Trash/Camera/DataMapping/CameraDisplayImageResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraDisplayImageResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/Trash/Camera/DataMapping/CameraDisplayImageResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraDisplayPostResponseDTO.swift b/14th-team5-iOS/Data/Sources/Trash/Camera/DataMapping/CameraDisplayPostResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraDisplayPostResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/Trash/Camera/DataMapping/CameraDisplayPostResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraRealEmojiImageItemResponseDTO.swift b/14th-team5-iOS/Data/Sources/Trash/Camera/DataMapping/CameraRealEmojiImageItemResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraRealEmojiImageItemResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/Trash/Camera/DataMapping/CameraRealEmojiImageItemResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraRealEmojiPreSignedResponseDTO.swift b/14th-team5-iOS/Data/Sources/Trash/Camera/DataMapping/CameraRealEmojiPreSignedResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraRealEmojiPreSignedResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/Trash/Camera/DataMapping/CameraRealEmojiPreSignedResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraTodayMissionResponseDTO.swift b/14th-team5-iOS/Data/Sources/Trash/Camera/DataMapping/CameraTodayMissionResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraTodayMissionResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/Trash/Camera/DataMapping/CameraTodayMissionResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraUpdateRealEmojiResponseDTO.swift b/14th-team5-iOS/Data/Sources/Trash/Camera/DataMapping/CameraUpdateRealEmojiResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Camera/DataMapping/CameraUpdateRealEmojiResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/Trash/Camera/DataMapping/CameraUpdateRealEmojiResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/Camera/Repository/CameraRepository.swift b/14th-team5-iOS/Data/Sources/Trash/Camera/Repository/CameraRepository.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Camera/Repository/CameraRepository.swift rename to 14th-team5-iOS/Data/Sources/Trash/Camera/Repository/CameraRepository.swift diff --git a/14th-team5-iOS/Data/Sources/Keychain/KeychainRepository.swift b/14th-team5-iOS/Data/Sources/Trash/Keychain/KeychainRepository.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Keychain/KeychainRepository.swift rename to 14th-team5-iOS/Data/Sources/Trash/Keychain/KeychainRepository.swift diff --git a/14th-team5-iOS/Data/Sources/Member/Repository/MemberRepository.swift b/14th-team5-iOS/Data/Sources/Trash/Member/Repository/MemberRepository.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Member/Repository/MemberRepository.swift rename to 14th-team5-iOS/Data/Sources/Trash/Member/Repository/MemberRepository.swift diff --git a/14th-team5-iOS/Data/Sources/Members/DataMapping/MembersProfileResponseDTO.swift b/14th-team5-iOS/Data/Sources/Trash/Members/DataMapping/MembersProfileResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Members/DataMapping/MembersProfileResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/Trash/Members/DataMapping/MembersProfileResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/Members/MemberAPI/MembersAPIWorker.swift b/14th-team5-iOS/Data/Sources/Trash/Members/MemberAPI/MembersAPIWorker.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Members/MemberAPI/MembersAPIWorker.swift rename to 14th-team5-iOS/Data/Sources/Trash/Members/MemberAPI/MembersAPIWorker.swift diff --git a/14th-team5-iOS/Data/Sources/Members/MemberAPI/MembersAPIs.swift b/14th-team5-iOS/Data/Sources/Trash/Members/MemberAPI/MembersAPIs.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Members/MemberAPI/MembersAPIs.swift rename to 14th-team5-iOS/Data/Sources/Trash/Members/MemberAPI/MembersAPIs.swift diff --git a/14th-team5-iOS/Data/Sources/Members/Repositories/MembersRepository.swift b/14th-team5-iOS/Data/Sources/Trash/Members/Repositories/MembersRepository.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Members/Repositories/MembersRepository.swift rename to 14th-team5-iOS/Data/Sources/Trash/Members/Repositories/MembersRepository.swift diff --git a/14th-team5-iOS/Data/Sources/Mission/DataMapping/MissionContentResponseDTO.swift b/14th-team5-iOS/Data/Sources/Trash/Mission/DataMapping/MissionContentResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Mission/DataMapping/MissionContentResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/Trash/Mission/DataMapping/MissionContentResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/Mission/MissionAPI/MissionAPIWorker.swift b/14th-team5-iOS/Data/Sources/Trash/Mission/MissionAPI/MissionAPIWorker.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Mission/MissionAPI/MissionAPIWorker.swift rename to 14th-team5-iOS/Data/Sources/Trash/Mission/MissionAPI/MissionAPIWorker.swift diff --git a/14th-team5-iOS/Data/Sources/Mission/MissionAPI/MissionAPIs.swift b/14th-team5-iOS/Data/Sources/Trash/Mission/MissionAPI/MissionAPIs.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Mission/MissionAPI/MissionAPIs.swift rename to 14th-team5-iOS/Data/Sources/Trash/Mission/MissionAPI/MissionAPIs.swift diff --git a/14th-team5-iOS/Data/Sources/Mission/Repository/MissionRepository.swift b/14th-team5-iOS/Data/Sources/Trash/Mission/Repository/MissionRepository.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Mission/Repository/MissionRepository.swift rename to 14th-team5-iOS/Data/Sources/Trash/Mission/Repository/MissionRepository.swift diff --git a/14th-team5-iOS/Data/Sources/Post/PostList/DataMapping/PostListRequestDTO.swift b/14th-team5-iOS/Data/Sources/Trash/Post/PostList/DataMapping/PostListRequestDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Post/PostList/DataMapping/PostListRequestDTO.swift rename to 14th-team5-iOS/Data/Sources/Trash/Post/PostList/DataMapping/PostListRequestDTO.swift diff --git a/14th-team5-iOS/Data/Sources/Post/PostList/DataMapping/PostListResponseDTO.swift b/14th-team5-iOS/Data/Sources/Trash/Post/PostList/DataMapping/PostListResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Post/PostList/DataMapping/PostListResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/Trash/Post/PostList/DataMapping/PostListResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/Post/PostList/DataMapping/PostRequestDTO.swift b/14th-team5-iOS/Data/Sources/Trash/Post/PostList/DataMapping/PostRequestDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Post/PostList/DataMapping/PostRequestDTO.swift rename to 14th-team5-iOS/Data/Sources/Trash/Post/PostList/DataMapping/PostRequestDTO.swift diff --git a/14th-team5-iOS/Data/Sources/Post/PostList/DataMapping/PostResponseDTO.swift b/14th-team5-iOS/Data/Sources/Trash/Post/PostList/DataMapping/PostResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Post/PostList/DataMapping/PostResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/Trash/Post/PostList/DataMapping/PostResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/Post/PostList/PostAPI/PostAPIs.swift b/14th-team5-iOS/Data/Sources/Trash/Post/PostList/PostAPI/PostAPIs.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Post/PostList/PostAPI/PostAPIs.swift rename to 14th-team5-iOS/Data/Sources/Trash/Post/PostList/PostAPI/PostAPIs.swift diff --git a/14th-team5-iOS/Data/Sources/Post/PostList/PostAPI/PostListAPIWorker.swift b/14th-team5-iOS/Data/Sources/Trash/Post/PostList/PostAPI/PostListAPIWorker.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Post/PostList/PostAPI/PostListAPIWorker.swift rename to 14th-team5-iOS/Data/Sources/Trash/Post/PostList/PostAPI/PostListAPIWorker.swift diff --git a/14th-team5-iOS/Data/Sources/Post/PostList/Repository/PostRepository.swift b/14th-team5-iOS/Data/Sources/Trash/Post/PostList/Repository/PostRepository.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Post/PostList/Repository/PostRepository.swift rename to 14th-team5-iOS/Data/Sources/Trash/Post/PostList/Repository/PostRepository.swift diff --git a/14th-team5-iOS/Data/Sources/Privacy/DataMapping/BibbiAppInfoDTO.swift b/14th-team5-iOS/Data/Sources/Trash/Privacy/DataMapping/BibbiAppInfoDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Privacy/DataMapping/BibbiAppInfoDTO.swift rename to 14th-team5-iOS/Data/Sources/Trash/Privacy/DataMapping/BibbiAppInfoDTO.swift diff --git a/14th-team5-iOS/Data/Sources/Privacy/PrivacyAPI/PrivacyAPIWorker.swift b/14th-team5-iOS/Data/Sources/Trash/Privacy/PrivacyAPI/PrivacyAPIWorker.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Privacy/PrivacyAPI/PrivacyAPIWorker.swift rename to 14th-team5-iOS/Data/Sources/Trash/Privacy/PrivacyAPI/PrivacyAPIWorker.swift diff --git a/14th-team5-iOS/Data/Sources/Privacy/PrivacyAPI/PrivacyAPIs.swift b/14th-team5-iOS/Data/Sources/Trash/Privacy/PrivacyAPI/PrivacyAPIs.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Privacy/PrivacyAPI/PrivacyAPIs.swift rename to 14th-team5-iOS/Data/Sources/Trash/Privacy/PrivacyAPI/PrivacyAPIs.swift diff --git a/14th-team5-iOS/Data/Sources/Privacy/Repositories/PrivacyViewRepository.swift b/14th-team5-iOS/Data/Sources/Trash/Privacy/Repositories/PrivacyViewRepository.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Privacy/Repositories/PrivacyViewRepository.swift rename to 14th-team5-iOS/Data/Sources/Trash/Privacy/Repositories/PrivacyViewRepository.swift diff --git a/14th-team5-iOS/Data/Sources/Resign/DataMapping/AccountFamilyResignDTO.swift b/14th-team5-iOS/Data/Sources/Trash/Resign/DataMapping/AccountFamilyResignDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Resign/DataMapping/AccountFamilyResignDTO.swift rename to 14th-team5-iOS/Data/Sources/Trash/Resign/DataMapping/AccountFamilyResignDTO.swift diff --git a/14th-team5-iOS/Data/Sources/Resign/DataMapping/AccountFcmResignDTO.swift b/14th-team5-iOS/Data/Sources/Trash/Resign/DataMapping/AccountFcmResignDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Resign/DataMapping/AccountFcmResignDTO.swift rename to 14th-team5-iOS/Data/Sources/Trash/Resign/DataMapping/AccountFcmResignDTO.swift diff --git a/14th-team5-iOS/Data/Sources/Resign/DataMapping/AccountResignDTO.swift b/14th-team5-iOS/Data/Sources/Trash/Resign/DataMapping/AccountResignDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Resign/DataMapping/AccountResignDTO.swift rename to 14th-team5-iOS/Data/Sources/Trash/Resign/DataMapping/AccountResignDTO.swift diff --git a/14th-team5-iOS/Data/Sources/Resign/Repositories/AccountResignViewRepository.swift b/14th-team5-iOS/Data/Sources/Trash/Resign/Repositories/AccountResignViewRepository.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Resign/Repositories/AccountResignViewRepository.swift rename to 14th-team5-iOS/Data/Sources/Trash/Resign/Repositories/AccountResignViewRepository.swift diff --git a/14th-team5-iOS/Data/Sources/Resign/ResignAPI/ResignAPIWorker.swift b/14th-team5-iOS/Data/Sources/Trash/Resign/ResignAPI/ResignAPIWorker.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Resign/ResignAPI/ResignAPIWorker.swift rename to 14th-team5-iOS/Data/Sources/Trash/Resign/ResignAPI/ResignAPIWorker.swift diff --git a/14th-team5-iOS/Data/Sources/Resign/ResignAPI/ResignAPIs.swift b/14th-team5-iOS/Data/Sources/Trash/Resign/ResignAPI/ResignAPIs.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Resign/ResignAPI/ResignAPIs.swift rename to 14th-team5-iOS/Data/Sources/Trash/Resign/ResignAPI/ResignAPIs.swift diff --git a/14th-team5-iOS/Data/Sources/UserDefaults/FamilyUserDefautls.swift b/14th-team5-iOS/Data/Sources/Trash/UserDefaults/FamilyUserDefautls.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/UserDefaults/FamilyUserDefautls.swift rename to 14th-team5-iOS/Data/Sources/Trash/UserDefaults/FamilyUserDefautls.swift diff --git a/14th-team5-iOS/Data/Sources/UserDefaults/PostUserDefaults.swift b/14th-team5-iOS/Data/Sources/Trash/UserDefaults/PostUserDefaults.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/UserDefaults/PostUserDefaults.swift rename to 14th-team5-iOS/Data/Sources/Trash/UserDefaults/PostUserDefaults.swift diff --git a/14th-team5-iOS/Data/Sources/UserDefaults/UserDefaultsRepository.swift b/14th-team5-iOS/Data/Sources/Trash/UserDefaults/UserDefaultsRepository.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/UserDefaults/UserDefaultsRepository.swift rename to 14th-team5-iOS/Data/Sources/Trash/UserDefaults/UserDefaultsRepository.swift diff --git a/14th-team5-iOS/Domain/Sources/Widget/Entities/RecentFamilyPostData.swift b/14th-team5-iOS/Domain/Sources/Entities/Widget/RecentFamilyPostEntity.swift similarity index 93% rename from 14th-team5-iOS/Domain/Sources/Widget/Entities/RecentFamilyPostData.swift rename to 14th-team5-iOS/Domain/Sources/Entities/Widget/RecentFamilyPostEntity.swift index 468e2185b..41ad38079 100644 --- a/14th-team5-iOS/Domain/Sources/Widget/Entities/RecentFamilyPostData.swift +++ b/14th-team5-iOS/Domain/Sources/Entities/Widget/RecentFamilyPostEntity.swift @@ -7,7 +7,7 @@ import Foundation -public struct RecentFamilyPostData: Codable { +public struct RecentFamilyPostEntity: Codable { public var authorName: String public var authorProfileImageUrl: String? public var postId: String? diff --git a/14th-team5-iOS/Domain/Sources/Widget/Interfaces/WidgetRepositoryProtocol.swift b/14th-team5-iOS/Domain/Sources/Repositories/WidgetRepositoryProtocol.swift similarity index 85% rename from 14th-team5-iOS/Domain/Sources/Widget/Interfaces/WidgetRepositoryProtocol.swift rename to 14th-team5-iOS/Domain/Sources/Repositories/WidgetRepositoryProtocol.swift index b35e063f3..9cd7f9ffa 100644 --- a/14th-team5-iOS/Domain/Sources/Widget/Interfaces/WidgetRepositoryProtocol.swift +++ b/14th-team5-iOS/Domain/Sources/Repositories/WidgetRepositoryProtocol.swift @@ -8,5 +8,5 @@ import Foundation public protocol WidgetRepositoryProtocol { - func fetchRecentFamilyPost(completion: @escaping (Result) -> Void) + func fetchRecentFamilyPost(completion: @escaping (Result) -> Void) } diff --git a/14th-team5-iOS/Domain/Sources/Account/AccessToken.swift b/14th-team5-iOS/Domain/Sources/Trash/Account/AccessToken.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Account/AccessToken.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Account/AccessToken.swift diff --git a/14th-team5-iOS/Domain/Sources/Account/AccountSignInStateInfo.swift b/14th-team5-iOS/Domain/Sources/Trash/Account/AccountSignInStateInfo.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Account/AccountSignInStateInfo.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Account/AccountSignInStateInfo.swift diff --git a/14th-team5-iOS/Domain/Sources/Account/DataMapping/AccountNickNameEditDTO.swift b/14th-team5-iOS/Domain/Sources/Trash/Account/DataMapping/AccountNickNameEditDTO.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Account/DataMapping/AccountNickNameEditDTO.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Account/DataMapping/AccountNickNameEditDTO.swift diff --git a/14th-team5-iOS/Domain/Sources/Account/DataMapping/AccountRefreshDTO.swift b/14th-team5-iOS/Domain/Sources/Trash/Account/DataMapping/AccountRefreshDTO.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Account/DataMapping/AccountRefreshDTO.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Account/DataMapping/AccountRefreshDTO.swift diff --git a/14th-team5-iOS/Domain/Sources/Account/Entity/AccountNickNameEditResponse.swift b/14th-team5-iOS/Domain/Sources/Trash/Account/Entity/AccountNickNameEditResponse.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Account/Entity/AccountNickNameEditResponse.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Account/Entity/AccountNickNameEditResponse.swift diff --git a/14th-team5-iOS/Domain/Sources/Account/Entity/AccountRefreshResponse.swift b/14th-team5-iOS/Domain/Sources/Trash/Account/Entity/AccountRefreshResponse.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Account/Entity/AccountRefreshResponse.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Account/Entity/AccountRefreshResponse.swift diff --git a/14th-team5-iOS/Domain/Sources/Account/FamilyInfo.swift b/14th-team5-iOS/Domain/Sources/Trash/Account/FamilyInfo.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Account/FamilyInfo.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Account/FamilyInfo.swift diff --git a/14th-team5-iOS/Domain/Sources/Account/MeberInfo.swift b/14th-team5-iOS/Domain/Sources/Trash/Account/MeberInfo.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Account/MeberInfo.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Account/MeberInfo.swift diff --git a/14th-team5-iOS/Domain/Sources/Account/Parameters/AccountNickNameEditParameter.swift b/14th-team5-iOS/Domain/Sources/Trash/Account/Parameters/AccountNickNameEditParameter.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Account/Parameters/AccountNickNameEditParameter.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Account/Parameters/AccountNickNameEditParameter.swift diff --git a/14th-team5-iOS/Domain/Sources/Account/Parameters/AccountRefreshParameter.swift b/14th-team5-iOS/Domain/Sources/Trash/Account/Parameters/AccountRefreshParameter.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Account/Parameters/AccountRefreshParameter.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Account/Parameters/AccountRefreshParameter.swift diff --git a/14th-team5-iOS/Domain/Sources/Account/SNS.swift b/14th-team5-iOS/Domain/Sources/Trash/Account/SNS.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Account/SNS.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Account/SNS.swift diff --git a/14th-team5-iOS/Domain/Sources/Calendar/UseCases/CalendarUseCase.swift b/14th-team5-iOS/Domain/Sources/Trash/Calendar/UseCases/CalendarUseCase.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Calendar/UseCases/CalendarUseCase.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Calendar/UseCases/CalendarUseCase.swift diff --git a/14th-team5-iOS/Domain/Sources/Camera/Entity/CameraCreateRealEmojiEntity.swift b/14th-team5-iOS/Domain/Sources/Trash/Camera/Entity/CameraCreateRealEmojiEntity.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Camera/Entity/CameraCreateRealEmojiEntity.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Camera/Entity/CameraCreateRealEmojiEntity.swift diff --git a/14th-team5-iOS/Domain/Sources/Camera/Entity/CameraMissionFeedQuery.swift b/14th-team5-iOS/Domain/Sources/Trash/Camera/Entity/CameraMissionFeedQuery.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Camera/Entity/CameraMissionFeedQuery.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Camera/Entity/CameraMissionFeedQuery.swift diff --git a/14th-team5-iOS/Domain/Sources/Camera/Entity/CameraPostEntity.swift b/14th-team5-iOS/Domain/Sources/Trash/Camera/Entity/CameraPostEntity.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Camera/Entity/CameraPostEntity.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Camera/Entity/CameraPostEntity.swift diff --git a/14th-team5-iOS/Domain/Sources/Camera/Entity/CameraPreSignedEntity.swift b/14th-team5-iOS/Domain/Sources/Trash/Camera/Entity/CameraPreSignedEntity.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Camera/Entity/CameraPreSignedEntity.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Camera/Entity/CameraPreSignedEntity.swift diff --git a/14th-team5-iOS/Domain/Sources/Camera/Entity/CameraRealEmojiImageItemEntity.swift b/14th-team5-iOS/Domain/Sources/Trash/Camera/Entity/CameraRealEmojiImageItemEntity.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Camera/Entity/CameraRealEmojiImageItemEntity.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Camera/Entity/CameraRealEmojiImageItemEntity.swift diff --git a/14th-team5-iOS/Domain/Sources/Camera/Entity/CameraRealEmojiItems.swift b/14th-team5-iOS/Domain/Sources/Trash/Camera/Entity/CameraRealEmojiItems.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Camera/Entity/CameraRealEmojiItems.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Camera/Entity/CameraRealEmojiItems.swift diff --git a/14th-team5-iOS/Domain/Sources/Camera/Entity/CameraRealEmojiPreSignedEntity.swift b/14th-team5-iOS/Domain/Sources/Trash/Camera/Entity/CameraRealEmojiPreSignedEntity.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Camera/Entity/CameraRealEmojiPreSignedEntity.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Camera/Entity/CameraRealEmojiPreSignedEntity.swift diff --git a/14th-team5-iOS/Domain/Sources/Camera/Entity/CameraTodayMssionEntity.swift b/14th-team5-iOS/Domain/Sources/Trash/Camera/Entity/CameraTodayMssionEntity.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Camera/Entity/CameraTodayMssionEntity.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Camera/Entity/CameraTodayMssionEntity.swift diff --git a/14th-team5-iOS/Domain/Sources/Camera/Entity/CameraUpdateRealEmojiEntity.swift b/14th-team5-iOS/Domain/Sources/Trash/Camera/Entity/CameraUpdateRealEmojiEntity.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Camera/Entity/CameraUpdateRealEmojiEntity.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Camera/Entity/CameraUpdateRealEmojiEntity.swift diff --git a/14th-team5-iOS/Domain/Sources/Camera/Interfaces/CameraRepositoryProtocol.swift b/14th-team5-iOS/Domain/Sources/Trash/Camera/Interfaces/CameraRepositoryProtocol.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Camera/Interfaces/CameraRepositoryProtocol.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Camera/Interfaces/CameraRepositoryProtocol.swift diff --git a/14th-team5-iOS/Domain/Sources/Camera/Parameters/CameraCreateRealEmojiParameters.swift b/14th-team5-iOS/Domain/Sources/Trash/Camera/Parameters/CameraCreateRealEmojiParameters.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Camera/Parameters/CameraCreateRealEmojiParameters.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Camera/Parameters/CameraCreateRealEmojiParameters.swift diff --git a/14th-team5-iOS/Domain/Sources/Camera/Parameters/CameraDisplayImageParameters.swift b/14th-team5-iOS/Domain/Sources/Trash/Camera/Parameters/CameraDisplayImageParameters.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Camera/Parameters/CameraDisplayImageParameters.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Camera/Parameters/CameraDisplayImageParameters.swift diff --git a/14th-team5-iOS/Domain/Sources/Camera/Parameters/CameraDisplayPostParameters.swift b/14th-team5-iOS/Domain/Sources/Trash/Camera/Parameters/CameraDisplayPostParameters.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Camera/Parameters/CameraDisplayPostParameters.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Camera/Parameters/CameraDisplayPostParameters.swift diff --git a/14th-team5-iOS/Domain/Sources/Camera/Parameters/CameraRealEmojiParameters.swift b/14th-team5-iOS/Domain/Sources/Trash/Camera/Parameters/CameraRealEmojiParameters.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Camera/Parameters/CameraRealEmojiParameters.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Camera/Parameters/CameraRealEmojiParameters.swift diff --git a/14th-team5-iOS/Domain/Sources/Camera/Parameters/CameraUpdateRealEmojiParameters.swift b/14th-team5-iOS/Domain/Sources/Trash/Camera/Parameters/CameraUpdateRealEmojiParameters.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Camera/Parameters/CameraUpdateRealEmojiParameters.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Camera/Parameters/CameraUpdateRealEmojiParameters.swift diff --git a/14th-team5-iOS/Domain/Sources/Camera/UseCases/Camera/CreateCameraImageUseCase.swift b/14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/Camera/CreateCameraImageUseCase.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Camera/UseCases/Camera/CreateCameraImageUseCase.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/Camera/CreateCameraImageUseCase.swift diff --git a/14th-team5-iOS/Domain/Sources/Camera/UseCases/Camera/CreateCameraUseCase.swift b/14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/Camera/CreateCameraUseCase.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Camera/UseCases/Camera/CreateCameraUseCase.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/Camera/CreateCameraUseCase.swift diff --git a/14th-team5-iOS/Domain/Sources/Camera/UseCases/Camera/FetchCameraUploadImageUseCase.swift b/14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/Camera/FetchCameraUploadImageUseCase.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Camera/UseCases/Camera/FetchCameraUploadImageUseCase.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/Camera/FetchCameraUploadImageUseCase.swift diff --git a/14th-team5-iOS/Domain/Sources/Camera/UseCases/Mission/FetchCameraTodayMissionUseCase.swift b/14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/Mission/FetchCameraTodayMissionUseCase.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Camera/UseCases/Mission/FetchCameraTodayMissionUseCase.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/Mission/FetchCameraTodayMissionUseCase.swift diff --git a/14th-team5-iOS/Domain/Sources/Camera/UseCases/Profile/EditCameraProfileImageUseCase.swift b/14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/Profile/EditCameraProfileImageUseCase.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Camera/UseCases/Profile/EditCameraProfileImageUseCase.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/Profile/EditCameraProfileImageUseCase.swift diff --git a/14th-team5-iOS/Domain/Sources/Camera/UseCases/RealEmoji/FetchCameraRealEmojiListUseCase.swift b/14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/RealEmoji/FetchCameraRealEmojiListUseCase.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Camera/UseCases/RealEmoji/FetchCameraRealEmojiListUseCase.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/RealEmoji/FetchCameraRealEmojiListUseCase.swift diff --git a/14th-team5-iOS/Domain/Sources/Camera/UseCases/RealEmoji/FetchCameraRealEmojiUpdateUseCase.swift b/14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/RealEmoji/FetchCameraRealEmojiUpdateUseCase.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Camera/UseCases/RealEmoji/FetchCameraRealEmojiUpdateUseCase.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/RealEmoji/FetchCameraRealEmojiUpdateUseCase.swift diff --git a/14th-team5-iOS/Domain/Sources/Camera/UseCases/RealEmoji/FetchCameraRealEmojiUploadUseCase.swift b/14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/RealEmoji/FetchCameraRealEmojiUploadUseCase.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Camera/UseCases/RealEmoji/FetchCameraRealEmojiUploadUseCase.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/RealEmoji/FetchCameraRealEmojiUploadUseCase.swift diff --git a/14th-team5-iOS/Domain/Sources/Camera/UseCases/RealEmoji/FetchCameraRealEmojiUseCase.swift b/14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/RealEmoji/FetchCameraRealEmojiUseCase.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Camera/UseCases/RealEmoji/FetchCameraRealEmojiUseCase.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/RealEmoji/FetchCameraRealEmojiUseCase.swift diff --git a/14th-team5-iOS/Domain/Sources/FCM/Entities/FCMToken.swift b/14th-team5-iOS/Domain/Sources/Trash/FCM/Entities/FCMToken.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/FCM/Entities/FCMToken.swift rename to 14th-team5-iOS/Domain/Sources/Trash/FCM/Entities/FCMToken.swift diff --git a/14th-team5-iOS/Domain/Sources/FCM/UseCases/FCMUseCase.swift b/14th-team5-iOS/Domain/Sources/Trash/FCM/UseCases/FCMUseCase.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/FCM/UseCases/FCMUseCase.swift rename to 14th-team5-iOS/Domain/Sources/Trash/FCM/UseCases/FCMUseCase.swift diff --git a/14th-team5-iOS/Domain/Sources/Family/Interfaces/Repositories/JoinFamilyRepository.swift b/14th-team5-iOS/Domain/Sources/Trash/Family/Interfaces/Repositories/JoinFamilyRepository.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Family/Interfaces/Repositories/JoinFamilyRepository.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Family/Interfaces/Repositories/JoinFamilyRepository.swift diff --git a/14th-team5-iOS/Domain/Sources/Family/UseCases/FamilyUseCase.swift b/14th-team5-iOS/Domain/Sources/Trash/Family/UseCases/FamilyUseCase.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Family/UseCases/FamilyUseCase.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Family/UseCases/FamilyUseCase.swift diff --git a/14th-team5-iOS/Domain/Sources/Interfaces/FCMRepositoryProtocol.swift b/14th-team5-iOS/Domain/Sources/Trash/Interfaces/FCMRepositoryProtocol.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Interfaces/FCMRepositoryProtocol.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Interfaces/FCMRepositoryProtocol.swift diff --git a/14th-team5-iOS/Domain/Sources/Interfaces/KeychainRepositoryProtocol.swift b/14th-team5-iOS/Domain/Sources/Trash/Interfaces/KeychainRepositoryProtocol.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Interfaces/KeychainRepositoryProtocol.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Interfaces/KeychainRepositoryProtocol.swift diff --git a/14th-team5-iOS/Domain/Sources/Interfaces/UserDefaultsRepositoryProtocol.swift b/14th-team5-iOS/Domain/Sources/Trash/Interfaces/UserDefaultsRepositoryProtocol.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Interfaces/UserDefaultsRepositoryProtocol.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Interfaces/UserDefaultsRepositoryProtocol.swift diff --git a/14th-team5-iOS/Domain/Sources/Me/MeRepositoryProtocol.swift b/14th-team5-iOS/Domain/Sources/Trash/Me/MeRepositoryProtocol.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Me/MeRepositoryProtocol.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Me/MeRepositoryProtocol.swift diff --git a/14th-team5-iOS/Domain/Sources/Me/MeUseCase.swift b/14th-team5-iOS/Domain/Sources/Trash/Me/MeUseCase.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Me/MeUseCase.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Me/MeUseCase.swift diff --git a/14th-team5-iOS/Domain/Sources/Member/Interfaces/Repositories/MemberRepository.swift b/14th-team5-iOS/Domain/Sources/Trash/Member/Interfaces/Repositories/MemberRepository.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Member/Interfaces/Repositories/MemberRepository.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Member/Interfaces/Repositories/MemberRepository.swift diff --git a/14th-team5-iOS/Domain/Sources/Member/UseCases/MemberUseCase.swift b/14th-team5-iOS/Domain/Sources/Trash/Member/UseCases/MemberUseCase.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Member/UseCases/MemberUseCase.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Member/UseCases/MemberUseCase.swift diff --git a/14th-team5-iOS/Domain/Sources/Members/Entities/MembersProfileEntity.swift b/14th-team5-iOS/Domain/Sources/Trash/Members/Entities/MembersProfileEntity.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Members/Entities/MembersProfileEntity.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Members/Entities/MembersProfileEntity.swift diff --git a/14th-team5-iOS/Domain/Sources/Members/Interfaces/MembersRepositoryProtocol.swift b/14th-team5-iOS/Domain/Sources/Trash/Members/Interfaces/MembersRepositoryProtocol.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Members/Interfaces/MembersRepositoryProtocol.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Members/Interfaces/MembersRepositoryProtocol.swift diff --git a/14th-team5-iOS/Domain/Sources/Members/Parameters/ProfileImageEditParameter.swift b/14th-team5-iOS/Domain/Sources/Trash/Members/Parameters/ProfileImageEditParameter.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Members/Parameters/ProfileImageEditParameter.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Members/Parameters/ProfileImageEditParameter.swift diff --git a/14th-team5-iOS/Domain/Sources/Members/UseCases/Members/DeleteMembersProfileUseCase.swift b/14th-team5-iOS/Domain/Sources/Trash/Members/UseCases/Members/DeleteMembersProfileUseCase.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Members/UseCases/Members/DeleteMembersProfileUseCase.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Members/UseCases/Members/DeleteMembersProfileUseCase.swift diff --git a/14th-team5-iOS/Domain/Sources/Members/UseCases/Members/FetchMembersPostListUseCase.swift b/14th-team5-iOS/Domain/Sources/Trash/Members/UseCases/Members/FetchMembersPostListUseCase.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Members/UseCases/Members/FetchMembersPostListUseCase.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Members/UseCases/Members/FetchMembersPostListUseCase.swift diff --git a/14th-team5-iOS/Domain/Sources/Members/UseCases/Members/FetchMembersProfileUseCase.swift b/14th-team5-iOS/Domain/Sources/Trash/Members/UseCases/Members/FetchMembersProfileUseCase.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Members/UseCases/Members/FetchMembersProfileUseCase.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Members/UseCases/Members/FetchMembersProfileUseCase.swift diff --git a/14th-team5-iOS/Domain/Sources/Members/UseCases/Members/UpdateMembersProfileUseCase.swift b/14th-team5-iOS/Domain/Sources/Trash/Members/UseCases/Members/UpdateMembersProfileUseCase.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Members/UseCases/Members/UpdateMembersProfileUseCase.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Members/UseCases/Members/UpdateMembersProfileUseCase.swift diff --git a/14th-team5-iOS/Domain/Sources/Mission/Entities/MissionContentEntity.swift b/14th-team5-iOS/Domain/Sources/Trash/Mission/Entities/MissionContentEntity.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Mission/Entities/MissionContentEntity.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Mission/Entities/MissionContentEntity.swift diff --git a/14th-team5-iOS/Domain/Sources/Mission/Interfaces/MissionRepositoryProtocol.swift b/14th-team5-iOS/Domain/Sources/Trash/Mission/Interfaces/MissionRepositoryProtocol.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Mission/Interfaces/MissionRepositoryProtocol.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Mission/Interfaces/MissionRepositoryProtocol.swift diff --git a/14th-team5-iOS/Domain/Sources/Mission/UseCases/Mission/CheckMissionAlertShowUseCase.swift b/14th-team5-iOS/Domain/Sources/Trash/Mission/UseCases/Mission/CheckMissionAlertShowUseCase.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Mission/UseCases/Mission/CheckMissionAlertShowUseCase.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Mission/UseCases/Mission/CheckMissionAlertShowUseCase.swift diff --git a/14th-team5-iOS/Domain/Sources/Mission/UseCases/Mission/FetchMissionContentUseCase.swift b/14th-team5-iOS/Domain/Sources/Trash/Mission/UseCases/Mission/FetchMissionContentUseCase.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Mission/UseCases/Mission/FetchMissionContentUseCase.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Mission/UseCases/Mission/FetchMissionContentUseCase.swift diff --git a/14th-team5-iOS/Domain/Sources/Pick/UseCases/PickUseCase.swift b/14th-team5-iOS/Domain/Sources/Trash/Pick/UseCases/PickUseCase.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Pick/UseCases/PickUseCase.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Pick/UseCases/PickUseCase.swift diff --git a/14th-team5-iOS/Domain/Sources/PostComment/UseCase/PostCommentUseCase.swift b/14th-team5-iOS/Domain/Sources/Trash/PostComment/UseCase/PostCommentUseCase.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/PostComment/UseCase/PostCommentUseCase.swift rename to 14th-team5-iOS/Domain/Sources/Trash/PostComment/UseCase/PostCommentUseCase.swift diff --git a/14th-team5-iOS/Domain/Sources/Privacy/Entity/BibbiAppInfoResponse.swift b/14th-team5-iOS/Domain/Sources/Trash/Privacy/Entity/BibbiAppInfoResponse.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Privacy/Entity/BibbiAppInfoResponse.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Privacy/Entity/BibbiAppInfoResponse.swift diff --git a/14th-team5-iOS/Domain/Sources/Privacy/Interfaces/PrivacyViewInterface.swift b/14th-team5-iOS/Domain/Sources/Trash/Privacy/Interfaces/PrivacyViewInterface.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Privacy/Interfaces/PrivacyViewInterface.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Privacy/Interfaces/PrivacyViewInterface.swift diff --git a/14th-team5-iOS/Domain/Sources/Privacy/Parameters/BibbiAppInfoParameter.swift b/14th-team5-iOS/Domain/Sources/Trash/Privacy/Parameters/BibbiAppInfoParameter.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Privacy/Parameters/BibbiAppInfoParameter.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Privacy/Parameters/BibbiAppInfoParameter.swift diff --git a/14th-team5-iOS/Domain/Sources/Privacy/Privacy.swift b/14th-team5-iOS/Domain/Sources/Trash/Privacy/Privacy.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Privacy/Privacy.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Privacy/Privacy.swift diff --git a/14th-team5-iOS/Domain/Sources/Privacy/UseCases/PrivacyViewUseCase.swift b/14th-team5-iOS/Domain/Sources/Trash/Privacy/UseCases/PrivacyViewUseCase.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Privacy/UseCases/PrivacyViewUseCase.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Privacy/UseCases/PrivacyViewUseCase.swift diff --git a/14th-team5-iOS/Domain/Sources/Resign/Entity/AccountFamilyResignResponse.swift b/14th-team5-iOS/Domain/Sources/Trash/Resign/Entity/AccountFamilyResignResponse.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Resign/Entity/AccountFamilyResignResponse.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Resign/Entity/AccountFamilyResignResponse.swift diff --git a/14th-team5-iOS/Domain/Sources/Resign/Entity/AccountFcmResignResponse.swift b/14th-team5-iOS/Domain/Sources/Trash/Resign/Entity/AccountFcmResignResponse.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Resign/Entity/AccountFcmResignResponse.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Resign/Entity/AccountFcmResignResponse.swift diff --git a/14th-team5-iOS/Domain/Sources/Resign/Entity/AccountResignResponse.swift b/14th-team5-iOS/Domain/Sources/Trash/Resign/Entity/AccountResignResponse.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Resign/Entity/AccountResignResponse.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Resign/Entity/AccountResignResponse.swift diff --git a/14th-team5-iOS/Domain/Sources/Resign/Interfaces/AccountResignInterface.swift b/14th-team5-iOS/Domain/Sources/Trash/Resign/Interfaces/AccountResignInterface.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Resign/Interfaces/AccountResignInterface.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Resign/Interfaces/AccountResignInterface.swift diff --git a/14th-team5-iOS/Domain/Sources/Resign/UseCases/AccountResignUseCase.swift b/14th-team5-iOS/Domain/Sources/Trash/Resign/UseCases/AccountResignUseCase.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Resign/UseCases/AccountResignUseCase.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Resign/UseCases/AccountResignUseCase.swift diff --git a/14th-team5-iOS/Domain/Sources/SignOut/UseCases/SignOutUseCase.swift b/14th-team5-iOS/Domain/Sources/Trash/SignOut/UseCases/SignOutUseCase.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/SignOut/UseCases/SignOutUseCase.swift rename to 14th-team5-iOS/Domain/Sources/Trash/SignOut/UseCases/SignOutUseCase.swift diff --git a/14th-team5-iOS/Domain/Sources/Widget/UseCases/FetchRecentFamilyPostUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Widget/FetchRecentFamilyPostUseCase.swift similarity index 83% rename from 14th-team5-iOS/Domain/Sources/Widget/UseCases/FetchRecentFamilyPostUseCase.swift rename to 14th-team5-iOS/Domain/Sources/UseCases/Widget/FetchRecentFamilyPostUseCase.swift index 3536804a1..1d0507363 100644 --- a/14th-team5-iOS/Domain/Sources/Widget/UseCases/FetchRecentFamilyPostUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/Widget/FetchRecentFamilyPostUseCase.swift @@ -8,7 +8,7 @@ import Foundation public protocol FetchRecentFamilyPostUseCaseProtocol { - func excute(completion: @escaping (Result) -> Void) + func excute(completion: @escaping (Result) -> Void) } public class FetchRecentFamilyPostUseCase: FetchRecentFamilyPostUseCaseProtocol { @@ -18,7 +18,7 @@ public class FetchRecentFamilyPostUseCase: FetchRecentFamilyPostUseCaseProtocol self.widgetRepository = widgetRepository } - public func excute(completion: @escaping (Result) -> Void) { + public func excute(completion: @escaping (Result) -> Void) { return widgetRepository.fetchRecentFamilyPost(completion: completion) } } From 673b7d1dbd2ebd607fb67e4032dcd4887a06a849 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Wed, 26 Jun 2024 10:46:03 +0900 Subject: [PATCH 140/263] =?UTF-8?q?feat:=20Splash=20=EA=B4=80=EB=A0=A8=20W?= =?UTF-8?q?rapper=20=EA=B5=AC=ED=98=84=20(#580)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../OnboardingViewControllerWrapper.swift | 8 ++++ .../Splash/SplashViewControllerWrapper.swift | 37 +++++++++++++++++++ .../Splash/Dependency/SplashDIContainer.swift | 5 ++- .../Presentation/Splash/SplashReactor.swift | 13 +++---- .../Splash/SplashViewController.swift | 4 +- 5 files changed, 56 insertions(+), 11 deletions(-) create mode 100644 14th-team5-iOS/App/Sources/Application/Wrapper/Onboarding/OnboardingViewControllerWrapper.swift create mode 100644 14th-team5-iOS/App/Sources/Application/Wrapper/Splash/SplashViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/Wrapper/Onboarding/OnboardingViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Wrapper/Onboarding/OnboardingViewControllerWrapper.swift new file mode 100644 index 000000000..8b9f2402a --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/Wrapper/Onboarding/OnboardingViewControllerWrapper.swift @@ -0,0 +1,8 @@ +// +// OnboardingViewControllerWrapper.swift +// App +// +// Created by 김건우 on 6/26/24. +// + +import Foundation diff --git a/14th-team5-iOS/App/Sources/Application/Wrapper/Splash/SplashViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Wrapper/Splash/SplashViewControllerWrapper.swift new file mode 100644 index 000000000..336ec8ebe --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/Wrapper/Splash/SplashViewControllerWrapper.swift @@ -0,0 +1,37 @@ +// +// SplashViewControllerWrapper.swift +// App +// +// Created by 김건우 on 6/26/24. +// + +import Core +import Foundation + +final class SplashViewControllerWrapper: BaseWrapper { + + // MARK: - Typealias + + typealias R = SplashReactor + typealias V = SplashViewController + + // MARK: - Properties + + var reactor: SplashReactor { + makeReactor() + } + + var viewController: SplashViewController { + makeViewController() + } + + // MARK: - Make + + func makeReactor() -> SplashReactor { + SplashReactor() + } + + func makeViewController() -> SplashViewController { + SplashViewController(reactor: makeReactor()) + } +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Splash/Dependency/SplashDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/Splash/Dependency/SplashDIContainer.swift index 99142da5b..d76621458 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Splash/Dependency/SplashDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Splash/Dependency/SplashDIContainer.swift @@ -10,9 +10,10 @@ import Data import Domain import Core +@available(*, deprecated, renamed: "SplashViewControllerWrapper") public final class SplashDIContainer { public typealias ViewContrller = SplashViewController - public typealias Reactor = SplashViewReactor + public typealias Reactor = SplashReactor public func makeViewController() -> ViewContrller { return SplashViewController(reactor: makeReactor()) @@ -35,6 +36,6 @@ public final class SplashDIContainer { } public func makeReactor() -> Reactor { - return SplashViewReactor(meRepository: makeMeUseCase(), familyUseCase: makeFamilyUseCase()) + return SplashReactor() } } diff --git a/14th-team5-iOS/App/Sources/Presentation/Splash/SplashReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Splash/SplashReactor.swift index eb7672e84..e150ecd4e 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Splash/SplashReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Splash/SplashReactor.swift @@ -11,8 +11,9 @@ import Domain import ReactorKit import RxSwift +import Data -public final class SplashViewReactor: Reactor { +public final class SplashReactor: Reactor { // MARK: - Action public enum Action { case viewDidLoad @@ -32,15 +33,13 @@ public final class SplashViewReactor: Reactor { } // MARK: - Properties - private let meRepository: MeUseCaseProtocol - private let familyUseCase: FamilyUseCaseProtocol + private let meRepository = MeUseCase(meRepository: MeAPIs.Worker()) // TODO: - Injected로 수정하기 + @Injected var familyUseCase: FamilyUseCaseProtocol + public let initialState: State = State() // MARK: - Intializer - init(meRepository: MeUseCaseProtocol, familyUseCase: FamilyUseCaseProtocol) { - self.meRepository = meRepository - self.familyUseCase = familyUseCase - } + init() { } // MARK: - Mutate public func mutate(action: Action) -> Observable { diff --git a/14th-team5-iOS/App/Sources/Presentation/Splash/SplashViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Splash/SplashViewController.swift index baa1f1eff..cb76f57bb 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Splash/SplashViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Splash/SplashViewController.swift @@ -14,7 +14,7 @@ import RxCocoa import SnapKit import Then -public final class SplashViewController: BaseViewController { +public final class SplashViewController: BaseViewController { // MARK: - Views private let bibbiImageView = UIImageView() @@ -56,7 +56,7 @@ public final class SplashViewController: BaseViewController { } } - override public func bind(reactor: SplashViewReactor) { + override public func bind(reactor: SplashReactor) { Observable.just(()) .map { Reactor.Action.viewDidLoad } .bind(to: reactor.action) From 69533982f421e8d22aeaf9bae82bd6fdffd0c364 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Wed, 26 Jun 2024 10:46:29 +0900 Subject: [PATCH 141/263] =?UTF-8?q?feat:=20Onboarding=20=EA=B4=80=EB=A0=A8?= =?UTF-8?q?=20Wrapper=20=EA=B5=AC=ED=98=84=20(#580)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../OnboardingViewControllerWrapper.swift | 30 ++++++++++ .../Dependency/OnBoardingDIContainer.swift | 3 +- .../OnBoarding/OnBoardingReactor.swift | 5 +- .../xcschemes/Bibbi-Workspace.xcscheme | 56 +++++++++++++++++++ 4 files changed, 90 insertions(+), 4 deletions(-) diff --git a/14th-team5-iOS/App/Sources/Application/Wrapper/Onboarding/OnboardingViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Wrapper/Onboarding/OnboardingViewControllerWrapper.swift index 8b9f2402a..03ce27274 100644 --- a/14th-team5-iOS/App/Sources/Application/Wrapper/Onboarding/OnboardingViewControllerWrapper.swift +++ b/14th-team5-iOS/App/Sources/Application/Wrapper/Onboarding/OnboardingViewControllerWrapper.swift @@ -5,4 +5,34 @@ // Created by 김건우 on 6/26/24. // +import Core import Foundation + +final class OnboardingViewControllerWrapper: BaseWrapper { + + // MARK: - Typealias + + typealias R = OnBoardingReactor + typealias V = OnBoardingViewController + + // MARK: - Properties + + var reactor: OnBoardingReactor { + makeReactor() + } + + var viewController: OnBoardingViewController { + makeViewController() + } + + // MARK: - Make + + func makeReactor() -> OnBoardingReactor { + OnBoardingReactor() + } + + func makeViewController() -> OnBoardingViewController { + OnBoardingViewController(reactor: makeReactor()) + } + +} diff --git a/14th-team5-iOS/App/Sources/Presentation/OnBoarding/Dependency/OnBoardingDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/OnBoarding/Dependency/OnBoardingDIContainer.swift index 0445d4109..f75a2ddf1 100644 --- a/14th-team5-iOS/App/Sources/Presentation/OnBoarding/Dependency/OnBoardingDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/OnBoarding/Dependency/OnBoardingDIContainer.swift @@ -11,6 +11,7 @@ import Core import Data import Domain +@available(*, deprecated, renamed: "OnboardingViewControllerWrapper") public final class OnBoardingDIContainer { public func makeViewController() -> OnBoardingViewController { @@ -26,6 +27,6 @@ public final class OnBoardingDIContainer { } public func makeReactor() -> OnBoardingReactor { - return OnBoardingReactor(familyUseCase: makeUseCase()) + return OnBoardingReactor() } } diff --git a/14th-team5-iOS/App/Sources/Presentation/OnBoarding/OnBoardingReactor.swift b/14th-team5-iOS/App/Sources/Presentation/OnBoarding/OnBoardingReactor.swift index 9ee65df26..784f11774 100644 --- a/14th-team5-iOS/App/Sources/Presentation/OnBoarding/OnBoardingReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/OnBoarding/OnBoardingReactor.swift @@ -17,7 +17,7 @@ import ReactorKit public final class OnBoardingReactor: Reactor { public var initialState: State = State() - private var familyUseCase: FamilyUseCaseProtocol + @Injected var familyUseCase: FamilyUseCaseProtocol public enum Action { case permissionTapped @@ -31,8 +31,7 @@ public final class OnBoardingReactor: Reactor { var permissionTappedFinish: Bool = false } - init(familyUseCase: FamilyUseCaseProtocol) { - self.familyUseCase = familyUseCase + init() { self.initialState = State() } } diff --git a/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme b/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme index 5cc9613cf..defe8e429 100644 --- a/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme +++ b/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme @@ -272,6 +272,62 @@ ReferencedContainer = "container:Tuist/Dependencies/SwiftPackageManager/.build/checkouts/firebase-ios-sdk/Firebase.xcodeproj"> + + + + + + + + + + + + + + + + Date: Wed, 26 Jun 2024 10:49:50 +0900 Subject: [PATCH 142/263] =?UTF-8?q?feat:=20WebContent=20=EA=B4=80=EB=A0=A8?= =?UTF-8?q?=20Wrapper=20=EA=B5=AC=ED=98=84=20(#580)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../WebContentViewControllerWrapper.swift | 46 +++++++++++++++++++ .../Dependency/WebContentDIContainer.swift | 6 +-- ...wReactor.swift => WebContentReactor.swift} | 2 +- .../Content/WebContentViewController.swift | 4 +- 4 files changed, 52 insertions(+), 6 deletions(-) create mode 100644 14th-team5-iOS/App/Sources/Application/Wrapper/WebContent/WebContentViewControllerWrapper.swift rename 14th-team5-iOS/App/Sources/Presentation/Content/Reactor/{WebContentViewReactor.swift => WebContentReactor.swift} (95%) diff --git a/14th-team5-iOS/App/Sources/Application/Wrapper/WebContent/WebContentViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Wrapper/WebContent/WebContentViewControllerWrapper.swift new file mode 100644 index 000000000..74fc5e9c5 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/Wrapper/WebContent/WebContentViewControllerWrapper.swift @@ -0,0 +1,46 @@ +// +// WebContentViewControllerWrapper.swift +// App +// +// Created by 김건우 on 6/26/24. +// + +import Core +import Foundation + +final class WebContentViewControllerWrapper: BaseWrapper { + + // MARK: - Typealias + + typealias R = WebContentReactor + typealias V = WebContentViewController + + // MARK: - Properties + + let url: URL? + + var reactor: WebContentReactor { + makeReactor() + } + + var viewController: WebContentViewController { + makeViewController() + } + + // MARK: - Intializer + + init(url: URL?) { + self.url = url + } + + // MARK: - Make + + func makeReactor() -> WebContentReactor { + WebContentReactor(contentURL: url) + } + + func makeViewController() -> WebContentViewController { + WebContentViewController(reactor: makeReactor()) + } + +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Content/Dependency/WebContentDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/Content/Dependency/WebContentDIContainer.swift index 4437b2c3c..72427634e 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Content/Dependency/WebContentDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Content/Dependency/WebContentDIContainer.swift @@ -7,12 +7,12 @@ import Foundation - +@available(*, deprecated, renamed: "WebContentViewControllerWrapper") public final class WebContentDIContainer { // UseCase or Repository 사용할지 // Reactor 만 사용할지 논의 필요 public typealias ViewController = WebContentViewController - public typealias Reactor = WebContentViewReactor + public typealias Reactor = WebContentReactor public let webURL: URL? @@ -27,7 +27,7 @@ public final class WebContentDIContainer { } public func makeReactor() -> Reactor { - return WebContentViewReactor(contentURL: webURL) + return WebContentReactor(contentURL: webURL) } diff --git a/14th-team5-iOS/App/Sources/Presentation/Content/Reactor/WebContentViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Content/Reactor/WebContentReactor.swift similarity index 95% rename from 14th-team5-iOS/App/Sources/Presentation/Content/Reactor/WebContentViewReactor.swift rename to 14th-team5-iOS/App/Sources/Presentation/Content/Reactor/WebContentReactor.swift index 305940d75..dda6e45fa 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Content/Reactor/WebContentViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Content/Reactor/WebContentReactor.swift @@ -10,7 +10,7 @@ import Foundation import RxSwift import ReactorKit -public class WebContentViewReactor: Reactor { +public class WebContentReactor: Reactor { public var initialState: State public enum Action { diff --git a/14th-team5-iOS/App/Sources/Presentation/Content/WebContentViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Content/WebContentViewController.swift index 3b3eb7766..59abea96c 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Content/WebContentViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Content/WebContentViewController.swift @@ -14,7 +14,7 @@ import RxSwift import RxCocoa import ReactorKit -public class WebContentViewController: BaseViewController { +public class WebContentViewController: BaseViewController { private let webView: WKWebView = WKWebView() private let webViewIndicatorView: UIActivityIndicatorView = UIActivityIndicatorView(style: .medium) @@ -58,7 +58,7 @@ public class WebContentViewController: BaseViewController } - public override func bind(reactor: WebContentViewReactor) { + public override func bind(reactor: WebContentReactor) { super.bind(reactor: reactor) Observable.just(()) From 7107620cffcfd74ea91d0c7061030bc84430275e Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Wed, 26 Jun 2024 11:07:37 +0900 Subject: [PATCH 143/263] =?UTF-8?q?feat:=20JoinFamily=20=EA=B4=80=EB=A0=A8?= =?UTF-8?q?=20Wrapper=20=EA=B5=AC=ED=98=84=20(#580)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...InputFamilyLinkViewControllerWrapper.swift | 38 +++++++++++++++++++ .../JoinFamilyViewControllerWrapper.swift | 38 +++++++++++++++++++ .../JoinedFamilyViewControllerWrapper.swift | 38 +++++++++++++++++++ .../InputFamilyLinkDIContainer.swift | 3 +- .../Dependency/JoinFamilyDIContainer.swift | 3 +- .../Dependency/JoinedFamilyDIContainer.swift | 3 +- .../Reactor/InputFamilyLinkReactor.swift | 7 ++-- .../Reactor/JoinFamilyReactor.swift | 7 ++-- .../Reactor/JoinedFamilyReactor.swift | 4 +- 9 files changed, 128 insertions(+), 13 deletions(-) create mode 100644 14th-team5-iOS/App/Sources/Application/Wrapper/JoinFamily/InputFamilyLinkViewControllerWrapper.swift create mode 100644 14th-team5-iOS/App/Sources/Application/Wrapper/JoinFamily/JoinFamilyViewControllerWrapper.swift create mode 100644 14th-team5-iOS/App/Sources/Application/Wrapper/JoinFamily/JoinedFamilyViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/Wrapper/JoinFamily/InputFamilyLinkViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Wrapper/JoinFamily/InputFamilyLinkViewControllerWrapper.swift new file mode 100644 index 000000000..467e45f14 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/Wrapper/JoinFamily/InputFamilyLinkViewControllerWrapper.swift @@ -0,0 +1,38 @@ +// +// InputLinkViewControllerWrapper.swift +// App +// +// Created by 김건우 on 6/26/24. +// + +import Core +import Foundation + +final class InputFamilyLinkViewControllerWrapper: BaseWrapper { + + // MARK: - Typealias + + typealias R = InputFamilyLinkReactor + typealias V = InputFamilyLinkViewController + + // MARK: - Properties + + var reactor: InputFamilyLinkReactor { + InputFamilyLinkReactor() + } + + var viewController: InputFamilyLinkViewController { + InputFamilyLinkViewController(reactor: makeReactor()) + } + + // MARK: - Make + + func makeReactor() -> InputFamilyLinkReactor { + InputFamilyLinkReactor() + } + + func makeViewController() -> InputFamilyLinkViewController { + InputFamilyLinkViewController(reactor: makeReactor()) + } + +} diff --git a/14th-team5-iOS/App/Sources/Application/Wrapper/JoinFamily/JoinFamilyViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Wrapper/JoinFamily/JoinFamilyViewControllerWrapper.swift new file mode 100644 index 000000000..4cd4137bc --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/Wrapper/JoinFamily/JoinFamilyViewControllerWrapper.swift @@ -0,0 +1,38 @@ +// +// JoinFamilyViewControllerWrapper.swift +// App +// +// Created by 김건우 on 6/26/24. +// + +import Core +import Foundation + +final class JoinFamilyViewControllerWrapper: BaseWrapper { + + // MARK: - Typealias + + typealias R = JoinFamilyReactor + typealias V = JoinFamilyViewController + + // MARK: - Properties + + var reactor: JoinFamilyReactor { + makeReactor() + } + + var viewController: JoinFamilyViewController { + makeViewController() + } + + // MARK: - Make + + func makeReactor() -> JoinFamilyReactor { + JoinFamilyReactor() + } + + func makeViewController() -> JoinFamilyViewController { + JoinFamilyViewController(reactor: makeReactor()) + } + +} diff --git a/14th-team5-iOS/App/Sources/Application/Wrapper/JoinFamily/JoinedFamilyViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Wrapper/JoinFamily/JoinedFamilyViewControllerWrapper.swift new file mode 100644 index 000000000..914804609 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/Wrapper/JoinFamily/JoinedFamilyViewControllerWrapper.swift @@ -0,0 +1,38 @@ +// +// JoinedFamilyViewControllerWrapper.swift +// App +// +// Created by 김건우 on 6/26/24. +// + +import Core +import Foundation + +final class JoinedFamilyViewControllerWrapper: BaseWrapper { + + // MARK: - Typealias + + typealias R = JoinedFamilyReactor + typealias V = JoinedFamilyViewController + + // MARK: - Properties + + var reactor: JoinedFamilyReactor { + makeReactor() + } + + var viewController: JoinedFamilyViewController { + makeViewController() + } + + // MARK: - Make + + func makeReactor() -> JoinedFamilyReactor { + JoinedFamilyReactor() + } + + func makeViewController() -> JoinedFamilyViewController { + JoinedFamilyViewController(reactor: makeReactor()) + } + +} diff --git a/14th-team5-iOS/App/Sources/Presentation/JoinFamily/Dependency/InputFamilyLinkDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/JoinFamily/Dependency/InputFamilyLinkDIContainer.swift index 44f99f1c9..248602f0c 100644 --- a/14th-team5-iOS/App/Sources/Presentation/JoinFamily/Dependency/InputFamilyLinkDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/JoinFamily/Dependency/InputFamilyLinkDIContainer.swift @@ -11,6 +11,7 @@ import Core import Data import Domain +@available(*, deprecated, renamed: "InputFamilyLinkControllerWrapper") final class InputFamilyLinkDIContainer { public func makeViewController() -> InputFamilyLinkViewController { return InputFamilyLinkViewController(reactor: makeReactor()) @@ -25,7 +26,7 @@ final class InputFamilyLinkDIContainer { } public func makeReactor() -> InputFamilyLinkReactor { - return InputFamilyLinkReactor(initialState: .init(), familyUseCase: makeUseCase()) + return InputFamilyLinkReactor() } } diff --git a/14th-team5-iOS/App/Sources/Presentation/JoinFamily/Dependency/JoinFamilyDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/JoinFamily/Dependency/JoinFamilyDIContainer.swift index 97f532d56..537c1f0c1 100644 --- a/14th-team5-iOS/App/Sources/Presentation/JoinFamily/Dependency/JoinFamilyDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/JoinFamily/Dependency/JoinFamilyDIContainer.swift @@ -11,6 +11,7 @@ import Core import Data import Domain +@available(*, deprecated, renamed: "JoinFamilyViewControllerWrapper") final class JoinFamilyDIContainer { public func makeViewController() -> JoinFamilyViewController { return JoinFamilyViewController(reactor: makeReactor()) @@ -25,6 +26,6 @@ final class JoinFamilyDIContainer { } public func makeReactor() -> JoinFamilyReactor { - return JoinFamilyReactor(initialState: .init(), familyUseCase: makeUsecase()) + return JoinFamilyReactor() } } diff --git a/14th-team5-iOS/App/Sources/Presentation/JoinFamily/Dependency/JoinedFamilyDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/JoinFamily/Dependency/JoinedFamilyDIContainer.swift index aa3ddaadf..166c81d7b 100644 --- a/14th-team5-iOS/App/Sources/Presentation/JoinFamily/Dependency/JoinedFamilyDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/JoinFamily/Dependency/JoinedFamilyDIContainer.swift @@ -11,12 +11,13 @@ import Core import Data import Domain +@available(*, deprecated, renamed: "JoinedFamilyViewControllerWrapper") final class JoinedFamilyDIContainer { public func makeViewController() -> JoinedFamilyViewController { return JoinedFamilyViewController(reactor: makeReactor()) } public func makeReactor() -> JoinedFamilyReactor { - return JoinedFamilyReactor(initialState: .init()) + return JoinedFamilyReactor() } } diff --git a/14th-team5-iOS/App/Sources/Presentation/JoinFamily/Reactor/InputFamilyLinkReactor.swift b/14th-team5-iOS/App/Sources/Presentation/JoinFamily/Reactor/InputFamilyLinkReactor.swift index 78cf701ec..dd2f3db4a 100644 --- a/14th-team5-iOS/App/Sources/Presentation/JoinFamily/Reactor/InputFamilyLinkReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/JoinFamily/Reactor/InputFamilyLinkReactor.swift @@ -38,11 +38,10 @@ public final class InputFamilyLinkReactor: Reactor { // MARK: - Properties public let initialState: State - private let familyUseCase: FamilyUseCaseProtocol + @Injected var familyUseCase: FamilyUseCaseProtocol - init(initialState: State, familyUseCase: FamilyUseCaseProtocol) { - self.initialState = initialState - self.familyUseCase = familyUseCase + init() { + self.initialState = State() } } diff --git a/14th-team5-iOS/App/Sources/Presentation/JoinFamily/Reactor/JoinFamilyReactor.swift b/14th-team5-iOS/App/Sources/Presentation/JoinFamily/Reactor/JoinFamilyReactor.swift index 119caed41..dc5e79654 100644 --- a/14th-team5-iOS/App/Sources/Presentation/JoinFamily/Reactor/JoinFamilyReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/JoinFamily/Reactor/JoinFamilyReactor.swift @@ -33,11 +33,10 @@ public final class JoinFamilyReactor: Reactor { // MARK: - Properties public let initialState: State - private let familyUseCase: FamilyUseCaseProtocol + @Injected var familyUseCase: FamilyUseCaseProtocol - init(initialState: State, familyUseCase: FamilyUseCaseProtocol) { - self.initialState = initialState - self.familyUseCase = familyUseCase + init() { + self.initialState = State() } } diff --git a/14th-team5-iOS/App/Sources/Presentation/JoinFamily/Reactor/JoinedFamilyReactor.swift b/14th-team5-iOS/App/Sources/Presentation/JoinFamily/Reactor/JoinedFamilyReactor.swift index fdf1be6a3..49a335515 100644 --- a/14th-team5-iOS/App/Sources/Presentation/JoinFamily/Reactor/JoinedFamilyReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/JoinFamily/Reactor/JoinedFamilyReactor.swift @@ -27,8 +27,8 @@ public final class JoinedFamilyReactor: Reactor { } public var initialState: State - init(initialState: State) { - self.initialState = initialState + init() { + self.initialState = State() } } From cc2af0de88aca528ec3b941013b165c049080393 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Wed, 26 Jun 2024 16:01:14 +0900 Subject: [PATCH 144/263] =?UTF-8?q?feat:=20BaseBavigator=20=EB=B0=8F=20App?= =?UTF-8?q?Navigator=20=EA=B5=AC=ED=98=84=20(#563)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DIContainer/NavigatorDIContainer.swift | 35 +++++++++++++ .../Application/Navigator/AppNavigator.swift | 49 +++++++++++++++++++ .../Calendar/DailyCalendarDIContainer.swift | 0 ...MonthlyCalendarViewControllerWrapper.swift | 0 .../CameraDisplayViewControllerWrapper.swift | 0 .../Camera/CameraViewControllerWrapper.swift | 0 .../CommentViewControllerWrapper.swift | 0 ...InputFamilyLinkViewControllerWrapper.swift | 0 .../JoinFamilyViewControllerWrapper.swift | 0 .../JoinedFamilyViewControllerWrapper.swift | 0 .../MainFamilyViewControllerWrapper.swift | 0 .../Main/MainPostViewControllerWrapper.swift | 0 .../Main/MainViewControllerWrapper.swift | 0 .../Management/ManagementViewController.swift | 0 .../OnboardingViewControllerWrapper.swift | 0 .../PostDetail/PostDetailCellWrapper.swift | 0 .../PostDetailViewControllerWrapper.swift | 0 ...ReactionMembersViewControllerWrapper.swift | 0 .../ReactionViewControllerWrapper.swift | 0 ...SelectableEmojiViewControllerWrapper.swift | 0 .../ProfileDetailViewControllerWrapper.swift | 0 ...ProfileFeedPageViewControllerWrapper.swift | 0 .../ProfileFeedViewControllerWrapper.swift | 0 .../ProfileViewControllerWrapper.swift | 0 .../Splash/SplashViewControllerWrapper.swift | 0 .../WebContentViewControllerWrapper.swift | 0 .../Core/Sources/Base/BaseNavigator.swift | 12 +++++ 27 files changed, 96 insertions(+) create mode 100644 14th-team5-iOS/App/Sources/Application/DIContainer/NavigatorDIContainer.swift create mode 100644 14th-team5-iOS/App/Sources/Application/Navigator/AppNavigator.swift rename 14th-team5-iOS/App/Sources/Application/{ => Navigator}/Wrapper/Calendar/DailyCalendarDIContainer.swift (100%) rename 14th-team5-iOS/App/Sources/Application/{ => Navigator}/Wrapper/Calendar/MonthlyCalendarViewControllerWrapper.swift (100%) rename 14th-team5-iOS/App/Sources/Application/{ => Navigator}/Wrapper/Camera/CameraDisplayViewControllerWrapper.swift (100%) rename 14th-team5-iOS/App/Sources/Application/{ => Navigator}/Wrapper/Camera/CameraViewControllerWrapper.swift (100%) rename 14th-team5-iOS/App/Sources/Application/{ => Navigator}/Wrapper/Comment/CommentViewControllerWrapper.swift (100%) rename 14th-team5-iOS/App/Sources/Application/{ => Navigator}/Wrapper/JoinFamily/InputFamilyLinkViewControllerWrapper.swift (100%) rename 14th-team5-iOS/App/Sources/Application/{ => Navigator}/Wrapper/JoinFamily/JoinFamilyViewControllerWrapper.swift (100%) rename 14th-team5-iOS/App/Sources/Application/{ => Navigator}/Wrapper/JoinFamily/JoinedFamilyViewControllerWrapper.swift (100%) rename 14th-team5-iOS/App/Sources/Application/{ => Navigator}/Wrapper/Main/MainFamilyViewControllerWrapper.swift (100%) rename 14th-team5-iOS/App/Sources/Application/{ => Navigator}/Wrapper/Main/MainPostViewControllerWrapper.swift (100%) rename 14th-team5-iOS/App/Sources/Application/{ => Navigator}/Wrapper/Main/MainViewControllerWrapper.swift (100%) rename 14th-team5-iOS/App/Sources/Application/{ => Navigator}/Wrapper/Management/ManagementViewController.swift (100%) rename 14th-team5-iOS/App/Sources/Application/{ => Navigator}/Wrapper/Onboarding/OnboardingViewControllerWrapper.swift (100%) rename 14th-team5-iOS/App/Sources/Application/{ => Navigator}/Wrapper/PostDetail/PostDetailCellWrapper.swift (100%) rename 14th-team5-iOS/App/Sources/Application/{ => Navigator}/Wrapper/PostDetail/PostDetailViewControllerWrapper.swift (100%) rename 14th-team5-iOS/App/Sources/Application/{ => Navigator}/Wrapper/PostDetail/ReactionMembersViewControllerWrapper.swift (100%) rename 14th-team5-iOS/App/Sources/Application/{ => Navigator}/Wrapper/PostDetail/ReactionViewControllerWrapper.swift (100%) rename 14th-team5-iOS/App/Sources/Application/{ => Navigator}/Wrapper/PostDetail/SelectableEmojiViewControllerWrapper.swift (100%) rename 14th-team5-iOS/App/Sources/Application/{ => Navigator}/Wrapper/Profile/ProfileDetailViewControllerWrapper.swift (100%) rename 14th-team5-iOS/App/Sources/Application/{ => Navigator}/Wrapper/Profile/ProfileFeedPageViewControllerWrapper.swift (100%) rename 14th-team5-iOS/App/Sources/Application/{ => Navigator}/Wrapper/Profile/ProfileFeedViewControllerWrapper.swift (100%) rename 14th-team5-iOS/App/Sources/Application/{ => Navigator}/Wrapper/Profile/ProfileViewControllerWrapper.swift (100%) rename 14th-team5-iOS/App/Sources/Application/{ => Navigator}/Wrapper/Splash/SplashViewControllerWrapper.swift (100%) rename 14th-team5-iOS/App/Sources/Application/{ => Navigator}/Wrapper/WebContent/WebContentViewControllerWrapper.swift (100%) create mode 100644 14th-team5-iOS/Core/Sources/Base/BaseNavigator.swift diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/NavigatorDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/NavigatorDIContainer.swift new file mode 100644 index 000000000..26dd82f07 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/NavigatorDIContainer.swift @@ -0,0 +1,35 @@ +// +// NavigatorDIContainer.swift +// App +// +// Created by 김건우 on 6/26/24. +// + +import Core +import Foundation +import UIKit + +final class NavigatorDIContainer: BaseContainer { + + // MARK: - Make UI + + private func makeUIWindow() -> UIWindow { + try! container.resolve(type: UIWindow.self) + } + + private func makeUINavigationController() -> UINavigationController { + try! container.resolve(type: UINavigationController.self) + } + + // MARK: - Register + + func registerDependencies() { + container.register(type: AppNavigatorProtocol.self) { _ in + AppNavigator( + window: makeUIWindow(), + navigationController: makeUINavigationController() + ) + } + } + +} diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/AppNavigator.swift b/14th-team5-iOS/App/Sources/Application/Navigator/AppNavigator.swift new file mode 100644 index 000000000..bd5c54f1d --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/Navigator/AppNavigator.swift @@ -0,0 +1,49 @@ +// +// AppNavigator.swift +// App +// +// Created by 김건우 on 6/26/24. +// + +import Core +import UIKit + +protocol AppNavigatorProtocol: BaseNavigator { + func toSplash() + func toHome() +} + +final class AppNavigator: AppNavigatorProtocol { + + // MARK: - Properties + + var window: UIWindow + var navigationController: UINavigationController + + // MARK: - Intializer + + init( + window: UIWindow, + navigationController: UINavigationController + ) { + self.window = window + self.navigationController = navigationController + } + + // MARK: - To + + func toSplash() { + let vc = SplashViewControllerWrapper().viewController + navigationController.setViewControllers([vc], animated: false) + window.rootViewController = vc + window.makeKeyAndVisible() + } + + func toHome() { + let vc = MainViewControllerWrapper().viewController + navigationController.setViewControllers([vc], animated: false) + window.rootViewController = vc + window.makeKeyAndVisible() + } + +} diff --git a/14th-team5-iOS/App/Sources/Application/Wrapper/Calendar/DailyCalendarDIContainer.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Calendar/DailyCalendarDIContainer.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Wrapper/Calendar/DailyCalendarDIContainer.swift rename to 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Calendar/DailyCalendarDIContainer.swift diff --git a/14th-team5-iOS/App/Sources/Application/Wrapper/Calendar/MonthlyCalendarViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Calendar/MonthlyCalendarViewControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Wrapper/Calendar/MonthlyCalendarViewControllerWrapper.swift rename to 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Calendar/MonthlyCalendarViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/Wrapper/Camera/CameraDisplayViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Camera/CameraDisplayViewControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Wrapper/Camera/CameraDisplayViewControllerWrapper.swift rename to 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Camera/CameraDisplayViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/Wrapper/Camera/CameraViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Camera/CameraViewControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Wrapper/Camera/CameraViewControllerWrapper.swift rename to 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Camera/CameraViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/Wrapper/Comment/CommentViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Comment/CommentViewControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Wrapper/Comment/CommentViewControllerWrapper.swift rename to 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Comment/CommentViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/Wrapper/JoinFamily/InputFamilyLinkViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/JoinFamily/InputFamilyLinkViewControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Wrapper/JoinFamily/InputFamilyLinkViewControllerWrapper.swift rename to 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/JoinFamily/InputFamilyLinkViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/Wrapper/JoinFamily/JoinFamilyViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/JoinFamily/JoinFamilyViewControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Wrapper/JoinFamily/JoinFamilyViewControllerWrapper.swift rename to 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/JoinFamily/JoinFamilyViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/Wrapper/JoinFamily/JoinedFamilyViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/JoinFamily/JoinedFamilyViewControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Wrapper/JoinFamily/JoinedFamilyViewControllerWrapper.swift rename to 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/JoinFamily/JoinedFamilyViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/Wrapper/Main/MainFamilyViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Main/MainFamilyViewControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Wrapper/Main/MainFamilyViewControllerWrapper.swift rename to 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Main/MainFamilyViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/Wrapper/Main/MainPostViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Main/MainPostViewControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Wrapper/Main/MainPostViewControllerWrapper.swift rename to 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Main/MainPostViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/Wrapper/Main/MainViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Main/MainViewControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Wrapper/Main/MainViewControllerWrapper.swift rename to 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Main/MainViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/Wrapper/Management/ManagementViewController.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Management/ManagementViewController.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Wrapper/Management/ManagementViewController.swift rename to 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Management/ManagementViewController.swift diff --git a/14th-team5-iOS/App/Sources/Application/Wrapper/Onboarding/OnboardingViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Onboarding/OnboardingViewControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Wrapper/Onboarding/OnboardingViewControllerWrapper.swift rename to 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Onboarding/OnboardingViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/Wrapper/PostDetail/PostDetailCellWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/PostDetail/PostDetailCellWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Wrapper/PostDetail/PostDetailCellWrapper.swift rename to 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/PostDetail/PostDetailCellWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/Wrapper/PostDetail/PostDetailViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/PostDetail/PostDetailViewControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Wrapper/PostDetail/PostDetailViewControllerWrapper.swift rename to 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/PostDetail/PostDetailViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/Wrapper/PostDetail/ReactionMembersViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/PostDetail/ReactionMembersViewControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Wrapper/PostDetail/ReactionMembersViewControllerWrapper.swift rename to 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/PostDetail/ReactionMembersViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/Wrapper/PostDetail/ReactionViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/PostDetail/ReactionViewControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Wrapper/PostDetail/ReactionViewControllerWrapper.swift rename to 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/PostDetail/ReactionViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/Wrapper/PostDetail/SelectableEmojiViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/PostDetail/SelectableEmojiViewControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Wrapper/PostDetail/SelectableEmojiViewControllerWrapper.swift rename to 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/PostDetail/SelectableEmojiViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/Wrapper/Profile/ProfileDetailViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Profile/ProfileDetailViewControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Wrapper/Profile/ProfileDetailViewControllerWrapper.swift rename to 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Profile/ProfileDetailViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/Wrapper/Profile/ProfileFeedPageViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Profile/ProfileFeedPageViewControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Wrapper/Profile/ProfileFeedPageViewControllerWrapper.swift rename to 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Profile/ProfileFeedPageViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/Wrapper/Profile/ProfileFeedViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Profile/ProfileFeedViewControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Wrapper/Profile/ProfileFeedViewControllerWrapper.swift rename to 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Profile/ProfileFeedViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/Wrapper/Profile/ProfileViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Profile/ProfileViewControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Wrapper/Profile/ProfileViewControllerWrapper.swift rename to 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Profile/ProfileViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/Wrapper/Splash/SplashViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Splash/SplashViewControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Wrapper/Splash/SplashViewControllerWrapper.swift rename to 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Splash/SplashViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/Wrapper/WebContent/WebContentViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/WebContent/WebContentViewControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Wrapper/WebContent/WebContentViewControllerWrapper.swift rename to 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/WebContent/WebContentViewControllerWrapper.swift diff --git a/14th-team5-iOS/Core/Sources/Base/BaseNavigator.swift b/14th-team5-iOS/Core/Sources/Base/BaseNavigator.swift new file mode 100644 index 000000000..979450a37 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Base/BaseNavigator.swift @@ -0,0 +1,12 @@ +// +// BaseNavigator.swift +// Core +// +// Created by 김건우 on 6/26/24. +// + +import UIKit + +public protocol BaseNavigator { + var navigationController: UINavigationController { get set } +} From 410aaae92004af67faab7d45dc405e38ab0411e0 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Wed, 26 Jun 2024 16:37:27 +0900 Subject: [PATCH 145/263] =?UTF-8?q?feat:=20SplashNavigator=20=EB=B0=8F=20N?= =?UTF-8?q?avigator=20=ED=94=84=EB=A1=9C=ED=8D=BC=ED=8B=B0=20=EB=9E=98?= =?UTF-8?q?=ED=8D=BC=20=EA=B5=AC=ED=98=84=20(#563)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DIContainer/NavigatorDIContainer.swift | 6 ++ .../Application/Navigator/AppNavigator.swift | 4 +- .../Navigator/SplashNavigator.swift | 50 +++++++++++++ .../Splash/SplashViewController.swift | 5 ++ .../Core/Sources/DIContainer/Container.swift | 70 ------------------- .../Injected+PropertyWrapper.swift | 45 ++++++++++++ .../InjectedSafe+PropertyWrapper.swift | 32 +++++++++ .../Navigator+PropertyWrapper.swift | 46 ++++++++++++ 8 files changed, 186 insertions(+), 72 deletions(-) create mode 100644 14th-team5-iOS/App/Sources/Application/Navigator/SplashNavigator.swift create mode 100644 14th-team5-iOS/Core/Sources/DIContainer/Injected+PropertyWrapper.swift create mode 100644 14th-team5-iOS/Core/Sources/DIContainer/InjectedSafe+PropertyWrapper.swift create mode 100644 14th-team5-iOS/Core/Sources/DIContainer/Navigator+PropertyWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/NavigatorDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/NavigatorDIContainer.swift index 26dd82f07..00ac9b1e5 100644 --- a/14th-team5-iOS/App/Sources/Application/DIContainer/NavigatorDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/NavigatorDIContainer.swift @@ -30,6 +30,12 @@ final class NavigatorDIContainer: BaseContainer { navigationController: makeUINavigationController() ) } + + container.register(type: SplashNavigatorProtocol.self) { _ in + SplashNavigator( + navigationController: makeUINavigationController() + ) + } } } diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/AppNavigator.swift b/14th-team5-iOS/App/Sources/Application/Navigator/AppNavigator.swift index bd5c54f1d..d416cbc70 100644 --- a/14th-team5-iOS/App/Sources/Application/Navigator/AppNavigator.swift +++ b/14th-team5-iOS/App/Sources/Application/Navigator/AppNavigator.swift @@ -35,14 +35,14 @@ final class AppNavigator: AppNavigatorProtocol { func toSplash() { let vc = SplashViewControllerWrapper().viewController navigationController.setViewControllers([vc], animated: false) - window.rootViewController = vc + window.rootViewController = navigationController window.makeKeyAndVisible() } func toHome() { let vc = MainViewControllerWrapper().viewController navigationController.setViewControllers([vc], animated: false) - window.rootViewController = vc + window.rootViewController = navigationController window.makeKeyAndVisible() } diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/SplashNavigator.swift b/14th-team5-iOS/App/Sources/Application/Navigator/SplashNavigator.swift new file mode 100644 index 000000000..e2c9ccdc5 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/Navigator/SplashNavigator.swift @@ -0,0 +1,50 @@ +// +// SplashNavigator.swift +// App +// +// Created by 김건우 on 6/26/24. +// + +import Core +import UIKit + +protocol SplashNavigatorProtocol: BaseNavigator { + func toHome() + func toJoined() + func toSignIn() + func toOnboarding() +} + +final class SplashNavigator: SplashNavigatorProtocol { + // MARK: - Properties + + var navigationController: UINavigationController + + // MARK: - Intializer + + init(navigationController: UINavigationController) { + self.navigationController = navigationController + } + + // MARK: - To + + func toHome() { + let vc = MainViewControllerWrapper().viewController + navigationController.setViewControllers([vc], animated: false) + } + + func toJoined() { + let vc = JoinedFamilyViewControllerWrapper().viewController + navigationController.setViewControllers([vc], animated: false) + } + + func toSignIn() { + // ... + } + + func toOnboarding() { + let vc = SplashViewControllerWrapper().viewController + navigationController.setViewControllers([vc], animated: false) + } + +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Splash/SplashViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Splash/SplashViewController.swift index cb76f57bb..a539f5c81 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Splash/SplashViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Splash/SplashViewController.swift @@ -95,6 +95,8 @@ public final class SplashViewController: BaseViewController { guard let sceneDelegate = UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate else { return } var container: UINavigationController +// @Navigator var splashNavigator: SplashNavigatorProtocol + guard let member = member else { container = UINavigationController(rootViewController: AccountSignInDIContainer().makeViewController()) sceneDelegate.window?.rootViewController = container @@ -104,14 +106,17 @@ public final class SplashViewController: BaseViewController { if let _ = member.familyId { if UserDefaults.standard.inviteCode != nil { +// splashNavigator.toJoined() container = UINavigationController(rootViewController: JoinedFamilyDIContainer().makeViewController()) } else { +// splashNavigator.toHome() container = UINavigationController(rootViewController: MainViewControllerWrapper().makeViewController()) } sceneDelegate.window?.rootViewController = container sceneDelegate.window?.makeKeyAndVisible() return } else { +// splashNavigator.toOnboarding() container = UINavigationController(rootViewController: OnBoardingDIContainer().makeViewController()) sceneDelegate.window?.rootViewController = container sceneDelegate.window?.makeKeyAndVisible() diff --git a/14th-team5-iOS/Core/Sources/DIContainer/Container.swift b/14th-team5-iOS/Core/Sources/DIContainer/Container.swift index 093ef2b45..f759234b6 100644 --- a/14th-team5-iOS/Core/Sources/DIContainer/Container.swift +++ b/14th-team5-iOS/Core/Sources/DIContainer/Container.swift @@ -19,73 +19,3 @@ public class Container: Injectable { required public init() { } } - - -// MARK: - Property Wrapper - -@propertyWrapper -public struct Injected { - - // MARK: - Container - public static func container() -> Injectable { - Container.standard - } - - // MARK: - Properties - private let identifier: InjectIdentifier - private let container: Resolvable - private let `default`: V? - - // MARK: - Intializer - public init( - identifier: InjectIdentifier? = nil, - container: Resolvable? = nil, - default: V? = nil - ) { - self.identifier = identifier ?? .by(type: V.self) - self.container = container ?? Self.container() - self.default = `default` - } - - // MARK: - Wrapped Value - public lazy var wrappedValue: V = { - if let value = try? container.resolve(identifier) { - return value - } - - if let `default` { - return `default` - } - - fatalError("Could not resolve with \(identifier) and default is nil") - }() - -} - - - -@propertyWrapper -public struct InejctedSafe { - - // MARK: - Container - public static func container() -> Injectable { - Container.standard - } - - // MARK: - Properties - private let identifier: InjectIdentifier - private let container: Resolvable - - // MARK: - Intiaializer - public init( - identifier: InjectIdentifier? = nil, - container: Resolvable? = nil - ) { - self.identifier = identifier ?? .by(type: V.self) - self.container = container ?? Self.container() - } - - // MARK: - WrappedValue - public lazy var wrappedValue: V? = try? container.resolve(identifier) - -} diff --git a/14th-team5-iOS/Core/Sources/DIContainer/Injected+PropertyWrapper.swift b/14th-team5-iOS/Core/Sources/DIContainer/Injected+PropertyWrapper.swift new file mode 100644 index 000000000..67bde7480 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/DIContainer/Injected+PropertyWrapper.swift @@ -0,0 +1,45 @@ +// +// Injected+PropertyWrapper.swift +// Core +// +// Created by 김건우 on 6/26/24. +// + +@propertyWrapper +public struct Injected { + + // MARK: - Container + public static func container() -> Injectable { + Container.standard + } + + // MARK: - Properties + private let identifier: InjectIdentifier + private let container: Resolvable + private let `default`: V? + + // MARK: - Intializer + public init( + identifier: InjectIdentifier? = nil, + container: Resolvable? = nil, + default: V? = nil + ) { + self.identifier = identifier ?? .by(type: V.self) + self.container = container ?? Self.container() + self.default = `default` + } + + // MARK: - Wrapped Value + public lazy var wrappedValue: V = { + if let value = try? container.resolve(identifier) { + return value + } + + if let `default` { + return `default` + } + + fatalError("Could not resolve with \(identifier) and default is nil") + }() + +} diff --git a/14th-team5-iOS/Core/Sources/DIContainer/InjectedSafe+PropertyWrapper.swift b/14th-team5-iOS/Core/Sources/DIContainer/InjectedSafe+PropertyWrapper.swift new file mode 100644 index 000000000..1e9066ccf --- /dev/null +++ b/14th-team5-iOS/Core/Sources/DIContainer/InjectedSafe+PropertyWrapper.swift @@ -0,0 +1,32 @@ +// +// InjectedSafe+PropertyWrapper.swift +// Core +// +// Created by 김건우 on 6/26/24. +// + +@propertyWrapper +public struct InjectedSafe { + + // MARK: - Container + public static func container() -> Injectable { + Container.standard + } + + // MARK: - Properties + private let identifier: InjectIdentifier + private let container: Resolvable + + // MARK: - Intiaializer + public init( + identifier: InjectIdentifier? = nil, + container: Resolvable? = nil + ) { + self.identifier = identifier ?? .by(type: V.self) + self.container = container ?? Self.container() + } + + // MARK: - WrappedValue + public lazy var wrappedValue: V? = try? container.resolve(identifier) + +} diff --git a/14th-team5-iOS/Core/Sources/DIContainer/Navigator+PropertyWrapper.swift b/14th-team5-iOS/Core/Sources/DIContainer/Navigator+PropertyWrapper.swift new file mode 100644 index 000000000..a66774558 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/DIContainer/Navigator+PropertyWrapper.swift @@ -0,0 +1,46 @@ +// +// Navigator+PropertyWrapper.swift +// Core +// +// Created by 김건우 on 6/26/24. +// + +@propertyWrapper +public struct Navigator { + + // MARK: - Container + public static func container() -> Injectable { + Container.standard + } + + // MARK: - Properties + private let identifier: InjectIdentifier + private let container: Resolvable + private let `default`: V? + + // MARK: - Intializer + public init( + identifier: InjectIdentifier? = nil, + container: Resolvable? = nil, + default: V? = nil + ) { + self.identifier = identifier ?? .by(type: V.self) + self.container = container ?? Self.container() + self.default = `default` + } + + // MARK: - Wrapped Value + public lazy var wrappedValue: V = { + if let value = try? container.resolve(identifier) { + return value + } + + if let `default` { + return `default` + } + + fatalError("Could not resolve with \(identifier) and default is nil") + }() + +} + From d6775a5a87d8605b886186392216519343088549 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Wed, 26 Jun 2024 21:30:10 +0900 Subject: [PATCH 146/263] =?UTF-8?q?feat:=20MonthlyCalendarNavigator=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#563)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DIContainer/NavigatorDIContainer.swift | 6 +++ .../Navigator/MonthlyCalendarNavigator.swift | 41 ++++++++++++++++ .../Calendar/DailyCalendarDIContainer.swift | 9 ++-- .../Sources/Application/SceneDelegate.swift | 49 ++++++++++++++----- .../Reactor/CalendarPageViewCellReactor.swift | 15 ++---- .../Reactor/MonthlyCalendarViewReactor.swift | 2 + .../Splash/SplashViewController.swift | 3 +- 7 files changed, 97 insertions(+), 28 deletions(-) create mode 100644 14th-team5-iOS/App/Sources/Application/Navigator/MonthlyCalendarNavigator.swift diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/NavigatorDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/NavigatorDIContainer.swift index 00ac9b1e5..340d41527 100644 --- a/14th-team5-iOS/App/Sources/Application/DIContainer/NavigatorDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/NavigatorDIContainer.swift @@ -36,6 +36,12 @@ final class NavigatorDIContainer: BaseContainer { navigationController: makeUINavigationController() ) } + + container.register(type: MonthlyCalendarNavigatorProtocol.self) { _ in + MonthlyCalendarNavigator( + navigationController: makeUINavigationController() + ) + } } } diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/MonthlyCalendarNavigator.swift b/14th-team5-iOS/App/Sources/Application/Navigator/MonthlyCalendarNavigator.swift new file mode 100644 index 000000000..39ebe18a4 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/Navigator/MonthlyCalendarNavigator.swift @@ -0,0 +1,41 @@ +// +// MonthlyCalendarNavigator.swift +// App +// +// Created by 김건우 on 6/26/24. +// + +import Core +import UIKit + +protocol MonthlyCalendarNavigatorProtocol: BaseNavigator { + func backToHome() + func toDailyCalendar(selection date: Date) +} + +final class MonthlyCalendarNavigator: MonthlyCalendarNavigatorProtocol { + + // MARK: - Properties + + var navigationController: UINavigationController + + // MARK: - Intializer + + init(navigationController: UINavigationController) { + self.navigationController = navigationController + } + + // MARK: - Back + + func backToHome() { + navigationController.popViewController(animated: true) + } + + // MARK: - To + + func toDailyCalendar(selection date: Date) { + let vc = DailyCalendarViewControllerWrapper(selection: date).viewController + navigationController.pushViewController(vc, animated: true) + } + +} diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Calendar/DailyCalendarDIContainer.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Calendar/DailyCalendarDIContainer.swift index a80e367a3..22a7f9b45 100644 --- a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Calendar/DailyCalendarDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Calendar/DailyCalendarDIContainer.swift @@ -8,7 +8,7 @@ import Core import Foundation -final class DailyCalendarDIContainer: BaseWrapper { +final class DailyCalendarViewControllerWrapper: BaseWrapper { // MARK: - Typealias @@ -18,7 +18,7 @@ final class DailyCalendarDIContainer: BaseWrapper { // MARK: - Properties let date: Date - let link: NotificationDeepLink? + let link: NotificationDeepLink? // TODO: - link 지우기 var reactor: DailyCalendarViewReactor { makeReactor() @@ -30,7 +30,10 @@ final class DailyCalendarDIContainer: BaseWrapper { // MARK: - Intializer - init(date: Date, link: NotificationDeepLink? = nil) { + init( + selection date: Date, + link: NotificationDeepLink? = nil + ) { self.date = date self.link = link } diff --git a/14th-team5-iOS/App/Sources/Application/SceneDelegate.swift b/14th-team5-iOS/App/Sources/Application/SceneDelegate.swift index d141e7911..6444e6947 100644 --- a/14th-team5-iOS/App/Sources/Application/SceneDelegate.swift +++ b/14th-team5-iOS/App/Sources/Application/SceneDelegate.swift @@ -18,32 +18,33 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? + @Navigator var navigator: AppNavigatorProtocol + + + // MARK: - WillConnectTo + func scene( _ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions ) { guard let scene = (scene as? UIWindowScene) else { return } - + + setupUI(scene: scene) + // For when app is terminated if let url = connectionOptions.urlContexts.first?.url { if let deepLink = decodeWidgetDeepLink(url) { DeepLinkManager.shared.handleWidgetDeepLink(data: deepLink) } - - self.window = UIWindow(windowScene: scene) - self.window?.rootViewController = UINavigationController(rootViewController: MainViewControllerWrapper().makeViewController()) - self.window?.makeKeyAndVisible() + navigator.toHome() } guard let userActivity = connectionOptions.userActivities.first, userActivity.activityType == NSUserActivityTypeBrowsingWeb, let incomingURL = userActivity.webpageURL, let components = NSURLComponents(url: incomingURL, resolvingAgainstBaseURL: true) else { - - window = UIWindow(windowScene: scene) - window?.rootViewController = UINavigationController(rootViewController: SplashDIContainer().makeViewController()) - window?.makeKeyAndVisible() + navigator.toSplash() return } @@ -54,12 +55,12 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { let inviteCode = pathComponents[2] UserDefaults.standard.inviteCode = inviteCode } - - window = UIWindow(windowScene: scene) - window?.rootViewController = UINavigationController(rootViewController: SplashDIContainer().makeViewController()) - window?.makeKeyAndVisible() + navigator.toSplash() } + + // MARK: - OpenUrlContext + // For when app is background or foreground func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { if let url = URLContexts.first?.url { @@ -76,6 +77,28 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } } + +// MARK: - Extensions + +extension SceneDelegate { + + func setupUI(scene: UIWindowScene) { + let container = Container.standard + let window = UIWindow(windowScene: scene) + let navigationController = UINavigationController() + self.window = window + + container.register(type: UIWindow.self) { _ in window } + container.register(type: UINavigationController.self) { _ in navigationController } + + NavigatorDIContainer().registerDependencies() + } + +} + + + +// TODO: - 없애버리기 extension SceneDelegate { private func decodeWidgetDeepLink(_ url: URL) -> WidgetDeepLink? { let urlString = url.absoluteString diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPageViewCellReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPageViewCellReactor.swift index f271a0628..0db98dea5 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPageViewCellReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPageViewCellReactor.swift @@ -41,31 +41,24 @@ public final class CalendarCellReactor: Reactor { // MARK: - Properties public var initialState: State - @Injected var calendarUseCase: CalendarUseCaseProtocol @Injected var provider: GlobalStateProviderProtocol + @Injected var calendarUseCase: CalendarUseCaseProtocol -// private let calendarUseCase: CalendarUseCaseProtocol -// private let provider: GlobalStateProviderProtocol + @Navigator var navigator: MonthlyCalendarNavigatorProtocol // MARK: - Intializer - init( - yearMonth: String -// calendarUseCase: CalendarUseCaseProtocol, -// provider: GlobalStateProviderProtocol - ) { + init(yearMonth: String) { self.initialState = State( yearMonth: yearMonth, displayMemoryCount: 0 ) - -// self.calendarUseCase = calendarUseCase -// self.provider = provider } // MARK: - Mutate public func mutate(action: Action) -> Observable { switch action { case let .dateSelected(date): + // navigator.toDailyCalendar(selection: date) return provider.calendarGlabalState.pushCalendarPostVC(date) .flatMap { _ in Observable.empty() } diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/MonthlyCalendarViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/MonthlyCalendarViewReactor.swift index 523af4591..9763db5b5 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/MonthlyCalendarViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/MonthlyCalendarViewReactor.swift @@ -43,6 +43,8 @@ public final class MonthlyCalendarViewReactor: Reactor { @Injected var provider: GlobalStateProviderProtocol @Injected var calendarUseCase: CalendarUseCaseProtocol + @Navigator var navigator: MonthlyCalendarNavigatorProtocol + // MARK: - Intializer init() { self.initialState = State( diff --git a/14th-team5-iOS/App/Sources/Presentation/Splash/SplashViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Splash/SplashViewController.swift index a539f5c81..1deb17ee5 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Splash/SplashViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Splash/SplashViewController.swift @@ -95,7 +95,8 @@ public final class SplashViewController: BaseViewController { guard let sceneDelegate = UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate else { return } var container: UINavigationController -// @Navigator var splashNavigator: SplashNavigatorProtocol + // TODO: - Reactor로 집어넣기 + @Navigator var splashNavigator: SplashNavigatorProtocol guard let member = member else { container = UINavigationController(rootViewController: AccountSignInDIContainer().makeViewController()) From e82c43bde48e14505a9ff164810f3dbbc9c11ae6 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Wed, 26 Jun 2024 22:04:10 +0900 Subject: [PATCH 147/263] =?UTF-8?q?fix:=20RefreshTokenAPI=20Header=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#563)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Data/Sources/APIs/OAuth/OAuthAPI/OAuthAPIWorker.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/OAuthAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/OAuthAPIWorker.swift index 930cb9aec..3682d4eb4 100644 --- a/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/OAuthAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/OAuthAPIWorker.swift @@ -33,8 +33,9 @@ extension OAuthAPIWorker { public func refreshAccessToken(body: RefreshAccessTokenRequestDTO) -> Single { let spec = OAuthAPIs.refreshToken.spec + let headers = BibbiHeader.commonHeaders() // TODO: - Header 없애기 - return request(spec: spec, jsonEncodable: body) + return request(spec: spec, headers: headers, jsonEncodable: body) .subscribe(on: Self.queue) .do { if let str = String(data: $0.1, encoding: .utf8) { From 4a48e8a3edeff7dd463de6b3840ad6408fdaadba Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Wed, 26 Jun 2024 22:34:37 +0900 Subject: [PATCH 148/263] =?UTF-8?q?feat:=20SignInViewControllerWrapper=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#537)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Navigator/SplashNavigator.swift | 3 +- .../SignIn/SignInViewControllerWrapper.swift | 40 +++++++++++++++++++ .../AccountSignInDIContainer.swift | 3 +- .../AccountSignIn/AccountSignInReactor.swift | 10 ++--- 4 files changed, 49 insertions(+), 7 deletions(-) create mode 100644 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/SignIn/SignInViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/SplashNavigator.swift b/14th-team5-iOS/App/Sources/Application/Navigator/SplashNavigator.swift index e2c9ccdc5..ba1a664f1 100644 --- a/14th-team5-iOS/App/Sources/Application/Navigator/SplashNavigator.swift +++ b/14th-team5-iOS/App/Sources/Application/Navigator/SplashNavigator.swift @@ -39,7 +39,8 @@ final class SplashNavigator: SplashNavigatorProtocol { } func toSignIn() { - // ... + let vc = SignInViewControllerWrapper().viewController + navigationController.setViewControllers([vc], animated: false) } func toOnboarding() { diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/SignIn/SignInViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/SignIn/SignInViewControllerWrapper.swift new file mode 100644 index 000000000..d9f16ae71 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/SignIn/SignInViewControllerWrapper.swift @@ -0,0 +1,40 @@ +// +// SignInViewControllerWrapper.swift +// App +// +// Created by 김건우 on 6/26/24. +// + +import Core +import Foundation + +// TODO: - SignInViewReactor로 이름 수정하기 +// TODO: - SignInViewController로 이름 수정하기 +final class SignInViewControllerWrapper: BaseWrapper { + + // MARK: - Typealias + + typealias R = AccountSignInReactor + typealias V = AccountSignInViewController + + // MARK: - Properties + + var reactor: AccountSignInReactor { + makeReactor() + } + + var viewController: AccountSignInViewController { + makeViewController() + } + + // MARK: - Make + + func makeReactor() -> AccountSignInReactor { + AccountSignInReactor() + } + + func makeViewController() -> AccountSignInViewController { + AccountSignInViewController(reactor: makeReactor()) + } + +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignIn/AccountSignInDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignIn/AccountSignInDIContainer.swift index 661f61ae9..4406fc4c6 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignIn/AccountSignInDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignIn/AccountSignInDIContainer.swift @@ -11,6 +11,7 @@ import Data import Domain import Core +@available(*, deprecated, renamed: "SignInViewControllerWrapper") public final class AccountSignInDIContainer: BaseDIContainer { public typealias ViewContrller = AccountSignInViewController public typealias Repository = AccountImpl @@ -23,7 +24,7 @@ public final class AccountSignInDIContainer: BaseDIContainer { return AccountRepository() } public func makeReactor() -> Reactor { - return AccountSignInReactor(accountRepository: AccountRepository(), fcmUseCase: makeFCMUseCase()) + return AccountSignInReactor(/*accountRepository: AccountRepository(), fcmUseCase: makeFCMUseCase()*/) } } diff --git a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignIn/AccountSignInReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignIn/AccountSignInReactor.swift index 5f9e452a5..6415c501f 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignIn/AccountSignInReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignIn/AccountSignInReactor.swift @@ -15,8 +15,8 @@ import FirebaseMessaging public final class AccountSignInReactor: Reactor { public var initialState: State - private var accountRepository: AccountImpl - private let fcmUseCase: FCMUseCaseProtocol + private var accountRepository: AccountImpl = AccountRepository() + private let fcmUseCase: FCMUseCaseProtocol = FCMUseCase(FCMRepository: MeAPIs.Worker()) private let disposeBag = DisposeBag() public enum Action { @@ -33,9 +33,9 @@ public final class AccountSignInReactor: Reactor { var pushAccountSingUpVC: Bool } - init(accountRepository: AccountRepository, fcmUseCase: FCMUseCaseProtocol) { - self.accountRepository = accountRepository - self.fcmUseCase = fcmUseCase + init(/*accountRepository: AccountRepository, fcmUseCase: FCMUseCaseProtocol*/) { +// self.accountRepository = accountRepository +// self.fcmUseCase = fcmUseCase self.initialState = State(pushAccountSingUpVC: false) } } From 6b7aedcb77f77461a7b63b89463e4cb4f12f79ac Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Wed, 26 Jun 2024 23:17:15 +0900 Subject: [PATCH 149/263] =?UTF-8?q?feat:=20BBNavigationViewController?= =?UTF-8?q?=EC=9D=98=20PopViewController=20=EB=A1=9C=EC=A7=81=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#563)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Base/BBNavigationViewController.swift | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/14th-team5-iOS/Core/Sources/Base/BBNavigationViewController.swift b/14th-team5-iOS/Core/Sources/Base/BBNavigationViewController.swift index 91ac5f41a..50ddf4f96 100644 --- a/14th-team5-iOS/Core/Sources/Base/BBNavigationViewController.swift +++ b/14th-team5-iOS/Core/Sources/Base/BBNavigationViewController.swift @@ -35,6 +35,12 @@ open class BBNavigationViewController: ReactorViewController where R: Reac public let navigationBarView = BBNavigationBarView() public let contentView = UIView() + // MARK: - Properties + + /// 왼쪽 버튼이 특정 타입시, popViewController 기본 구현을 제공할 지 여부를 결정합니다. + /// 기본값은 true입니다. + public var enableAutoPopViewController = true + // MARK: - Intitalizer public override init() { super.init() @@ -125,11 +131,13 @@ extension BBNavigationViewController { extension BBNavigationViewController { private func popViewController(_ ifTypeIsXMark: TopBarButtonStyle?) { - switch ifTypeIsXMark { - case .arrowLeft, .xmark: - self.navigationController?.popViewController(animated: true) - @unknown default: - return + if enableAutoPopViewController { + switch ifTypeIsXMark { + case .arrowLeft, .xmark: + self.navigationController?.popViewController(animated: true) + @unknown default: + return + } } } From 738852c0eefdffca77303a10d77b52838fce9ca9 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Thu, 27 Jun 2024 02:51:53 +0900 Subject: [PATCH 150/263] =?UTF-8?q?feat:=20DailyCalendarNavigator=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#563)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Navigator/DailyCalendarNavigator.swift | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 14th-team5-iOS/App/Sources/Application/Navigator/DailyCalendarNavigator.swift diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/DailyCalendarNavigator.swift b/14th-team5-iOS/App/Sources/Application/Navigator/DailyCalendarNavigator.swift new file mode 100644 index 000000000..b24bc4078 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/Navigator/DailyCalendarNavigator.swift @@ -0,0 +1,50 @@ +// +// DailyCalendarNavigator.swift +// App +// +// Created by 김건우 on 6/27/24. +// + +import Core +import UIKit + +protocol DailyCalendarNavigatorProtocol: BaseNavigator { + func toProfile(memberId: String) + func toComment(postId: String) + func dismissComment(completion: (() -> Void)?) +} + +final class DailyCalendarNavigator: DailyCalendarNavigatorProtocol { + + // MARK: - Properties + + var navigationController: UINavigationController + + // MARK: - Intializer + + init(navigationController: UINavigationController) { + self.navigationController = navigationController + } + + // MARK: - To + + func toProfile(memberId: String) { + let vc = ProfileViewControllerWrapper(memberId: memberId).viewController + navigationController.pushViewController(vc, animated: true) + } + + func toComment(postId: String) { + let vc = CommentViewControllerWrapper(postId: postId).viewController + navigationController.presentPostCommentSheet(vc, from: .calendar) + // TODO: - present 메서드 수정하기 + } + + // MARK: - Back + + func dismissComment(completion: (() -> Void)? = nil) { + navigationController.dismiss(animated: true) { + completion?() + } + } + +} From ec1e401e92d496c23790b04b620dbd206f993ff3 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Thu, 27 Jun 2024 03:02:56 +0900 Subject: [PATCH 151/263] =?UTF-8?q?feat:=20CommentNavigator=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#536)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Navigator/CommentNavigator.swift | 43 +++++++++++++++++++ .../Navigator/DailyCalendarNavigator.swift | 9 ---- 2 files changed, 43 insertions(+), 9 deletions(-) create mode 100644 14th-team5-iOS/App/Sources/Application/Navigator/CommentNavigator.swift diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/CommentNavigator.swift b/14th-team5-iOS/App/Sources/Application/Navigator/CommentNavigator.swift new file mode 100644 index 000000000..26e599fcb --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/Navigator/CommentNavigator.swift @@ -0,0 +1,43 @@ +// +// CommentNavigator.swift +// App +// +// Created by 김건우 on 6/27/24. +// + +import Core +import UIKit + +protocol CommentNavigatorProtocol: BaseNavigator { + func toProfile(memberId: String) + func dismiss(completion: (() -> Void)?) +} + +final class CommentNavigator: CommentNavigatorProtocol { + + // MARK: - Properties + + var navigationController: UINavigationController + + // MARK: - Intializer + + init(navigationController: UINavigationController) { + self.navigationController = navigationController + } + + // MARK: - To + + func toProfile(memberId: String) { + let vc = ProfileViewControllerWrapper(memberId: memberId).viewController + navigationController.pushViewController(vc, animated: true) + } + + // MARK: - Back + + func dismiss(completion: (() -> Void)? = nil) { + navigationController.dismiss(animated: true) { + completion?() + } + } + +} diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/DailyCalendarNavigator.swift b/14th-team5-iOS/App/Sources/Application/Navigator/DailyCalendarNavigator.swift index b24bc4078..ec6e8673d 100644 --- a/14th-team5-iOS/App/Sources/Application/Navigator/DailyCalendarNavigator.swift +++ b/14th-team5-iOS/App/Sources/Application/Navigator/DailyCalendarNavigator.swift @@ -11,7 +11,6 @@ import UIKit protocol DailyCalendarNavigatorProtocol: BaseNavigator { func toProfile(memberId: String) func toComment(postId: String) - func dismissComment(completion: (() -> Void)?) } final class DailyCalendarNavigator: DailyCalendarNavigatorProtocol { @@ -39,12 +38,4 @@ final class DailyCalendarNavigator: DailyCalendarNavigatorProtocol { // TODO: - present 메서드 수정하기 } - // MARK: - Back - - func dismissComment(completion: (() -> Void)? = nil) { - navigationController.dismiss(animated: true) { - completion?() - } - } - } From 81f8639abd07689cedff56afaf1ab8ec0a27f15e Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Thu, 27 Jun 2024 03:08:11 +0900 Subject: [PATCH 152/263] =?UTF-8?q?feat:=20ManagementNavigator=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#563)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DIContainer/NavigatorDIContainer.swift | 12 +++++++ .../Navigator/ManagementNavigator.swift | 34 +++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 14th-team5-iOS/App/Sources/Application/Navigator/ManagementNavigator.swift diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/NavigatorDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/NavigatorDIContainer.swift index 340d41527..5a2c4c673 100644 --- a/14th-team5-iOS/App/Sources/Application/DIContainer/NavigatorDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/NavigatorDIContainer.swift @@ -42,6 +42,18 @@ final class NavigatorDIContainer: BaseContainer { navigationController: makeUINavigationController() ) } + + container.register(type: DailyCalendarNavigatorProtocol.self) { _ in + DailyCalendarNavigator( + navigationController: makeUINavigationController() + ) + } + + container.register(type: CommentNavigatorProtocol.self) { _ in + CommentNavigator( + navigationController: makeUINavigationController() + ) + } } } diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/ManagementNavigator.swift b/14th-team5-iOS/App/Sources/Application/Navigator/ManagementNavigator.swift new file mode 100644 index 000000000..d2489cc76 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/Navigator/ManagementNavigator.swift @@ -0,0 +1,34 @@ +// +// ManagementNavigator.swift +// App +// +// Created by 김건우 on 6/27/24. +// + +import Core +import UIKit + +protocol ManagementNavigatorProtocol: BaseNavigator { + func toProfile(memberId: String) +} + +final class ManagementNavigator: ManagementNavigatorProtocol { + + // MARK: - Properties + + var navigationController: UINavigationController + + // MARK: - Intializer + + init(navigationController: UINavigationController) { + self.navigationController = navigationController + } + + // MARK: - To + + func toProfile(memberId: String) { + let vc = ProfileViewControllerWrapper(memberId: memberId).viewController + navigationController.pushViewController(vc, animated: true) + } + +} From a3b6d630d293d42cf9f3278b8d24e9b6d2ffab7e Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Fri, 28 Jun 2024 18:32:04 +0900 Subject: [PATCH 153/263] =?UTF-8?q?feat:=20RxInterval,=20RxScheduler=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#584)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Repositories/Member => }/Emojis.swift | 1 + .../BBRx/Repositories/App/App.swift | 1 + .../DeepLink/DeepLinkRepository.swift | 1 + .../Models/NotificationDeepLink.swift | 0 .../DeepLink/Models/WidgetDeepLink.swift | 0 .../Member/MemberRepository.swift | 1 + .../Repositories/Token/TokenRepository.swift | 2 + .../Sources/{ => Trash}/BBRx/RxUtils.swift | 2 + .../Types/Protocols/ItemIndexable.swift | 0 .../{Utilties/Types/Enums => }/URLTypes.swift | 1 + .../Mixpanel/MixPanelService+Account.swift | 0 .../Mixpanel/MixPanelService+Camera.swift | 0 .../Mixpanel/MixPanelService+Family.swift | 0 .../Mixpanel/MixPanelService+Home.swift | 0 .../Analytics/Mixpanel/MixpanelService.swift | 0 .../{Utilties => Utilities}/Haptic.swift | 0 .../Modifiers/Shimmer.swift | 0 .../Sources/Utilities/Rx/RxInterval.swift | 54 +++++++ .../{BBRx => Utilities/Rx}/RxObject.swift | 3 + .../Sources/Utilities/Rx/RxScheduler.swift | 135 ++++++++++++++++++ 20 files changed, 201 insertions(+) rename 14th-team5-iOS/Core/Sources/{BBRx/Repositories/Member => }/Emojis.swift (98%) rename 14th-team5-iOS/Core/Sources/{ => Trash}/BBRx/Repositories/App/App.swift (81%) rename 14th-team5-iOS/Core/Sources/{ => Trash}/BBRx/Repositories/DeepLink/DeepLinkRepository.swift (83%) rename 14th-team5-iOS/Core/Sources/{ => Trash}/BBRx/Repositories/DeepLink/Models/NotificationDeepLink.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Trash}/BBRx/Repositories/DeepLink/Models/WidgetDeepLink.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Trash}/BBRx/Repositories/Member/MemberRepository.swift (96%) rename 14th-team5-iOS/Core/Sources/{ => Trash}/BBRx/Repositories/Token/TokenRepository.swift (94%) rename 14th-team5-iOS/Core/Sources/{ => Trash}/BBRx/RxUtils.swift (94%) rename 14th-team5-iOS/Core/Sources/{Utilties => Trash}/Types/Protocols/ItemIndexable.swift (100%) rename 14th-team5-iOS/Core/Sources/{Utilties/Types/Enums => }/URLTypes.swift (96%) rename 14th-team5-iOS/Core/Sources/{Utilties => Utilities}/Analytics/Mixpanel/MixPanelService+Account.swift (100%) rename 14th-team5-iOS/Core/Sources/{Utilties => Utilities}/Analytics/Mixpanel/MixPanelService+Camera.swift (100%) rename 14th-team5-iOS/Core/Sources/{Utilties => Utilities}/Analytics/Mixpanel/MixPanelService+Family.swift (100%) rename 14th-team5-iOS/Core/Sources/{Utilties => Utilities}/Analytics/Mixpanel/MixPanelService+Home.swift (100%) rename 14th-team5-iOS/Core/Sources/{Utilties => Utilities}/Analytics/Mixpanel/MixpanelService.swift (100%) rename 14th-team5-iOS/Core/Sources/{Utilties => Utilities}/Haptic.swift (100%) rename 14th-team5-iOS/Core/Sources/{Utilties => Utilities}/Modifiers/Shimmer.swift (100%) create mode 100644 14th-team5-iOS/Core/Sources/Utilities/Rx/RxInterval.swift rename 14th-team5-iOS/Core/Sources/{BBRx => Utilities/Rx}/RxObject.swift (84%) create mode 100644 14th-team5-iOS/Core/Sources/Utilities/Rx/RxScheduler.swift diff --git a/14th-team5-iOS/Core/Sources/BBRx/Repositories/Member/Emojis.swift b/14th-team5-iOS/Core/Sources/Emojis.swift similarity index 98% rename from 14th-team5-iOS/Core/Sources/BBRx/Repositories/Member/Emojis.swift rename to 14th-team5-iOS/Core/Sources/Emojis.swift index b22e7186f..c7bb5d44e 100644 --- a/14th-team5-iOS/Core/Sources/BBRx/Repositories/Member/Emojis.swift +++ b/14th-team5-iOS/Core/Sources/Emojis.swift @@ -8,6 +8,7 @@ import UIKit import DesignSystem +// TODO: - 요거 옮겨주세요~ public enum Emojis: Int { case emoji1 = 1 case emoji2 = 2 diff --git a/14th-team5-iOS/Core/Sources/BBRx/Repositories/App/App.swift b/14th-team5-iOS/Core/Sources/Trash/BBRx/Repositories/App/App.swift similarity index 81% rename from 14th-team5-iOS/Core/Sources/BBRx/Repositories/App/App.swift rename to 14th-team5-iOS/Core/Sources/Trash/BBRx/Repositories/App/App.swift index 7852310f9..9b98ecaf1 100644 --- a/14th-team5-iOS/Core/Sources/BBRx/Repositories/App/App.swift +++ b/14th-team5-iOS/Core/Sources/Trash/BBRx/Repositories/App/App.swift @@ -7,6 +7,7 @@ import Foundation +@available(*, deprecated, message: "KeyChainWrapper 혹은 UserDefaultsWrpper 사용") public enum App { public static let indicator = BibbiLoadIndicator() diff --git a/14th-team5-iOS/Core/Sources/BBRx/Repositories/DeepLink/DeepLinkRepository.swift b/14th-team5-iOS/Core/Sources/Trash/BBRx/Repositories/DeepLink/DeepLinkRepository.swift similarity index 83% rename from 14th-team5-iOS/Core/Sources/BBRx/Repositories/DeepLink/DeepLinkRepository.swift rename to 14th-team5-iOS/Core/Sources/Trash/BBRx/Repositories/DeepLink/DeepLinkRepository.swift index d2b1e185b..fcdc08921 100644 --- a/14th-team5-iOS/Core/Sources/BBRx/Repositories/DeepLink/DeepLinkRepository.swift +++ b/14th-team5-iOS/Core/Sources/Trash/BBRx/Repositories/DeepLink/DeepLinkRepository.swift @@ -10,6 +10,7 @@ import Foundation import RxSwift import RxCocoa +@available(*, deprecated, message: "KeyChainWrapper 혹은 UserDefaultsWrpper 사용") public class DeepLinkRepository: RxObject { public let notification = BehaviorRelay(value: nil) public let widget = BehaviorRelay(value: nil) diff --git a/14th-team5-iOS/Core/Sources/BBRx/Repositories/DeepLink/Models/NotificationDeepLink.swift b/14th-team5-iOS/Core/Sources/Trash/BBRx/Repositories/DeepLink/Models/NotificationDeepLink.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBRx/Repositories/DeepLink/Models/NotificationDeepLink.swift rename to 14th-team5-iOS/Core/Sources/Trash/BBRx/Repositories/DeepLink/Models/NotificationDeepLink.swift diff --git a/14th-team5-iOS/Core/Sources/BBRx/Repositories/DeepLink/Models/WidgetDeepLink.swift b/14th-team5-iOS/Core/Sources/Trash/BBRx/Repositories/DeepLink/Models/WidgetDeepLink.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBRx/Repositories/DeepLink/Models/WidgetDeepLink.swift rename to 14th-team5-iOS/Core/Sources/Trash/BBRx/Repositories/DeepLink/Models/WidgetDeepLink.swift diff --git a/14th-team5-iOS/Core/Sources/BBRx/Repositories/Member/MemberRepository.swift b/14th-team5-iOS/Core/Sources/Trash/BBRx/Repositories/Member/MemberRepository.swift similarity index 96% rename from 14th-team5-iOS/Core/Sources/BBRx/Repositories/Member/MemberRepository.swift rename to 14th-team5-iOS/Core/Sources/Trash/BBRx/Repositories/Member/MemberRepository.swift index b52e7d4ae..3093cec84 100644 --- a/14th-team5-iOS/Core/Sources/BBRx/Repositories/Member/MemberRepository.swift +++ b/14th-team5-iOS/Core/Sources/Trash/BBRx/Repositories/Member/MemberRepository.swift @@ -10,6 +10,7 @@ import Foundation import RxSwift import RxCocoa +@available(*, deprecated, message: "KeyChainWrapper 혹은 UserDefaultsWrpper 사용") public class MemberRepository: RxObject { public let familyId = BehaviorRelay(value: nil) public let memberID = BehaviorRelay(value: nil) diff --git a/14th-team5-iOS/Core/Sources/BBRx/Repositories/Token/TokenRepository.swift b/14th-team5-iOS/Core/Sources/Trash/BBRx/Repositories/Token/TokenRepository.swift similarity index 94% rename from 14th-team5-iOS/Core/Sources/BBRx/Repositories/Token/TokenRepository.swift rename to 14th-team5-iOS/Core/Sources/Trash/BBRx/Repositories/Token/TokenRepository.swift index 41116db69..43d2b3b00 100644 --- a/14th-team5-iOS/Core/Sources/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) public struct AccessToken: Codable, Equatable { public var accessToken: String? public var refreshToken: String? @@ -22,6 +23,7 @@ public struct AccessToken: Codable, Equatable { } } +@available(*, deprecated, message: "KeyChainWrapper 혹은 UserDefaultsWrpper 사용") public class TokenRepository: RxObject { public lazy var keychain = KeychainWrapper(serviceName: "Bibbi", accessGroup: "P9P4WJ623F.com.5ing.bibbi") diff --git a/14th-team5-iOS/Core/Sources/BBRx/RxUtils.swift b/14th-team5-iOS/Core/Sources/Trash/BBRx/RxUtils.swift similarity index 94% rename from 14th-team5-iOS/Core/Sources/BBRx/RxUtils.swift rename to 14th-team5-iOS/Core/Sources/Trash/BBRx/RxUtils.swift index 5fed12199..43a92aa24 100644 --- a/14th-team5-iOS/Core/Sources/BBRx/RxUtils.swift +++ b/14th-team5-iOS/Core/Sources/Trash/BBRx/RxUtils.swift @@ -9,6 +9,7 @@ import Foundation import RxSwift +@available(*, deprecated, renamed: "Rxinterval") public enum RxConst { static public var milliseconds100Interval: RxTimeInterval { return .milliseconds(100) @@ -19,6 +20,7 @@ public enum RxConst { } } +@available(*, deprecated, renamed: "RxScheduler") public enum RxSchedulers { public static let main = { MainScheduler.instance diff --git a/14th-team5-iOS/Core/Sources/Utilties/Types/Protocols/ItemIndexable.swift b/14th-team5-iOS/Core/Sources/Trash/Types/Protocols/ItemIndexable.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Utilties/Types/Protocols/ItemIndexable.swift rename to 14th-team5-iOS/Core/Sources/Trash/Types/Protocols/ItemIndexable.swift diff --git a/14th-team5-iOS/Core/Sources/Utilties/Types/Enums/URLTypes.swift b/14th-team5-iOS/Core/Sources/URLTypes.swift similarity index 96% rename from 14th-team5-iOS/Core/Sources/Utilties/Types/Enums/URLTypes.swift rename to 14th-team5-iOS/Core/Sources/URLTypes.swift index a8a34dbdc..8eab9b244 100644 --- a/14th-team5-iOS/Core/Sources/Utilties/Types/Enums/URLTypes.swift +++ b/14th-team5-iOS/Core/Sources/URLTypes.swift @@ -7,6 +7,7 @@ import UIKit +// TODO: - 요거 옮겨주세요~ public enum BibbiFeedType: Int { case survival = 0 case mission = 1 diff --git a/14th-team5-iOS/Core/Sources/Utilties/Analytics/Mixpanel/MixPanelService+Account.swift b/14th-team5-iOS/Core/Sources/Utilities/Analytics/Mixpanel/MixPanelService+Account.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Utilties/Analytics/Mixpanel/MixPanelService+Account.swift rename to 14th-team5-iOS/Core/Sources/Utilities/Analytics/Mixpanel/MixPanelService+Account.swift diff --git a/14th-team5-iOS/Core/Sources/Utilties/Analytics/Mixpanel/MixPanelService+Camera.swift b/14th-team5-iOS/Core/Sources/Utilities/Analytics/Mixpanel/MixPanelService+Camera.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Utilties/Analytics/Mixpanel/MixPanelService+Camera.swift rename to 14th-team5-iOS/Core/Sources/Utilities/Analytics/Mixpanel/MixPanelService+Camera.swift diff --git a/14th-team5-iOS/Core/Sources/Utilties/Analytics/Mixpanel/MixPanelService+Family.swift b/14th-team5-iOS/Core/Sources/Utilities/Analytics/Mixpanel/MixPanelService+Family.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Utilties/Analytics/Mixpanel/MixPanelService+Family.swift rename to 14th-team5-iOS/Core/Sources/Utilities/Analytics/Mixpanel/MixPanelService+Family.swift diff --git a/14th-team5-iOS/Core/Sources/Utilties/Analytics/Mixpanel/MixPanelService+Home.swift b/14th-team5-iOS/Core/Sources/Utilities/Analytics/Mixpanel/MixPanelService+Home.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Utilties/Analytics/Mixpanel/MixPanelService+Home.swift rename to 14th-team5-iOS/Core/Sources/Utilities/Analytics/Mixpanel/MixPanelService+Home.swift diff --git a/14th-team5-iOS/Core/Sources/Utilties/Analytics/Mixpanel/MixpanelService.swift b/14th-team5-iOS/Core/Sources/Utilities/Analytics/Mixpanel/MixpanelService.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Utilties/Analytics/Mixpanel/MixpanelService.swift rename to 14th-team5-iOS/Core/Sources/Utilities/Analytics/Mixpanel/MixpanelService.swift diff --git a/14th-team5-iOS/Core/Sources/Utilties/Haptic.swift b/14th-team5-iOS/Core/Sources/Utilities/Haptic.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Utilties/Haptic.swift rename to 14th-team5-iOS/Core/Sources/Utilities/Haptic.swift diff --git a/14th-team5-iOS/Core/Sources/Utilties/Modifiers/Shimmer.swift b/14th-team5-iOS/Core/Sources/Utilities/Modifiers/Shimmer.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Utilties/Modifiers/Shimmer.swift rename to 14th-team5-iOS/Core/Sources/Utilities/Modifiers/Shimmer.swift diff --git a/14th-team5-iOS/Core/Sources/Utilities/Rx/RxInterval.swift b/14th-team5-iOS/Core/Sources/Utilities/Rx/RxInterval.swift new file mode 100644 index 000000000..4bb8a6b17 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Utilities/Rx/RxInterval.swift @@ -0,0 +1,54 @@ +// +// RxInterval.swift +// Core +// +// Created by 김건우 on 6/28/24. +// + +import Foundation + +import RxSwift + +public enum RxInterval { + + public static var _100milliseconds: RxTimeInterval { + RxTimeInterval.milliseconds(100) + } + + public static var _200milliseconds: RxTimeInterval { + RxTimeInterval.milliseconds(200) + } + + public static var _300milliseconds: RxTimeInterval { + RxTimeInterval.milliseconds(300) + } + + public static var _400milliseconds: RxTimeInterval { + RxTimeInterval.milliseconds(400) + } + + public static var _500milliseconds: RxTimeInterval { + RxTimeInterval.milliseconds(500) + } + + public static var _600milliseconds: RxTimeInterval { + RxTimeInterval.milliseconds(600) + } + + public static var _700milliseconds: RxTimeInterval { + RxTimeInterval.milliseconds(700) + } + + public static var _800milliseconds: RxTimeInterval { + RxTimeInterval.milliseconds(800) + } + + public static var _900milliseconds: RxTimeInterval { + RxTimeInterval.milliseconds(900) + } + + public static func cusomtMilliseconds(_ custom: Int) -> RxTimeInterval { + RxTimeInterval.milliseconds(custom) + } + +} diff --git a/14th-team5-iOS/Core/Sources/BBRx/RxObject.swift b/14th-team5-iOS/Core/Sources/Utilities/Rx/RxObject.swift similarity index 84% rename from 14th-team5-iOS/Core/Sources/BBRx/RxObject.swift rename to 14th-team5-iOS/Core/Sources/Utilities/Rx/RxObject.swift index a2e52f164..4f23b1951 100644 --- a/14th-team5-iOS/Core/Sources/BBRx/RxObject.swift +++ b/14th-team5-iOS/Core/Sources/Utilities/Rx/RxObject.swift @@ -10,8 +10,11 @@ import Foundation import RxSwift public class RxObject: NSObject { + + // MARK: - Properties private(set) var disposeBag = DisposeBag() + // MARK: - Bind func bind() {} func unbind() { disposeBag = DisposeBag() diff --git a/14th-team5-iOS/Core/Sources/Utilities/Rx/RxScheduler.swift b/14th-team5-iOS/Core/Sources/Utilities/Rx/RxScheduler.swift new file mode 100644 index 000000000..6bdbea658 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Utilities/Rx/RxScheduler.swift @@ -0,0 +1,135 @@ +// +// RxSchedulers.swift +// Core +// +// Created by 김건우 on 6/28/24. +// + +import Foundation + +import RxSwift + +public enum RxScheduler { + + // MARK: - Main + + public static var main: MainScheduler { + MainScheduler.instance + } + + public static var asyncMain: SerialDispatchQueueScheduler { + MainScheduler.asyncInstance + } + + + // MARK: - Concurrent + + public enum Concurrent { + + public static var background: ConcurrentDispatchQueueScheduler { + ConcurrentDispatchQueueScheduler( + queue: DispatchQueue( + label: "background", + qos: .background + ) + ) + } + + public static var utility: ConcurrentDispatchQueueScheduler { + ConcurrentDispatchQueueScheduler( + queue: DispatchQueue( + label: "utility", + qos: .utility + ) + ) + } + + public static var `default`: ConcurrentDispatchQueueScheduler { + ConcurrentDispatchQueueScheduler( + queue: DispatchQueue( + label: "default", + qos: .default + ) + ) + } + + public static var userIntiated: ConcurrentDispatchQueueScheduler { + ConcurrentDispatchQueueScheduler( + queue: DispatchQueue( + label: "UserIntiated", + qos: .userInitiated + ) + ) + } + + public static var userInteractive: ConcurrentDispatchQueueScheduler { + ConcurrentDispatchQueueScheduler( + queue: DispatchQueue( + label: "UserInteractive", + qos: .userInteractive + ) + ) + } + + public static func custom(_ label: String, qos: DispatchQoS) -> ConcurrentDispatchQueueScheduler { + ConcurrentDispatchQueueScheduler( + queue: DispatchQueue( + label: label, + qos: qos + ) + ) + } + + } + + + // MARK: - Serial + + public enum Serial { + + public static var background: SerialDispatchQueueScheduler { + SerialDispatchQueueScheduler( + qos: .background, + internalSerialQueueName: "background" + ) + } + + public static var utility: SerialDispatchQueueScheduler { + SerialDispatchQueueScheduler( + qos: .utility, + internalSerialQueueName: "utility" + ) + } + + public static var `default`: SerialDispatchQueueScheduler { + SerialDispatchQueueScheduler( + qos: .default, + internalSerialQueueName: "default" + ) + } + + public static var userIntiated: SerialDispatchQueueScheduler { + SerialDispatchQueueScheduler( + qos: .userInitiated, + internalSerialQueueName: "userIntiated" + ) + } + + public static var userInteractive: SerialDispatchQueueScheduler { + SerialDispatchQueueScheduler( + qos: .userInteractive, + internalSerialQueueName: "userInteractive" + ) + } + + public static func custom(_ label: String, qos: DispatchQoS) -> SerialDispatchQueueScheduler { + SerialDispatchQueueScheduler( + qos: qos, + internalSerialQueueName: label + ) + } + + } + + +} From 794452f961ca398bced0f57ae0f9e58db23da3d7 Mon Sep 17 00:00:00 2001 From: Do-hyun-Kim Date: Sun, 30 Jun 2024 19:11:59 +0900 Subject: [PATCH 154/263] =?UTF-8?q?fix:=20Tuist=20Version=204.0=20?= =?UTF-8?q?=EB=B2=84=EC=A0=84=EC=9C=BC=EB=A1=9C=20=EB=A7=88=EC=9D=B4?= =?UTF-8?q?=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98=20=EC=9E=91=EC=97=85=20-?= =?UTF-8?q?=20Tuist=20Bibbi=20Macro=20=EC=9D=98=EC=A1=B4=EC=84=B1=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20-=20Tuist=203.0=20Dependencies.swift=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=A0=9C=EA=B1=B0=20=EB=B0=8F=20Tuist=204?= =?UTF-8?q?.0=20=EB=B2=84=EC=A0=84=20Package.swift=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20-=20Tuist=20Core=20Module=20issue=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .tuist-version | 1 - 14th-team5-iOS/App/Project.swift | 4 +- .../Activity/CopyInvitationUrlActivity.swift | 0 .../Activity/UrlActivityItemSource.swift | 0 .../BBAlertBuilder/BibbiAlertAction.swift | 0 .../BBAlertBuilder/BibbiAlertBuilder.swift | 0 .../BBAlertBuilder/BibbiAlertStyle.swift | 0 .../BBAlertBuilder/BibbiAlertTitle.swift | 0 .../BibbiAlertViewController.swift | 0 .../BBCommons/BBButton/BibbiButton.swift | 0 .../BBCommons/BBLabel/BibbiLabel.swift | 0 .../BBNavigationBar+DelegateProxy.swift | 0 .../BBNavigationBarButtonStyle.swift | 0 .../BBNavigationBarView.swift | 0 .../BibbiSegmentedControl.swift | 0 .../BibbiNavigationBarView.swift | 0 .../RxBibbiNavigationDelegateProxy.swift | 0 .../BibbiToastMessageView.swift | 0 .../BBCommons/Lottie/BibbiLoadingView.swift | 0 .../Lottie/BibbiLottileIndicator.swift | 0 .../BBCommons/Lottie/LottieType.swift | 0 .../BBCommons/Lottie/LottieView.swift | 0 .../BBCommons/Popover/BibbiPopOverView.swift | 0 .../DescriptionPopoverViewController.swift | 0 .../ShareView/BibbiInquireBannerView.swift | 0 .../ShareView/BibbiMissionView.swift | 0 .../ShareView/BibbiProfileView.swift | 0 .../BBCommons/ShareView/BibbiTermsView.swift | 0 .../Sources/{ => Bibbi}/BBNetwork/API.swift | 0 .../{ => Bibbi}/BBNetwork/APIConfig.swift | 0 .../{ => Bibbi}/BBNetwork/APISpec.swift | 0 .../BBRx/Repositories/App/App.swift | 0 .../DeepLink/DeepLinkRepository.swift | 0 .../Models/NotificationDeepLink.swift | 0 .../DeepLink/Models/WidgetDeepLink.swift | 0 .../BBRx/Repositories/Member/Emojis.swift | 0 .../Member/MemberRepository.swift | 0 .../Repositories/Token/TokenRepository.swift | 0 .../Sources/{ => Bibbi}/BBRx/RxObject.swift | 0 .../Sources/{ => Bibbi}/BBRx/RxUtils.swift | 0 .../BBServices/ActivityGlobalState.swift | 0 .../BBServices/CalendarGlobalState.swift | 0 .../BBServices/GlobalStateProvider.swift | 0 .../{ => Bibbi}/BBServices/MainService.swift | 0 .../BBServices/PostGlobalState.swift | 0 .../BBServices/ProfileFeedGlobalState.swift | 0 .../BBServices/ProfileGlobalState.swift | 0 .../BBServices/RealEmojiGlobalState.swift | 0 .../BBServices/TimerGlobalState.swift | 0 .../BBServices/ToastMessageGlobalState.swift | 0 .../InMemoryWrapper/InMemoryKey.swift | 0 .../InMemoryWrapper/InMemoryStore.swift | 0 .../InMemoryWrapper/InMemorySubscript.swift | 0 .../InMemoryWrapper/InMemoryType.swift | 0 .../InMemoryWrapper/InMemoryWrapper.swift | 0 .../KeychainItemAccessibility.swift | 0 .../KeychainWrapper/KeychainType.swift | 0 .../KeychainWrapper/KeychainWrapper.swift | 0 .../KeychainWrapper/KeychainWrapperKey.swift | 0 .../KeychainWrapperSubscript.swift | 0 .../UserDefaultsWrapper/UserDefaultsKey.swift | 0 .../UserDefaultsSubscript.swift | 0 .../UserDefaultsType.swift | 0 .../UserDefaultsWrapper.swift | 0 .../Base/BBNavigationViewController.swift | 0 .../Base/BaseCollectionViewCell.swift | 0 .../{ => Bibbi}/Base/BaseContainer.swift | 0 .../{ => Bibbi}/Base/BaseDIContainer.swift | 0 .../{ => Bibbi}/Base/BaseNavigator.swift | 0 .../Base/BasePageViewController.swift | 0 .../{ => Bibbi}/Base/BaseService.swift | 0 .../{ => Bibbi}/Base/BaseTableViewCell.swift | 0 .../Sources/{ => Bibbi}/Base/BaseView.swift | 0 .../{ => Bibbi}/Base/BaseViewController.swift | 0 .../{ => Bibbi}/Base/BaseViewModel.swift | 0 .../{ => Bibbi}/Base/BaseWrapper.swift | 0 .../{ => Bibbi}/Base/BaseWrapperView.swift | 0 .../Base/ReactorViewController.swift | 0 .../{ => Bibbi}/DIContainer/Container.swift | 0 .../{ => Bibbi}/DIContainer/DIContainer.swift | 0 .../DIContainer/InjectIdentifier.swift | 0 .../Injected+PropertyWrapper.swift | 0 .../InjectedSafe+PropertyWrapper.swift | 0 .../Navigator+PropertyWrapper.swift | 0 .../Extensions/AVCapturePhotoOutput+Ext.swift | 0 .../{ => Bibbi}/Extensions/Array+Ext.swift | 0 .../{ => Bibbi}/Extensions/Bundle+Ext.swift | 0 .../{ => Bibbi}/Extensions/Codable+Ext.swift | 0 .../{ => Bibbi}/Extensions/Date+Ext.swift | 0 .../Extensions/DateFormatter+Ext.swift | 0 .../{ => Bibbi}/Extensions/Int+Ext.swift | 0 .../Extensions/NSItemProvider+Ext.swift | 0 .../NSMutableAttributedString+Ext.swift | 0 .../Extensions/Notification+Ext.swift | 0 .../{ => Bibbi}/Extensions/Reactive+Ext.swift | 0 .../{ => Bibbi}/Extensions/String+Ext.swift | 0 .../{ => Bibbi}/Extensions/UIButton+Ext.swift | 0 ...UICollectionViewLayoutAttributes+Ext.swift | 0 .../{ => Bibbi}/Extensions/UIColor+Ext.swift | 0 .../{ => Bibbi}/Extensions/UIFont+Ext.swift | 0 .../{ => Bibbi}/Extensions/UIImage+Ext.swift | 0 .../Extensions/UIImageView+Ext.swift | 0 .../UINavigationController+Ext.swift | 0 .../Extensions/UIPageViewController+Ext.swift | 0 .../{ => Bibbi}/Extensions/UIScreen+Ext.swift | 0 .../Extensions/UIStackView+Ext.swift | 0 .../Extensions/UITextField+Ext.swift | 0 .../{ => Bibbi}/Extensions/UIView+Ext.swift | 0 .../Extensions/UIViewController+Ext.swift | 0 .../Extensions/UIVisualEffectView+Ext.swift | 0 .../{ => Bibbi}/Extensions/UIWindow+Ext.swift | 0 .../Extensions/URLRequeset+Ext.swift | 0 .../Extensions/UserDefaults+Ext.swift | 0 .../Mixpanel/MixPanelService+Account.swift | 0 .../Mixpanel/MixPanelService+Camera.swift | 0 .../Mixpanel/MixPanelService+Family.swift | 0 .../Mixpanel/MixPanelService+Home.swift | 0 .../Analytics/Mixpanel/MixpanelService.swift | 0 .../Sources/{ => Bibbi}/Utilties/Haptic.swift | 0 .../Utilties/Modifiers/Shimmer.swift | 0 .../Utilties/Types/Enums/URLTypes.swift | 0 .../Types/Protocols/ItemIndexable.swift | 0 Bibbi.xcworkspace/contents.xcworkspacedata | 161 ++--- .../xcschemes/Bibbi-Workspace.xcscheme | 560 +++++++++++++++--- Tuist/Config.swift | 3 - Tuist/Dependencies.swift | 41 -- Tuist/Package.resolved | 320 ++++++++++ Tuist/Package.swift | 44 ++ .../DeploymentTarget+Templates.swift | 4 +- .../Modular+Templates.swift | 59 +- .../ModuleType+Templates.swift | 22 +- .../Project+Templates.swift | 4 +- .../Scheme+Templates.swift | 14 +- 133 files changed, 995 insertions(+), 242 deletions(-) delete mode 100644 .tuist-version rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBCommons/Activity/CopyInvitationUrlActivity.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBCommons/Activity/UrlActivityItemSource.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBCommons/BBAlertBuilder/BibbiAlertAction.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBCommons/BBAlertBuilder/BibbiAlertBuilder.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBCommons/BBAlertBuilder/BibbiAlertStyle.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBCommons/BBAlertBuilder/BibbiAlertTitle.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBCommons/BBAlertBuilder/BibbiAlertViewController.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBCommons/BBButton/BibbiButton.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBCommons/BBLabel/BibbiLabel.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBCommons/BBNavigationBarView/BBNavigationBar+DelegateProxy.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBCommons/BBNavigationBarView/BBNavigationBarButtonStyle.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBCommons/BBNavigationBarView/BBNavigationBarView.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBCommons/BBSegmentedControl/BibbiSegmentedControl.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBCommons/BibbiNavigationBarView/BibbiNavigationBarView.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBCommons/BibbiNavigationBarView/DelegateProxy/RxBibbiNavigationDelegateProxy.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBCommons/BibbiToastMessageView/BibbiToastMessageView.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBCommons/Lottie/BibbiLoadingView.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBCommons/Lottie/BibbiLottileIndicator.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBCommons/Lottie/LottieType.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBCommons/Lottie/LottieView.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBCommons/Popover/BibbiPopOverView.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBCommons/Popover/DescriptionPopoverViewController.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBCommons/ShareView/BibbiInquireBannerView.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBCommons/ShareView/BibbiMissionView.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBCommons/ShareView/BibbiProfileView.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBCommons/ShareView/BibbiTermsView.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBNetwork/API.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBNetwork/APIConfig.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBNetwork/APISpec.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBRx/Repositories/App/App.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBRx/Repositories/DeepLink/DeepLinkRepository.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBRx/Repositories/DeepLink/Models/NotificationDeepLink.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBRx/Repositories/DeepLink/Models/WidgetDeepLink.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBRx/Repositories/Member/Emojis.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBRx/Repositories/Member/MemberRepository.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBRx/Repositories/Token/TokenRepository.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBRx/RxObject.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBRx/RxUtils.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBServices/ActivityGlobalState.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBServices/CalendarGlobalState.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBServices/GlobalStateProvider.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBServices/MainService.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBServices/PostGlobalState.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBServices/ProfileFeedGlobalState.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBServices/ProfileGlobalState.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBServices/RealEmojiGlobalState.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBServices/TimerGlobalState.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBServices/ToastMessageGlobalState.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBStorages/InMemoryWrapper/InMemoryKey.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBStorages/InMemoryWrapper/InMemoryStore.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBStorages/InMemoryWrapper/InMemorySubscript.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBStorages/InMemoryWrapper/InMemoryType.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBStorages/InMemoryWrapper/InMemoryWrapper.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBStorages/KeychainWrapper/KeychainItemAccessibility.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBStorages/KeychainWrapper/KeychainType.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBStorages/KeychainWrapper/KeychainWrapper.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBStorages/KeychainWrapper/KeychainWrapperKey.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBStorages/KeychainWrapper/KeychainWrapperSubscript.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBStorages/UserDefaultsWrapper/UserDefaultsKey.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBStorages/UserDefaultsWrapper/UserDefaultsSubscript.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBStorages/UserDefaultsWrapper/UserDefaultsType.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/BBStorages/UserDefaultsWrapper/UserDefaultsWrapper.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/Base/BBNavigationViewController.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/Base/BaseCollectionViewCell.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/Base/BaseContainer.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/Base/BaseDIContainer.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/Base/BaseNavigator.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/Base/BasePageViewController.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/Base/BaseService.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/Base/BaseTableViewCell.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/Base/BaseView.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/Base/BaseViewController.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/Base/BaseViewModel.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/Base/BaseWrapper.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/Base/BaseWrapperView.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/Base/ReactorViewController.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/DIContainer/Container.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/DIContainer/DIContainer.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/DIContainer/InjectIdentifier.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/DIContainer/Injected+PropertyWrapper.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/DIContainer/InjectedSafe+PropertyWrapper.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/DIContainer/Navigator+PropertyWrapper.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/Extensions/AVCapturePhotoOutput+Ext.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/Extensions/Array+Ext.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/Extensions/Bundle+Ext.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/Extensions/Codable+Ext.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/Extensions/Date+Ext.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/Extensions/DateFormatter+Ext.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/Extensions/Int+Ext.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/Extensions/NSItemProvider+Ext.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/Extensions/NSMutableAttributedString+Ext.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/Extensions/Notification+Ext.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/Extensions/Reactive+Ext.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/Extensions/String+Ext.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/Extensions/UIButton+Ext.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/Extensions/UICollectionViewLayoutAttributes+Ext.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/Extensions/UIColor+Ext.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/Extensions/UIFont+Ext.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/Extensions/UIImage+Ext.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/Extensions/UIImageView+Ext.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/Extensions/UINavigationController+Ext.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/Extensions/UIPageViewController+Ext.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/Extensions/UIScreen+Ext.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/Extensions/UIStackView+Ext.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/Extensions/UITextField+Ext.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/Extensions/UIView+Ext.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/Extensions/UIViewController+Ext.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/Extensions/UIVisualEffectView+Ext.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/Extensions/UIWindow+Ext.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/Extensions/URLRequeset+Ext.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/Extensions/UserDefaults+Ext.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/Utilties/Analytics/Mixpanel/MixPanelService+Account.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/Utilties/Analytics/Mixpanel/MixPanelService+Camera.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/Utilties/Analytics/Mixpanel/MixPanelService+Family.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/Utilties/Analytics/Mixpanel/MixPanelService+Home.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/Utilties/Analytics/Mixpanel/MixpanelService.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/Utilties/Haptic.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/Utilties/Modifiers/Shimmer.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/Utilties/Types/Enums/URLTypes.swift (100%) rename 14th-team5-iOS/Core/Sources/{ => Bibbi}/Utilties/Types/Protocols/ItemIndexable.swift (100%) delete mode 100644 Tuist/Config.swift delete mode 100644 Tuist/Dependencies.swift create mode 100644 Tuist/Package.resolved create mode 100644 Tuist/Package.swift diff --git a/.tuist-version b/.tuist-version deleted file mode 100644 index b1f5ebeda..000000000 --- a/.tuist-version +++ /dev/null @@ -1 +0,0 @@ -3.20.0 \ No newline at end of file diff --git a/14th-team5-iOS/App/Project.swift b/14th-team5-iOS/App/Project.swift index 861faa1e9..9bd40d87e 100644 --- a/14th-team5-iOS/App/Project.swift +++ b/14th-team5-iOS/App/Project.swift @@ -58,7 +58,7 @@ private let targets: [Target] = [ .build(.prd, name: "PRD"), .build(.stg, name: "STG") ]), - entitlements: .relativeToRoot("App.entitlements") + entitlements: .file(path: .relativeToRoot("App.entitlements")) ) ), .makeModular(extenions: .Widget, factory: .init( @@ -83,7 +83,7 @@ private let targets: [Target] = [ .build(.prd, name: "PRD"), .build(.stg, name: "STG") ]), - entitlements: .relativeToRoot("WidgetExtension.entitlements") + entitlements: .file(path: .relativeToRoot("App.entitlements")) ) ) ] diff --git a/14th-team5-iOS/Core/Sources/BBCommons/Activity/CopyInvitationUrlActivity.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/Activity/CopyInvitationUrlActivity.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBCommons/Activity/CopyInvitationUrlActivity.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/Activity/CopyInvitationUrlActivity.swift diff --git a/14th-team5-iOS/Core/Sources/BBCommons/Activity/UrlActivityItemSource.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/Activity/UrlActivityItemSource.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBCommons/Activity/UrlActivityItemSource.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/Activity/UrlActivityItemSource.swift diff --git a/14th-team5-iOS/Core/Sources/BBCommons/BBAlertBuilder/BibbiAlertAction.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertBuilder/BibbiAlertAction.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBCommons/BBAlertBuilder/BibbiAlertAction.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertBuilder/BibbiAlertAction.swift diff --git a/14th-team5-iOS/Core/Sources/BBCommons/BBAlertBuilder/BibbiAlertBuilder.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertBuilder/BibbiAlertBuilder.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBCommons/BBAlertBuilder/BibbiAlertBuilder.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertBuilder/BibbiAlertBuilder.swift diff --git a/14th-team5-iOS/Core/Sources/BBCommons/BBAlertBuilder/BibbiAlertStyle.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertBuilder/BibbiAlertStyle.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBCommons/BBAlertBuilder/BibbiAlertStyle.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertBuilder/BibbiAlertStyle.swift diff --git a/14th-team5-iOS/Core/Sources/BBCommons/BBAlertBuilder/BibbiAlertTitle.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertBuilder/BibbiAlertTitle.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBCommons/BBAlertBuilder/BibbiAlertTitle.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertBuilder/BibbiAlertTitle.swift diff --git a/14th-team5-iOS/Core/Sources/BBCommons/BBAlertBuilder/BibbiAlertViewController.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertBuilder/BibbiAlertViewController.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBCommons/BBAlertBuilder/BibbiAlertViewController.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertBuilder/BibbiAlertViewController.swift diff --git a/14th-team5-iOS/Core/Sources/BBCommons/BBButton/BibbiButton.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBButton/BibbiButton.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBCommons/BBButton/BibbiButton.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBButton/BibbiButton.swift diff --git a/14th-team5-iOS/Core/Sources/BBCommons/BBLabel/BibbiLabel.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBLabel/BibbiLabel.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBCommons/BBLabel/BibbiLabel.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBLabel/BibbiLabel.swift diff --git a/14th-team5-iOS/Core/Sources/BBCommons/BBNavigationBarView/BBNavigationBar+DelegateProxy.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBarView/BBNavigationBar+DelegateProxy.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBCommons/BBNavigationBarView/BBNavigationBar+DelegateProxy.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBarView/BBNavigationBar+DelegateProxy.swift diff --git a/14th-team5-iOS/Core/Sources/BBCommons/BBNavigationBarView/BBNavigationBarButtonStyle.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBarView/BBNavigationBarButtonStyle.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBCommons/BBNavigationBarView/BBNavigationBarButtonStyle.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBarView/BBNavigationBarButtonStyle.swift diff --git a/14th-team5-iOS/Core/Sources/BBCommons/BBNavigationBarView/BBNavigationBarView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBarView/BBNavigationBarView.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBCommons/BBNavigationBarView/BBNavigationBarView.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBarView/BBNavigationBarView.swift diff --git a/14th-team5-iOS/Core/Sources/BBCommons/BBSegmentedControl/BibbiSegmentedControl.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBSegmentedControl/BibbiSegmentedControl.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBCommons/BBSegmentedControl/BibbiSegmentedControl.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBSegmentedControl/BibbiSegmentedControl.swift diff --git a/14th-team5-iOS/Core/Sources/BBCommons/BibbiNavigationBarView/BibbiNavigationBarView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BibbiNavigationBarView/BibbiNavigationBarView.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBCommons/BibbiNavigationBarView/BibbiNavigationBarView.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BibbiNavigationBarView/BibbiNavigationBarView.swift diff --git a/14th-team5-iOS/Core/Sources/BBCommons/BibbiNavigationBarView/DelegateProxy/RxBibbiNavigationDelegateProxy.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BibbiNavigationBarView/DelegateProxy/RxBibbiNavigationDelegateProxy.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBCommons/BibbiNavigationBarView/DelegateProxy/RxBibbiNavigationDelegateProxy.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BibbiNavigationBarView/DelegateProxy/RxBibbiNavigationDelegateProxy.swift diff --git a/14th-team5-iOS/Core/Sources/BBCommons/BibbiToastMessageView/BibbiToastMessageView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BibbiToastMessageView/BibbiToastMessageView.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBCommons/BibbiToastMessageView/BibbiToastMessageView.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BibbiToastMessageView/BibbiToastMessageView.swift diff --git a/14th-team5-iOS/Core/Sources/BBCommons/Lottie/BibbiLoadingView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/Lottie/BibbiLoadingView.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBCommons/Lottie/BibbiLoadingView.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/Lottie/BibbiLoadingView.swift diff --git a/14th-team5-iOS/Core/Sources/BBCommons/Lottie/BibbiLottileIndicator.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/Lottie/BibbiLottileIndicator.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBCommons/Lottie/BibbiLottileIndicator.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/Lottie/BibbiLottileIndicator.swift diff --git a/14th-team5-iOS/Core/Sources/BBCommons/Lottie/LottieType.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/Lottie/LottieType.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBCommons/Lottie/LottieType.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/Lottie/LottieType.swift diff --git a/14th-team5-iOS/Core/Sources/BBCommons/Lottie/LottieView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/Lottie/LottieView.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBCommons/Lottie/LottieView.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/Lottie/LottieView.swift diff --git a/14th-team5-iOS/Core/Sources/BBCommons/Popover/BibbiPopOverView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/Popover/BibbiPopOverView.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBCommons/Popover/BibbiPopOverView.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/Popover/BibbiPopOverView.swift diff --git a/14th-team5-iOS/Core/Sources/BBCommons/Popover/DescriptionPopoverViewController.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/Popover/DescriptionPopoverViewController.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBCommons/Popover/DescriptionPopoverViewController.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/Popover/DescriptionPopoverViewController.swift diff --git a/14th-team5-iOS/Core/Sources/BBCommons/ShareView/BibbiInquireBannerView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/ShareView/BibbiInquireBannerView.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBCommons/ShareView/BibbiInquireBannerView.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/ShareView/BibbiInquireBannerView.swift diff --git a/14th-team5-iOS/Core/Sources/BBCommons/ShareView/BibbiMissionView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/ShareView/BibbiMissionView.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBCommons/ShareView/BibbiMissionView.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/ShareView/BibbiMissionView.swift diff --git a/14th-team5-iOS/Core/Sources/BBCommons/ShareView/BibbiProfileView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/ShareView/BibbiProfileView.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBCommons/ShareView/BibbiProfileView.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/ShareView/BibbiProfileView.swift diff --git a/14th-team5-iOS/Core/Sources/BBCommons/ShareView/BibbiTermsView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/ShareView/BibbiTermsView.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBCommons/ShareView/BibbiTermsView.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/ShareView/BibbiTermsView.swift diff --git a/14th-team5-iOS/Core/Sources/BBNetwork/API.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/API.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBNetwork/API.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/API.swift diff --git a/14th-team5-iOS/Core/Sources/BBNetwork/APIConfig.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/APIConfig.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBNetwork/APIConfig.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/APIConfig.swift diff --git a/14th-team5-iOS/Core/Sources/BBNetwork/APISpec.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/APISpec.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBNetwork/APISpec.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/APISpec.swift diff --git a/14th-team5-iOS/Core/Sources/BBRx/Repositories/App/App.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBRx/Repositories/App/App.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBRx/Repositories/App/App.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBRx/Repositories/App/App.swift diff --git a/14th-team5-iOS/Core/Sources/BBRx/Repositories/DeepLink/DeepLinkRepository.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBRx/Repositories/DeepLink/DeepLinkRepository.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBRx/Repositories/DeepLink/DeepLinkRepository.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBRx/Repositories/DeepLink/DeepLinkRepository.swift diff --git a/14th-team5-iOS/Core/Sources/BBRx/Repositories/DeepLink/Models/NotificationDeepLink.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBRx/Repositories/DeepLink/Models/NotificationDeepLink.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBRx/Repositories/DeepLink/Models/NotificationDeepLink.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBRx/Repositories/DeepLink/Models/NotificationDeepLink.swift diff --git a/14th-team5-iOS/Core/Sources/BBRx/Repositories/DeepLink/Models/WidgetDeepLink.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBRx/Repositories/DeepLink/Models/WidgetDeepLink.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBRx/Repositories/DeepLink/Models/WidgetDeepLink.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBRx/Repositories/DeepLink/Models/WidgetDeepLink.swift diff --git a/14th-team5-iOS/Core/Sources/BBRx/Repositories/Member/Emojis.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBRx/Repositories/Member/Emojis.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBRx/Repositories/Member/Emojis.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBRx/Repositories/Member/Emojis.swift diff --git a/14th-team5-iOS/Core/Sources/BBRx/Repositories/Member/MemberRepository.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBRx/Repositories/Member/MemberRepository.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBRx/Repositories/Member/MemberRepository.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBRx/Repositories/Member/MemberRepository.swift diff --git a/14th-team5-iOS/Core/Sources/BBRx/Repositories/Token/TokenRepository.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBRx/Repositories/Token/TokenRepository.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBRx/Repositories/Token/TokenRepository.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBRx/Repositories/Token/TokenRepository.swift diff --git a/14th-team5-iOS/Core/Sources/BBRx/RxObject.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBRx/RxObject.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBRx/RxObject.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBRx/RxObject.swift diff --git a/14th-team5-iOS/Core/Sources/BBRx/RxUtils.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBRx/RxUtils.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBRx/RxUtils.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBRx/RxUtils.swift diff --git a/14th-team5-iOS/Core/Sources/BBServices/ActivityGlobalState.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ActivityGlobalState.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBServices/ActivityGlobalState.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBServices/ActivityGlobalState.swift diff --git a/14th-team5-iOS/Core/Sources/BBServices/CalendarGlobalState.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBServices/CalendarGlobalState.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBServices/CalendarGlobalState.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBServices/CalendarGlobalState.swift diff --git a/14th-team5-iOS/Core/Sources/BBServices/GlobalStateProvider.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBServices/GlobalStateProvider.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBServices/GlobalStateProvider.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBServices/GlobalStateProvider.swift diff --git a/14th-team5-iOS/Core/Sources/BBServices/MainService.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBServices/MainService.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBServices/MainService.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBServices/MainService.swift diff --git a/14th-team5-iOS/Core/Sources/BBServices/PostGlobalState.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBServices/PostGlobalState.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBServices/PostGlobalState.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBServices/PostGlobalState.swift diff --git a/14th-team5-iOS/Core/Sources/BBServices/ProfileFeedGlobalState.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ProfileFeedGlobalState.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBServices/ProfileFeedGlobalState.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBServices/ProfileFeedGlobalState.swift diff --git a/14th-team5-iOS/Core/Sources/BBServices/ProfileGlobalState.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ProfileGlobalState.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBServices/ProfileGlobalState.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBServices/ProfileGlobalState.swift diff --git a/14th-team5-iOS/Core/Sources/BBServices/RealEmojiGlobalState.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBServices/RealEmojiGlobalState.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBServices/RealEmojiGlobalState.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBServices/RealEmojiGlobalState.swift diff --git a/14th-team5-iOS/Core/Sources/BBServices/TimerGlobalState.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBServices/TimerGlobalState.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBServices/TimerGlobalState.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBServices/TimerGlobalState.swift diff --git a/14th-team5-iOS/Core/Sources/BBServices/ToastMessageGlobalState.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ToastMessageGlobalState.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBServices/ToastMessageGlobalState.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBServices/ToastMessageGlobalState.swift diff --git a/14th-team5-iOS/Core/Sources/BBStorages/InMemoryWrapper/InMemoryKey.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/InMemoryWrapper/InMemoryKey.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBStorages/InMemoryWrapper/InMemoryKey.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBStorages/InMemoryWrapper/InMemoryKey.swift diff --git a/14th-team5-iOS/Core/Sources/BBStorages/InMemoryWrapper/InMemoryStore.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/InMemoryWrapper/InMemoryStore.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBStorages/InMemoryWrapper/InMemoryStore.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBStorages/InMemoryWrapper/InMemoryStore.swift diff --git a/14th-team5-iOS/Core/Sources/BBStorages/InMemoryWrapper/InMemorySubscript.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/InMemoryWrapper/InMemorySubscript.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBStorages/InMemoryWrapper/InMemorySubscript.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBStorages/InMemoryWrapper/InMemorySubscript.swift diff --git a/14th-team5-iOS/Core/Sources/BBStorages/InMemoryWrapper/InMemoryType.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/InMemoryWrapper/InMemoryType.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBStorages/InMemoryWrapper/InMemoryType.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBStorages/InMemoryWrapper/InMemoryType.swift diff --git a/14th-team5-iOS/Core/Sources/BBStorages/InMemoryWrapper/InMemoryWrapper.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/InMemoryWrapper/InMemoryWrapper.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBStorages/InMemoryWrapper/InMemoryWrapper.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBStorages/InMemoryWrapper/InMemoryWrapper.swift diff --git a/14th-team5-iOS/Core/Sources/BBStorages/KeychainWrapper/KeychainItemAccessibility.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/KeychainWrapper/KeychainItemAccessibility.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBStorages/KeychainWrapper/KeychainItemAccessibility.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBStorages/KeychainWrapper/KeychainItemAccessibility.swift diff --git a/14th-team5-iOS/Core/Sources/BBStorages/KeychainWrapper/KeychainType.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/KeychainWrapper/KeychainType.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBStorages/KeychainWrapper/KeychainType.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBStorages/KeychainWrapper/KeychainType.swift diff --git a/14th-team5-iOS/Core/Sources/BBStorages/KeychainWrapper/KeychainWrapper.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/KeychainWrapper/KeychainWrapper.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBStorages/KeychainWrapper/KeychainWrapper.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBStorages/KeychainWrapper/KeychainWrapper.swift diff --git a/14th-team5-iOS/Core/Sources/BBStorages/KeychainWrapper/KeychainWrapperKey.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/KeychainWrapper/KeychainWrapperKey.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBStorages/KeychainWrapper/KeychainWrapperKey.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBStorages/KeychainWrapper/KeychainWrapperKey.swift diff --git a/14th-team5-iOS/Core/Sources/BBStorages/KeychainWrapper/KeychainWrapperSubscript.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/KeychainWrapper/KeychainWrapperSubscript.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBStorages/KeychainWrapper/KeychainWrapperSubscript.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBStorages/KeychainWrapper/KeychainWrapperSubscript.swift diff --git a/14th-team5-iOS/Core/Sources/BBStorages/UserDefaultsWrapper/UserDefaultsKey.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/UserDefaultsWrapper/UserDefaultsKey.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBStorages/UserDefaultsWrapper/UserDefaultsKey.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBStorages/UserDefaultsWrapper/UserDefaultsKey.swift diff --git a/14th-team5-iOS/Core/Sources/BBStorages/UserDefaultsWrapper/UserDefaultsSubscript.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/UserDefaultsWrapper/UserDefaultsSubscript.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBStorages/UserDefaultsWrapper/UserDefaultsSubscript.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBStorages/UserDefaultsWrapper/UserDefaultsSubscript.swift diff --git a/14th-team5-iOS/Core/Sources/BBStorages/UserDefaultsWrapper/UserDefaultsType.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/UserDefaultsWrapper/UserDefaultsType.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBStorages/UserDefaultsWrapper/UserDefaultsType.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBStorages/UserDefaultsWrapper/UserDefaultsType.swift diff --git a/14th-team5-iOS/Core/Sources/BBStorages/UserDefaultsWrapper/UserDefaultsWrapper.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/UserDefaultsWrapper/UserDefaultsWrapper.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/BBStorages/UserDefaultsWrapper/UserDefaultsWrapper.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBStorages/UserDefaultsWrapper/UserDefaultsWrapper.swift diff --git a/14th-team5-iOS/Core/Sources/Base/BBNavigationViewController.swift b/14th-team5-iOS/Core/Sources/Bibbi/Base/BBNavigationViewController.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Base/BBNavigationViewController.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/Base/BBNavigationViewController.swift diff --git a/14th-team5-iOS/Core/Sources/Base/BaseCollectionViewCell.swift b/14th-team5-iOS/Core/Sources/Bibbi/Base/BaseCollectionViewCell.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Base/BaseCollectionViewCell.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/Base/BaseCollectionViewCell.swift diff --git a/14th-team5-iOS/Core/Sources/Base/BaseContainer.swift b/14th-team5-iOS/Core/Sources/Bibbi/Base/BaseContainer.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Base/BaseContainer.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/Base/BaseContainer.swift diff --git a/14th-team5-iOS/Core/Sources/Base/BaseDIContainer.swift b/14th-team5-iOS/Core/Sources/Bibbi/Base/BaseDIContainer.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Base/BaseDIContainer.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/Base/BaseDIContainer.swift diff --git a/14th-team5-iOS/Core/Sources/Base/BaseNavigator.swift b/14th-team5-iOS/Core/Sources/Bibbi/Base/BaseNavigator.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Base/BaseNavigator.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/Base/BaseNavigator.swift diff --git a/14th-team5-iOS/Core/Sources/Base/BasePageViewController.swift b/14th-team5-iOS/Core/Sources/Bibbi/Base/BasePageViewController.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Base/BasePageViewController.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/Base/BasePageViewController.swift diff --git a/14th-team5-iOS/Core/Sources/Base/BaseService.swift b/14th-team5-iOS/Core/Sources/Bibbi/Base/BaseService.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Base/BaseService.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/Base/BaseService.swift diff --git a/14th-team5-iOS/Core/Sources/Base/BaseTableViewCell.swift b/14th-team5-iOS/Core/Sources/Bibbi/Base/BaseTableViewCell.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Base/BaseTableViewCell.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/Base/BaseTableViewCell.swift diff --git a/14th-team5-iOS/Core/Sources/Base/BaseView.swift b/14th-team5-iOS/Core/Sources/Bibbi/Base/BaseView.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Base/BaseView.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/Base/BaseView.swift diff --git a/14th-team5-iOS/Core/Sources/Base/BaseViewController.swift b/14th-team5-iOS/Core/Sources/Bibbi/Base/BaseViewController.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Base/BaseViewController.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/Base/BaseViewController.swift diff --git a/14th-team5-iOS/Core/Sources/Base/BaseViewModel.swift b/14th-team5-iOS/Core/Sources/Bibbi/Base/BaseViewModel.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Base/BaseViewModel.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/Base/BaseViewModel.swift diff --git a/14th-team5-iOS/Core/Sources/Base/BaseWrapper.swift b/14th-team5-iOS/Core/Sources/Bibbi/Base/BaseWrapper.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Base/BaseWrapper.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/Base/BaseWrapper.swift diff --git a/14th-team5-iOS/Core/Sources/Base/BaseWrapperView.swift b/14th-team5-iOS/Core/Sources/Bibbi/Base/BaseWrapperView.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Base/BaseWrapperView.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/Base/BaseWrapperView.swift diff --git a/14th-team5-iOS/Core/Sources/Base/ReactorViewController.swift b/14th-team5-iOS/Core/Sources/Bibbi/Base/ReactorViewController.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Base/ReactorViewController.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/Base/ReactorViewController.swift diff --git a/14th-team5-iOS/Core/Sources/DIContainer/Container.swift b/14th-team5-iOS/Core/Sources/Bibbi/DIContainer/Container.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/DIContainer/Container.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/DIContainer/Container.swift diff --git a/14th-team5-iOS/Core/Sources/DIContainer/DIContainer.swift b/14th-team5-iOS/Core/Sources/Bibbi/DIContainer/DIContainer.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/DIContainer/DIContainer.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/DIContainer/DIContainer.swift diff --git a/14th-team5-iOS/Core/Sources/DIContainer/InjectIdentifier.swift b/14th-team5-iOS/Core/Sources/Bibbi/DIContainer/InjectIdentifier.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/DIContainer/InjectIdentifier.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/DIContainer/InjectIdentifier.swift diff --git a/14th-team5-iOS/Core/Sources/DIContainer/Injected+PropertyWrapper.swift b/14th-team5-iOS/Core/Sources/Bibbi/DIContainer/Injected+PropertyWrapper.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/DIContainer/Injected+PropertyWrapper.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/DIContainer/Injected+PropertyWrapper.swift diff --git a/14th-team5-iOS/Core/Sources/DIContainer/InjectedSafe+PropertyWrapper.swift b/14th-team5-iOS/Core/Sources/Bibbi/DIContainer/InjectedSafe+PropertyWrapper.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/DIContainer/InjectedSafe+PropertyWrapper.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/DIContainer/InjectedSafe+PropertyWrapper.swift diff --git a/14th-team5-iOS/Core/Sources/DIContainer/Navigator+PropertyWrapper.swift b/14th-team5-iOS/Core/Sources/Bibbi/DIContainer/Navigator+PropertyWrapper.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/DIContainer/Navigator+PropertyWrapper.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/DIContainer/Navigator+PropertyWrapper.swift diff --git a/14th-team5-iOS/Core/Sources/Extensions/AVCapturePhotoOutput+Ext.swift b/14th-team5-iOS/Core/Sources/Bibbi/Extensions/AVCapturePhotoOutput+Ext.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Extensions/AVCapturePhotoOutput+Ext.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/Extensions/AVCapturePhotoOutput+Ext.swift diff --git a/14th-team5-iOS/Core/Sources/Extensions/Array+Ext.swift b/14th-team5-iOS/Core/Sources/Bibbi/Extensions/Array+Ext.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Extensions/Array+Ext.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/Extensions/Array+Ext.swift diff --git a/14th-team5-iOS/Core/Sources/Extensions/Bundle+Ext.swift b/14th-team5-iOS/Core/Sources/Bibbi/Extensions/Bundle+Ext.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Extensions/Bundle+Ext.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/Extensions/Bundle+Ext.swift diff --git a/14th-team5-iOS/Core/Sources/Extensions/Codable+Ext.swift b/14th-team5-iOS/Core/Sources/Bibbi/Extensions/Codable+Ext.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Extensions/Codable+Ext.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/Extensions/Codable+Ext.swift diff --git a/14th-team5-iOS/Core/Sources/Extensions/Date+Ext.swift b/14th-team5-iOS/Core/Sources/Bibbi/Extensions/Date+Ext.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Extensions/Date+Ext.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/Extensions/Date+Ext.swift diff --git a/14th-team5-iOS/Core/Sources/Extensions/DateFormatter+Ext.swift b/14th-team5-iOS/Core/Sources/Bibbi/Extensions/DateFormatter+Ext.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Extensions/DateFormatter+Ext.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/Extensions/DateFormatter+Ext.swift diff --git a/14th-team5-iOS/Core/Sources/Extensions/Int+Ext.swift b/14th-team5-iOS/Core/Sources/Bibbi/Extensions/Int+Ext.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Extensions/Int+Ext.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/Extensions/Int+Ext.swift diff --git a/14th-team5-iOS/Core/Sources/Extensions/NSItemProvider+Ext.swift b/14th-team5-iOS/Core/Sources/Bibbi/Extensions/NSItemProvider+Ext.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Extensions/NSItemProvider+Ext.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/Extensions/NSItemProvider+Ext.swift diff --git a/14th-team5-iOS/Core/Sources/Extensions/NSMutableAttributedString+Ext.swift b/14th-team5-iOS/Core/Sources/Bibbi/Extensions/NSMutableAttributedString+Ext.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Extensions/NSMutableAttributedString+Ext.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/Extensions/NSMutableAttributedString+Ext.swift diff --git a/14th-team5-iOS/Core/Sources/Extensions/Notification+Ext.swift b/14th-team5-iOS/Core/Sources/Bibbi/Extensions/Notification+Ext.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Extensions/Notification+Ext.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/Extensions/Notification+Ext.swift diff --git a/14th-team5-iOS/Core/Sources/Extensions/Reactive+Ext.swift b/14th-team5-iOS/Core/Sources/Bibbi/Extensions/Reactive+Ext.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Extensions/Reactive+Ext.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/Extensions/Reactive+Ext.swift diff --git a/14th-team5-iOS/Core/Sources/Extensions/String+Ext.swift b/14th-team5-iOS/Core/Sources/Bibbi/Extensions/String+Ext.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Extensions/String+Ext.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/Extensions/String+Ext.swift diff --git a/14th-team5-iOS/Core/Sources/Extensions/UIButton+Ext.swift b/14th-team5-iOS/Core/Sources/Bibbi/Extensions/UIButton+Ext.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Extensions/UIButton+Ext.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/Extensions/UIButton+Ext.swift diff --git a/14th-team5-iOS/Core/Sources/Extensions/UICollectionViewLayoutAttributes+Ext.swift b/14th-team5-iOS/Core/Sources/Bibbi/Extensions/UICollectionViewLayoutAttributes+Ext.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Extensions/UICollectionViewLayoutAttributes+Ext.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/Extensions/UICollectionViewLayoutAttributes+Ext.swift diff --git a/14th-team5-iOS/Core/Sources/Extensions/UIColor+Ext.swift b/14th-team5-iOS/Core/Sources/Bibbi/Extensions/UIColor+Ext.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Extensions/UIColor+Ext.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/Extensions/UIColor+Ext.swift diff --git a/14th-team5-iOS/Core/Sources/Extensions/UIFont+Ext.swift b/14th-team5-iOS/Core/Sources/Bibbi/Extensions/UIFont+Ext.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Extensions/UIFont+Ext.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/Extensions/UIFont+Ext.swift diff --git a/14th-team5-iOS/Core/Sources/Extensions/UIImage+Ext.swift b/14th-team5-iOS/Core/Sources/Bibbi/Extensions/UIImage+Ext.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Extensions/UIImage+Ext.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/Extensions/UIImage+Ext.swift diff --git a/14th-team5-iOS/Core/Sources/Extensions/UIImageView+Ext.swift b/14th-team5-iOS/Core/Sources/Bibbi/Extensions/UIImageView+Ext.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Extensions/UIImageView+Ext.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/Extensions/UIImageView+Ext.swift diff --git a/14th-team5-iOS/Core/Sources/Extensions/UINavigationController+Ext.swift b/14th-team5-iOS/Core/Sources/Bibbi/Extensions/UINavigationController+Ext.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Extensions/UINavigationController+Ext.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/Extensions/UINavigationController+Ext.swift diff --git a/14th-team5-iOS/Core/Sources/Extensions/UIPageViewController+Ext.swift b/14th-team5-iOS/Core/Sources/Bibbi/Extensions/UIPageViewController+Ext.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Extensions/UIPageViewController+Ext.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/Extensions/UIPageViewController+Ext.swift diff --git a/14th-team5-iOS/Core/Sources/Extensions/UIScreen+Ext.swift b/14th-team5-iOS/Core/Sources/Bibbi/Extensions/UIScreen+Ext.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Extensions/UIScreen+Ext.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/Extensions/UIScreen+Ext.swift diff --git a/14th-team5-iOS/Core/Sources/Extensions/UIStackView+Ext.swift b/14th-team5-iOS/Core/Sources/Bibbi/Extensions/UIStackView+Ext.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Extensions/UIStackView+Ext.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/Extensions/UIStackView+Ext.swift diff --git a/14th-team5-iOS/Core/Sources/Extensions/UITextField+Ext.swift b/14th-team5-iOS/Core/Sources/Bibbi/Extensions/UITextField+Ext.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Extensions/UITextField+Ext.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/Extensions/UITextField+Ext.swift diff --git a/14th-team5-iOS/Core/Sources/Extensions/UIView+Ext.swift b/14th-team5-iOS/Core/Sources/Bibbi/Extensions/UIView+Ext.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Extensions/UIView+Ext.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/Extensions/UIView+Ext.swift diff --git a/14th-team5-iOS/Core/Sources/Extensions/UIViewController+Ext.swift b/14th-team5-iOS/Core/Sources/Bibbi/Extensions/UIViewController+Ext.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Extensions/UIViewController+Ext.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/Extensions/UIViewController+Ext.swift diff --git a/14th-team5-iOS/Core/Sources/Extensions/UIVisualEffectView+Ext.swift b/14th-team5-iOS/Core/Sources/Bibbi/Extensions/UIVisualEffectView+Ext.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Extensions/UIVisualEffectView+Ext.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/Extensions/UIVisualEffectView+Ext.swift diff --git a/14th-team5-iOS/Core/Sources/Extensions/UIWindow+Ext.swift b/14th-team5-iOS/Core/Sources/Bibbi/Extensions/UIWindow+Ext.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Extensions/UIWindow+Ext.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/Extensions/UIWindow+Ext.swift diff --git a/14th-team5-iOS/Core/Sources/Extensions/URLRequeset+Ext.swift b/14th-team5-iOS/Core/Sources/Bibbi/Extensions/URLRequeset+Ext.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Extensions/URLRequeset+Ext.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/Extensions/URLRequeset+Ext.swift diff --git a/14th-team5-iOS/Core/Sources/Extensions/UserDefaults+Ext.swift b/14th-team5-iOS/Core/Sources/Bibbi/Extensions/UserDefaults+Ext.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Extensions/UserDefaults+Ext.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/Extensions/UserDefaults+Ext.swift diff --git a/14th-team5-iOS/Core/Sources/Utilties/Analytics/Mixpanel/MixPanelService+Account.swift b/14th-team5-iOS/Core/Sources/Bibbi/Utilties/Analytics/Mixpanel/MixPanelService+Account.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Utilties/Analytics/Mixpanel/MixPanelService+Account.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/Utilties/Analytics/Mixpanel/MixPanelService+Account.swift diff --git a/14th-team5-iOS/Core/Sources/Utilties/Analytics/Mixpanel/MixPanelService+Camera.swift b/14th-team5-iOS/Core/Sources/Bibbi/Utilties/Analytics/Mixpanel/MixPanelService+Camera.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Utilties/Analytics/Mixpanel/MixPanelService+Camera.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/Utilties/Analytics/Mixpanel/MixPanelService+Camera.swift diff --git a/14th-team5-iOS/Core/Sources/Utilties/Analytics/Mixpanel/MixPanelService+Family.swift b/14th-team5-iOS/Core/Sources/Bibbi/Utilties/Analytics/Mixpanel/MixPanelService+Family.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Utilties/Analytics/Mixpanel/MixPanelService+Family.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/Utilties/Analytics/Mixpanel/MixPanelService+Family.swift diff --git a/14th-team5-iOS/Core/Sources/Utilties/Analytics/Mixpanel/MixPanelService+Home.swift b/14th-team5-iOS/Core/Sources/Bibbi/Utilties/Analytics/Mixpanel/MixPanelService+Home.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Utilties/Analytics/Mixpanel/MixPanelService+Home.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/Utilties/Analytics/Mixpanel/MixPanelService+Home.swift diff --git a/14th-team5-iOS/Core/Sources/Utilties/Analytics/Mixpanel/MixpanelService.swift b/14th-team5-iOS/Core/Sources/Bibbi/Utilties/Analytics/Mixpanel/MixpanelService.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Utilties/Analytics/Mixpanel/MixpanelService.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/Utilties/Analytics/Mixpanel/MixpanelService.swift diff --git a/14th-team5-iOS/Core/Sources/Utilties/Haptic.swift b/14th-team5-iOS/Core/Sources/Bibbi/Utilties/Haptic.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Utilties/Haptic.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/Utilties/Haptic.swift diff --git a/14th-team5-iOS/Core/Sources/Utilties/Modifiers/Shimmer.swift b/14th-team5-iOS/Core/Sources/Bibbi/Utilties/Modifiers/Shimmer.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Utilties/Modifiers/Shimmer.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/Utilties/Modifiers/Shimmer.swift diff --git a/14th-team5-iOS/Core/Sources/Utilties/Types/Enums/URLTypes.swift b/14th-team5-iOS/Core/Sources/Bibbi/Utilties/Types/Enums/URLTypes.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Utilties/Types/Enums/URLTypes.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/Utilties/Types/Enums/URLTypes.swift diff --git a/14th-team5-iOS/Core/Sources/Utilties/Types/Protocols/ItemIndexable.swift b/14th-team5-iOS/Core/Sources/Bibbi/Utilties/Types/Protocols/ItemIndexable.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Utilties/Types/Protocols/ItemIndexable.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/Utilties/Types/Protocols/ItemIndexable.swift diff --git a/Bibbi.xcworkspace/contents.xcworkspacedata b/Bibbi.xcworkspace/contents.xcworkspacedata index 1219ce311..7e570dad8 100644 --- a/Bibbi.xcworkspace/contents.xcworkspacedata +++ b/Bibbi.xcworkspace/contents.xcworkspacedata @@ -21,83 +21,88 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + location = "container:" + name = "Dependencies"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme b/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme index defe8e429..1781b4e89 100644 --- a/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme +++ b/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme @@ -17,7 +17,7 @@ BlueprintIdentifier = "2A717419231F3C0B8A3E0F06" BuildableName = "Alamofire.framework" BlueprintName = "Alamofire" - ReferencedContainer = "container:Tuist/Dependencies/SwiftPackageManager/.build/checkouts/Alamofire/Alamofire.xcodeproj"> + ReferencedContainer = "container:Tuist/.build/tuist-derived/Alamofire/Alamofire.xcodeproj"> + + + + + + + + + + + + + + + + + ReferencedContainer = "container:Tuist/.build/tuist-derived/RxDataSources/RxDataSources.xcodeproj"> + ReferencedContainer = "container:Tuist/.build/tuist-derived/Promises/Promises.xcodeproj"> + ReferencedContainer = "container:Tuist/.build/tuist-derived/FSCalendar/FSCalendar.xcodeproj"> + ReferencedContainer = "container:Tuist/.build/tuist-derived/Firebase/Firebase.xcodeproj"> + ReferencedContainer = "container:Tuist/.build/tuist-derived/Firebase/Firebase.xcodeproj"> + ReferencedContainer = "container:Tuist/.build/tuist-derived/Firebase/Firebase.xcodeproj"> + ReferencedContainer = "container:Tuist/.build/tuist-derived/Firebase/Firebase.xcodeproj"> + ReferencedContainer = "container:Tuist/.build/tuist-derived/Firebase/Firebase.xcodeproj"> + ReferencedContainer = "container:Tuist/.build/tuist-derived/Firebase/Firebase.xcodeproj"> + ReferencedContainer = "container:Tuist/.build/tuist-derived/Firebase/Firebase.xcodeproj"> + ReferencedContainer = "container:Tuist/.build/tuist-derived/Firebase/Firebase.xcodeproj"> + ReferencedContainer = "container:Tuist/.build/tuist-derived/Firebase/Firebase.xcodeproj"> + ReferencedContainer = "container:Tuist/.build/tuist-derived/Firebase/Firebase.xcodeproj"> + ReferencedContainer = "container:Tuist/.build/tuist-derived/Firebase/Firebase.xcodeproj"> + ReferencedContainer = "container:Tuist/.build/tuist-derived/GoogleAppMeasurement/GoogleAppMeasurement.xcodeproj"> + ReferencedContainer = "container:Tuist/.build/tuist-derived/GoogleDataTransport/GoogleDataTransport.xcodeproj"> + ReferencedContainer = "container:Tuist/.build/tuist-derived/GoogleDataTransport/GoogleDataTransport.xcodeproj"> + ReferencedContainer = "container:Tuist/.build/tuist-derived/GoogleUtilities/GoogleUtilities.xcodeproj"> + ReferencedContainer = "container:Tuist/.build/tuist-derived/GoogleUtilities/GoogleUtilities.xcodeproj"> + ReferencedContainer = "container:Tuist/.build/tuist-derived/GoogleUtilities/GoogleUtilities.xcodeproj"> + ReferencedContainer = "container:Tuist/.build/tuist-derived/GoogleUtilities/GoogleUtilities.xcodeproj"> + ReferencedContainer = "container:Tuist/.build/tuist-derived/GoogleUtilities/GoogleUtilities.xcodeproj"> + ReferencedContainer = "container:Tuist/.build/tuist-derived/GoogleUtilities/GoogleUtilities.xcodeproj"> + ReferencedContainer = "container:Tuist/.build/tuist-derived/GoogleUtilities/GoogleUtilities.xcodeproj"> + ReferencedContainer = "container:Tuist/.build/tuist-derived/GoogleUtilities/GoogleUtilities.xcodeproj"> + + + + + + + + + + + + + + + + + + + + + + + + + BlueprintIdentifier = "2B598F04910F15488C9998AE" + BuildableName = "GoogleUtilities_GoogleUtilities_Reachability.bundle" + BlueprintName = "GoogleUtilities_GoogleUtilities-Reachability" + ReferencedContainer = "container:Tuist/.build/tuist-derived/GoogleUtilities/GoogleUtilities.xcodeproj"> + BlueprintIdentifier = "72B78008E647BF5B555D350D" + BuildableName = "GoogleUtilities_GoogleUtilities_UserDefaults.bundle" + BlueprintName = "GoogleUtilities_GoogleUtilities-UserDefaults" + ReferencedContainer = "container:Tuist/.build/tuist-derived/GoogleUtilities/GoogleUtilities.xcodeproj"> + ReferencedContainer = "container:Tuist/.build/tuist-derived/KakaoOpenSDK/KakaoOpenSDK.xcodeproj"> + ReferencedContainer = "container:Tuist/.build/tuist-derived/KakaoOpenSDK/KakaoOpenSDK.xcodeproj"> + ReferencedContainer = "container:Tuist/.build/tuist-derived/KakaoOpenSDK/KakaoOpenSDK.xcodeproj"> + ReferencedContainer = "container:Tuist/.build/tuist-derived/KakaoOpenSDK/KakaoOpenSDK.xcodeproj"> + ReferencedContainer = "container:Tuist/.build/tuist-derived/KakaoOpenSDK/KakaoOpenSDK.xcodeproj"> + ReferencedContainer = "container:Tuist/.build/tuist-derived/KakaoOpenSDK/KakaoOpenSDK.xcodeproj"> + ReferencedContainer = "container:Tuist/.build/tuist-derived/KakaoOpenSDK/KakaoOpenSDK.xcodeproj"> + ReferencedContainer = "container:Tuist/.build/tuist-derived/KakaoOpenSDK/KakaoOpenSDK.xcodeproj"> + ReferencedContainer = "container:Tuist/.build/tuist-derived/KakaoOpenSDK/KakaoOpenSDK.xcodeproj"> + ReferencedContainer = "container:Tuist/.build/tuist-derived/KakaoOpenSDK/KakaoOpenSDK.xcodeproj"> + ReferencedContainer = "container:Tuist/.build/tuist-derived/Kingfisher/Kingfisher.xcodeproj"> + + + + + ReferencedContainer = "container:Tuist/.build/tuist-derived/Lottie/Lottie.xcodeproj"> + + + + + + + + + + + + + ReferencedContainer = "container:Tuist/.build/tuist-derived/Mixpanel/Mixpanel.xcodeproj"> + + + + + ReferencedContainer = "container:Tuist/.build/tuist-derived/Promises/Promises.xcodeproj"> + ReferencedContainer = "container:Tuist/.build/tuist-derived/ReactorKit/ReactorKit.xcodeproj"> + ReferencedContainer = "container:Tuist/.build/tuist-derived/ReactorKit/ReactorKit.xcodeproj"> + ReferencedContainer = "container:Tuist/.build/tuist-derived/RxAlamofire/RxAlamofire.xcodeproj"> + ReferencedContainer = "container:Tuist/.build/tuist-derived/RxSwift/RxSwift.xcodeproj"> + ReferencedContainer = "container:Tuist/.build/tuist-derived/RxSwift/RxSwift.xcodeproj"> + ReferencedContainer = "container:Tuist/.build/tuist-derived/RxDataSources/RxDataSources.xcodeproj"> + ReferencedContainer = "container:Tuist/.build/tuist-derived/RxKakaoOpenSDK/RxKakaoOpenSDK.xcodeproj"> + + + + + ReferencedContainer = "container:Tuist/.build/tuist-derived/RxKakaoOpenSDK/RxKakaoOpenSDK.xcodeproj"> + ReferencedContainer = "container:Tuist/.build/tuist-derived/RxKakaoOpenSDK/RxKakaoOpenSDK.xcodeproj"> + ReferencedContainer = "container:Tuist/.build/tuist-derived/RxKakaoOpenSDK/RxKakaoOpenSDK.xcodeproj"> + ReferencedContainer = "container:Tuist/.build/tuist-derived/RxKakaoOpenSDK/RxKakaoOpenSDK.xcodeproj"> + ReferencedContainer = "container:Tuist/.build/tuist-derived/RxKakaoOpenSDK/RxKakaoOpenSDK.xcodeproj"> + ReferencedContainer = "container:Tuist/.build/tuist-derived/RxSwift/RxSwift.xcodeproj"> + ReferencedContainer = "container:Tuist/.build/tuist-derived/RxSwift/RxSwift.xcodeproj"> + ReferencedContainer = "container:Tuist/.build/tuist-derived/SnapKit/SnapKit.xcodeproj"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ReferencedContainer = "container:Tuist/.build/tuist-derived/Then/Then.xcodeproj"> + ReferencedContainer = "container:Tuist/.build/tuist-derived/WeakMapTable/WeakMapTable.xcodeproj"> + + + + + ReferencedContainer = "container:Tuist/.build/tuist-derived/nanopb/nanopb.xcodeproj"> + ReferencedContainer = "container:Tuist/.build/tuist-derived/nanopb/nanopb.xcodeproj"> + ReferencedContainer = "container:Tuist/.build/tuist-derived/GoogleUtilities/GoogleUtilities.xcodeproj"> @@ -1026,7 +1446,7 @@ BlueprintIdentifier = "2A717419231F3C0B8A3E0F06" BuildableName = "Alamofire.framework" BlueprintName = "Alamofire" - ReferencedContainer = "container:Tuist/Dependencies/SwiftPackageManager/.build/checkouts/Alamofire/Alamofire.xcodeproj"> + ReferencedContainer = "container:Tuist/.build/tuist-derived/Alamofire/Alamofire.xcodeproj"> @@ -1042,7 +1462,7 @@ BlueprintIdentifier = "2A717419231F3C0B8A3E0F06" BuildableName = "Alamofire.framework" BlueprintName = "Alamofire" - ReferencedContainer = "container:Tuist/Dependencies/SwiftPackageManager/.build/checkouts/Alamofire/Alamofire.xcodeproj"> + ReferencedContainer = "container:Tuist/.build/tuist-derived/Alamofire/Alamofire.xcodeproj"> diff --git a/Tuist/Config.swift b/Tuist/Config.swift deleted file mode 100644 index ff8e64fa9..000000000 --- a/Tuist/Config.swift +++ /dev/null @@ -1,3 +0,0 @@ -import ProjectDescription - -let config = Config() diff --git a/Tuist/Dependencies.swift b/Tuist/Dependencies.swift deleted file mode 100644 index e4d5edaed..000000000 --- a/Tuist/Dependencies.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// Dependencies.swift -// Config -// -// Created by Kim dohyun on 11/16/23. -// - -import ProjectDescription -import ProjectDescriptionHelpers - - -let dependencies = Dependencies( - swiftPackageManager: SwiftPackageManagerDependencies([ - .reactorKit, - .rxSwift, - .rxDatasources, - .snapkit, - .then, - .firebase, - .kakaoSDK, - .kakaoSDKRx, - .kingFisher, - .fsCalendar, - .mixPanel, - .lottie, - .googleUtilities - ], productTypes: [ - "FSCalendar": .framework, - "Firebase": .framework, - "Lottie": .framework - ], - baseSettings: .settings( - configurations: [ - .build(.dev), - .build(.stg), - .build(.prd) - ] - ) - ), - platforms: [.iOS] -) diff --git a/Tuist/Package.resolved b/Tuist/Package.resolved new file mode 100644 index 000000000..c4e2eadb1 --- /dev/null +++ b/Tuist/Package.resolved @@ -0,0 +1,320 @@ +{ + "pins" : [ + { + "identity" : "abseil-cpp-binary", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/abseil-cpp-binary.git", + "state" : { + "revision" : "748c7837511d0e6a507737353af268484e1745e2", + "version" : "1.2024011601.1" + } + }, + { + "identity" : "alamofire", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Alamofire/Alamofire", + "state" : { + "revision" : "f455c2975872ccd2d9c81594c658af65716e9b9a", + "version" : "5.9.1" + } + }, + { + "identity" : "app-check", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/app-check.git", + "state" : { + "revision" : "3b62f154d00019ae29a71e9738800bb6f18b236d", + "version" : "10.19.2" + } + }, + { + "identity" : "bibbi-ios-package", + "kind" : "remoteSourceControl", + "location" : "https://github.com/bibbi-team/bibbi-iOS-package.git", + "state" : { + "revision" : "5fca42b0964fdce2a210fcd330fefaa2d5b95d9b", + "version" : "0.1.1" + } + }, + { + "identity" : "combine-schedulers", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/combine-schedulers", + "state" : { + "revision" : "9dc9cbe4bc45c65164fa653a563d8d8db61b09bb", + "version" : "1.0.0" + } + }, + { + "identity" : "firebase-ios-sdk", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/firebase-ios-sdk.git", + "state" : { + "revision" : "e57841b296d04370ea23580f908881b0ccab17b9", + "version" : "10.28.1" + } + }, + { + "identity" : "fscalendar", + "kind" : "remoteSourceControl", + "location" : "https://github.com/WenchaoD/FSCalendar.git", + "state" : { + "revision" : "0fbdec5172fccb90f707472eeaea4ffe095278f6", + "version" : "2.8.4" + } + }, + { + "identity" : "googleappmeasurement", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleAppMeasurement.git", + "state" : { + "revision" : "fe727587518729046fc1465625b9afd80b5ab361", + "version" : "10.28.0" + } + }, + { + "identity" : "googledatatransport", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleDataTransport.git", + "state" : { + "revision" : "a637d318ae7ae246b02d7305121275bc75ed5565", + "version" : "9.4.0" + } + }, + { + "identity" : "googleutilities", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleUtilities.git", + "state" : { + "revision" : "57a1d307f42df690fdef2637f3e5b776da02aad6", + "version" : "7.13.3" + } + }, + { + "identity" : "grpc-binary", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/grpc-binary.git", + "state" : { + "revision" : "e9fad491d0673bdda7063a0341fb6b47a30c5359", + "version" : "1.62.2" + } + }, + { + "identity" : "gtm-session-fetcher", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/gtm-session-fetcher.git", + "state" : { + "revision" : "a2ab612cb980066ee56d90d60d8462992c07f24b", + "version" : "3.5.0" + } + }, + { + "identity" : "interop-ios-for-google-sdks", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/interop-ios-for-google-sdks.git", + "state" : { + "revision" : "2d12673670417654f08f5f90fdd62926dc3a2648", + "version" : "100.0.0" + } + }, + { + "identity" : "kakao-ios-sdk", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kakao/kakao-ios-sdk.git", + "state" : { + "revision" : "e9e649d3ba823c3673867d3d09010fc77005a940", + "version" : "2.22.3" + } + }, + { + "identity" : "kakao-ios-sdk-rx", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kakao/kakao-ios-sdk-rx.git", + "state" : { + "revision" : "37ac7e289fea5f62b3b1f62d06290a18a202a5b0", + "version" : "2.22.3" + } + }, + { + "identity" : "kingfisher", + "kind" : "remoteSourceControl", + "location" : "https://github.com/onevcat/Kingfisher.git", + "state" : { + "revision" : "2ef543ee21d63734e1c004ad6c870255e8716c50", + "version" : "7.12.0" + } + }, + { + "identity" : "leveldb", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/leveldb.git", + "state" : { + "revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1", + "version" : "1.22.5" + } + }, + { + "identity" : "lottie-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/airbnb/lottie-ios.git", + "state" : { + "revision" : "769b88d83a42ca8d5572b020c96f47e3690b3796", + "version" : "4.4.3" + } + }, + { + "identity" : "mixpanel-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/mixpanel/mixpanel-swift.git", + "state" : { + "revision" : "7f73084879cf6f8c50ab93e34ab0aae2569b04a8", + "version" : "4.2.7" + } + }, + { + "identity" : "nanopb", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/nanopb.git", + "state" : { + "revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1", + "version" : "2.30910.0" + } + }, + { + "identity" : "ohhttpstubs", + "kind" : "remoteSourceControl", + "location" : "https://github.com/AliSoftware/OHHTTPStubs.git", + "state" : { + "revision" : "12f19662426d0434d6c330c6974d53e2eb10ecd9", + "version" : "9.1.0" + } + }, + { + "identity" : "promises", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/promises.git", + "state" : { + "revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac", + "version" : "2.4.0" + } + }, + { + "identity" : "reactorkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ReactorKit/ReactorKit", + "state" : { + "revision" : "8fa33f09c6f6621a2aa536d739956d53b84dd139", + "version" : "3.2.0" + } + }, + { + "identity" : "rxalamofire", + "kind" : "remoteSourceControl", + "location" : "https://github.com/RxSwiftCommunity/RxAlamofire.git", + "state" : { + "revision" : "9535b58695b91fb67f56d58d6fd0c76462d7743a", + "version" : "6.1.2" + } + }, + { + "identity" : "rxdatasources", + "kind" : "remoteSourceControl", + "location" : "https://github.com/RxSwiftCommunity/RxDataSources", + "state" : { + "revision" : "90c29b48b628479097fe775ed1966d75ac374518", + "version" : "5.0.2" + } + }, + { + "identity" : "rxswift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ReactiveX/RxSwift", + "state" : { + "revision" : "b06a8c8596e4c3e8e7788e08e720e3248563ce6a", + "version" : "6.7.1" + } + }, + { + "identity" : "snapkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/SnapKit/SnapKit", + "state" : { + "revision" : "2842e6e84e82eb9a8dac0100ca90d9444b0307f4", + "version" : "5.7.1" + } + }, + { + "identity" : "swift-clocks", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-clocks", + "state" : { + "revision" : "a8421d68068d8f45fbceb418fbf22c5dad4afd33", + "version" : "1.0.2" + } + }, + { + "identity" : "swift-concurrency-extras", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-concurrency-extras", + "state" : { + "revision" : "bb5059bde9022d69ac516803f4f227d8ac967f71", + "version" : "1.1.0" + } + }, + { + "identity" : "swift-dependencies", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-dependencies", + "state" : { + "revision" : "00bc30ca03f98881329fab7f1bebef8eba472596", + "version" : "1.3.1" + } + }, + { + "identity" : "swift-protobuf", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-protobuf.git", + "state" : { + "revision" : "9f0c76544701845ad98716f3f6a774a892152bcb", + "version" : "1.26.0" + } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-syntax.git", + "state" : { + "revision" : "64889f0c732f210a935a0ad7cda38f77f876262d", + "version" : "509.1.1" + } + }, + { + "identity" : "then", + "kind" : "remoteSourceControl", + "location" : "https://github.com/devxoul/Then", + "state" : { + "revision" : "d41ef523faef0f911369f79c0b96815d9dbb6d7a", + "version" : "3.0.0" + } + }, + { + "identity" : "weakmaptable", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ReactorKit/WeakMapTable.git", + "state" : { + "revision" : "cb05d64cef2bbf51e85c53adee937df46540a74e", + "version" : "1.2.1" + } + }, + { + "identity" : "xctest-dynamic-overlay", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", + "state" : { + "revision" : "6f30bdba373bbd7fbfe241dddd732651f2fbd1e2", + "version" : "1.1.2" + } + } + ], + "version" : 2 +} diff --git a/Tuist/Package.swift b/Tuist/Package.swift new file mode 100644 index 000000000..7200682b6 --- /dev/null +++ b/Tuist/Package.swift @@ -0,0 +1,44 @@ +// swift-tools-version: 5.9 +import PackageDescription +import CompilerPluginSupport + +#if TUIST + import ProjectDescription + + let packageSettings = PackageSettings( + // Customize the product types for specific package product + // Default is .staticFramework + // productTypes: ["Alamofire": .framework,] + productTypes: ["Macros": .macro], + baseSettings: .settings(configurations: [ + .debug(name: .configuration("DEV")), + .release(name: .configuration("PRD")) + ]) + ) +#endif + +let package = Package( + name: "App", + dependencies: [ + .package(url: "https://github.com/ReactiveX/RxSwift.git", from: "6.6.0"), + .package(url: "https://github.com/SnapKit/SnapKit.git", from: "5.6.0"), + .package(url: "https://github.com/devxoul/Then.git", from: "3.0.0"), + .package(url: "https://github.com/firebase/firebase-ios-sdk.git", from: "10.24.0"), + .package(url: "https://github.com/RxSwiftCommunity/RxDataSources.git", from: "5.0.0"), + .package(url: "https://github.com/kakao/kakao-ios-sdk.git", from: "2.22.0"), + .package(url: "https://github.com/kakao/kakao-ios-sdk-rx.git", from: "2.22.0"), + .package(url: "https://github.com/onevcat/Kingfisher.git", from: "7.9.1"), + .package(url: "https://github.com/WenchaoD/FSCalendar.git", from: "2.8.3"), + .package(url: "https://github.com/mixpanel/mixpanel-swift.git", from: "4.2.0"), + .package(url: "https://github.com/airbnb/lottie-ios.git", from: "4.4.0"), + .package(url: "https://github.com/google/GoogleUtilities.git", from: "7.13.2"), + .package(url: "https://github.com/bibbi-team/bibbi-iOS-package.git", from: "0.1.0") + ], targets: [ + .target( + name: "App", + dependencies: [ + .product(name: "Macros", package: "bibbi-iOS-package") + ] + ) + ] +) diff --git a/Tuist/ProjectDescriptionHelpers/DeploymentTarget+Templates.swift b/Tuist/ProjectDescriptionHelpers/DeploymentTarget+Templates.swift index b69bf8faa..87f19168f 100644 --- a/Tuist/ProjectDescriptionHelpers/DeploymentTarget+Templates.swift +++ b/Tuist/ProjectDescriptionHelpers/DeploymentTarget+Templates.swift @@ -7,7 +7,7 @@ import ProjectDescription -extension DeploymentTarget { - public static let `defualt` = DeploymentTarget.iOS(targetVersion: "15.0", devices: [.iphone]) +extension DeploymentTargets { + public static let `defualt` = DeploymentTargets.iOS("15.0") } diff --git a/Tuist/ProjectDescriptionHelpers/Modular+Templates.swift b/Tuist/ProjectDescriptionHelpers/Modular+Templates.swift index 296a0504a..4a4f80d76 100644 --- a/Tuist/ProjectDescriptionHelpers/Modular+Templates.swift +++ b/Tuist/ProjectDescriptionHelpers/Modular+Templates.swift @@ -10,37 +10,40 @@ import ProjectDescription public struct ModularFactory { var name: ModuleLayer.RawValue - var platform: Platform + var destinations: Destinations var products: ProductsType + var platform: Platform var dependencies: [TargetDependency] var bundleId: String - var deploymentTarget: DeploymentTarget? + var deploymentTargets: DeploymentTargets? var infoPlist: InfoPlist? var sources: SourceFilesList? var resources: ResourceFileElements? var settings: Settings? - var entitlements: ProjectDescription.Path? + var entitlements: Entitlements? public init( name: ModuleLayer.RawValue = "", - platform: Platform = .iOS, + destinations: Destinations = .iOS, products: ProductsType = .framework(.static), + platform: Platform = .iOS, dependencies: [TargetDependency] = [], bundleId: String = "", - deploymentTarget: DeploymentTarget? = .defualt, + deploymentTargets: DeploymentTargets? = .defualt, infoPlist: InfoPlist? = .default, sources: SourceFilesList? = .default, resources: ResourceFileElements? = .default, settings: Settings? = nil, - entitlements: ProjectDescription.Path? = nil + entitlements: Entitlements? = nil ) { self.name = name - self.platform = platform + self.destinations = destinations self.products = products + self.platform = platform self.dependencies = dependencies self.bundleId = bundleId - self.deploymentTarget = deploymentTarget + self.deploymentTargets = deploymentTargets self.infoPlist = infoPlist self.sources = sources self.resources = resources @@ -54,16 +57,16 @@ extension Target { public static func makeModular(extenions layer: ExtensionsLayer, factory: ModularFactory) -> Target { switch layer { case .Widget: - return Target( + return .target( name: layer.rawValue + "Extension", - platform: factory.platform, + destinations: factory.destinations, product: factory.products.isExtensions ? .appExtension : .app, bundleId: factory.bundleId.lowercased(), - deploymentTarget: factory.deploymentTarget, + deploymentTargets: factory.deploymentTargets, infoPlist: factory.infoPlist, sources: factory.products.isExtensions ? .widgetExtensionSources : .default, resources: factory.products.isExtensions ? .widgetExtensionResources : .default, - entitlements: factory.entitlements, + entitlements: factory.entitlements, dependencies: factory.dependencies, settings: factory.settings ) @@ -74,12 +77,12 @@ extension Target { switch layer { case .App: - return Target( + return .target( name: layer.rawValue, - platform: factory.platform, + destinations: .iOS, product: factory.products.isApp ? .app : .staticFramework, bundleId: factory.bundleId.lowercased(), - deploymentTarget: factory.deploymentTarget, + deploymentTargets: factory.deploymentTargets, infoPlist: factory.infoPlist, sources: factory.sources, resources: factory.resources, @@ -88,12 +91,12 @@ extension Target { settings: factory.settings ) case .Data: - return Target( + return .target( name: layer.rawValue, - platform: factory.platform, + destinations: .iOS, product: factory.products.isLibrary ? .staticFramework : .framework, bundleId: "com.\(layer.rawValue).project".lowercased(), - deploymentTarget: factory.deploymentTarget, + deploymentTargets: factory.deploymentTargets, infoPlist: factory.infoPlist, sources: factory.sources, resources: factory.resources, @@ -102,12 +105,13 @@ extension Target { settings: factory.settings ) case .Domain: - return Target( + + return .target( name: layer.rawValue, - platform: factory.platform, + destinations: .iOS, product: factory.products.isFramework ? .staticFramework : .framework, bundleId: "com.\(layer.rawValue).project".lowercased(), - deploymentTarget: factory.deploymentTarget, + deploymentTargets: factory.deploymentTargets, infoPlist: factory.infoPlist, sources: factory.sources, resources: factory.resources, @@ -116,12 +120,12 @@ extension Target { settings: factory.settings ) case .Core: - return Target( + return .target( name: layer.rawValue, - platform: factory.platform, + destinations: .iOS, product: factory.products.isLibrary ? .framework : .staticFramework, bundleId: "com.\(layer.rawValue).project".lowercased(), - deploymentTarget: factory.deploymentTarget, + deploymentTargets: factory.deploymentTargets, infoPlist: factory.infoPlist, sources: factory.sources, resources: factory.resources, @@ -130,12 +134,13 @@ extension Target { settings: factory.settings ) case .DesignSystem: - return Target( + + return .target( name: layer.rawValue, - platform: factory.platform, + destinations: .iOS, product: factory.products.isFramework ? .staticFramework : .framework, bundleId: "com.\(layer.rawValue).project".lowercased(), - deploymentTarget: factory.deploymentTarget, + deploymentTargets: factory.deploymentTargets, infoPlist: factory.infoPlist, sources: factory.sources, resources: factory.resources, diff --git a/Tuist/ProjectDescriptionHelpers/ModuleType+Templates.swift b/Tuist/ProjectDescriptionHelpers/ModuleType+Templates.swift index 5fbf6ebe0..b2cbfc759 100644 --- a/Tuist/ProjectDescriptionHelpers/ModuleType+Templates.swift +++ b/Tuist/ProjectDescriptionHelpers/ModuleType+Templates.swift @@ -48,29 +48,33 @@ public enum ModuleLayer: String, CaseIterable, ModuleType { .with(.Core), .with(.Data), .external(name: "ReactorKit"), - .external(name: "Lottie") + .external(name: "Lottie"), + .external(name: "Macros") ] case .Data: return [ .with(.Domain), .external(name: "Alamofire"), .external(name: "KakaoSDK"), - .external(name: "RxKakaoSDK") + .external(name: "RxKakaoSDK"), + .external(name: "Macros") ] case .Domain: return [ .external(name: "RxSwift"), - .with(.Core) + .with(.Core), + .external(name: "Macros") ] case .Core: return [ .with(.DesignSystem), - .external(name: "SnapKit"), - .external(name: "Then"), - .external(name: "Kingfisher"), - .external(name: "FSCalendar"), - .external(name: "RxDataSources"), - .external(name: "Lottie") + .external(name: "SnapKit", condition: .when(.all)), + .external(name: "Then", condition: .when(.all)), + .external(name: "Kingfisher", condition: .when(.all)), + .external(name: "FSCalendar", condition: .when(.all)), + .external(name: "RxDataSources", condition: .when(.all)), + .external(name: "Lottie", condition: .when(.all)), + .external(name: "Macros") ] case .DesignSystem: return [] diff --git a/Tuist/ProjectDescriptionHelpers/Project+Templates.swift b/Tuist/ProjectDescriptionHelpers/Project+Templates.swift index 7421e4778..f78e441a1 100644 --- a/Tuist/ProjectDescriptionHelpers/Project+Templates.swift +++ b/Tuist/ProjectDescriptionHelpers/Project+Templates.swift @@ -11,7 +11,7 @@ public struct AppFactory { var products: [ProductsType] var dependencies: [TargetDependency] var bundleId: String - var deploymentTarget: DeploymentTarget? + var deploymentTarget: DeploymentTargets? var infoPlist: InfoPlist? var sources: SourceFilesList? var resources: ResourceFileElements? @@ -26,7 +26,7 @@ public struct AppFactory { products: [ProductsType] = [.app, .uiTests, .unitTests], dependencies: [TargetDependency] = [], bundleId: String, - deploymentTarget: DeploymentTarget? = .defualt, + deploymentTarget: DeploymentTargets? = .defualt, infoPlist: InfoPlist? = .default, sources: SourceFilesList? = .default, resources: ResourceFileElements? = .default, diff --git a/Tuist/ProjectDescriptionHelpers/Scheme+Templates.swift b/Tuist/ProjectDescriptionHelpers/Scheme+Templates.swift index 15fa00504..f549d35a3 100644 --- a/Tuist/ProjectDescriptionHelpers/Scheme+Templates.swift +++ b/Tuist/ProjectDescriptionHelpers/Scheme+Templates.swift @@ -13,27 +13,27 @@ extension Scheme { let buildName = type.rawValue switch type { case .dev: - return Scheme( + return .scheme( name: "\(name)-\(buildName.uppercased())", - buildAction: BuildAction(targets: ["\(name)"]), - runAction: .runAction(configuration: type.configurationName), + buildAction: .buildAction(targets: ["\(name)"]), + runAction: .runAction(configuration: type.configurationName) , archiveAction: .archiveAction(configuration: type.configurationName), profileAction: .profileAction(configuration: type.configurationName), analyzeAction: .analyzeAction(configuration: type.configurationName) ) case .prd: - return Scheme( + return .scheme( name: "\(name)-\(buildName.uppercased())", - buildAction: BuildAction(targets: ["\(name)"]), + buildAction: .buildAction(targets: ["\(name)"]), runAction: .runAction(configuration: type.configurationName), archiveAction: .archiveAction(configuration: type.configurationName), profileAction: .profileAction(configuration: type.configurationName), analyzeAction: .analyzeAction(configuration: type.configurationName) ) case .stg: - return Scheme( + return .scheme( name: "\(name)-\(buildName.uppercased())", - buildAction: BuildAction(targets: ["\(name)"]), + buildAction: .buildAction(targets: ["\(name)"]), runAction: .runAction(configuration: type.configurationName), archiveAction: .archiveAction(configuration: type.configurationName), profileAction: .profileAction(configuration: type.configurationName), From 555f169e7b6ddf79f9b52337f67d3ef54c884155 Mon Sep 17 00:00:00 2001 From: Do-hyun-Kim Date: Sun, 30 Jun 2024 19:25:52 +0900 Subject: [PATCH 155/263] =?UTF-8?q?chore:=20tuist=20install=20=EB=AA=85?= =?UTF-8?q?=EB=A0=B9=EC=96=B4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/swift.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 1a3df593b..e0dd576c8 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -43,7 +43,7 @@ jobs: token: ${{secrets.ACTION_TOKEN}} - name: Install Tuist CLI - run: bash <(curl -Ls https://install.tuist.io) + run: brew install tuist - name: Install FastLane uses: ruby/setup-ruby@v1 @@ -55,7 +55,7 @@ jobs: run: tuist clean - name: Tuist Fetch Command - run: tuist fetch + run: tuist install - name: Tuist Generate Commnad run: tuist generate From c8f17e72c4a4c8358150ee7123ebe78a494d5e08 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Wed, 10 Jul 2024 00:06:04 +0900 Subject: [PATCH 156/263] =?UTF-8?q?fix:=20Core=EB=AA=A8=EB=93=88=20?= =?UTF-8?q?=ED=95=84=EC=9A=94=EC=97=86=EB=8A=94=20=ED=8C=8C=EC=9D=BC=20Tra?= =?UTF-8?q?sh=EB=A1=9C=20=EC=9D=B4=EB=8F=99,=20=EB=B9=8C=EB=93=9C=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EC=88=98=EC=A0=95=20(#592)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .mise.toml | 2 + .../MonthlyCalendarViewController.swift | 7 ++ .../Base/BBNavigationViewController.swift | 0 .../Base/BaseCollectionViewCell.swift | 0 .../{Bibbi => }/Base/BaseContainer.swift | 0 .../{Bibbi => }/Base/BaseDIContainer.swift | 0 .../{Bibbi => }/Base/BaseNavigator.swift | 0 .../Base/BasePageViewController.swift | 0 .../{Bibbi => }/Base/BaseService.swift | 0 .../{Bibbi => }/Base/BaseTableViewCell.swift | 0 .../Sources/{Bibbi => }/Base/BaseView.swift | 0 .../{Bibbi => }/Base/BaseViewController.swift | 0 .../{Bibbi => }/Base/BaseViewModel.swift | 0 .../{Bibbi => }/Base/BaseWrapper.swift | 0 .../{Bibbi => }/Base/BaseWrapperView.swift | 0 .../Base/ReactorViewController.swift | 0 .../Bibbi/BBRx/Repositories/App/App.swift | 19 ---- .../DeepLink/DeepLinkRepository.swift | 24 ----- .../Models/NotificationDeepLink.swift | 20 ---- .../DeepLink/Models/WidgetDeepLink.swift | 16 --- .../BBRx/Repositories/Member/Emojis.swift | 102 ------------------ .../Member/MemberRepository.swift | 86 --------------- .../Repositories/Token/TokenRepository.swift | 63 ----------- .../Core/Sources/Bibbi/BBRx/RxObject.swift | 19 ---- .../Core/Sources/Bibbi/BBRx/RxUtils.swift | 58 ---------- .../Mixpanel/MixPanelService+Account.swift | 26 ----- .../Mixpanel/MixPanelService+Camera.swift | 20 ---- .../Mixpanel/MixPanelService+Family.swift | 19 ---- .../Mixpanel/MixPanelService+Home.swift | 21 ---- .../Analytics/Mixpanel/MixpanelService.swift | 83 -------------- .../Core/Sources/Bibbi/Utilties/Haptic.swift | 30 ------ .../Bibbi/Utilties/Modifiers/Shimmer.swift | 100 ----------------- .../Bibbi/Utilties/Types/Enums/URLTypes.swift | 37 ------- .../Types/Protocols/ItemIndexable.swift | 23 ---- .../{Bibbi => }/DIContainer/Container.swift | 0 .../{Bibbi => }/DIContainer/DIContainer.swift | 0 .../DIContainer/InjectIdentifier.swift | 0 .../Injected+PropertyWrapper.swift | 0 .../InjectedSafe+PropertyWrapper.swift | 0 .../Navigator+PropertyWrapper.swift | 0 .../Extensions/AVCapturePhotoOutput+Ext.swift | 0 .../{Bibbi => }/Extensions/Array+Ext.swift | 0 .../{Bibbi => }/Extensions/Bundle+Ext.swift | 0 .../{Bibbi => }/Extensions/Codable+Ext.swift | 0 .../{Bibbi => }/Extensions/Date+Ext.swift | 0 .../Extensions/DateFormatter+Ext.swift | 0 .../{Bibbi => }/Extensions/Int+Ext.swift | 0 .../Extensions/NSItemProvider+Ext.swift | 0 .../NSMutableAttributedString+Ext.swift | 0 .../Extensions/Notification+Ext.swift | 0 .../{Bibbi => }/Extensions/Reactive+Ext.swift | 0 .../{Bibbi => }/Extensions/String+Ext.swift | 0 .../{Bibbi => }/Extensions/UIButton+Ext.swift | 0 ...UICollectionViewLayoutAttributes+Ext.swift | 0 .../{Bibbi => }/Extensions/UIColor+Ext.swift | 0 .../{Bibbi => }/Extensions/UIFont+Ext.swift | 0 .../{Bibbi => }/Extensions/UIImage+Ext.swift | 0 .../Extensions/UIImageView+Ext.swift | 0 .../UINavigationController+Ext.swift | 0 .../Extensions/UIPageViewController+Ext.swift | 0 .../{Bibbi => }/Extensions/UIScreen+Ext.swift | 0 .../Extensions/UIStackView+Ext.swift | 0 .../Extensions/UITextField+Ext.swift | 0 .../{Bibbi => }/Extensions/UIView+Ext.swift | 0 .../Extensions/UIViewController+Ext.swift | 0 .../Extensions/UIVisualEffectView+Ext.swift | 0 .../{Bibbi => }/Extensions/UIWindow+Ext.swift | 0 .../Extensions/URLRequeset+Ext.swift | 0 .../Extensions/UserDefaults+Ext.swift | 0 .../Trash/BBRx/Repositories/App/App.swift | 2 +- .../DeepLink/DeepLinkRepository.swift | 1 - .../Member/MemberRepository.swift | 1 - .../Repositories/Token/TokenRepository.swift | 2 - .../Core/Sources/Trash/BBRx/RxUtils.swift | 2 - .../Trash/Types/Protocols/ItemIndexable.swift | 23 ---- Bibbi.xcworkspace/contents.xcworkspacedata | 21 ++++ 76 files changed, 31 insertions(+), 796 deletions(-) create mode 100644 .mise.toml rename 14th-team5-iOS/Core/Sources/{Bibbi => }/Base/BBNavigationViewController.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi => }/Base/BaseCollectionViewCell.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi => }/Base/BaseContainer.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi => }/Base/BaseDIContainer.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi => }/Base/BaseNavigator.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi => }/Base/BasePageViewController.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi => }/Base/BaseService.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi => }/Base/BaseTableViewCell.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi => }/Base/BaseView.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi => }/Base/BaseViewController.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi => }/Base/BaseViewModel.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi => }/Base/BaseWrapper.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi => }/Base/BaseWrapperView.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi => }/Base/ReactorViewController.swift (100%) delete mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBRx/Repositories/App/App.swift delete mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBRx/Repositories/DeepLink/DeepLinkRepository.swift delete mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBRx/Repositories/DeepLink/Models/NotificationDeepLink.swift delete mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBRx/Repositories/DeepLink/Models/WidgetDeepLink.swift delete mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBRx/Repositories/Member/Emojis.swift delete mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBRx/Repositories/Member/MemberRepository.swift delete mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBRx/Repositories/Token/TokenRepository.swift delete mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBRx/RxObject.swift delete mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBRx/RxUtils.swift delete mode 100644 14th-team5-iOS/Core/Sources/Bibbi/Utilties/Analytics/Mixpanel/MixPanelService+Account.swift delete mode 100644 14th-team5-iOS/Core/Sources/Bibbi/Utilties/Analytics/Mixpanel/MixPanelService+Camera.swift delete mode 100644 14th-team5-iOS/Core/Sources/Bibbi/Utilties/Analytics/Mixpanel/MixPanelService+Family.swift delete mode 100644 14th-team5-iOS/Core/Sources/Bibbi/Utilties/Analytics/Mixpanel/MixPanelService+Home.swift delete mode 100644 14th-team5-iOS/Core/Sources/Bibbi/Utilties/Analytics/Mixpanel/MixpanelService.swift delete mode 100644 14th-team5-iOS/Core/Sources/Bibbi/Utilties/Haptic.swift delete mode 100644 14th-team5-iOS/Core/Sources/Bibbi/Utilties/Modifiers/Shimmer.swift delete mode 100644 14th-team5-iOS/Core/Sources/Bibbi/Utilties/Types/Enums/URLTypes.swift delete mode 100644 14th-team5-iOS/Core/Sources/Bibbi/Utilties/Types/Protocols/ItemIndexable.swift rename 14th-team5-iOS/Core/Sources/{Bibbi => }/DIContainer/Container.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi => }/DIContainer/DIContainer.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi => }/DIContainer/InjectIdentifier.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi => }/DIContainer/Injected+PropertyWrapper.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi => }/DIContainer/InjectedSafe+PropertyWrapper.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi => }/DIContainer/Navigator+PropertyWrapper.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi => }/Extensions/AVCapturePhotoOutput+Ext.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi => }/Extensions/Array+Ext.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi => }/Extensions/Bundle+Ext.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi => }/Extensions/Codable+Ext.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi => }/Extensions/Date+Ext.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi => }/Extensions/DateFormatter+Ext.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi => }/Extensions/Int+Ext.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi => }/Extensions/NSItemProvider+Ext.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi => }/Extensions/NSMutableAttributedString+Ext.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi => }/Extensions/Notification+Ext.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi => }/Extensions/Reactive+Ext.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi => }/Extensions/String+Ext.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi => }/Extensions/UIButton+Ext.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi => }/Extensions/UICollectionViewLayoutAttributes+Ext.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi => }/Extensions/UIColor+Ext.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi => }/Extensions/UIFont+Ext.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi => }/Extensions/UIImage+Ext.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi => }/Extensions/UIImageView+Ext.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi => }/Extensions/UINavigationController+Ext.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi => }/Extensions/UIPageViewController+Ext.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi => }/Extensions/UIScreen+Ext.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi => }/Extensions/UIStackView+Ext.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi => }/Extensions/UITextField+Ext.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi => }/Extensions/UIView+Ext.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi => }/Extensions/UIViewController+Ext.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi => }/Extensions/UIVisualEffectView+Ext.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi => }/Extensions/UIWindow+Ext.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi => }/Extensions/URLRequeset+Ext.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi => }/Extensions/UserDefaults+Ext.swift (100%) delete mode 100644 14th-team5-iOS/Core/Sources/Trash/Types/Protocols/ItemIndexable.swift diff --git a/.mise.toml b/.mise.toml new file mode 100644 index 000000000..b5e4598cb --- /dev/null +++ b/.mise.toml @@ -0,0 +1,2 @@ +[tools] +tuist = "4.3.4" diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/MonthlyCalendarViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/MonthlyCalendarViewController.swift index f87b35029..65308f6ce 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/MonthlyCalendarViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/MonthlyCalendarViewController.swift @@ -16,6 +16,13 @@ import RxDataSources import SnapKit import Then +import MacrosInterface + +@Codable +struct MemberDTO { + var name: String +} + fileprivate typealias _Str = CalendarStrings public final class MonthlyCalendarViewController: BaseViewController { // MARK: - Views diff --git a/14th-team5-iOS/Core/Sources/Bibbi/Base/BBNavigationViewController.swift b/14th-team5-iOS/Core/Sources/Base/BBNavigationViewController.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/Base/BBNavigationViewController.swift rename to 14th-team5-iOS/Core/Sources/Base/BBNavigationViewController.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/Base/BaseCollectionViewCell.swift b/14th-team5-iOS/Core/Sources/Base/BaseCollectionViewCell.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/Base/BaseCollectionViewCell.swift rename to 14th-team5-iOS/Core/Sources/Base/BaseCollectionViewCell.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/Base/BaseContainer.swift b/14th-team5-iOS/Core/Sources/Base/BaseContainer.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/Base/BaseContainer.swift rename to 14th-team5-iOS/Core/Sources/Base/BaseContainer.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/Base/BaseDIContainer.swift b/14th-team5-iOS/Core/Sources/Base/BaseDIContainer.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/Base/BaseDIContainer.swift rename to 14th-team5-iOS/Core/Sources/Base/BaseDIContainer.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/Base/BaseNavigator.swift b/14th-team5-iOS/Core/Sources/Base/BaseNavigator.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/Base/BaseNavigator.swift rename to 14th-team5-iOS/Core/Sources/Base/BaseNavigator.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/Base/BasePageViewController.swift b/14th-team5-iOS/Core/Sources/Base/BasePageViewController.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/Base/BasePageViewController.swift rename to 14th-team5-iOS/Core/Sources/Base/BasePageViewController.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/Base/BaseService.swift b/14th-team5-iOS/Core/Sources/Base/BaseService.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/Base/BaseService.swift rename to 14th-team5-iOS/Core/Sources/Base/BaseService.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/Base/BaseTableViewCell.swift b/14th-team5-iOS/Core/Sources/Base/BaseTableViewCell.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/Base/BaseTableViewCell.swift rename to 14th-team5-iOS/Core/Sources/Base/BaseTableViewCell.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/Base/BaseView.swift b/14th-team5-iOS/Core/Sources/Base/BaseView.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/Base/BaseView.swift rename to 14th-team5-iOS/Core/Sources/Base/BaseView.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/Base/BaseViewController.swift b/14th-team5-iOS/Core/Sources/Base/BaseViewController.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/Base/BaseViewController.swift rename to 14th-team5-iOS/Core/Sources/Base/BaseViewController.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/Base/BaseViewModel.swift b/14th-team5-iOS/Core/Sources/Base/BaseViewModel.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/Base/BaseViewModel.swift rename to 14th-team5-iOS/Core/Sources/Base/BaseViewModel.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/Base/BaseWrapper.swift b/14th-team5-iOS/Core/Sources/Base/BaseWrapper.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/Base/BaseWrapper.swift rename to 14th-team5-iOS/Core/Sources/Base/BaseWrapper.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/Base/BaseWrapperView.swift b/14th-team5-iOS/Core/Sources/Base/BaseWrapperView.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/Base/BaseWrapperView.swift rename to 14th-team5-iOS/Core/Sources/Base/BaseWrapperView.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/Base/ReactorViewController.swift b/14th-team5-iOS/Core/Sources/Base/ReactorViewController.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/Base/ReactorViewController.swift rename to 14th-team5-iOS/Core/Sources/Base/ReactorViewController.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBRx/Repositories/App/App.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBRx/Repositories/App/App.swift deleted file mode 100644 index 7852310f9..000000000 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBRx/Repositories/App/App.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// App.swift -// Core -// -// Created by geonhui Yu on 12/14/23. -// - -import Foundation - -public enum App { - - public static let indicator = BibbiLoadIndicator() - - public enum Repository { - public static let token = TokenRepository() - public static let member = MemberRepository() - public static let deepLink = DeepLinkRepository() - } -} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBRx/Repositories/DeepLink/DeepLinkRepository.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBRx/Repositories/DeepLink/DeepLinkRepository.swift deleted file mode 100644 index d2b1e185b..000000000 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBRx/Repositories/DeepLink/DeepLinkRepository.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// DeepLinkRepository.swift -// Core -// -// Created by 김건우 on 3/13/24. -// - -import Foundation - -import RxSwift -import RxCocoa - -public class DeepLinkRepository: RxObject { - public let notification = BehaviorRelay(value: nil) - public let widget = BehaviorRelay(value: nil) - - public override func bind() { } - - public override func unbind() { - super.unbind() - } -} - - diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBRx/Repositories/DeepLink/Models/NotificationDeepLink.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBRx/Repositories/DeepLink/Models/NotificationDeepLink.swift deleted file mode 100644 index b66f2df57..000000000 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBRx/Repositories/DeepLink/Models/NotificationDeepLink.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// NotificationDeepLinkInfo.swift -// Core -// -// Created by 김건우 on 3/13/24. -// - -import Foundation - -public struct NotificationDeepLink { - public let postId: String - public let openComment: Bool - public let dateOfPost: Date - - public init(postId: String, openComment: Bool, dateOfPost: Date) { - self.postId = postId - self.openComment = openComment - self.dateOfPost = dateOfPost - } -} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBRx/Repositories/DeepLink/Models/WidgetDeepLink.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBRx/Repositories/DeepLink/Models/WidgetDeepLink.swift deleted file mode 100644 index d99e1fc0b..000000000 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBRx/Repositories/DeepLink/Models/WidgetDeepLink.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// WidgetDeepLink.swift -// Core -// -// Created by 김건우 on 3/15/24. -// - -import Foundation - -public struct WidgetDeepLink { - public let postId: String - - public init(postId: String) { - self.postId = postId - } -} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBRx/Repositories/Member/Emojis.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBRx/Repositories/Member/Emojis.swift deleted file mode 100644 index b22e7186f..000000000 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBRx/Repositories/Member/Emojis.swift +++ /dev/null @@ -1,102 +0,0 @@ -// -// Emojis.swift -// App -// -// Created by 마경미 on 14.12.23. -// - -import UIKit -import DesignSystem - -public enum Emojis: Int { - case emoji1 = 1 - case emoji2 = 2 - case emoji3 = 3 - case emoji4 = 4 - case emoji5 = 5 - - public static func getEmojiImage(index: Int) -> UIImage { - switch index { - case 1: - return DesignSystemAsset.emoji1.image - case 2: - return DesignSystemAsset.emoji2.image - case 3: - return DesignSystemAsset.emoji3.image - case 4: - return DesignSystemAsset.emoji4.image - case 5: - return DesignSystemAsset.emoji5.image - default: - return DesignSystemAsset.emoji1.image - } - } - - public var emojiImage: UIImage { - switch self { - case .emoji1: - return DesignSystemAsset.emoji1.image - case .emoji2: - return DesignSystemAsset.emoji2.image - case .emoji3: - return DesignSystemAsset.emoji3.image - case .emoji4: - return DesignSystemAsset.emoji4.image - case .emoji5: - return DesignSystemAsset.emoji5.image - } - } - - public var emojiBadgeImage: UIImage { - switch self { - case .emoji1: - return DesignSystemAsset.emojipoint1.image - case .emoji2: - return DesignSystemAsset.emojipoint2.image - case .emoji3: - return DesignSystemAsset.emojipoint3.image - case .emoji4: - return DesignSystemAsset.emojipoint4.image - case .emoji5: - return DesignSystemAsset.emojipoint5.image - } - } - - public var emojiString: String { - switch self { - case .emoji1: - return "EMOJI_1" - case .emoji2: - return "EMOJI_2" - case .emoji3: - return "EMOJI_3" - case .emoji4: - return "EMOJI_4" - case .emoji5: - return "EMOJI_5" - } - } - - public var emojiIndex: Int { - return self.rawValue - } - - public static var allEmojis: [Emojis] { - return [.emoji1, .emoji2, .emoji3, .emoji4, .emoji5] - } - - public static func emoji(forIndex index: Int) -> Emojis { - return Emojis(rawValue: index) ?? .emoji1 - } - - public static func emoji(forString name: String) -> Emojis { - switch name { - case "EMOJI_1", "emoji_1": return .emoji1 - case "EMOJI_2", "emoji_2": return .emoji2 - case "EMOJI_3", "emoji_3": return .emoji3 - case "EMOJI_4", "emoji_4": return .emoji4 - case "EMOJI_5", "emoji_5": return .emoji5 - default: return .emoji1 - } - } -} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBRx/Repositories/Member/MemberRepository.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBRx/Repositories/Member/MemberRepository.swift deleted file mode 100644 index b52e7d4ae..000000000 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBRx/Repositories/Member/MemberRepository.swift +++ /dev/null @@ -1,86 +0,0 @@ -// -// MemberRepository.swift -// Core -// -// Created by geonhui Yu on 1/3/24. -// - -import Foundation - -import RxSwift -import RxCocoa - -public class MemberRepository: RxObject { - public let familyId = BehaviorRelay(value: nil) - public let memberID = BehaviorRelay(value: nil) - public let postId = BehaviorRelay(value: nil) - public let inviteCode = BehaviorRelay(value: nil) - public let nickname = BehaviorRelay(value: nil) - public let familyCreatedAt = BehaviorRelay(value: nil) - - override public func bind() { - memberID - .withUnretained(self) - .bind(onNext: { $0.0.saveMemberId(with: $0.1) }) - .disposed(by: disposeBag) - - familyId - .withUnretained(self) - .bind(onNext: { $0.0.saveFamilyId(with: $0.1) }) - .disposed(by: disposeBag) - - nickname - .withUnretained(self) - .bind(onNext: { $0.0.saveNicknmae(with: $0.1) }) - .disposed(by: disposeBag) - - inviteCode - .withUnretained(self) - .bind(onNext: { $0.0.saveInviteCode(with: $0.1) }) - .disposed(by: disposeBag) - - postId - .withUnretained(self) - .bind(onNext: { $0.0.savePostId(with: $0.1) }) - .disposed(by: disposeBag) - - familyCreatedAt - .withUnretained(self) - .bind(onNext: { $0.0.saveFamilyCreatedAt(with: $0.1) }) - .disposed(by: disposeBag) - } - - private func saveMemberId(with id: String?) { - guard let memberId = id else { return } - UserDefaults.standard.memberId = memberId - } - - private func saveFamilyId(with id: String?) { - guard let familyId = id else { return } - UserDefaults.standard.familyId = familyId - } - - private func saveInviteCode(with code: String?) { - guard let code = code else { return } - UserDefaults.standard.inviteCode = code - } - - private func saveNicknmae(with nickname: String?) { - guard let nickname = nickname else { return } - UserDefaults.standard.nickname = nickname - } - - private func savePostId(with postId: String?) { - guard let postId = postId else { return } - UserDefaults.standard.postId = postId - } - - private func saveFamilyCreatedAt(with date: Date?) { - guard let date = date else { return } - UserDefaults.standard.createdAt = date - } - - override public func unbind() { - super.unbind() - } -} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBRx/Repositories/Token/TokenRepository.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBRx/Repositories/Token/TokenRepository.swift deleted file mode 100644 index 41116db69..000000000 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBRx/Repositories/Token/TokenRepository.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// TokenRepository.swift -// Core -// -// Created by geonhui Yu on 12/14/23. -// - -import Foundation - -import RxCocoa -import RxSwift - -public struct AccessToken: Codable, Equatable { - public var accessToken: String? - public var refreshToken: String? - public var isTemporaryToken: Bool? - - public init(accessToken: String?, refreshToken: String?, isTemporaryToken: Bool?) { - self.accessToken = accessToken - self.refreshToken = refreshToken - self.isTemporaryToken = isTemporaryToken - } -} - -public class TokenRepository: RxObject { - public lazy var keychain = KeychainWrapper(serviceName: "Bibbi", accessGroup: "P9P4WJ623F.com.5ing.bibbi") - - public let accessToken = BehaviorRelay(value: (KeychainWrapper.standard[.accessToken] as String?)?.decode(AccessToken.self)) - - public func clearAccessToken() { - KeychainWrapper.standard.remove(forKey: .accessToken) - accessToken.accept(nil) - } - - override public func bind() { - super.bind() - accessToken - .distinctUntilChanged() - .subscribe(on: RxSchedulers.io) - .withUnretained(self) - .subscribe { - guard let jsonData = try? JSONEncoder().encode($0.1), - let jsonStr = String(data: jsonData, encoding: .utf8) else { - KeychainWrapper.standard.remove(forKey: .accessToken) - return - } - - if let jsonData = jsonStr.data(using: .utf8), - let decodedUser = try? JSONDecoder().decode(AccessToken.self, from: jsonData) { - self.keychain.set(decodedUser.accessToken ?? "fail", forKey: "accessToken") - } else { - print("Failed to decode jsonStr or jsonStr is nil") - } - - KeychainWrapper.standard[.accessToken] = jsonStr - } - .disposed(by: disposeBag) - } - - override public func unbind() { - super.unbind() - } -} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBRx/RxObject.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBRx/RxObject.swift deleted file mode 100644 index a2e52f164..000000000 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBRx/RxObject.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// RxObject.swift -// Core -// -// Created by geonhui Yu on 12/14/23. -// - -import Foundation - -import RxSwift - -public class RxObject: NSObject { - private(set) var disposeBag = DisposeBag() - - func bind() {} - func unbind() { - disposeBag = DisposeBag() - } -} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBRx/RxUtils.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBRx/RxUtils.swift deleted file mode 100644 index 5fed12199..000000000 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBRx/RxUtils.swift +++ /dev/null @@ -1,58 +0,0 @@ -// -// RxUtils.swift -// Core -// -// Created by geonhui Yu on 12/14/23. -// - -import Foundation - -import RxSwift - -public enum RxConst { - static public var milliseconds100Interval: RxTimeInterval { - return .milliseconds(100) - } - - static public var milliseconds300Interval: RxTimeInterval { - return .milliseconds(300) - } -} - -public enum RxSchedulers { - public static let main = { - MainScheduler.instance - }() - - public static let background = { - ConcurrentDispatchQueueScheduler(queue: DispatchQueue(label: "background", qos: .background)) - }() - - public static let utility = { - ConcurrentDispatchQueueScheduler(queue: DispatchQueue(label: "utility", qos: .utility)) - }() - - public static let io = { - ConcurrentDispatchQueueScheduler(queue: DispatchQueue(label: "io", qos: .default)) - }() - - public static let `default` = { - ConcurrentDispatchQueueScheduler(queue: DispatchQueue(label: "default", qos: .default)) - }() - - public static let computation = { - ConcurrentDispatchQueueScheduler(queue: DispatchQueue(label: "computation", qos: .userInitiated)) - }() - - public static let userInitiated = { - ConcurrentDispatchQueueScheduler(queue: DispatchQueue(label: "userInitiated", qos: .userInitiated)) - }() - - public static let immediate = { - ConcurrentDispatchQueueScheduler(queue: DispatchQueue(label: "immediate", qos: .userInteractive)) - }() - - public static let userInteractive = { - ConcurrentDispatchQueueScheduler(queue: DispatchQueue(label: "userInteractive", qos: .userInteractive)) - }() -} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/Utilties/Analytics/Mixpanel/MixPanelService+Account.swift b/14th-team5-iOS/Core/Sources/Bibbi/Utilties/Analytics/Mixpanel/MixPanelService+Account.swift deleted file mode 100644 index 384206b19..000000000 --- a/14th-team5-iOS/Core/Sources/Bibbi/Utilties/Analytics/Mixpanel/MixPanelService+Account.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// MixPanelService+Account.swift -// Core -// -// Created by geonhui Yu on 1/21/24. -// - -import Foundation - -extension MPEvent { - public enum Account: String, MixpanelTrackable { - case viewLogin = "View_Login" - case allowNotification = "Click_Allow_Notification" - case creatGroup = "Click_Create_NewGroup_1" - case creatGroupFinished = "Click_Create_NewGroup_2" - case invitedGroup = "Click_Enter_Group_1" - case invitedGroupFinished = "Click_Enter_Group_2" - case leaveGroup = "Click_LeaveGroup" - case withdrawl = "Click_Withdrawal" - - public func track(with properties: Encodable?) { - MP.mainInstance().track(event: self.rawValue, properties: properties?.asMixpanelDictionary()) - debugPrint("[Bibbi - Mixpanel - Account - \(self.rawValue)") - } - } -} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/Utilties/Analytics/Mixpanel/MixPanelService+Camera.swift b/14th-team5-iOS/Core/Sources/Bibbi/Utilties/Analytics/Mixpanel/MixPanelService+Camera.swift deleted file mode 100644 index 92afb67f8..000000000 --- a/14th-team5-iOS/Core/Sources/Bibbi/Utilties/Analytics/Mixpanel/MixPanelService+Camera.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// MixPanelService+Camera.swift -// Core -// -// Created by geonhui Yu on 1/21/24. -// - -import Foundation - -extension MPEvent { - public enum Camera: String, MixpanelTrackable { - case photoText = "Click_PhotoText" - case uploadPhoto = "Click_UploadPhoto" - - public func track(with properties: Encodable?) { - MP.mainInstance().track(event: self.rawValue, properties: properties?.asMixpanelDictionary()) - debugPrint("[Bibbi - Mixpanel - Camera - \(self.rawValue)") - } - } -} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/Utilties/Analytics/Mixpanel/MixPanelService+Family.swift b/14th-team5-iOS/Core/Sources/Bibbi/Utilties/Analytics/Mixpanel/MixPanelService+Family.swift deleted file mode 100644 index ce3e86b71..000000000 --- a/14th-team5-iOS/Core/Sources/Bibbi/Utilties/Analytics/Mixpanel/MixPanelService+Family.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// MixPanelService+Family.swift -// Core -// -// Created by geonhui Yu on 1/21/24. -// - -import Foundation - -extension MPEvent { - public enum Family: String, MixpanelTrackable { - case shareLink = "Click_ShareLink_Family" - - public func track(with properties: Encodable?) { - MP.mainInstance().track(event: self.rawValue, properties: properties?.asMixpanelDictionary()) - debugPrint("[Bibbi - Mixpanel - Family - \(self.rawValue)") - } - } -} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/Utilties/Analytics/Mixpanel/MixPanelService+Home.swift b/14th-team5-iOS/Core/Sources/Bibbi/Utilties/Analytics/Mixpanel/MixPanelService+Home.swift deleted file mode 100644 index 3c8111c8a..000000000 --- a/14th-team5-iOS/Core/Sources/Bibbi/Utilties/Analytics/Mixpanel/MixPanelService+Home.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// MixPanelService+Home.swift -// Core -// -// Created by geonhui Yu on 1/20/24. -// - -import Foundation -import Mixpanel - -extension MPEvent { - public enum Home: String, MixpanelTrackable { - case shareLink = "Click_ShareLink_Home" - case cameraTapped = "Click_btn_Camera" - - public func track(with properties: Encodable?) { - MP.mainInstance().track(event: self.rawValue, properties: properties?.asMixpanelDictionary()) - debugPrint("[Bibbi - Mixpanel - Home - \(self.rawValue)") - } - } -} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/Utilties/Analytics/Mixpanel/MixpanelService.swift b/14th-team5-iOS/Core/Sources/Bibbi/Utilties/Analytics/Mixpanel/MixpanelService.swift deleted file mode 100644 index a28abb49c..000000000 --- a/14th-team5-iOS/Core/Sources/Bibbi/Utilties/Analytics/Mixpanel/MixpanelService.swift +++ /dev/null @@ -1,83 +0,0 @@ -// -// MixpanelService.swift -// Core -// -// Created by geonhui Yu on 1/20/24. -// - -import Foundation -import Mixpanel - -// MARK: Mixpanel Protocol -protocol MixpanelTrackable { - func track(with properties: Encodable?) -} - -public typealias MP = Mixpanel -public typealias MPEvent = MixpanelService.Event -public enum MixpanelService { - static var distinctId: String { - return MP.mainInstance().distinctId - } - public enum Event { - // 추후 계정정보 관련 필요한 경우 추가예정 - static func setPeople(with token: AccessToken?) { - guard let t = token else { - if token?.accessToken == nil { - return - } - - if MP.mainInstance().userId == nil { - MP.mainInstance().reset() - } - debugPrint("[Bibbi - Mixpanel - reset") - return - } - } - } -} - -extension MPEvent {} - -// MARK: Mixpanel Properties -extension MPEvent { - enum Property { - struct People: Codable { - var nickname: String? - enum CodingKeys: String, CodingKey { - case nickname = "$nickname" - } - } - - enum Account { - struct SignedIn: Codable { - var type: String? - } - - struct SignedUp: Codable { - var type: String? - } - } - } -} - -// MARK: Extensions -//extension Encodable { -// func asDictionary() -> [String: Any]? { -// do { -// let data = try JSONEncoder().encode(self) -// guard let dict = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any] else { return nil } -// -// return dict -// } catch let error { -// print(error) -// return nil -// } -// } -//} - -extension Encodable { - func asMixpanelDictionary() -> [String: MixpanelType]? { - return self.asDictionary() as? [String: MixpanelType] - } -} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/Utilties/Haptic.swift b/14th-team5-iOS/Core/Sources/Bibbi/Utilties/Haptic.swift deleted file mode 100644 index 6e46268c7..000000000 --- a/14th-team5-iOS/Core/Sources/Bibbi/Utilties/Haptic.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// Haptic.swift -// Core -// -// Created by 김건우 on 1/27/24. -// - - -import Foundation -import UIKit - -public final class Haptic { - // MARK: - Notification - public static func notification(type: UINotificationFeedbackGenerator.FeedbackType) { - let generator = UINotificationFeedbackGenerator() - generator.notificationOccurred(type) - } - - // MARK: - Impact - public static func impact(style: UIImpactFeedbackGenerator.FeedbackStyle) { - let generator = UIImpactFeedbackGenerator(style: style) - generator.impactOccurred() - } - - // MARK: - Selection - public static func selection() { - let generator = UISelectionFeedbackGenerator() - generator.selectionChanged() - } -} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/Utilties/Modifiers/Shimmer.swift b/14th-team5-iOS/Core/Sources/Bibbi/Utilties/Modifiers/Shimmer.swift deleted file mode 100644 index fd54a15ee..000000000 --- a/14th-team5-iOS/Core/Sources/Bibbi/Utilties/Modifiers/Shimmer.swift +++ /dev/null @@ -1,100 +0,0 @@ -// -// Shimmer.swift -// Core -// -// Created by 김건우 on 5/20/24. -// - -import Foundation -import SwiftUI - -public struct Shimmer: ViewModifier { - private let animation: Animation - private let gradient: Gradient - private let min, max: CGFloat - @State private var isIntialState = true - @Environment(\.layoutDirection) private var layoutDirection - - init( - animation: Animation = Self.defaultAnimation, - gradient: Gradient = Self.defaultGradient, - bandSize: CGFloat = 0.3 - ) { - self.animation = animation - self.gradient = gradient - self.min = 0 - bandSize - self.max = 1 + bandSize - } - - public static let defaultAnimation = Animation.linear(duration: 1.5).delay(0.25).repeatForever(autoreverses: false) - - public static let defaultGradient = Gradient(colors: [ - Color.black.opacity(0.3), - Color.black, - Color.black.opacity(0.3) - ]) - - /* - Calculating the gradient's animated start and end unit points: - min,min - \ - ┌───────┐ ┌───────┐ - │0,0 │ Animate │ │ "forward" gradient - LTR │ │ ───────►│ 1,1│ / // / - └───────┘ └───────┘ - \ - max,max - max,min - / - ┌───────┐ ┌───────┐ - │ 1,0│ Animate │ │ "backward" gradient - RTL │ │ ───────►│0,1 │ \ \\ \ - └───────┘ └───────┘ - / - min,max - */ - - var startPoint: UnitPoint { - if layoutDirection == .rightToLeft { - return isIntialState ? UnitPoint(x: max, y: min) : UnitPoint(x: 0, y: 1) - } else { - return isIntialState ? UnitPoint(x: min, y: min) : UnitPoint(x: 1, y: 1) - } - } - - var endPoint: UnitPoint { - if layoutDirection == .rightToLeft { - return isIntialState ? UnitPoint(x: 1, y: 0) : UnitPoint(x: min, y: max) - } else { - return isIntialState ? UnitPoint(x: 0, y: 0) : UnitPoint(x: max, y: max) - } - } - - public func body(content: Content) -> some View { - content - .mask(LinearGradient(gradient: gradient, startPoint: startPoint, endPoint: endPoint)) - .animation(animation, value: isIntialState) - .onAppear { - DispatchQueue.main.asyncAfter(deadline: .now()) { - isIntialState = false - } - } - } -} - -// MARK: - Extensions -public extension View { - @ViewBuilder - func shimmering( - active: Bool = true, - animation: Animation = Shimmer.defaultAnimation, - gradient: Gradient = Shimmer.defaultGradient, - bandSize: CGFloat = 0.3 - ) -> some View { - if active { - modifier(Shimmer(animation: animation, gradient: gradient, bandSize: bandSize)) - } else { - self - } - } -} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/Utilties/Types/Enums/URLTypes.swift b/14th-team5-iOS/Core/Sources/Bibbi/Utilties/Types/Enums/URLTypes.swift deleted file mode 100644 index a8a34dbdc..000000000 --- a/14th-team5-iOS/Core/Sources/Bibbi/Utilties/Types/Enums/URLTypes.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// URLTypes.swift -// Core -// -// Created by Kim dohyun on 1/1/24. -// - -import UIKit - -public enum BibbiFeedType: Int { - case survival = 0 - case mission = 1 -} - - -public enum URLTypes { - case settings - case appStore - case privacy - case terms - case inquiry - - public var originURL: URL { - switch self { - case .settings: - return URL(string: UIApplication.openSettingsURLString) ?? URL(fileURLWithPath: "") - case .appStore: - return URL(string: "itms-apps://itunes.apple.com/app/6475082088") ?? URL(fileURLWithPath: "") - case .privacy: - return URL(string: "https://no5ing.kr/app/privacy") ?? URL(fileURLWithPath: "") - case .terms: - return URL(string: "https://no5ing.kr/app/terms") ?? URL(fileURLWithPath: "") - case .inquiry: - return URL(string: "https://forms.gle/VUsWgzNfR3ELGozP7") ?? URL(fileURLWithPath: "") - } - } -} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/Utilties/Types/Protocols/ItemIndexable.swift b/14th-team5-iOS/Core/Sources/Bibbi/Utilties/Types/Protocols/ItemIndexable.swift deleted file mode 100644 index daefda522..000000000 --- a/14th-team5-iOS/Core/Sources/Bibbi/Utilties/Types/Protocols/ItemIndexable.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// ItemIndexable.swift -// Core -// -// Created by 마경미 on 20.01.24. -// - -import UIKit -import RxDataSources - -public protocol ItemIndexable { - associatedtype Item - - subscript(indexPath: IndexPath) -> Item { get set } -} - -extension ItemIndexable { - public func item(at index: IndexPath) throws -> Item { self[index] } - public func items(at indexes: [IndexPath]) throws -> [Item] { try indexes.map(self.item(at:)) } -} - -extension TableViewSectionedDataSource: ItemIndexable { } -extension CollectionViewSectionedDataSource: ItemIndexable { } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/DIContainer/Container.swift b/14th-team5-iOS/Core/Sources/DIContainer/Container.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/DIContainer/Container.swift rename to 14th-team5-iOS/Core/Sources/DIContainer/Container.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/DIContainer/DIContainer.swift b/14th-team5-iOS/Core/Sources/DIContainer/DIContainer.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/DIContainer/DIContainer.swift rename to 14th-team5-iOS/Core/Sources/DIContainer/DIContainer.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/DIContainer/InjectIdentifier.swift b/14th-team5-iOS/Core/Sources/DIContainer/InjectIdentifier.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/DIContainer/InjectIdentifier.swift rename to 14th-team5-iOS/Core/Sources/DIContainer/InjectIdentifier.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/DIContainer/Injected+PropertyWrapper.swift b/14th-team5-iOS/Core/Sources/DIContainer/Injected+PropertyWrapper.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/DIContainer/Injected+PropertyWrapper.swift rename to 14th-team5-iOS/Core/Sources/DIContainer/Injected+PropertyWrapper.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/DIContainer/InjectedSafe+PropertyWrapper.swift b/14th-team5-iOS/Core/Sources/DIContainer/InjectedSafe+PropertyWrapper.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/DIContainer/InjectedSafe+PropertyWrapper.swift rename to 14th-team5-iOS/Core/Sources/DIContainer/InjectedSafe+PropertyWrapper.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/DIContainer/Navigator+PropertyWrapper.swift b/14th-team5-iOS/Core/Sources/DIContainer/Navigator+PropertyWrapper.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/DIContainer/Navigator+PropertyWrapper.swift rename to 14th-team5-iOS/Core/Sources/DIContainer/Navigator+PropertyWrapper.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/Extensions/AVCapturePhotoOutput+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/AVCapturePhotoOutput+Ext.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/Extensions/AVCapturePhotoOutput+Ext.swift rename to 14th-team5-iOS/Core/Sources/Extensions/AVCapturePhotoOutput+Ext.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/Extensions/Array+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/Array+Ext.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/Extensions/Array+Ext.swift rename to 14th-team5-iOS/Core/Sources/Extensions/Array+Ext.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/Extensions/Bundle+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/Bundle+Ext.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/Extensions/Bundle+Ext.swift rename to 14th-team5-iOS/Core/Sources/Extensions/Bundle+Ext.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/Extensions/Codable+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/Codable+Ext.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/Extensions/Codable+Ext.swift rename to 14th-team5-iOS/Core/Sources/Extensions/Codable+Ext.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/Extensions/Date+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/Date+Ext.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/Extensions/Date+Ext.swift rename to 14th-team5-iOS/Core/Sources/Extensions/Date+Ext.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/Extensions/DateFormatter+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/DateFormatter+Ext.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/Extensions/DateFormatter+Ext.swift rename to 14th-team5-iOS/Core/Sources/Extensions/DateFormatter+Ext.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/Extensions/Int+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/Int+Ext.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/Extensions/Int+Ext.swift rename to 14th-team5-iOS/Core/Sources/Extensions/Int+Ext.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/Extensions/NSItemProvider+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/NSItemProvider+Ext.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/Extensions/NSItemProvider+Ext.swift rename to 14th-team5-iOS/Core/Sources/Extensions/NSItemProvider+Ext.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/Extensions/NSMutableAttributedString+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/NSMutableAttributedString+Ext.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/Extensions/NSMutableAttributedString+Ext.swift rename to 14th-team5-iOS/Core/Sources/Extensions/NSMutableAttributedString+Ext.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/Extensions/Notification+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/Notification+Ext.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/Extensions/Notification+Ext.swift rename to 14th-team5-iOS/Core/Sources/Extensions/Notification+Ext.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/Extensions/Reactive+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/Reactive+Ext.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/Extensions/Reactive+Ext.swift rename to 14th-team5-iOS/Core/Sources/Extensions/Reactive+Ext.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/Extensions/String+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/String+Ext.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/Extensions/String+Ext.swift rename to 14th-team5-iOS/Core/Sources/Extensions/String+Ext.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/Extensions/UIButton+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/UIButton+Ext.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/Extensions/UIButton+Ext.swift rename to 14th-team5-iOS/Core/Sources/Extensions/UIButton+Ext.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/Extensions/UICollectionViewLayoutAttributes+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/UICollectionViewLayoutAttributes+Ext.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/Extensions/UICollectionViewLayoutAttributes+Ext.swift rename to 14th-team5-iOS/Core/Sources/Extensions/UICollectionViewLayoutAttributes+Ext.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/Extensions/UIColor+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/UIColor+Ext.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/Extensions/UIColor+Ext.swift rename to 14th-team5-iOS/Core/Sources/Extensions/UIColor+Ext.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/Extensions/UIFont+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/UIFont+Ext.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/Extensions/UIFont+Ext.swift rename to 14th-team5-iOS/Core/Sources/Extensions/UIFont+Ext.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/Extensions/UIImage+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/UIImage+Ext.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/Extensions/UIImage+Ext.swift rename to 14th-team5-iOS/Core/Sources/Extensions/UIImage+Ext.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/Extensions/UIImageView+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/UIImageView+Ext.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/Extensions/UIImageView+Ext.swift rename to 14th-team5-iOS/Core/Sources/Extensions/UIImageView+Ext.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/Extensions/UINavigationController+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/UINavigationController+Ext.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/Extensions/UINavigationController+Ext.swift rename to 14th-team5-iOS/Core/Sources/Extensions/UINavigationController+Ext.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/Extensions/UIPageViewController+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/UIPageViewController+Ext.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/Extensions/UIPageViewController+Ext.swift rename to 14th-team5-iOS/Core/Sources/Extensions/UIPageViewController+Ext.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/Extensions/UIScreen+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/UIScreen+Ext.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/Extensions/UIScreen+Ext.swift rename to 14th-team5-iOS/Core/Sources/Extensions/UIScreen+Ext.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/Extensions/UIStackView+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/UIStackView+Ext.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/Extensions/UIStackView+Ext.swift rename to 14th-team5-iOS/Core/Sources/Extensions/UIStackView+Ext.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/Extensions/UITextField+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/UITextField+Ext.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/Extensions/UITextField+Ext.swift rename to 14th-team5-iOS/Core/Sources/Extensions/UITextField+Ext.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/Extensions/UIView+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/UIView+Ext.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/Extensions/UIView+Ext.swift rename to 14th-team5-iOS/Core/Sources/Extensions/UIView+Ext.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/Extensions/UIViewController+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/UIViewController+Ext.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/Extensions/UIViewController+Ext.swift rename to 14th-team5-iOS/Core/Sources/Extensions/UIViewController+Ext.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/Extensions/UIVisualEffectView+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/UIVisualEffectView+Ext.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/Extensions/UIVisualEffectView+Ext.swift rename to 14th-team5-iOS/Core/Sources/Extensions/UIVisualEffectView+Ext.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/Extensions/UIWindow+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/UIWindow+Ext.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/Extensions/UIWindow+Ext.swift rename to 14th-team5-iOS/Core/Sources/Extensions/UIWindow+Ext.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/Extensions/URLRequeset+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/URLRequeset+Ext.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/Extensions/URLRequeset+Ext.swift rename to 14th-team5-iOS/Core/Sources/Extensions/URLRequeset+Ext.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/Extensions/UserDefaults+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/UserDefaults+Ext.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/Extensions/UserDefaults+Ext.swift rename to 14th-team5-iOS/Core/Sources/Extensions/UserDefaults+Ext.swift diff --git a/14th-team5-iOS/Core/Sources/Trash/BBRx/Repositories/App/App.swift b/14th-team5-iOS/Core/Sources/Trash/BBRx/Repositories/App/App.swift index 9b98ecaf1..ba3c9873f 100644 --- a/14th-team5-iOS/Core/Sources/Trash/BBRx/Repositories/App/App.swift +++ b/14th-team5-iOS/Core/Sources/Trash/BBRx/Repositories/App/App.swift @@ -7,7 +7,7 @@ import Foundation -@available(*, deprecated, message: "KeyChainWrapper 혹은 UserDefaultsWrpper 사용") +@available(*, deprecated, message: "BBStorage를 사용하세요.") public enum App { public static let indicator = BibbiLoadIndicator() diff --git a/14th-team5-iOS/Core/Sources/Trash/BBRx/Repositories/DeepLink/DeepLinkRepository.swift b/14th-team5-iOS/Core/Sources/Trash/BBRx/Repositories/DeepLink/DeepLinkRepository.swift index fcdc08921..d2b1e185b 100644 --- a/14th-team5-iOS/Core/Sources/Trash/BBRx/Repositories/DeepLink/DeepLinkRepository.swift +++ b/14th-team5-iOS/Core/Sources/Trash/BBRx/Repositories/DeepLink/DeepLinkRepository.swift @@ -10,7 +10,6 @@ import Foundation import RxSwift import RxCocoa -@available(*, deprecated, message: "KeyChainWrapper 혹은 UserDefaultsWrpper 사용") public class DeepLinkRepository: RxObject { public let notification = BehaviorRelay(value: nil) public let widget = BehaviorRelay(value: nil) diff --git a/14th-team5-iOS/Core/Sources/Trash/BBRx/Repositories/Member/MemberRepository.swift b/14th-team5-iOS/Core/Sources/Trash/BBRx/Repositories/Member/MemberRepository.swift index 3093cec84..b52e7d4ae 100644 --- a/14th-team5-iOS/Core/Sources/Trash/BBRx/Repositories/Member/MemberRepository.swift +++ b/14th-team5-iOS/Core/Sources/Trash/BBRx/Repositories/Member/MemberRepository.swift @@ -10,7 +10,6 @@ import Foundation import RxSwift import RxCocoa -@available(*, deprecated, message: "KeyChainWrapper 혹은 UserDefaultsWrpper 사용") public class MemberRepository: RxObject { public let familyId = BehaviorRelay(value: nil) public let memberID = BehaviorRelay(value: nil) 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 43d2b3b00..41116db69 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,7 +10,6 @@ import Foundation import RxCocoa import RxSwift -@available(*, deprecated) public struct AccessToken: Codable, Equatable { public var accessToken: String? public var refreshToken: String? @@ -23,7 +22,6 @@ public struct AccessToken: Codable, Equatable { } } -@available(*, deprecated, message: "KeyChainWrapper 혹은 UserDefaultsWrpper 사용") public class TokenRepository: RxObject { public lazy var keychain = KeychainWrapper(serviceName: "Bibbi", accessGroup: "P9P4WJ623F.com.5ing.bibbi") diff --git a/14th-team5-iOS/Core/Sources/Trash/BBRx/RxUtils.swift b/14th-team5-iOS/Core/Sources/Trash/BBRx/RxUtils.swift index 43a92aa24..5fed12199 100644 --- a/14th-team5-iOS/Core/Sources/Trash/BBRx/RxUtils.swift +++ b/14th-team5-iOS/Core/Sources/Trash/BBRx/RxUtils.swift @@ -9,7 +9,6 @@ import Foundation import RxSwift -@available(*, deprecated, renamed: "Rxinterval") public enum RxConst { static public var milliseconds100Interval: RxTimeInterval { return .milliseconds(100) @@ -20,7 +19,6 @@ public enum RxConst { } } -@available(*, deprecated, renamed: "RxScheduler") public enum RxSchedulers { public static let main = { MainScheduler.instance diff --git a/14th-team5-iOS/Core/Sources/Trash/Types/Protocols/ItemIndexable.swift b/14th-team5-iOS/Core/Sources/Trash/Types/Protocols/ItemIndexable.swift deleted file mode 100644 index daefda522..000000000 --- a/14th-team5-iOS/Core/Sources/Trash/Types/Protocols/ItemIndexable.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// ItemIndexable.swift -// Core -// -// Created by 마경미 on 20.01.24. -// - -import UIKit -import RxDataSources - -public protocol ItemIndexable { - associatedtype Item - - subscript(indexPath: IndexPath) -> Item { get set } -} - -extension ItemIndexable { - public func item(at index: IndexPath) throws -> Item { self[index] } - public func items(at indexes: [IndexPath]) throws -> [Item] { try indexes.map(self.item(at:)) } -} - -extension TableViewSectionedDataSource: ItemIndexable { } -extension CollectionViewSectionedDataSource: ItemIndexable { } diff --git a/Bibbi.xcworkspace/contents.xcworkspacedata b/Bibbi.xcworkspace/contents.xcworkspacedata index 7e570dad8..7bb900224 100644 --- a/Bibbi.xcworkspace/contents.xcworkspacedata +++ b/Bibbi.xcworkspace/contents.xcworkspacedata @@ -26,6 +26,9 @@ + + @@ -35,6 +38,9 @@ + + @@ -44,6 +50,9 @@ + + @@ -77,15 +86,27 @@ + + + + + + + + From 7ee5752b661314a26e9970635ede6ba36338e2d9 Mon Sep 17 00:00:00 2001 From: Do-hyun-Kim Date: Wed, 10 Jul 2024 16:11:58 +0900 Subject: [PATCH 157/263] =?UTF-8?q?feat:=20BibbiNetworkMonitor=20Class=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20-=20APIWorker=20=ED=83=80=EC=9E=85=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=8D=BC=ED=8B=B0=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EB=B0=8F=20Session=20=ED=94=84=EB=A1=9C=ED=8D=BC=ED=8B=B0=20?= =?UTF-8?q?=EB=82=B4=EB=B6=80=20URLSessionConfiguration,=20Monitor,=20Requ?= =?UTF-8?q?estIntercepter=20=EC=A3=BC=EC=9E=85=20-=20APIWorker=20debug,=20?= =?UTF-8?q?print=20=EA=B5=AC=EB=AC=B8=20=EC=A0=9C=EA=B1=B0=20-=20=EA=B0=81?= =?UTF-8?q?=20APIWorker=20print=20=EA=B5=AC=EB=AC=B8=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Bibbi/Extensions/Codable+Ext.swift | 8 ++++ .../Data/Sources/APIs/APIWorker.swift | 31 +++++++------ .../Sources/APIs/BibbiNetworkMonitor.swift | 39 ++++++++++++++++ .../CalendarAPI/CalendarAPIWorker.swift | 25 ----------- .../Comment/CommentAPI/CommentAPIWorker.swift | 20 --------- .../Family/FamilyAPI/FamilyAPIWorker.swift | 30 ------------- .../MainViewAPI/MainViewAPIWorker.swift | 10 ----- .../APIs/OAuth/OAuthAPI/OAuthAPIWorker.swift | 25 ----------- .../APIs/Pick/PickAPI/PickAPIWorker.swift | 15 ------- .../ReactionAPI/ReactionAPIWorker.swift | 15 ------- .../RealEmojiAPI/RealEmojiAPIWorker.swift | 20 --------- .../Widget/WidgetAPI/WidgetAPIWorker.swift | 5 --- .../Account/AccountAPI/AccountAPIWorker.swift | 20 --------- .../Trash/Account/MeAPI/MeAPIWorker.swift | 36 --------------- .../Camera/CameraAPI/CameraAPIWorker.swift | 45 ------------------- .../Members/MemberAPI/MembersAPIWorker.swift | 21 --------- .../Mission/MissionAPI/MissionAPIWorker.swift | 5 --- .../PostList/PostAPI/PostListAPIWorker.swift | 5 --- .../Privacy/PrivacyAPI/PrivacyAPIWorker.swift | 10 ----- .../Resign/ResignAPI/ResignAPIWorker.swift | 5 --- 20 files changed, 64 insertions(+), 326 deletions(-) create mode 100644 14th-team5-iOS/Data/Sources/APIs/BibbiNetworkMonitor.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/Extensions/Codable+Ext.swift b/14th-team5-iOS/Core/Sources/Bibbi/Extensions/Codable+Ext.swift index b4d35a703..6c1d712fa 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/Extensions/Codable+Ext.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/Extensions/Codable+Ext.swift @@ -16,6 +16,14 @@ import RxSwift // MARK: Data Decodable (Data to Decodable) public extension Data { + + var toPrettyPrintedString: String? { + guard let object = try? JSONSerialization.jsonObject(with: self, options: []), + let data = try? JSONSerialization.data(withJSONObject: object, options: [.prettyPrinted]), + let jsonString = String(data: data, encoding: .utf8) else { return nil } + return jsonString + } + func decode(_ type: T.Type, using decoder: JSONDecoder? = nil) -> T? where T: Decodable { let decoder = decoder ?? JSONDecoder() diff --git a/14th-team5-iOS/Data/Sources/APIs/APIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/APIWorker.swift index 145f366e9..56ecd3cc6 100644 --- a/14th-team5-iOS/Data/Sources/APIs/APIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/APIWorker.swift @@ -21,8 +21,17 @@ public class APIWorker: NSObject { // MARK: - Identifier var id: String = "APIWorker" - - + private static let session: Session = { + let networkMonitor: BibbiNetworkMonitor = BibbiNetworkMonitor() + let networkConfiguration: URLSessionConfiguration = AF.session.configuration + let networkInterceptor: RequestInterceptor = NetworkInterceptor() + let networkSession: Session = Session( + configuration: networkConfiguration, + interceptor: networkInterceptor, + eventMonitors: [networkMonitor] + ) + return networkSession + }() // MARK: - Request @@ -35,8 +44,8 @@ public class APIWorker: NSObject { let headers = self.httpHeaders(headers) let parameters = self.parameters(parameters) - - return AF.rx.request( + + return APIWorker.session.rx.request( spec.method, spec.url, parameters: parameters, @@ -46,7 +55,6 @@ public class APIWorker: NSObject { ) .validate(statusCode: 200..<300) .responseData() - .debug("API Worker has received data from \"\(spec.url)\"") } func request( @@ -59,7 +67,7 @@ public class APIWorker: NSObject { let headers = self.httpHeaders(headers) let parameters = parameters.asDictionary() - return AF.rx.request( + return APIWorker.session.rx.request( spec.method, spec.url, parameters: parameters, @@ -69,7 +77,6 @@ public class APIWorker: NSObject { ) .validate(statusCode: 200..<300) .responseData() - .debug("API Worker has received data from \"\(spec.url)\"") } private func refreshRequest( @@ -88,13 +95,11 @@ public class APIWorker: NSObject { request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.headers = headers request.httpBody = jsonData - print("interCepter call with name \(url)") - return AF.rx.request(urlRequest: request) + return APIWorker.session.rx.request(urlRequest: request) .retry(5) .validate(statusCode: 200..<300) .responseData() - .debug("API Worker has received data from \"\(spec.url)\"") } private func request( @@ -113,15 +118,13 @@ public class APIWorker: NSObject { request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.headers = headers request.httpBody = jsonData - print("interCepter call with name \(url)") - return AF.rx.request( + return APIWorker.session.rx.request( urlRequest: request, interceptor: NetworkInterceptor() ) .validate(statusCode: 200..<300) .responseData() - .debug("API Worker has received data from \"\(spec.url)\"") } func request( @@ -155,7 +158,7 @@ public class APIWorker: NSObject { request.headers = headers return Single.create { single -> Disposable in - AF.upload( + APIWorker.session.upload( image, to: url, method: spec.method, diff --git a/14th-team5-iOS/Data/Sources/APIs/BibbiNetworkMonitor.swift b/14th-team5-iOS/Data/Sources/APIs/BibbiNetworkMonitor.swift new file mode 100644 index 000000000..f2e587b9a --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/BibbiNetworkMonitor.swift @@ -0,0 +1,39 @@ +// +// 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/Calendar/CalendarAPI/CalendarAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/CalendarAPIWorker.swift index b8d52cde9..c1afd82fe 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/CalendarAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/CalendarAPIWorker.swift @@ -38,11 +38,6 @@ extension CalendarAPIWorker { return request(spec: spec) .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("CalendarResponse Fetch Result: \(str)") - } - } .map(ArrayResponseCalendarResponseDTO.self) .catchAndReturn(nil) .asSingle() @@ -57,11 +52,6 @@ extension CalendarAPIWorker { return request(spec: spec) .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("StatisticsSummary Fetch Result: \(str)") - } - } .map(FamilyMonthlyStatisticsResponseDTO.self) .catchAndReturn(nil) .asSingle() @@ -76,11 +66,6 @@ extension CalendarAPIWorker { return request(spec: spec) .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("MonthlyCalendar Fetch Result: \(str)") - } - } .map(ArrayResponseMonthlyCalendarResponseDTO.self) .catchAndReturn(nil) .asSingle() @@ -95,11 +80,6 @@ extension CalendarAPIWorker { return request(spec: spec) .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("DailyCalendar Fetch Result: \(str)") - } - } .map(ArrayResponseDailyCalendarResponseDTO.self) .catchAndReturn(nil) .asSingle() @@ -115,11 +95,6 @@ extension CalendarAPIWorker { return request(spec: spec) .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("Banner Fetch Result: \(str)") - } - } .map(BannerResponseDTO.self) .catchAndReturn(nil) .asSingle() 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 46150179b..92c8f18c5 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/CommentAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/CommentAPIWorker.swift @@ -44,11 +44,6 @@ extension CommentAPIWorker { return request(spec: spec) .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("Fetch PostComment Result: \(str)") - } - } .map(PaginationResponsePostCommentResponseDTO.self) .catchAndReturn(nil) .asSingle() @@ -72,11 +67,6 @@ extension CommentAPIWorker { }() // TODO: - APIWorker 리팩토링되는 대로 코드 삭제하기 return request(spec: spec, headers: headers, jsonEncodable: body) .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("Create PostComment Result: \(str)") - } - } .map(PostCommentResponseDTO.self) .catchAndReturn(nil) .asSingle() @@ -94,11 +84,6 @@ extension CommentAPIWorker { return request(spec: spec, jsonEncodable: body) .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("Update PostComment Result: \(str)") - } - } .map(PostCommentResponseDTO.self) .catchAndReturn(nil) .asSingle() @@ -115,11 +100,6 @@ extension CommentAPIWorker { return request(spec: spec) .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("Delete PostComment Result: \(str)") - } - } .map(PostCommentDeleteResponseDTO.self) .catchAndReturn(nil) .asSingle() diff --git a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIWorker.swift index c2fb3353b..1ebf1bce4 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIWorker.swift @@ -38,11 +38,6 @@ extension FamilyAPIWorker { return request(spec: spec, jsonEncodable: body) .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("Join Family Result: \(str)") - } - } .map(JoinFamilyResponseDTO.self) .catchAndReturn(nil) .asSingle() @@ -56,11 +51,6 @@ extension FamilyAPIWorker { return request(spec: spec) .subscribe(on: Self.queue) - .do(onNext: { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("Resign Family result: \(str)") - } - }) .map(DefaultResponseDTO.self) .catchAndReturn(nil) .asSingle() @@ -74,11 +64,6 @@ extension FamilyAPIWorker { return request(spec: spec) .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("Family Create Result: \(str)") - } - } .map(CreateFamilyResponseDTO.self) .catchAndReturn(nil) .asSingle() @@ -92,11 +77,6 @@ extension FamilyAPIWorker { return request(spec: spec) .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("InvigationUrl Fetch Result: \(str)") - } - } .map(FamilyInvitationLinkResponseDTO.self) .catchAndReturn(nil) .asSingle() @@ -110,11 +90,6 @@ extension FamilyAPIWorker { return request(spec: spec) .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("FamilyCreatedAt Fetch Result: \(str)") - } - } .map(FamilyCreatedAtResponseDTO.self) .catchAndReturn(nil) .asSingle() @@ -131,11 +106,6 @@ extension FamilyAPIWorker { return request(spec: spec) .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("FamilyMember Fetch Result: \(str)") - } - } .map(PaginationResponseFamilyMemberProfileDTO.self) .catchAndReturn(nil) .asSingle() diff --git a/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/MainViewAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/MainViewAPIWorker.swift index d9d71cfac..b81f4f1a5 100644 --- a/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/MainViewAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/MainViewAPIWorker.swift @@ -43,11 +43,6 @@ extension MainAPIWorker { let spec = MainViewAPIs.fetchMain.spec return request(spec: spec, headers: headers) .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("Main Fetch Result: \(str)") - } - } .map(MainResponseDTO.self) .catchAndReturn(nil) .map { $0?.toDomain() } @@ -58,11 +53,6 @@ extension MainAPIWorker { let spec = MainViewAPIs.fetchMainNight.spec return request(spec: spec, headers: headers) .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("Main Night Fetch Result: \(str)") - } - } .map(MainNightResponseDTO.self) .catchAndReturn(nil) .map { $0?.toDomain() } diff --git a/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/OAuthAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/OAuthAPIWorker.swift index 3682d4eb4..172d1c6a1 100644 --- a/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/OAuthAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/OAuthAPIWorker.swift @@ -37,11 +37,6 @@ extension OAuthAPIWorker { return request(spec: spec, headers: headers, jsonEncodable: body) .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("Refresh Token Result: \(str)") - } - } .map(AuthResultResponseDTO.self) .catchAndReturn(nil) .asSingle() @@ -55,11 +50,6 @@ extension OAuthAPIWorker { return request(spec: spec, jsonEncodable: body) .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("Auth Token Result: \(str)") - } - } .map(AuthResultResponseDTO.self) .catchAndReturn(nil) .asSingle() @@ -73,11 +63,6 @@ extension OAuthAPIWorker { return request(spec: spec, jsonEncodable: body) .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("Auth Token Result: \(str)") - } - } .map(AuthResultResponseDTO.self) .catchAndReturn(nil) .asSingle() @@ -92,11 +77,6 @@ extension OAuthAPIWorker { return request(spec: spec, jsonEncodable: body) .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("FCM Register Result: \(str)") - } - } .map(DefaultResponseDTO.self) .catchAndReturn(nil) .asSingle() @@ -110,11 +90,6 @@ extension OAuthAPIWorker { return request(spec: spec) .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("FCM Delete Result: \(str)") - } - } .map(DefaultResponseDTO.self) .catchAndReturn(nil) .asSingle() diff --git a/14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/PickAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/PickAPIWorker.swift index cb628af35..44cb4a698 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/PickAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/PickAPIWorker.swift @@ -36,11 +36,6 @@ extension PickAPIWorker { private func pickMember(spec: APISpec, headers: [APIHeader]?) -> Single { return request(spec: spec, headers: headers) .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("PickMember Result: \(str)") - } - } .map(PickResponseDTO.self) .catchAndReturn(nil) .map { $0?.toDomain() } @@ -65,11 +60,6 @@ extension PickAPIWorker { private func fetchWhoDidIPickMember(spec: APISpec, headers: [APIHeader]?) -> Single { return request(spec: spec, headers: headers) .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("Who Did I Pick: \(str)") - } - } .map(PickMemberListResponseDTO.self) .catchAndReturn(nil) .map { $0?.toDomain() } @@ -94,11 +84,6 @@ extension PickAPIWorker { private func fetchWhoPickedMeMember(spec: APISpec, headers: [APIHeader]?) -> Single { return request(spec: spec, headers: headers) .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("Who Picked Me: \(str)") - } - } .map(PickMemberListResponseDTO.self) .catchAndReturn(nil) .map { $0?.toDomain() } diff --git a/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/ReactionAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/ReactionAPIWorker.swift index 802be2cf3..debd90141 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/ReactionAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/ReactionAPIWorker.swift @@ -44,11 +44,6 @@ extension ReactionAPIWorker { let spec = ReactionAPIs.fetchReactions(query).spec return request(spec: spec, headers: headers) .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("Fetch Reaction Result: \(str)") - } - } .map(FetchReactionResponseDTO.self) .catchAndReturn(nil) .map { @@ -62,11 +57,6 @@ extension ReactionAPIWorker { let spec = ReactionAPIs.addReactions(query.postId).spec return request(spec: spec, headers: headers, jsonEncodable: requestDTO) .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("Add Reaction Result: \(str)") - } - } .map(AddReactionResponseDTO.self) .catchAndReturn(nil) .map { $0?.toDomain() } @@ -78,11 +68,6 @@ extension ReactionAPIWorker { let spec = ReactionAPIs.removeReactions(query.postId).spec return request(spec: spec, headers: headers, jsonEncodable: requestDTO) .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("Remove Reaction Result: \(str)") - } - } .map(RemoveReactionResponseDTO.self) .catchAndReturn(nil) .map { $0?.toDomain() } diff --git a/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/RealEmojiAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/RealEmojiAPIWorker.swift index ae20c5367..7c36f155a 100644 --- a/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/RealEmojiAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/RealEmojiAPIWorker.swift @@ -47,11 +47,6 @@ extension RealEmojiAPIWorker { return request(spec: spec, headers: headers) .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("RealEmojiList Fetch Result: \(str)") - } - } .map(FetchRealEmojiListResponseDTO.self) .catchAndReturn(nil) .map { @@ -65,11 +60,6 @@ extension RealEmojiAPIWorker { return request(spec: spec, headers: headers) .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("Real Emoji Items Result: \(str)") - } - } .map(MyRealEmojiResponseDTO.self) .catchAndReturn(nil) .map { @@ -84,11 +74,6 @@ extension RealEmojiAPIWorker { return request(spec: spec, headers: headers, jsonEncodable: body) .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("Add Real Emoji Result: \(str)") - } - } .map(AddRealEmojiResponseDTO.self) .catchAndReturn(nil) .map { @@ -102,11 +87,6 @@ extension RealEmojiAPIWorker { return request(spec: spec, headers: headers) .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("Remove Real Emoji Result: \(str)") - } - } .map(RemoveRealEmojiResponseDTO.self) .catchAndReturn(nil) .map { diff --git a/14th-team5-iOS/Data/Sources/APIs/Widget/WidgetAPI/WidgetAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/Widget/WidgetAPI/WidgetAPIWorker.swift index 95a881fe2..bdec40477 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Widget/WidgetAPI/WidgetAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Widget/WidgetAPI/WidgetAPIWorker.swift @@ -43,11 +43,6 @@ extension WidgetAPIWorker { return request(spec: spec, headers: headers, parameters: parameters) .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("Fetch Recent Family Post Result: \(str)") - } - } .map(RecentFamilyPostResponseDTO.self) .catchAndReturn(nil) .map { $0?.toDomain() } diff --git a/14th-team5-iOS/Data/Sources/Trash/Account/AccountAPI/AccountAPIWorker.swift b/14th-team5-iOS/Data/Sources/Trash/Account/AccountAPI/AccountAPIWorker.swift index 94266e7e5..7bc0b864a 100644 --- a/14th-team5-iOS/Data/Sources/Trash/Account/AccountAPI/AccountAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Trash/Account/AccountAPI/AccountAPIWorker.swift @@ -34,11 +34,6 @@ extension AccountAPIWorker { private func signInWith(spec: APISpec, jsonEncodable: Encodable) -> Single { return request(spec: spec, headers: [BibbiAPI.Header.xAppKey], jsonEncodable: jsonEncodable) .subscribe(on: Self.queue) - .do(onNext: { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("SignIn result : \(str)") - } - }) .map(AccessTokenResponse.self) .catchAndReturn(nil) .asSingle() @@ -57,11 +52,6 @@ extension AccountAPIWorker { return request(spec: spec, headers: headers, jsonEncodable: jsonEncodable) .subscribe(on: Self.queue) - .do(onNext: { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("SignUp result : \(str)") - } - }) .map(AccessTokenResponse.self) .catchAndReturn(nil) .asSingle() @@ -83,11 +73,6 @@ extension AccountAPIWorker { return request(spec: spec, headers: [BibbiAPI.Header.xAppKey, BibbiAPI.Header.xAuthToken(accessToken), BibbiAPI.Header.acceptJson], jsonEncodable: parameter) .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("Account nickName update Result: \(str)") - } - } .map(AccountNickNameEditDTO.self) .catchAndReturn(nil) .asSingle() @@ -98,11 +83,6 @@ extension AccountAPIWorker { print("RefreshToken: \(parameter)") return request(spec: spec, headers: [BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.contentJson], jsonEncodable: parameter) .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("Account Refresh Token Result \(str)") - } - } .map(AccountRefreshDTO.self) .asSingle() } diff --git a/14th-team5-iOS/Data/Sources/Trash/Account/MeAPI/MeAPIWorker.swift b/14th-team5-iOS/Data/Sources/Trash/Account/MeAPI/MeAPIWorker.swift index 1779a2f35..69aadfc8f 100644 --- a/14th-team5-iOS/Data/Sources/Trash/Account/MeAPI/MeAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Trash/Account/MeAPI/MeAPIWorker.swift @@ -34,12 +34,6 @@ extension MeAPIWorker: MeRepositoryProtocol, JoinFamilyRepository, FCMRepository return request(spec: spec, headers: headers, jsonEncodable: token) .subscribe(on: Self.queue) - .do(onNext: { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("saveFcmToken result : \(str)") - KeychainRepository.shared.saveFCMToken(token: token.fcmToken) - } - }) .map(VoidResponse.self) .catchAndReturn(nil) .map { $0?.toDomain() } @@ -49,11 +43,6 @@ extension MeAPIWorker: MeRepositoryProtocol, JoinFamilyRepository, FCMRepository private func deleteFcmToken(spec: APISpec, headers: [APIHeader]?) -> Single { return request(spec: spec, headers: headers) .subscribe(on: Self.queue) - .do(onNext: { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("deleteFcmToken result : \(str)") - } - }) .map(VoidResponse.self) .catchAndReturn(nil) .map { $0?.toDomain() } @@ -63,11 +52,6 @@ extension MeAPIWorker: MeRepositoryProtocol, JoinFamilyRepository, FCMRepository private func getMemberInfo(spec: APISpec) -> Single { return request(spec: spec) .subscribe(on: Self.queue) - .do(onNext: { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("getMemberInfo result : \(str)") - } - }) .map(MemberInfo.self) .catchAndReturn(nil) .asSingle() @@ -78,11 +62,6 @@ extension MeAPIWorker: MeRepositoryProtocol, JoinFamilyRepository, FCMRepository return request(spec: spec, headers: headers, jsonEncodable: jsonEncodable) .subscribe(on: Self.queue) - .do(onNext: { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("Join Family result : \(str)") - } - }) .map(FamilyInfo.self) .catchAndReturn(nil) .asSingle() @@ -95,11 +74,6 @@ extension MeAPIWorker: MeRepositoryProtocol, JoinFamilyRepository, FCMRepository return request(spec: spec, headers: headers, jsonEncodable: requestDTO) .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("Join Family Fetch Result: \(str)") - } - } .map(JoinFamilyResponseDTO.self) .catchAndReturn(nil) .map { $0?.toDomain() } @@ -110,11 +84,6 @@ extension MeAPIWorker: MeRepositoryProtocol, JoinFamilyRepository, FCMRepository private func resignFamily(spec: APISpec, headers: [APIHeader]?) -> Single { return request(spec: spec, headers: headers) .subscribe(on: Self.queue) - .do(onNext: { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("Resign Family result: \(str)") - } - }) .map(AccountFamilyResignResponse.self) .catchAndReturn(nil) .asSingle() @@ -123,11 +92,6 @@ extension MeAPIWorker: MeRepositoryProtocol, JoinFamilyRepository, FCMRepository private func fetchAppVersion(spec: APISpec) -> Single { return request(spec: spec, headers: [BibbiAPI.Header.xAppKey]) .subscribe(on: Self.queue) - .do(onNext: { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("Fetch AppVersion result: \(str)") - } - }) .map(AppVersionInfo.self) .catchAndReturn(nil) .asSingle() diff --git a/14th-team5-iOS/Data/Sources/Trash/Camera/CameraAPI/CameraAPIWorker.swift b/14th-team5-iOS/Data/Sources/Trash/Camera/CameraAPI/CameraAPIWorker.swift index 066006513..8aabb169b 100644 --- a/14th-team5-iOS/Data/Sources/Trash/Camera/CameraAPI/CameraAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Trash/Camera/CameraAPI/CameraAPIWorker.swift @@ -38,11 +38,6 @@ extension CameraAPIWorker { return request(spec: spec, headers: [BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.xAuthToken(accessToken)], jsonEncodable: parameters) .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("upload Profile Image URL Fetch Reuslt: \(str)") - } - } .map(CameraDisplayImageResponseDTO.self) .catchAndReturn(nil) .asSingle() @@ -56,11 +51,6 @@ extension CameraAPIWorker { return request(spec: spec, headers: [BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.xAuthToken(accessToken)], jsonEncodable: parameters) .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("uploadImage Fetch Reuslt: \(str)") - } - } .map(CameraDisplayImageResponseDTO.self) .catchAndReturn(nil) .asSingle() @@ -70,11 +60,6 @@ extension CameraAPIWorker { let spec = CameraAPIs.editProfileImage(memberId).spec return request(spec: spec, headers: [BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.xAuthToken(accessToken)], jsonEncodable: parameters) .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("editProfile Image Upload Result: \(str)") - } - } .map(MembersProfileResponseDTO.self) .catchAndReturn(nil) .asSingle() @@ -94,11 +79,6 @@ extension CameraAPIWorker { return request(spec: spec, headers: [BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.xAuthToken(accessToken)], jsonEncodable: parameters) .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("editProfile Image Upload Result: \(str)") - } - } .map(CameraDisplayPostResponseDTO.self) .map { dto in guard let dto = dto else { return nil } @@ -116,11 +96,6 @@ extension CameraAPIWorker { return request(spec: spec, headers: [BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.xAuthToken(accessToken)], jsonEncodable: parameters) .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("RealEmoji Image Presigned URL Reuslt: \(str)") - } - } .map(CameraRealEmojiPreSignedResponseDTO.self) .catchAndReturn(nil) .asSingle() @@ -132,11 +107,6 @@ extension CameraAPIWorker { return request(spec: spec, headers: [BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.xAuthToken(accessToken)], jsonEncodable: parameters) .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("Real Image upload to S3 Result: \(str)") - } - } .map(CameraCreateRealEmojiResponseDTO.self) .catchAndReturn(nil) .asSingle() @@ -147,11 +117,6 @@ extension CameraAPIWorker { return request(spec: spec, headers: [BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.xAuthToken(accessToken)]) .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("Real Emoji Items Result: \(str)") - } - } .map(CameraRealEmojiImageItemResponseDTO.self) .catchAndReturn(nil) .asSingle() @@ -161,11 +126,6 @@ extension CameraAPIWorker { let spec = CameraAPIs.modifyRealEmojiImage(memberId, realEmojiId).spec return request(spec: spec, headers: [BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.xAuthToken(accessToken)], jsonEncodable: parameters) .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("Real Emoji Modify Result: \(str)") - } - } .map(CameraUpdateRealEmojiResponseDTO.self) .catchAndReturn(nil) .asSingle() @@ -175,11 +135,6 @@ extension CameraAPIWorker { let spec = CameraAPIs.fetchMissionToday.spec return request(spec: spec, headers: [BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.xAuthToken(accessToken)]) .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("Fetch Today Mission Items : \(str)") - } - } .map(CameraTodayMissionResponseDTO.self) .catchAndReturn(nil) .asSingle() diff --git a/14th-team5-iOS/Data/Sources/Trash/Members/MemberAPI/MembersAPIWorker.swift b/14th-team5-iOS/Data/Sources/Trash/Members/MemberAPI/MembersAPIWorker.swift index 50b0dd6c9..3ec32d8bc 100644 --- a/14th-team5-iOS/Data/Sources/Trash/Members/MemberAPI/MembersAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Trash/Members/MemberAPI/MembersAPIWorker.swift @@ -38,11 +38,6 @@ extension MembersAPIWorker { return request(spec: spec, headers: [BibbiAPI.Header.xAppVersion, BibbiAPI.Header.xUserPlatform, BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.xAuthToken(accessToken)]) .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("fetch Profile Member Result: \(str)") - } - } .map(MembersProfileResponseDTO.self) .catchAndReturn(nil) .asSingle() @@ -54,11 +49,6 @@ extension MembersAPIWorker { return request(spec: spec, headers: [BibbiAPI.Header.xAppVersion, BibbiAPI.Header.xUserPlatform, BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.xAuthToken(accessToken)], jsonEncodable: parameters) .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("createPresinged URL \(str)") - } - } .map(CameraDisplayImageResponseDTO.self) .catchAndReturn(nil) .asSingle() @@ -70,7 +60,6 @@ extension MembersAPIWorker { return upload(spec: spec, headers: [BibbiAPI.Header.xAppVersion, BibbiAPI.Header.xUserPlatform, BibbiAPI.Header.xAppKey, BibbiAPI.Header.xAuthToken(accessToken)], image: imageData) .subscribe(on: Self.queue) .catchAndReturn(false) - .debug("preSingedURL Upload To Profile") .map { _ in true } } @@ -79,11 +68,6 @@ extension MembersAPIWorker { return request(spec: spec, headers: [BibbiAPI.Header.xAppVersion, BibbiAPI.Header.xUserPlatform, BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.xAuthToken(accessToken)], jsonEncodable: parameter) .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("updateProfile Image Result: \(str)") - } - } .map(MembersProfileResponseDTO.self) .catchAndReturn(nil) .asSingle() @@ -94,11 +78,6 @@ extension MembersAPIWorker { return request(spec: spec,headers: [BibbiAPI.Header.xAppVersion, BibbiAPI.Header.xUserPlatform, BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson]) .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("delete Profile Image Result: \(str)") - } - } .map(MembersProfileResponseDTO.self) .catchAndReturn(nil) .asSingle() diff --git a/14th-team5-iOS/Data/Sources/Trash/Mission/MissionAPI/MissionAPIWorker.swift b/14th-team5-iOS/Data/Sources/Trash/Mission/MissionAPI/MissionAPIWorker.swift index d3679fd2b..2932965a1 100644 --- a/14th-team5-iOS/Data/Sources/Trash/Mission/MissionAPI/MissionAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Trash/Mission/MissionAPI/MissionAPIWorker.swift @@ -30,11 +30,6 @@ extension MissionAPIWorker { private func getMissionContent(spec: APISpec, headers: [APIHeader]?) -> Single { return request(spec: spec, headers: headers) .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("Mission Content Result: \(str)") - } - } .map(MissionContentResponseDTO.self) .catchAndReturn(nil) .map { $0?.toDomain() } diff --git a/14th-team5-iOS/Data/Sources/Trash/Post/PostList/PostAPI/PostListAPIWorker.swift b/14th-team5-iOS/Data/Sources/Trash/Post/PostList/PostAPI/PostListAPIWorker.swift index c8a2d4d32..af7fdbab9 100644 --- a/14th-team5-iOS/Data/Sources/Trash/Post/PostList/PostAPI/PostListAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Trash/Post/PostList/PostAPI/PostListAPIWorker.swift @@ -33,11 +33,6 @@ extension PostAPIWorker { let spec = PostAPIs.fetchPostList.spec return request(spec: spec, parameters: requestDTO) .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("PostList Fetch Result: \(str)") - } - } .map(PostListResponseDTO.self) .map { return $0?.toDomain() diff --git a/14th-team5-iOS/Data/Sources/Trash/Privacy/PrivacyAPI/PrivacyAPIWorker.swift b/14th-team5-iOS/Data/Sources/Trash/Privacy/PrivacyAPI/PrivacyAPIWorker.swift index 44c5d0974..739fac2c7 100644 --- a/14th-team5-iOS/Data/Sources/Trash/Privacy/PrivacyAPI/PrivacyAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Trash/Privacy/PrivacyAPI/PrivacyAPIWorker.swift @@ -41,11 +41,6 @@ extension PrivacyAPIWorker { return request(spec: spec, headers: [BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.xAuthToken(accessToken)], parameters: parameter) .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("fetch BibbiApp Info Result: \(str)") - } - } .map(BibbiAppInfoDTO.self) .catchAndReturn(nil) .asSingle() @@ -57,11 +52,6 @@ extension PrivacyAPIWorker { return request(spec: spec, headers: [BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.xAuthToken(accessToken)]) .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("fetch family resign Reuslt: \(str)") - } - } .map(AccountFamilyResignDTO.self) .catchAndReturn(nil) .asSingle() diff --git a/14th-team5-iOS/Data/Sources/Trash/Resign/ResignAPI/ResignAPIWorker.swift b/14th-team5-iOS/Data/Sources/Trash/Resign/ResignAPI/ResignAPIWorker.swift index 8999d1166..41c4b26c7 100644 --- a/14th-team5-iOS/Data/Sources/Trash/Resign/ResignAPI/ResignAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Trash/Resign/ResignAPI/ResignAPIWorker.swift @@ -36,11 +36,6 @@ extension ResignAPIWorker { return request(spec: spec, headers: [BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.xAuthToken(accessToken)]) .subscribe(on: Self.queue) - .do { - if let str = String(data: $0.1, encoding: .utf8) { - debugPrint("fetch resign Account Result: \(str)") - } - } .map(AccountResignDTO.self) .catchAndReturn(nil) .asSingle() From 7c799012d15b8b2dee69fd610986f7ce000b29a9 Mon Sep 17 00:00:00 2001 From: Do-hyun-Kim Date: Wed, 10 Jul 2024 17:04:36 +0900 Subject: [PATCH 158/263] =?UTF-8?q?fix:=20APIWorker=20=EA=B8=B0=EC=A1=B4?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=20=EC=9B=90=EB=B3=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 14th-team5-iOS/Data/Sources/APIs/APIWorker.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/14th-team5-iOS/Data/Sources/APIs/APIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/APIWorker.swift index 56ecd3cc6..192d13728 100644 --- a/14th-team5-iOS/Data/Sources/APIs/APIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/APIWorker.swift @@ -96,7 +96,7 @@ public class APIWorker: NSObject { request.headers = headers request.httpBody = jsonData - return APIWorker.session.rx.request(urlRequest: request) + return AF.rx.request(urlRequest: request) .retry(5) .validate(statusCode: 200..<300) .responseData() From 56b2b9a365f33ecbf705ed9cf681c2695eb21850 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Wed, 10 Jul 2024 19:42:23 +0900 Subject: [PATCH 159/263] =?UTF-8?q?feat:=20App=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=ED=86=B5=EC=8B=A0=20=EB=B0=8F=20=EC=9D=98=EC=A1=B4=EC=84=B1=20?= =?UTF-8?q?=EC=A3=BC=EC=9E=85=20=EA=B5=AC=ED=98=84=20(#589)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DIContainer/AppDIContainer.swift | 23 +++++++++- .../APIs/App/AppAPI/AppAPIWorker.swift | 45 +++++++++++++++++++ .../Sources/APIs/App/AppAPI/AppAPIs.swift | 22 +++++++++ .../DataMapping/AppVersionResponseDTO.swift | 36 +++++++++++++++ .../APIs/App/Repository/AppRepository.swift | 40 +++++++++++++++++ .../Entities/App/AppVersionEntity.swift | 30 +++++++++++++ .../Sources/Repositories/AppRepository.swift | 14 ++++++ .../Sources/{ => Trash}/AppVersion.swift | 0 .../UseCases/App/FetchAppVersionUseCase.swift | 32 +++++++++++++ 9 files changed, 241 insertions(+), 1 deletion(-) create mode 100644 14th-team5-iOS/Data/Sources/APIs/App/AppAPI/AppAPIWorker.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/App/AppAPI/AppAPIs.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/App/AppAPI/DataMapping/AppVersionResponseDTO.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/App/Repository/AppRepository.swift create mode 100644 14th-team5-iOS/Domain/Sources/Entities/App/AppVersionEntity.swift create mode 100644 14th-team5-iOS/Domain/Sources/Repositories/AppRepository.swift rename 14th-team5-iOS/Domain/Sources/{ => Trash}/AppVersion.swift (100%) create mode 100644 14th-team5-iOS/Domain/Sources/UseCases/App/FetchAppVersionUseCase.swift diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/AppDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/AppDIContainer.swift index 2b0fe1f5b..88f28ab39 100644 --- a/14th-team5-iOS/App/Sources/Application/DIContainer/AppDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/AppDIContainer.swift @@ -7,13 +7,34 @@ import Core import Data -import Foundation +import Domain final class AppDIContainer: BaseContainer { + // MARK: - Make UseCase + + private func makeFetchAppVersionUseCase() -> FetchAppVersionUseCaseProtocol { + FetchAppVersionUseCase( + appRepository: makeAppRepository() + ) + } + + + // MARK: - Make Repository + + private func makeAppRepository() -> AppRepositoryProtocol { + return AppRepository() + } + + // MARK: - Register + func registerDependencies() { + container.register(type: FetchAppVersionUseCaseProtocol.self) { _ in + self.makeFetchAppVersionUseCase() + } + // ServiceProvider 등록 container.register(type: GlobalStateProviderProtocol.self) { _ in return GlobalStateProvider() diff --git a/14th-team5-iOS/Data/Sources/APIs/App/AppAPI/AppAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/App/AppAPI/AppAPIWorker.swift new file mode 100644 index 000000000..9c3693d07 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/App/AppAPI/AppAPIWorker.swift @@ -0,0 +1,45 @@ +// +// AppAPIWorker.swift +// Data +// +// Created by 김건우 on 7/10/24. +// + +import Core +import Domain +import Foundation + +import RxSwift + +public typealias AppAPIWorker = AppAPIs.Worker +extension AppAPIs { + public final class Worker: APIWorker { + static let queue = { + ConcurrentDispatchQueueScheduler(queue: DispatchQueue(label: "AppAPIQueue", qos: .utility)) + }() + + public override init() { + super.init() + self.id = "AppAPIWorker" + } + } +} + +// MARK: - Extensions + +extension AppAPIWorker { + + + // MARK: - Fetch App Info + + public func fetchAppVersion(appKey: String) -> Single { + let spec = AppAPIs.appVersion(appKey).spec + + return request(spec: spec) + .subscribe(on: Self.queue) + .map(AppVersionResponseDTO.self) + .catchAndReturn(nil) + .asSingle() + } + +} diff --git a/14th-team5-iOS/Data/Sources/APIs/App/AppAPI/AppAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/App/AppAPI/AppAPIs.swift new file mode 100644 index 000000000..6e188a5d0 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/App/AppAPI/AppAPIs.swift @@ -0,0 +1,22 @@ +// +// AppAPIs.swift +// Data +// +// Created by 김건우 on 7/10/24. +// + +import Core +import Foundation + +public enum AppAPIs: API { + + case appVersion(String) + + public var spec: APISpec { + switch self { + case let .appVersion(appKey): + return APISpec(method: .get, url: "\(BibbiAPI.hostApi)/me/app-version&appKey=\(appKey)") + } + } + +} diff --git a/14th-team5-iOS/Data/Sources/APIs/App/AppAPI/DataMapping/AppVersionResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/App/AppAPI/DataMapping/AppVersionResponseDTO.swift new file mode 100644 index 000000000..5c4a64372 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/App/AppAPI/DataMapping/AppVersionResponseDTO.swift @@ -0,0 +1,36 @@ +// +// AppVersionResponseDTO.swift +// Data +// +// Created by 김건우 on 7/10/24. +// + +import Domain +import Foundation + +public struct AppVersionResponseDTO: Decodable { + private enum CodingKeys: String, CodingKey { + case appKey + case appVersion + case latest + case inReview + case inService + } + var appKey: String + var appVersion: String + var latest: Bool + var inReview: Bool + var inService: Bool +} + +extension AppVersionResponseDTO { + func toDomain() -> AppVersionEntity { + return .init( + appKey: appKey, + appVersion: appVersion, + latest: latest, + inReview: inReview, + inService: inService + ) + } +} diff --git a/14th-team5-iOS/Data/Sources/APIs/App/Repository/AppRepository.swift b/14th-team5-iOS/Data/Sources/APIs/App/Repository/AppRepository.swift new file mode 100644 index 000000000..958df3f0e --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/App/Repository/AppRepository.swift @@ -0,0 +1,40 @@ +// +// AppRepository.swift +// Data +// +// Created by 김건우 on 7/10/24. +// + +import Domain +import Foundation + +import RxSwift + +public final class AppRepository: AppRepositoryProtocol { + + // MARK: - Properties + public let disposeBag = DisposeBag() + + // MARK: - APIWorker + private let appApiWorker = AppAPIWorker() + + // MARK: - Intializer + public init() { } + +} + + +// MARK: - Extensions + +extension AppRepository { + + public func fetchAppVersion() -> Observable { + // TODO: - xAppKey 불러오는 코드 다시 작성하기 + let appKey = "7c5aaa36-570e-491f-b18a-26a1a0b72959" + + return appApiWorker.fetchAppVersion(appKey: appKey) + .map { $0?.toDomain() } + .asObservable() + } + +} diff --git a/14th-team5-iOS/Domain/Sources/Entities/App/AppVersionEntity.swift b/14th-team5-iOS/Domain/Sources/Entities/App/AppVersionEntity.swift new file mode 100644 index 000000000..cbc88819d --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Entities/App/AppVersionEntity.swift @@ -0,0 +1,30 @@ +// +// AppVersionEntity.swift +// Domain +// +// Created by 김건우 on 7/10/24. +// + +import Foundation + +public struct AppVersionEntity { + public var appKey: String + public var appVersion: String + public var latest: Bool + public var inReview: Bool + public var inService: Bool + + public init( + appKey: String, + appVersion: String, + latest: Bool, + inReview: Bool, + inService: Bool + ) { + self.appKey = appKey + self.appVersion = appVersion + self.latest = latest + self.inReview = inReview + self.inService = inService + } +} diff --git a/14th-team5-iOS/Domain/Sources/Repositories/AppRepository.swift b/14th-team5-iOS/Domain/Sources/Repositories/AppRepository.swift new file mode 100644 index 000000000..d6910c75d --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Repositories/AppRepository.swift @@ -0,0 +1,14 @@ +// +// AppRepository.swift +// Domain +// +// Created by 김건우 on 7/10/24. +// + +import Foundation + +import RxSwift + +public protocol AppRepositoryProtocol { + func fetchAppVersion() -> Observable +} diff --git a/14th-team5-iOS/Domain/Sources/AppVersion.swift b/14th-team5-iOS/Domain/Sources/Trash/AppVersion.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/AppVersion.swift rename to 14th-team5-iOS/Domain/Sources/Trash/AppVersion.swift diff --git a/14th-team5-iOS/Domain/Sources/UseCases/App/FetchAppVersionUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/App/FetchAppVersionUseCase.swift new file mode 100644 index 000000000..a0c89d1ff --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/UseCases/App/FetchAppVersionUseCase.swift @@ -0,0 +1,32 @@ +// +// FetchAppVersionUseCase.swift +// Domain +// +// Created by 김건우 on 7/10/24. +// + +import Foundation + +import RxSwift + +public protocol FetchAppVersionUseCaseProtocol { + func execute() -> Observable +} + +public class FetchAppVersionUseCase: FetchAppVersionUseCaseProtocol { + + // MARK: - Repositories + let appRepository: AppRepositoryProtocol + + // MARK: - Intializer + public init(appRepository: AppRepositoryProtocol) { + self.appRepository = appRepository + } + + // MARK: - Execute + + public func execute() -> Observable { + appRepository.fetchAppVersion() + } + +} From 171cc564f508caf92c025a632a0c040a2bf2be25 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Wed, 31 Jul 2024 14:54:25 +0900 Subject: [PATCH 160/263] =?UTF-8?q?refactor:=20BBLabel=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20(#587)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AccountSignInViewController.swift | 2 +- .../AccountDateViewController.swift | 6 +- .../AccountNicknameViewController.swift | 6 +- .../AccountProfileViewController.swift | 2 +- .../Calendar/View/Cell/CalendarCell.swift | 4 +- .../View/Cell/CalendarImageCell.swift | 2 +- .../Calendar/View/Cell/CalendarPostCell.swift | 4 +- .../Camera/CameraDisplayViewController.swift | 2 +- .../Camera/CameraViewController.swift | 2 +- .../CellReactor/DisplayEditCellReactor.swift | 4 +- .../Cell/DisplayEditCollectionViewCell.swift | 2 +- .../View/AirplaneLottieView.swift | 2 +- .../View/BibbiFetchFailureView.swift | 4 +- .../View/BlurAiraplaneLottieView.swift | 2 +- .../View/Cell/FamilyMemberProfileCell.swift | 6 +- .../View/InvitationUrlContainerView.swift | 4 +- .../FamilyManagementViewController.swift | 4 +- .../ViewControllers/MainViewController.swift | 2 +- .../Presentation/Home/Views/BalloonView.swift | 4 +- .../Home/Views/ContributorProfileView.swift | 6 +- .../Home/Views/ContributorView.swift | 4 +- .../Home/Views/InviteFamilyView.swift | 4 +- .../Views/MainFamilyCollectionViewCell.swift | 2 +- .../Views/MainPostCollectionViewCell.swift | 4 +- .../Presentation/Home/Views/TimerView.swift | 2 +- .../JoinFamilyViewController.swift | 2 +- .../JoinedFamilyViewController.swift | 4 +- .../JoinFamily/Views/MakeNewFamilyView.swift | 4 +- .../OnBoarding/OnBoardingViewCell.swift | 2 +- .../PostComment/View/Cell/CommentCell.swift | 8 +- .../PostComment/View/NoCommentLabel.swift | 4 +- .../View/PostCommentTopBarView.swift | 2 +- .../ReactionMembersViewController.swift | 2 +- .../Views/AddCommentCollectionViewCell.swift | 2 +- .../PostDetail/Views/MissionTextView.swift | 2 +- .../Views/PostDetailCollectionViewCell.swift | 4 +- .../PostDetail/Views/PostNavigationView.swift | 6 +- .../Views/ReactionCollectionViewCell.swift | 2 +- .../PrivacyAuthorizationTableViewCell.swift | 2 +- .../Privacy/Cell/PrivacyTableViewCell.swift | 4 +- ...PrivacyAuthorizationHeaderFooterView.swift | 2 +- .../PrivacyHeaderFooterView.swift | 2 +- .../Cell/ProfileFeedCollectionViewCell.swift | 6 +- .../Cell/ProfileFeedDescrptionCell.swift | 2 +- .../ProfileFeedEmptyCollectionViewCell.swift | 2 +- .../Profile/ProfileDetailViewController.swift | 2 +- .../Resign/AccountResignViewCotroller.swift | 6 +- .../BBAlertBuilder/BibbiAlertAction.swift | 2 +- .../BBAlertBuilder/BibbiAlertBuilder.swift | 8 +- .../BBAlertBuilder/BibbiAlertTitle.swift | 2 +- .../BibbiAlertViewController.swift | 4 +- .../BBCommons/BBButton/BibbiButton.swift | 6 +- .../Bibbi/BBCommons/BBLabel/BBFontStyle.swift | 172 ++++++++++++++++++ .../{BibbiLabel.swift => BBLabel.swift} | 18 +- .../BBNavigationBarView.swift | 4 +- .../BibbiNavigationBarView.swift | 2 +- .../BibbiToastMessageView.swift | 2 +- .../DescriptionPopoverViewController.swift | 2 +- .../ShareView/BibbiInquireBannerView.swift | 4 +- .../ShareView/BibbiMissionView.swift | 2 +- .../ShareView/BibbiProfileView.swift | 4 +- 61 files changed, 284 insertions(+), 106 deletions(-) create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBLabel/BBFontStyle.swift rename 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBLabel/{BibbiLabel.swift => BBLabel.swift} (82%) 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 e16157833..8a38d93bf 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignIn/AccountSignInViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignIn/AccountSignInViewController.swift @@ -29,7 +29,7 @@ public final class AccountSignInViewController: BaseViewController { - private let titleLabel = BibbiLabel(.head2Bold, textColor: .gray300) + private let titleLabel = BBLabel(.head2Bold, textColor: .gray300) private let yearInputFieldView = UITextField() private let yearLabel = UILabel() @@ -30,12 +30,12 @@ final class AccountDateViewController: BaseViewController private let fieldStackView = UIStackView() - private let errorLabel = BibbiLabel(.body1Regular, textColor: .warningRed) + private let errorLabel = BBLabel(.body1Regular, textColor: .warningRed) private let errorImage = UIImageView() private let errorStackView = UIStackView() private let nextButton = UIButton() - private let descLabel = BibbiLabel(.body1Regular, textColor: .gray400) + private let descLabel = BBLabel(.body1Regular, textColor: .gray400) private let infoCircleFill = DesignSystemAsset.infoCircleFill.image .withRenderingMode(.alwaysTemplate) diff --git a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountNicknameViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountNicknameViewController.swift index 0cf3e4022..c4a64123a 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountNicknameViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountNicknameViewController.swift @@ -18,13 +18,13 @@ import Then fileprivate typealias _Str = AccountSignUpStrings.Nickname public final class AccountNicknameViewController: BaseViewController { // MARK: SubViews - private let titleLabel = BibbiLabel(.head2Bold, textColor: .gray300) + private let titleLabel = BBLabel(.head2Bold, textColor: .gray300) private let inputFielView = UITextField() - private let errorLabel = BibbiLabel(.body1Regular, textColor: .warningRed) + private let errorLabel = BBLabel(.body1Regular, textColor: .warningRed) private let errorImage = UIImageView() private let errorStackView = UIStackView() private let nextButton = UIButton() - private let descLabel = BibbiLabel(.body1Regular, textColor: .gray400) + private let descLabel = BBLabel(.body1Regular, textColor: .gray400) private let infoCircleFill = DesignSystemAsset.infoCircleFill.image .withRenderingMode(.alwaysTemplate) diff --git a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountProfileViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountProfileViewController.swift index 1ee8ccf5a..6d09dd963 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountProfileViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountProfileViewController.swift @@ -20,7 +20,7 @@ import Then fileprivate typealias _Str = AccountSignUpStrings.Profile final class AccountProfileViewController: BaseViewController { // MARK: SubViews - private let titleLabel = BibbiLabel(.head2Bold, textAlignment: .center, textColor: .gray300) + private let titleLabel = BBLabel(.head2Bold, textAlignment: .center, textColor: .gray300) private let profileButton = UIButton() private var pickerConfiguration: PHPickerConfiguration = { diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarCell.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarCell.swift index 8f18abea5..a9f327259 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarCell.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarCell.swift @@ -24,8 +24,8 @@ final class CalendarCell: BaseCollectionViewCell { // MARK: - Views private lazy var labelStack: UIStackView = UIStackView() - private let titleLabel: BibbiLabel = BibbiLabel(.head2Bold, textAlignment: .center, textColor: .gray200) - private let countLabel: BibbiLabel = BibbiLabel(.body1Regular, textColor: .gray200) + private let titleLabel: BBLabel = BBLabel(.head2Bold, textAlignment: .center, textColor: .gray200) + private let countLabel: BBLabel = BBLabel(.body1Regular, textColor: .gray200) private let infoButton: UIButton = UIButton(type: .system) private lazy var bannerView: BannerView = BannerView(viewModel: bannerViewModel) diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarImageCell.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarImageCell.swift index f5cf31849..d2b6db359 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarImageCell.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarImageCell.swift @@ -22,7 +22,7 @@ final public class CalendarImageCell: FSCalendarCell, ReactorKit.View { static let id: String = "ImageCalendarCell" // MARK: - Views - private let dayLabel: BibbiLabel = BibbiLabel(.body1Regular, textAlignment: .center) + private let dayLabel: BBLabel = BBLabel(.body1Regular, textAlignment: .center) private let containerView: UIView = UIView() private let thumbnailView: UIImageView = UIImageView() private let todayStrokeView: UIView = UIView() diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarPostCell.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarPostCell.swift index 7e2c27a0a..32cfba83b 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarPostCell.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarPostCell.swift @@ -25,8 +25,8 @@ final class CalendarPostCell: BaseCollectionViewCell { private let authorStackView: UIStackView = UIStackView() private let authorImageContainerView: UIView = UIView() private let authorImageView: UIImageView = UIImageView() - private let authorNameLabel: BibbiLabel = BibbiLabel(.caption, textColor: .gray200) - private let authorFirstNameLabel: BibbiLabel = BibbiLabel(.caption, textColor: .bibbiWhite) + private let authorNameLabel: BBLabel = BBLabel(.caption, textColor: .gray200) + private let authorFirstNameLabel: BBLabel = BBLabel(.caption, textColor: .bibbiWhite) private let postImageView: UIImageView = UIImageView() private let missionTextView: MissionTextView = MissionTextView() private let contentCollectionView: UICollectionView = UICollectionView( diff --git a/14th-team5-iOS/App/Sources/Presentation/Camera/CameraDisplayViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Camera/CameraDisplayViewController.swift index 939a5a71f..1f87b607c 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Camera/CameraDisplayViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Camera/CameraDisplayViewController.swift @@ -24,7 +24,7 @@ public final class CameraDisplayViewController: BaseViewController { private let cameraIndicatorView: BibbiLoadingView = BibbiLoadingView() private let filterView: UIImageView = UIImageView() private let zoomView: UIButton = UIButton() - private let realEmojiDescriptionLabel = BibbiLabel(.body1Regular, textColor: .mainYellow) + private let realEmojiDescriptionLabel = BBLabel(.body1Regular, textColor: .mainYellow) private let realEmojiFaceView = UIView() private let realEmojiFaceImageView = UIImageView() private let realEmojiHorizontalStakView = UIStackView() diff --git a/14th-team5-iOS/App/Sources/Presentation/Camera/Cell/CellReactor/DisplayEditCellReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Camera/Cell/CellReactor/DisplayEditCellReactor.swift index c824588e4..04ce9d593 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Camera/Cell/CellReactor/DisplayEditCellReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Camera/Cell/CellReactor/DisplayEditCellReactor.swift @@ -21,11 +21,11 @@ public final class DisplayEditCellReactor: Reactor { public struct State { var title: String var radius: CGFloat - var font: UIFont.PretendardStyle + var font: BBFontStyle } - init(title: String, radius: CGFloat, font: UIFont.PretendardStyle) { + init(title: String, radius: CGFloat, font: BBFontStyle) { self.initialState = State(title: title, radius: radius, font: font) } diff --git a/14th-team5-iOS/App/Sources/Presentation/Camera/Cell/DisplayEditCollectionViewCell.swift b/14th-team5-iOS/App/Sources/Presentation/Camera/Cell/DisplayEditCollectionViewCell.swift index 0a11ccb32..316b8de1a 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Camera/Cell/DisplayEditCollectionViewCell.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Camera/Cell/DisplayEditCollectionViewCell.swift @@ -19,7 +19,7 @@ import Then public final class DisplayEditCollectionViewCell: BaseCollectionViewCell { static let id = "DisplayEditCollectionViewCell" - private var descriptionLabel: BibbiLabel = BibbiLabel(.head1, textAlignment: .center) + private var descriptionLabel: BBLabel = BBLabel(.head1, textAlignment: .center) private let blurContainerView: UIVisualEffectView = UIVisualEffectView.makeBlurView(style: .dark) diff --git a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/View/AirplaneLottieView.swift b/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/View/AirplaneLottieView.swift index 5a57d7d1b..ad56dc066 100644 --- a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/View/AirplaneLottieView.swift +++ b/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/View/AirplaneLottieView.swift @@ -17,7 +17,7 @@ public class AirplaneLottieView: UIView { // MARK: - Views private let lottieStack: UIStackView = UIStackView() private let lottieView: LottieView = LottieView(with: .loading) - private let loadingLabel: BibbiLabel = BibbiLabel(.body1Regular, textAlignment: .center, textColor: .gray500) + private let loadingLabel: BBLabel = BBLabel(.body1Regular, textAlignment: .center, textColor: .gray500) // MARK: - Properties public var lottieSize: CGFloat = 80 { diff --git a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/View/BibbiFetchFailureView.swift b/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/View/BibbiFetchFailureView.swift index 62073c385..5cba1d616 100644 --- a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/View/BibbiFetchFailureView.swift +++ b/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/View/BibbiFetchFailureView.swift @@ -23,8 +23,8 @@ final public class BibbiFetchFailureView: UIView { // MARK: - Views private let emptyImageView: UIImageView = UIImageView() private let labelStack: UIStackView = UIStackView() - private let mainLabel: BibbiLabel = BibbiLabel(.body1Regular, textAlignment: .center, textColor: .gray300) - private let subLabel: BibbiLabel = BibbiLabel(.body2Regular, textAlignment: .center, textColor: .gray500) + private let mainLabel: BBLabel = BBLabel(.body1Regular, textAlignment: .center, textColor: .gray300) + private let subLabel: BBLabel = BBLabel(.body2Regular, textAlignment: .center, textColor: .gray500) // MARK: - Properties private var type: FailureType? diff --git a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/View/BlurAiraplaneLottieView.swift b/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/View/BlurAiraplaneLottieView.swift index a4a756246..e8560e306 100644 --- a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/View/BlurAiraplaneLottieView.swift +++ b/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/View/BlurAiraplaneLottieView.swift @@ -18,7 +18,7 @@ final public class BlurAiraplaneLottieView: UIView { private let lottieStack: UIStackView = UIStackView() private let lottieView: LottieView = LottieView(with: .loading) - private let loadingLabel: BibbiLabel = BibbiLabel(.body1Regular, textAlignment: .center, textColor: .gray500) + private let loadingLabel: BBLabel = BBLabel(.body1Regular, textAlignment: .center, textColor: .gray500) // MARK: - Properties public var containerSize: CGFloat = 125 { diff --git a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/View/Cell/FamilyMemberProfileCell.swift b/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/View/Cell/FamilyMemberProfileCell.swift index b27eec828..fba1aac5b 100644 --- a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/View/Cell/FamilyMemberProfileCell.swift +++ b/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/View/Cell/FamilyMemberProfileCell.swift @@ -17,13 +17,13 @@ import Then final class FamilyMemberProfileCell: BaseTableViewCell { // MARK: - Views private let containerView: UIView = UIView() - private let firstNameLabel: BibbiLabel = BibbiLabel(.head2Bold, textAlignment: .center, textColor: .gray200) + private let firstNameLabel: BBLabel = BBLabel(.head2Bold, textAlignment: .center, textColor: .gray200) private let profileImageView: UIImageView = UIImageView() private let dayOfBirthBadgeView: UIImageView = UIImageView() private let labelStack: UIStackView = UIStackView() - private let nameLabel: BibbiLabel = BibbiLabel(.body1Regular, textColor: .gray200) - private let isMeLabel: BibbiLabel = BibbiLabel(.body2Regular, textColor: .gray500) + private let nameLabel: BBLabel = BBLabel(.body1Regular, textColor: .gray200) + private let isMeLabel: BBLabel = BBLabel(.body2Regular, textColor: .gray500) private let rightArrowImageView: UIImageView = UIImageView() diff --git a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/View/InvitationUrlContainerView.swift b/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/View/InvitationUrlContainerView.swift index 626b772a4..9d52c8a6a 100644 --- a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/View/InvitationUrlContainerView.swift +++ b/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/View/InvitationUrlContainerView.swift @@ -16,8 +16,8 @@ public final class InvitationUrlContainerView: BaseView, UICollectio private let familyViewController: MainFamilyViewController = MainFamilyViewControllerWrapper().makeViewController() private let timerView: TimerView = TimerView(reactor: TimerReactor()) - private let descriptionLabel: BibbiLabel = BibbiLabel(.body2Regular, textAlignment: .center, textColor: .gray300) + private let descriptionLabel: BBLabel = BBLabel(.body2Regular, textAlignment: .center, textColor: .gray300) private let imageView: UIImageView = UIImageView() private let contributorView: ContributorView = ContributorView(reactor: ContributorReactor()) diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Views/BalloonView.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Views/BalloonView.swift index dc17af52c..78ede6532 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Views/BalloonView.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Views/BalloonView.swift @@ -17,7 +17,7 @@ final class BalloonView: BaseView { private let containerView: UIView = UIView() private let polygonImageView: UIImageView = UIImageView() private let stackView: UIStackView = UIStackView() - private let textLabel: UILabel = BibbiLabel(.body2Regular) + private let textLabel: UILabel = BBLabel(.body2Regular) let balloonTypeRelay: BehaviorRelay = BehaviorRelay(value: .normal) @@ -157,7 +157,7 @@ extension BalloonView { } private func makeLabel(name: String){ - let label = BibbiLabel(.caption2, textAlignment: .center, textColor: .gray200) + let label = BBLabel(.caption2, textAlignment: .center, textColor: .gray200) label.backgroundColor = .gray800 label.layer.borderWidth = 2 label.layer.borderColor = UIColor.mainYellow.cgColor diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Views/ContributorProfileView.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Views/ContributorProfileView.swift index 7c896636e..66569d801 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Views/ContributorProfileView.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Views/ContributorProfileView.swift @@ -16,9 +16,9 @@ import RxCocoa import RxSwift final class ContributorProfileView: BaseView { - private let nameLabel = BibbiLabel(.body2Bold, textAlignment: .center, textColor: .gray200) - private let countLabel = BibbiLabel(.body2Regular, textAlignment: .center, textColor: .gray300) - private let defaultNameLabel = BibbiLabel(.head1, textAlignment: .center, textColor: .gray200) + private let nameLabel = BBLabel(.body2Bold, textAlignment: .center, textColor: .gray200) + private let countLabel = BBLabel(.body2Regular, textAlignment: .center, textColor: .gray300) + private let defaultNameLabel = BBLabel(.head1, textAlignment: .center, textColor: .gray200) private let imageView = UIImageView() private let questionView = UIImageView() private let badgeView = UIImageView() diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Views/ContributorView.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Views/ContributorView.swift index 3688dcb68..d5f3accff 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Views/ContributorView.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Views/ContributorView.swift @@ -17,9 +17,9 @@ import RxCocoa final class ContributorView: BaseView { private let containerView: UIView = UIView() - private let titleLabel: BibbiLabel = BibbiLabel(.head2Bold, textColor: .gray200) + private let titleLabel: BBLabel = BBLabel(.head2Bold, textColor: .gray200) let infoButton: UIButton = UIButton() - private let subTitleLabel: BibbiLabel = BibbiLabel(.body2Regular, textColor: .gray300) + private let subTitleLabel: BBLabel = BBLabel(.body2Regular, textColor: .gray300) private let firstProfileView: ContributorProfileView = ContributorProfileView(reactor: ContributorProfileReactor(initialState: .init(rank: .first))) private let secondProfileView: ContributorProfileView = ContributorProfileView(reactor: ContributorProfileReactor(initialState: .init(rank: .second))) diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Views/InviteFamilyView.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Views/InviteFamilyView.swift index ccd8e9cbe..777bb6cd7 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Views/InviteFamilyView.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Views/InviteFamilyView.swift @@ -33,8 +33,8 @@ final class InviteFamilyView: UIView { private let inviteImageView: UIImageView = UIImageView() private let labelStack: UIStackView = UIStackView() - private let subLabel: UILabel = BibbiLabel(.body2Regular) - private let titleLabel: UILabel = BibbiLabel(.head2Bold) + private let subLabel: UILabel = BBLabel(.body2Regular) + private let titleLabel: UILabel = BBLabel(.head2Bold) private let nextIconImageView: UIImageView = UIImageView() private var openType: InviteType diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Views/MainFamilyCollectionViewCell.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Views/MainFamilyCollectionViewCell.swift index d7d54c2ad..6ac172dc1 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Views/MainFamilyCollectionViewCell.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Views/MainFamilyCollectionViewCell.swift @@ -17,7 +17,7 @@ final class MainFamilyCollectionViewCell: BaseCollectionViewCell { private let dividerView: UIView = UIView() - private let timerLabel: BibbiLabel = BibbiLabel(.head1, textAlignment: .center) + private let timerLabel: BBLabel = BBLabel(.head1, textAlignment: .center) override func bind(reactor: TimerReactor) { bindInput(reactor: reactor) diff --git a/14th-team5-iOS/App/Sources/Presentation/JoinFamily/ViewController/JoinFamilyViewController.swift b/14th-team5-iOS/App/Sources/Presentation/JoinFamily/ViewController/JoinFamilyViewController.swift index 31f5590aa..3a8d9bfb8 100644 --- a/14th-team5-iOS/App/Sources/Presentation/JoinFamily/ViewController/JoinFamilyViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/JoinFamily/ViewController/JoinFamilyViewController.swift @@ -20,7 +20,7 @@ import Then fileprivate typealias _Str = JoinFamilyStrings final class JoinFamilyViewController: BaseViewController { // MARK: - Views - private let titleLabel: BibbiLabel = BibbiLabel(.head1, textColor: .gray100) + private let titleLabel: BBLabel = BBLabel(.head1, textColor: .gray100) private let joinFamilyButton: InviteFamilyView = InviteFamilyView(openType: .inviteUrl) private let makeFamilyButton: MakeNewFamilyView = MakeNewFamilyView() diff --git a/14th-team5-iOS/App/Sources/Presentation/JoinFamily/ViewController/JoinedFamilyViewController.swift b/14th-team5-iOS/App/Sources/Presentation/JoinFamily/ViewController/JoinedFamilyViewController.swift index 069c92daa..9271a3724 100644 --- a/14th-team5-iOS/App/Sources/Presentation/JoinFamily/ViewController/JoinedFamilyViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/JoinFamily/ViewController/JoinedFamilyViewController.swift @@ -18,8 +18,8 @@ import Then fileprivate typealias _Str = JoinedFamilyStrings final class JoinedFamilyViewController: BaseViewController { - private let titleLabel = BibbiLabel(.head1, textColor: .gray100) - private let captionLabel = BibbiLabel(.body1Regular, textColor: .gray300) + private let titleLabel = BBLabel(.head1, textColor: .gray100) + private let captionLabel = BBLabel(.body1Regular, textColor: .gray300) private let labelStack = UIStackView() private let imageView = UIImageView() diff --git a/14th-team5-iOS/App/Sources/Presentation/JoinFamily/Views/MakeNewFamilyView.swift b/14th-team5-iOS/App/Sources/Presentation/JoinFamily/Views/MakeNewFamilyView.swift index 754529cc7..1e72e99ab 100644 --- a/14th-team5-iOS/App/Sources/Presentation/JoinFamily/Views/MakeNewFamilyView.swift +++ b/14th-team5-iOS/App/Sources/Presentation/JoinFamily/Views/MakeNewFamilyView.swift @@ -11,8 +11,8 @@ import DesignSystem final class MakeNewFamilyView: UIView { private let imageView = UIImageView() - private let titleLabel = BibbiLabel(.body2Regular) - private let captionLabel = BibbiLabel(.head2Bold) + private let titleLabel = BBLabel(.body2Regular) + private let captionLabel = BBLabel(.head2Bold) private let labelStack = UIStackView() override init(frame: CGRect) { diff --git a/14th-team5-iOS/App/Sources/Presentation/OnBoarding/OnBoardingViewCell.swift b/14th-team5-iOS/App/Sources/Presentation/OnBoarding/OnBoardingViewCell.swift index f51c6b200..e06faf8cd 100644 --- a/14th-team5-iOS/App/Sources/Presentation/OnBoarding/OnBoardingViewCell.swift +++ b/14th-team5-iOS/App/Sources/Presentation/OnBoarding/OnBoardingViewCell.swift @@ -12,7 +12,7 @@ import DesignSystem final class OnBoardingCollectionViewCell: BaseCollectionViewCell { static let id = "onBoardingCollectionViewCell" - private let titleLabel = BibbiLabel(.head1, textColor: .bibbiBlack) + private let titleLabel = BBLabel(.head1, textColor: .bibbiBlack) private let imageView = UIImageView() private let screenSize = UIApplication.shared.connectedScenes diff --git a/14th-team5-iOS/App/Sources/Presentation/PostComment/View/Cell/CommentCell.swift b/14th-team5-iOS/App/Sources/Presentation/PostComment/View/Cell/CommentCell.swift index e373a1231..069fc951c 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostComment/View/Cell/CommentCell.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostComment/View/Cell/CommentCell.swift @@ -17,15 +17,15 @@ import Then final public class CommentCell: BaseTableViewCell { // MARK: - Views private let containerView: UIView = UIView() - private let firstNameLabel: UILabel = BibbiLabel(.head2Bold, textAlignment: .center) + private let firstNameLabel: UILabel = BBLabel(.head2Bold, textAlignment: .center) private let profileImageView: UIImageView = UIImageView() private let profileButton: UIButton = UIButton() private let userNameStack: UIStackView = UIStackView() - private let userNameLabel: BibbiLabel = BibbiLabel(.body2Bold, textColor: .gray100) - private let createdAtLabel: BibbiLabel = BibbiLabel(.body2Regular, textColor: .gray500) + private let userNameLabel: BBLabel = BBLabel(.body2Bold, textColor: .gray100) + private let createdAtLabel: BBLabel = BBLabel(.body2Regular, textColor: .gray500) - private let commentLabel: BibbiLabel = BibbiLabel(.body1Regular, textColor: .gray100) + private let commentLabel: BBLabel = BBLabel(.body1Regular, textColor: .gray100) // MARK: - Properties static var id: String = "CommentCell" diff --git a/14th-team5-iOS/App/Sources/Presentation/PostComment/View/NoCommentLabel.swift b/14th-team5-iOS/App/Sources/Presentation/PostComment/View/NoCommentLabel.swift index b2ba254e8..e5e56f794 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostComment/View/NoCommentLabel.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostComment/View/NoCommentLabel.swift @@ -14,8 +14,8 @@ import SnapKit final class NoCommentLabel: UIView { // MARK: - Views private let labelStack: UIStackView = UIStackView() - private let mainLabel: BibbiLabel = BibbiLabel(.body1Bold, textAlignment: .center) - private let subLabel: BibbiLabel = BibbiLabel(.body2Regular, textAlignment: .center, textColor: .gray500) + private let mainLabel: BBLabel = BBLabel(.body1Bold, textAlignment: .center) + private let subLabel: BBLabel = BBLabel(.body2Regular, textAlignment: .center, textColor: .gray500) // MARK: - Intializer override init(frame: CGRect) { diff --git a/14th-team5-iOS/App/Sources/Presentation/PostComment/View/PostCommentTopBarView.swift b/14th-team5-iOS/App/Sources/Presentation/PostComment/View/PostCommentTopBarView.swift index c4f9f885a..5b6ada112 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostComment/View/PostCommentTopBarView.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostComment/View/PostCommentTopBarView.swift @@ -15,7 +15,7 @@ final class PostCommentTopBarView: UIView { // MARK: - Views private let grabber: UIView = UIView() - private let titleLabel: BibbiLabel = BibbiLabel(.body1Bold) + private let titleLabel: BBLabel = BBLabel(.body1Bold) private let barDividerView: UIView = UIView() // MARK: - Intializer diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/ReactionMembersViewController.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/ReactionMembersViewController.swift index 3e38ade3f..34b6e4144 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/ReactionMembersViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/ReactionMembersViewController.swift @@ -17,7 +17,7 @@ final class ReactionMembersViewController: BaseViewController { typealias Layout = PostAutoLayout.NavigationView - private let defaultNameLabel: UILabel = BibbiLabel(.head1, textColor: .gray200) + private let defaultNameLabel: UILabel = BBLabel(.head1, textColor: .gray200) private let backButton: UIButton = UIButton() private let profileImageView: UIImageView = UIImageView() - private let nameLabel: UILabel = BibbiLabel(.body1Regular, textColor: .gray100) - private let dateLabel: UILabel = BibbiLabel(.caption, textColor: .gray400) + private let nameLabel: UILabel = BBLabel(.body1Regular, textColor: .gray100) + private let dateLabel: UILabel = BBLabel(.caption, textColor: .gray400) convenience init(reactor: Reactor? = nil) { self.init(frame: .zero) diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/ReactionCollectionViewCell.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/ReactionCollectionViewCell.swift index 2c25a1b21..79730dbe5 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/ReactionCollectionViewCell.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/ReactionCollectionViewCell.swift @@ -20,7 +20,7 @@ final class ReactionCollectionViewCell: BaseCollectionViewCell private let emojiImageView = UIImageView() private let badgeView = UIImageView() - private let countLabel = BibbiLabel(.body2Regular, textAlignment: .left) + private let countLabel = BBLabel(.body2Regular, textAlignment: .left) convenience init(reacter: TempCellReactor? = nil) { self.init(frame: .zero) diff --git a/14th-team5-iOS/App/Sources/Presentation/Privacy/Cell/PrivacyAuthorizationTableViewCell.swift b/14th-team5-iOS/App/Sources/Presentation/Privacy/Cell/PrivacyAuthorizationTableViewCell.swift index e87f67fc4..a9eb063a9 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Privacy/Cell/PrivacyAuthorizationTableViewCell.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Privacy/Cell/PrivacyAuthorizationTableViewCell.swift @@ -19,7 +19,7 @@ import Then public final class PrivacyAuthorizationTableViewCell: BaseTableViewCell { //MARK: Views - private let descrptionLabel: BibbiLabel = BibbiLabel(.head2Regular, textColor: .warningRed) + private let descrptionLabel: BBLabel = BBLabel(.head2Regular, textColor: .warningRed) private let arrowAccessView: UIImageView = UIImageView() diff --git a/14th-team5-iOS/App/Sources/Presentation/Privacy/Cell/PrivacyTableViewCell.swift b/14th-team5-iOS/App/Sources/Presentation/Privacy/Cell/PrivacyTableViewCell.swift index dd1555b30..839c6dfe2 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Privacy/Cell/PrivacyTableViewCell.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Privacy/Cell/PrivacyTableViewCell.swift @@ -17,8 +17,8 @@ import Then public final class PrivacyTableViewCell: BaseTableViewCell { //MARK: Views - private let descrptionLabel: BibbiLabel = BibbiLabel(.head2Regular, textColor: .gray200) - private let versionLabel: BibbiLabel = BibbiLabel(.caption, textColor: .gray200) + private let descrptionLabel: BBLabel = BBLabel(.head2Regular, textColor: .gray200) + private let versionLabel: BBLabel = BBLabel(.caption, textColor: .gray200) private let arrowAccessView: UIImageView = UIImageView() diff --git a/14th-team5-iOS/App/Sources/Presentation/Privacy/ReusableView/PrivacyAuthorizationHeaderFooterView.swift b/14th-team5-iOS/App/Sources/Presentation/Privacy/ReusableView/PrivacyAuthorizationHeaderFooterView.swift index 6e87cf2df..044592ece 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Privacy/ReusableView/PrivacyAuthorizationHeaderFooterView.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Privacy/ReusableView/PrivacyAuthorizationHeaderFooterView.swift @@ -13,7 +13,7 @@ import Then public final class PrivacyAuthorizationHeaderFooterView: UITableViewHeaderFooterView { - private let headerLabel: BibbiLabel = BibbiLabel(.body2Regular, textColor: .gray300) + private let headerLabel: BBLabel = BBLabel(.body2Regular, textColor: .gray300) public override init(reuseIdentifier: String?) { super.init(reuseIdentifier: reuseIdentifier) diff --git a/14th-team5-iOS/App/Sources/Presentation/Privacy/ReusableView/PrivacyHeaderFooterView.swift b/14th-team5-iOS/App/Sources/Presentation/Privacy/ReusableView/PrivacyHeaderFooterView.swift index 04db16da3..f13a2e33c 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Privacy/ReusableView/PrivacyHeaderFooterView.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Privacy/ReusableView/PrivacyHeaderFooterView.swift @@ -14,7 +14,7 @@ import Then public final class PrivacyHeaderFooterView: UITableViewHeaderFooterView { - private let headerLabel: BibbiLabel = BibbiLabel(.body2Regular, textColor: .gray300) + private let headerLabel: BBLabel = BBLabel(.body2Regular, textColor: .gray300) public override init(reuseIdentifier: String?) { diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/Cell/ProfileFeedCollectionViewCell.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/Cell/ProfileFeedCollectionViewCell.swift index 398e884dc..2d1ced97c 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/Cell/ProfileFeedCollectionViewCell.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/Cell/ProfileFeedCollectionViewCell.swift @@ -23,10 +23,10 @@ public final class ProfileFeedCollectionViewCell: BaseCollectionViewCell = .init { dataSources, collectionView, indexPath, sectionItem in diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/Cell/ProfileFeedDescrptionCell.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/Cell/ProfileFeedDescrptionCell.swift index b7a6705bc..45e493ceb 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/Cell/ProfileFeedDescrptionCell.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/Cell/ProfileFeedDescrptionCell.swift @@ -16,7 +16,7 @@ import Then final class ProfileFeedDescrptionCell: BaseCollectionViewCell { - private let descrptionLabel: BibbiLabel = BibbiLabel(.caption2, textAlignment: .center) + private let descrptionLabel: BBLabel = BBLabel(.caption2, textAlignment: .center) private let blurContainerView: UIVisualEffectView = UIVisualEffectView.makeBlurView(style: .dark) diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/Cell/ProfileFeedEmptyCollectionViewCell.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/Cell/ProfileFeedEmptyCollectionViewCell.swift index a8c6b5b57..0ada28ebc 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/Cell/ProfileFeedEmptyCollectionViewCell.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/Cell/ProfileFeedEmptyCollectionViewCell.swift @@ -18,7 +18,7 @@ final class ProfileFeedEmptyCollectionViewCell: BaseCollectionViewCell { private let profileImageView: UIImageView = UIImageView() - private let nickNameLabel: BibbiLabel = BibbiLabel(.head1, textAlignment: .center, textColor: .gray200) + private let nickNameLabel: BBLabel = BBLabel(.head1, textAlignment: .center, textColor: .gray200) private let blurView: UIVisualEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .systemChromeMaterialDark)) override func viewDidLoad() { diff --git a/14th-team5-iOS/App/Sources/Presentation/Resign/AccountResignViewCotroller.swift b/14th-team5-iOS/App/Sources/Presentation/Resign/AccountResignViewCotroller.swift index 09b3cfeae..008215378 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Resign/AccountResignViewCotroller.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Resign/AccountResignViewCotroller.swift @@ -18,9 +18,9 @@ import Then final class AccountResignViewCotroller: BaseViewController { //TODO: 텍스트 컬러, 폰트 정해지면 수정 - private let resignDesrptionLabel: BibbiLabel = BibbiLabel(.body1Regular, textColor: .gray400) - private let resignReasonLabel: BibbiLabel = BibbiLabel(.head1, textColor: .gray200) - private let resignExampleLabel: BibbiLabel = BibbiLabel(.body1Regular, textColor: .gray400) + private let resignDesrptionLabel: BBLabel = BBLabel(.body1Regular, textColor: .gray400) + private let resignReasonLabel: BBLabel = BBLabel(.head1, textColor: .gray200) + private let resignExampleLabel: BBLabel = BBLabel(.body1Regular, textColor: .gray400) private let resignIndicatorView: UIActivityIndicatorView = UIActivityIndicatorView(style: .medium) private let confirmButton: UIButton = UIButton() private let bibbiTermsView: BibbiCheckBoxView = BibbiCheckBoxView(frame: .zero) diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertBuilder/BibbiAlertAction.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertBuilder/BibbiAlertAction.swift index 838a9f501..6c0f238d9 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertBuilder/BibbiAlertAction.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertBuilder/BibbiAlertAction.swift @@ -12,6 +12,6 @@ struct BibbiAlertAction { var text: String? var textColor: UIColor? var backgroundColor: UIColor? - var fontStlye: BibbiFontStyle? + var fontStlye: BBFontStyle? var action: Action } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertBuilder/BibbiAlertBuilder.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertBuilder/BibbiAlertBuilder.swift index 7631d024b..d57314e73 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertBuilder/BibbiAlertBuilder.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertBuilder/BibbiAlertBuilder.swift @@ -36,7 +36,7 @@ public final class BibbiAlertBuilder { public func setMainTitle( _ text: String? = nil, textColor: UIColor = .gray100, - fontStyle: BibbiFontStyle = .head2Bold + fontStyle: BBFontStyle = .head2Bold ) -> Self { mainTitle = BibbiAlertTitle( text: text != nil ? text : alertStyle?.mainTitle, @@ -49,7 +49,7 @@ public final class BibbiAlertBuilder { public func setSubTitle( _ text: String? = nil, textColor: UIColor = .gray300, - fontStyle: BibbiFontStyle = .body2Regular + fontStyle: BBFontStyle = .body2Regular ) -> Self { subTitle = BibbiAlertTitle( text: text != nil ? text : alertStyle?.subTitle, @@ -68,7 +68,7 @@ public final class BibbiAlertBuilder { _ text: String? = nil, textColor: UIColor = .bibbiBlack, backgroundColor: UIColor = .mainYellow, - fontStyle: BibbiFontStyle = .body1Bold, + fontStyle: BBFontStyle = .body1Bold, action: Action = nil ) -> Self { confirmAction = BibbiAlertAction( @@ -85,7 +85,7 @@ public final class BibbiAlertBuilder { _ text: String? = nil, textColor: UIColor = .gray400, backgroundColor: UIColor = .gray700, - fontStyle: BibbiFontStyle = .body1Bold, + fontStyle: BBFontStyle = .body1Bold, action: Action = nil ) -> Self { cancelAction = BibbiAlertAction( diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertBuilder/BibbiAlertTitle.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertBuilder/BibbiAlertTitle.swift index 31e580eb3..38e76818a 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertBuilder/BibbiAlertTitle.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertBuilder/BibbiAlertTitle.swift @@ -10,5 +10,5 @@ import UIKit struct BibbiAlertTitle { var text: String? var textColor: UIColor? - var fontStyle: BibbiFontStyle? + var fontStyle: BBFontStyle? } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertBuilder/BibbiAlertViewController.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertBuilder/BibbiAlertViewController.swift index 874f8fbbc..cd81bff95 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertBuilder/BibbiAlertViewController.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertBuilder/BibbiAlertViewController.swift @@ -17,8 +17,8 @@ public final class BibbiAlertViewController: UIViewController { private var containerView: UIView = UIView() private var alertStackView: UIStackView = UIStackView() private var labelStackView: UIStackView = UIStackView() - private var subTitleLabel: BibbiLabel = BibbiLabel(.head2Bold, textAlignment: .center) - private var mainTitleLabel: BibbiLabel = BibbiLabel(.body2Regular, textAlignment: .center) + private var subTitleLabel: BBLabel = BBLabel(.head2Bold, textAlignment: .center) + private var mainTitleLabel: BBLabel = BBLabel(.body2Regular, textAlignment: .center) private var imageView: UIImageView = UIImageView() private var buttonStackView: UIStackView = UIStackView() private var confirmButton: BibbiButton = BibbiButton(type: .system) diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBButton/BibbiButton.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBButton/BibbiButton.swift index 6c3155c55..f57bc8895 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBButton/BibbiButton.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBButton/BibbiButton.swift @@ -12,7 +12,7 @@ import SnapKit final public class BibbiButton: UIButton { // MARK: - Views - public var bibbiTitleLabel: BibbiLabel = BibbiLabel() + public var bibbiTitleLabel: BBLabel = BBLabel() // MARK: - Properties public override var titleLabel: UILabel? { @@ -50,8 +50,8 @@ final public class BibbiButton: UIButton { titleLabel?.textColor = color } - public func setTitleFontStyle(_ fontStyle: BibbiFontStyle) { - if let titleLabel = titleLabel as? BibbiLabel { + public func setTitleFontStyle(_ fontStyle: BBFontStyle) { + if let titleLabel = titleLabel as? BBLabel { titleLabel.fontStyle = fontStyle } } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBLabel/BBFontStyle.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBLabel/BBFontStyle.swift new file mode 100644 index 000000000..8385aec8c --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBLabel/BBFontStyle.swift @@ -0,0 +1,172 @@ +// +// BBFontStyle.swift +// Core +// +// Created by 김건우 on 7/31/24. +// + +import UIKit + +import DesignSystem + +public typealias BBFontStyle = BBLabel.PretendardStyle +public typealias BBFontFamily = BBLabel.PretendardFamily +public typealias BBFontAttributes = BBLabel.Font.Attributes + +extension BBLabel { + + public typealias Attributes = Font.Attributes + public typealias PretendardStyle = Font.Pretendard.Style + public typealias PretendardFamily = Font.Pretendard.Family + + public enum Font { + + public enum Pretendard { + public enum Style { + case title + case head1 + case head2Bold + case head2Regular + case body1Bold + case body1Regular + case body2Bold + case body2Regular + case caption + case caption2 + } + + public enum Family { + case bold + case semiBold + case regular + } + } + + public enum Cafe24 { + public enum Style { + + } + + public enum Family { + + } + } + + public struct Attributes { + let size: CGFloat + let weight: BBFontFamily + let letterSpacing: CGFloat + let lineHeight: CGFloat + } + } + +} + + +extension BBLabel.Font.Attributes { + + public var font: DesignSystemFontConvertible { + switch weight { + case .bold: + return DesignSystemFontFamily.Pretendard.bold + case .semiBold: + return DesignSystemFontFamily.Pretendard.semiBold + case .regular: + return DesignSystemFontFamily.Pretendard.regular + } + } + +} + +extension BBLabel { + + public static func pretendard(_ textStyle: BBFontStyle) -> UIFont? { + let attributes = fontAttributes(textStyle) + return UIFont( + font: attributes.font, + size: attributes.size + ) + } + +} + +extension BBLabel { + + static func fontAttributes( + _ fontStyle: BBFontStyle + ) -> Attributes { + switch fontStyle { + case .title: + return Attributes( + size: 36.0, // 폰트 크기 + weight: .bold, // 폰트 굵기 + letterSpacing: -0.3, // 자간 (px) + lineHeight: 1.38 // 높이 (%) + ) + case .head1: + return Attributes( + size: 24.0, + weight: .bold, + letterSpacing: -0.3, + lineHeight: 1.38 + ) + case .head2Bold: + return Attributes( + size: 18.0, + weight: .semiBold, + letterSpacing: -0.3, + lineHeight: 1.40 + ) + case .head2Regular: + return Attributes( + size: 18.0, + weight: .regular, + letterSpacing: -0.3, + lineHeight: 1.50 + ) + case .body1Bold: + return Attributes( + size: 16.0, + weight: .semiBold, + letterSpacing: -0.3, + lineHeight: 1.50 + ) + case .body1Regular: + return Attributes( + size: 16.0, + weight: .regular, + letterSpacing: -0.3, + lineHeight: 1.50 + ) + case .body2Bold: + return Attributes( + size: 14.0, + weight: .semiBold, + letterSpacing: -0.3, + lineHeight: 1.40 + ) + case .body2Regular: + return Attributes( + size: 14.0, + weight: .regular, + letterSpacing: -0.3, + lineHeight: 1.40 + ) + case .caption: + return Attributes( + size: 12.0, + weight: .regular, + letterSpacing: -0.3, + lineHeight: 1.50 + ) + case .caption2: + return Attributes( + size: 12, + weight: .bold, + letterSpacing: -0.3, + lineHeight: 1.38 + ) + } + } + +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBLabel/BibbiLabel.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBLabel/BBLabel.swift similarity index 82% rename from 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBLabel/BibbiLabel.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBLabel/BBLabel.swift index c6f50caba..9db79dde6 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBLabel/BibbiLabel.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBLabel/BBLabel.swift @@ -9,20 +9,23 @@ import UIKit import DesignSystem -public class BibbiLabel: UILabel { +public class BBLabel: UILabel { // MARK: - Properties + public override var text: String? { didSet { setupText() } } - public var fontStyle: BibbiFontStyle { + public var fontStyle: BBFontStyle { didSet { setupFontStyle() } } + // MARK: - Intializer + public init( - _ fontStyle: BibbiFontStyle = .body1Regular, + _ fontStyle: BBFontStyle = .body1Regular, textAlignment alignment: NSTextAlignment = .left, textColor color: UIColor = .bibbiWhite ) { @@ -42,9 +45,11 @@ public class BibbiLabel: UILabel { } // MARK: - Extensions -extension BibbiLabel { + +extension BBLabel { + private func setupText() { - let attr = UIFont.bibbiFontAttributes(fontStyle) + let attr = BBLabel.fontAttributes(fontStyle) guard let text = text else { return } let attrText = NSMutableAttributedString(string: text) @@ -54,11 +59,12 @@ extension BibbiLabel { } private func setupFontStyle() { - self.font = UIFont.pretendard(fontStyle) + self.font = BBLabel.pretendard(fontStyle) } private func configureFontAttributes() { setupText() setupFontStyle() } + } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBarView/BBNavigationBarView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBarView/BBNavigationBarView.swift index f38046ab3..28c627a03 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBarView/BBNavigationBarView.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBarView/BBNavigationBarView.swift @@ -19,7 +19,7 @@ public final class BBNavigationBarView: UIView { // MARK: - Views private let containerView: UIView = UIView() - private let navigationTitleLabel: BibbiLabel = BibbiLabel(.head2Bold, textColor: .gray200) + private let navigationTitleLabel: BBLabel = BBLabel(.head2Bold, textColor: .gray200) private var navigationImageView: UIImageView = UIImageView() private let leftBarButton: UIButton = UIButton(type: .system) @@ -41,7 +41,7 @@ public final class BBNavigationBarView: UIView { } // NavigationBar의 FontStyle을 바꿉니다. - public var navigationTitleFontStyle: BibbiFontStyle = .head2Bold { + public var navigationTitleFontStyle: BBFontStyle = .head2Bold { didSet { navigationTitleLabel.fontStyle = navigationTitleFontStyle } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BibbiNavigationBarView/BibbiNavigationBarView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BibbiNavigationBarView/BibbiNavigationBarView.swift index b8af3d1f5..45311f169 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BibbiNavigationBarView/BibbiNavigationBarView.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BibbiNavigationBarView/BibbiNavigationBarView.swift @@ -18,7 +18,7 @@ public final class BibbiNavigationBarView: UIView { // MARK: - Views private let containerView: UIView = UIView() - private let navigationTitleLabel: BibbiLabel = BibbiLabel(.head2Bold, textColor: .gray200) + private let navigationTitleLabel: BBLabel = BBLabel(.head2Bold, textColor: .gray200) private var navigationImageView: UIImageView = UIImageView() fileprivate let leftBarButton: UIButton = UIButton(type: .system) diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BibbiToastMessageView/BibbiToastMessageView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BibbiToastMessageView/BibbiToastMessageView.swift index 70c4dcec2..a4851500c 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BibbiToastMessageView/BibbiToastMessageView.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BibbiToastMessageView/BibbiToastMessageView.swift @@ -16,7 +16,7 @@ final public class BibbiToastMessageView: UIView { private let capsuleView: UIView = UIView() private let stackView: UIStackView = UIStackView() - private let textLabel: BibbiLabel = BibbiLabel(.body1Regular, textAlignment: .center, textColor: .bibbiWhite) + private let textLabel: BBLabel = BBLabel(.body1Regular, textAlignment: .center, textColor: .bibbiWhite) private let imageView: UIImageView = UIImageView() private let transtionButton: UIButton = UIButton() diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/Popover/DescriptionPopoverViewController.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/Popover/DescriptionPopoverViewController.swift index 413635e48..cce7b8627 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/Popover/DescriptionPopoverViewController.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/Popover/DescriptionPopoverViewController.swift @@ -12,7 +12,7 @@ import Then public final class PopoverDescriptionViewController: UIViewController { // MARK: - Views - private let descrpitionLabel: BibbiLabel = BibbiLabel(.body2Regular) + private let descrpitionLabel: BBLabel = BBLabel(.body2Regular) // MARK: - Properties public var labelText: String diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/ShareView/BibbiInquireBannerView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/ShareView/BibbiInquireBannerView.swift index a086d6ade..f56b06837 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/ShareView/BibbiInquireBannerView.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/ShareView/BibbiInquireBannerView.swift @@ -16,8 +16,8 @@ public final class BibbiInquireBannerView: UIView { private let mainLogoView: UIImageView = UIImageView() private let subLogoView: UIImageView = UIImageView() - private let descrptionLabel: UILabel = BibbiLabel(.body1Bold, textAlignment: .left) - private let subtitleLabel: UILabel = BibbiLabel(.caption, textAlignment: .left) + private let descrptionLabel: UILabel = BBLabel(.body1Bold, textAlignment: .left) + private let subtitleLabel: UILabel = BBLabel(.caption, textAlignment: .left) private let arrowImageView: UIImageView = UIImageView() diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/ShareView/BibbiMissionView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/ShareView/BibbiMissionView.swift index a033e4218..892f21503 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/ShareView/BibbiMissionView.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/ShareView/BibbiMissionView.swift @@ -14,7 +14,7 @@ import SnapKit public final class BibbiMissionView: UIView { public let missionBadgeView: UIImageView = UIImageView() - public let missionTitleView: BibbiLabel = BibbiLabel(.body2Bold, textAlignment: .center, textColor: .mainYellow) + public let missionTitleView: BBLabel = BBLabel(.body2Bold, textAlignment: .center, textColor: .mainYellow) diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/ShareView/BibbiProfileView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/ShareView/BibbiProfileView.swift index 06cd76892..ca810c846 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/ShareView/BibbiProfileView.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/ShareView/BibbiProfileView.swift @@ -19,11 +19,11 @@ import Then public class BibbiProfileView: UIView { public let profileImageView: UIImageView = UIImageView() - public let profileDefaultLabel: BibbiLabel = BibbiLabel(.head1, textAlignment: .center, textColor: .gray200) + public let profileDefaultLabel: BBLabel = BBLabel(.head1, textAlignment: .center, textColor: .gray200) public let circleButton: UIButton = UIButton.createCircleButton(radius: 15) public let birthDayView: UIImageView = UIImageView() public let profileNickNameButton: UIButton = UIButton() - public let profileCreateLabel: UILabel = BibbiLabel(.caption, textAlignment: .center ,textColor: .gray400) + public let profileCreateLabel: UILabel = BBLabel(.caption, textAlignment: .center ,textColor: .gray400) public var isSetting: Bool = false { From b15b787081ac33ceb92b79449d369278936f8fc0 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Thu, 1 Aug 2024 15:58:15 +0900 Subject: [PATCH 161/263] =?UTF-8?q?refactor:=20UIFont=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20(#587)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Calendar/View/Cell/CalendarCell.swift | 4 +- .../DailyCalendarViewController.swift | 2 +- .../Views/MainFamilyCollectionViewCell.swift | 2 +- .../Bibbi/BBCommons/BBLabel/BBFontStyle.swift | 172 ------------------ .../Bibbi/BBCommons/BBLabel/BBLabel.swift | 4 +- .../BibbiSegmentedControl.swift | 4 +- .../Core/Sources/Extensions/UIFont+Ext.swift | 167 ++++++++++------- .../Resources/Cafe24Ssurround-v2.0.otf | Bin 0 -> 1609932 bytes 8 files changed, 106 insertions(+), 249 deletions(-) delete mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBLabel/BBFontStyle.swift create mode 100644 14th-team5-iOS/DesignSystem/Resources/Cafe24Ssurround-v2.0.otf diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarCell.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarCell.swift index a9f327259..69d35c20e 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarCell.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarCell.swift @@ -175,11 +175,11 @@ final class CalendarCell: BaseCollectionViewCell { $0.appearance.selectionColor = UIColor.clear - $0.appearance.titleFont = UIFont.pretendard(.body1Regular) + $0.appearance.titleFont = UIFont.style(.body1Regular) $0.appearance.titleDefaultColor = UIColor.bibbiWhite $0.appearance.titleSelectionColor = UIColor.bibbiWhite - $0.appearance.weekdayFont = UIFont.pretendard(.caption) + $0.appearance.weekdayFont = UIFont.style(.caption) $0.appearance.weekdayTextColor = UIColor.gray300 $0.appearance.caseOptions = .weekdayUsesSingleUpperCase diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/DailyCalendarViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/DailyCalendarViewController.swift index 2373d50bd..9766337a7 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/DailyCalendarViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/DailyCalendarViewController.swift @@ -327,7 +327,7 @@ public final class DailyCalendarViewController: BBNavigationViewController UIFont? { - let attributes = fontAttributes(textStyle) - return UIFont( - font: attributes.font, - size: attributes.size - ) - } - -} - -extension BBLabel { - - static func fontAttributes( - _ fontStyle: BBFontStyle - ) -> Attributes { - switch fontStyle { - case .title: - return Attributes( - size: 36.0, // 폰트 크기 - weight: .bold, // 폰트 굵기 - letterSpacing: -0.3, // 자간 (px) - lineHeight: 1.38 // 높이 (%) - ) - case .head1: - return Attributes( - size: 24.0, - weight: .bold, - letterSpacing: -0.3, - lineHeight: 1.38 - ) - case .head2Bold: - return Attributes( - size: 18.0, - weight: .semiBold, - letterSpacing: -0.3, - lineHeight: 1.40 - ) - case .head2Regular: - return Attributes( - size: 18.0, - weight: .regular, - letterSpacing: -0.3, - lineHeight: 1.50 - ) - case .body1Bold: - return Attributes( - size: 16.0, - weight: .semiBold, - letterSpacing: -0.3, - lineHeight: 1.50 - ) - case .body1Regular: - return Attributes( - size: 16.0, - weight: .regular, - letterSpacing: -0.3, - lineHeight: 1.50 - ) - case .body2Bold: - return Attributes( - size: 14.0, - weight: .semiBold, - letterSpacing: -0.3, - lineHeight: 1.40 - ) - case .body2Regular: - return Attributes( - size: 14.0, - weight: .regular, - letterSpacing: -0.3, - lineHeight: 1.40 - ) - case .caption: - return Attributes( - size: 12.0, - weight: .regular, - letterSpacing: -0.3, - lineHeight: 1.50 - ) - case .caption2: - return Attributes( - size: 12, - weight: .bold, - letterSpacing: -0.3, - lineHeight: 1.38 - ) - } - } - -} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBLabel/BBLabel.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBLabel/BBLabel.swift index 9db79dde6..84f54d383 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBLabel/BBLabel.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBLabel/BBLabel.swift @@ -49,7 +49,7 @@ public class BBLabel: UILabel { extension BBLabel { private func setupText() { - let attr = BBLabel.fontAttributes(fontStyle) + let attr = UIFont.fontAttributes(fontStyle) guard let text = text else { return } let attrText = NSMutableAttributedString(string: text) @@ -59,7 +59,7 @@ extension BBLabel { } private func setupFontStyle() { - self.font = BBLabel.pretendard(fontStyle) + self.font = UIFont.style(fontStyle) } private func configureFontAttributes() { diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBSegmentedControl/BibbiSegmentedControl.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBSegmentedControl/BibbiSegmentedControl.swift index d10777004..1ea6a9de9 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBSegmentedControl/BibbiSegmentedControl.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBSegmentedControl/BibbiSegmentedControl.swift @@ -62,7 +62,7 @@ public final class BibbiSegmentedControl: UIView { $0.configuration = .plain() $0.configuration?.baseForegroundColor = .bibbiBlack $0.configuration?.attributedTitle = AttributedString(NSAttributedString(string: "생존", attributes: [ - .font: UIFont.pretendard(.body2Bold), + .font: UIFont.style(.body2Bold), ])) $0.backgroundColor = .gray100 $0.layer.cornerRadius = 20 @@ -74,7 +74,7 @@ public final class BibbiSegmentedControl: UIView { $0.configuration = .plain() $0.configuration?.baseForegroundColor = .bibbiBlack $0.configuration?.attributedTitle = AttributedString(NSAttributedString(string: "미션", attributes: [ - .font: UIFont.pretendard(.body2Bold), + .font: UIFont.style(.body2Bold), ])) $0.configurationUpdateHandler = { [weak self] in diff --git a/14th-team5-iOS/Core/Sources/Extensions/UIFont+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/UIFont+Ext.swift index 66f1466f5..e7e0c2b8a 100644 --- a/14th-team5-iOS/Core/Sources/Extensions/UIFont+Ext.swift +++ b/14th-team5-iOS/Core/Sources/Extensions/UIFont+Ext.swift @@ -9,142 +9,171 @@ import UIKit import DesignSystem -public typealias BibbiFontStyle = UIFont.PretendardStyle -public typealias BibbiFontFamily = UIFont.PretendardFamily -public typealias BibbiFontAttributes = UIFont.Attributes +public typealias BBFontStyle = UIFont.FontStyle +public typealias BBFontAttributes = UIFont.FontAttributes -extension UIFont { - public typealias PretendardStyle = Pretendard.Style - public typealias PretendardFamily = Pretendard.Family +public extension UIFont { - public enum Pretendard { - public enum Style { - case title - case head1 - case head2Bold - case head2Regular - case body1Bold - case body1Regular - case body2Bold - case body2Regular - case caption - case caption2 - } - - public enum Family { - case bold - case semiBold - case regular - } + typealias FontStyle = UIFont.Style + typealias FontAttributes = UIFont.Attributes + + enum Style { + case homeTitle + case title + case head1 + case head2Bold + case head2Regular + case body1Bold + case body1Regular + case body2Bold + case body2Regular + case caption + case caption2 } - public struct Attributes { + struct Attributes { let size: CGFloat - let weight: BibbiFontFamily let letterSpacing: CGFloat let lineHeight: CGFloat } + } +public extension UIFont { + + /// Bibbi 스타일에 정의된 폰트를 불러옵니다. 폰트 사이즈도 함께 맞춰집니다. + static func style( + _ fontStyle: FontStyle + ) -> UIFont? { + let font = UIFont.font(fontStyle) + let size = UIFont.fontAttributes(fontStyle).size + + return UIFont( + font: font, + size: size + ) + } + +} -extension UIFont.Attributes { - public var font: DesignSystemFontConvertible { - switch weight { - case .bold: +public extension UIFont { + + // 폰트를 불러옵니다. + static func font( + _ fontStyle: FontStyle + ) -> DesignSystemFontConvertible { + switch fontStyle { + case .homeTitle: + return DesignSystemFontFamily.Cafe24SsurroundOTF.bold + case .title: + return DesignSystemFontFamily.Pretendard.bold + case .head1: return DesignSystemFontFamily.Pretendard.bold - case .semiBold: + case .head2Bold: + return DesignSystemFontFamily.Pretendard.semiBold + case .head2Regular: + return DesignSystemFontFamily.Pretendard.regular + case .body1Bold: return DesignSystemFontFamily.Pretendard.semiBold - case .regular: + case .body1Regular: + return DesignSystemFontFamily.Pretendard.regular + case .body2Bold: + return DesignSystemFontFamily.Pretendard.semiBold + case .body2Regular: return DesignSystemFontFamily.Pretendard.regular + case .caption: + return DesignSystemFontFamily.Pretendard.regular + case .caption2: + return DesignSystemFontFamily.Pretendard.bold } } + } -extension UIFont { - public static func pretendard(_ textStyle: BibbiFontStyle) -> UIFont? { - let attributes = bibbiFontAttributes(textStyle) - return UIFont( - font: attributes.font, - size: attributes.size - ) - } -} - -extension UIFont { - static func bibbiFontAttributes( - _ fontStyle: BibbiFontStyle - ) -> Attributes { +public extension UIFont { + + // 폰트에 포함된 속성을 불러옵니다. + static func fontAttributes( + _ fontStyle: FontStyle + ) -> FontAttributes { switch fontStyle { - case .title: - return Attributes( - size: 36.0, // 폰트 크기 - weight: .bold, // 폰트 굵기 + case .homeTitle: + return FontAttributes( + size: 18, // 폰트 크기 letterSpacing: -0.3, // 자간 (px) lineHeight: 1.38 // 높이 (%) ) + + case .title: + return FontAttributes( + size: 36.0, + letterSpacing: -0.3, + lineHeight: 1.38 + ) + case .head1: - return Attributes( + return FontAttributes( size: 24.0, - weight: .bold, letterSpacing: -0.3, lineHeight: 1.38 ) + case .head2Bold: - return Attributes( + return FontAttributes( size: 18.0, - weight: .semiBold, letterSpacing: -0.3, lineHeight: 1.40 ) + case .head2Regular: - return Attributes( + return FontAttributes( size: 18.0, - weight: .regular, letterSpacing: -0.3, lineHeight: 1.50 ) + case .body1Bold: - return Attributes( + return FontAttributes( size: 16.0, - weight: .semiBold, letterSpacing: -0.3, lineHeight: 1.50 ) + case .body1Regular: - return Attributes( + return FontAttributes( size: 16.0, - weight: .regular, letterSpacing: -0.3, lineHeight: 1.50 ) + case .body2Bold: - return Attributes( + return FontAttributes( size: 14.0, - weight: .semiBold, letterSpacing: -0.3, lineHeight: 1.40 ) + case .body2Regular: - return Attributes( + return FontAttributes( size: 14.0, - weight: .regular, letterSpacing: -0.3, lineHeight: 1.40 ) + case .caption: - return Attributes( + return FontAttributes( size: 12.0, - weight: .regular, letterSpacing: -0.3, lineHeight: 1.50 ) + case .caption2: - return Attributes( + return FontAttributes( size: 12, - weight: .bold, letterSpacing: -0.3, lineHeight: 1.38 ) } } + } diff --git a/14th-team5-iOS/DesignSystem/Resources/Cafe24Ssurround-v2.0.otf b/14th-team5-iOS/DesignSystem/Resources/Cafe24Ssurround-v2.0.otf new file mode 100644 index 0000000000000000000000000000000000000000..31358461d4d36b7bff86f32c4b769384e7dab1ec GIT binary patch literal 1609932 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�VTY#N{U`vgO4Q@8E(-GVa#)HJpV8Pv0b~Xj~ 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%0D8smYMfEal3tP!mjjz-f3tS4OK__WN5n!VESBQ<& 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
&8f1 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ℑMLC1rr~W(p5`y*X`Q7!tuyGumHwa) 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� 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 z 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 z zkn2bcr{ew}KuE=77oo56h)L}IS?`Zr8uby5xwCpSk9kO>HzzHJL^4<$CBx2X0z$em z0w@YXuTPnj$zJBwu#ognqS+3-)pJ#2ARtgZUs4LPmnudz#1kOYoAnWNxZAqGK5J- zh*$E4o1S5>9_E(oWEhIjemKMN=HD=`7@1EMOPH&kZNOaVM~Ah=mct^sA8k0;q@T&* z+vV^H`+A%|d(KPDgHq&3Pp5NJ3IVc^La_0MxZ?&tsDW&Kl@2kac`jceCw0FTWYJK; zOE=y#*7>vr(-+PF?7*hI1d3vemsSkGuTC4ddL;N2M_7la!LRL%nJ4BRr;Rj@^MB1c z-15-DJYpz0)V#tW7=UHX5(ul?{t*c4vi&R;TJp&}7s-R+kBR~k2bt((Hc%C~7w{jF z0wjA6CF}>nkABhf3hd*_dp+a8+tUTCJ(E+rFsD`fR_tbCk&1Hmlflz7*&H@sR6Gt) zzvStGS2d7?i8JF*qSetkDAzA&o!mhXraHAff=kE*uZb*=eQHA@ywW9a)KU?-&;N+T zI#48~=*F`6LG+ljT5Att29LS9F&+j;Haw@7*~@wyz3aT$3eT?2E1d(sh4q{W-Q}w| zPj$2s@8eh_&I=che8jG4FV~RI8AQ+7uEz5U16X%`ji)!B)BnTNO+3ub#v$t^4CuiM zC~IOl57cm|h;Xg}o$DR^6zB6dMB@UmE(mtCP|2`!SAU1My2?H@T}b;NH8pKwlC@S0 zb5<4yb=)jG7CGW83m(#53pE4&4%eI{2GY&(^hC`oFdC=gKBcvKNbem{Ytaz@JGo{H z_Q8WZNv`QxN1Uv``pI+BQ}9$brUB$t;C<3+C2H8w=~-8`j&9fz_;`m}nKO{DSJP8f zxUA`^@0bxY%1ea`%S&?MULY^^q4ttR8IbC+AxxR%sYf5cfy_&V1L+>k^1#D~VGtY6 z6T<*kC@8J8T6KBNMi|f+8d51=q#@<0wIv@Dd4~HQ9U#vz6~81$%wzYtg->(c7|fT> z+G3~XJx@4z(=Cv~ceUy`#Pl6tEvkCKfE87owRR3g@+?eU*p+sA9)euK# zjda>2;Gy(x6E}=Bk_^?~(hFqd%}$Xwy@1MZLB=E)FZHC&D7+=R{<@cO60@0Z2qwxP zv(J z*r1g361QA41THBgf01PL7bSt@HF}^y6nX7i*y7j7M(=ga^B2thYcd`38ky+VycRkr zFu<#F&&#WVMJ^9CTs@0NWMpdrsrTsgs3Q-LOGqY!!Z<;7DMe+3vd%-hIG*BJ5eZ^6 zFEbFQmIKGhQOi9;r`Ewm3pgZypkP_qpOKCy4e~pH`-70)!K)*QJ(D-jWzJa9+TdGp z2c-VIm)E&ZcW&+9d1v6T_2aIP0kpSKDhExZ7%0kGP#@PFAlf%Fg}J*TSJHc+@^Tq$ z93|v5_zdYsI8mX4aHJ|O`)RGCMN5J|KF_461uBw7bbBq+*%>3(3_<3uok!r}SuMB^ zW7uimr>>zh_bq*mei&!2ja&Z04X&`96cN9O2=H-;L@+uoCPlPNsC3f)LZV6_QBBj= z5;>!@zW50TE97v_@{4Gn0GAzfCa1kxuF9?o~-;H}TgGj>e7U>UP~>}9Ls<{@o>4jc-_dlHVw1GqS%eVmEN z53vD^bD#*+Bj~N-aflvnaIn`^Q=l(C5w$!jQ*VNH8)^4$$KIx?sF*5_o|nDRn}9yL zxHq|k;fLySBjojnozUuAhOQX)xfP<}5tkVcLIS+*==IgH-F)~YSJk-OgA0b(-&)WfI| zLspNuI5$e$ge0^nZ;Xm`*J2pl)EZ2UL0w!urWO+m;U0@ksIO!e*TpwHtQF%G(ilSB zqK*i8MpWXqTOg&O0tn(SeLr%_^4U2pg@=x2D2@|{0tYB=U`BZe6j7>-w6n@cJF75= z>Y2vF`YusF38KV9WWJe;f?D*Tue2Aas4PD67fSM1Npd01RmUVyC5bl}2+C&b2@V$h z&BcX~Fj*)UBp*oo;T;wot4G2S zB1r_1$9F{JF+W?!Eu4U(_8b63x3nf^)SvEW(bFNVbV#e&4R%Xb2hvDFKYLqws;ajf z8y5a4tuYy84&?Lz8RKIAzv>=gG)B>*!LkU9F`o@|-0N>Xr46CfuO5SqibuCmN*w@J_D!`li<1!Inqe4W-chKW~ zb%-){$n>ZSzUo7De&0p)hjNRH4`qBR=Ss=ABK|T_jom0EW3#s(TlslEgbls?Z>duz7 zS2hpHXFaBm8HbFwY+uzOcPb6XTXo2tRa|vQipMB9G(yTJy{}jDhAp;3+G0CoWz)P7 zMoposVgHZt=QUBKpT@?T|*=4qgo> zGMbYmp=5Kiw9tgUl)N-y6eTZB7(vNP6Gq9)+fhCkhSD5C$z*UC3Hg$E$u1| z?GvqvG}i8LhM-s44P-GHHG-~USVoDUtK(L^wRetn-i-M(XGAJnt~NiK_W2y^7jwtY zzqByq;^L!wZ$&D-u1Ztx>bT|O55Xr!ICmKCV6T=W&%5g7&T~v&wl+%H@{PZq5bc>Z zxNyfEi@%sqRXY}+=8#!D6)B4cAExWw!P00;X{8tA#OYm z;eBKU8PBtWc%i3Ogy-3*aFFpl$KNUjUF7lgaJ!v|ulWydSAlVGThM_KTP7Z|sy%t- z_AKg4?b#=)1!;DFwIHMi;28H;3(`=`ti&-+)q?gJM|cG2t>^#;TZ&OJ=WPCLf}$g(DzIJ5q3{5|nKm-A^8LC$*(XIP%HF)4XMXq?Y{0 z5gryXB0Q|PHY|T#J;RsW#)|L*roRV zr;`<#G)D`MqLGRvl)JAo5Y6q76>ooztaX}M%D~0icyc2BRe%iTXp{l^3xWI1Oz(-> zb59=7O^zX=q~$upnXLEYJ)reQ$mRaaF6+hoht{tw^z#?u^8P|U9*5@{=0=sD zf9@Y#_9wg8{57dOlCab~1)SIXr;Y`EiZ%Zan!{hM2kxGTKj=+n!S2DvaoLgQ;Bpwn zdhABA9#|aji^LMCIp(K~XSER@Ow<0}XVf097sr6H1+9VDh zIqNBD%1$Fy;xYSE+eNi`H1uMU+|jZ&55$Y#c}w$XHY&}7Yn8I7tCvsLYG%>k2KGl^ zSdZ#-9-YH>P2AaaQIwi*{(RBeZTX;!+ttphn~?u7V%Xr->6bF2^nBzrPaH{faifvb zTvJ#rYW@wpt9H^_0-lg7tY>)TW9$Zc-VL__x^p(Sv}W2iwVG=sQtWw^?rg}Kv1c-) zw1G|#ex%;eQq5{JEgD`XH@8Gyf%ORe9!Wz)y@JHAcsOD}Mr81aYIM1cD-b)=2C5s` zAgwg)I;T*yP+YOS(1vM)VZ3$_s8fazaSQlY$0b@Bq6MK9mk4aiw3qPwwgQFM@chT}`Hl6O?*7z?Zgk>! zAJ3~i$HX+S7<`B4!$2ol^gsg-)N!IeNx@M<3e&hLlsT(tZrJ>f^^MVB#S-5GsmPJo+{sZ8aW^+&9>g-oBUUxuOl$LOtJ+ySdncZ%9$2 z{K&g|m`DeqjIX=yW|%b#PQ;24Kj5iR##3L!KWpP6X-YO84bISN2#871w}j}GjO;WZ z6TbwJ>sZ!8*b<10aF2z)+~S7T!hAz}_ye+2vfX8v&S4sk;wD^l94q+6iRxtH3RJWO zzI;XSu_V%IO#-4(Xgp*I6(k#g641PID_Sl!qoJz=Z_%F7LJ_}-_x%0lromvn4#Nfs zD=7|=G?=fmxw#w@U#jVr>nk_r@5DugnJ3-+1g%{4|M>HPw*4gSn|@ z67{qh0Li7UVSjpl#pkISJgS~5Qu00JjMg#I@6mWQ5x-U>))MiHMYZD|+$oK|!fWH^ zXY_tv)x)UyGN=WA<#l8_DCsY8hP#Z>SAfeh8XLV|Hr7C!!GqQSX}sSF8|v--yh18o zzUEPT@p46ff&OUAJl7EMqH$L-u@!W)5?xkBF<$XaR~MtdV{|KnH_+0-I+d8C zx`>>D;;wE35r5#(zDI~0C>rQk{K6tUp)pgTguZ)S0~?$>)rGrme9bpeuG_Mu%GyL8 zeh{CeEOFLxur!os_D;wek%+AQRU+(D&aAld`%w12*KT@tb=PvdbJf50)R|bIy*|Xl z-FlB{83QVk z9(061e?xnoHcJg&<1gmfiQH72QMjPMgBp@sB#%F>8!1ZXTyFXeiM`?!J*ow7dJ70n zdWJkT@C+L^+~x_e1+;+POxs+C4Q1Hh#^Llc&{b9CX(?7Aa;?wlcl`7lDv~|XvuQUP|h9H8yp~ns-GHi z1}U}i7-)b%XCMPcbDr-!_G$pgyJAc}>knF0c@+D9jsrcA*GEF@U)*v6 zSOnhi8JywO=nU`XV5!(Fm{x6%v=q8{pdLJ`g}Gl>XaBdxaG@xh8|B|?U*jyQXmprV z2wa&)o*6`fwA-TNI>Oty$?h04LZ0<2#(zGS&iZm|Fow^(N4?P}?Kln|qf>)WZj&CG z2-66Oo1D$1N6zwg4N0C@Yf?2iaijPwEn=TYKqM4E%M8CdsJ9ib$Q0CIXSIB$scgR? zqX?gfYl+5yLBJ>#r;cK|t9wfJnKulO0vn;fD&RJ_d6`(oTh3+IMdI(AA!_x#bAT3y z4U_+=`v9P0@9_q3Z%^F>?r58fH+aRN zO#6;kECPpCVnk5|JWuiLh+*mcyZT+2STe8iZs(}%y(ciQePQDUN& zUxVGjH`_yDTu1iNX{P*Yzhc_U+TBe6BMjQwPobiOFAW&UCA+t=0@>IYqcP-);t{ao zwi%7ty^R%c^T!z7LcM`2h58^8Xp{Y9qpAYG+Wf5mU|LNCelLP+3F};puF*Jo3UglNqv2|KzPo_`HHC-IOe$eZ$z0<{tLg*0nHqcI8L;`S> z@~;9Bob;gyW(&EEc|Uv4A&t$ih~Dd@SUOh^fmfOk^Lb1Herp z5(^<^<=0d>Xd1Ed%WYVHfu5JyTJ@&8s{E{qcHE-;mP%u$@mj^Z+A0s~Q?yf;%E2ms zrrviSW+YEfC-#8|IU!Hs>n2ZUrX0TFgeE@xH4lPrKYT@lZ*utS0;1vg*v7zNI{9m7dFE-vww4$S zf3}D0DbnWZvD51815E#%IQ>zcbQTRs$RXUl?H__#TfnNvL@AGM`})okS0gGZ5`Uv< zfd}&EFz1`RI@WvzUhidEt)-w3#vn=iVie$*&N1gcWZka9>fUQk%F8%p4f8Z-TX`j5 zm93(|E|Jif)^Byx{k(D6T^lSB=Xk;lyj&hd#?_5{Zh09($PmxbdptM;+&Y(e&=kC! z)2A{qLt6=a`#Byo8CGx)liFN9p)BNcYK-}^YR z?BZzDcwGyQyTsTliY1yx5|a>P5>)Kb*rPPp3W8qp>s1 zyv`yl)x{}dts2L9Pb_wcqaCf?Wr8D%G7n<|y+WDT@tySxl{xDtS>~wMOG4Ro1<0nT^E>nkw5WzAL8S(u&7qNRj(Yp%@qRJP@Q&}{T8%DA%_DEU1WUYG1TJ<)N za#uMgm)vJ1XpZ7U(aF)?U81ZQLL@xWAEiD`7oI7i*_)!8T18aZ$9tk_ic&F7kdC4# zmN!_&_{&e(72Xb5VNnS05JO)Nn{Q2m-cq%)VvoelG@Iwo9A+vIE3^r(ib3`t#SzQ9 z9%rRv*bZ5_rlLiXXz{V|S@mcqmZesYl%f<_y-l)u!+Vw*@cNX$g0KTy6vST&W@}xI zm!j1!MJsU=)h-OKtLIOsTPkmG87^ijsc}MC-}FfkK5q-32vH*yMb#0W%D`UURM^0t z-rQ##KD&E!_B%SK>>l`17BO%b4pfE?1`hS6LMA$^Ax&3LZ?y(?6nj~n-I)^C%gSps zl$X3FZV#aA>G%acYYxXYCVggT&(%s|rLE~-Ki=Rf9@*pvAxrF7oITD6Ag)Dc<#vuE1g_ttrEJvrnSG4TcKp?D%-(Le-l-GG zv@!zOv@YHny~Xq`Z*_ppI<^?121G|L37<~VeZuPxU?b+HK(+?~N%1L%kH!X1G0zVf zW-JgGl~S%c$nlUbJI!xk@#NE_lZQ#?glp1KA0tIv*@W9xba(`3kPDw@uJ_J`BV$xO(W?Mu92V`I3bElF2IVL!{iAp>X+h##s|iO3B&6_q zCs@e(S>G)&&7HAmn%N60)O(^ek_J>8ZpltciqAg$?b;DjrYsyW7_ISxtJWi77~Dh- z#gAYX)Zh)E=nn z%u6_-OG&k8D|ecuvYD{W*@;&_#FSJ&!ONHMv*m_DsTX#bJok!bpH}D#ZAG_dvMRoWPdNLSah}ZboU=-6vG2qZ zEL2$QtVi5)xe;&cCucduk^d9kb^>oxRnYv(1;GHTM^N`28*l$7n%_p9;eoYG4%DN_ zicTPxNt5V)pK7iYnH>|=E=ZiYVc+gq$Xx53nr@$tbO{^wBwMU~OwJj*>@$!jVbh+S z^S%a!^}Oz*1>?g^>N5SfHKC`C!eH-ipt)2jO6N^6yA-FB*zS1~!{G1pW;)HAoOxj{ z#VvXmg%$Z=k6{e@;%zI~Z#6owTio+8>#Ux^ae|Isyw-fw%bVAXM#mq;TlZk5OQIuh zKa6po10Yj6-{*Dqq36el7adLAJ-#pom0cpL>=G|zJWUeK-g%S(yRE12&4i}BV1Ktq zagS^Q>Y{lI@O8_aouc;Itd0QNY<(me>-okr6WXS36>%76#6lXO$ zFzsXdzdim(w4?F3sPdEGgY5^&1#fsscpof956H7QcA>L*!{UGZA+smE3xhw|KH3nt z=-`fT)*L)>eC^0E^TLq>O=^{DqUI%>2V_!yw(VZ9D>BLW?dtVglD+T>-nc0&iJxs@ z>myDqG0rwehfeTPYv%PAp2jQo*PjP7M>VJa2TTa=iRg!)t))j9Qr9bYtp1`-Ne)w= z>ACVBcK>|4p4c2W?k*FQdkw&h15x*4foROzqZyB@`&xjs3qyTiE0$6$;}@!|4{NbYmP$S!@@$NM-1le zZXjYtF)8QsGv@E&ZD%n&L}%t2)pQyBMBjvLm~nEkG143zG#>iS{Y2D&z5{u~EDk$d?rs4F#E)fi-Y$@O;$7iM-hruD zfL9c#EUzEhRQ0o)5djhByao>f-xi2W%z_2{4eSj*Fb|H-PBMvI>PC1Ehsb%L1}ZY= zo~Oov>!20bmzz8oT6UkP(++BhnvUY4uthwbD}gx20w!zJbncD8;5h{QgqgePS5 zA;2I_fPPYdi*PdVZ2>~1>G?Ncctm9mj@}0WCToty;Y=rXO%7)}o3lOkQO??UJo_g4 z3SQZvh@C6xRHCO_&!5sm_MUKVy1;{Ss|1b$8W3Vq(?KPEbP&jGI3967*yHeV!*K~V zf}@*(*HWdPYNs+eD(v@}>9ptwAw^a75F9Hr^(p@5$8fB!A+EWSQh2*%*kk9tMKv7z zyO_ag{82Q@72f;Nd65`Kc~;;!hUxe0hV}c>=2$Vo9@}f*uRFcknu_zn^~np8KQ@-s z()F=S3!8|%GA4U^uyeoegB4*&Ic?C7T^XKd{Es~ed;I3lXKznR`fB@r-flg6r5bPU zh3yr)9G-~tRG!`CukB{9Ap-oa|8*LcJGQXz&iCud;PdP*$`-sAe|0*8T=094Jjq+R zx}P`kgs+mzo0#Cz&a`Upe<|*l^=$?Bmgc+f*;j&T!$_kmgT`~CSoHKqJ(8wHrxA4K~TL45-4ez7NIM_ zB=4(NW@LH%rh+-ajpq^2Sv{6_n@^eWybmEG1yvs6ARS&818;tW1;x$y$s~;9)FwFI z>L*|n4xBcFiRJ%ERGYd*dP!-hUo992X%HE4&hKU56sH_qW>^w&a1*JovecqR31*NH znU?dv=;=rP;4e;Lsak`zyg&`sNBEi{a?v>6ksO&sslTE?slK8_(q3^QDX%y%d*gKT zlsVHz!oNu*{hf7&x}B(r8_0*E;^7yMIo}WSpKLl@O_a^|g}cxm^%)Yi$}w{_i#NmT zuU?i#UQLrlUcF33Ub`&&OPbbSE>mfz)ELnL4&r6|i}<@Q!Tzbcz#IF@3#7t$*F{t@ zwZWsak3K@xuzUGtAKG~C%k@8D!CwJ1y)A!fFM=!z$gM&!)j%L;+hH{nB`5S58MUk3tbkMq=YTB1@Uo+RXSav zNSgu=D%9H!-z_fGuVgJ;uTa0|qu_ak`q?p*28R0WoNpaWyN&7T>HUHlO1YWutY zB7Q+hN2Egyu{z$vtMGv(1!BTOqmkG6glN29RJkDdI{*RU4DaW)@~C>DtG*4Z{?vt`LA2@*k*j_uZPmv?j<@fA$HZ-4))FT2 zUH<}_SZ-}$PtLdO$#}cQ3^qw$Ugr{PFFK!+B^@o@39?tdBSr1X+e~36mrO_)*JLw} zbZmyb&6= zYESlDz!j?^nvlpy#?|2lW~Z8>Lx%AFOmzDUEngBKS2E5ANg;S0G@KfszTJ}RR|4?P zxx6N2uSrbNpPRIG%;+z|KQt}@1|QIIHT<*{_Wjf|@rJ4@Y>yH2!%XJ@!(qgg=G+~w z6M>GlSix?dr=@FkNn1xPJna2>*65QzcHgw0V`Z|{ICKroKZ+C*(% zrvABwT|KYX#*X56@-O*^6}s7%qQ@B7_8*EKq0r%Jt7E?y3WE{dM)mL)9{50?xqH4t z$#2a0P`zj1tx9o%$WK-k?%ZiBZ+??~XCDirb?*+7l44DX+kMQa?6iIxJ2Aw(aDryM zvdjC$OD1Kbb@zgA=59CQ$A@FqZ~4Xx&t=ZqjC1eOb-4G=tjIC+5UTj?5~H%&I(OOv z^F$<`zBowKGKvRkF>g=>9;Y9bscvzsFmUA-Tff;WBWe?ZXuI&*(_lk*v20Q0gx~{( zE}h0vGtupwtl+os*fC2`{~p9&Y{8r@u|+9#R}YB#DGR9=ABu_FlH^s^g&)f<97^Mg z3?KC0!O@vXDRFy`eH%L=c=Cd=gG^UMrq*$N;7sITtX=bEt!2l3Oda>?7Iy2rPfzkl z)1_V0SLl$+H-wBYvTA;_IFJmztK%bHy$J&Vz-#(w_SbKgjHoSsM*iX=is%U@)9Hx? zV?fVaG-i+3UTuNF9EcqhvR3G2MFqTBD%Oz#-Vj0V?paiE6jWZaYKtH^Q)FaS5GEbg zP9wbIiOum1D_QRNKneuU%Pr6o+dl*nHnQb}RTHXoGCL%tg7*%fiMREoHR3$)7=}eJ zf;SqT#8Ih$^RMFUipFm5v}m8;-HN1v7$$t@NtG`VzP=FSk+-->{^=sons>jdP0Z%F zsD0#%dM}x`Z0>BY4%q`lOJh;B&u1sV0!-L%QXX2fH_kGf=gb;ux+><%9;7}%530L= z$CezESnJFBqZ9f0e@C6@%Ps6U6kS~Rlb#IzRO4%ORGrsX!iWHwMmEu?UK#2nEH2oo zzJ_zWeOl)j#QGK!`oz1Ar#>V)YfkD7a#9mCCv_w_sl4U8+O~)Ey3bf0V|jxk%$f5& z4vbVYSV@20@eCY!0xB+A%Xayd?`_z=&^G!v;+$h!>DTO>4tGoEGkRl1l<7>2wu^Ko zpaRN-o0U+ei!s{v(Zv7__^E_0eQ)KhzD8aLtE&{NtCULk(iy9(m{Aq*CETopFKJaU zDi=pp-lcNkOy9@wrU0OK#kdNJK|ydZTZyWRMWa!o`c}~hTXXu|#c(lwuzTA58ErD< zV!w!dlwYPWrzOh=8zV1c2hY*Hq%RXU5c|Ike0Hm!Nxh*fg&mSNYM*K`XlRm#HVK`7 zrrH`P(->7}+-_H}xsII^#lWlQ(Pq^{gYyG#B&-;9fY<7eMDW)~-J*ZevC$-fC!}&; z#oOd#vhk6)6E;yAVguz?y3~h+o^F{dLr?flw&*=<3RQ-ll;6|K(WlGl8`T3qxYN&_ z){FPakKD>zy;vE23ewQ0VShuPg6!ddA%ZHRPeBrW3enJ~AVQxaQh`1>;dagl5%16? z)UDx9vQFLmBNbXYa&#I01$w$=c2&V#Rt4pEb6KZUE}J#k&6?~b9kESc6A%)%ZtrRu zB4sIsnY>d0Z4maMtv;pj67N(E`0yoyQSh>UNPV8oYaW3emR+(9+1Oq!*+{OL_O8Ff zQIlx1|LS-Rj+&&wQ9~=jQIq6`|FubSzyI3!ayV*yB{*t42OK4_DCKI9Ed6p2m3}1! zrE8GXcu{X1AgR|xy(o!F6-tDtP(z5yP^rRSBq~+7w*o3v_=^_TN z9z|f3^7MjMuRi-v>9uo#3s3K!g_D3k z)ty~FZtIvPh_xw$KMBmz`tIMstisDu3+F0;S%sG+m{pin0n94AtSRBLCfhkuf(y)= z4g6Pu(6W!`$_5`qdM8+m$~209fwel+rD6xRkI`VQc6BSlS{>@j!Ulef!@QvKfqi4B zI1ScnA0s`i4gu1`>JXs8TKJT(7F`ppMc3GR8c&}Y@SE_d!CI*fcvEp5yrr~(Y2GC6 zK!Z2r1;UjgJR?Q@SdM9fVgQs|$)Xia%Q?IrjvLG&)cSRaTHl)JnmQ(yPaXG?V{m%L z5B66B^qB9$X|wtYN6@!&IUQ@kMICNpauQ!42dd{ZQGdOLRrD0~mk`|F4jIU+xn`A? zK(B61#IaQ(o%~W>|BSqa{s* zm(4U0{%%HDs>GlES=K+1s9PK49DHkt8;L@BghbsMQW1%|wSlJOdm9LRxHkkylotw$ zB_v7~94iZsrAh)23KI1bvFB1%RP7+@ual6JB6{6{R^x4=?ehUlZH&70nvSrr^?tl9 zQhQ!lv1#kB=ST%z07_fLID!EH7HNw9#;;7#a)PFe*veqPeYCi&_PcaiU%Hj`-H7#! zimtGnRe+-6UC|@)TJ%U|C@RiRLQ!}|LQzSwIHQte(`*Sh1hpJT0Ry%jvwqj1BXJ>NVH&EKCv30@3Fq8bjly{pY_;%{!PQFXdTtirZcum5YX)!~nR(CWn5YWgW&}`a5}?ycZ0c6N#HQ}$ zQ}_5i2h9{LyG4%d7CEG;$t*+82qw|#Qh^NLpN6|LAITCrE& zcB4!G=+l*Ordt934QIL)BXOo%I5|winFwIL6(hSf%*PZ2Dc$oq8XJNjrQo3^HJjTRtjt0(OnUZps=BB+9F*T9xepeV{=`=kf(ppQ~ zUBjAk#mi@aH9fJ}uc@sKd(y}WXm*Czs{^mb=JU~Q-pGf&pB9mBbe^}D81P)sSHZbP zk%9x=g8(>Tq)i@YI)-y9Tk6$@yWSHxIAG6t;bkn)i$>!v!KU;HeHRC3uD9cCU~B@k z7uNwzX}=OpoevGdL;u~TxNY0MTG-#HR@HyHbnfCAO{UF_Ic2i<6}_-~Z4>XDQQy(m z-YSe~yg|4`u}%u_6NF+t$aj2QIvtFhw*X_oT}bGJ^X0#E?aTB3wkcnf&ub$*_b1nW z{MW!fbMmG-H=?8CEBzk$hdI+2_GfP8ytP_a`Ym8Gb%^0=-W&Sb1XGa^8bVbjMlyf# zER9R;aoXhnI&bRdtR!BX2L_PLO%3tx2NQXd1#F{)o*pE-eUFC(JryNu&=Z1N2zruW z7f@5o@K}3_l1uk6UgIsqe~P;Xlu{;%s1h%9Rv>#WxC`=(F2TI^B!G9kfsF1sRG@}+ zJ;COZ7Ud?fE)k#=_$m3F8DHh90K0di%=4DV0533d34L7DSlH>(wXr`iol%SRr>zg>)v|1f!l{jru0JT8H@K)>K#P1wQs9Z2qm zqIO!gjWDVsbR#S?ux11Ga}3Kk&%3f1z~&!o^5=8YJdcUc`Te z#CWsq6^2&cRNyRtOQD-3Z>^Ti;u=ZthIaFs@eDf&-E%~v?`R8MgS`rIzE&aQFZ!@? ztIRk{8V4X9#S3mY;MZ5&%TWrCxVCc&Z_u{-(;Kw=#Jj~}xjAHqJ^WWM-t0B}^3XK8 zgC5~)z`c`K1^_7mv?WW|iB2{+t&OAv$M9bRG|<)I9vZ?09ZQHS*h69#gFQ6RBG`jA z>@}1t*h9ix!5((-*zE)C85rr8gFVz+%ydO38p56$W+7p&AXtT@xjM`Oad1+ngx ztc_k70<^;od8K4|OUNrF6T5wgejJAD2CK1hW!n7peQ-GUNxc#jXuUE3sUHa1lIj7p z1@(L;d?%u%5HBa8trQK~8m2*8DO7%O3Kd^GOg33@3MnO~q&-kArFa}g2R;ZAP3$W* zEr{HmATn}b?^+rrKaLIa+ zV4!ErakH`)8PQAOF2y(~B|Jk?=Hdfl=HgM(68!{P zawB3{6D)wlz&-+t>e5cFry#CSleLCuaaObdZI1>I4(0I*KsUea0a2Cv=mcG$P+X!5 zl7xgyl4ymOJxH{|vr;q{N>+$d5df1FW?IT|GTbBi9%{YoA)Y7s=yxPvvbqfO-31G; zNx!@7!C%~tPh8iUDzA}5OE<(GLk)N@&>|ZU*qLj@xsfvx+bC+>;dS3)N?@A5>}Uf! z_>+JG+y6+q`#~+>HK6!`?ZCbNip6Lc9-blD0?+xA?gG#LC}pFckZcl2>zn!`xp{rl zT9ca>o7S3uwAi!&0@C`X1rU(dKP`ZOw7|3gM|-A>!v_E|JVF3jU|MScWL7&Ua)ELF za-_h$+o4VC8iL_3bW0^xR$FwxDVLR#z{-jvKyQJ^S0X*W66x__E3UDl8%>OgBVwNu z=}o~^9O7uoJEf63cUx4MCv>So_qNb2qBk}doqv`u{DB}bn9g-)!)9{$%Z>2|X5Q>u zx_casTV2o(hr?~d9{sg~b$NQ_+TCcqa{sQVLQkmJR{8U;`Z;?`_v5n3TgEoQH`DOs zUFaP%{-1X*<-x9UBU^w6itC+2=Wma_J7S%+kTMJv-Dsda&*8O)v$uFHY_gna$=2mX zOY_D?GSS|*MB!+MJqw%^LcG*~e?z>SIUQH)&V8|UG&0ZSjF}Yvp~-6B&%1aKLT1$< zFEs#=7q9sem_gQmU|drWrH~Uaa=_=KjlyJ~X^=Gl<*v$Fp-UvptB=qvCX~Zp^!i*I zy}4HLDtU|Ej-Ce15XyGRSrK;Rf`K?IJ8AITiN){?Ei@-a+_T3EAAk6QNTO1Wlz zX87$&52T+&K`x7Hf#?w=_0wO2u5MVR$#v68uZ}AjH>?GAKr|1nn#J|V zYFjrljMr}fYQeWoAA0VgA3gWbD%!g3F1UmnY0<^v*bO)=R^1<6022CFEY793f5Md) zn6nSBy@{!_9o=1>>oBr^)x_@lM*DN>TpIRMv2%qWLyk0m5dY8#t9k1 z;HJ5=&@Pdor*?@9odU_77RdW{xjypRF}V0E0&}XiQrpoz@7wWOTB##WUO`{BW3|>` zqkq)nccAOLd(TppOw|@ z2G#6_t7f0cn%zLnG@MC;l{J(J?bA(k{}QSzMR))V>ei0;IOQ z5ppB>0YlOa#D0-dYSjiO*PjI|g&E~|mDSSfG=Qa4Xdl2xZ=jrvyN*3f&4izcHG;p= z7RN)9p9v>3SuBE+c~>@FH9Bl{>RktV%V0+XayV%PR8s!DLajK~JgeVQqq5g5*G5@Q zs$U&)+PS95PP*SEaGHdZ@K z7&D81fHhJ>cqEqbl|BS}1K{PC*nc3t(ht}qpD_y2i&Or8(Uq<`gAB@Vesb5u-dKMZ z};xcx)2Q%=Rwq7Z|FD{}6BC6B&FJNh_L!*h0*P-!XNlS`Ur| zZc22C*DVC#g6*KaL}0SG?WZC$v-Ui10B)kOZemfT~~%Alz}7svtaZjr|I3&EUJnYAO+&_)A$#EK+wM zIx$xqCVD~1U+g4Snig{fa?=%VP+)~;J%;6ca-Js0Agd$GpKx!Hb@H~s{bxxtE!`YW8>hRztyB2l0 zzr0e4=>Ds#QlK@xC*U1*Mer435{@G#;m#=jiYKNrttvU&D%XMVf<>a)SnMJ0Lnt$X zPvO_dWDB4=B@opqUoxGpR$w}by=b(mFwx9|(9dnbx(ZyUdOLB*i0HH*F3}5kkv~2u zCi=Q$=3L*vbyt{kF0TWK{d#hUifr)LCvwkqbOq|^3ef=nNjlC3;yOJ5lP8q!@LDh8 zV31_k%Nw8@n@+i$7_8^D(Yk+=AC7YS4ACB>yQQM(QDUXiqMF&O_qo&>1jA zLFq!L=Uoz+{nzDV(agMFB2yPN!(!(NqDHRTM(=#xj+msP6c>rdMX6c)cZ7trBl#F& zFY%3wQ-Ohyf>J9f2u@QX*A&z-Rdi}Ae4<3N@6lgyyo?em8DXKPrG{pan-qG1>(cR& z!Rw~d6%@3D6ckEiFeNls#Jh-6X}J^DgkCTTf^w*|_(!Wl{IDgk)o76RpRKAJxT=Ng)L^+5>O;WZy3cTc*gq2o;#%AKJ` zVkt>3u*c~ll8GcY-|B#UK3dvGYu5+TkG>>dn}Ga~xxgFvlLzdIKi#QQKUh8%&LQ=4!qxBeI4a2Hw>z`L3*fyseW z;r1Vc0}}$SN-{pDTVP(x;_(%gA1TRJQ!OGJFw<1a^8|cM*8={g`*A%?;{o`rhW)&O z_*8lzO$H#qz)U|)(Sey3O{$Adkfz2@w>*!d7g(O(!w+UyD9lfzUbz-8kixoOE1P5- zi@>*nN8-Dl=E{F(R$;>F9LDU|h(#s-=!B6tN^LPf&#Sk={Mn@P2VQ`rsx~ zt>>?lFlT4(i`pSvzh@uJV*%XPgh0akIgxATu8%g(k562j-o$g@ z3B>$#VytU^(qJhnjCcQpAq0YmP&7w_2sQNIh)^;AMugf#M5y)l5X=ezP$lN+qSm~{ z5aiYnRT4xqq#BewsOCt4@*;{jEZ!j=l!qt=4~oAT#1dz32M1~cI8dG+0CM8Z-)05k zw1i-;;&5yIryx10Jp!ekPP_qj{iL#qK2-DospXdF@c~{e8BVYsz;Gh#A<#Y)3)hB{ zz`rq^YS~}c7*1qI5W{IaFotm;LT7*oo#cF-7*1KVf}gXvSMak4R#RN?QVf|cSlnum zLm`)LH(ZDdd{wFevU14I|O=6B6z?kJ3d%S`4|k{N{l z_wo-KHOT&aGE?tctSnyrT+C$7D#QsQJ`iX@k`wfj7H(ywaI4W;xK%UWXf!LC;mE=c zH4%e0h-T5`H6JLh2dWD3gOG#3dnZ9Fa1gU#Vpk#v9r=GI2=zjaHsxpAKh_JFC z7^m}S1fiRfAY>18etbm!ry3Bg-K8KNAa1-pvw zhqN|4hPo90opW@aI7iR^JLl+e8RzJki*uCZ;v7BBpBz8^@Iqshd4BLXH|Oa0iku_+ zr_LC}{|3nT-!NB0_HZ>MQIVYeh<0>RRDq?koLE3mJ4yK{zf#~dT9RU?!8IbP55A3a z{!VnG$N2>!mx80k#^pqu;)V-!Q*F5fO`V_~qMQYr?Q2U%kTnmkQH4@AiaOm+_eD`r z5tWOI%C1;cR79Lp5{sg;K`fF*xyeM?y!rc>U8@_E8QF4nT|I|47y;5zwpMN38jwA}Sp|b_CkCNvewp|1V_}H57X6{; z=M==Bf-vk97aa%(g-;|p5MI9qEf^ zchn&H?x-Ys_nahp_nblU-E)%UyXOS$RMRAF=Wr1BOhluO-$}_0+(k&Z;t2;<0Y!BZ zzL6;6S&rz&JvShp39$;;YJt(cFltF;gjc8ieZd!1o~4V{`;QkSEKXSXmFe#h%OXHl zGeA}|-A!9T?wWwlXMYU~06P9x1!p;D6XfyyM}j<_UlNf-=5b32+dLI~7Vvg_h4)d2 zczuGT?OfNia9z{Fb*Y7$QVTaVE!@|g zycy}0za`dG*ImTc3FO9X@J*qIUL|m|MEi=AoxYN?bG96csMy46dw1PsJK*q=9?p5Z z`X}fDeGbq#bC<8a{jeu;Jv{DGnDDgAW6*Y7qoa*ze@L_kz||;E)NLcH4a=d95`4)b z;)T_q1fR7KLr%QpeA94tOG0`|#I`=jbD2&lHt-Pl{tTIE@NcH&qj}RpgJUC*0>4f$ zTqfh6%syqVLD`jQS^o8=x^I|i*}U0AzuX$1 zU^RZDv#y^NF5ekBFLI{QIoKX(a8vwn8}ftar+jhNV&A4l8;-GRMx@cD4bgdv$TQiaMAN%OEU@bNiO&19a?=35c%!$0W|67U zh_FOxOd{Od{5LYuWO$JknM6cC<*-=XG4%PCX^YHZB&CZ(L{T zA6NmK@K%2@wdEZBTbA*`CahB9;)C*x8|MK3eVh-I#zisWCe=kKlB*OL1}#K~3t$wz zAUd27-hDMj(PL|+)vDrB^~f6Q+%I94$D{rYW_e_l#3JRf^)Hbk8AXq*$2555@hD;x z)gVtH7s;D1KwQ1@c+?V|-@FMyrfbgr#4dUq<2Z%C$>MWITdb%=Dnc&2Wg75xuo%ar zN@H<|=(Rvw#9iE?YyVf=qCE>zB6omW6i+N1aElfYw`g*F#Noy07ttYQ#G>o=*QItl zxU}2bplr6GN5WcE;eMaB&4E|AaT4{AFqj?+YzzffKp=lD09@W4eMLhotndBup6D?e zePWA6v+Uq!fdRfxgJ`x`0KHrwd}g;rv)0lqm94d#ni~^40;iMs5iVj>ezlZdfD}mL z=y!fHL0P|ABqE}jotK<%5UBI3_sIPt(@%ND>UU57rVpO+`*?0huqj&q>F^0StOp08 zh@grV9UvcYhoVKZ)j~%+ymUFFx{GcHwXVI#1pr}KjgkzaUcw323AF3VydWZwgH=QX zy_7|XmhDE{>J7x!2W}|Y`ghFc*vQOt=q1_^F+Suo|KA_8N`P}^gXhT3m%OM)ePNE#3>_7K)aT6HS3 zhsavESMaZknMf49%V&5(g~)~?w*}=R9i$}amh*_*GUVB$3{PyDv8FsBnnnm&ibNbD zfy#+PBoYyaDn*)TVJ4C?B&Ey#zmSfQ9g;rb+CiH>(2R7Me+Bbtpb+D9FxQH1cFfpZP33hz)2-H{|A zuM{Mz6aSGcA)1J%t&2=uKrErfmSuCI{&$v;-K@s&HxQNa*t<`-**t0e%rmC*_DDHn ztAAn}&QD_weJR%obq9_;p0#4mviVCq$Ie+2Hp44;&5XTsjZ5P;E=y?g{x=c(jbgw; z{HiW`6UavDT8&whi44KQYqT&1Rz`^OEE+`g>W5Z#PVF$Dq%J$zxg03EV-bxO^t+_C zY@+%Me_L(1z|>OD+r#Z^Ty45}oYn+MSwt(% zJ-$Hh@jA^t4kR{`M4iG#L$mORAdkwOCvO0}b~wdxf5)Aocn8kx`IPR?-Wa=MXKcuE z&>LhK+sA{Dav`7Dmbvkx_eDdDX7z=?VMr7yev0}ec7aZfs(>NkW;up*2Gl3o`#Ce} z9~jaZF`IXyW6BAns!&Kda@6O4DWV)X`dbl%90efxp@Uwo0YQU=*D6tcwD1~GMnS5j zfkC96lzd0rS*Rfh_?h=mm9wL75B-fDow*XJZL9I_F9K4}mza=A$;fvnx7#;L!-Y(! znMO%6x)R)(lzpR;;x-*Qwr;kW>TO;ydx#MvBncHMyG9XOWWqHeMn)h;xJ955-6G(~ zSP3|)pcXg6Ln8k{6eSmU^oO}Jc!cjHcyueh0(kVR7{h(KO2~+}yjX@7;axC`>L?Kp z31eZ-XM4|yx>7tadeb#8)z@{qs0 zP~ODVPG@VvkEY)z{3!c=Iet{JZi$-0^Ufg$~oE-@rmjjs4pRS0?cOw_glIqD+n ztdYdB&Z7H4?4=C=)>@c}%xPT!&3%=T7QUso$vGz-S%1h3cAnw2e`fZX!Oq2G{9fhF z5#I%}OH=IEJ-zgAxqk8qe0rMgLRdPn#d_th>_NKsW=)wi_N(a^O}5WgR5^ zay$wus@!H0k3vfk9wl2!;!$NSwN+iqO+cd#AqC29gdjGKKummuW#yM)S@+Vl)%s%k2yM0gPB(V_&Uw3=)Go^s zD5rT3(K$yeLqb}AOmu9Kkk+eZsQ@)Z;K17W%}Ejwx(z!kY`x41_(!rtyzV6v;w8~8 zlMC(2C$#IHmUooUuBjj$nB{~x-h|4?$^Rm5kR5SCH!^`q&_sqd$hYfiZ|eysSSoUX}|O`=b&s3&5Bc+iHIWY-=zV zuq0iKl}pmCSh*zq5vxfhuWA!o)<-n7iH0pJ0A}@3bb_SuIEA5PkMpl|BozKVfGTTHGRfOhO&?q6dwoRm97J>9op5El$n-q3V#? z8dd@TyUETWstylnX(HqOv^0_Nz+c`(2;T3`@$~5M?9qwjt10o`n!6V7_sN{lAh>G* zpHCe&0_fBn$5NzxcoS*LxoHyhAaGOSy^+ZJQ76_{uOxYEjbwwL_=)TP6ZQwd8EV8L zXF{Z?H}zS#sQ3D_T?ozq1Gw8gS8wV76mD;}<6Q*xDSI=;OREoN-}bsa?x5;rbiT>E zA*T12OkV335P33J#co`&rHQz!qgyqqGS!I@C0hNwL)0=US(&27ySvm^j7rue^_6#n z)f#H`pMyjzQ&D-cU>HteyiF{a7n{MnxB+rSB!2_RV-tjl^3vry#~75I*$C!&sS`s6 zkBn^oW>xmF4)^FE9pn_FUGU%EdLPmAe2D<)@ZItV##0s&BNvu$=3)uJuR(OTxx_nw;h^Ir-EUqxk zN-7y_G5LINXFGIn`~XIXq>>4e;$ptmlXJA5oTK&R9IYqItJ)Grq_xvUE|UeDE8&R6 z=@Lhja-?f=;AjAnBc0^XP{qG#24udubUKXB4$*xTjmRN5=>WMS3U%k=QtTJlN4L-5 ze8EES^ELo;#MkG2{ON+u8~h?Jl3&{!Km|ic1ycXFhLq|5k0G*op#O_Q%JeVkXzm68U`au4SzzY@u$+ON zH9AlkjNP<+Ig_X5RGi57Ax;?ccp7v{<`P2x)(hh;;nhMIV>Hk1dkx5}DqjAcfJ~d+ zfe^>n;UNTD*)FyA40w3kJ|RG}x&$s%0!hg0W7#?dslwMLeZ2q66(NF zKb6m4>RXvkwqZrw%5|$e(Vfg|wnqNgBo5s+KWM(5*U4u1AHQe`r~sX<-E2lzU9gvS zCSOJvpUJWBqX^>%05%=~*w7LlmKUxu$|@9pF z^wV54gs9#A(@jH7FVa>evMXL3RVM2wiJThU8rwHY6M9tNl{+Fcn`FskBlme*mAy7! zKXum9#dFDR)2cG6a#b{N6-7u#wH1@#>MmC}^{!E_s*KXAf~<`|QZFv8grr`KDMwN- zCTd8kyLK9q>My*G3$K>&_b`i;gQ*#?d2lQeZBm^wJF4C?>`z1#F+fi2`;8ejCEOor zJj^f8(N7FtWSNc0%$%|`Ji)(-x>>@eljCO`T4;)#yg;UDynr-~!V?ptg2awdNyaU! z;#bA5MPD*cT(xCtPLqx3OXh(y^mxmgb0%T;_#5-sO_TGC+oE*uC(*V^Z*^sVrX&@V zrI1v9C^$`s?kV$-MTd~t>Wn#g^%0wG^=Bbri$5D|ydiGOl#s1uO32#ol#m(@3xu@1 z?`atz(V#~~E$~Uam3st~1{&Orc`-tEysHti4ev$Bj)$AQ5O8o27-|8g+K9@H=1LNI z4^S$SL|%@NNg^+2S4{8h~||Vs1K;Fn@aNVBTJlDY%pwH)Q(A$RiN#pXnnbZ^-nKGmgmg zkuz>oN*}rZ24xD_e@()<`){Z}(}fGj)hm(LNZ`n;gyu$GlV~n-N0#eT zve!{{T4xkHxpy?15TPG6GkWek(}sD|)_#r-2;*~V?5sTtO$#FlV(hr@W2BI2wa@lf z!jgmqn~j@SZCJf_t>^6c?F$Yx*`iaPn1dqZEN0b>T)SiL?k4I{obXr%Z8&N>t0KnY zZ(uO5@hfGD>*ONP6$zso622ZH`Ez-xL^(p_9%y^EZ z4(b(*K+U|*B#GzxXOxD}h#!?v>>i{N!|r;j9Si&QAPpYgfmswR&)UdDk|xEdL1J_w zYBTaib{9s86hG-p<%_;VD0-aA>;Hs63WAHNJX{l@=~=2)9>Rv;`aioQGe;6;{7l1) z%L+OSo*dP;5>L)o82<(!hbK{yQAt!}_#jziIN_&MB(AB*s6kqhQAt!}R8Scmt}>MD zE}5WYD!HxbatN6xTZ=|e^t-}$0>&vIomeSKVD0a=uLZs66}e^Q>#=F21?E3?Ql2#K&|lyx*C56Y3BMQ zHx2IaB+(Fj^U+L&%hN`u9G<}2`T=#G)O)dcf=R=j-Qq}k95qxqJud&z>*$(*p{s*W z11LIBLeXNXP7(cGVV{A?^R|7q+DK8IqQ6Wd`J}5v<4wwwK#9m3D|KM>qu8Km9rr18 zWtt?+`thZ5%=$^7t7=cW%Bmq472GuYdrq?it3^KvR+kA)k__BPHQ{$!_GKx=h%775-;kX@%GT9`N=8#pAzc!sk&R$?K zQEthxI?78)$^Qq2ScNB2rKmd)u`w_thAX|~%_2&Z0v=uYWC_rQ_;Z*o*Tb-=i@IXQ ztP8*biJ#*^s&_Nylv~sHQce{2!c9peT}wj+Ab1y6>=#ATRASCOFe8i@$BfmuP?jum z)5`c&Yhyg8uiZB1WRtB(WMvKtm-F6MU0_V&?2IOIffpyV1%4_P_@Uvnx<}77Es0;S zZf*=H6y}(@GgnR4=2&xVROUjHh7+SawdRGvNX888J5fzE$dr|NYE4RKi7#C^^!)-2 zhdu@hF76IqOi3y8uyki&=^h_Sl=;xb6KMIafGuMJ*78$6m^jJ|@|Aj0FR>ICd(XW@ zRzi}05KTYQkYt3~VyO&5bpz(>8Kn7U&>AK`Fjui2}OPo=thzIh${Vr*Vn?!*JW&j zq}M40M?#Q&wJP0B)9d7->8}`P7G- zqS<)2!PhS4^Nu`NSp-q6s!>5GC29L4=!Ri6`0q zW(jFX^A1V!OY)Osz(KCiz(F!J&_rosq#^Di1SWYCAuuUGEG2~4-Qj@{+jwgPeQKmY z$hR4oaD4~}im0In@c_#xqF`k5*7q@on%&0H9kbTrl-E(YU2xd>3NZzdrPaQ`{v24S z#uY3dP=xBbESPexpo{D&Uwy#le)Z3`@>fHz!FU@H0WqJ!Bd67h+E?WBj;~-1%9l%W zKFwQC@@W;gC(kuV0M(3AR8w>h0)pT(VYbYmbiD34Qo_BcKEb^Z3`YV;xECov%F{;5 zER3?=(kmmQ@J@cYaLOHW!bmv;QMyQ%jL!EzmM~*_RUQ{e;@=Z9Ce+FjO9&VeXUg=EkQp0w5&}l{kFxxc#En4; zNtrJe8A23fL2oxwj@5~-v3h4>#TpqU!`^ptQO!$79IR;uk zV6DLIQ}$T3fHT~tYV5I+b-=qJm~M}acO2{@{waH6T-?9%200KTsnwDM7#>5ZX2PvV zHKT;b*xvygP|ot>RN;mM$}0v9ybX3H=o;-SzhRf?(oy)1R}D>Ok!5w_xLDlNjZUa9|4_B2+&M zQ%}^VHSV~0;a5@RaFGK9V3Y9GG*Q2*MNy%J0>fDxZ@Oz<(nltB_i<>V$6OlvJ4%MJ9yogJH9Hc~sFs2AHf}X-(r=4j|GiPn;a%!68~a+k zu&XeqIP0%^cVS}#$^8r&)(r=PsVcjuDoUwMU8m0diwQ4#fwLK2Z2MWY@M_R%Z?(iQ zY3tM@#&~vgYhqef_|~B&HAXiqJbX0v@?T{;9`C+$;z`?wncRCDwdb3n<_Xc(7X$0l zVd9Z^WH>VM`44ROVgAx6+zL`HYDOt6ml}>L&c^hN>cNyxu9bYnL<1*cnmP?C-HnW1 zgTBI_p{4NQIt_Jt;S!x#p@r%M9>l* zja!>9*iaE_cdFk~brWZYnW*EaRjG!xIcvhwA7}E`#ae%COBPLik7T7}qT^37n%92J zG7syN5@d@!mv!32^?d=krn51kvGK?l-exFs*5q|Qh0c=sD>y&z1O7%3E++Ce?;)sh zBG;!;ST5H+Wf$4@GG+CQM2Jx;?}*@qml2tiIa&2aJknVw-t%jwim0wI-nBiFD1(HO zNy-_4mp9{+kRcv9FONJ?q&v+b~8zQrap6^EpP5ryQB1^*R0S*lD!H3-+4ViS38?kEFvFj4J4H* zCF4p<@He&;=tjvn$37e#?N*mMj?^{QxenmGI>b4cxtq7vJM0+(M|Gm@D$yRB!-2xM zQMAPyD>Slbyy9HuX)kvZj}?uVqm9*rI$jH>PNt%v+hIqrBv!4xOX$!g#+}))F~ubG z`fnC~J=my@!>!o9b7zH3T0ifM3116+nA(tT^0p0_{Z50kU&UvnM#cBRcYS&HB5M01 zTVjzcX+1(z2kYL72^(ipgY{$9&ADV$L+nMo9{K~qq0xt^1N~8<1HBqj*5s#fRF7=( z5VgK+^3s=Olb3$U)G@p(@_C~hX)P@JtG^kP18CYMvN20~A@Tk=ICo198<-8jX)!3l%opUD){j z7HWlVfGw4Qxt8lp*^z50*^ZCPekJGNNL=76x^lT##b6#^IT$sy#59BjZ za7c`}S^XUZs}_Fy7dDsDPA@n9F0Y+b`GW?D|%p%A>01ZNPS*&-rSSUY5bWA95^q2t0_jm zbzL0y9!^ECSwt(_V*c`TCPJl1HKW)L_NSc6c{k-BGkAcySY2Eah2O?F*P*BY-uWn9 zh-#s_kL)w$WWLTh-5>Wxl}yB0*1-FU5EU^C%VaP0Rk;$^uZe?!ktCYy8ndHVc1V#! zdCe^Lqk5b-7>b{ALplN+?57P<;5EDwG4PX+MVX77=Xe{N)yl*HpdVg<(*d8m;~YVO2|Mb6PGw z=Pi9vzU60iId7fGjxJj>JJKs!AGsh>F1Gy^AjBGn$4n}n!9LF%yL#zyPv954Sp!&a zFbB>T^L9<$4OdNcD{DAkYPiGHaPiAym&O{=6xplSZT*HfhRalH{nRq+i^*%l4=urn zwJwWZK7YQ~O#NFIKCjTI_B{0>e+@QRsg*Z>hAiHQp~b@i3paj+2DQe=(>kn6T)t_^ z+Qpt_4a@7V!%0u>c1;k~(%ozH3xby|V0!K~nb{4zCG6Q2v)SZz;RlzPc(aQ@W)${K z+4SEIt8GdC-&b1|Ii$p^Y#$P|&=jxcV@THS_;}Y-%^cuex-sViUgHJWQQoC5u#!-F z9V$rq9}AAJLSN=}pTnu7vfjLx`e*X(jEZX{mcvT(=08_P*R1JihgtKrl@Ku9xiHl} zS9kWSb?IL%j<>8et+mWt71YEzH`yMeJGpT~YSNs90Vd~Px;|fqfx|qRS4Z+|*J^5> zAH&rx{UiqW88nnJ`o;CqH0EQy{p3KOVNEAz~V*#;f`4B>q^fc z?!=<%AL6x^KfXGU{lrVuee)n??|yTHTDSSP!;9t`J9w93!Ral2Hw+8ku_t|N;*nzs zQzv{eYswIl_z-6T({a5{d0cNM&P!?ejM=kFmbAx0Y7xheeZ+={g^rimBvI?A@OX^C z@27i<>isEv!nqE!>Vn~&V|aS2CEc+atQ$W1?g79Eu~ zm$UEaq&*Vr5i+t$v5YuAw|!eU0oVwvBpDq--?0asT8Cpl|jXS~qOB)@SX#@2XZk z3;WTz%Gnc20?*Ichp7Qh2ixp0me!^mvkMJW2cvSsb~JVtsJRiK=57HoC&Jm0esjZH z)Z{fok@pEds=YuM5AlW+)A-pt^PWi`Yqc9B^x0R?2;^ zEsHkK-)Q`P<%W$rx$bp#z&XDKzKjv<7c7~rSeoO7k~|V&I>>!7C8_XI1CGWI>l=)W+lkAfSJ|{ZgjO`(Jb+4hHP38V!C4o9?k3YvnM>nnSVQy z(^Ha^$grUGy=O6KS3k7tukL15|3_=qmw4mJjCWMA-(UBrbJ}0*8+#Gd z0*z>zd5-j~U!i@Tr;>K0vudM3le8iNF{Ynq93xe^N5a!TPb6Z9ZD)nZ{$bO|A=&7^xyymvps1qUU zM9bN-=}_0#kUHu4c;@_>>#%O?P>a`n(p>Oze-Ts{qyf!V_ zxG>(xTVOUU_FrUKI%hV{?rH6_KlrZ2II#_H{W?P+YXlDd@gFw-E1aiV2oat_GlJ0sV>SrcAhjC~W(askTywaNduSdOI{#S-3**ljY+dc8hJUXiU zm7c<9s;HXtV3({?G%CTSc)#G_gW7u|go0#aWx2 zxR4wRd2xdVc#s zs*di(V4CVZfSMLpvUQ97&GB;%E-@}!_~pWdUc89|ovyw%(c*Z|WowPp;@>;FmRj*e zbMh057nZ_LEN=e&x=qQ>)65AH#%MJ7*762dikizFSs$7fOPsqtM(^~8@94~B=@lCJ zzIX-NbT@K+oNkE*cD1joc`I}|Ln$z&w9zG4O4B}cAiTdqd8t2XO{=vm8dg;?H-rsP zBbc&ffWs56-d>u$?olF(?rlZ$-oj^fVH%q6Jzx4Xh0a_Oe{QzBTG$WTV^dk@H z5{8cTu-8Rfj`eU1$Hzw=&PJ7+^o;CjA*ZOPiJ2-mg)={pF5=Ao7IG28`jd+Y_fWwN z`^Rt%ca=-`ifH~3>>Ff7XhHsAtVL>MJe)}C@#Fh}Iy4&jj}!3!5Y1ccZxOFo5@386 z3+ZEgXA221)Hw=n;~MEUuF>4aSj}yW)!asW{DPS}R(gy0>M;}HAWeDfz1>?h+f>-@ z0k1!uj(g}HMAItfSDZ+`5Jt~?(&#B_u6pg&#jIHUS`@al3c_?zLs{X3Iun z+=|#0Ygc&gUK1CW#NUVmm2m#z)Ir zx#mE`IuvxF zjtxML=3cxSLKGW0M#!b(*;0rd74D@x^Or`6X#KB7iMF-7>n*wr$4fPD$kda zkZNn13a3OzA|v|qrE-~3)Vl7KW(RvrlKe-!Ob0z+br}vyHnNHoh#r)yMtQbS+C38Uf4#;6OczdgVebPIM&dy46|`ZM#Ytyh$nRi^IOs$dSZ`x)Md+ox)xL(_r>5t8~Gv?`Y|e zN>f?(x9oIEy`p-T5!+as3Q6Y@ZCfd@udoaA#bSo4fGxxQ(EZmsSu6IoD(58Cojb|{ z&)D}_J)~Q(-hj1K|EWwXe+Es^8uSC}dq5&vJf8+3cXB}GhLR`Y%;N4F6Ph~SmmD*# z~%A)1C;}{NMTpwfkD8a4nIQfY#aiw+%+7u2iKr`-V4yUA<~Q1OR#t0{25KFKauz5 zRZ_IXdL&UttMSeuVYIGM1u7eZ?|(BxjdfnYm$^11r{1fvC33xeogF5=&CH*)_8=vA z!Nc^2x%Gw(&q{|;iu&6eS-dSof{UN#fq~~a+DP&`>mq;y>4}w|x5^$3%oKJuJ^s0QfUlIGUf`q1T{Of%_@dAt*QYNqFy( z2l{9r)P~lI?dQ5Y2A}dy^Lh6$-su|eeu;O7L2CI$_Bn$>{!0L>SC5k9%mAKg1F~pH zYRyk_RqZi zp{fp#U~yyU$c^&rO~a>2s_?b^@}-uyl6Z$paA2I3EWtUF>>t4WK}fLL|KJiNfXzmuZLY?8cNGIcQM{e#)ni z?jJEdG&SNBoRO8jYa(J-Eb`R+R`hb#hxeB0Mi3vDPgejpU!X*fO5f?RS;RQs?8K~M zU?GMd-ZVcvJPxyb2{t_ye89Jo#m!riy#u&6UjD0#bmP+N z-@yBF)Y5`=*(AN1H4alZov9?L7zbzgx9M5OzV}RBml~I>JC;ZVH2rPTUEZ2K7llp8 zH`)G|R_?=E{ujL6boD8s6sJ^SRUqvoRryNil$<&tmR4A;u*$_uY6om*#j9hW!J} zD_YIia~%8Q*kq<^C-HFmG_9t8E(IQUBWFwy`>gkNjc)X z&vI|1o8qgTxOWNfegX1G-~Lt^`7LGSx5UT`?MKM^y|qDDzqihj^?PdrdwL97iK27l z%?-qgn+hwwlx={zPG-;d0ibm@W0NV)nCqmrLl5zb-n_#{2xBnrag#Ow6!ydjX!>Ax zK3<96`9tu!TgtZ|G0%!qEI^byos1J zSD};Nw5wTH7*unw8imC>+|-jS-r=Ut=I#vE(NE6^TRu#0foJH$;imIH1*$2MnZw;K z@b(e+`+S%4ODjx%M<^4RKuUBx$GeZ`eaB$}kq1h=tOCLiSXtFo;p7ov%z6rAHlC@- z72n1yaQqAbmgsoz)$`or;Z2< z7Ob-C!J4Ti8Tmip(5G9s=jeG`^`15R5qg=&RA(f;AfzFeG;=YVx8%Rrv&rQ9LX*C4 zVAVFH$b+|6WC~$f?@vI|6A#Zmq!nB+!swJKlrIXI!s~9Sz<$(zj%3Iv?ArXPMhbP} z5(e$pibxrW83!*K1vkEs|Ilhmh8ro`S)!P-kXD$om@K0++KKm-XlJGn^QOE#+z~V{ zEx7l;#Ssj{itO4lK~eC?GC@)B2)yo=_S%oKt`O_sF=g!8=49@cQ6w~2B@cjc1|bjb zSw<8Ezl;n{>^1wT+0&?`vfGGrCY6bh=++Do65X01LZWyPwrvJQLh(^00@$x&(bG2V z_abdHXR%Xs1COxTyf61+9xqErig9q0-N{*#<;b1Ki9Jx+bI#k7nAA#=r&)Wl9+hM^ zel4EV^JRL{zhFJqKB>3X`h;Bq4mWG^J$-*H?OA8+smvYzl39SL$M>_OUun;Kz}vmc z{igFyFY@*|y!#a1cO>YsZT-I=9V1zvl{g|=-?c9#TWo@|F{IJ-LswKXf6ZAA1cc%uYmvq;G>l zQ-Rm0(D0QJwF(XS_w%>0*WM%0kG^m4PB#>CD}7lHBrjF?D(CM=I)4G9S?N4~x5uTg zO(PX+2lIYc+UJ-IK2#RLQv3mp7JyFH;B>uWU-EYr;SJW!D#8BiLE`Z#j6%}uPP~hb zzHKrk`%qEOc>7&p%bj@FSZL01>V#;== z|5HkiI35d|FCi}4yb`Rs-Tl?93D74d*Q$bmEQj;qc7npFlL^^^Q70#WQPBJZqZT+d zzc5T+YtsBq4uR%J3XA&E9M;qyP(niMDmj>OS@+weF3}GKgd2 zmE>X8_nE#wcnh_ZbemE)}P)tfNjKoi7I0onNzY(Uy!47kAyVyep ziVXyZI{idd5#X0#H4e2-*D9NO_?IdB58Q>mlmdN;bv8j0U@zR3_!II(?B?NgCYB^c zg@wmN22EL$o~_#~--bEKuUh74<*Qv_K8D}9)Icd9Lo$cGHdMlrgshy!y!ArrTUY!# z_hlYX!nlvqs4TRiUK#_fE^0VX{DAf*_Rn&#QkATqAjp8_jzLP@rA0hAH=xEv@R9kMNZ&y;?$A7sz|23VLM$i$>m zq&%>`U;rb^gRAZ7!qznKV6ru@LQN-ov_17Af8>6tA6$qY=>G>T>ybO&;k&2O^7ba&Pgpe4we6O zQtYSL$8{= zRLy?EDzFqfqu5m<+AIH^`>x6v!Ui%PIcc94Cgy*HiMf$X%nvDqZLRa($@K%+R-A^2 zUR%2Ns28**Dp>b{k`I5WKe+M|lLp}2GFmjy`->0l`nUd}nn^1WTxQKMny03#h88V5 ztdoD|e^}u&@fFc&&0mPgBJOTA)(=p>5DM58S4dlShdsA!rv6uSOlm5*_YEkZDRy?wdARD*HcG4KFLtKHoo zS=>9++SfdbqS=^X__X~(m031f^6N*lADZO0h-O<&k43W|!c!}v*@B)%;q1Rm^}^Y0 zD4cCE)rGUIruyM*ns&spEi;hzAW8B|+%u0qbHK^PM~L+GfT`sa!fFE_AY%?*k)T+? zkVUgQn7Oi-r3vw|8b!0M>7_9PHB|=Yi6m^i5U(oWu_azr!h;kEM|%Vu?G*;Z2@qut zb_Kb!R4XFFV#30CHz_PTJSk$kdoFt=SF(Iu%SME}4=~MO1daRA~wKrI}** z9JU4gg3Py#k|~X=H)${0qgxV2LQM5j^KW;aBpOXpsA`KaYS=xAM0^UvY;Ul=HbpDF%Nf~5s|ui zfP0+g0Sy)~54X`nLByf=i8yqO4Aj7ieU3LEH90&GHApVj%OO(EpM3OvGA>oT1LCXw zc!$2+caaSfc6tzIp}&@?CP|@Nmz>wL=0_yUWtt9!mQyi;mebyiW#Uu}>!iZ>M^cCZ z?%(lRBI>laLe%L&L=-;do8UMgvpssG{I#Ts_Mc_#T}v5LS4jK+RM-MLFOvJc#J!E& zcP95F*P#y9dQ>Lsl8#)e%>Ao&`|B8c7THyq7GyxU2#gvi2#iWAAP zFq<(+wnXu}veP?VH%X8ynj|G*hx%#MChTN8CX>#=hgy5p{wxB?*{;b^^A2D^Kh~uw za&I%yxkGbU%2~Dh9X)$FU7|5}x4ezr180S%5a>TRZ3~sb7d|%l!hvFT3J+Z0o&p6Y z84AuMC^)O2;JBu$FiDxJ!b}R_QSeTO$6~&!DdHSKZ~X< z{}Syr%jAbJ)-Um0u+^_~GyeVB9!Y;Y5H~EiF0A(J6oba6tQ_r| zDVm*%9k6{8BnuY&L>nBK&y)Rr#Kd1?zQ_`3hyXSAW^9ZI zM$c7tr>+Hb0Ps;Zbd~S<|J$~RgCoVh3gKpCp>4AEtlJ?egZZW)p$G4|nD%CrqV$Bv z+>+1xiL9Q#62*C69az5QNdA4#ZEeRhK+j{Lt8eGyd?M?US1nko;tN#-M+ zd7wyV?$lATzsvgLlz`j3OE$I(%LN4Z(y?(?dN@zGKmr2{2M{0veDmp~oraUvoqF|D z8jx?{-W5KE?`qH5D!4V!LI^?S4-iy-QZ&&b#JK7}zJ8iE_s@B1`4Uf}qqfRds4UJQ zRQWbOSDf@WQdu-rfnP%TS6`Pz&^geXV$M~8GeY3K+iT%aXO#0fP@%p8%iN{Q}{|m%=74w1d=-VLvT-8{EJqQL5hXy-bi8>#xWiIrrS9sq};*`E` z#6Hx+g1v{^h@Dh#3=QyCMFYJ5mNoxbhPIyOTIy1NKSz0~#CHo)tjkqv4oeyCaHF2g|5w0plxT+r$E+JV}{~%e_iey!SD4{Y7(GunbHkk&@ zLCkwk?cw!1_gf|;9lQx}AmZK8c?#Gvlmp%w?5%geBMLkfOrfj_?E4&=C~<)Y{Vf;Z z$AE$}LYixzO4093vCETw-?TmLlhOFcKyD2D&6jVW435cNFX{HPDe=Qidb@gnfrrv!aZ7!FdwlVet?S2*nV98-aaUuY$A{RgjKk zO-w*c@zdB%){6)#cli?u`EKp+B0JgiRYv}?e^EuLMs^!oIOXb6UBC9ME5g;gk!J8E z?=x5}zsujF>Qa2AUV$mT`dE$W9PaT^^~aBHaJSM&t>7eR3WqpqOp(HD-2|KWj=Fg5 zQKJs2>;@pgMNeNWIp*?*Oud*MEkAjM$oV8=|Dj`^^{Y}eHsko58J{lE`2(IRVAaA< zJ%A`yh^iF3(OH#(FaB*|3cmZJ{J!>MlpVP7A>&jnm|wMIgo_T8*&cV7;IQa!#eL8i(Qk zM2(?7HHOEjF%UKS1JZ@0sAYPo$DtFajx5a};p5v8)m$%m)zjsL_we{uf1 z!=q-+i`TKHZzA!Xif3E%XFg><`=o(`iG^@@SZ`I-tCxN98^oB6pJeVYIPS@MAtl*r z8V4pV(HSvs_3CH0K18m3UHvR><0CB3Xg}pcMyQb=3V1-9vzV6oVXfMXO099Es5M?o zwZCgFQl>#y*Eh%?(D#(2M7(htJk7dqle){J{yCbBGTME=9jYs%9mt;V zh_NYEv{T%GXhQaT4~w7mTa?UBb(5RJ1@b;JbhR^ajT~OpT)YOjP0N~lYK3kjYyJ_6 zSM1M=lq*q5TZbJzv9A7x+dA+=`M_YgzGsM^UTD7g)G6E(NNZiszv3NneR+aQxD zyA5_z>TZE0`x^>xgG{3GHpuij?!?zb?QMy|1W|fROO2Q?m`r}JpAbQfmgM%3V6Z?b zwEe9_8D~YSDro2}c~>!wqUKiP7$jbpq_r^gCT z_aRQ-JwS}Ub3oL!Cb8x};^K2YWR7aEjH>3!&qd+x1!v;;e%B9Iw1Sq?p&gjO{odgn zKIR?)+z%7q52@pY(@i{}vCYn!21;`hks7cDCQ-R*xEv^~?ywmmSROWA1j}(HYIUbm zu-raHqDtMmvHBeWwHMr!XSeSmk+Dryr)$_3Azn1@3yOH%%~izf?q;WW)gN@On;3Pj z5U)F%fh0`hz6jw0SjnABZiD2>qP+Jrkr`faNo0oKy(BWj3qEsYhJ!YS>Rhm*qwEG= zu;%V)sL@s9NTL~mG2Y2Cm}bP0CMa_x3AuNS5Od*Z|JYnOMi5o<2$~DIy`V`>Ba(*7 z1lcWbRf6MnEa`{a!-(BvitCB! z)W5qFlMhwAbh^+Xl5HatY24Ozqh0N!h1~Q-V5%RfjN(ZWoohLYL6m7*J{xlBp8V})6wEPD}M%z)T zb`BV3>gk)OUf02Rv5TX!#wb5|9@V{ux>H=^@NkuvYV3*1K|Vs9n!sP|g+0sTh-0Sq zq~-Oo*fl)t?Je`E$`p@ie4gV|QEQ5){3s9B7K{GfL#CsWP?QK)mr{kWax}qU?TOn( zdGKq{03CE#;#6u}B#cLm%bA5^d>8>=u3Qo_W9y2`dKze>Vqx5fovpvNl+IB;WU>n9 zRA;3Uw_fS6p>gT32UALitMdsKqop*EHM=1pI@n87;}1L9_J$BS=v){OcX- zcC+(Xcj-O&+Euv+d+sHPKVz{7rNiH)blA=t0rNyWG4tlI6btkk{hx0g6h(TSFEv8ch>xvqC*(@G-3W4(Xx<~EgO`r zi$afZDqA)xtfPu<#9$P53=`5blZH?FpQ3YFp>w@W^J9bPPgaeV&(=3`MLqK2>SGVT z_zPGZ-@&YXN{BTIk`+05nL$ocH>D(Q-HcLUL&2V%Iq`+=RGXZ-*%hE64lmRlv#Sx1p0GLpeDT>-IC{@CbBnWzZxh zE?owdzMHH?tQ|Gebf`NL#X*uN4ia=&#^EMs(3Yl@Q7d|n)4tGB3U`gcnn-4^QdV8# zp_Em(P&Orq6!iC+`ue-$agDjgr7`19r_#(ib*Ih4xv**=S+mi#c$3)B`(o#JnECr; z(soxA7f?+HxWimGSm;s8czqt*x*h&kU>IydSleF=(?=O*q8KI$tP87)#abE2nl2D) z#h!s8MQ31MV=o)^No{Q2S$qvJ0E807v}d@s5iFk?gIGSUA%7s2kC=C5$Yiq6AjQ1f z%V^%^x8zH#`7~+a!yZ59-aqhL-p?n1G?N3Z2~vPG+YV0MQPgb5KzEF0q&uDj1E^@* zd};^jj(gP!>o+8L#wRruugF5##k#yNEl6I9Oj;9z!us_IUK=*V zD}N{G;&H7@z}pE}$R}gyGG4baZo}Hup37ol=jh_cOvt zKX)>5@7m0fIuvYv7_n#}uD$1PTns3fK764z4}-&WhL9fYp$%WQVbd!8s*O}mznRME zPb6+i-n}8mi_cKy>|Pg_tk;|{C&wnPUE>+2j)`5n%B+i9wQOCO*JYGX^=mD${u$B} zycO?-K5>AA>eX6p36uMGmoD&KYSvCAIflx}Rp0&I2J8p?-&#V7`sgdqEcMT?<;zS5 zWL=rPHoJ#@K2Q{<5OUN({-icEVey8jjqXt!V-mM|ZcuMcO4`Pre^cTUR0lHS&D-?b z%t^q)Sf^ec6B}jLMVS|`3ypW@AM!D*ZHi?7g{dz~mZek&e}Len&U5wQe}I?TL}m$c z>E&tXnV($>l&7&*CrYxPWsH`i>ypi zB&T|2@dDOj54xpUCez+|+T#m3nl(c#=Tydpq?KfAv*s{cPkXS|g9+2>-HS(S$KXV@ zq5)*E9*ZUWPAOq+jEgD*_6jFe20>NEFd#lQ__b(kQsUQ~Kc!h&wqngn1@$W0Fnmp# zK)qT{ru@n>C+bzJEWTELhCc(KB{Xzs!u|dO1>|Izr2-uo2;a&w2)JmUr(_ppuP*S% zILquC4mie1y)n4EWgPjcHNRtUWk>iPHS6pz@sFp%NGTgq7l8d9|KNW3m=Q^v9quln#-rUSeHkUv1?+A1vo z&UQ`NIr$aV`&)v?vI%{sjK=@$W~l{Y?)~l-R_6U&_IvmHg-#5t0sQGRylZbRY2L|q zvxNPrO-WAJfQ22a zZw}XL#^V-)q#2KQUljMig2(-e1%Fsb2C+yn;=v-Pz&cQz6pTZwjH8AEwmeGHciam>U*(q82SW*Fpf|gA# zC=tTY>ism1TqlNBkz%#?`R-sXy9q_>i(umLmZbF?wjd!Ycv0exrDwcYrwA!#?fUpw zJvgi;4#Ob*CK#Y1%?AU%b~dHLBLN(5v1YKfzL`zM;0gL))tWD6$0sJPPxNxmgXW7- zm zC&OQpchqFtBB4)Kv56|kamhS~NVbGeCv_4_8UY-t6)dP5Qs_BbFA{JhkZ;&J3cBk}OhFWB~#}Pl> z4!XE2hZCaM0wuYCK~ee!orB!_O83Fh5Vv((wyfV;$5K=(6M5Ia5+eM)y+C_jt=T`- z(=<> z+TVlMhJLPsoHIZMONS4j;&b=_NLKsZC24Jd4TfyZ4;5^%yXP&>0v`dGUrxZUyVnKa z*FAOoMpO@bWvLQ3B_t#w=&nF^@ehD*<3#u(crVa&-3jrf*WA5MFkd>re3{|M@`@M# zVr-@SxSF+yka%ejmKvdlAzXTq5X5v`O-}%X7|IQRANB@TNIiryO=ULW&kb9!y!TERE(EAUS25);IUh9HGC!g zp*E+a#HZHIIQc4Q%BKm31aQa@*ZbfGCEf46A(HO*Dh`MbZW^qYCHjc>2fX0W8)Y_k zY^^s9mW85+|CQstkpdb77+*UKaYc<9E9EW z_(lC*wW8G`eSJ5n#^EW;smRQBCEgrwj`N(Q9(s8q&<<06){cu#-KB@IP<>@r%C<5c zUjYp6pt2PB_T$vcqNB&^KIIfE3uOHgDUh#d90XP-lqB}z+Y;H4iOW|rl!0;bu0-s>rDWX}(jwtao^`cU3{CF|c`>X<5fCVcCeq~5V6H6lruvNdV@hD>)} zq_U!xD_@ncC}~C7D)*?EnCM8)ShZ31TGertZgfoevZQfaMju^QywTnArUfkoM@djK z2&=6={F$O+k!PVH`WY!%j$W=)^SYc2vR%MtK|-N0IS)`km@RJTGU$7+9&Q;#jlBjR zb=!|fLlLt%QT9@i4L_O7PT8stXNLO$MF%lsy@m>T60mYE6CR(!H3Snr-=zUGKp%q} zzc2IFp&Bipbs8YemdCIr$x;vNP}bCdaeoGve)E$O*0fXmapI7@N$Ku7%ix;x68|V% z*0C3cO0prFAG52ZlYBq48?iX+0Lc^;jR48yxd-T%=0Z29115XTI#~SRb2cY9S)H2Q z@g0eEJt4WY4=Qu6%vPwW4KOI{gH((6xxd6(4VL6^Y;Z#F3c-fQTK!AvXq^PIG>&kE zvheNO2|jg(>fH9|2#9v%3e!lP9HLTAdRWDW6v+ncn7s55Q5-BUb;6ImVns?FR4wop z90KPb*dyuzjxmpCrQFIrE*i;_RS2w)JT^?QZXT+8`lX^Y1zVtC-A+X@Bsel?BUc z`SdhAtxYWtB>U*+P++g+IA}akIx;|S@2VOQxnSC?o#6;YHkGv-q~(^DD0(R4oo^QK zHkoF?Na;ULL^ba(($Ynqvd6bmxVP@lCgYawFU0XxR9pqo!ExKbdRNlj#)x^-^(_|jHiDuz`AgJ!ZJ@~Lz6}UDoQe-T#622Z zcEovtynG_yfp231Hp!Qf-i;Lh@?nc8@f*4n?)x1Ai{PkG7Ppagg6PuPIg7TuYC;lOA`49$H70f6As7|qu z0F_k+eVn`yfD#J{P$HfHC0dk8*~}-DGL(DFWzUa`OwRly!pjxwSCz>Mz@@klLp7&Hkh+W*RGArB#d<#kB zgEWP^cq+~Ia)3(xBet@&20bNOXhp_gF!x)*eMeF{_jrVTC5YNYysA?1iguvxJMkLe zRz5R9LYr}@?l0qdxZ62-CJe`&0?16z>#y~(z|F1le{X5XL_MX5>zr%8%3N>FSG84^96=P; zU-DJ{3WFa3Iv)&v1k^6ql79rWG1ro>@E1$I%3oRXr1_Wrnf>FNoTURCO~3`|*)wpm zK#u)tz+T%{GFO2xLl!Hp>}^YV9~ILfk?v*IcA!)~d6LU|#Z3xI+EY%Hv}TqqNFGSw zov!TQP14M%F{%2pjj61&T{;D6wS5|EGDk;wcKjloch>y~VNFA}ne|gKE7pWlTUH`J zawX+QP7{zom*5uBSd*|n-$RCZ8|Yq1W0hB16YnYf*u5!oZ;<*f9`6)**3hxbnZ8tcR;7uSng4VT|Kv>aCyEk9ahsEd{>Ty4X>r%_xU zMdrTC`FL!-7$Q-82d_l&9hk(l_U`zb%0q%JIrs}8Y~^^FZnD2Etp+FkO$sOR4Z+EK zCPCk=?@bDQ^a!Hw&J+{hDmz59JV;cP=S?vYj@s=hCc;t63o<%!)bbwVZb!`hTf8Ii zdu~w^Wwy|qCxrwh5 zu~laTtySj~Qt>33^X5xn{Wa$2bY*IcGQ2IlH0G*$u_) zhRkG5;I9hJF;ZoDevXkU%kztk{3Ok8{yt=$LHbBOh>Us8Ii2U4Q+gEfEL6hjQ>GBR}Y zbIM@of;=khjv?}p@FvK^7X^7`f=%=0mnnRrClaIL<#MfN@u4yb&Mz*?ut(BM86~rW z2E^zol^t&gyEv=Sq;>;pBNZkHWHlVDN(%d2gCITlph1uxd@x9m9-M0^4;cZASQC7} zAgtnp8J1EhJ|i}BD&mOvxY25%2N3DO6AXg%;0Zwr>GVV*eJmNwqvi(PMm&CQkRy{` zJVeBPu7`-&WSNSU-X-}|b$E35C-Xqoa=gCCIZr|anOs-OPdjQhXas29f?C;7s`l#)x zEdYU8wStcIMFJ=43;SXuI(q`!p`xwaZHyYv7!VnXX7UwnL(*q`qdVpOGEltv6NSJ0 z!QFOLe85?vl3jcoRy6QQooALALD}9~3w9sgnRQK*>{C@IV`goi;q{gzcWx&kI{2PY zNUQwoA5MyraE??!C<)6GU{bl%fZ?R{K@HG2=4uNqlV^f3fBRTMFLNDPLMKqP)Y>C! z4vKt$V)>TzYCnnFUZlFR=D$J(^X2ZjH@B4m>#qI~zDJ&UXWniZ_nV3|`R3gBA@57I zPuK~3bFsj&AwX-z`XyCHP>Jnec}q8#vd9^)=@ zo<1XsflP6ewEwOyLYWc>ZDAqwipu)xLfg~S9zK&IREd zr$@&qT6H2%{%DeQ5Ebsr=TFm$8==x1&Fr;ue$W6<-;XBpXU)1}T+O;9f|Z$~r2lB9 z$cD!ivf(u!Wy-(uDy`^wHT4SLX6gx(d;|m@>8Xcuc+05A@{)IP@3xFg$)M%!SraZMvy7N4F}f zP1lzxxBescN;DvYAQumee1KIi0d_@Q;}Sx}PF!xeB!x1~SeP~d)4c?TO9itf~=)a^O28T2=~7X$@Tf;CoNMzpb3hf&0T9_sI{vN_vlU)P;# zxawHbUI5_0ITtR}aAD}!fcVUsy6CO-?}^LVIP^MY+*f5N8TSURZp0y#JyRM_dH7X{&OH2uKs#vj3f>??+QlLL z-*SlX>*{l;LM$BAGnijRqIccS#9IE~)CEwVGr1n0wgRslCL|Q;j?hh@NP8X-<6R0x znh>o3+r9L7u?KNy;01w9pu@%`I7=Ra|ADidm1>q-|cNEjlzJPiNgv9Zv?0vQ^(OqD3pOn;v6fnCjx_j zZ?jN?Z*gpa{y`(vtWRI*4ciHQf+UlzuuNra3nHwpk6-6x?YwJ%WXGEo5>gT*FE$KE#lf; zal6;-(VyOwx+_Ptr7uJ4RO*L^vg?>-<|S|oJacXmZ>#&g$@JJzlprlU)<*~ zR5J8q)sz=mPtKNpQy0hWeM4stJ$D&l<86{$N-e7{1gb{HMlYP>IcvxKuXUFFGy6*R z&nzEc*9qsHN{{j;0uxU!09*8bN+|*rD% zP>{R|SRR(!(pA~Qo`Fy6o7Q}VR0T9;MYeyB_)b@BTt1NW%97;zyNPA*m{-mZS$cf+Wp@n78u? zCgXwmXdX3*#&Pr@25g%`Z%qjS+XSuO+Fv1$Sd0MV_e3bl65^u6_*f~VAh%O>ARC=E)UQf0p!tr+Ln3O zbQdg$WIbZMbICE@WJg?jOuGJK!UQ4_TX6zmokt04SDGW1L&QcoMQjfo5LH0g*$p_N zDs~`kDNuG66CBY>LfJvn)AQOA!INd`D|2>EAsNDWnLJd;n%vhZY?IcJB(5Myw6AbV zn02ajm~|Zvh{0a0FC7ddj@xev)2zxeC>zu1BNrNBI&4n?H~PS=r3H#o&yTj^OL3g;#;g$n1qsBz{^Y* zAc5n#&kioV3Nj{6HS&1=yH+!ChLQETgrFBt#o^FPkmMNywDUGEyROqr9Af-rx+-Er zRN^XJj4)0K2CG?{Ez&OYw%Anh`7o*Yf|@ZpXh zRpf8OmzUGOM*fnu8*6%%gsaI}-sD_$_Jdc@Y?eSd36HRAJyr`mLaXYF)Xe<-(b?|+ z!@$>Gbk!V z7!EAMk=9}C*(jP5?sIfMe=6Oa7Mr3!PRLG#7qm>9ot}oB^iL>M&iU$qR+i*$`+ffp z=v<`Ykhm-TUPWMBTpcpD$ zww~Xgw5>#^v2J9|&Pe~}!N9CZ%{v^q6__<`@%7cyLbgTU&|3oRZ$td~J+Tp2ZI4}c_yn#rT^v@{3>S?oGnyH!;7qf6NYj+yA4a^X3KiqBsM#Tsx+4K}5`{bYu zK{j2}di(fZ24N$8{3)QAKsGJ~*(dNZ$kv<)F#IZ?7LLED00W^@MERG+T6+`i3NnS5 zg@N4rML|AcNq)jTfk-ImCoIV*JgN&8ub=>4J`f1^-n)W+%mdLsOVGcFwZM)Bx>@Va z_#J%B&lj&wg6WzFj#LPabP|k9L2v{0h?a3LuKB(lP0eg5UT3 zfK|Q=0FrrP1r6jKfdLAR6NKa~?lXkoo2#gD=+f5A;`Z`EhrXnR8l5iN@N z1i}3mW=MZ@dU){haXBTa18ZuQtNyoT zw607>++~D6do6@seM@)j&PCqL2;YLJoyd8dqQc+)gq$(ovWCX>gOuNRNN@4>%HklP z%NyKBZUvxAI)zG#f|%E4$^IV*IY7dkQ&UomcQG|3`+~5hCu2B^J>m5Z5=l4(hk}HE zvLGjj&@XWbXP>6zP!MZ*M0!s}UvkY0J)MSG5*|duELng{=e!&(4ifWnv?xQ&%aWoX zzQ`V`1+)giYAh)V+yl%r06}S-HLfp(EFuKM$aA+6UTCmd*+_#jiV^orj>_cfxq8iyOj(HmFO5ji3*a^3__=8i;(A^+>+5#PIPH#5T(iA;jueW$0+{am-_9Tn>cmwaUo z*2q$={VCF-pp#Y>DLUy>q{l-ct<0H1dOT{5;LyCAdZx6V0`L>>`6i8_2!2h{=H)Sf zr~({O3NAY7VDRp!*DX{upT?Pu;FR96bzQQ_$=@AGVjxw*28gl_Fuv%Zg&T?Hldk5|H);Q z(mOO0`R2FZl}phubdl096n9-1K)9w8mPAb-(a2r;5AdgUe{Mx`34?K;&6{i!x~5Q! zU+)0pV#Tpofq4oIGt#qT=?r*BAHcfbmQ4yXK4CfO2>eDyg~IF#6=s*eeFoW6PYZ?e z8z%P>3gzpy3MH)Rt|bS^RDH&&R@$E-wKAPQai-pWz{PGrhF4`7d%04&zI|U#VZ<(@ z-ahOM+!fIFhpT4AtX>iB8M=J|pmtwZ%?N%Y_NAE7ap8-SCMHcdx`yQTL+5r-0bBJS z_5KFifzbc&lrhr(TgJgT2pw^eP*-qb5IMGk@U$aS>`m(wp_bY{7iWQOhoY4ngym)H zDXoi*k6FuJuuAqe2QH`-ZR2xA+4x-O8au=hVf|bj+|{4c8Q@Ydy8e=jF?TJL>Uh9| ztf%Cj+9SogF6g;Vowc?I-OiSlDUA3~HGJLbMe{vFk|QqZET!}8rC5JU!$XpzF6k{O z*-2!sw3g8i$q0`#M7@>&(Vf`5!lkdNK24y*YcZ5yo<=TBZuZ4Q^C$` zXSQ7!vhC1y3fXqKqATb%N1TG6dXf29r38SXA@9_hHe0#{m*ru&EZIfg3)tGI9RM^b zx_p+uJEuN6>IYi*j&pc(XQ~}VVjU*x4vJi>8O~fQSNBi3LFXd^N~#B*iX^+kL;{lR z`hyC&51M@14g5O!8BtN_b9q01Mv3;mfZQzQ;c{^wiu2hF<@q+ogWK)rou29oaHK+@eS+=yDLeQH#1@@6WLb@viw-4W$&7Q`V5Djt(k#(MjJ z`9@Pj>~c8Kn_e2pT7Mwbfu=U>0ZP;TEdubXkoRX_)fLz%MXX(KK#}$UEYvEJJ6i_0 z_zj#fL9BOY37!_yO3#j$=Rk<$;$Dy|eBLA`K}upRHY27e8_vmn{*($Wzt;xq2!V+p zpQx{u3xzG%-s+z{fnkJYm<7#sI1R;!ovOeD$cu&pD4;{l+^1sME!tx^y|&;7XUXx4_1q zf!I^hR?i~-GJAR@5-Db=Rwu+H#OOvt={3|Q&Pmqc?8O4fV&uTybdKBE2ejnRy}x`* z$f(1_-~}?Oqn|a)>3VaFY19BzsOHdQ5s0di z*Vc#j3_?)7nvPk`!V0Od{|w9gjiZ5lv+h0jpN-YYeQKE2sggs*=a%LRCGrSs~_Bt{~>rW?Ooo10Dxyy4^$`xXo^eBLq)=x zV>RWgIod1fYL0<|MKX%Lzrjf{1URZqT{x=pphh^VIgqKZ0Hg|cE&ykpNoX!0RfoUx z2NC;muB%Xyf9CL^{4VsK#b`pR zB%{@Vsa~Ta5xr-q(5iNRl@zoaMv;Pc)riHnADwjCswoj|3MHalGr9zyCOPkj9lZP(yf=8EEVFB(&*wFxaN1lm)rVD8vd+$Ox18Vqqoi{YdU#h# zJ?n-c+rk^Co}Hycn30%eThU{wXH9vvmU~~#;4RV#`cz-GDUF~{(P)Le@*Ug*2-wF^ zhxu?iV85Lf zCzWtu8zS%MZ{z{+_#_YCmI-;dZEPS9`=A5`kP;No6+J(ZnDwaK*|tE*b^DR`?NZSN z#31R7wBP?$rQ~kq<5c!tLR(rz*<+j5C&JeSM`T*M=f|X^Jg)l6ZbJ3tZgq;K?4~G| z8Fmw;j#1v21^_j^t*^S82Gpv-Hq)DwCaEeVJA^m=n7i%&{rd(mtdnSEiS)9Qy^&tl zG(;*b_xw4yRQA%z=RX#iod}F71R|0jF zSJJZE!&`2T+qNbZqMW*eGPOceBgzn!yE8JzWR6@x)g7cP{tiuIC&F?kMrJ4Pgf@7h zcRj${B~EzO6aAdHj|O5mxU&AKCemo z?Yx+^zXu7wN1G&mt|MzlFv;>2Af;WU{toiJH$@VdP_)Gu@yh~z6%A4Z`fBSU8t07R zl>8;TKv*X`lfP_a&HVQ%O4W7gac~DT95V8~_S297tU6ijwT^Hl)5}pLGQAw(cF+CF zSdRg3vUmFzxy z8Z~NxYS|gSN->w*-!+#!@J)fN(u$B(;1z?*97*fFXp=Z4`)ErJZNIEP&PPsA`xix& z(v|KwVsoY+SyNB!n{EPh1vmdRK}9#{_$6M7bHwyG`U-CTe*j>?qyAqoSQ_K6+JL_b zlavrxKou`0+0F|cKK&DzpSVjVV75$)* zM}ZO>lR#kQ&k$po=E<6Q%*+R*V8}MRU?15Lf(BTJ-PK?1J3r+@U~RG1_wrT!<@1K9zSzv$C?1Lur@v zN4}qG$(4?%(KeP^u@U?VMZb+xk%M3q5G;{EU{T;#SR0tWcMN}nmU^VJ0T2s8g*xB` zxq%l1g8msiX8@{UHTE+8%rvL3E;Vvl;xaEyjZvw=>o)?$$a5uA`fhWE7j7Jeb9~6u z`RZ{?mrkMbI(TPoGIZica{K9+ZrL$HV_dwaxa7ALXwIrVW!rFm06QLvR{> zp@Fu5isSGWMRblBQAFqPomRrwbt!0^>Vv4o3n_003ZSY%>}|?2)d1mi<=+fNo`lchPY8lTbRF* zwSA4q4-0T3`e%$MFGSd2H{pf|n*w1A6b}W6>=(LjO4WPea3P9?u$2kq2A6{J&@Y83 z3Tqd%<5Cetf|JoK9fF%?Ni7^-2>c`0Iw5e`kbNfLiM~EjLSxmueI};PL>@+_iRrTN zy3Yi*&s?k4uCaLJsLf;T@Ga>s7Og}+9VcGK7l`Hiy7Z3C1YUaH_nmL~8>q3f5 zg8d;mCgOhxO2vp|TndsyW|@fFA=8b5-MCapCWsl$2U%|CZwIPh14QIbK$c{=#XAxJ zi?YwKckia{>H2tp$*curR|~X=fi#K9X@p1X!(M|D>oCiDkqP5y>vUg0K;tvlWRFU4 z$Ka*`rNVo5^3v_mx~2AH3FvDMpWSORmZj+qlR!FiN z@nnAUAQ3~{ahOvU$HgAkxK#1#Uq!5~Dwj2J}70=>`w zIEWHu5GCa20pD>cC?}!kwNN&>kP$%H3}o%P)3HEEy3?*2S--(>TzqMylpG-C#%HeG zH+Cb51!~P9zjTLBpfn|y4hw89xMhOKi@9M$UW}8q@RN`%1Yb1>VZfyj?rBGiBn;Dv z4T36MD#ERx+gvrlf3o+gfY;r7HIRz+KDZhv&~4zQ66iKpO#PYTE>?5%j+J=q<+#$eio*F$5$AU*AcpKa;pM(eFv~nVhhT4Xj z2AnYU@NPW*3Gar~20Iex<{>K&Hd=3>-Tcg-$W*j8g1XW0UgMz|BXK$asvBYJ))+0Q z_p9-^zWD$;;PyY5vROs@#_v>(&%0?w6jFH1l5` ztmjF3&xNH!`cF+vBmF0jFG8z`r%Dva-uA5BVw4H=(nOfJz|_h3`rQ^$}d zYd7BprcU9^v&7)uSv zF(K0NA3PzhH|L(}xJ7q@bbf;Qd1^eZxAtr1j(MFKfZFmDd*K5}HNp12ru|4+ZxgZJ zMmpD9eGDLb;;~KtA9L>k7S+|Y4c{;@GaNd@AmuPa7m%)W1d^yRY7(O+v5OrQ1r-rN z1v~a$v7n+N7VKS7WA8D>R7;{MnphLv2hYIst$hYtp6C0&_j}*}zpnqvm5`Y^XYaMw zZfos*uY1j3wV|FbSZqZ8Y^~E&;#TGim1$RdaOdgiOSa$ASxb%*dEywla4ma_?$<;q zZxpVeynVBf$`uN@ zD}%KfY$oNcd|NnKq2;YL)AH(Si+j+`+#MmSv#6mX1F~Y>|0N7qIGWHM#Nb)0(Dl}n z4bfVlED1E1{d>~v@OjybwQu$!eXo#RQc-v1qxwqP>x*frSJ-)Jf2emJlJ-$u!3z5j z84sq3o21;DkA)gK+OCc2qU;2NY4fTrF-N~~aK}qJqL${?)-&_IY%Lka)LLR8I#@sE zUvcx2AN87>w>I6(&D-H1Z*!LpbP!@HJIhIWUr`j9&3G>Sm->Swd^&4I#Ji~q zXA$LCY)?H!oJGVIM;Ve7rri?Jld>+q&u7iymdrj^bm`Br(W2`UYf0_#iOU!%j!IpRB)%rE`Aa!ysQ9+B$ zaE@v2Fgl@i-Obb%jF{{x!Z=PPNf#8l*<-gnw~EQX37MM4S37ZvS!tBqjcpNF3A@7c z$C#7HEEI!&70j7qD2*2Joz4{fB~Bk(%`^wh^s(#(?K3=YOf7lM`-xnO77>2@pdmW$ zl0^oZRAn1Hw~brzk)hT>{n5Bl{j%rhQ5=DoKoM7sIWK3e7z^P#+ao4W?X%cQ&MvE~ z`?vE6IyyMOEH)}M%lyQtTm8wzzaaWu6E4UpvLMt_jwfA>rKn#auuPMilF6lxZ0j(7 z@bK2#qB)OCT`^J9o*`;l(d)P%UyK)>@BO2wAaW4@g}2hNunbea=vm4S5?m>3nf-o+ ztCiHCli%WLeM&>uG;{*`lpJ}rUo98TAzQ(h1%<_%~`GmS-VL?{z!h)@nIrH=s;(#QY z=vGIDUY=rLMeGSxy=?oYv1M(HwS$NXGH+1d#iP#~YaC=je`rzZ`pV)ZosIIl>dwVu zvvL*{9x}@PIOK5b6|pG|3tF%cbl1{6RSjMjRl_aO3BD zo*!V?Z)VFQ%GWp=VFiZI8%4Mn9`W{iK%3dG)c`rn69W>j2hn2C|gm# z`?3@IOAD57t1Mb0dejNISxVjl8t98?=u_^TMXQW+HN=v~$#}<@#{oC(L^>td^>Eui z(!kIt2sIxXkQ+@zv6RTACPX3@`6hxiol-#QYRld{1ZZ2I4MuEqU8pC&q(fLDztqWP z{6v6o4_od`VJ;EBzfsk5N~oY_p-Wg8`;=;n%?}@{x^w3Gtv5O2 z=>YjRbCQ_u$`L^~(N;}vQ@5AydnFO(9Vc4#U82V?!Xg&1k9n?Tq2Uzvl3ia1>rM{i zkQ%zos~RFUTqwL<$q=QIuJUuvT{Gl{94F{L@5Y&HvZ;y$MytOhyo!+>U+BHH%bnkzIzoe-B=SL+&}#W{4V_) zD8wz-ixuLQ>*h?Q^oL)mUsp1f(odYYg*?A~%yK{HHC47e?)OnrGQJY)F|-)a zch~0c*kyc7G%n>t<8qcP()53LfGnE$KgGnse7jr1-!QKN%9(I&{mH!9y?eUv!8K;F zlV?j&DmD9aH7n&NVt|%ki0_kHch}HqeU_dZz|vS2LOXD==(|UFGk*$I!P5%Vlczp? znG9v+yrpOP=0k6olPHM3F9bH2N+7&KA^B#%DjP2M%+8BW_KH$<(N0J zn-KLVrAVP)TT6L7u^Q2i=C-8IM|o0{5T@E;UP#}j9i?k#w@w1jO;Qsvx9a)UzAoidWl znK+8y%EXaw&cxyTrWiDA(yw+S=ZDkxldTmvn&z(`3*t7G1MMH1?^gV}>*0?7uusug zsF_31x#XFGY2s#SEU`Y%-&Q(#QSm(EyyCoB1HDXRE2KHB{FQ2}}px zVvL7i{DzRUQFM?n=ID!dWab~8J;hpgxU8myAWc;)`!dC{Kgv(5%qM?jyL(=FMy4el zVbkc;T09o*z86>%GTzyhOB_mgi0n4a2PfnZIpl+OvW8Hn?O2GBWy?L}kp$T_Y zvD(RIEHtqMZ+`U4Ux7~usqk)@I4=_Vtsc0#J6Q3Htl3SY+>4w;csfbuXZfmL$R@WHJ_}4KMBS!ew zm4tYBSax_pK3$iOBubc73ZOYwFIv+w+EeRC~rrI=m)=uFx4mqpwAGCjtwrIGpLlZABAi008T2 zvNJm{>(m8>*%PyK=4AZXUOY?rt2bOwR2aNac@*QMC`q zJZ~$Ul>5C2OYWlAee_gxBVzA{raqL?aUjnJF?(R$Pn5KApyW;$f&;JeFGkUSa{mcP zuBL|G{EJTpQapidnLWpA*B%vJe-(%TzZ;pBulu}|T_%>9!fK*>syix1#BgXzvsa?m z!;QB-sknEOh&2fs%F7I5aZgBdoRW+fmZKU7k~L2_ZpIZ;zck=ee>t;THf8C=MMg1@ zR+HLogdZM5_+jhI!wJcDk5E6Qg!+-U@hrSY<`)wATVZFzV>aMFfrl%ftiAX3{R+aR z5`g$T3wVVdzI8@Xpzc-$jrLMzuVni*)-)1yx6X(@KL{N2{jFkzGGDswQ;XU!gymv@ zyTDW4-m2i4e~t9$i(eJMu!-j684ehJbmgCb;Z3h3Akgpy=Km5gTz$eUKy~>)1*ksp zQh;w{57C3@{_9Qp71C}6AY6T|9w6LB&!wp@H%n7}f=i>TzD7vY7mA%RrwbDip{?$`G#_XM~b@TyCzu9}rt z^|PYHs{0&Zt#||X&ECMT3Z8e>{eOq^zTHqiQt2x!?}&PN2Adn|VInW6Y6QQ7dg&&Z z)w=Z=Z}8R!yutYdBi8L5tblj(6zuK@1-hH3Ab0Z=hapdK81l@p-4P0EH}7rGZn=5c z@H@Z)USoBC?H) zX@%}1Hm23imskNVuVZ%Ry_JxIVq;}Vo~V#0(c(d*K?-D$XLM&7d;@)%&7BsH{5Ju- z`yTxCdco(bRwOwyS{`@6;kz%c)GQ{~Bq&gz)aY@=W(Vcn?i%`Wl(Lp75M5f!yf~Ke zpRnAwFu8;l>pYG#i%^;bTUj7E-p1lSzir0it`qLP7KpI8vJZ>;=`Aqs3qGZ2^M0|7 z??hh`PFZuE_MIzl-1W$918PJ0ceR1ST8Z&}Np5R~**+OeXrwE#5|NQVzoG!~o^N}- zS#Nx~SwBCcKx|*86(^Yrr6=n)XQ#eKT+mMx9VTQg|tXw*+ zWPx$P)Y+4YJ&UJI98Y9UZ~7WLMx=yy?&T*Ppfw>N9ix(Q)8X-|;I%6N%cJxVuc>7o+_ znChetMZYp(ziBIFMEsfkMn}7(n9@UHg9nQB>st>2R=pB|MP;!#4i^<8(*?IlrCd9w)Ex15&b&7p>0o! zRdr-4x?zpuPSttQC<>l)XHfX)8Op_`7X3aGZsNLN6Pf2O=1vekiyJKN5xXa- z*a0~}^syx8X0;$)s~w>igS>?y%&_a(ErxOe-98gN=%P7oI=?EY88?6~9+L#qsSgBc zL^*f7CBt8D`%2!bd9sX1K<))N6dqGP@*CzAIYv}m+VY=u?PT0fZ7CRK?4VnvN z9zHC-)JGxSNPkD5qg!vLy2Y%km}YAYjYqm7`iYvziApKBb~9CuI^%NEo4T)KaGOe!n z>uY#;R8b<=pzgs@xwEXR`-}(?zpeEmDft4nnLBf$^KK$clh@TH3aqy@kF-|TLRiJL zOe>`bZxpA{GZj|Q@j8(WWm0J#btzQZm%OyX3TY$_sS37UU>}&&b#z#Bu!0h3n*xL< za_?G=Ak(Nkxb0p|B=Z;QV#xQcmsZE(Zq`dnbOFuTB0_W2gpt>fe~i2aipclZ^2L_R zD0N`tNH|e9Qusq^FYF|B`mnfj{B0UA&#%tkwRIjFv~PnbR0sDw#<@U zD8bknQ9GZ1Vs2~(bK^yrh^cmoOcx)If(I=c_?3aC_Qz@|f&a8^(#B>6xt(bD9(`&{ zgSfpZg)H`4Elp1Ri+2R+JvxA?7jYYlB$oV1tid7a5na8MZ~^NmA>+4#X|##fJcwUSDU#W)>OBq6$U3kI8Ll#%nAJ5M#)7ENVtW^>gn1zf{AhS~|5qS|isDVi?G=FGRKA;X%`kd}VzM+~cn8QHX zj?e)WZN~Ek_;Y=Ksy@4n^gUW^G)E8vHz<$290ZwsB+ihhiyFGMsoQpT+3IbSz_OyE zXm!`w2G)W`!?ry$N>^|6+%{&_hX%R7*t9^PFDnkoEof+Mn^Y4??X2r3tXI@NK{!cL z8JJf0>F>o{u21jMMJ?R~efljSE-UEo@5QWvdHm5&yzT;%m0tJZ-C{Q@yH7I&BC zE)U3maI~hd#+%X|ij4^!`0!})$jKvy@euWo=~Rfps%z19GOsCmW%2d{Vpu9SvG?jl z9pyRiPMq^c(+dmw6wDpFc)W4kqVmbxyre615)nrPd6cp9AX#sgFV{(D7G1xv)7faW z?m<}%fdpy=Q$w*aUBo;r8(%PmCqJ^j_z@3{?>4rQ*IKrIQRTtI3kK2QQWjsH)`-^C ze0?Iy+GiY1PwUfhTAz;BuTSzr;cs4_826FjOKqb}H(S|Qy%@NK0-#16nQRno2}AK9 zL+<1+sypynzn)vUou`kyIAB$;p8ZFDV3?+%-$Q29JQ3zdyOez(gRk7rGuAJxJS6H? z2%X1mq8axrW0|SF{s<)yDSOaSEvo6d{drp!P!meaH)}_&rn`*8qRvO4z^^2Jf3ADZ z5==UQG(s%kFX)Ko9J60Pv3&FzS~gNIlC#QV(G;iyzIReqr$`RPuh9O@V_@Oo0wkCz5+fZ3oes=Im~Q78ACFp0T53 z_qy+3HVS^g;RSu^+_Mj@AUo*XbG<1=w5EOG+hUrXA$*JT6b_Lp=_S6}$KI-6={@P% zgSxJ{s9i0{b!;9#Y&y>D*GyV9ekH|ta^7H<uNt?)CNLOOHxs~M9j^~ z22$k1H;^JS)+91!Eu`OAlBy#L4j zp2;wS>9x$V?vmgj5eKLRtDu=Mfxo;+lJ#;bz5JsbsBS!V@Z>Irc9V7w+wXN?&6*vH zjZ62>y>!Eqb*49Kb#v+Bm~CSZh27+kQmyxPXb!I`-@0pL zStp}hqV6;@ua{9NM|OL7{LaP)+kU4slX0}_I{i&Vp4C!c>8gI{qWnV!mZCniaOJwn z;^hQMNhU~2L6%XLKheLc$6PJj^^QJgdEQ|IJFPyvtZaSd=#^B_X?3T9{4C=knLtuS zabIK-{N?J@zSFqxqx`HWT5n@`iGm-On=(vvyO)UR4Jzs#^xUAB0_ z$bzDAdB)!AyyAt+NnDYRA3AgOh+I8`2`wV6Rn*gtb(UI+j*fz9shY^mFV*Ym89krR zpVv~;#_@MLJ<~JdAJP7vN+1z-3k%{C)sp%{|j#BGTQ)!!!9!LZ9-^x4H zEkzFp;gW$pr;DqN=?0lt^@|7h8MtV`b)#w0cZ6&iuyDjh12Z(HJdt%j2=Yf%@=)UW znocRG*Ama)4d$gjzi8EF3f1qTp`5Y=d@{(nB-P+-qXSh>ZFITt{9EQip=4^82P|$Y zi#yG{FEDRic5%(0l!BhsrU`O$^{;|lOJl70IiYr@q!klsN>&w%F?j?YDVDyFujq9n zi`mQz@=T3VtTnT``8k&dWdZqL}JSS*28b zCd&n4%A-Xs-RQu}R`E}-|8vA7e|1K-s2kz(|bUq7MS-}22n z3Rkr<%B|JulZN&&njGqN+?sw{wSx;|47~Z<>j!InnTMWIP4k}li{ciNgEV6FTEVPu z>&H)?|G?BikGWc?vggc(qOrLWmq;`8WH|JgmY$(^zDhOa17(p7fnb zEAQ7olC%vq4r0@zl#M0uG7D7Ibd$>Tt>33R3UVeNi#9z4_PCB>Z3faCwJJ9;`ij8r zyZv%G+WZ#t|4&14>To@O&_Ob=bd_X~s`NRUKv@$lYkL^P#0$bt-+guS%h#Mm%641BA3gT0&|K<4vi6{w9oQt+>+tzAX%>9$h`ESqbnoHg2t#0%d-7322SNA_WVf)&hW0>(=cl*xqxKHspH0Jx@JPXfAc9SG&@y8JdztQ=11j z?~ygB@0z|^ho{srBdKHlrhXw-cGce$IGUi5akUYL>axpf9<%%hgXc}n^LS<{yA!K` zT6T|@Eq&+|JopXA(n`)4!soTxW*?Gv*LCWMizM`*l zQl_3onuZffKiyt=CVgmAlG1tQ!<2*ir8VC?I7W1*Dv#fRv$wjB<#nv%d7RV0F)c}%nEF^8+lu5ga;f3&fxYDP;DdQ-o1 zQa{@Nc5c`*ZY%A7due|wq6`#z^Zq7rLcy1NW6p&)A9bK1d54n2u7f=Jqk}k|*mqh`g&ZGK-Xvd4#~_ zG%&aDahb=*01q+IA;zhQRD)eG3 zB|i?>AZX%c;>sLD?A*-9Lh8g-^9vEaF|B+mL8wWiTw~*8Ed`nvkW}NiR48_sn$RYJbHjsUkBIzqf zIy3r2aJWaEtES>5dvoz@_dWd`IYKGV*!aZ-;f}0stWGb=97xoU*NyCow64B(hu;{X zP$~pk4n@5jC$AC{$c|ab;-U!lrsj*p3}y5nn29~mdqUN(ImNcpCSG%%0n z7V#kS*~3SZUYJFhfJNUK^mcQC2e)Ej*IAgVrjcZ$XR`vRr@j$(D?PFII%RjUNfzW; zn%b_W4+%24oJQg2%tKTYaiPa2n)Fe;C?2L@L5G8JY>OBeFVwzIl?Kp*MAfcTNk)sg zPHKz=n_TNpXYrGnq?gIeQre?tE+m=@9nex@XEwgR$w!$bF?6$)MEpXAP-|M%R>`gj zfy=j)LB5j&>vF#+QwY(rfU<-JnXDYxH+E8#`|1k|oTVeMOKE7RvMcLJd zo=Bi4s@Uj2p{6xytF|@0CA-jf6nM_n*tA}arU3duqJwvp|NcTHQr+QLZJzb^&)up06f6m~rP7DhYeqdd0 zlWZs1Q$;wo$&Jg%wkdxcOeZeyv)E6+Q(~MbI;bhVbkL$gTA-4)OLO!=X&*IhXXux(7Vp?NXY)knoa0{d zyY-f7RZDkx(Q*Ai zmR{;n(|A?I?zvSXJ*q~Q_Rs2W>QLLzeR%&Kllm?1xpLyZwOh3ge~MvC1d6^O$*;Ys z!*a9o*bihFNMT~wSAxFM3(|fuY?APDT#86`F+E}>FhK78x?CeM=y1* ziCVK|UrFU?kL{zE^%yb8)V(&=J$FFQ34O}?t{lI=yhHFyQWZoc~PI? zQ;lO}qjh%4*pc15dK9i+S7Iz=M(edDOLkoLq9kJG*(hmfI)233>TXM#*NXkmlTUZ$ z_4K1jtISl|MgD68O>W8ri<%u`oVOsar$7UHivv=DO6taD=?M*Byug$2glL6c=I46z z=ai?|XnL&1?H`Ys2AyC{=+sDziV;j%v=GyW6zP4vyj~MNd}xadL(ieJ`>r0mzTf#} zRjahj=6v~g%*Kmk*-f=v6L+bcq`_{kT+$Ood#gLM{_8R@y?v*7W|r6iE?iG#6;} z|D8b6@1*XU60UpJg=@ac|5WqxC8BK<*MHcj>e!~K$4?w1TAnj;JK>n*MwXk{_q)cdpy^)!7~WKOHo)L%UB6VtjAfdQK{ZALUy> z#J?B*^7lRbcVBZ!4yG7BpmubH4v7^fh;=bOyzGB~bPo|C7E34)koBu8tQPNx5*Tf-N}CRGmU>^VvEN!GAWCmUF$6$_)N^Ia3y=JoaLIbvW& zvB7~1|FPt8u)p}i@wvWi-Q-E3|6v}(3i`~>U)}P$d&|_3xqW8juJp)W zGkI(2%D7O0|n1-Sz^6n(Zq2xjWfqhE*djV9xKPW-V+J0@bLTEtzQ9;ozIOLlr?p?5 z{_gxAo)3hzYxFPdcQ)VZtc75_> z=z&qyGmdKiJpBuE{QmZ5DHVoqD{mhE&Wq`ehem%k>a$_pj33D=Su>!StR2w4B&+jG zQhYMs)=wzg`Z>O>FIJ2T_JkW3rpjyV9Tl4<#)*PlNo-PmEm-$UDz;iWtp7<2xhF(x z_D|S8rfkgS-01`Iv-7ii&KSIO!mRPLim8>eHOuBKo-=>;&XrpytQ=B0eENW4c_RzD z7j~GSZKUZqcyyn>Su~k`)5u|^t$v*Nbn2GrTV`&a^Y?|HSVXC|rF>jJtM}@h-D9C+U{YG>&c!au7xeX#y+WJGVv%uzMs}V>xmW_d_G~F# zzrmQZZ1uR!UVAsL-D8wI$~0o5Rf4IcHcro9H%KmYAwgfbsCkJ-y30ANU!^%ZXWQy( zqIx^Nd+?{mVoishgF1ZPZ`1L~B&>4%k%oos_kETU0FN4+X~~7ft!l9E#6ip$ zFm_AzXOz<@yL9+rc|vD&!!U~y&lCH?0Q zD4dX`?NdCkcQdc%y*7V6)yO>5#}<{Z+c0KbM=F;tUZ$;{zIKf#VLpy7URSPs?I1@p zk12YV+h5Z*fBlw(hr@AJrV{y}D;cuMwRMzM3{8+m4vqb3Su@ji>krIM zy>;V?-3I2OW|$VRWv1J$1n%FXCD zaLq9T4Ub;>YW1;A%eO8w%HirqRxJK7B`&4Ek?P%reO9#b>POrXo-F>s?Web&Hhkow zyTWRvPbr{6-DYym@azS{X@KWa_b-_0_89+V9loX00nH&_r8fih5Gz*vfp$74AY`uB9*6wl$hQR!^8X zZIahibriH#zKw60>v@4|s}_0Yn+?9Xz0X7pIo>!?BMq&`*3Z{7E8$**W{kPoR(uPxbD z>G?g26+^QK6~JQ7TCg%PoG_iwm6b_~{{QjHWY$9)(!)M!@&7>&y8M@#FzEKZsSK_O z<3BZ_xUF0nQUk6Gc{-cTY%OSurn9B#Vw6dci`ep87N?{ll{APw*2{O*pNv|+xngm} zd9vXRLeA*Ht&OHA*Uj%N4eWcQ7u7mvyH zbdW1~K})EhB~;Fu#Y;CXGqC$CdE>I`1#5eItm!*CYd`^IPq%gdq0jf3-}l-wYRSNn zeMe_^AKz`Ye4iz_XDusUwVKK)V!c_I1sg`-fI-<~#^)Qe3VKh-Ez2pNvVHAZZRM8l zN_XGg+VeZF&8rqItu*dlSw3!Mep%j(%#kCseKV8CXQqF?ubG#F{9G~kz9xH*&HC@z zd#vD<+uZTB+g$SBbDP;*rCHL%|5>vncQPNOliYmZ{m$~K<7-_1I~~t^i;bY}su}n{ z>UX7_Tq=jK9siSlR|d93_*zv>8Xwp{5lxnb^cSXTc`SedSnf@A>g&TRvI@+L9=pR>EB!Ze0xyWokmz(0`XM zoWI!4j`=KFFmsKa{fkzvHTU&j=-z#*0xSR}?fVR0N5XJGLiEN;Q# zE-dcD;t?!rU5te?ijDTf4EStjeJy^DZWoKAsz;XaAb6}YV%h9l$2+LAf zmcg2a9H(+@OmJeb143;mTvVzJMDrcw~K;;Wn5LA&+#X^+? z)jLpq099M4K8Gp;szFcv6Cyf%R-yFM#zLSXaV&C#(;_`UI>m!}e*%{S0au8VhJN&^STk22CV1O`&NH&4l?(2Ry=0yI;hnFY;4 zXqH2>2AT?Jc0#isnlGTa3C$g7BxnU_-Jmr>8wzbCwDHiULfanN&d_E++aKB-X!D_+ z0PQSjmq5D`+Ktfeg7y%!C!jqC?Nw;Mh4v=2ccHxx?cdOv_>8e}fQ<)i{9y9|Y(9of zXV~e?Vb>UTsj&M1b|1s81MGUht{?1% zz%CDVV_-J{c2i+D8+Hp}w-t8BVfP*Eeumv2urtA41$z(J`@lXJ_Dx`)2K(l){}A?{ z!M-c(d%=DX?1#dBB<#n)egf>L!hSyNOJTnb_M2h93-*U#e+>3#V1E_%-@^VT?0<&+ zL)bGoxWYk%gC87X;gAl8)^O+whdyu^42NPkOozi+=9bB zI6Q*Gb2!w((GrdtI6A=54UXP$41i-JIL5#+0gkQU_z4`lz_AY;v*DNz$H{P92* z+ycj)a6AUb3vm1zjz7ZjCpg}R<6}5VaI%1t9h_X@WQ0=)oSMKX1y0T2)CNxN;M56D zJ>b+2PFZjo4yOV*6~k#VoMymj9-NlIX$73h;j{%#d*O5xP8Z;G7fvQPYvAk-XCs`0 z;T#R;csQrR`2#q2fOBU!_knX3oQJ}B6r3l(c@~@(!+9l~E8x5x&imke63*x0d=<{$ z!TAoH@5A{CoFzE_3oh1hv4e{XT-@PefJ*>e!r_tzm-pe)7A_s((jP9v;4&XBYv8g8 zF1z4z2rd`l@&jCchRdID`4@C{(78hA37s!=4WWyME*ZLaplc0Xd+0hrmjPWSblK32 zfNnH&6QG+0-CXFFKvxFc2I#gxw+p(1&>e^F40Kna`xd%e(ES44AJ9F4t_H3exH`i% z0IuP1O@`}xaBU6O4sh)T*Zy!F4%Y&>7Q=NaT<5@bDO^{>bpu>?!1VxJPr~&QT(86R z23&uE>jSv{4ObKND(G#XcY?kF^akhyp$~&T2Kof(Q=xALeS7G8KtBNbVbG6(z6AQY z(655N68eMCAA|l3^w*)k1^sW(KY;#k=>G*b0d6jEbBCKh+(O|N1Gf~oHHTYUxOIkG zU$_m1TQ1xR;8p^+xo}$ox3zHF2)8P@9e~?$xSfOBRk(cvw;$nl2X24B?J3-9&;V$l zK?4Ug(4&DD8u+0>Fd9UlK^z*SpuxLn&=L(kLW2%y&>0PK(O?W3EJTBCXmAh>PNTs^ zH247x9-sk(yB6+la5us|816}MZwdDdxDSB)2)Gx+eKy<|zf0Phwy#| zZy5$l7;Ip0f}sHn1{eZiXaqwH42dwL!O#MR4`Jv4LuVK=VCV-!77W8+$cJGp4AWrP z3&T+uPQvgN3^!r855o%>H88rsXn-*o#yA+$VQc~8hcLE-u_KJ#VeAKE7K|fdoCM=+ z7?;Ah3dRjEZi8_TjE7-74&xaZFTwaNjJIL@6~;eddFZszTWWlhwrEG?F-+*@Xd$sMEEX( zZz+7M;Cm3hU%>Y|e1C!OL-@+@vxc8N{EYAmgkJ>w(%{z|ey!p6Is7u=mkqyB@GF7e zJoqhv-%9vxfZq=I?T6n9_??5_75IGvzZ>wo3%~pDdkjAr{wny}!QTb`p78gBe<=K8 z;GYQpH2AlG|A+AJ0ROJ=?+^cB@E-;LV)##n|4jHVfPX3c%iv!Q|Bdk94FB!$-vj@H z@IMa!)9^nJ|10pn2LEs2e*^w^;r~1QAHe@F_&0*nXirwFJ)pcMjb5a@(JHw1bkFaUue2yBYL z_Yl|`f$b6434s|1?1#Xi2+T*|7z9p0;7kP0N8l<1u0!A^1a3#*egsw{@FW5+BJdgl ze?Z_b2z(48-V3rukTZhx2oe$Gi=c)G3P(^Jf|?@eJp{EuPg2-YIl5y5%{dn4En!J!C_LU0^{-$8II1h+--=LqhF-~kBELGVZfk3sMR z1W!fqJOr;q@HzxViBJ<7Y0$_Kjoi>E z1dS5VC>4#Gqfr|)>VQT)(I_8{iqU8;8ZAMiRcLe?jV_|m*J$)38vTk!e zBg_Y3K?sXNSW|>GM_7A=^+4DFgpEYlc!W(u*g}M@Lf8g`Z9&*>gq=dzC4_yAup0>b z9bu0V_8eh#2-hH7hj4F%2O&HR;f)dg4#M9@_=gDZfbbp&ABgZ=gcl-w0>Y;vd^W-t zA$%pm%MrdA;X4t25aGuVeg@%RA^Zly?;!jSgg-_23q)8V!UhqJh;T!MHzEQM5r&8+ zh)71ndx&U-h))pF5fMEQ(H9Ye5HS=HBM~tM5fczG6%n%$u@DiZh**t?4T#u+h@FVo zhlult_#P2IBjP?Ho+6?Sk@kpmL8KQV0}vUB$XG-sA+k9l+aWRok%JI93Xu~KS&GO_ zh&+VIQ;58P$ZLrF5s^P5@)05>L|G%s5m5~gWkggcqGAx0h^TiE)f!QsAnJ2O^+i-R zqDCTWJfh|xsuWS>h}wjx9f&%Ns4oz822qy~^&_J0AnFf9JwcQS(Uyp|N3;&n-iQuB zbQq#z5uJ|c7Kr{3(Vrr^E24WLdJv+AA$k;|ixE8q(K8Xf0MVt0UW@2TL~lp*K15d| z`V^usA^JL^ZzB3vME{BCzYzTbF&2o?BE|tRdc=r`@k2}_#Ka&b0WqnFX@;0qh-r(M zUWgfhm>k69A!Z_C<|AeWVyY0c4>4yDa|JPfBIX%lYSGvkjf2s+2^znH#;wq}JsM}D zaUmK{N8`0P3EG> zax__sCR@;CKbo9ClP}Ta2Q;~hCJ)f$FEpt^tOa7Vh;=}$D`Jg^4Mc1VV$%@Y2C?lB z+ZnO_5SxwIk%%oq>@>vAMQkZz*CVzHvHKBQjo5RDy@J?p5qk@wP5m$h?afmBH+-$@xLfi_(l_PEw z;Le-yz{P67D161rjZgC?L@riT+58 zKw<(CQ<2yVi60^Hb0qdg;y@(kB5@27OOQAZiA#~V8i_lRcmRnfk$4e_Um@`Z67M1L z2@+o*$r?!xNNRv2ZzMHDQaF<0k@OCdS|O<&k~$-)50VBVDF;b;NE(Br2}qiWq{T>D zjid@BRUv5)k`5v11d=Wy=^B#0N7Bzox{su%NP2-}6_N!cJ0sa0$v#MKh~x+)$09ie z$?qY#4U*diks5^5D5Rz!wG~pkB6T=Y3y@lj)Doo5MQSNh*C2H>QuiYDG*Z7p>P@8H zL+W#+)*(%eG<&3ZADxV0HjADJrU`tNNLiz}# z7b1Pi|Dx!;|3Xl_H~u~id&?dfm5>P6{oLzbga%EbC8BAMw5+Xmh4=ng?h_ZGUj=$4@S2;JxCmZMvVZVkG>&~0I=GE>_#wHH$dFm)(Xhck6FQ~zM< zM5gL8)qtsHOtog}DyFVyY7kTRGW7~mZ!ooxsn41Eo~eyY>&mo&OjBptXr@hM+B~MY zFl`0Xe3<6Pv^`9VVp$XWIHsRw`faA?G5sFXpECU&(`%UCfS#b&7QJ5RjX`e`ddBF@MQ`laYULH`x{AJG4fej^60 zG3bIp9}HA67>0ok1`{#R#b5>ovoNs6U?B#s7_7lyJq7_7L|||bgGdbGFgS(5c?_;% za2tbM3`#I~jKNC`Dlqtr!4C`?FjT;>Erwk&?2Vx+h8h@-!f-r>6EW1q&=5m23@tI7 zkD(KW%P?Gnp%;c5G2Dh>2!=Z`+>hZ=3{PN~h~XIwFJO2T! zGov>%1~FqCGfbEf$c$agIK+$?W~4IXGBYxnQNoPJ%y`L+3TD(Xvo$ljF;ktHW0?6D zGxeD{hnb6+xr&)Pn7NyoN0}MV%+t)g%*-5S7BTY~Ge0o%3r30MuRcZ!e|^u ze`BPFkr75_7+GQDiqUF}HeeKn(NT;NFgk}(21Z#JJ;3M%MjtWyfl(vI3K+M?xGTmz zG47A?5RA1k{vXD=7@J~ji?J)l?ig>tcq_)i7$3km8sj94Q!&1daTdmT7(c-HDaPd( zf5Nzy|E?=0?J?<&$skNdV)8d8dYG7FG7l3+OjcpyjmZ{F0x{W*$q`JBW0H)?1xzw9 z$;G4?lSi1m!sH_+)tJ;{s(@(+Ob22*0#hAK|Hf1oQ)5i6Fm=Fm6{en;`e7P?>26Gq zVH%6+DNHY7dL7d{m=R*P9Z=7M<} z%sXS=1M>lx55;^0<~o@F5A(^G>tk+$`5ep_VD5zZQp{Il?uq&O|2SKihhn}5^TU`& zV;+zBY0S@Keg*RznBT>`2=j-SKf}D4SzVabk6CKW8p|v_X3b@m1GCmLD}Y&hnRSR+ ziOf33tbArYU{(dQYM9-c*&UfZgxT86p2X}K%$~*U`OJ1<_G)HtVRk69_b~emvoA3_ zo7pAIe#`8S%&x(rJr*ih48USI7UQv)goP0nv#_wk!V!y=Sa@Tx35x(Mj$v^Mi(6O} zVo`?0ODujfN0B)lnKP6*lbAD|IhM>>#GGZ!@nX(q=0q^(AakOblg^wQ%qe8fQ|5eN zPBn9WGq)3S2QYUebH_7x5_2t>>%!a>%=KpOHs*#g_Xu-OF!vO5)0vyW+&tz!V{R35 z>#$V9vI~|yu^fn{7MA~FsgLC>EbXv##BwE;>#z*OG7QUuSVm%*h~*h9&tsW^WiFP* zSU$$`HI^T+ticMb+GEudtHD@}!RjxpreS4_)oiTnuyV#~8CLFC`C%1?)jq7Euu8%z z6{~ctZeW#%)jh19VD$#84_JM{supV{tUF`f7i)E_M`8UZ*1A~FztpZdO6k} zSZ~BS0P6^>4`LmM^=YgxVx58Y9juG7eu8y5)>T-4!}=H2E!ecerUN$JvFVG=U~Dw8 z(Z*&1HveFwhm9#VbFs0*#tEC{*sR579X9^hgkZB1n}gUyVH1bVDQwPTa}}Fg*yLhU zg3V)WUSRVcn`&%+V+*#evF(Iy4{ZBmtAVW+w&St=2ivLG8e(gPttGbev310DCAMDJ zZo+muwxQVW!S*n=(b&dgdkWh$Y%gP*iETEv1=!xhwhY^s*j8X$iS0LR>#%KNo)Yug zGp`%-`Y>-0^E8;J#k{f1`ycZrF>gBa%$VoIycNt_%e+m@+seF9<{f5U67$lTmy4Yu zcAc>8P`#QcHG z{~z=9nQy^-C+06@z8mv>n7@Ph`?^RZ!TuKuVPPv4c4T2c77l0O z|5&KcLTeT}vv36qJy^Jvh2bpR$HEjAUSVN23m>xZH48tou=@Y6<%`;~s5^_aSoAlG z%vfZ{qU9{|WsyINf>?BrMF}iQWzj_z-DXi9iypG*Ig38As166gp&bsLap;YM8V)0H z7>C2ZI2ht!ih~Ufi*RtkVGRyjaR|a;7Y>JUh{xd+4(D;mz#$8VVjN!JP>#b_9O`js z!Lc2Vy>J|Yqb81j;;4_K369n{F2vClM>iZd;24PGJ{)6kOvEt_$Lly2;#h`bIgVd( ztj9?ir!F}4#%T~vnmGM|(^Q;{ak9W^9!^d;t-#3>r;Rv8;B)||Xq-}U%ET!jr$;!w z!0A0s-*Ebkvm(yzaaO^30M2SSYvDWr=Sev0;XDgxE1VbN?27X$oV{?~h;ty$yKp{& zb2QG$IG@M)I?i`+F2wmE&d+gvgY!q6t8s3^MH!dQxb(qgC@!OL`4g8ZxESDKipv~a z7U1H7%Sv24aoK>&R$M}G*@eqNT%vJF!sRS3>9}O#l8s9tE)Q^diOYLjzT#4cOAD^8 zaP5d|H(dMRItbTcxN7722d@9%It^E2T<73whpQ8=%W!qW)dyEUTmx_o!*w^Vhj2ZP zYdo%}aXpXg6=4?OEKF#l2WOfW<>tJd(wK zuvnkPvst{7#qKQjXK^5l_ptadi(^=v!Qu}rZeWQLOZu>62usGWWHL*PS>nQyRV?vj zi62XjvLucrS6GtAl9w#0WvQ^V7fZERYQR!^mabr_J4*vudWfYlEKOu-I!mv!G>4@{ zEPcY#MwWGCS#OrTt7ful7OSjT<;1ELtny;j zW>y8UDvDL9th&moo2Z!>a!rnpxeN)ty=0ht(Ra9?$A2tTtw~C98c|?a%5U zR_|u@5mujLbtbEeSzX5JH>|E^^>5Y)YdW!JAZs*O^9O6Du*Q@%maJLCnx(AqV2vMZ z!dY{KH8HF?&6>-sDP+x4)_h>iZ`_n{>x^3;+*EPXz)c&sKX99f+f>|4aI?bA1-CW0 zt-~!0w_~^^<8~3Z4BQHEdxBd9ZdJI|u@=^LVr_5MYOwZC)=puq0c*`zJD0T!SnI;t zwXEI5+Hlq$W$g*pCb9McYp=34o3+KPea+gBtZl))E$-cL?~nUP-2cX17k6{qZE<(R zeKqbIaNml1IPQmWkHtL|_YB-~aKDfHE8Ht_uf+pA+T+m^kN$WJ!{ZM;rr=?QhaDbE z@K}w9Hy-|YgyOLukE3|R;gN|)As%ITl;iOkkDqun+5Y&+~X@;+c=?klkh%^cRJpgcxU5Xi1!1$pW*!$?@xGt!@B_=Wqdm0(+8iy_zcHK2cN(2nSzf2 zKBoB0#b+Kq4)`p?#~q*b_-w^z2R^&;IfTz~eB$vrjn4&quHkbVpFDg@@hQXS6+Z9r z`HasGd>Zgoz_%^FUGVLN??8Oj@zuh29KL_yI~iX+e2ws(g|7|1i|}>DcLly{@%8zS zFp2Lrd_(ZviSK@VkKlU}-(-Bx;d>e18~A49TY&F9e9Q2CiSIYoDX~t4bz@lf7waro z=fJv1)?H&=A?seT?mg>jS>KlRgIPb8^}4K|#rmzR4`qD{>o2fAm-UZX|AzI=Y-q=Z z!E6}Lh6!w#%mza?*s#Ho4W4WWV#6UeoMJ;J8}ist%7zLyd}X7sQI(CO+4vV5P1tC| z#-(gr%SJyohOse^0_j7@%Q z+R3J4Y>H!3E}N?H1HX3ospB^mzrXP_#LpZ*Tl`$`TZx}Le*XCF#4irN3;1Q=SB&3# z{C=`o*xZKADr_Fd=E-a}V6!QkZP@I@=H+bOz-E6ohqCzqo8#Dgn$78KE@1N$HkY&c zE1PTC0$aMUMU^ch*fNGKf3ro8E#_>Q$Cl-6@nlOlTMn`%k}b(>xyF_PwtQmC54JSm zuYi9i{CnU(2>(Cv*Tdf!|GD@(;lC39_4o(jzYG84_$T6j2LD_57vf)r|110}@vp(Z zk*(d?I)tq{Z2gO^)7d(gt@GIG$kx?t^o>Ok zW}710IAF}BCD z{VdzBvHcd?i`o91?GeI1VUhE0{aoDPM|h{6A3gV(1O4P1TGwp=LyUru!z711imKl6M?k^K~Q^wx)RijAXS1i2^vk%9|TP%$dsV@1T81XgP=_W z1rQWL&>@0i2s%Yj8bQ|y$|mSOL2n59POt*OZ3yl{aDReF5c~(h{}4Qt;F$!Q6KqMa z9l_27uOQfq;Ee=o&)33Ef0! zAffvRJwj*8gbyHGjqs6#k0pE};nN5=BHV&-8^RY6zL@Y;gnJUcf$*(_hY-Gt z@PmX$5gteQDZB3GgcJ5(k3_H`q*}$G)_UvQNY4&8Xr+__W>?vo@7xpx=w-tMPvv(qU4cTkX z-g)eGVXrTHgV?*1y;1B98%-ZI1WwbkOhbAIpoSA zZw`fVXg`M%ICO?X7dUj2LxmiA!lAbus^QQt4hx6dakwjo`*B!{!+&si5{HdAY|Y_? z9QNUGFo$<>_yC6^IULL3Qyjj|;l~{Q#NlroX~mJw9O=iA@f?}R5o3;6am1M;%Q&)$ zBLN)Q$B{&ioZ-k-jy&MVGmgCF$bZ6L9BsqVP8{vaQFV^~kE4?~I*X$VIJ%Of9vt1s z(LEfE;%G8QFLCrHNAo#a%F%Zmt>I`B$J%hL7svW@OowBWI5vl4&Kz6KF+YxlaO^0@ z;yHGnW0@Sg%dujPJ>%G0j#YE)Cy}j)R3UN@ks3t)L!<$b7DU<*=}6=fBG(Yo8AQz^%9SX0qBao~ zO4MGWqKHZ->NZjLiF!sfp$?WOJZOjlz15~EJcNMa@sqf3l2 zF_y$EB4#x)n}`V`<|r|-#H0{&o|v1&6cJNK%v)kU5>vxTB~EtaWN%JtaPkjM>T_}y zC+#@tz{#bYT*JvtoDAmV0ZztpGL@58IhoDLQcgbMWCbU`a`Go98;MmUwj;5Q!OOS}>B*2Fs!zmj+#;x`c=K>RM^j}m`^_+;YG5r38V zEaLNtzeoHN;@=Vfh4@LH!g><6 zkq}10eiDw6aFT>GBwQik4he-MJS5=-3GYbwLc&iH8c9?lu>*-JB=#e52#KRf{2z(B zB+ew!ff<%kz^&3+mqal#t{ zX(C0Ll#ZnIAY}k4>ZFV$MTe9>N%@BqJyJ|bnM;ZtDUPHpC1njM-lS|IWjiUMr0gN( zFe%ZbB#?53l#8TXBjpw;Ii!@5@|#nwI5mh<6FFtUsYRSx#wj;WZRFG*PQ`Poh*KXp zRl}(UPQ&T;obJtObxx1t^uL_e=d=l@S93am(=nVr%jt`pzRl_ToPNpaPn>S%Oc&1d z;*1t&{^X1~XY4rR!I|xxIl`F}oXOzK9nL)FObw|*YCBRplRBK#F{J8~I-Ar5q`Hyn zN9s;e50M&2YC5UcNxemCDXGs${YYxf|3{k6w&Ls%&T4Vig0n81_2sNTXCpX!h_i{D zP2=n>&K7a@DQCZMt_|mUb54VEV>qYBIU~+Fan6l%KAa2X+&<2o;9NTAiaGa=a}A_* zAgw!T{Yld%?Qha1lQx?)SJFI4+fLd}(xOSbNm?FhPf7dA`L>)N#CdhjPv^W9=bbq3 z!FfN<2Xg)p=TC6{4Ck+MKAZE!oUh<~CFg%|LAcPL3u;`@=E8U`{L6(IT(IWCA}*}v zLO2&vxp0XKx4H0u3op3vfeY1KXy9T$E{@}(9v4lyxQvTlT-?mXKrSBQ;t4LE=VCDz zUvsgNi}hSm=290fjo{M1Tr%L28JC>6w1P{XTw2ei04_yvDUwTPxpa+7*<8BMrE)I) zAYGC4uB7)RU6b@Nq)#Ay8tJB_&mnz2>5EBsBYi#T{-lSHevI@u(o;ykO?ocrrKCS4 z{SE1#NdL)YMJ~7Ha(6Bd=JGHukK^)0E*o;$lFRm7Ud&}LF7M&;NiL^y`6`#QxLnNT zS6u$YY9epm2|FTaiy3m54rM^ zEAP4Tg)6_gs>Ic9Tph|)Ew28_RTHjSan+Hl%ecCctJ}D`kE=;sy~5Q(u9k7NimMG= z>%g^cTft_Ll2yx!#5A zy}7Q&^-)}($aQ_L8*zOO*KN7Jkn4-N?#6Y0uJ7XdQLe{u{S?>JxSqxJ60VnV{Xdhh zTyG(x0~x)^7(#{)8B@qGC&QkM6=Zml;YY?cGQ!E&OU4m0;>b8p##J(M$#_7Gu*nw zts-tc;nqiP{p3~)x7%@Bh1&zTJ%Zcgx&05fXK>q++m75`!EG;Y`*Ayr+k3fvgxfLP zKF940Zs&3P8MohayO!I{WVIoyGg-aJQYC8?Srf_9Cuy1xKqfT2i$qXov++!B)c`)-N^1owi?-^$euv&;>|olJgfiy5yLWV@b{eau$>0M$US2{K*L=XD>N1`&;=M6cZ$*Ct- zncObq_9It=Tpe=%A=iN1+2mT2yO7*v4Hk#~r^81hb$ zcZs~4{mIuLe+>D5kw2AuBl72vZ%e)-`OC@oAb%tI z0pv%Je~|oW@)O8EP5w3V?~-3gei`}Y14iqeNZ53j8Ptq+llnhbTBfK{5sBDY#C-Z3+q~cu2uZ3O-Qqg@QT?p|CZD zoha-NRcu{oha%>kt#*QC>l-CpA=1|$bcdz~B}tT=r6iq_OiHpTDWv29CC?~%OUWlnzESdvk`_u^QQDDG z6-xV2I)u{Ul#ZeFPf90Isz<3Yr52RhQo4{*7fP2?>OtvdN<%3Cifn4?+f>uxo^RJ8}2XT{s!)c zaQ^`J6S#ka`#Icy#r-ebZ{R@(9t`5apFEhx14kZs@n9nlLU?e92eCZ3%Y$+r{N!Of z9uDH+|9EK5!wo##!^0#VX7i{OkNWXQhezf-TE!zj9v$aVJdbjCRLY}QJo?1rPCOpK zV>KRY^LP@E4R~zFV`m<(;;}!ElX+ag7s^JE`SqIhzFC)ap# zhbQ-W@`)!ud8)$G!93OG=_H<-^3;*1OL*$dQ$L=D@pL~=<9K?Vr+GYm%+q%~t>alQ zo@w%I2G4AGwv=ZcJX^;zKc4O6*#(~6htGcpk;`B%WXA`7@qZ@VuE9t$ERt7lU{)iWd`kq0fuCys+iPDqeW-!jBgryg0&( zle|dfMJ6wDcu~fS3SNBS#V=ke^0FH*`}0zRm*aUkjh9BewB)4=FFkp=g_ohcJi*Hg zyu8KBB3?e?&j^D2o~XL)s% zR|UL!%c~k*HSxL~uecvxhfFd2@z0w|G;`n>W0v z=FKnOwD7hyZwK&JgSUV1b`o!mcsrZ7F1%gM+jYF%!P}F(J;&R0-rnGC9&aD=_9<`Q z@%9IA8z}Ecc@N4}Dc7WYJmm(In^A60xeMjXDR-mXm+~;mk5C>%c{1f^DbJuhhw^)r zzo7gR<;_&Ir=lAb1F0B6#aJq)QZa{$1yro1Vha^JsMtkC6cwkbxI#q%6=hUZ@lJTx zop*zHHeGlIE z=e;`bC-Hs;?`QGej`xndU%`7n-tXakH1Csmf0g&yyf5MXE8f57{Wsn>@Ii?WDts8r zhkyBCzz1_aIPqZ>A2#w~D<8u7aF7o%d`RTO89rR&Lm?l^_)x)z&wTjJM@2q%L)L=JN3YA1nFzi%;$O)Qe9Vd>X~4 z@qE(d(`-Ij^JxK}mh;JjPkwyb!KcG~O5;-|pK|$B#-|T_`oX7WD%()0LghdzwWu6R z!}i|+ELY=s{T}s zr0NfBg4PM| zseCc#iydDa__CNUo_yKDmmt3E=Su=#F7o9DU-J3#fG=lwaYLFB*r1~GKO{ktjwKLT#srI3IE7d!xK1g*G)yY(!r@Dmd=Tv{D`a9o*Z*BS3jc=-a z8_qW!zWvKLBfib%n=RiQ__l;^tN6BoZy|g;$hT;|CG+hv-!l1@!?z;7z2@6{zSZ!p ziJFeos8FL$%}8qgrpAOC8){srSx$``H5;f2pk^O6C#gxH<|;L}sVSi5AvLe5six)^ z-xc`Yf$zQeuEzHfeE*a0x_md~`&_=y=lf#5d+^s+z zd;ECGk4k>jQ46)LsqIc}e`+1F6%bZUS|>)EQGZo4WbbEv9Y_b?d3yN?ka0 zd#Q_}F7`j-8Fd$^yG~sWb@!=zN?iqYRn-0D7yRnNuYvp;#jo-Fn!+zbe$C;RJ--(7 zYbC$D__c*!5&Sy9uULMi@+*U1S^O&ER~f%v^6MkNzVqupNNY!atMYpozen-=PkvA0 zw;{i0^V^o+PW)cUZ*P7F@cRJ2Pw@K;zpwH84!=wI{es_>{QgBf)OV!5FZIKyA5Z;c z>St0vm->0syHLNJdN1m?P#;SDKI)HCpG^Hl>TgnCK>Y*iUs3;#`YP)G;{!lLTN=91 z(2s^8Gz_Cbi-vJDOr&8N4MsFr(cnnKDjK|L*i1tR4ZCSLK*KQ_VrfXGA&rJBH0087 zpN1DSRM7C1hMzS4*S^z+#%?tBr%{8(F*N>7<8&I0Xq-*sJQ|&7TtTBdjq7OKN@FOE z`)E8$V=RrSG+v_dI*s`>KB2LK#%dZHXlg}MSDO0MG=wHin#R#IktThbOlh*A$%&>_ zH2KoBg{EMd_R@5erjsCs0S|VwQqvbR$7ir0$ zC7YHaS{~8zl9mcuKGRZ5OOv1|v=_Pys>1O9l5)bo!gRqxSRgDB+=Wd-u&`H%5)y>7 z!WH4RP#`=MUJ4)n8)QPgh~j^xP9i--28gJOj1u`nMKMPdi$(E~D83NI3Q?>Q#UG;B zC`wAA)IpR~M5(_hsfm)7D2*4Te?)1zC>e{=98t0pB_~l@E=umAv|g09iqZ~I+AB)O zL@7>`&WKXFDBTpLJW;wYN>4@UjVOH-rD{>C6QyQRRu<(BqTF4S`-<{lQPvdY(V{#- zl>Zgw>7r~T%Ckk;MwAzdva2Ys6lD)lUN6f2q8u#BJ4N|`C`XENtSG05a+)Y#5#^hr zoFmG`qWnmdUx;#rC|8N{cTujFRwAw1NUP4$s;9IXAgzWo8{7GgS3f|HbWO z4wbfA(sq%wb(OZ>(sr}7JuGe0rR_~=TPSVIr0qv(`$gKdm3Dol-B4*aN!l4my9LtD zRob~ryBKMAM%rDLc6X#*zO;KL?aHOyCu!Ft?UkhcP-#C^+FMF{2Wjss?E|F!acQ3{ z?e9qYB5D6t+JBe!_0nO0bQmukCP{~x(!oJG_(+E^>5w2D&Paz$>F_{0ypj&J(osb^ z>PW|l(s8DAoFg3Q<6Y2CpI#o-jX6f8sI`@#y>e5+TI!~9*j?&p(I`5Fq$E5Qa z>3mT-7f9!a()qn~{v}-$q>H+A87W= zmkQ}pEnRy^*OAh7igY!Vt_!5AuXOd7t~;dbKIs}ST~np&P3ihjy1tUGmD06Fy0wyS z1Ekvs={8QfnMgNl>EAqIF`%3ps(mhDJ@00E^(mhSO-<9t7r27l${#jHIl`f*vM^uK1ims>_ zii(A(*ocaQsJM&DHc>eyDv6?!CMq{YB~MiDi^>a8`7SCA(xbEV7%V-;NsoV}hoSVC zB|YX#4;SgNQF`o^9&ys+g7her9`({wQF?Zfo`a<4DCs#)dg@6}Bk5@=Jw2r7Zs~bQ zdL~HEE7J3>^n4>dtEFe7^y((P)TP%S(rcph(vx0}(rbnE@|0e|(rcIWijiK)((A1B zx-Gq)Nv{g&RVlq1q_>jvZYRCFNbmmAd${x-C%tv0w~h36klw4MccAn>B)y}h_igE2 zD80+1ce(WbEWLk7pH|YRr}WW~K4YcNWa(oheJrJqv-EM3KAWV^4(W4H`Xou8H0g6$ z`eaI($`n|Zk4_f()XP7&6K{y()Wq< zt(Csb(yz7j>n#1WrQcuDZ@Tm|lYR@O-*V~aDgFGUUxf5KCjFA6Uz+s0Dg9nbzpv7- zUi!C}{;JY{ob>-!`cISov!(w6>F*@{H%R||(mzW2r%3;s(mzM~f0O=AGN6MD=qm$8 z$$&p(fSwGnkO7NjzKxANd8K^D;b!6b5GSFBC zTFbx{GH|U7Tqgr}$iPS$ct!@^kby5`;73svQSB+J{Y7 z)y1N^T2#G7b)%>Ti|T$+jTY4uQN1XtS)%$xR9}ngXHjjELG5Kw4;eH>1`U@%6J(IS z46>0yi)GLz85Aai4$7eOGU%2JDw07@Wl*^cs**v!WpGCs+*<|@mccqQc%lq8k-$gX?5)iwtQaL%PV2-ZEsk3>hm! zbY;j~8L~=-_{orP8M04?M9Yw58FE>M+?FAwGNep~ypkawWJrw+`7J{`%Fuo?bfgUZ zONN@t(D^cSxeRrep}sP7iwq5vp?hU$tPDLZL(^sG4H;T2L!Zjf@1oXP)cT2qUKwsE3RCAyGdr>T#l;BI*}Jy+G8Ti27Sm zuMrIe(dZ%?eMCc5G}J`n57C$;8it}_E*f^Cu~;;GMPr+2>=%s$(YPWSS)%bkG+v6v z2hsRX{!50nl3|@?SRWZSOosgSS26Xm$|I9-`S#G&MwXlxR*CO*7H75>0#2Tqc_CqPbo)gG6(WXdV&GIMF;S zn(3l>OEmLE^POmZ6U{~$-d2VWlHuwye5?$gD8mh8xQPt6mf_Aae3=aQk>Q(Ucz_Jw zC&Oc8_&FJVU501N@B$frUxvSt;ngy{Sw?h_5#3}&9~q%0BeZ42BpG2YBNobtr82@x zMr@T4yJW-x84)ET&d7*sGUBd`D3uXaGNN8aD#^%>GP0M9)Rd9qWaM8maWHAqHj z$f(gW>Mt2(B%|ibC|4P^LPmMZsBJQ8my9|rqvB=MB^h;FMit4ZCo<}_jQS#@ev7uE zXm=3p-lDB4+QUWrPtl$v+IpgGEZXx$dx>a!i1sGY4i@b_q8%mLiK2Z$w6jIKNVFe{ z_H)s$5bZCb-6*3KWppPQ-A6{N$>`BC`Y#!6Afqj0^a2^}B%|GA^adI2FQY?c^j;Yq zDWj8QbefFLmC+Ambft{`A!C$eOeYyLP{s_CG2>*+L>V(x#+b<%D;eV|V^+%;KN%Au zWA@9KqcSF5#-z!Z3>i}-W1h*FcQWRyjQK4(3Zm0qbW}uVu;`2sopGY`ujuHDj=AW} z7oDY|vsQFAh>pMLM2OB&(Mc4Yi=uN)bZ&`Gf#{Tp&TG;6AUZ!}tfGu$UvxaBf#gNzH3al2&P5g8XRO)|cfj8~EILu9;`jQ>-{PnPi}GJc+nUn1kZWc)T6A0huo(Rqf2 z;J97beTWFzgoKPpGTNoR?Chu%A$w*-C@W+{W|WE|4SVnLv-jS6&+HL-|L^y6&gXNT z>w2EsCIy>~*zCaO7&e!&xrNOmY+hsY1KXn58ev-=+gjK*#?}T~Pi)&`+XLG^*bc=u z65Cj86R}-|?OJR%V!IvNgV>(J_6oMQuziBlt*ww?%5<7eB{ITnaT?BR` zup5irH0T>}O%0h4>L+syR{|$#CI2hqj1&2B~G{M0h z2TvT@;Lr(&o;dWwVK5G3afrcT77hz>SdK$F4!Jn&#Ni+g$8k7^!&Mv};P3*6_c(mV z;UA8MI99^3Hjb7!+TrMpV<#MY<2VS%kvLAoaTbmXa7@B+9gdrE+>hfa953Q{7sr=4 ze!=k{PDVIY#i=$<);KxfJHa9dYS~OBgOga2bb7 zEH3kKS&mCOF1fhu#^o3;7jU_a%QIX);_{nT2DBN>5S(CQlR%1;(8d@lek{O^){|gaD9vGS6mBl z6K*AOtAJY#-0IxP{|36t}Us#o@LHw^g`h;I;|3J-8ji?L2N* zal4P(OWZ!;R)D(!?#8&A;BJn)749~;yW;MTduQB(aUX#DNZcplJ_Gj!xUaxH9rqmE zcjA5!_Y=5Z!2Jg94{(2h`*%FR!w`>3c+|$D5gyI)XoZI#9$oMV#bXE_5JR9I?ji&>i9(cCFvkRWVc=pG0 zIG*G2jKOmbo{RBZh37gvbMf4X=RrJA;CTVh8+bmz^97zC@cfQvAzsDtDvMVYylUaq z0I#Na+2Q4imoHu&@am3NZ@dQJH5{*Tcum1;7G4YRT838&UYU61;k6U5gLs|5>jGXk z@OpsP3%owy^&PK5yo=*q7Vj!}*TTC2-d1?q;O&gJC*G~`4#2wy-o5eekM{_?qwt=N z_k6sU29G}McG{?sQA2)n_@oA4w zSA2r-3CCv$K4b8igwJ$*;_*quXB9qc@yW(#8$SE+IflQI;AerKHGX#ZwZhLEzqa^w#;+%S zVfaPhHyppQ_(kJ49lyEwEy8aFerxc{#4i`W?fC7(?_ePt@ejv;F#cokkH&vK{@M6n!v8k@ zFKJzZ*5zpJKx;2r2hcj0*0X56kk)y$K1k~ew7yO2XS6YLUaF!0Q=uk+<3UsVN$A)xt zpkr4$j-=yMI;PMui;i39_>h1i1XLuT4goF%bReJ~0kH%uB_M@>T?CvW;3)y0=mecA z(Wx<=hR`XBPV?!MMW?-VI!&irbb3pte{{B>vn`$d=^RMs;dG9s^9(vKrt^9_KcVwi zx|E_zdAih~ixpkG>C%xdBj^%Kmw37?p-T!~cGBf2T~5>G7F}M^FP$;Zgd?= z*RgbsrE5H0Q|P*duBYiHx|z|66uvmuLJZtPp^CQdQVU#f?5*fPEa?31`;%yph*PH zASi*Ll>}`k=m0?%2)akmbAo;nY)o)nf*TQRN3b`+-3bmMxIe+8363Io4#A5F&LQ|X z!IuesK=3Dme-UCxh$$iU2x&@)2O+Hq2_a-4AtMQyPRM*h5(!BqB%6?Zgxn(JF(I!A z`AzTg^tPtA7ri^tdm_DK=$%0Cb@bj&@9Xq_LhpQf|01*wq1J@D5$a87M?$+2+KbR( zgvJoMfY7CcZXk3Mp$7@QNa$TcUlLkCSXsiV5!Q$>JHp%uYeQHU!a@ieLD($95(!&P z*jmDJ2s=vHDZ(xic89Rng#DtAF@2iQ$CEzY>C=Zk1L!l1K2h{pNS`$Np5w}5aX!c7UcB;1kk_JsE# zd^q7_2%kduLc-S(zJ>7pgkPs$G5Q(P&y0SS^z)}*H~NLqZ#@0x&@YL8+4MV2zsvM{ zNWZuAH=w@}{VUPmg8uIGZ%6-N`bW@zH2o*je-{0B(*Gv?|EK)TfKm*o#sG5$xG=z< z0X-QolmXKju#5q_7;u>Z?-=ld0e^`oMMMoE>J!n72qz-i5)nv5Um`{jF@cD=L?jZC zM#N4ct`c#Zh^IunA>tDQi!!h*1I-xNjDa2u?8v|{1`cB2cm~EWZ~+5XGBBNiI~jPH zfwvg=l|i)_svfWLQatnK8_YVO|XD#IS)3i)7dohRtEvQii24EQ?|L8FrLm=Na~Z zVJ{i>c=mf^P<{)*ur z7+%1LQjBQC2x~^zF~W-x?HLish(3&%#E999$YR89MjT+#A8N$X2f4cmSLnR zBkdXK$H#-i}7N7S;kjmd_BfDVZ0~f+cG|w@qHOTjPc_bAH(7tiwc0CORGOo?VnJX4l2C6y^zOxeejBTPBV zl)FrM$&_zQHDGF4rq*PtHB;@G>c`X`Ozq3m(M*kI>O!V2XKET#H!^hxQ;#wA3RCYg z^(9k3GxZNKC5S0YOci2GiLoHYo|rbobSI_{F_FYfC1x%$ONdD!W<4=mh&e>eHDVqU zlTXY)VoMNPh1lA}S`+I?tUs{<#P%RIl-Qxfjw3dX*d@fK5xa%hW5k{&_A;^ehatZMk!`g zV}?01teN4&41Z?yU`8l2hBIR-GZL7Q#Efia>}1AaW}Iclb!OaW#s_BnVrD63R$^ui zX4Ye-H8UNV>BY>B%nV}YP-c#2=2T|RVdgSsrZRIqGdDAH4>M0P^9nN`Gc%u=znNvo ztSZc^$E;?|a%PqXv)VALE3-nGHIP}6%!*~!a%Qb#))r=+WY&FVyDFF@HVtw=#b}^G`DW4)fo$pa=^p zvY<8#tXN>j0%sO@v!ERdy0M@)3kI`b6bqtQFpUKZSg@J}>shda1qWDgmIb$2@R9}p z3;D)^LKc>0VRaVPXJIoII$vg$0W%Q735d9$n?%eu0xH_HaGY%I&Bvur-gl3A9^ zvI8tT$+DX)d&aU4EGuMrF_u?id3Bc8W4R5>-B{j%<-J)xfaRlD9>elkEMLO%6qavf z`Bs)6WceAE-(dL@mcL{9PgWFTMLAZ~WQ7GQtXbi}3O82xvZ4bkda+^vD@LM8CG6nOxX5NrOomM^Y?F3rR{QDVwA%Bpo8@JV|#+dQ8$sl76$wkX2P!WyY$8tg>a57ppq4 zDwI`&ST&kelUOx}Rm)kG&Z^C@Rsth&LfXRP|ns^6?OWVJD?Oz9e@dIhf=Kl1Gy~k>r^qCy>0HLIpJ=Qd5jVo)~vL=u<;j9_Sn#ru0%bMk^S;v|!tl7t!Q>?kpnkTGz&zj$) z79+JhsWnKoAk~&sPg2{H+MU!;Qu~uSoYe88#*jLP)WxK(BsHDX4W#Cgx{K7qq@E=8 z5~;UIeM0J6QooXBKw5dyYLI45niXmGq5NW4LyGq)9(q5AGiL?UJ4M;a8-Gp>=(yd6hA>Ea9f6_aX9!&ZG(npd$k@Oj) zFCcvd>FK2BkiL`j!=#@f{Tk^HNPk88XVU+$wm56cv9>yE>#?>eYaLkY$=Y_T?ataT z)(&RvXx2_-?KIZTW^Dp%m$5d5wVABVW9?4Xo@VWJ);?tITh{(y9jr5AT~*eZv(B1z z&aCreT^H7cvTgwDhOsV+bu(Evk9CQxTf@3c*5$HpJL^uc?h@;6vF;h`zL8Oc3?nkC zkWq(>CS=%?;Ymh&G6Km6Cu2AnQDjUdV=fs>$w(z5n~a@g93kTz88^vzLdJVCzK~H! zW=S$Dk!eO|V=`^YbSJYdncd0kL*^hdBgvdf=3FwDl9@_oHkmugJVfRhGOv^Qh|Jey zekQYk^~G3UhV@lgZ^n8H);DLp6YITL-;VX&Sl^rV5v(7{`Y6^;Sw8evEdOLUa=vc4L`{$LY5&}<;bc= zR&BB@$ZAHGJz1_~`Hb2eJB(VmSSY;4QMZfp!?;~+MUVdG>r#<6h`8&|P0gN>WmxQC6$ z*m$0eH`(}@jc?ibjg9}vE=hJpvQ5cuNOp6w9msYg+n4P2WOpSyi0r;(4qK5J^1{g*O5RxV zrjQp$-U9NLlDC??wd8FiZwq<5$vZ^e3G&X9ca6Ne^}{*gS{L$!y-i=GScg z!4^xl_^@RdTlTT#6k9&96}FaTYjw8PW~&8TTe7tSTL-gs0$XF*x{|FY*?NJkPuTi} zZ8mIkVOv|a1+r~2+h(&ZiEV4xc9w0|+4h3%mD%2y?G9`YWcxg}uVni+wqIiV9k#z_ zhdDc}*wKw0gV+(pj#zeVW=A1AjoDd^oekO9lAXTn?90yK?2KjSTy~yf=LdEbVOMQ- zHDs3)yV|fTf?bj9n#QgL>{`RF{p>o$uFLFtz^+1e7iYIIyX&yK5xZ^K-HYAh*d532 z#q8e5?vw1k!tVR*&Sy^%_LN{xdG<7Aj|Y3&v1cfI#<6D;d-k#C2761h*OI*>*c-*( z`RqNy-gE5z#J(!*Yr(!&?CZD_IG9f zDE7y)e+m0Huzx@MpRoTE`+su4m;*I9(1-&qIM9v*y*MzG17kT5$APsR*u{aH95moy z4Gz}lpfd-3Ihe@7Z5%wx!IvB=&7nFRYRMr-4t3_xAPz0(P#%YlbGQVDEjZks!@W5? zlEafZoWSAr96rq9uN*PthzUn(bHs`x4jk#qks%z3;z%4v5;&5>kzE`)#*qsgdBTz3 z94*Sx#vJwK=xB~E;OJhCKH=zVjuvpNGRMp~=ESi8jsIbO(rmHE$s z|Jv|hAO0J`e=+z?tWqdC!>w&KhyHA!nO$)|InCoE^s5SkA_C zb`@uHIlGIqM>uKsRxM0BrYc9BOA&3k8 zxe&*NrCeCcg~MF9%!U8SesZxY7wd7+pNk<}9L~ilT%5_pL@p+CaWfbHi~iE0wrn!WA>FIB~^`DE3W%-J&@~R zT#w><4A(9CVjT=R{QIi`M-0#wcz?b7KZKR&pbY z8{4?CpBrbmahn@Yxbce{|F~(yO;c_*;-(unJ94uJHwSQYJU3&wxqzFixw(y-`?z_Q zn|HbSgqyFp`GuQ*xK);0)w$JxTQ=PC;#OyF_261OtBwUt}PxpjtHH@WqM zTLs)U;C5MVH|Dl8w>`Ptk=wnvJ&oIO+@8nnCEU*7_D*h}kK+Dh?l0kf z68E=o{|xuc{gE$^!@L(4Yj`83e4=(fI zJ`Xn8F9!K*yj>qeGyqU*`czl+}k9qux$KQBd$dd{@Y0eWDo&@luA5W(6 zB$X!{c(R`-=XvsmCj~tH-yKXvo?7tKnWsKH4dCe*o-X5Q22XeK^f*uN@booLzw*q0 zXVrODk7rgqbLUxCo`v!(nrG8_HkW5fJj>wOE}os^*%O|<=h+vY74qDe=hb*_#`6X| zx8->N&jWcrnCHt>#rWulDfj1g|df>H)9vc~!{k z(!8$0>lVEB=XC(DBX~WW*JF7-mDlmSUc~DRUT@{~0bZZw^$lJ>;`JL|f93UG-qhud z18+L@it-d*6` zE8czOy#epdd2i2qf8K}iei-i~c|U{qD|x?>_xpK&oA=Ln|BDaB_)v)twfJDkhn9Ts zv0(^3j@)j(qgz zV-G$KREBXKUREkfv_|%9`Hhl8rQ%64a=F<>9Me-?zPjP%&$fsmJ zZQ|2GKAq>&T|T|y(@#EEzf#7``U*HJPtl`MR60NBMe=uebR6l&|0UR)lXxe5=B@ z`h07~H*dZL@U17``tWTm-(vVShi^%I%i!A{zMbdWeZGC-TOr?#_+FLo7JPT+yAR(x z@jZm^gZMt0?-ThR$M;0OujhL%-}mtSKfYh#`&+*M=0{0>)a6HGemL>Niywjf2m$e&zD(5Wg<+>jA&s@ar$Xi}SlYziaZlIlrCw?aS`~e)r+`7=BOZ_X>Wm<@YXr zU*`86em~;(M}8MjP?Q2A3hGhNk^)Bx+EdVlf}RwNq+k*Siz&#WU^4~#C^$mFSqg4a z@RovK{4wN@8Gr2fy)aPYiz+@F#;mdHmVKpJV*Fz@HoZdB~sd{Q1XU zWB%6RZ)5&C@YjdGUHIFNzoYpZ!{535P2%qs{vPG;S^nPT?{ofs;_puiOHx>!LQ4uA zDfFbU4TYU4456?eg(E1ONMSsMi4>+$m_^|>3inWWgu+u4UZ(Ihg@5^H%)jdVtH-~l z{A741Y<5hjL+ z86r`v5!vE^IPt&!D(;GB;(zu6sf1KfswG)UPLiM0OByJRl%`0D(pqV=v{O1RU6Jlc zPo$43ii(y|Q4ZqdLD%wFsLsYb%ijG#%nJT(iMUz!@ql)fP(c>z5QAKa6 z=sOktsbXbRtg?#LRWVByvr#c86?0Ru?kX0pV#8Hznu^7%*a{WPP_fM_c3j0Ssn|0W z`zeE>GAJv9Dl%v&gO)OIl!1>7+R31o3?gJOP6ktDFi!?6WRM|)Ei%|AgQGIID1*B) zcp-z2GWez9rBuAKiq}uHt=Ee4vU)tN1JxU##LOD!xg@52^S`6~CzB zH&pzIihoe?-zrgBC2FgLl}fm&gpW$JSBYLK(O)GZRU%p?W~oGiN+hX7hDzkA!~vB! zrxLeS;+0C|tHeJUmX)E24C~3TsSKTD=qbbYGVCJ5AQ=vqVU!GK$S^^MYh{=%!<{lb zAjAJ;cvXfEW%yc#-(*;zlBHGBL?s)jWOJ2tQAuBw?5UCwDjBJgQ&n=FN-j~!bd}ts zk_T1tv`XGn$>%DWuabXM%2=i9s#G(Ta#N|+D%DA)daBd_l^Uf|lT>PfO07|;%__B9 zrB0~S6_vW9Qtwsjt4jTmk)ez#$f%Btn#ib?j5^CGNJax>6e*)=GD?t9ii~n(v_nS6 zWOPABw`KHDM(<@*pwcB(y1Yu8skEg^d#QAQO7~Ie;VK=e(i2sBwn{Hk>2)f-Nu_tF z^bwW5sM2>-`khMuQW+zasj4#ODr2iM9xBsDWjd=&kje~DnMjqHt};tiCQW6wsmy+r zIi)f;RpzzI6sW9$%2ribGnKVaStphCRoTue+fQX9Rd$xjCa7$(%I2u-L6tqDviDT> zoyz`}aS0h$l(DIdt!3Ow#;s-CRmKBk94X@|GLDyVvWz#%_<)Sh$@spEKgjr}%9T{P z3Myx=ayBaGrE&o(*GJ_>s9dzl%~ZKXDz`@EHmlr0mAj~Nw^Z)6%KcDzseEabucq=1 zRlbGFd#QX!mG7AMg(|;X<=3iwj>;cU`3oxlQ03pN{2x^)sS1@;p^hrF zPz85Y=%flEsxVX)Cac0MRY*{UWL4Op3OiKcs4ARQg?Zr-~(1v63p* zQN^aJXs?Q{s@PT)168q)Dh^h~@v1mQ72{PgQ592FF;^A$s^WjDcu5s+tKvsh{Gm$4 zRH?iw)mEh@s${Q9-m27Dl|of%pel`1rD#=(Q>8>zTB}OiROyf^ol~Xjs`NyaUaQhC zRW?xN%Boyjm7A)vt1A1eatBooR^jsuHUzi&P~=RkBrOyQ&;hm5Zu!OI7Zx$}3gLSJk4b zT3J==t7=nKbx>6|RrOQVE~*-&s{K@Th^mfO)hVhvLsb)0b+xK)P}S|KdO%hGQ`HBm z`d(H4s9H%?Ggh@4s#Z_cnyQ+;s(GkdTUG0(YJF90psI~hwF#;gr)mkRwno+RRBf-S z9apups&-4&o~YUfRr@Iu$)u!As>q~{Oq$BXRwgbo@sUXfnS{t>s7%JoWR^^p$|O}L zSu)uwlY=rjFOzFBc_5SbGWjBtf2v+e)lF2rfvUGqbq`e!Q1xD_-cQxXsCta5FI4pu zRnJxR{i=Re)$geKQ&oSf>c3T^m}-<&jcTe@<3u9_~Y>8qNZR5L_1hp6UQ)tsc7b5wJ= zYG$bBHq|_$n&(yXu4=wk&9AEYSEfZ}T3)7RGPRPavrK(t+C`>)WI9-;BV{^HrqME; zDbqxmrpa`ZO!vw3q)e~N^r=ig%Ji>l8L3uz)iP79#;RqjT5hV=%v#B;t;{;ftcT3{ z%WRCyqGT2)v!yayE3+*!J0!F7GP@aonsLm?Y$yA*!sK0Yq zimGd_x-C@KO?BI;Zco(>SKX1SJ6Uz(RClrJu2J0_)!n1ICsg;U>fTk|C#w5ib-&6S znU|KiiOd_wyrs-tWZqilon;;*^MNuSA@gXN&yo2unWxD-Pv-k%eoE%oW&T9w?`8g5 z^-8E-Mb)dVdRD6Eqlzv`V`r}o9hU(8#{gtYpsruVg|FG(xSN*H1 ze@FFSs{TjS|Dy&a)u4(Rn5#iEHE>n~FE!|(20>~tPz}ba!8A39SA!*LkgNt-YH&ae zE~~*4HTbLsKh?0P8kSW<6E!qfLn}43SHo6n=%WvyHH=cj>1sG%4VSCo z8a2#P!)Wf>vM(XyN@%h|F_lx2!6*U55|EceRtKUrRqYEnT>Ox46f zOw)%?#A6jG9$eGgCFI zuVziutfiVcsaYE}>!oIc)GSKP;?!)unkB1QuA1#rv*T)ZUd`^Q*;_UHt>z`wyqcOf zRPz>U?xE%ZY96ZQ{nb2D&8MsRVl`i_=9y}~SsQ}gd?QA90D zsYNBVsHGMbYSBV1T-3r>Edtb{ms<2wi{WZ9K`o}K#XPlGrWUDcu~99ytHnXJIHeX> z)Z(66yikjLwfL=;#niH_T2@obx@y@(E$!6OT`k+FWmmQAt(F7Ta^Esv_@Ikmi@mXFl(jaq(H%R<>0%BF&BYRaa8Y?{f&NjBcH=^&dx+4Pmo zP}z)=O^j^j%4UgdQe?A1Hd|%0Up6OXb4fOLWb;fmA7t}WwvugW*;bKlZP_-Kt&MD5 zW$Po`cCzgy+kvtjC)*jaT`b#F+2+Z1ziiLQ_Lgj4$o7M5zst5zcEx2^R(4fnS4(yc zWY<)7cCvGoov-XV$gaEWddqHr?1sy3ob0B^ZkFs8$ZnbJQe>AYyFA(Ll-)tuosiuH z+1-%c1KGWh-3Qqf$lg%)m1SR7_SUj@k$r2~cb9#*>_^HzO7_!aA20hQvQL(MrtCM# zez)w8$o`D%ugdny&SvCF;tENh z{3yqtazaj}bTxQ56O)lHza$BuRsa0dOYOhxR$<IJXgpwUtX2u)m&a4@)|C$De_t? zujBGABX1{pKa@`s`MAnwiG0q==Y@Q}$mg$o-Q_!4zEkA8RldjM`$&G(<<~`iv*q`n z{I1CFll-g7-%S2t^3Rd~1NrBxwT)W$QtPp5Jx#5zsZB|>*`PMN)#jzz)>YdfYMZCF zht&42+CEj=UutKrcGhYauXfAS?ylMwReKY)@1XX5)qbwpuUGqhYX4mw%BVwKb+A?k zZ*{n#jwF<2cV)iF*TQ`K>wIv!WYe+sCk04oK!DWHu4x+|cs0!AnxN&!g< z*rk9=>QqjhOw`FyokpwEc6G{E=QirxU7bg(^JH~SRp(3UQd3~5Ko4SUpYnHkmR@XP`R$1Nb)Gb)u`m5V)bxTmUwd(etx=Y>d)V;I1N2~iX zbzh_Iht&PPy1!G8BI;3GJsPM-bM>%S4_Eb=q#j$;#2IZQ&2SpHBgYJg2pIl zmVy>3C`Un06_l@_-wJkC@CXIJQt&T@R8dF+g;*=3k3xnjWQ{^@E99+u7gO(M>g}oC z0qPy8-m&VvK)ut|dyjhGQ>dXrtrXf(p%WCkN}(4NW}vWU3UgIhYlRI`*gS=;QrKpN zU02v^g%zkz8TIi|pU&!YLVYdNw}<-9Qr}F4n<%`V!mSnFM&Z2`9;NVjg|AZhe+vJi zel^w4QT<%iFF^f*)$geK6{^3p`gc?RQ1!o{{!cZarUo?D051&~tpRZwa9RVdE25|( zOcmjwh<1wTric(lBq`#MB0g)Nu?9M7V5a_$r?(7<>ifQjbuAh2jsl_>I06bbc7g$R zcNYQz(j8LLCEcmm2?i>5cPF+VyE_LQ!2cP)zvp@5F!!F=XYIAuUgyo|bOKKI;Pf8O zjp1wz=aF!>h4W>&bcf4$xY)qO87={ENr1}%xSWB@O}Ms(>lC;~!*vH-|H91-ZnxzA$XpK=Tmsr!b=lg?(n(=ujlYKfOlJXFNAjhyi?)*1U{qT zGZ#MI@VNrt-taYtuN8d9z}FYPdGOr=-$U>-f?r4YO@N;#{Lj-*?pw9?WBDfKPoe-RY-~t41LGU32 zKSFQ~LbMSw93e9hVuuiCgoGj_7a`jaQh|`u2)TohrwI9mP(6ey5ZV!;vk_W=&~k(x zN9b*YzCfrFVGR)06=8i5HU(k62n$149Kx~?b_QYh5cV2jzY$i8<$bVxGM4*dc@~zJ zV)<<>e~s|^2yck+76@;T@W}|bLAW=<3lLs`@aKpajEGT)ut7u?A`TGzfhZsl1gd%1oVm2XWJ7Vr3 z<`-hM5IX>|V-ahE*b2nHK%5+LJrFkmaWfESgSbS*Wg_l2;yxhmH{$*xUP8PA@!b*M z5ApVhPeObJ;_oB=J>sj7&=3hGNH9Z!ITA)AVF41{kZ=+SzmQlTiF!zEjl|JN3_;?0 zBpyTJ10=q}ih5XKj1|MN!WAp>uwoNdoWqLyNa}{9X-En~QX-Plk#q$~;^9jq8zOlG zl4l@!DUyAV9D?KmByT|SK_uTp@>itjA*C}?dLzXKDKSV%MoJk{_9NvqQf?vT1yVJT z+778^NL_%`4eMxWELZH7c#FQ^D#2N zBC83q1|Z7{S(A`816dBpNl5wcz*>o>CNBfAl@yC8cMvICJ_jO?q({(u}A za`chY7da!5GZQ%;$XSk@Oym?JXCrbhAm=%9z96?ga$6zS9=Tq~jX-W1a&wSdg4~11 zJ%il4$bFBzX2|P{JS*f)K;9zcg&;2hc^i;-A9?SQFGGG)jZxGaMUzmp5JkBt+Kr+|DEfn9Efiazcod2!qSy_^ zaVX9~aVd%qqxd|EZ=v`xid9(I1S=I-X^xe1vCz zl|EKkVAU9`vcswntjfWvGORj?RhO~q9ZIxO(gG#MD6vGzB9tsaNf1gBQBsVO11LFx zl4~e=jgl&?Zh_UkvDzG~Ct~${toFg`SgcON>Z4fw5NjG?jTzQV#u_iI$;6sHSaTR_ z&STAeta*pEjj^@|)=t1$53F5=wJBJ;25S#s?PaWeiFI&{`_C#?H{^}za;Slz`o#A8e3eLl0~ijSbG&P=*ap zu;D8DW3STOF{~3tI!QH3nNZV(VpW{erD3Y%{?&Gi)=*wk6o+j%~5nR)%fI zvF$##J;gQ^O64eRhteJ>9g0#%lm?APN3onDjuNXDJs5VpA7qqux}Lh&Bs0u z>K`Ihu7oq9vnW4!w+!y8xGguh$fD-$B|JuG95?!aHJGRDskiojyA;6HaOY? zN5|smBph9YqoFvu5l2tp=p`I|gri?^tUiuu<5&wEYl~yuaLgRX=HQqgj>X_u5sn?l zu`4*H#PNDK-T}w^;J77@FT(Kv9M8b<5*$B*1JQ}b}j z4yOWfDh;RdacV72ox`a*oNk5FQ*k;Tr&Dk`2d7JLx*Vr(;PgA3{)y9naYh$sTH{O? zoEe5Q^Kr%(XV&A)Hk`SQGaqoK3TJ_{ZE)5MXNTgfHO@xh>?WMOg0pXMwif5w;oLx+ zn~ZY{aLya&LU3*k&Q;*t4V-(1bDwZdiSsRR-UR0@aDFt-PsMp#oDatNES%qs^KWrp zg$tUv&=?n5z#4EAFdC>_4&BI1lOH#Js#H!as3dk-^TT4xc&t<>fweAH#*~nIc|){ zjRm;jjvGO^k%SvXxUma2j^oBN+^mP2O>ol;H%H>;eB6x0%{{nz6E{EMW)*Jf;8s7} znvGj_xaEagQMgr%TV=R)2)EAR)_vUih1&+WJp#8E;kGw!C*k%c+&+)n4{`e|Dv3%x zRJKRuKvWJzXaa&c!Z?o{B;72J7 zchBMOb=-Y{yB~134)-M7Q{Y~E+%v;H3*4K9d$zdeihF*z7lC`pxVH`WF5=!Z-1~+5 z+PL2p_ebOYBHZ`J{pGlyiu=X5zXkXA;QkTZzlHlBaQ_b;=;OgCJeY6(FHI1;Kd-kn1L5Ic(EKWlJFuEFV^5iDPHWw zi>r9?A6~q}i=TM$2QP`2+IZOwFGu6$BE0m*%Ot!k!pqHgc@Qrz;^iH@{DM~v@TwVJ zb;GN1cr^>JLh&jEuL|&LEne-$t221@5U*a~)knP6!t3^UZI0Iy@p?L5yW({YUKio@ z2E5*h*N5@?JYK)V>l(aihBrO%W*FYg#Ty^IS&lb3c(Vy__TkM@%Ay^D)CMe@0#FUN4)EXcSG@R9NyXDT_E1Az`Jz3%fY+# zcy|!*F5ulQynBRqAMoxw-Z#X1L%i>f_XF{MDBe5aeF)yi;e8t3=i~iGyx))a=kfj? z-hagh89wOYLuY&#i4Si05P}bh_>hke<@j(0AD-jGTYUI~j}kt%!bdZFw8Y0n_!x$d z+4#5xA9vy74Salpk1z4@7e3bFlO8^q;*&W(jl`!Z_~eC83HX$bPX+jN6raxE(^Gu< ziqG}&Sr?yM;d2*!9)ZuZ@YxNYBk?&Cp9}GMBR-em^D%tBj?b_0xei}+@I`?yo$zG@ zzRbp#P<+Y3mr{H=j4xO5iF6MSofZyoWi2fkV0+hly3fp6CM=7ev)_!fb0N%&TbZyWJ#H@+Rkx6}A` z3*X-3dwqOwi0^v%ZjA38@qIYH&%t+he2>KUbbQan_hNiskMH~O{TjZ1#E<&;(F8w? z@nbN4%*78|{0PL4RQy{ItZ+nfPgspPu-cfS)<|xeq@p@$(&i z)x$3X{OXNgWASSyemUS*AbzFd*J}LQf?vn+>n(nL!|w+8-2%T&@VgIwkHGJV_-&2f ze)ye;-?{j`6TeU5cO`x+p_D^u2xV_52SPa(${A2Dfzk!a7$`SFSpnrqD4#%Cg(?}U z^iXAjDoa#NM3o(?JW&;dsu)ydpsEN}8&Gu|RhLk8166NP4OF*8br)3kLiHe2k45!7 zRNJ6B4Am*9&O&tws&}FK1gdYN`V*>a@TU>}bikj!_%j@T#^cXy{ISQM0Q`x^pKSao z!JlpTvk!l6*fUsLG)_4b?@cZb9_~st-{8MU6IU zx}#<$YJ5-=hnj5E>_*Lb)Lci+bJYAqO)Y9A)V4uwFVqe}?MT#4K<#YQ+M(73wZ5ng zM{OEv*PwPUYA>Po25LW|wib1Us2hR0$*7x$ItSGGp>8?q;!u}~x?VD#{CjJ`WZ(IDez~4#u>wv!=_#2PE>G-<_e@pTAIR4(i-$(fSoAvZquQls+WWC|6 zH;MJ^Suc?F@>p*l>s@8N*Q{5?`f}E9$@-ne?}xI!1?yX~{z%pz&-$}je?%W3D&>J`uAD?Gwat7O$bAx17S(bB7%q{qL?TpP7)7@DpHHoBTdN8WG`|S zxs>!FL&#Ke2YHaZLB1rvut7aGXu<|&Y%rV+X0w448-%by4jZgtg9<25yulr^ACqRfc0 zfs{?4Y#L?rD6^-`gR)S{vMJj{*-^^QQFfoQ8ft1&vm-UlsX3CGv#Gg+nt{}eqGk#; zS5k8`HFr_-HZ?y}t3I`wQmZ|+`clh+T4Sg+ms$?g@}QOTEfpK;v7s3o4rIeIY`BOGeb_LS4YS#BJsa+3!{cmtmJKhn;Y&*ODYc~3lhPnc zBPlJW*|;Mc4`$;D zY&?&Rz1cX4jkmDzQ8vEL#&_BHB^!TbV->Xnd{+WV<} zhT4~?T}kba)TvLM#?;ZLPABS^Q)eo59I4|;odD`2QfDP~wo>O5b?#H=4RwA|w?1`s zsN0UZ{ith2-5J!KPhAJX}l{oO+X}=RiGw>Mf^U4)r!r?+o>B zQSUAFRBWotrY+cX0GkeHQ#&>dVAEtaUCE}~*z^dSK4#O;Z2E`I8nan*HtWu2BiL*@ zn>n*t9Gm5_*?KlBXS4Hc_JqyeP`?57TT#CQ^#@RYEcGW--;Vm8)DNS60`*g=UrhbA z)Za<{gVeu9{pZyGz~=SYyg8ejvH37IpTg$0Z0^J6k!-%2%{Q`n1)CpZ^BZjbfX$z> z`A@bWTeM+|UTiUrEo|Asmo1XnB9kq)vBfdAxWg9j*`kgu+py(eww%e9)@#Y`IoI4u~jR!>d984*vg)*0@*5(t$+Jvq9 zu=Oamp3c^GZ0*O^;cT75)?3*6AX}ed>t}3TO@$T}MpX2sViXl~sIa5LpNe=Y@~Bu( z#WpG`s5niV6!Ca-DonI<(fB~9ftZA#NNH0?~&el#6I({VJNOH*r_y3*8_ zrpsxXMALklZlLLQnjWI*8Jgat>0_F{rD-kO>#}`&w(rjNL)d;I+s|Tqd$#vy`$V?i z!1gED{v6xiXZw$A|BD@D>|nqSUD#nXJJ_*|}=ub~wupkJ;fn zJN#is9dks z&aS5HI*?swu&WKbda~;Zb}eDoa&|qJIxx?tP{<8(QGu$=F!ZJW~@IV&a&Hm zcKgC^wd~%Q-4*QKhusIW`vi8kW_Le!k7oBIb}wT03U{Xw==Va6Iqb8IefF}?arU{%K9AYwE&GCfo3O75`}Squ zQS3XDeI3|0gnd)kH=ljW+4lnbK4jmI>?dPCefBeEzaH$@kNrlm-(vQ2Vm}Y|i)FtO z_B+6Sm)P$<`#opB&+MmUe>wZNV1Hxw@67%~*nceh&t`vT_77$MRQ6xT{#)695BpzW z|9kBJj{R#nK!*d`azGCbu;PH}9AM7@Asmp%0qZ#6AO~FHfQKCLnFCZDC~=@M2U>98 z7!F*_fzBKl$brcmn9qSLIdBsP?&QG39C)4suX5mR4t!5@(p*mS)->-%^AR+kL~}=) zhtWKV=2;@q!jL9Hh@d-8smdgGO=CTn_T)pac%ea?oQA`pv-&Ik*)EcjaJ9 z4xY=wP8{sZ!Eqd%%fZ_?_%sJsa`1Z&uI3Ou4(ZAvRva>wLu@$2nM0OyNFIl5=a8cu za+X7`amarh@{U7(b7%t&ZOWl096FdoCv&JBhX!zHJcq90&^;V_ltWK*XeEa}=FoQ> zT1`tGTDG9287-}7Ih&TAv|LWhBwD7^awRP{(Q+3p57Y7zEh}mHl9pd+Sx2kJv{KNj zE3Nv_YACJ7(rOy5=F!TDRz9=}rBxiQ(rC4YR(oi5j#iIp^_fxUVhb`nVHx3Kuuq+PS#9_xc>@J5r=dcePR>k2BIJ^mm8*_Lc4!7WND-NH|;f@?0 z%;6~qCH2Lal{~wn8*3adZ$z zM{{&OM{nckBOHB$qhE3KFOJdT7z2*!#4)`&#)4zUam*r)S;8?s9J8Eb(mAGtV>WQi zHjb&_n8O@%onszx%nyzQ$Lesb3CH%~*ufk-j$>zYtUJfXaBLCBZsph$9D9yquW;;L zj{VHBzd5#sHTm&I{wIBpNeo#(jg9CwG~UUS@U zj&IEIrW`+r(~Uc|``ob1WTVVs=A$pxHT!pU1Wc^@aA;pAJK{EU;| zak7$=YdNJMr?lXd_MFm{Q~Gm?C8vz%ltrB4#wnqklENt^oU)r!&T`5t@Y zry6r=Cr<6lsiQb`0jD}}swbxga%u{v7IErUPTj+)7diDlr+(zrT29mDv<{p$nA4_m znj@$Aa9Rkb#dBIVr)}i4y_|N2({6FvGfsQUY1N!gPH(~KT{(R)r%&K?TTb`n^k7a; z=k#Ju-_GfKIsGuFU*q&Aoc@I~G&rL*XY}HXF`Ti8GdwvXoHKGbV=ZTtamF6bIKvrN zIO7Fp{N_vW-w>QaApo?Zsg3voOzuyUvs9Cvl?=i4rdv1 zmKkRa;jD?AWzAWBoE5@Z(VUgeS!+0JFJ~R$tZSTgm$Tk-mXfm@bG8X*n{)Ok&YsEH zOE^1_vy(Wxkh6Dk_6g3u&Dl>m`y1zIa87g1>A*SWoHLPgW^s-!=lF6?H0R`V&T7ut z!8r#x=N#ue;v6wv#ksniYs|U*ICnhfF6LZE&h_Qo1kNqu+%25DhjY(z?mf&1DaId3ZG&E`B;&RfoTah#XOd8M3pl=IGUUM1%}<-AXvSI7AcIlndM z_vCyF&Y!~h)|~Ia`N5o@#`)_x{{ZJ-<@^_%|CRIqa)CA%7;`}nE*Q)Oleu6C7x-{N z1Q(=n!747;%?0PVpppw7bHQ6K_``*bxUdZu_U1x!F0|yrnOx}1g&|xR%Z1roxP=Q3 za^Wd1tmMKsT=<&{Yq?09i`sBe4=%FcqA6Tt%|+f^6wE~_T(p{t%DCtx7gci6V=nr{ zMYXinrL`fgd(nCrt!LAE39Wr;9ZBnST5q8BL0X@q^&?t;<6;>Xx8~yZT-=X~M{@Bz zE_UVOKrT+^;yfe99iZM)ER0Bx;kJBhaQY3o2+H`@BsHj1{%v@M|RI@<1} z?Lpd}qwQ_lKBMgi+Ww?necCmqodNAkY1f^018FykcGGFMfOht@bERE4?NVr0M7tfd zJ3+fj+P$LP7ux;ek_KF&%O$2<(uYe1amffSnZqSZxFnEElDK3gmy~kJVJ^AOB`>+; zBkk+aUW@h$+MChdiuN;UKcDs3EQi$LV;TjxXt0#ib3ov<;Vb<5CMQ9m}P&xYU_TgSj-3OILE~b}l{1 zrI)$%F_(VhQYD>abZSZ`13Gn}Q-3;*rqe7s+0x0GPGNLPqthxnmC@-iovza90i8b3 z=`Wp|(zzp@d(n9Wou|{;md0C`04Z7&k#e^;c=rWux zGw8CEF3aeWOqXK1l+xt@U5?S^0$r}rLKZhPr=o^B86_Ka>n>0Y1ix^y?8yBXd4(S11GXVKk`?*4R-rF$0L z*VBCm-A~f}Jl*fn{R7>9)1x6hn$e>TJ-X0i2tB6KV+lP1=n+njRC*NBV>3Mt(c?Tl zZqeg0Jzmn|3q3(k9eNtlvl~6F=sA&|i|Oe@&k%Yh(zBSJ8|is~o|oxaNzb?RtfrR+ zz4Ym2OfNHf4WidXdM%=t7rjF0l}fKQ^x8wOGxVyY*IRo1rdJ)kW%SmkcL#c#(|aPl z7t`B^-VyZ9qW4;Q@1*x(dS9mZ1A4!ucNM+=(np6rM)c`HpW*bGMjw0n1kxv(K8f^6 zqfZfiHqmD{eU8!R5`FH{=NWxI(Wjce_2}D(zAfo%O5gtU9ZTQ&^mU_eD1D>on@!)1 z^xZ+<6ZE}7-)HpwMnBR|pMJ*l>rKCr^qWY(ne=m{-!l5e(l3vG>*-fPzf<(PO24P{ zdr!X_`fJc%pZ@LXKY;$@=)Zve?(`3*e;obu>A#--rSv~a|LgR>L;tt*|HA+~s z2DD*74+aclzzhaBFu<1q!3;=dKp_K48E}*V7Z`Al0k0YGg#lFztjEB{3~a-|9t<49 zz=;gBW}p`XqZpXYz;z7V%fNFCywAW74E)7qnq1b3%er#e5H6d@W%IbqmCM4oERD<7 zaM>0v+s|bexa> z5$22-%ZM3_ux5laBRm-q%!pV>#`GJw&83jh^ zF{&M-dNXP`qoy;;j!{00iegkIqt-I2oKYtkb%9a$8TF1)Um5kAQFV;gX0#!rO&Q&n z(IXi>lhO8!_G5Gmqq7;kp3!?4eVWm?8U29K?-~7vF%n}87}JF@7K|Csn5m4hVT>1J zmNO=mF~yA8%9w+UIl-74jCsbGZ;VkgR+F(U7~7Gt0~l+`*eQ%%%vevxhBG#eu_cV% z!PsMry~fxljD5pcCF3+1*Mf1TjO)v|k&K(pI2*=!FfNpFD;QV6xXp~)%ed2wyT!QY zjQhs;dW_d$ydmSeF@7-P$1r|2;~g09$M|T*XEJ^b<99Ot7~`)p{xRb}GG4_5i3zQk z(1{5Hm@t|NvzTDd1Xm__GhrDM;+c@kgbhsC!-P{zxXFZjOnAeD-%QkCVlyV1FtIli zhcR&~6K6Bgfr)NR3}NC5CKfPpGZXhQ@hlT7nfQ{4Kez&1(S$3ExuOSG4CRVRT(OWV zT)85cD-yXPpDQ+T#dfYZ!WEaf;vrYO=Zb13X)#HgNo|;9#-u?^8qcJ8OtN8;CzF;l zDTPVJOxnhzgG{=>qZ1{RWP*!Q+qPif~g~!I+>|+nQF&WH>L(KHG-)tn3~Ph zRZQK?)N-aCX6jj{UT5k9roLqA2c}jtO_OQ*Ofz9xFQ!>CZ4%QKFwL21flP~KS`yO= zn6`;&dzp5MX*ZbmglV6creb=1rt2`h4b!_a-JI!`OrON`1x$Blx|eP+F3))!_e zne~_1n#|T=b_-@3F}owPdoX(dvn`oDhS^h?J%`zgnC-&sKxW4J)n7xhJ`8m^s~;W5Jv;%$dm?TjqE$Cxkf(%*kQSTITFv&SBNxo?>J zlX>--r_H?9%m|w$!hAe2# z0uvVWWWf*?jAy|d7TB}En+3~Rki>#~7HnWaISY=l;4%yDv*0xgez33}3mdc0fQ21d z*q4RFSU8!53s~sHLVp%Uu`rE=#Vp*y!U`6iV&QcbK4#&27AjfPfJIGMWXPhsN*MO#=@&Z0vsI?bXhEV{#@CoFo)qHiqv z!(y_y5sRC$*nq`tS=@=m-K(!kHZ58RldJkDTR&B{mg~E02;FZe zm)*&&a+2iwOLKg#7>b8Q9(Tk;h2DSSnW9-&Nv`OcB(41}AFXJmEnPR1*EEtUC3!?h zda;qVs;^S#`)d%0{M~EXg$$I{n1@*=6`lX!o(aMSK>VP!*&NNyGDHdh_rI%R*k<|UUxJ3E)q35Igt+9lG#x~uy*kvGh$Z)DOlq+@jDs|<1H?F#FBv&-cloWCSn@nN+RmA(JS{<~8i1BvTmfc2j z&GF$*E=F>dqHdl5u(w=N^gSxcU5fnf7%DfZAu25oStTHnUAHf^wO$%DPAorI)my)2 zhotDVOtKT{ClCLGNu8@>k^77D<@giR*rP=@HsRn=Stz_QYRZ4dJs!q7O zVA5b^Jyks=)k*3r_LGe*58o@_TV8Tito&7q(iB8dnd+}C-g2(?gfvBUN3yQ4y?D;x zfu>wjwO_pJt88{s*-U=gGvCR{d--$`c&~qy(N|~)+%)Yo+yxh{Vm$>rF7-B2Ng^hNwwl1zA0InJ#LI^;emxalBl#wp?DjQ-mo~==h5`w@Io! zHKWAVz4fd9%#{?~!$by6uS!Z?nf#(mIa(S~Jy9&IXbVYx&Oj8v6l=L`T%@~~;9y^6 z{R7|ChjSD2|Ccej+$!8%@Kf1CH9=oc;j$Ki@fiM<$5c%c7ER>l3v$K+N;!1Kftc9rN8CMR(cfzX-zc^<+(*MfkyI*UHMl; z<>z?|>}%IbwO7PZyrrX(LbgYcujYnS`&#T}`MsA?bG=fb(*ADorBI3X-{i`tQq5EK zfjSBS`lBap5xMBCl$HM~Rf?bV>7#$_AID^hrY1rmn&@2?pxNiSiCy$nrxZQRByD?1 z*L}3F-6o_&Up3%c+ODK_N#i;L{X7Zdlfnr0=jr5%-(NOV=-9S zaJ!PF%7*efckx2hVIy@uts>QE>nj=`lJr$knpM{%Wt5`nW=UJsOYC`A?AD^Ar06$P z{Lx{lq|g|x-eHrXeTk&4Xf{`>vQ=nE;*oE@e|fxNV&sa*2thT=&wZ7K+27ZFmdmc> zY$)3lxpkZn6opZgI6_}#<7djo@}tQ+S6wxb>;D-nX4~fk2!JjZm6nRc>mF1#kt_Rd zRQ44+O&4eE<}Hm-YRj&=2@P4O))0k;rKEgUU8M%(S6ii4+4o+bHp!KrrLD4HXIC#b zcZK4l3VfU}1q=QOH z9vTqi?Peg5iEtCet!_zYMV+bXx&McJ|AxZSa_?MgBO%vr9_q#QIfK^ zsNGdUgbS^VRJ{egL&Z7@tv%AE+|~0X)r8>xE3XzZc}Y=5uHi+YI|gkefiZ88hD_hEod`jY0C#y@jjk1kAI3zm4P_9|KQDESj<9|_vskKL` zr6-^696CufqL6@p+P0y#vEZocj8xl7Qs=LavYD#spKW52OHq*M8P+1)l_J~%5w5ie zH%vWC$dO#%Eoa#k!>S<)S%0Z!h$wkKZ-JHNZXmXxgfS(64%5A=MPTtSS&6PZd2U z2vmd_*!W%0OfGszYO+S{6fsbpHI-Q4s-&7<*XN&HcsRHfxd^#12v>E{w2JT-@s5vl z@?4tZepyY@A+tp2w+ae&mxY#$6w*C<$FjY9x2+QPLL9x-S!FA^qS1J%%0?!yvzAu= zqfL`rNBV*?-!Qp171orlME`kY)_6;P}_S%b*yr9lTn&csL#z17N(Q|44 zj}FRq3d2&dpI5Q}0YgPc@lszkRa4RafEqrJocWsr)&^}_zGK6h;>~AD*01#0GHp%p z4)4wRYgcYwopac2vsxHhoe?5^)g#Z|-aT9thp-+Sc5e^bINV4sT8Ys$0fLpLV}|=J zkza-00nxL?H}9KjQmUE?`y#8%E#9#$V)ZZ)c0X~%K7wrHg+ORJWH{ashqlmHHcS*J zQg<0em;QpKe@^~?q%}D#c}glx;g0kbJ(fyJ5x>Ity`<1PD0UV7;L%*ECTUkyQ0*M4 zCTLGpkobB{(L`I4*BzIty2=!?5UHlC;EcM9j!Sa#GBOoTB~=_pAs7G3ZWe9Wxh-U? z$b$a1xT7nI_G-yxRo%oMnxCYx9}~aHML*DzUrjr_>puf|#IiU)Zy_k^D%Y;^guz4AL5TaOyaOV+RQ*&osVrYySqan@$_oX@B;%J&j zd~%mMdqvC>e;$tAGE#gf44O&ujX6b!4dqvIHtyIQSt>O6$IjPfzxCuw9ifB9@-3@# zj<{|S^&T==$WodbH)a1s@j}_@xw4box?=IVa|W4%rI;$=I|d$hxDs!8J<%`en(%50 zi*eF!p*$^)ONy>J(yrLeR>Gv_`|pY~j0;N)2{I5Yijwk0=hToZ#|lP_l4{-v>wZp+ z=2YoVmaxDw`5!S`rYX$|&Mg$SY^n7Bo3Way!bQoHog`s4mx;$YzR+dM|pr=pK|S1A3K$u+Orue7ysQFn+dX@|=n8^{N0T6?SIQwX5ODbWPi zuZZ3`YYMpA}wQO$M=dW%+2*IZIss?JK?wmy|rO_NmpWwmET$b)>O z+!Z+qIffT=R+ny!Tt7j?Ffr1{%Oxk^oYDWf-l($^-X*BJe3_=AbB($?t`nYWh?p!& zeeaLXO1bIxT%|^b-|}@bMU(52eDPkp^Jfh%YL@wxO`Nja(aliNs)stNp8|=(fR< z>k%O?jU`0~;WiZ=pGk@aLSFw5Ct>#22(#~Bb?1M5Kwzov12RR!Q~%^jVbVr&7D{2H zY-*>{m05{VAqk<0frimD&j(ke3h}vIR47dKy7A()1_Dh*vyM`gz*E^=c6jAl;a&b+ zkkl7hQT0n)gVYHWA}7$-9KC&6g`xKRtg?`u;(A*r&L{ixM2%vRzoTK3Z3Zr}> zdg4lEEUB+zX$h(E$uU~Ww*r!KX|tl`FhLb(X}dzUMsiYUYDsdB9M@}xiVh*tm`Km1 zB7DP2X>(-B7$Ze9;mWqFQY95sYsK2eO+|Vt$`l#{B_~z2aDnwCMWgpp%J**XR{fSM zS}m3aJX!VOUkBdwU3fYn7wY%9Ki2&gDQ|mIl8;q1+9h2Thf&s3_Ego=l*=qbeU`Wu zxZM*8xRSHAN{rF>$PN%Dw&6;3Hk2B>zm|!T6{PnOiSqcLzi;_k5LAuUeq{r>>}b4O z(Na;o+m&ne#SO1Cpu$kNE4BTaVx1ta+;T_PAfaqBtMH{lJ2wAM{{*x4?%l5TCwC>on#*!Un-7xxzqyLO&Y}Uyo#7u)p(t36OSe2c- zGTHEqkjtG*OBOC%8t5ogze)N3Y`5T}nroMf)(A&eEO@S}FRr?d;!#{ud~&>@Xj3W= zLHhp9>qM5kihV_Q5x90$W7%KTU3?t*e^h;UKvY-ue-iZ#+=nRWpulwojlCNT#$IBK zVoQvPF=_-vdIu2!r6^SdM2a+N(xqc>Q46^EsdMIcGxheSN~n5#jIO;SZrj5vS5{4qlE8L?jOga$by*MSmq#D+^f- zkHj-|k8KeEKc}w9V)8$euK=g?^k<^J7qKv(d7E|koDV79TawP_bF5Gi6PCRQb9<9m zkG}zUFOlV`+(GH#oV*O(CsOvow3IBpIiyoI_3lQ9dtZcp&3^uL;shqTMzgs~zZ%Vy zE}FAzXZZkvNwc(k`VyA2;doL94!as<^;F^R)1P9uV{LlgWZaI{doW|dPr81%riw3c z!HLtvCL~0q>QbV@(}EnWm45uCe7sBdsLw6_!owZ{mViDw!K z>@TaskpLNnUH4f*S$ji#mFoe-6kjoA91IViu${?enR%T$ks`_N*Z}uHh9pQ2nDGE2 z`cYWy6Sz8`j}TpQjgIj1EQvy7@c0x_Ac!w+<<%piB{ZdNh-*4f*Nlk;u_H#}DlWi|{!u!Ue;+w{hp3{Rh(z>g0XVVLtvbZ{vbmVTNOKa$=`FVEI6%Wxt(eYk&tuCBw46f)pe#Zim{C4$3zPS13^)qn2otH8 z(izOSP;=WDPMdr++vdG%EgU33^2nZb1WNj=oX9ui;=h55=hji|WoF$C`e9ralh5Zy zhVVg2IBwE)?l6n3g){8)K~+D?)$tYE59-7)jokY&Tf6x~O)qweR;l8{o#<}0tP)m} z&$-Xw8rKOx zeVazkOgWIe&q0oA+QE@`HRxJXiCF?1@uJ zG>z2d2?9&>y8senWt?9ij+j*oj=orKm<)3|@HGCf%tUN_orxGev4EM@Nrr!67L^#B zr#}IbwQy;;{>#@~e!{lK2G5ApZHiCwKj@8Z98wZ_S|`G#)5YaAjWNYr^h&t2B|64m zFT4G{Er*%z^dR~oQ1{CeHh-t#K9f`&db}MjRm5p%bUK6$Dv>&m{mFcj;S_h60elS^ z)pw7(RdMS@ESx;F%N5W6VSsn4hj9aT7l^n435Y^HrIoEw9qnyLZs;CsINO8Gv=k@U zhbehd@0XbH@Wa8^b*eSy4%STZLzEB(bJFD%$$F#M6@($Pm>3Gyo+5}934Y*gLT#>K^3z1@g!Pl*9iF1SHuleh^g0Z-}f zNf2ABoKg3ev@+Vy2Ph3zLaP+`!(csP-%D~;`rew?f%g21{1JQuW?sN{0?57t43n@c zIWi0+6R-juar}8G{aH0tbm4p8@Nm%acj*eSxyIvQ0CDL+NLN9*&}o?n6R7dZ^8Z37 z-v?U7qAKICZQyl>;>XpZB4C>3iJ_obiE$>x^D@0WyaNV_zZi|Dx8Y7dv2%G9)!h0d z9v=>}>1)>Y>2eLXna0k;JLZsgynYF=JtQvz8>3g^6VEdF0w|X8toIBhaJbC^tSWG$ zUcMl#5xv7<;@#nR_igEPUR6zFLOHh4ZD~tFB(@Q^n~YBl<2DcRsaXrjC-EsjnA(WE zuoF7N0Ey2VgG=@9C+=TCOgxzxMr#0IwMvQbVAjULd;koqnvmc&pD)81y?V-Zn}lb0}200#hi ziBEE+n_M1CPt(O-5(9F@vy>@F(Db>8HxQ9BF{1!RbhkSZXM9)#h6;{l2n?N>Fz3V4 z@TayH-RPW1d=g-XrYnFC9z_c_36aQ&16+b7?ERF9OPbz`^k=6hxaGv>f&XO9@|O5h zI_r!iH~~j_fv{*tvV)G+B~6!%(3WTcxllaU4zDj3O4~QgikbNrq|gfO&x%$fYfl?i zZqdk7g{BiU>(OLy@33TZmH@EJMVXwdHSo%YYHU>GXNoI?^0XN7~@QC;#;rkcMPkL~p#`8z&PPqSH| z`lAB)Bdy{J7^|kbBqCJCKg+l_mkC{U*PW72Ex2tRj)tDShYXhjFH*?lD1c&^a^KB( zBE<$_x#1GRa#K(X*TFlh9QsU{_TK5RalMn6Y-UIODAKjGVb+EN0>_$<7=bNj9KyJT z56$&#m}ui?tS637ev8Q;94f9e{LYMfabPW&X*CuNQN?uy{;&HfGt6dkNm@~Ip{^~f zprVS)Utwu`zGupGOoKKju$()`A+9m5LZl?@_)uJsTm8b8NNWBO?ly&GZ*LE`kTdr0 zPv`b+*buk!s(>8ofgChlKHciHoK?eiWh8(eanSeJr)d-#MQrma3B3n9bj|ZHjLn|a zFcCV9>a0`~BKpBtV9!m!Ro}>Njc!Mx^IcXe$&HmcDlLex(+%TYRu_BxcwG^Y&~*)x zIjdkvmR0ki<1ra+9=Y!B9*Ns^GNRFTyub&&#N_GR>NpMr?2h&Us7_%=d8tWq@X@%6 zR14WXICi<-+SsTZ#`20cKUUtPjW(o8Xyw9?>V1{z7IJ8Q2x0;tAMHi_6=*b^e4lq- zIJOie{pgpn7~~J<)Ak-LOY8SU`oV7cnr`9(1yc&m^; z#K#vg@y6Q-Zabd(*_i()R3l6~UPx6>%DO+p zHeCm>rs9+tTm$PNPIAkoSW5$T7oRu}3n%gM-{b$5_nC4=wJyKm->xuttT3AUHPWTj z@RIG0?%|YvU9v0M!>4rTUAn*L9&wWojbdgaAjaYh@&TX(ZgT!N z>?+Cd5nY>@a`PX!E|gJh0l&+X)x@)RFK3<{Miw;NI5W#OSjUJ%0&MeV@;{_a2Wxh>fUPqJ2%0Y6?mYg6rtjUtBMRfZ#-dLg=g88*xy zbNp#}Q)bF#9&QmdeGN$$_LqQ#)}E*?0ZLHK z=?qysW7~qAEC%>1{cw)jQ>qu}gp|tI60jTH5o_W}&1#)!p?!vgkh<*ju z66s&!UHywyj6htxUS69R5(Zy6cRcei^Do%lXpzP3lbFe#4|;`VNqeJrMsEq{3#{2c zIxk)oowwUFTz{q`1mV60frLPy(M^9 zGsZ!_R80o|8#Yi0V@m&%VwhnjQ(jeLPCn6!P2_^IjMB$K>T`z!8JpF66}#bos9n72KJst($1`iBp7@J1u|Yunft>3EIp@JA ztbxhqE@ZvNRu2*Tc*nN~^4F8Gjs*e#*J zcspEu$K@*M0=m>)Ji=q{Z~cLDMhGkK=@TwzVQ z#S6I4xH|UnH|by9c92(gO5MX?Zf_Kow>8FAg8|lXosQ0d{YEC+{?3OWu7^9v<|p~bFd5X#jYgL$zCWgy8B6ll7!$$Pi4UR;}!IzaU8FnnaG_mPt1QVw< zD|K!tKjHlg|Cl1=8ZlY1|9!!eVj+$EL!Qb0qXv59*x<4;lg^k)#ku#N`1b`>9`slE zAh(HrokE6n^1!CY41Y;VBX5KSS|#3`2ZxoIgF7K#3n$Iz#N%Xvf)cN>BSNQGU`1}_ zRUl{$0==4X7$ zj&3+816^9w--oP=hKDB_1t+B8dO%cV<~CYm;rIZg1LX~H6HI&vjNJ}6-kZe@r1x=E zz0wN=lwM<Odn+?=V(5_cDT$i!M#y2t{oMGU`Hv_5eDKi=Kg!Ddhi(5kiU%fs1CSOeXdg;J zbH_P-te)~Bz*X7<-v1ioL%NY&*>PRZha+79QJlIY)yT-XQbtZa;8TUeaRe<|fz5+= zyg`fB7+h8&6;08l z@7=#|FVfRJ057mOTQQ`$GgPxgP6Q8Tmf=`+fqwj$-ZINW9l%6kS@53<(SP(KEOB`s zj_Li0iP?a$No0t*?JBmIskEA8gkKg;|HP*0wqS;ET!BQ5P~PNYAY9kPAXCA;Liyl< zxaqCDX3D!j#8k$u+;LOoMLy{N7@u_k|G$}E*HRx(8!8-J9Q;`e;cSvi8BZ{wk@v-< z?A(FFY5*E0QWB@F*)ZKlBX5e>6;!+#E23(y2*9>JX~p@JG7j#~&T#-t8wH~Zm~;v- z3Dl@Fs8M+3M&iL-;G8DNg$Q|eV;_kM@hNC67cmliv#|D1WC>(O1uV~rW96{c09Rpz z>0JyI)y8GSXC~r`HF@QTKvo~9i|%lg3*0*Px8}%Y*||9dMY10sioGW$t)%l|(*msN z5fZf~P1gSF%yf;sdZ+&u%#tJJBF*F{1)+Q#tb1u{F#^H%Y=Nz$RnDna>m00BA0^Fa z0i&JxS@BVEM~lNUQ{R)u)uNN&&sX1o>d4UB+DmV*oIJ;(UKMD|GW?#*PqSV9=@ z!Rzp2CU-zA19`1Gn6V$&^33_EUNshCY)x72?|IY46@8&k`Ls+5sG424!CRQmDBO{}i&lgqIG{jbbW>-zY zQA!pNDGmQ4cY<JW=n7s6887I2loU zy$0dUL^3=RKq$f-a_dY?FP3bz(hp>HG^_hd{N!*2`(Fn}g^!xT#+w3hNL1CI7#^(H z;U>@#bUc|PMyQe@AnXyKAIQyF!;P1W$GevujcRW{N^V7a=16f%T~x(p91?_NTczG1 zc>UGtn3FnEM%$dHFiXv0OR9cirkQI3b`H@aN8;`cM`lfK_aVIQ#)RV$Cmm(me+U;B zL^19@^kQ0GUV6SNi?e7D{pty6naN4WI;LyL`xCA7A*{Ld>am?|DkU(W3;YW4=Kp{T zqt(KBfqn_ActUo zp+f4yEyRp>@u`n;>N;^jRz(>~#B})A`U3qs^eQ~X-~K*d10t&wsgw7PB{08FBr}Oa znX9Yj%$1Qg94`dnEXkLU?C7px)<2V2Y6tQ!`t!!?e)@ z=CQ?;i4ltoW3+}xrp2TIYBZT4o|P*wvT~7*o;@0{C;k6ogO~C zVsGv1IG)3&GxO$CQ{e=EzbJ;VFQx*z{I$~iso|xbeN>2s)C1&Ibn;7Eb-X`C4BTl6 zt2u#XxI-kJ=5A?qXY;Ol=<2jw^2HQCruQ&=Jgf8?wjynRJ*k>(Tey`MMhc*4AOOYZ z`ylP)zllqBETOFmAGb8(&jgxPxe4yz36m_IeP29_-Rmq1KR#d#4iLcJKXFo|B{3n~ z0puL)N)hZzEbNMwkD`wZ@=&bc6r9C?^Eg_9`BTzX;`WH`D7#2A4Kbsb{xTRGzv3`B zZ;#-_?dAS*X>UeO~!lHps!j6E%Dh8uuwr6mP6zA)&0hl1a zp4gEoS4GwCX|0hv_L3dNBWIJAXH=6azjATA=zQ?z?5D-6!4Ner7=DN5RZrl)mbQ3rvz4> z+rTeHfjMa?$!^z)2U2@!azNm&WKX^FK=M2gomuZ7(b-Cl4?7RQ_5V=AvJ&u=<}kWl8SB;;n5KVmxe>=_CXWFG%j& z7Ve&(NCJnPjk1~?0)SWmgt1ghl@_GP>IJgkqkm+J5{P=?@(1iHqzT-}fM?0tk@}oF z9fvFT%LgGL9%5J3atCZA4ISgwV`We)jle|e#T}|^d1dn_YMz(Ga_dr2|MMk?h?|E? z8(Q`h@u6c`tarfST@-(NW`kt7$0vTrLP-jBT$MtF<%OQni8a#Myy~Mk(|1dY+8bjl z7D9-qWyUy+)P554P{oeDV;JyCsNwC@?f1Nu%4F?aV*zE zCIebi>hj}*z=%ly{dCV|CLAH2P-_?(fpVm-+>W~D#AYHkxWg(|jaxu9bhz|@4qy&@ z6T1}`Z>nuds6!CX=YNTFK;WwbOK=Da2rJ#L7e7kv#qgY$m7yAIvxvHdkck-JPAuue z_MaLJ{tRS59j1A243s#`ip}$ozqNZ+7HS8~R@+6m`v>1s3 zw4kv7CbTU_v)=>g+f(sQHlzg**TH!24cw-ew}n6rsF%&j=lmk7KoO%B7e6t8^GiqpH=1MI*Bo zNKVYMWPIWg)`BTTLhEhF1F+f5$mB~10tlJF$Qx||YhGBXGP7Df8qSaucQB@wSD89d zm`Np`JBLE&S;oSp%c-GP!BwiirsG)&;p` zdN+hMg`CNTMCapgvOd4Q=#Zu1Bd@a*4OO-A zNfit)XqHt)oj6gUl2=xDh%f=`ZUQ>tgYDT#ZuL2x(S>5nH8FdLKaZBU5?t&%`7%W;`#QXNSZQ9YOng~j#)clYhYBuSjAti&@HkYl-xtBGf;VF0Ee z))H$Bwvb&(a_YKG?pxi#tbsCCG9<7hh0rb*xa#Es=I*rkXjER z9F$kI@=ay2WQ`CCY641-1<}OM5>&yYJl{+@H^P-zN%3X}?9m9(4%5|7xIa1eME+0H z-y0tS@f8E`0pa=w!95kVifl=KH95o=PiuFH0;-q0LER2M!`TeB8O&$xhJ!QnrX2IS zY9Z!v&245n!KJ07n)F#UEqHo7AMgi8xQY*6!(>QWK|uV6DbV#`&jL7`>%*u{g9>t> z1u>2Xw{ZpJ%nBwpk$}glS%qrseP%d|prDR%U5^*Xra9Q@Gq$tmN-Xv~;96Z7UE2ThvGd863Z!d`pT@DfbC zv6K%+)B`?fG*Alw8S?mi4jps76|){)y|=l|;dq))$wtQwUc0vXrTEVG(Z9Ph0Xabn z#ItSe?cYrA!XW7qH2z_`W(4at#zce`z5^uLf|z3fY)&L}_RYZ0_aOG<5OK<2MCQSI zx?@xj+>Y~Mdh1?=86bM4rGOP z9F*WocZYff6b62(H@xr3=bd7!T6T4S2LQo~CFRb)W98l=Rpeeg&Dfm)_B5>$%G|iQ zMnP7sCAS`~POD65N?xlnR9kk2g!q;1Jgqn6`kD?(NTE7$yA0My`wQ@`>Pfx1W(^q( z=;27<(=(CvjKR%DYU?$Wag`|DU;w#BI2Pe9bTMGjG(G;zv_k5g#{Q7S`z*T2twCO# zvNIADLQWkNW=`74#8$G8<1ZjrMTOoXMlEO6LDp9f5*u`K1fDA^Ib+{GKE#>%?aM0# z5W0CFzpT}PPka$?805}qt#td;fjmb8;kv81I&wMU4bA&uhkg9$(0U`K5CD(RHc8bn z@Fwtg1*kzPfeDuu?B$~nC&8?s7O;;SLtd*>NEJ`z8wI6pb&1skIpJ3e2ywcDJ#^x>lgxQKJ;ZaHwf=AV%5PEYnsHfsH2jPs!7hegd?x&6yAu?NKO~{ z=It7wLM^mYuvX__us(r7lJ1pMH+RGy1x8&I6O2Luf8++1)u<8|49}cSC{M4*K9U0m z$nCGvJe*~wsh}XEyyeXiF2zUd(`_ z19$ZU>}VE^ruq*W@-rPzsMHu$S*Z7K0I#4?6}o1@CJf5QPV63dL~sFk7u6@J#W|)F z$*j%OUN1n3EU^hbrYpCyxhAmzzDFxof32)WXf#n`sDmD_JtarSqF8eqHULFp{ZN`P zl|7ZId@&pZxMF`&py(taJ|oUCNlw|F78{M)Ct=kD)(SLzGy?rJkCM>$lH|(teMEz5 zR;BC-3UrV|^okYg{N&${KeEN0Sf=(03rba|1$V6Vfz!91vv2&pHqfXL`^R#|(!&O(pcRgb(>QZt?tkbRw;vFhda2a|j;O?<4Uip#n3GGz)wdHu)`xePKwXenc2Bhl@ z*p(~AGsY+Ej`B<|dE*-@tS)1vx7jX%8-UIK`3xJ&HJ}B>?@+Z@Le*aFnn_$2j(BA& zpA*BB;d~H9*ra&1)tKhsV{Oa?dtsc%dKRi7w9Xo7!Lbidl85p6n(@(aq@TmJ+*W2{ z-{)*?P4CF$Q$WqB50tls9cWA2ouDnb{U=C{u*37%9)N1tMk`vPQ?x|9|E)e< zpV3l*%U{PWlbHgHgwPQ&%(R8%x34%c{(LUmdfffWRflTS(KWAJvuAsNE>!c{yKiiT z5}ELT3uy|(=_%|_+$Hx$FcF0dz^p*Wqzr3THDJWcL;IDRo|~4dQ>8J6m3U;kerh8F zGgI2G3IU}XOu0$*yoUYU+6M0#GLPy6Wm7~QA2uG>J|;tu-g=JTFIHifN6zn4Nb#r(tjX<^s9P zy#%uK=;b_ok7N%%q9wSWx z?6D8}I0V3aBcQ#5%d`emZP*YH?xmA&{K`zGUU(8dcsR>7AVLD3G4pTQBkpIisWV8B zTMD;sq}d&5L1PHP7*sk2Ih1-P|2|b(-M$NOSNK8rhZ^pQajYi30ic>5jcV6)`xqaJ z3BW@{;NKg0<&D(|RV4F4cyNL;5=cEzv7~sW5vCj}9Aqjrw?ay`x&>3YKtxm&w=y#M zpOP2`t$k5WJ{4I>*L)%1^d$kObiKHoGa|FPLctZOdOWRK!hcJWe>?Nu^}jh3Q^sj1 zR3aF>sxa>g&GYsNP9Wn86}rZj*x|BbT$frZajE9Ip=4=VYFM9duWznzg58E%1QV}kP#hgrh{NWj4@hcVoONpXK-38Xo zaKup2QHd8yqu_VsGwC7ue()YE(h|Z0kfs`R9otDv7b;0JOR>at!0x7j-ED}kdKzWK%WE5ssrQsk5Hd~>j|@E&h!8XyNA2nyS#BP}P9 zyB}WAuf2e%LUt)vrpsrN>!4DGBUz32C-n1&MP%?Vn(9M2~!6bTiTBW z?(*4=BcOYrexd>$_^WkLOkJWS&^mz+Npz0dC(-Zeu zPM^E~jWg0pknrl{*4VOD=m5jS25hLooj`zQ3G&8hVw<^h4xRu0>CEsgAC!$c)J&#K z!UnEpX^Hz26CC6(*vdO(@n0r`&$Z6%)9Xv&@*#LJHsKG}1DNR=@4pKM3Ke)BV0^ht z49nS{o~_@Xk)ClNGd=x)ELFF8JIV z_>U$MtIlUlrabmJrYyP4IVcp7!^dF5-DBKw4=%!O4&#zY)Sg1cUE9^Az~iN1vybM}^mr?Z0;o0s4@W`C3u;i(4Jo8OAj1qMOy<(1?n~_4hZ{BBmF#p6 zJhL($-W7D9nu%Mh%Bx`D4eapotO}$}KvUG>(n=XDzgU9;f-=g-uA$6?mfL-f$F`Bk zsUjz22YdS8+)M8^s+i_ynigH8fC3yKnV9TjJJ1?S8gDH&K@ybE2CsJDcK>M zPx=f65rA|$_P%0;YKd0dgNgz;$4JlJg+{BdZWX-Dt9bSm(59i3aI6@v=A>ccUpKzS zewF_R(Wy;!ltQD53b?kUMxH|kV-oHTRO^5 z^T~@a%bIiEl^ZsAN5EP`nfP48FEu~O-FED-Z0=(u-T{h)eg%wLIRZbT5#anl0(OPv zdwPb&!*FZ)@UJlViF^>Sv9=}nWNzJ23%R(s-~*lTBCwrcnhK54IG`VH1*negzn&E0 zV)93DO_PbsM=|}5z?7UU)ESl zuB}m>9c@QQ1?L*Q6GfV_l<6V9jM^>Xzpo|l2?}!9RuuX-;c%C`=$8aBXee zk+cpAImEYQb7Kr(@N9E$Eq9VlqL{->l7eU?wbC)QZwDw_An8SM{uo{`hS$X{oEfI? zv*HF!E)kyo0z5W?)5gY*Sjyu7H4MB%xn10&gIGtBi$LLp zbw=lh`&&nys=NJMHbsUkIx8B)B6H+f|T-bw{{$=1B@ zL5No^n{!!-R~lp67UEL&gb|YBQg_EX$Tbx?<%jihXH|1^4Y2ka@x4|qugJ+Q)nBMS z)7pCC{ryXd!kt$7^hJx zv5H#J(WaXyqd+dy#F*&~ZV^l+sWa&QFIJ+Zj#fN-NZ4Ne?8fJ=qW|lE?Bx=iOI31t zVyW%~x29-R-@vZr?2ghWYxvm7_+7JxDY+~Z?6PP^k_o%TPR-{T7m5p@_+0KYJ|E7D zk2}O%6NqHsBj+HHJNLz{&t3m`hfn_oYj6prhijP$UemaZYwhu)qc1cx{X|LdqD`g> z`dOiz!)&?$()_^W11bAclK;!Esa2tVh0Ec?6+sFV%1Nmt1J>xKJ=;Ba5$p_)Nw0?!?ddYPN89B3J zk$fC*0_lflqaJrQsfYN?&k3{W=duPEuLHmfC;&-ha%X#KSB4%!IxM{&@-#mD5=s~$ z>P2}R*VL;8LeibWqs<+0m5cSJ6Vl>1l${lZ-GS|-T*?8!0QbY@*hL6 zbtb8f5BiD``Ho%_hDp?GVlB5A#jaBA(uzGHp#j;@RvWe>-Cfa8okE&*v4yE(C(;S( z6!cP|K2tbrcS}n;n|4)#)THVc1l-7J!Ssq|uK5P<;PNMokKD!Nvq#gS^r@Oi-~A}K z%9URC-Cqt%wReA1S{OjZYL?S>KsGBAmi8$VjDCH6B&d-}kH7H6g!Oy#zje`qY1G2# z(%{0~jiFUh?P*6E3oG|Gg;dBEVh#ivV1)iUa0U~er4CswVjPVfOmP?3D)<}qG-$@^ z&c|+<$Mpm7$B*+$nrjnk&}0e!Eo!bysK9?w9|qLx8@6YiUJM+otj-B6FZ*NQ8iodJtih2Yf1eVG+YB)ttKB| zlR&rV#n@mIk ze6lQc9L%YxbwDMWEPa%^y={MYyNa*hHcQGNCtMk#@vl!((0O32ZA_aEJD>!Moxo~9( ztXO{bcQKSfGOE_^+2lR}qmW-r#UW$W;t;ifkwpBs)@S+EsGlMMs^+#gTv#B*!w^U; z7NfjLU_dP-h4YeE*dl7q2FiS%8fIL{D|wGV9G1kv0V9CO%8oP>ayZ580$Z5aYb%*D zn|hH5P|}0Sj1?g4{DP=ERA|oIhu-RprzjBFko}~4S5v^sKLKKCmtcx$mw-``aFgKt zsB<6~dX6%&6tn9Z?-#JWAmj!i83z|aGRTn*05;MFAKG~uU;<=2Vn~G0ztgcZ3s7<3 z`VwBGcBfs{L?k~RyekCCHdL2V@0rSWwG z|MZxM)JOm~VBYFqDt+gnd z_0Y*`Qz2`pRf0dk{Jp4lSoK0?EFEZk&;2}>UVA8Xp*>@yeZca(QTzx#MdV7;xx2IxDHWW|a=H0u8 z2zQ_g@zicIX!DXs6#~K~N4OkRSLYeAhT1(rBWGs&!PIE*f@0H1Ja2jqAfKcBL5e?= zm{8l=9uwnGtbRz0DnX|pEb1r#53LySwK9S*Cvxgpr_HKES=~!As+8Bi019*vB#SsD zPAOurtz42^oQV2L7iAjgA%bs)C3fZ(qg2m)p77%==$&Zu^v{L*SNoa4d65QmRWUX7 z6*^DF5%yoL?WoB+4F2nQO%YJCUMwVSqrH-heD^5OY*Xc>Nz`Akx#IBYgJ`w$4La@k zQqeFVW(hua+OGnOOt}k`FAr7q#Lu5YP6nfzOn3i4xW10tnmZN}QWwY#A5Civs6+t} z^}6;!=V9_LLenBshd&JN*>?0w5~CXF^Mu;?G@@g~HFKqnc$P|Sdd6@IiagOCjvAxk z5E`rS>x)*%LkdR|pbj6n1vT>TbJoL5<@jxHUw6?hAg`Lld95;n&u(Ow`Aj#wciarn zCN|9M^$iNx^lqdM27o(nnoFzGFZW5!_n^In+vO^0cVQR#`8FNNYv@&2gc;qTxYd}e?61L`PY%?CV!XmB|M)BGswt-<|gD2!Y8 z;xX0`WcVHW<)aTot+_{#SVTBi0nX*l9|FQHlnab2m!}G|1@-h2yA9R_z?0r76cwb2xb(z8dMnPu4oh+G>39PCn zZ~2HR2xh_R$;xvW;!6t|5}I1&`dIe!w-cxx(Sn!07|-N<Vy$LoPHxZ`_l%B_9n zsb5(XbGcM~ehpDlR8NYw?21)~s;Y1fMooq6-1#Na>zFh#zU!hGhpU ze86I6eFF3ix1G=2Fo|hI$8F+6W?&Vtq3^0d@r=-E)M^~K!)$H`@1<3~&@5|!IEebc ziZ3XU4OSgF!=v1OC+jP?dk3|dfDCP|#aFWRT)&CQYQq=wMv((CAd`_1^Hk6)m{<(M zP;`#lcED3<`BadOeC!Qo{R{7Z7pxfwK?D9`9mhy!JFna>ghA6_d{*2Ym zW5Ea}xZ`>(FdsCQQNuPp@U9_~Lc4n;?8k1JLc2+;y=ZXWfUQ0vk$r|HiH4=rtUxwb z8lp`pfKmfMDYm}Szq^9T>Qob>4mZjHy#AMyS@Og!R}4vpc*`ZM&8Q@1;w zbv6u>Lj||>!xA8V2m()hKt&0V5C^LSHDu!0#9n#mkjUjb7%B1ML%w5v)w@pVxif;U zs^p3U#E2P%l~oS)u@%66ZDO(Jubv=$>LF$XK4^IGU4c+6hS3g^dPfSlwgk9aj z4AT*1?F2ylfhngaqT47~Bpup0W^ZpxXgg4qQJt4t1#5!w`TaFMQHSD)&0w0m{lIA0 zW}P~B-{HR3dkUvDojjA3Ruwl>&8k);ybax`X|ACIy+(@TV)QAR*y#B6I>jH*>=Q;b z3rayC;@eC$_SUbXoGk_lhMk}3)f8D7c%kA&`t``%VPo6<*_iClhuFJsfBHUsdt&He zUjTsB$-Be+`p7P9-xkH4g*;ya03C_doqbu#ODs$(P6YugqviraYgjQpe?0b(U^V}q z_H?2c75}|$6EQ<5i|e;S49y00^8tjlCNTOhW-$3hRlV%yp$iDi-B=%46$R~%{umv7 z(Jzr(IItrdazLlSRyNCNiRs92Ecs~t!&IE}7r@Szp(Ak>#YQK^ITZ85_<<$uP+3xL zwUJpnbUQ=Nj{H5O);Fbf2U}tQy7&U(felBM7|x!-LBMPG_$0*Z(yi@>-QM-y4H$?M z{Vh0(Tfv97Z426iI6!RM&h4+^>8y4$YZb(BO#_~@vZ%TdO-DltcQ=Gq?rKXr(txsT zI33D>Iemg4lp3V^D+3Y!nCpxD3Tb)X;?gR#!O*C=QL#qV>1+Zldubwu2B_bTo*gEH zwNs^&DPej3j>Le`3-jNgVdfoas7|N^hSr?%EBEjWqUc0RXAmODn$|iMB|3mGc$)|V z>Y2^0A{Z)>#-sN+pMMCq_$dEK1A>N;n(Fv^uT#(5? zEgU6yQ_UMXc?X|>5LpaM7fwlB!U$CW>sRcHhmt~)1Jc8EMnr!2>WhzbC}I0ZE$Hq@ z?aV*)Ue1M(>J5;~aO>Y;r`$TXWu;uSH~{<3#JX1ixTtQ{YY z^!|EGu+tRuj6)=-N}bSJj5_xVyLDplQ6RjFVhUmJ0muHJRP1j%BvT2V)f#w4hn8*; zyWRKcmr3$pIsgbPbz(2FqJ<$xlc`A!r4mr7^WWkMRiLA|;{KDW$pZxiL77g?dZ2Kp zUxV>$8KP#84=V2j&+BV$T^;2~_)PAgDtnx!$tn%r*G>gzW5NZ%&eM{u7}5dvIQzc z>Bo6u@ypPt?N8aPTKUX}Q>}4mVhpI^;%dx&bu=mi-|UDfPr70uhqcDHrk}$2>>w}& zi#h@L5gMHdAf&xPNRwy^_24i#s8Yq5niPkbun1#r{uGLC9NX6>T^ZHag2*s74nRZ= zDTZAjDUh=meii)k?18P)l@)fkP7 zGe~ZUplsil79gP-vjaAedK{bk1W=HEE3XCI6Lnk^GaJRsHTXbKV@fSbYt)iQoweG| z##-kVusevP&NQCEpCWgQ5XbTi5+y`;XAs@pDVBL?4u2@0>euy7awrxlQuOZ;WeFgDYPOu4V|LoVB+MO91opf~(OHNzEG5vJb+vjzj2* zKwjP1QdvszzI>f*5noiCR)l(e}q26xgYKl0%S;#Jee#Z$|9k(P_re zfo21gSLp}Qh0>8q9I{a2AS|84s2usqKi)u*hFPwQQU+?Kgm~2#5y;k{Snp~#QV#@2 z$a+^85gD~^VS1Uk5Uj_uiW>x%@fisR_G0by$}wqClALkG0&!(|!EUw6;RXEPL8Z%N z50F4`2`9JS_?CoWPPPvq6BCoEdRy8Uy(2*OD+;=YrqbW8&{!+8m5tcKMai(?Apr_q>c+cEz8w|HHeqQ<<_Bjvx?G$@lo(b}?kUNKlVz7aXmO zWGcm4l^BHQ=7W;auZ#BwRJ+fIKV&2aEt8hThWG{+1>M#g%B0sbAv$%G>%=kb=-yh; z7`<@fRqp|`6Zy}lgQ4g8i_lkfv%AvX9bu8ncXKPme|+j5#^>e`?kjR)H!dWwx355^ z#>Wi@a^iOFXgP&qIK{d9G&A)`e8fh2rx#Sv-y*r}#m8o%M;&xQABbfLE&0HILTKua zt2#VjN;WY4%YU|89TLJkymnzVOso6YF+TiNcG<9cJ%|Ivg7Gl}X^yP!GL@1=f?IEm zYk?#(4MM^~Y=`P$D5)OgUsI&`{X3EabcyS(#hIREMiA6WRi`L}d#qu~V{QRyn!Sla zikX@CwxKp=VR{Rb`v*Wx5x1{qWvS(1%6u%MHw(!P{S?}|g{&t`&P&cl z>x2}|zT~6?w3kP#-C)Nq@iu>HC{}VIGt9#`ftcoDYvQ|{jzU0v4|kgID?6N$nYf$V zAaH~nW84!ad*=szsH;RNQ56$sN3$Smz_m8fFI+zo9X62XpU@_TNOHDAGq+yBd|+i& z>9nx^p!p)#A7Kaf3xos-#(8Pf`|uNCOOYyq#01QNs$-~Dbqwb0&&BF9ZLKFyZM;O; zpIyhcRBf!@R2GAVwI}eA@#k6N_Rh_GoFfaXbgP$p4zSW!(Nxiv)-oOHiwVQoR^d>^ z2d!i-I8Y(zL}Om@FZr;U*a0rs?yiP8TCSbU_~4;Xevd=)Ihw&Ttfa2zaNWhoAtCzf zMhIe>$V|@6Ow6MX)*_u!ipdJ~_&zdlg&hqx9`X?vAK5U85g~ zx1!Zrt{BQk#b9CQGs8Hl{NZ+E*`~b6s)S058{F9h<@)WOpYi$&+6T;3-@!*;#DrGj zeU?qE9QpwIu*jvNd&dWiSRy?ic7t}%0a9X{szmvDRXFWfl_RZNPlR(2>C<27nEb`Ma87B$5;_`FTfobibtvpX10 zg&M|Gs9_8WlZJSpZrNx=$I*!m*(Au{>h!|;O6cy>YEoJS9M(<~Waq0(Kz(>2Yic?{YKIO8hveA%ZM20J; z&d|&!Ad=Q9h3Xgl6!Ib1?y!S)c})Icc{eNv=+o!e%Q0vuZ;g}%&F~gJx}2eki_73r z<_ZgN1b=oF*Ojmq8N(`rgG+H~UYQd>!SNJw=0 zcD?dwl!G9{7f&HL6s#OqHtkX0u`xkSQC#X9nhk=7zqx`0fqf zzZ;^QRg91~Kp_m9fq8|-Y(yQQR&gZL|E&qSj{{#4Uo#?-3HRP)>_!de2)y~I`6!qx z!^meqvhgy;G> z`iCZNNY=lL-$HZ!K@L;VKMA2d%tC0`?#s;kaO72t0KI7*;-W7c!<3d3mz3t0m^EQ* zSm6a)R;8AfwKz$Ni{BmVt9v7?ZTmS#wXlm%^;6qi_KZhiwcQL{WY&+&_#H;Gxd&9{ zZ@vY1P-q5pjbLb(OP zWaL@3XNoFnp#O(REnM0h6XhRLyyFZu8%e18^+~9dQ>Ds#Y?pFcvt?%(Ae~RQ2VsNQFDXgKfp_6({x(V1HlwzC?Tlk-bug4|(V9;zJZxiV&j; zFt9G_9s~spn3$!Zc;}pmgu#+ zr)L*7rd_9a6HR$>sIg{5d!v$W&{bgM4-tCILfeH9kp1D*hKw^5f>OIhZ)z;WElqe7 z-7RdZ)3dnkH~593z`f{)nZUZ?z#(LwME_d0(%E0&$&`_+T$SN!uhmSihA<0C`^*xf zLMiie6A2RT{Fp5d@_P4O9RyS_^FYvgJC?V84Yuz8L-}HGFOxP1i{f{@BW~VF`Tr9VX}a)W=n( zm8Ul5lpd7##V5x^c8_NfXe*GNSC(1l2z^#vS!!|~u4D9v-Kp)%3v>27)T&nHdB z<1dU2-R4soh{xBc9n|+L&m6EYm(U6A1u?|*6f{>x>4B017V39jaHn4}^mq82Cu}CF zfoj^+YR?{j^w<6~XGp!1>6?p?ct=ih8GV1Csq!~4p2Pt>Y<}o3bc_F0`x6#%DiTaB z!YWz}UG)$xSq92RS8g#zWuu?{lZ{4qL+OLmVDqlxgKjd;wo}7+zmU*GU&O;r)gT8x z5krE7rEP_PV}dm?ILL;-v9vt2wLguSLyf4)NhpJ4^5M6DdJx&qkUmTcMy(|_tI*W` zg2siNzuKM?&!bmp#pW!XK$1<^oE8ID@cALQ2=ts5wU@F^UUrZZRf+wTUN|M;QYV~( zdz{pn+=<4%5!BeXD*(E1|2+t=;c=S!B5!1LikUpyUH5LpKA$7o5LnW7I9y&_r)~$Z zKS4sbfa>Ov9^NEItRo_b!bL5%6|#t3I*};JW;tBUO}zm~#^A+p5ddN^_G=M4@^QTf zm`b4N|0S)=sNLO(yc?8|{j<5F{1H+8@!q!3$b+qNC?xbJ` zBAJ3tQghi=3f{~G#nd@nb35peuU9ywXwwf-x#|^^==OygnD_`*3U2EpNuuS4m;O`t zp!NrIj`o~%v?MdDKrfs7DAIG7hxH+@->0qb#e+}?>W43=KyS8*zaIMmv-=+U{@RLB z_b8%7TGZu{0S@KidyglJQAt0WkF(7O=soCYJc1=y6c_Ag#((&4kiS_^i_WlpC+rjWGrW5g(^+im6M zN5?+L1C5dANtaXn(Pio`_PY(5v<56KUjJY zXc!TtvQUC)3=SrAf$^xN+ZZbZbl?EaL7|RsuKmRD31C#A@iE(@NgV^e(dh*kfv&(m zJMclJQIDpi;0VF;xS0Rri2p#FXcm)egTfDFMWJX;r_^Fb%wiiIUJv}EZz!iT_j|$pL`w#FCZG9#&=f7qPS4z7dvXvk$0lETXIo>>Cbr`BO1G>Yq>v-1NC&$wDfHn7RsrSPP(z^+}56~cn z=#le~Dhl}+zN3?bJ@~#_9J}>W)VT^3q=mik*#S3Ei(;NQ^B zlgO_%La3GUQh7>IQUPEDsu=%8)!;*^x%YT%Mo5l{azgVxticK4>V}6nDO|VW$3@|a z6q5Ew!*k{;Lh10jg{=QtC-wK;R zrJ4BQnH5At8kKC&?F>ukcAlj~H)SN_n-Bmy7t^;Tpy$wmlMv6N%|PyZbO7-M2e`8H zDo|lwSzfpXrO|Zq#|LY1b^7Kalxaj|#vY8vvYv_w%ntJG+&Gx>^4aV!J~!Xjdtd5 zd|Dpg7`08VG58v8DW|Nl3Qx{|GmQPh1bY5t4huL3p5%z4+8lF!Zvg z5MHn4j$0TC4=92c^aZA|0e+$W0et2F=HC!d9ot~hpmyuzWA`Hua{$J~Vh79Cq!tvX zmGLpR8GSE^`lT@_v2s$I?HmLyrmy@$u+v;qQA1s%eJi{Q;yWx3{kMzsAt`z1uBi1v z)J6K=CoW#ey>0Q8l;9P#+c$#Gr>LF3&R)!4T;qApLX75PA3#qfdA_wE09~ zbIVGt+^x(^%>@xfSrj)EP(V>2H(Xd1ltpC`a5qzP$x^dT%l5i;Z@2B*UT5$F{Ql4L z8SLKg|Iy7(%52= z+G)%NQig~=d-~-ry6OO7a3|xS)(!&Ls9Ca$Gq?(*Fc&@DH@0u0Z1Mm<(HMk4r`7Ct zoB&$E4Qa9!cPnI*qBL&$OEBjDn;%?DCG-Bs)HYCGg@UXud8kTT&Wd;i#}ik=a#%IzENcMm2ur3(R2sO)zI$HU)ry zpO@SxJ^K;{c1yk{Xg@_N&ieXwy59svA?~fOc-y-BZP2E@8UZhJM!ttiNk8DOjcrS_D&jP10 zlZziHTiQg;qhwnVi2XtOaDGC<^=G;ro0|UV!tuT`Yrf$_6GUg(NKYSU1_0!*c>tGC zCBhS$5v_|$-8g+)bOzN^4mm1jnNM&klSAJS^R~XIlZ#SP2JP0E2b~S!kvEjK)&i%% zQRb0ak~PwQ703FoG~9Te99AM`TcYj^fj;RBIS?xU2cgl*gV4~2?%8rMb7{UDu~t}I z0dg6y$!?)yTD`@g0qQT{o}FW!Cc9MkA+4e_W2vE8D=sEq-E`ox7$X>u80UQ&`50sqjAr|@aPRQlU>i)UfHyK*Ajp0S%7{Py(^sM7=8Cj^xYS_ z?^wWN**jLaz0ICZW5Jf(=nwphXNTFITD?3fB_L&M>b8`vvgMz`D3$}pi4OHj?K%ZN zaEdUdo~Q!L>kS{v?1RP;3Zt^oc{*<|U>f%u3>^Zv`zphLb!!60>f;f44}!{H@jwk7V#@iRJmiRVdVIYGiU zW4Z>4#UZFq3!?4v0pd2z#l*JWJ2v}hwUVqdwGlLpt zAEQTO{}|3w8gMLSJMk^K29AR@v7Few^}qU_66^*Qc_sDaJ@1#rQHxdk8^p15fTVW%G2QzLS>ww^?c> zNK18sRN7hy^HMD$?$V1#yG0l=w7YwCn^YuI)V8|IB8(D)^r{uLC&d_(m8Xbp00-Wr zlP8bu24<7mo2Ofnfi+1Nn~ijJ((9^D^!FsuZvc>YcW)&dT@VDPhe!>zeF#lm+5D&b z+o(1y)74O2e++OV(lIY&?Nel%cTwESPD|Mxusd~oa)MvN=8Z`yu9T$sG@`mQYAA@8 zEBp{cWi6p~-#7{aim~Vj` zbwm@Oo>(G>G$uW(=G~Vyu~d5>?L;aeO`t~^%#ln6^( zJQS>;Z1Xg`QVG||3%wK3CmX?y10!{#hr z2YIP&ddTzL(YsmS_s*&jYQUJ+&Aw*eF;g%fkj;@cEs-rL_vlq)epJllR2^JO7sr3q z)*{!pEh66jPSkW}x74Bp7X%p++xzK&uJIIB*FQnDtCq-B_iFDJ@YUyHBV zEH{XgXT^*5d!#^3GRG77bDtDrs5o)-KzV6#r5svBOCC$f-5lz-Fm`hY^+}c7ietbc zjR3xByPpE@NeW!^JbjQZn9Qx zd1|t%+}orVcG^F3F$2IPJ=cx!MQcH~PfgLUakme@0UWs_N=Mu!r%=fr}gYoDhUE(0fM?X0IGLA@4O*TY-gN%9(&p-KfAP|t_o1lW$=%D0%mEO7-MxzJ!dnOQAu(S@Y8;dk6x7E&}aM7I5cyJQWq$}F5&syukD^dCHU zEfi4d(|)zu4IS9M<%XiCLCa%&(nBU0Do2osI9aH3(rYY3AeW^B(1Rq&Ur4K=t}Fw2 z2OnRcm;E(Ik5=rv=8_#qY0D;;v2V9cTBqffzmr4%g9&g$OX68Mo>5+vv1zM{L^0gb59(uri!y)7D>F*zb&!F=?Iy3vn>EDA@$MM>Y{H#8GiKDovx8I_4 z=Fmf#$itx&D{b`Y=)tb7-r8OIifr0h&+iAWtB$ch4EpLYxFLE)Gb|yS7|`_)jt`E1 z2Ds4&+2s`{;twO3hmWd_YTU$#sKFts5fTqkG!nUSowp@J;AJ+$jN{KT&dE12a>}7+ zK=CaZ?=r@SSzMPK9>LEqsxMzC-xrkMcI6wS{J?wkgr9H;CFE8|Ko-gXh6@L4`)SU0 zHnI#*Wm{Pav9^&3OO}M}TKW+O$;aa(76oT7W3jVMSREc_Z4@-TkL+zMi#O+lpudBHfc$tdlLUu!_6k%6FV6W7=6K)+VWT zvvgZu2A1M9UjrWyj6%tNIN)KXlMy-;z~gr;Aq~t1vMmwPqw8^5&eSf;8#kswkpSv$ z;k_0eH`=`ydi-xj_0~s?NN6`BBA$s|JAN1fLWyhRrUF0D<&{Pu!HEIJyV_niVJy{v*jLELvO-Oxs%zOzVdbPorsxFifQHA|0L2DSX3kryUXFXI^+|HY9+g4KJl` z2-s#fic{9)J?jrK`g`e1prKcqk#*^UnnQDqK18#Q{<>(%BAzJNcXNw|IBEkSvNOKq zeqLr!LYdN%!}U%wrGFTRDJ=+%!kvjr?x#9#(i{lY>=WgiYzZ#~MXQc*hz02ES(?DX`1i3&yrLSFcqk0EJ?v<^Tn43& z5G_gahf1~_i)^!(X*K_PF~kP2a@;7Aka!AOUM~)Usl8rXw%AJYx9`dhFb?Jy<}I&S zS`Z6whDvN~8Xwn}f(0}eg7r6YkAG;IhKzc zDATtg?_NZ-8o6V|b(ecBO^kw=dJ0AUH*90K6s;jue#7>x2b}st0RUOe%+mSOW~C&*Ath-(@_pGihrcr*8d3{UwWI|MS0toV;_SQyL#`etT6!3E{ll{!Zsq^fj}-cb&SJYi9ZlhW8T z`;M7o_CO$f!9oh+7laZ;1mf20Lzy%_!M;FmAugsN8gL!Zsl7WwFH* zxSQ_vC`{YMYZFtWHoxTr>@{-1FyA&mUb5aGpGD z^~_gl)F8&`k7`s;B#>8aOhj)D0|<(h2z^#+**$fk6-px3mloTNE2-i8V4JD-gNsaV z3-_ic$n7RshFT%pm+9Q@L`%_O@tSqfdlwoJb9YAVi!qjkWy~T;orLA>az79aI$6L# z{*Xb3)Z1J@iJq})WrCIN%t|^#^|bJoz%HBVEAPj+v2I86wz#X6{0XqMaiHYhFiE?| zZC$@Dk*X6YS`3ym(y}(L@gv2)_h{*QrWSN9_L50S%a>b`Sb!0{FOn@~QGWD!mk}O| zV7r0%Gh#zk$t9z?k#^=HSY8k51H(7u`)W2Fb*IQi9A#3=u{mkV$=a3M461eOZNnh7odS(R=LoB)e|)5TC#GFd47fbLs~) z(zC>c|MFqHEj~=$bkuD)6J?<_CB01%T9X0{hp?bJONBXMCEV#v96g=MD3?{;7`oJRs^O)dMQvje0N2fi6Sa*g|0D+< zBM+ zX_}AhM{{9>=C8?1Dsmi_O*)BkThi*7)pO%y$Kh~wWV3Xdy}hRMYssS-vim_U0Lj1e z^^$+{EXhClP!~}{zDVoS1>8^_$byU{-RmzLjI1|m+VNWtwF7ENoL)`BK&O$LvP^YF zL+G;**?Iy2$%9A^PTP z-t)}k>eRXkqUM*HZMG|B|W2hrn@=*n}9*>Xf6e96M<5$;fmWGA+0I9fMq4Cxq%l3`a+9B zLbXiwwabBRdF2s5a^TfG=sp1Clk|FI*geueL#Wuar~A8+`$!8q%d*KF z5+-km;6Cd|Xs$2_QZkG+3xja%jF=B3HRM1)Ss5TUDq{&6cXAI z6D{wViCR5^FhCB_8NUAh;$zLS8zJRMeM=~SX(h1x8e69d@9PxhN&@$kr_3|C zzRcqemd;>cG#FD|J8xF#BvNC!Ap@yY+}e_p zUuI!tDcHw^EXi5@PFld~pV@uZLFNt6MCJkS8$D2xYz{qN43ufL{(;pvRu~8WF^kp#PvzBLlu9_khy9)osST z6?l=0n`cFcz4}pMj8^lpio17COnB^){ay=ZHu;))94oq^k|%v)g+=KJFh|Lzv_ah% zT8CV^bI(h=($ih)JzIM6)(wf(hHlh@y73RbC9pNkc`rZ}aEk@@Co46Z2Vs*on5x#a zA?NEoM~pwS;z;txj$-L#{yNL{?f~2R2iVq4OTyP~0^9lr*w!7TIzvY5M6(L3q2j;F zp+>c@7-~jq*DlK&q7PC;dQ)r+;UU*TvOCSA?#>tu$|g_ndY7T~-V7$06OBHTBM$SG zBW2g~dOX@RNpe34NlWaHm90t*#CLG5NTlt0HbxGYUIMfamBXr0Xw?#3q{z0VMXZmQ zyhK|rd9*xLgICKxNx!YR&KJ8<*!fYU_S?z{SVO?;VuWP?bibr=EUyHtB%(Oa;o)g> zzbniW)!&Kh#z%m{9pW=%v;DD@5ZP*~D0zLh?5_u;V~p5+cKbid{f=t+b=mfWSh*Oc zE~0vVaZam(_IF|DxOGr&w^f{Bp&RrXYbO9|-O<@}G$i1Cmb}qEbmP@TRQ|LIb1UA7 z--adKV7Ah5Ll5cJY;U-mR<}$L+Bj7q>dYD0L&BL7t}o7ctb&2Tx$*$>YspOWN*2?t!pCrlTx!;JQY+@b8+Ra|5C zb$&!_*g~g2XbB|28^s3d!a2WkCgkJvWB0eSRkMuLJ)3t2Xf^g%DBD>qHU`E@QJz@- zKWuk76yUqJVJ}%%KG8)nYgP=hWO=L32B-|8D6|qw_*qLF3K;rKmXb@h#97N@C(fS} znG{ukeA_|1NG2>{7eXb2B|$ zOyk2L=c8m#mpG7>UIcolVpb-{=!Vn~N01~S6F5bu@ih1Tj^Vs}twK3I_fw6KJw#;` zasgEosi%L_XN6^ei@~K9;@qKyIJazlDQ!!@_Oxwj+fuirwK4n(f4-N?^|V%^%x1hM z8{O55e&bQ%91H%VAimF#;2$K^H=6Sr6{YLwiQ4&uY)m5^tUaYSBP-xoVbPnMk-2Db zxCh(b&n-S&5qkhmC5~%E>f>Hb%)91yA|)zkiGNr`TyP5a>S6_pwq4ITP!(V>VYIj} z*|^b`D}?lU7S#sByam2N>;3p`wZFU|JtSm7@{6d!hi{6O_FFXUWMW_U_pT(Ec=@?tD~XYoY`Gacra=I>;TLYW+$y$8t@=2WW&rUCnSZ)(7Ph$W?TyejWa*KZyrCE5P02wfCA14i#**1=T?ta0 zVI0_n{Y$og#UsOq9d>Uq4#jA>QB9+ap<8P|Z~YRqdrSbrGb;J01cCTr0Wv#|${fP5t|{2ULBx@I%k!iYVP z@{Rj_589F9;RSAqQWXa(+v%M<4;}V9lvqVt+kG9iCgh;_%-oVy>NiddT1z#*UQ6|= z^!b3;m4#7ZJ)K_mXg;{E(iq3;xfBRol@YYZ)K&4}Zj74NRH{A@?%#!`0nAn%$7vdXE`U&M z8m2l%>td6YvF$JYMhn+^n`L~_!7{!GCo*UwFut)H{&!Ne1G#!rgSX9}#SgMgg?JtH zR-in-vgsST_pWndwi&CWZxgW}0=E`)rqL|Wf`7fn)Q*UP^|He*?$EHsN3UL(GyllC zwub#`Fu3pFb+}WK*+L}K?5AxPZs+Bl^r*)<@9ZimKfb<*(v{v6N84nN&uQ`aw1u)Q zWqAvvIr?Wc+SFuZskP`W4LZG06=(-2+597VP86RqsLxD{ja?R=yB_n3(M;SX&#G2Q zx}2d&;OL7s8x0ivIjcvN*R#qmQZ)`&e$RTcp2f=Oq*I{U0jLWA)4sfSyhVqwgg*pmHFM?}Y|qmm{8GMZ38$d;WY&?SZ`CbSt~et+(x_ zl@j`cCZ_^)m2H;R@dM5OyqK#;x0+9B&{AlD(I|}#nfaT_{0nW7Sp`XlNMYKhD#!E- zamx0*b-OTLc)DJRDf+30P`ipIipTgmI_;)UJoO^Z}4eOVij8A&4jC9Q6HVw;u=rsL?Uod9(13d7%n zwl|6FIR32Q_+uU$sd0jD?b*+Mf$Sh9v!#*7oy>#f$JQ5cLm!ZmUx8ScRxl)ljb6;a z7^1o;Lu23JhOoDw_hZ$6N3kZe)`l)-={n?PBn$GaC>M}zGO=l&rM$TAk(Up7i`HOzRGG zxYGnCp3VlMzF;9JIl>YLQ8r_-W>(#>du&9?9u1gKzjKCbRge}VRg*2M#RzLA%VD*@ ziE&yyj;ix=%$n9hMPMl?WV0;R!&J6qirC){xBL-ao^De|T3vW~Itzw`N#m=FgtTH) z;=;8XSfx!?fq_D8)a5QsJqag~q}tC>gCjw=+cI*sk)vf0K}GM18jXyC%~uC0-AK>GoE zMlE&7+-!DEb-bo)V9JMV3W|43kkEtMpWjsANfpTPYru~t4hhc4%rzG%N%g+C#AYjYE^Z0 zs|`fX#F(_o?W>wKFqW@MTDfh7!=vN9+GY)xi@KDDdPzSvY^^hM3N}L3Dgao{lLz)( z)PLOL=K;3S{^{6@X06l&-JC;ZXn-3c$!mi^;G=LRN$^V$F`)nPf)7 zig0F8Ncvo^0u;iTN7)Dln<%6}2y#bJC3l`WV*U7e_W;B*kzjd-aZ=sGX})FGKJv$a ztfeHRtzQRi26+<%B{R0C@7{{g$foEFz{gj2e6(f*~DgqY+p*1l=wLcP=aA;^r%^er+~)= zj#Lc-K^s{5IEd)a-x6^hnVN-brzRU~V>cz>g!0W~yO-*ft~%Ug@|zD?Dr9=?R@!{U z9Y-YQ#+I9!xtT_IfnS@wEP*L4?KaWZBn?$FLlkU1 zu(e>jV{=04`dAtCJ&F5Wd?0IchO20EaZ0i5^c2%0Q0LuEn7v=-^5_zcl zYKc*+B!}BdiyZB=5(lopU-kiFL~=|UN6GIGBL<&lNY0m6jh0e|x6;TL>2G z*fmKJte36)(3$8cJ|xs{K|VIa)+v7FV`E*?4-tX?@;mcCTA*R2anY-!{YO3uqsf%5 z{~yN@X{S!qP`2qKtUJ_3lxmE7!*dkLC_nb?#f3byi0q+k5e(R`CY|ehkP6JdM9+RY z`=bHcvOjU*fZsWMJmge;McHt%L>uSglzS15ijy}v9}s=($TU@FNv99byF4xS=Wokj zSL8_Dla-e3FZ+)XF=gvcSNVM}9Zf`UCW}+8kYXy|`>inFoP)N-du|QC^4cnD_8xbq zNv~<5<{#3)vOX=W1}i0wX4<^H#1^+GOgiXa+5ar1okls-hueq`^+Cz)tbQEr%z@vL zq}5U$2zvIL#??b)Ak!}$p9)2y6F_iJ0H)()D}pxk?kygRfKhZ476c zt7<-fh5@V9E+0On2;AxzKh&FLYvCz!AO6H|k8IkXDlTEB9Qr4r=zC*TU%m$^{Ye#e z3$9;RCn{n&GcJ=p@#1CKavZ|C@A zcCaCpaU!eMCQ_yM>%#0#LnBzJedD}p@F%7E9|0#=Yh0qMFpT=T?nES5rO$R|)o`Bl zn?PCI7mxNZLYp$S7a$nIYY_0GLH0(qZnu5z;j>Bu8u5>#`` zoBwtGsr=krB^FY*bs9^YHeTh=q?`;p~I2Kt#@y->uP9kcDSI}%Z3u=0^eaWQxoDS-{ssyp+goo0bah(W>A9n3REtscm{&j*r^ zPfu1!Xgtg6DRF=?-qRm>U39tq(qSmO+4k$;FDANH%I-b*2mkqEXZ4R_z3j1$+wCWv z+qgQKuA>!jUKM{mCYzqrIRnXfa=xwm@KHH*wzwp_2BK;5Bz)ebcu~J8KG)=iOxf}d zU)pmuV_tUqw^-G$-Vyv8z)emBPRqrO*Cy!dRp$NDq2KYE1>B1CwTU;AZA&)G;j~*x zWp3rJ3de11Z(5BN2*}qaLQ`ljfR1}RL~Wd5o+*}X4~j#34;JJUN=#CzF3Jw9o>cjW zL}jO4&^`V`#XMv(W2`f}Sh|qirPy|0bMA&rSKi*d!p!}$(LY#{`ke~4t`!7 z)kPc%i6S4w>T1(g_aAzFvpGPwiaDPWP#KHsGLbLd9WI_Y&%ceKhF5Kev{rMb!d+a; ztS&BhA2i#EntSeI8p8P_J#v$*4{q(8_v^#+N0<@ zr(R?;A6-VXVTRfC)X$K}AJ^FN-4Yt+uYGCc{C)-*GBB`^O;{KF8 zA1}a|Z1Fv#0VGcsR%g_4MsVKI`9baI%e68JWJ?O%-T;K{ zV^V@wE|lGh_+jVDOi+ok_3tbZ6l)PL>dJahNdSq#+Q+)gJ3^Ov_ixT$ndMRsgls@j zh{WE4WwFyY$$%flBH5-;7~RgtWW4EjHg(Z~7yPF$TsbQhi-xqR8#fMqCOB$iXx?In zm)X-Uy%|0u>tc~4kxdut3ZRzvaeriKvFNO_4A@OuKFRi%DHzn+ghLx`)H1W`_KDv~ z`A%dhpBJ|Nseno2uGw!yQf#U7_eNhzuGgxN zL;CAw#C3=zViMXYQ`(Li?Z0we}_SK~m!4;(Y7cRfKe?Vnu7tH<&Y016taC41$= z3=L<_ysEUSotRXf#H2Fp^(Cj*9Zk3-hcCd9^7^h5EI_#|H_NTX=VV$^!BhVCNZ4N4 zo|RJyr6@9U?PU%pz2JLN#5RipMZE%e|KHdhKj3BHE}x2%|37CAHT~~V{u`rQ*A4ao z8K+u(jA0BwZ6KK-C&{c1fIHe{LaH;nIxkdmqA$~iHJ$tE$uvrAsqW5X^M98iV_{|5 zSXddSVsn>vTF^r$sjq4n&gEI;^VDKaz(s}fkwLoYyjqG-vf;P*)n7DC>lYK$&JL(t zG*=t(H8~cn3)8^2%E7FGgxs?EAY3-<4v5Dlf588}3WYkj2OZF{d=KV@e1sC~JS%kTMjtJ;UId$)duvtg!n5$W^W~}ITV;2~Vg*F>5w`i9oagL#fFfsc9J+|cDA{T^qrjp%Kwe+h=^39^K8bRZxV#JxU{gX$N#QwMK2tRYsnRyxw1>Y`*+r_+H zMLJdF$w*538{~m>RW-n4xI^Zfz3jH_PWlPV$XlD!EO)DfJ|bAx-d_<_Wm&J{3}$AP zt5n%0O_-^N)h^lQC1L(Uv$kflVoqPCSB6|*+Dx>4e5@7iNdPzUV&cBSdfERq8{(dN z*2Oj{U4;L;%XS=rg86rhLOjjs=sP%xQDFgQsNGT$(c)ZDTAb^?I+8Oy8)6{7^mle0 zEUii)9na^+hy@GRK94HN_3heCy}qavoiK+iSoIZ4#cNKzir24s|9VWxoF5gFJpe}M;0FNmYE9Ra5s#q&vL4)UnwVDX3EO|@cFXOFh?0N}W@-WZ%!2DcWn5@eK z;(Pw|=X40C(TVz9<>92m8ZqFPtBy$f4yHBh{yD&b?uS%_)0nq-;IqGjExIMjz~ev~Tw_SKp`c*O+oPlyXfX}dp8u&3jW(E@8Z-27 z628@CQhqLs&qVzxiCOrA=xLnIBmWmuG)ps@WFSkM6IboJKhV(w0#q;Q0R6mP<=D&Y z8zh_06h%8o)1_T?RII2NH#vS`c$#bSEV3%2Rr@2V{Zln=kMNwU=55*B!BsX?`w{Q? zg-4(nn$G)urD{6Gy_lyUww_M-nj~jimlmHE=Sq!BTPJ&P63pEXHxcp9ng?a` zF*E{22y>{Vyb2gdO#~b8GL%?#rMnnD=o8i=+3F703<7C}r9Cg(b=M%5E(D8j8&+Z? zRwp*}CoO?4GFF%dnS3c})h?y7<#!@CH#;w5FQl+Mzsp}!q$KjsVij68pGiA@y($X8O2dn+qOM+oj~Ag}lOpC$?E;oByNzA6SD+1a%TtY^xsj zpOEvhhp&gBDY77Gkpgt4C!UVBwG2 zj8VxeW&~&h>z>5S%8^HpoDr2O=S;A|aYRpG_62*jz$IdXf-|3m(#! z)~(AI6Kw8(i)Z;%-%s}_r!t1&XUxgG?>~5k0)Buo+;$^v*`7tn$R}bTr1_3a!AE1` zLe{3Q&rAwP$|%}YMfGw9XIJ-U(+LP{>cv)CIDYBIRWY;u;>mCJCD9Zl$9=BKQQ?6vKiDsO!S~`nx$XJ<%I+K1=(^YH)WfT z$pl~zPO+?enW5|1uEH1dRBQHOb{3L}zMH=R#MhEWIF@`6QK=~aIZ$P7vdCE$a~Aw=YUty={T@1l>Y!#@5AB&A)Ha!7$_(5SL zt=|;80xd!er>YM)178qsYJ{>WGY=xA;a6h+*XXNe8y65(XiXbP1UfIUj6nz1W%$yB zd?w>`>ISRBjd!JR7zc$7*`7L;wH4Jn0EFwHn8rTAN*K*!fMYci zEbubI`SrrdH4pK^TD-y?Cu9#Lvgm5R*HfZ0Lj6M{6Q*d4p6^7Unzkrovh8LX?a4y@ zbhK_Is@v5Iyt6 zKm?Qb>_1qRxNm%b*$-;lau$Z+i)==Oz2DUrwW^B2hJdhHG{R=)FkR8K75`xoy-)sj zlv)Y(p^Zz|;79k|iA~y1;!TdTn^q)5Iwr4tJ>nb<(6HjynacId_lJM>z)>0&{XSp0 zCjI;HUmhQf;VZ-lbhoV9Wh*y%7VIBnbI`XtD2Ku@w-17#o&ap9Uv{Urp7srU1RD9&1R01^;Z3&6e zcQTg;`9k{6W5R;tzuk7>v45CBzx}Mka%kTwj28jE04t%jpNm~7Ubf~vUVJ= zzP0BBwZBS=rR3f>uq?c_aR1!dA*%qOS)6HY*?=l*l~6|_3H$zzt#Dye8d@~>#ksEV z$elCFRv0+zAiks*O~ZXxT*lJy*TU~^|M2h^#Lrrokgn|ZF*MCU?i#6WZd7foDS_ChSnZVe*0;Tqhh4t=Wa2O?0^nr>T1e?Dr zP8pO8z#`}ejjabx^N?l7U0ve%;^Ly0QbaD60aWZqH)ah?!9RAN7SW+VERuHq%j7(|$$xX&n81A`PJ zE>>)%K3{9p&?7v0L;AkTD}A((q74PXi$S0F`9XQpN0G{RAw8ubJy!H!X~OBqW-Yu$ zYb6_-xDvSz6nhL1;wwWJ=Ye$f7+jYo6wKh^_1t{ctRnbaKp(Xsd4V2rH**j3@E{M_ z8$!5+pdi_i@XkWiRjkRUFpvFEW<_~S_9)KXn&G{iiuP2B(#nsGz=hsH^h!S}MUbVz z3ph$tgL2BA$cvVJe?T?rOhvrOp}U{|rgrHvQ61mq+OC+V1vyzlgRJa zd!F>yeCmeAHuYfFUkd+XwrO~8OLM=r#-_z~Eja%_)>bNL2iO8#%a$z$fN!*6z_OG+ z(cVYnD&w*5$pXitOT89pk)6Vls$}^;Q@pSI*s>hdAYRTVVqAUTMU9Xs1|d<#q`#9} zLBEu;!>~P;RB9tD<1h8p&*gW0^bC98^zS}Of@)I6cg!>w+nVyetDGSgy2bl1<%$5M zXb>Y454=I)+2?!U9=%ysElaivYnha)lh)*MRwkd_k#j;zpY$Z4by5!=s_VgjJKZl! zS_!=DK(X!w?HJEEARs9HY44AQdbNI=%R?=vNUP=iY`?iRBOeo+`esJ*804nke=RC&k;`~}N)1T{ zet9ZjAC@(RI=5cmdWEXNpnyQM6{1g|#!*K7N(FsEAJt2jLJ{Ag6^2=7N~JI9uVZTu zKIYP*!dL-?q-)<#AaXkn>Ol$O{F(L9D+zgquVgV-j^n={1JVb&VyAlGS?<{Rjp~Wg zYGK;OgH!yCcRo-4^j$wJ^mblm!|p{TUq?F-wUqwEzmA8{CW)l+x1;SxeGY9R!5+2MB&{ZC)rWG-#?>ZGVWi!_E> zC(G9&^q|9xu7A>@`Sa>iL`US5A4f! zWU@Lgb<_F)BW_tzcqjoMiM?NJ#?i%R(h83;0_I^b!ayGZtjSOvzEm9=Qbx+E+FP1` zI0oOhc#26ub5u`-l(X^pniauIvZK(>*|GewGUew#Hy7wn!GTIwJdK{&-b0j zKw4eP$vMJxznOWUjQMAQ9yN{!M3aWa`D&QlAkJVkWRR-x+)O*q%{0+V&`^-C7!nOt z>GZjCVSXGYTh6e(YuFp82rRw9pUdv?$k+Lm#g&aD9@CDoEGOP)w3R3j+lZV9V^wVG zW`q=0|JLW*oZpP(xRf=k{Scu@Lde6|`TWV3fCv?w-SgfXejah<`NoNJetGt@M2|_w zQ!pfGfzrW)dUrm@I8y;b%FVujmEO`PAde)p-Yzb&trG5b`%HD?dT*TgQHx3UU~gR( z&OozxD8|PuFpr_d`#G4ZQ;#yr8mKuM1c#u4-i%r488Ti^C27@9e{&k4Af_GLP;%c3 z0jl1=;2S+AJ}}Y#)zsEg1aHMjCB%2M-$mVUgO#PnFqmzMv$$SpFv4BDd)1pXRh1eS zNK&XU8*qH;O#H3N1+A=3X53&Z2k#y%%d zneOm8vi2d)d}sSn#d@*`kKexdSv<=YBb; z9B8;F>s&2h1+T$tY+lU_+QCZHw=@9IHOo2j-t$spRY z()Oh6c4g;gX#K*_3j9D9>7@HF7Seux*I?OM8pf2Xi7D?iD18msOvg>})4P@BQs%tm0n!q4( z(K2UR1h1A|G_Z+RDydWkTfhhpZ>=vo)uS(4XX(qzMyExQ`dDKyic_Qw!mxJ~8>zb^#QavMG3yaG zN&W6rAycZ)qro36+dSl{o^7t_yWL(*PTvD(10+`uXY)PCh4_9W6kH608WP5tve|8` zr;k*BgM9AQqwYN3GH4TQNFR>~aofMdR{Oby1<&iEc$zD1-o}W|v!$5y_cUTN{L-oB z&)_1?shGL~a%qo<>L5}3{{QLq1of+1kL$jSAoVA*8t&%EZS=3c!tXUo6Ds5dzTmtH z*S-L*OoL^;2o zInb^1x(~UVIKO;Owu} z$|6}PT_*SLU&HKqpGJeeUrKJjYddsiRK!7}*#cSi9>r zE5t<8C7Y*sKX&Ce2(I7rlFq<0h_st1_eCK#&`7L3J1#gY5ttLl?K$oA9k86Fgj+ zmbfw2$RG}#vLD^p{0JkyXC3p+|MpMP`$6&VQRaihX5$34> zvAO&GLY6LXtaH{p;NiLJVlLHV@{b->>W;g#Ds>l2m0J28L;}yfmm1zfOEriTGd!ou5zaAhPuVGob&J}yO zrhoG#|Erg&M$D#z2mT8FuZEMjbd3}@iwlk&UcY~KdVm^Wy%w}p)IWb%KevSA8%gg+ zczge&2sTI`ClBqQ_##Ds>|8BhLN||J=B(FiYN3`R=E1bc6XL+dj-l zcvAYk%RhFxiP8oYZ0A|`_*wQ3G4~kY#mgfah$DcGn_d*TG_-%I9QqKiw(P1AK+>Dp zBz*x@wLJ0K+k0<2ezGO5T(Nd~7~bsrmt07x$TR*6SE2{m<-gKmZ{GK6_}ma*ffd+}^Tj{sVe@*6QloE=#`M8wGEUkc+i`p(kD{@~r}i_1ps`SX^t&sQk>OxDUi z@ROp5#Xc2||Iz#ZTbz^O+jc3{x$ zBb~;{ii0P!PvUqw4Is}AZl#M$sD-jk3TOoqzodZ&6bZ}Rj6iVEB|Sw3?h;;`v-VPd zmaSTgbz9c&ND46W7oFK!oca5!Mc-|?<~X@HXVH?V_|Q~W=+=c#hPWcv#zo{U;iKKp zi^8xA&!1i`hnI*0SU2idDi&T{puHSd9(u7zPYrOTei~QWET6&m2UvvOBBf>yAYbFu zpC=SoR4*H^(J1Gr%wo8{az+j1H~WXcMTGjWv*vcKjR%K^6pICixXiZhV?mIHOs>oIkYH+Ju!dp_t70dZ-3n$C(G)cPcY9Y6!^ z8N{NpwG>giR=*5FP4=TcIpQLVZ2w2ax!HC(_&wdGS8A)pKTaePHMV99w`(ZWXQO#f z6JOSk8vvx`lcP2ER7hicVI9oRiJF?C(L-N zd6V$2Q|W-WH==GJn>G;U=i1MX46NlNRjJh+yrl6Ky3ZO8k~4%j^04|s4lLlyz2=G9 zC05jFg}V2KiFv{6tw2}J5^9Nda5B4=DL-@d{UX`zH-5e8KgGR555E6k+WLbN z{O6r0w`r^aXqFS7pvx{r!b^!;VHC*`1CaY4$lhpnm)=!iDeuEOR2zU(ufH+H?sb2O z5@F)PlQx!7@_~9CrCoz6ku-mTblvFqWuf8O5w`-|uZ79sXT+4P6>G^_sYV;Oh@H|4 zg(BJ{f+6}PkFHAZE8?;-G9h$Ha8AT+{)xeqyZHR(;yj0Ykl7pe$-Pz;+JpXb4r^fW27HiMKh%>h$A42zWj@C zt=PNssLS03g9O$e3p`MQNWkNeD>ZL@{kn}Y5w*3VP&KO|k}`vw@?i?0Eb+HeBVT%N zt3&$#KfYw29R96XBfToPxB!1n21j||Qzc$5K6$L-RQc=^!Dp6e3cTSAr4~wp)i&!~ z!ilXeC{=rQL#e8!z!zeYvX#!8+(z?P1zJaXH!l*#8EK@67iO~f6l#?Gz>%WnCm^2t z`Y_ovi0sXJ5ZTin5E;dsf9iAi)H;rd7q;xpOD}SKNl=c+*BAVN-%bz(9!|kVW*!S3 zEFDB3r8kil$U)gxt1E|8trI51PT9oVD+@P(U;Qi^12qI_)&CtV+k3xG05i1{nl zJVUVkvUqQ*P47zyy+!btakvC#ZOhE?OUl}xTo@ep;laC+&^?{$szXbG&Xm{&p_ zuU{&!4GOhfV5K)R>v24YIVnIPjh2HR=l80y)HWN#B+{vbG($aU{vyf}^f?TpD71~$ zk1TXs_7|oXJ8omdY@%6H2zwDkp&OKgM+Ltwo?q#_mN4R3YvA9%eel*%oY_KU)0TpY zLGQW;xoKdooyYQikF3+1V25S*Kwf{Gfa?r;ik&$}XoeCtxeeID+)Vx(S^LT>686cq zwPMYp#aU}^FbA62ktd`MrS{oU(GwNsWxmu20W4`H-0rpGLg$(fn^!kJy? zv#;;FR#IAeaPjAhF06BCz3o$P_#GcE=G!OFnyV!t<*-1@XXc7@rHU=H8XId8*T;nV zEz_BzOx2ZrJ8-B7UJx@ur|sLgACOJxXtPKgGUVIR=%6#gjpcD$KT+e5zWy{9-W8sa z-0FySJulc>DT5y6$ahV2Ijx%M;A?~xd>sas9eK(2c0@)C9#*^|FxS9)enR&G{)&Y`_g>4|wO3YJzT z7Zi~Ck+zB0jp$v*<6>i|4f>?=zSbzvKq6NNU(L;P+V6K6jGaz|l0WP5%} z4+gwrwlf+I*xV;Q1`uuj$BLWDpUxr_!U&HS1$)biEMd=RH>uJ80)kP*IC@S#GJ*9Q zG|ZLFp}3}zsF}%-=)Oop;XWTuMA=EUz9EjfF%h_3YjSy-U^0v~)jfl7Mh1RDrX#X? z1e2fzLYH_F+z0+3~nTJl}(mPz3{2yV0J|=11G!kOlw@g1r>k^{_BL z-+AHi&wk1X+Hd-pK0y><83UHP&l2tKV{wcvQ84X;qL(u2wzhj`MNM1Tayk{=Crefz zmBaWV>sTLU9qXZG>U~Rk4P$7uti;rIp(%8x@ksK1gwmVTk$3Z#Ed;mJ?K5F6lA@*9!47k+n+PU+8-?c-PtX?8xVK@Uer~)pj)6RZ|Bi( z7TEtkw%!9Ss$+d0PgKsXa}X3*#AVz?>|ocRu|>8qzQ=h-c&?E zK)`|;Yhtg7F}>%e-D`?5nZ*O%|MQ;3Xg|NZ2;hh;f?&YYQd-g)OKVoIQndVi!h zx8-mbL)ZU1Vt}3ourzcH)cA3@mg+#D%7s;L?42vP>jSpQE9zCC)M!#jCgzOUA_Sm| zySlDlaPb;;xMq; zRCGm53qS+b5Ha5kw}B#l)0fno)`M;jT;2&J!8Qvit*p zDfPLn@pIoIys_E+t;>eHAo^lRaf>lDi|(?~ zefx61qzzLy7`f}0co4&G04T=sK?KeZ04(K}i*csDgl8ngdCtDa(-MKyf#117IwQ0D~nYOxHrm1HjiA3L4NYb8DlGu)wGSFygDwzT@W zu+K(ka2U$wU~6{w8DfZxptT2y-i9 zV)j2+jEl8kdGg`q~0I<`Fp=D6sdYiVfxWjBFFL|_(U3p2852B*6d8GzV zVb~R1qyqxlT!=D(%!L<=1e^-1bkIA1c;A(pot(sNr!wD^!b)^}4bq#w2sT#!z%_z% zi9=PCi;rqXV<~%>3G8QUihO(w_8xTaR#hD0LxxaSPg7|qPq)MM%#6pT-5)vg)zrwX z0owq1e?q~{PnDpEx9moG; z)6zG=j7FVIXw)ex8Wl=zSa=~dc_4unz{g^hrHrBJVh6WH8lp^^#jRJfYC^RG)Qa-M z5Or%`m1kp6bs8gvRzch80&UFMEf)G6WNfOpAwuI>+T>RnkF8w_ zd-xY!)4wR_mhlTGOdrC*sLW%;oWds|g z+t(5VnSC*=m%RXwCbNkyHv}Q6+yYn>(5T#D1~Xx?p;LTO#JtByNQF`zJN6wGYYJfZ zybhg=_Hm7h4NM3y4$$X}$jo~Iy0f3b*IPcpZhw<~{U)#^R7j)<7HCYuG#h>749#ia4&bYLDuBq=kzAZkUwG~H^NYJG|+WWghoy43{~sQ z3@iAYv{p4&1y%=x{b3V_>sXbR0K66N-^Ivqg&zCAS#}ta6xZb%__XVIFnzd9F)Dw| zw9mVHuOzQqZV2|-x$Z#3?yaqHEwJlA4MI$O@3d-1c9j71!0k?HWAd+izI|F4s^ARN z_@fQ%LNMCP&8TsL<3h56z}I5-`wl zM&fha5iZ0qV~!6FBA}oVoH7#1T=(z?tN`5i zMK-5E`IUBG4y#nqsQ?lneY=mV?Qv$}(_)MW?(#JK3w7$Y-1x}IlteEhAMzIZ#tcVf z$?F5-ws37fCStL5-z2O&x$ALUB7aFZ9T!e>p)5ji16hbWE|rdzA;N7pxBQg7qz2+y zxZ5RLwuNvz9cu|XTF|gZ&h)T|l-`Pr*_v!j&rRQ%fvDackG$O!E~R^;{%W;R*m(-< z2LuZLp_V&VvQdXGLZq?`#)HHjK?Qr2wD-L!ChFNr`4 zi#KJNDVWKdg0XNmPM6+E4<=h+?o!QKkmDZY+TqL>CPI96s9FO=K-?~zH}P@k0|J`fnCm(J`=~wgsh9cRe{oSCd|sTa|aN`gnWm3AIQlVp6!6nG}l>{X?L)uHgew3R#jE zj30(^y)%y0VDHrPEv^VweLH}d|C~~QIM_UnGcq~8Dy%WSdfUFFLz%#GrMfq4ZQjlq z^7I1fhF1ih0HXI08dLXxgs*SgAKS2WCqTml8iJIJ1(5&GlKDFs2L&SXlA~w}Ud1>* z>>@_r-Qw!D{fTw2PS_fyvu$g30mk_uMluWH|Mx-A zx1onj;@SWe?j768|)mA1inM@ zYPKD_6jbR6Vj!)DxiK2_c6`&ci^A$A2J$F!H+3jv%A2y!)_DI4uj&m)a~n(L;E?3t z%HSHm=Cs-}*_003hMGeMV#wi!51GzH+;-Gc_q^V1c_RgpEQx?a%fl9>o3|Bo`A>qk{FBii66l=W~YiPO9HZ zl!^~Gi~7k>zWDeYkVo z$U@|^&PGqt3EP~b-~Ooo+4DQ>pP4gIp)sM_)m3a}C_#Gf5O-RO@|To4JIZIyDs@S5 zv8akWbu-N3mkY}qxt$*tiJ-|nxF9qyatE4+n2pe2Pf5$Ao4CXm4@Dk1+*V0ygNx&R zz5PnOuOKht*2plVcgt#;8)9l-#T%E(s$1Z3_A(AHbL~_pf)GTt1v|hUfK+kUd5jG6 zq>oA~8_kY}08hV;wQr|yxsPvP^jg5YxNAM$fTrU>3^+5wrWl%%BGD88{pmzxlOjTK z!fNTMGO2ERMN$#c!9IM*C|oj$bdEc(Mu%@%>phsQHxU+i5p>glne+)E?=Xz|t=EjE zC@DRf55qnmDh%yvg<%s_7&>ywYYz}(Nby3U34JSx!H5&dnO@8ldgyNAF~_52fF#xm z7xVxXLQmjr?E-F*MHOQze7u~}MqtaLroyLuyP8uCo(*!EC38TT^t9LPp+e`3vQ6!}ocZT5Y=asi*Y z65p&Ygs9VEZ@e6ZZdmtdu>9C3_ngCO05d4)aezJ7{`sb`OZ^iFe=E6lPi|qcv8Z5I z39aiZ@Oz!opr*2LFDygxIUxuRf?-INSD*-R7%QLTvU;P)T&a`YgSIZsG}8S4I|M)H zZb$R?Z^G!aCh0DR9v6T`Z(>jmy@mHi6Si^UDa-~sG5AYvcb3T~_3h9QZ2^q5kIEX7 zHS4#>M+E9c7EUw?Q}iVSM0*0>xhXDU&^embobdw)CuEN(;*?j44|7z!fFwovro8pw z)+*rat4a8^=t#rcXZM`Ig@rsA{>Q;TZ2kh*8Ogj#o&7%&^oV?RJB1UpII;S|$sG=1 zGsi+mM;-v^$7_UBCDMdWbSF$BPMbCGCdgyRjs z(jig2A6*}Z6?^t4mYNT%F(B%2C?<$#&M+}){!#O!ZsF~v$~0>3RPJwXkJ$?cQ!!1% zqA8}~-XZ@w@C6Vq0L0`2jqH7z0j?I;A8JTy0n8rE02Bw2f1sqKJXxaHp8SdM$siV8 zGzdtZf@<)D@&t$!DjEZAxNQSq9wdH@NQ06lfCMrursmsU zD@RXrhn$KF$pSvn`b*;j1Vi~mdPp%O;kU#sH_Cq_&1tyUO<}6mA$iPgeTWQGDP&iL z{%t^Ebz;Gn$^s2`*&LV^R^C`03+pQ|2q=}kxD#5%3p{9uYRX9Ak*uPFEfG@DUyH%F zBHn_|wkN@?4T#1WN*Ho*3G7C&)AceLrI5jvQN9$+wSgFif#n-F21aB2e^^$>=cX|E z-fzONOcjfL(Ejh{-@%+huL72iA*fGAGIt2AZG)La*w+asEF#C_s6jX%QnBkpsNrQX zo?9bd6_Y-r|7OZ!7VycF@YgV}zOlI)9C#B}(~q!BN$1u*xN}zx`I}-89yi&O0EoP| zd3WGRzf<8PtpHPdSiWI zq0(T1zQ~7tg1ee563_&WG?@-L$i$6*DDvoK2JURl#6>wZIXJAB!;+c4BRyU3t66-5 zhNSS4J}9baQ4iPreaKxA&>k*iL@_}d+kAm<;NT$X#M(RLq=xN_*iMxEbgdP|hajVf z9;SU4N{x7xKE#^12QWTdn!Syf4LiWc3OU3S3-vFJK5qo;t|6(34>`g>8b4e|RmCRF zjZn-+0w+;|@kq!o z(eyR(=|Io<9a8hVgcfnJ-!Fe1S1BhB`^A7s1P^!05dGRI@|gF-@jRbzCcnnsb0~8l zk30|`Q=D*WAk2vokww?uXzu>F_&Cgf*3^&HiI5_fs#A@kAMU6_v!`+=$#UugQDz}& zbNRWb5Ks3qa)m|sIYQ2^=(6Ebn&+OVgII52dIajd{!C_j?pba1hcsIhsIvu%8})c{ z_88yN@|~?QSj)kpvmgWHZ_R7llY)G(z*pDo26;5LSOSsOKhq8Rz9FKft6KjffuQS2C8?tDjR&qa0IW|G7o?hd!V~dr7tdYTX}qGe2G?0%#Y3u z%LZL)niy~!eYB!6%P1y5TjoW!5&dKkvgnW?`MLq6+Utdo+rt9^=|p$uYs4GGF(>lQ z%}4q75%njx=V{e#_dJ>joIV7o3rIGF)Fk@7D5O++Cfa|iYb%{^btI7aG+ICp0y+5X z#g$O2otFsOYoR*uE|t}`9Ehny0)!h6!*C2HKF;49Y4w4)e0G0{`jlpg5#QaPkHbYV zkshk=CgQj4!^c7GOG;@GR;C>h&L`;+Dre37Oho0ddcoqO!XBBY@){y?=yM(1p_)F+G4L*>M+6{{AKC z7oOSMid*pNn;%)0fG(Jo3R6QoUD&S@LLS!Qz{=w(zmggk1WK(ch>dlD_(k`tMy6SG zAzf!pOG-t;^-f_u z{Ii*KH-qet9|MISPaMnx3OTq;+29nIfUBG+Wq?J6&Za`u5m26?sV8ap0*4*PFuCOk5ea9JDeCqf+~;loAjbxfBx8ZjdJ*cNy>L zppi|Q=0^Ai2KD-fFG~vELK8O_Exv|Vwj6~WH0t)h2|GVE`deiS=lv8!(A5HP0b;B{ zX!{{Hd>bAO@ zrUftHZq1I_M(nqPmg-o&6=_(?H- z9)ff$At56z(I_V-Wo9ND<@EF&S?OrrNl%9;Mj0b@I0SEVcjRW~8suFmIVnlh%I!o@ zDGb^ZUz;vNWP_-A#lf`+)tPclP+8=@2#BROVo50Xir1V;_pjLGh+=l5s;z7D4l)c< zi?yL1;0)e63gBz<_Qv8urReOqor%<)%}-Ry%FoI#L^nT`;#~pVX3eIwB;=m5cJrYf zEnwh1-w<=yfazH!FQF~mgQJ!i5$upF5g}1dUU53+2-xD8dy^ZMB3q|<=38`FdTqdK zXI69huG3oh6j!P~#RBc8YJ}SeA2oll6_j*hWUnS3>@-9jjTrM0s9Gpxl;l*Qva~dq z9x4K7XhGO9g8;2ARTG^O`wCQ?&C7=}JQS?kX#`>TiW!uq&W{h=gc<+Y*g*7ic=b}C z)ojWO{s7%T6-U5MR-iHwlZG1x6e5e8pvkV+-5De#wTpOEN1Q?z@x(d_FTSOiBS{W< zu{V0dkwb+y5u?tT=75W&yOZD6fuBAWljh*3dtxCZUyQJ6L9uS!+2A_Vd8c%~f~%BQ zhll%Bt^s!~A)vrV#=hPw*v=1(Ooi%-oP=pL1)U-#B`X<^bDHRX{4aXxcj`~7N@c^H zw%qqr^4==Bs;IcCO)uA_?Olo7U^iZJ3^EpYq^^E&^Vu)(;MEGTM!tXR&%?h{*uL8O zLnYctwb4abxO@(+HrTwbfS)??;Pu{ks^~CaL5`E3-xdHk11T6_GKbdfh@KK;UiQFz zW6ge3iRXiMQGSeVJ?H&67;b|K{t&L;$37Kp-B^R^_=FoOugC6FW@Q2jWULeW+r1Ne zFX|eC%eL%_lOYZ?;3o$9_y;Fxgt?r`GS#DL~;U*}m41#a&46e9{NA03Gtlk1DwbB`|Ia>i#d zzp}8~a7=P6Wzk@~_?mGHa?cNFkq7t$I|%!->Q*NEp=MXciVN|-A}uPxmT>?8pzH=M zwz12Z$(CSM%^hfO-A`oSa0tWL+AZ{#diB&%xS-TFUWF_UH?i0tibi;kvl}SinrNM#7!9wN^pNcr7W2J!k0t0YL7kGM6ytJixI?2?Ii zT#N9A&wI1Hilxyo*&y(+A6cc#)mS%8Y%`V~HT$81)rSdckmCM1kP+~~%W&y-x0&s)3EUACFmCr5n?#WYY{Ion zY>(x7lzqgjrfl4Dw;xQkjxp`0;&u;q8oJS+CwAkLKuqHij6BBbE1C|}*4KnpZR64) zJkaTU;$i&W`8Q@Nf-ySO=bIX0tCt$P=SWLqBLjVlw?Udir;J`jj$2f#9R1jUM7WL8 z-4JuYashue4!Woi93-y=BZt8Z0>MR=53^Ut4D)fh82Jc$&??A!%EwH_SnCuQ)JlQC z7_k=$v#F|F^{pPrRL6UCr_*b?Dpw7(=cDftUfLlO1~BASfFZ9F87XFUcQU;u6Df(0 zItBV;Iu5iyL-yBRU4Ip?#E$oFyr%&~11P1@%9DK56x{V}j;9Kbq#Yu!Bpx2}b3 z>l4@-O40p4?SsH&VaxVEoX-Nd#rLe*3nsc>Upg}6^TvSBNTPG0jqzVy4X-G!TyFmCDgqL?Z)MISm(bU zJ;#*7OJWyzcwe)%qp>au#ID68XyA^KANerTNX755N-!;Df&hzmWx^G{em9vi8QqQ# z0t&2J>Y^2HUqVXTgAy}vDDx2}uJgg~!SCGAv9T`gtoC4L=)on9ug3TWWEeA+uMJ(l zQESCV{?5or`^mzDFSG@kR(BLj+@pc?7(asnDzozgBXKX$zeW_XZd-Wi z%6!1L*n@r4g>Qe0>r$EWEVa2Yjj60?P+!CZuB+2zg&pXc~&vBg0}*K=7THx$Tpr=x$Yp6O9D-X~a1uWAGK5 zoDdUE(H}U&@pJk`U<9AC=hLI<`xdiz00AxIQ!rxi-OqB1pUj@N#xEZmi=qbuZI#2` zxueHAhY!B+c*{8@GbI_qC(DT|hPh*tSGboqpJu_lT6}8b+qLb|+tquTk+o&k?5fH? zyXV%0meXzP-(B`jaCu63dRf+rSprm@QoOAqulJ;S!nEXW!&jg%(4{4Cj93WRyzw%9B7}(;Rvh1 z26q3ix$wV1WIYc$3pfK#vmjuNLE*y3^kK^8ZMcw;J8`%B=pYxoJZxyM?NunpY}f`n z&_%`&2<`P>gHMe3h@Dp%9^$#K&1^sM>H>&Yw->y+TyW3QrYM%16$|d>i4RY%AXUQt zeOOIec}j^PwWN&Dv!)i645t&nK@vQefvtcI=8wcs3=R7^W-jIH(&9v-wJXkTK+teo z5CDRtB}sw7egzSjNDUqe-nE&Jdp{-8bfma>kn8?p2TLH+_Z~a)ul1uIGjATfipT|& z@f4(>50T1DQE**D3fMhvahGjSm2Y*F0hn1{1YmUx2B!2|EyvC_Uus!#{Pjy4 zI3Q5N?|8o_B<$V|at9^Y2_M2lt9n#uRwZs2#YZ4Jpt|ju`eV(9jvagJ47dL;-Wlu0 z_>7mZk5S-v*U&ka+plGA+!9%d`o^KB(QC@b07&#q?*1a5*D7icFkST~H-h(LokSto6-frV-Mtf^_pgY z^*;72zOKI?>^-;zEN&s9O_`eMmdc8}^_UgOU9{i;VMc&QItJvRjdT=+Bggkq5C3Av z-8=d-fc9Zmo9a=2HM8)4N53CIO1DI;T*G0;4e+{B%#~cdF2QFkDfOa zqoI2?X#W|WizetNb}kV*)NJ|m(=0dtY~HEhN@&I7A)jXls&Wg8fF!BlgJ$4lgQhYo z>`&+NNq^Gxp~VB{7&Z>!bY=698_Ww~li-pK9CnQ|1z-UartK*a7>IKKUvcR7Mnc7n zo5dBU_?%6+W1yh0U2pgkCm!7&NK}B3MxmzsZoNwDDpXUBlEdjAHs}l0ohEKQhDBrf zVFv6!XwtV3_#U@79+?D9=c~A73@EH;xEq!ao9fp=mwjhXI1uu$`oy>g8G@ryB6q8gh7-oeLJBxV0s5hnolZ=hRf*YJsF@c1`^Rz4H_RBqqKOrB7%z!smKsN((Z0OQTa zA*t)$amW-!Pd%!`6osdLvzHTvlbj~m>C>w0^qAF5xs3VMiaY&^4dkd!Be(er-`p&L z?Es~D?AOdFhL$<9(m&Ye-5VA@X4kC*yUv(kdYo}T^2Ul169l(hL`jzII@S&BGHUwY zNb<;HMW6c_H~xqvN<1Hfxu93=UHV7ueWRk)bWH~dmk`<9HjH7pC7*({Dj$iQQWbbz z6R9S;YOkICGc5L0K`LpH8Jc8cVqPK9s~}RGl^Jrhh7X&_5;M0%hZ;7$!r3M^MBOFu z1E_H{`{o86>r?0x+62y@Ns9;<$`y(-6EnF(auP?CMzjB zW#{&tsSY{mIT<-Hhjz%yB7@{?DJRn+qo_Eu%n@oO#JDH&J;b!ShGMk>V<^u%I5*^iQ+ zwi)KBaGD3ELMSl&d*%#myo_h#Ts7TDbO4%b#XycDY7E)OXF>v;=rolZiBRxzh5-)f zm{qwP$QFXFaog)TT1N2ChW}YyR-shl>RB z7Ekj2ZPZhqBAXoWf|kXRSu~%V1EfXI?`&r*#bG%*D|uIuK1WlOpI4q`$SU8Rb-+>N zNC$RjW|pJcS6q@=!AHJ~zq8H&Q|clVo8TyOv9h|UJ*EyA%zQrkGqg1LB{nPY`T|9- z9+hQ}e`8?Ho`+ez5B_}G%W9r^0+VzDJiM1nOA~`a(451Luv*T?F!KJ6(}!!Gd2gK< zhC3OG<-0AiMJ!K7FnJOUrA`c`#dFR9OAGkHT1+X}W#rQ`X?mah0l`NG+Ox|Iz$epM zf3R65gake0XJh=l-AewcDa z+<*t!w_{-f(7=Q)s(B6lQPqEuaAINQk;U4CJ-5PIid011C#G;2*@?c4uA@Kc!s=tj zmO+C%0=NlM5hba@E`-5-#3Rxvx45l4*koN@WL@zA@7Ke`2V64XH+L0SSKZ|nqnH(f zO3Ouk&TN*hS+>qwtwRj>lntjyOfRrnZg<^NwZMNUo(^XDT`eA%PS zm$=(_e3Jzi=9Rn(tWmVp8)rAI>o!j&go% zZ?3tORk62?TaRX5Wg*bbk=<0PX6JbJ9K65YXUpDHSy5h?LWszO#F${7svr6~Zuu^( zZPT_@zUl}KNj|s=Ut*idwlAqbQt})g_@m3HXMBX{dI0X(NDP!|2%Bov?mq-y3WLt=&}lDuprdyvi zP?N4d7mQd0kBn=oM~3OXNcoJ36C+rl2AqzB4aP*x%GWd2Y&8VN$A%VqgKN8k54wW{ z($n-R3X{lDjfD*BFJBL+-x_(pN6d^n)b=cx3Ly+t2;ls97Dqw=TyvLo=cB9*KnJc| zoV5q@>`SQn^dJUYW~M3jbrsDG1cz!4tlLIOTq#pqk(zF`+H{t!f1yQeG8w7x8^s~? zrNYcY5bv6Ei@?>G-NlL`nz5Bu8C8{5y%QoXr4^;M<$L*DS9-cTsB)LZ3w-nfXp-*bc4L@`Mxgdv41VE4r9bqk&$6hzRc5Ml zTW%ci!wIl7cp@WM<&bk1FvYZbR`o_6$lK%5ZMjn(YlX!XJ=%#%e_yheM4npl4AN9_ z7L$!Kz8Es9vseZw*3b-8E-(+Y6J(Q7Fav>_r-IwrGBWSH|3N$@MmJ3ZdZ^J(e;z>QoMkHlO`ljby^y52W7D*dGLv$X| zxd$4{bFp{$IciP&p>TqFsqU|QF`)7;&Fhv?ePJM#e8lPg)iby&%`rxiYBfakw^&4sB(uelu=92r;av1MP(D_IX<(T9fv|L zcQE@7b%=Qo!a&a+1-X;S`28#r*HuYmB^89ol?6nDAP~SC#Xz*w+y##-;itMyla{J+<-74LEDbGc4+7RKfRIK>M=PrLH^){2d?;SxBUvw(ceS_%UMW!E z@muz8KvCUp0qlL6R0gT)JD}{x+KZ+xSZn}b`@Xb&!baceEBRAL%B zREOn8Y;~LZ5}*;g=^@Zgp z`Cu}D7HoyE@%s}*pXJzfR(UsLZX6Kvl=n{zCvXzK|C0CF4vB{Lp< zL$fY(?JRdb3c;J({uh-`Y))r3%m>d51zU)aWXM$bD(=G4bH4mso5Y8K-r)?6?+x%3 z%=F{oNN9fplr@P1J7sg$pJk>e8ikR{C8m-`zp(Bvu>uB?m)jtlU%0}>L#9GW0Y)^9OO&0WU2u{)0bGZUuh;ZREYT5U)6q=M86$M%VtJz-doliM?Irm7_K!PMnbbxg`~Ft1O(Y%)lXm#`~)EJVC%v{&D=SJdiZXv*}57{J~Nj3vzpUa ztDw`30VGA@i*5`H0vo|pYaFD{@Fu->hP+AdE+xCBV>|P!_wgZ!DXw;KLU8-)2)E2* zfNuE;w`pP;IrUPp!9(-tEQH$}bwA5bH;}Tiie6T1HQ1J0$7eg^t7qBcVZ9I1=V0mr z#u1c#TRXGUz(`n#VYL98K`{psnB&gk#St5^&M#r9EDS*TLh;;>JG9`>Y;xEEAXLp# zo4~fJR7LsIaeU|^Hg^`Zw$=;%Zf?C8+}3&9gWohNnRsj>0a@@^tb`ro(&Ne(IsOkd*JdRXjbPP&08N)})B)$L!btGX@Z7SAkD_wOs4=WVb1R!$ zwy-_8FRu9pa4YdOy5tw|izSY*tn(`jy>8$xh)Q7Uy-8XgqH?18atJSC1G2w*oXQ!y zQVgdTG3p#WzExmPJEm(Q%n_Z~Wi&PmXV+LlbT0Vs+%5!*Bn*+VoU;6)^3v*ps`7^N zkfNxnP`RmV!s9O<2`NO?PU0WOl~zc=0K3e z1+pdFMT4<_P890+-1%(dW&~Qf>z3F8>BE@bg@&v&A6L$>HA)MYa80UGh2|%G7s!n` zv>;}cp1f`uX zn0#8mk`;b6c;GYp(!Ub6oI1GW4=fxL-q+0b)K`yA;)aIpVO=F+0+S~;eU9}wMM=yD z^88crH5c@q0kxYp`A1+;z{ldzP_dzAJR81(Y?pfe0IEEa8M;4^_?&7q1TJ2Ckjmtj8lq%RuH#wJ9Nvo*mcX8!@!V5lRNi32uMlchQ&0cOa3`5!JAO zTwVY?5qdAP%g(-vy4uP;TJcPp>ScNYd~k$lw7dbKutAUi};tKz(y!6?g8>w)&tke}KEs-$q>sW1vuff_S2hRSm4T)z@&^y{Hooz#?K6 z8wsrewIcWwaw*Zb{?Dbtu#Wfd!M8DQ_Oo#F!9s^jKVUQfFp6ZtX0sk_xMhTk9HucL zi81B!u@t|pucOFrk&2^Hn3>$LGN(G}U`jwg9>G?aK4s#9fWG9XNJjXWzMznCJ0~W0 z3J9-R#j?-VqT=y@tx}+t6(8V><>E#xgbs@_QfEcYf!5fnmyOCwbal|Wt(v0u&olIT z23b|JqS-9rZDu9SPm7G$Y*1OikOsj8NIga-p60_wgH{b1Ofh+py^b;;@1Mt30(zEm z_&M%6hy|WA!dUyM*?44pc0bnmW|^Ftm=U*)JKRD77J>el=50t>elfQDNmc?7So(0k z*Tz1Na)$>|_)zE6ncf7B1c?wyNRXxkE^`i@ZHzbc5M^CbSyg(KV_9idF7U17kW;KW zhCTXR&2jfU)VkVJ8ncSqN>6O5Usa63K6felMAuPngC=`q$NmM*flAdJ zYU;yh2jF))G?1fhxr77Se3NV*fmG>0B$t2_ZA zy?JSyJ&cMSMiZENs8yQ9_O#sr=x8OZdv&jD1=fBy7$mtd(YYhS3#*qe9lG#}7|#pzcW73mM*I8giN%HH zF{ZNo+_v14L8Wiz;a<$-U3&jj){JH!ul{eIz1UnD{eiHMQlxt zjYU4BZ5Pf@nO3m91yE>$!2-NB2~t~EG4h{p*?PF6QKd&=xqe>Q z>oD-O2rD0|o{Ze~8Cu5LA(Mj+nSL|eM-2ZX=8D9@ABn?wjmOU5j}Tw37MMN6>Jtd& z$(ajKD<(uoNetY1^u*WtZ@f<{dVuTNl3I{k`$~^&5!H?~9DjSy3GKdf`A5&|Ww%qC zZl)Rb|D{Wa&d2Pl^zA&1wV~Vtfd7+He%_9(Jfob^p5DF#K1`Kw?9$46ktV^~W&!4e z*ST>oT4m00)D*;ocg46?F#$5^fOSuXME60k!-nDqX!ejIguIs9)-zPJP`IgCcPxK@ ziF_!jBfpTWqH5kMmCtdT572yr_5nxUw;S6g($biS5HCX@B)|+6Z~)Z3b8b1n%p4SO zSyPo?%$<-tsv0)`WRT~_8 zEl!RAnn8IB^8&X-A~pj46R1~)gyYZPnF+kk6N3O#}Gvphx z%Ywlv2{o?rKoP>55Sk#L8l_9~VpGHsi05oLss60gRZ!DA>8RO7-;mTGN>WJG!IHRfQ6mFsuIl#6>V8FzNVK303!ku9_Y?zsOt>qG1b)0f z;^J>derdYjc8_Q!@amYpR=7>aSy8j#7ElMVLecs$3ALO^s3g5`k~5_3VvgkPF=zXP zlsD3XL-W=Ey{h`ZX=K2L57&$TsPxZ=C_u}!hGcSA5lB~jEC%Jh)4#tchKBrVb5?LP+!4`lx-eW9{eZ^pUncld zC@!deTBD>(#r3s&&))IB_VVW4SqRF6``@~Q;e@jM^nPc^YGy?)?G&K zwlXA|tQSIx1+m=Ayq6`zc0?7^*GP*Xi<1JlstciG;olUGYkOx?@y3mt;+NBMx1^?d ze;n;1N|#GZTAE^Om*ViMXZ5B{UXhFOfg1&t4K48{i)k;wwH1GSx4afhj%qA9G>Z~~ z0|^KwZ-%QAAm~)I`F;0KAE<;)s4sZ<7L(!5-$sz62q0rUa<04x%Z~EbjLN)5F_J{b z_Sg2DW#Pn;y_H2v*4>!!zC|7#3Z&ZH<&4Z=3?4+YT8iPl(NEPn6$8PTRfb($Caf-V z%XXZ3%#pLdRi;*=TqOIkjy5xqnGHe_0Wnbq32!7sP~1Am^ke2!G47femkOLZcBkNz z1*$2j0@q~K?y4vR$#mq$V%!U2`dgR+0RzQy0dVy{+DwRQPd<1w7DB+n4V;j1ppP#6 z6{_wd@HswbG0XLA3za3m%#7Zg^9Up~(zmCjrl+Q+q;5}5p-k57CyS1|Kvm~6Ohw`u z&tSR=RI)P6d_EnGE@7Ge{SIN7qx~i4qHOMEu*Xr8hZmDVK#XwEc&R_3WV6Mnv=UDr zC7Vw6gY;@{D@lL)65;8Hw63rzuGvwBiBS_6vl0RuIA6I+po z+}O3R*(gFZ&D-m-ycL2w;He933yFkcCuk$O3%3p>vuTwPraDnhF*{M(JZNTSN2uP>Fdm?WtCEadA>NsNDC|%h zn7L4mR|bMO+|bwJbmL#q@ss|v13?9^lDzCH*4*bkSGYXt|J8atI?aaSzcxP>1(`K_K?_Ry^~{~06<&;Q&c`kG6hRwso8s#ngHy~B!Iqye=o zY|rq4dTLeISk?uK33oV(pH{?fftOvR%24D^k(h^N&08r6A-;MDM#`EsDe+tU^vZmE z&$MIQj#6QV>mZ|Io|i(9HH9e|6pyC@N8(Q=Haf)*F}ex|#P;om1O^}LfIlK&g}iLr z*06MAN?ziw_+4^(LM9+zkFDE!ebi1-d10!q%MziYp{*&Jh_+rZtdnzsU{dWLoE};h zpp|n2YLfV{e5`_Fqq4U;hK8lZ#9*~V5(&L&k6c}ry|cuanYJS}O)npasa<7IK9^Q) z3rFy`E}#qvvLUYDtFj@84DrRPZr*adh$mrK)DK=<81Yb;35S89=o?1fFv9*>;doLw zzD!<9rZBc0du1<(324YUs97Q41PA(*hY`nhenOa!M}7pz>4+TxpiI$15>wCS&6|n? z0ArR`Zwn7Fo)a*?0x|<0ZZWEd8yy}nA6_Y}f_G#&6dtoyxr(jj;#lkh?ZxN^v)BV?KfCyxK7g+%4b*^QD%Anr%=sh}yHBDndQjZw zY$cxTkTbBkpkiDFi-2m%KGg^7N3JI|kw1^#uR~JbkJwkQL2{kk14nb~bWCkpX;FL- zPA_cWj@$?)(XF4wiF2~maO*2F2JB0ql9whcq@lbX#RnS8jq62qLew4}@;%e9C zqBTkKNgyBq=l72cDYq zwugY2qs!4$?a4l25Li(2A#bv-$ud;7`??6j?mwFDV~h$5VemBDh>|43^Sz8(n?_-I z;vSL;(7ElZ#S>2nyR=`>7CMmcz$JbX4CIVt8Yey)AdSqfb@R-wb=c;QQn&emA!`I_ zm1tE&>E#wcp1Pzk(6W}L#$XQs3w${q-~ zY=}@yg&XRD2WIN*C>Vaplu8+tiH+DSDo^#}40YHZ_`cX}VDh!1-K`Bt2SEMic7+U9 zvrsM5f<*`~Bv>Rt=@?asAG^z&ok#i1r?Cv_Il%q|2%YS-9-Ife>nou##ay7VPO)<`2*gu(w|$Uc8kM?jKwfb`sY;=Z@-eDP8m|g;!|oGNevd zN-7RCCzau@5U*?SaXyDu_qV(&H=1qPq&t;$9nDeos7GoRsh*MH*Kl|ZUhM*bKHshP=u zSs$auMHx5th`iF3p-E_Ep7ITe2h&SXlRJ3XmK%uLKY;i`pX7H z?=(bjtOM3YZiUtQVmuzu58A-eZL5-{^+k>6}j47j0&GCpuTQdT^* z)xwf`kZ}81$bPsLn;YEn6NDlJB|KX^OphY40>Mnz`4>c7HbCf7%p>9zHd>xO>2W>H za5F=mC2>0+AjeeCcIYI|>{xO7addAX|Gc z=Mn}Wp1wUsrh4!+W`$`?gkti*%Nor5>WiJ6W5_A318xtbAf3?UBOjz8d-#6a1Gze< zq{ASPGgZ4gs$6%uR~{dMA-Z^xLFp@D(T^rK>5vikJN}D#8n-~KbD^F{V*MflEi`hK za9f2LUUMz0d|#{TR4bsr2&Y=?X#3oSIp`DEfc@d^B-Ynw zV``GU2SN7Z3BQ7k6Fw=ke2?mV{Pbf8-!Xwai3?Gob64!DJB4tW=}tFU(HRhpj6KD`5| z-ZHiT6AX9Vg)^zy_z#$F`pdA?puHAw2+P4ExP}+_CGuf;&?oIHAIU1O-LG#2OAIQM z+2wVrJd!NFi_m})r6r2ZBl{O!S65_0a)d(Kd7y+ z8R2UxdO+{K*{FL5<+B?lz^M+O+lW3n2r|xwEtEovadBeG9(7`k5ks%yK+4*+V^aKk=MzIl%49hM#rXsaG zvkc^MQ>t{eq@occI4^%kzT;giU&Vlgk3WffL{gs-cp$j&Rp&tkx7WdIs*4%lutz~%;y8bE5>h8&F1L(v`z><1}FMd_+!AS z0)Ubn9Gn>amhm?^8d_A4JMI>G2Ce+g_Pw?FP-{^QbL-lm`eZK5 z!e(@~?i%h3UXIsDAOIR(mGr<-8^kdhTC9gVF4Bpe$8P@%4|%LI5FG<~XAtsEje8z# zr+^@Dk2Vyu&MC71ZbW$rTkCbTS+(05XBG#)+y7sBvdXl^kBl&hp1|^TLMu<*c|9p5RAyRI6m0Vs~`+Vn>FH~{lU)7 z8iW%pGE_yW8wJ(P^+|gWcD`vr-unjC&&cCgjBpYzFgk`Zw^?SV?>9m+Yt6_X3A-4m zCz;)9Us8vM%_qVxin^Gv|4;9c=Ear>$Uc$y8*&BR1=E~YG;;Ev6Mv}tMc$>a6^k|U zd!X*!J`M;sGB2bPgKW;>zM%aQ9}F1F&t*lBp4bLj7}Br zUG=zA)gxY~OYauc)B}R2ilAUhEZ9-#_$kE(`){D+g3e@n7Y_%WNP7ehr+6LswlK>! zK=50YZOFqb;*mcA29y`X8@hjMJ|jiJ5(63#AT_M@UbB2l|w( zZW&cARc}-Zf z_wMy(RR<^#RzN*@8+3diFBKzu!q6MZxYy?B)yW34$_ideLoS0b6cUyQpp+ag2H&I+ zKK!*+N?$of415ni#bzhuB*L3T7|!CSG!P0(9p&^CLi@{E*d8g~DCZ<4X2l_UannJb zT`n%l$|;1GRDE_)nGvE8352*%w*uj#lULS&7GBqbcD?@^#= zW$>YlS2*vap~c7709`eX7B(PU4%kn%W$8BK<3PSXk;1JM{D!qM;S=BE$-2GRXU9Qs z(<}wLReWpy?c@Qhs%88B4F|Zh7P9ttckOLBxV?@`7g^Y5;|1YrqD_f$Knpuyi6rJf zx&p+wDT@zWkGF@;h4oMYR2cPcP+?4hZ`Qhy`1QuUVkFliXW|p~!m7{)3p~(uU7Mt0 zV0iOko>5r^^Naz5SyXi5wv27s1eoIla%&wnjr0(lu;==NbZ!))?n1?3!jt%6xE;IP z-JehR2;FKCgn(+Z>$2;1YEu)^W23k|$SU@wQdN3sYLQ`Y+TL{d{izzB6F~f>CW&G7 z;sc#mc{EfgW}NTUgWy9h0{eRp>ta5%01)sBHWQF=!&B6PR$qaxWrN-uYwCkNzeZ*n zSZ${IKL>WZ9`$x-2n(-H4%-&YZTig3Emm+&yu3wXpDq;&Mm&!5#U>@{WX5ypj7%-rS-M z4O%flx|v^jpe4R+F~zIJ@iE^1`B68GVyv_w@Y#fCQr<`n-j=r^e|=LXv|>z)MZ4}e zqEus2N;E$pHaLa`%|@*xf_ywCDDy{?+7~JGEm9um>OjPeJX1 zkLJvn^_yzC+4H9E)V()>S-OMwrC+gi7Yc0=91qtq1<4C?0GFtd z1P#mS#rUPlSM-^pJ?IK=>UYMe378X)Bc8nlL z#Z0e;>9`SP3&57eBvv?^C7Pz=C+khDYXTsi%Hm;SF(14d1#^Q=?Eh2QuaQmnfI6E6 zggGijeA0Q={Zscko!n!Cu3n9I#sx!kqSqLbbHbE?y|n%{KJW&cp%F_&oVg(Dc68N{}JTRMs{O~K(aGc4*`2xs=NU28u&R=ywU%Nzj0wUgI{X*bkhfj^PQ= zpP3ZPobH1BF(2PE*a1}4QMmS{SZ15LrFgdT`dR$j4ax1xFD-3?2)@aY_v^q%b%o3{ zpqgvba8k2V;A7uFghjW$;u$d==BTEj3W6~Z2sU7R+JA_$IotJ75QtRcn7Y-iJjETR zkY#LE&W@d#Ioh1e?93eOKo9d^J%jl4S!}~E>4u|1(s`xiSLf}>&B`%~&!CW&m6}S% z7<|}9TV=6{g&X*pCP)t4#HDqUTE|fu(YI(sn>L&JfZMCb7E=^J67pC? zpJ%7?VNkZ1Jcdlc9I+hgI>im1tjCT!K?mae+U_gd1zi&t^%w5DUmCN0P&4_6&yNX) zbzs=ci;ng-{$$$p2;Et@bp6feb^+yi*k~mSrr)>Cp5G4kd_V>Exl9)^1}q#A2u@>p zW!8ToIT-n|b6y$f^H04=eT6Sz&y|MnOWCcxX&Melb?TOgkh#&^66pl}ZfS97U1F{F z8h-bIloTEs{z9z!J4~Wmb$rqt0KG`k`l@1sa^DfI-NH8bA=wIPIA>8FXX4*?){f|BclVBD; zO>j&;qMP|vZ|el`FA#taNC5^LT{y#xh`D{J$b{m}DgZ#A4>bP`pYjtE;{iH2@gtUO z;)L=P&aj_Rod*=s%#j1T4A8rI>3PN%`gU=fxiokrlD+CMY7<7ZCTvJ>5y4hZk7$ z2|j28Q<8zQr84?Ufcj+tJOcvg%sMdN5hCr#)J21R@By?>DPFsP-N@65CBn;g3S_-p zT{%2%dsrlQJj~Qy4H!8_|A3R&C}TY^$JQyifCo!c+51vvSlZUqn21;1Lza1T;iANN zDJLf@+c96t%E{i5?UXXjXC;m>l2 z@c#!BIfz|+_^(u&P|e2S6gw1f064`yL%TumefuU&XJgpvsmJ~XT`Rf6&?eKavB#h& zvL&P^*NG?hGgA)EV(VojO?(g_)?bKoCX}6FO5P1q@=a*EcG!6jEW;TFAa)*sO+9vo zkHOKiAK_i6J;xTuhX(i*LXIIlF*6}vKmXDq$a`R5^HCd^Q+n7A#RALo$AT-|stPAx zp*$^K!($uEdMpq?F5*N}V_9J)bP)|-a7T)Z&|y%Hvo%LIc3jf8-(rB|QarGw-ZuFJ z46@KU5m5QWrTDD$J3~xZ#@25r!B5uU^!E=TRyDU-#MY^VIb|BN{ZH$QCyz?D_ksyR zu8M|7O18gDS^-`SSE{UXU)>k$`QRn&=m9*33Bx;^nU=i_7Vo2cIJfoZH_ri$M_I*xRo z;zPHw8qltcUFaq$B%}E)e}JTrDOJ{rfP%Qnn%taf&EJaZ*j)dhsMrA1!*iS%nR1KH z3?kA66z_9#<|S332<%W3b1;QA-V>;xB;R?4(c##NyYEaYQ!>eWGZ1?gy$GKl8spRp}Y&8JTKkr#N%~*rFQz2OWPfEj?Wg zZI6Fb52s`#9ZottD)~@ydeXtZBHp`?!3Cr|^Z zQGSnFuSW6vP;_R^r6U1OuoiewGjy9Uuc$3Tb#OcvYhoZLlNVnjx>sL@w)v1qkAWmJ zy$;{}e5taU4z?u?LVRRsl-qMs`%COR9Rhn2{+ZGLff|@P6yGt$hWw@*1~bI6 znXE^b56r+BlTb$T3|r?R;SWng$ZSCI{R>MXqkYlIpW!QCqJzrtj?EaoH@GT(!aKn7 z@6r+3=;^`mBgo;#*=e{KKy$}W=(2s$_2hYUim+_mEmlB0G16FViIJryN#ve~j6| zR$2{@>?c0lR0(tyuE0uFQXgcA{&gZXV=yI^=`K@AiPoeWUJbLvqf*Bv)G6XI5k{E?@CpaubWJmw~u%xlG&# z>&Szg_X@pf2 zs&?mkK1kDq!ulv1PplP-Q)At{)S_OY+dwTt@qtKC`*wCTodUJ*&Qo3#t+kO&&d26n z+1s4>5g-~c(1ZXH;0Ogz9D+*K$LpeN-ptUlAjQ>;{K{r^1Mi`bsv*yPyxajVoUE0C zIBqhJkOIo!6+GUU5TbL%BF4J?gqk!#F7ST3gM0$Nl3HDz* zN8$(TXu)+g3LELzfdmYEI(YFK>;|gH;K5SuY5;HbB^1+ zV+S>TiXCr{elbcmbVRf#p9VK^Cp7^X$@z9&D>Ye9dVx>(6VbJL=$oj?I(&;5hVcfp z#?W86QsihtBN`#mB_GtleJMHd7BJw1QM1M?-J1`uGyfB;LR!;xCmB~Vob0fMpISn5E> zT0s`C-I20mXYW>`ZUtmY>`~0BVGa6245=Eea)e8_+|!TD~j{+GU(}haH-;G z8r6;_?;`=+2#yin>3F32aP?6-*avS?HIu6tfQ5AQMe=}BZVdwm?|bl;!9MD~+izi~ zWq{SlQp|5VbfsEdp*WpXu}CAmNi7n{Z^=UJ0cvH0&pE2l+Ytj3ax|TeQKe>(N;cD= zDwSkWqW>POjxjq)W`RW-`rh*_Q9u=reSp=idyLc&hlS~ZP~Ii1j0C?&;l9>n&}eSM z0vpDTQPXXN&7h(u*XCFmecMTi|YVMaraxnp7HBz0})J9!G8Gg=*n~k;+e$v~YJ4yEy7HQk6ciQW?hz6iv+v+OrTC3CQ8FT}pd|yDHd7@D%1qn(Zts$qw-UTqtaHvrz z8K4mTFpnyGh~FfRL1aS5d;nZdPtqniVF}K{c6-dNp~)x{Y#^WQNhhuBO}U74u~dj# zLB<*F40k=L(J~^%(xEWn$800|_t=142SO2Gq%|CTIU7Cca#o#IED@c5NYHSQ0 z-A(i+&2%)yN5(Az^z8`O{6LO0JrT=vbSz-|puJY(#X-N5ldJ`bxkJXdlZ0T!-tHoj znVp`?D`gz#@&0LB*dic28Jk6_JEQBJ>%~>%{rAx#6G0ZC#tfM#j*>)&Ws#R3Y~$ca zz?xl)V1e-oCV&>Wd@~v{!|_|dB4p?s0tV;sHpT1 z(9Icmo*xw*LoTQe=Q+Lz4W{O&6l&TJ=O3+7AB#G=Q6pUzHb(e)2Ns04rD#z}8y%?@ z)tvkX__25UW2m}hY6Z-WkFbkS*CWMa7y0sSHxH;26J(ncnh|)q7&f3tz5DnzJLo_K zo@pfP6}k=xnugvt(50iK*N>V_HMA`U&Z?q<0|8L#gD)0ix`BJNJAi4y-VOlPE8{K9FvEsPvsQ!b zZy}L(g0@`@Jr-BoNiBRK88Phah8z$F76$?wShC>%%Ry}oTecBMTyhVo?ngwv^hZzY zj`jiz_G^J@@u?ALA)8DVCvlmJ(13 z{#@_T$+@`r9eL7|K;EFUzD67rdyUdyU~S_Tz92R6^{;F6-_wyTq&pb&&eUinHcI{p zlz8`Gi4Q~fZ)1P=FDP(IkzmnfyCDd;GTPq<+@#d_DZXd=A0#0FTjeV)2%qV`PV+?g-00h3PI|NTe@@|K@G6=yIG(`XcZ%ZQ8{zN>fMJ1mN1#D$ww3h16oY!MT z-~!ex`%6YUx`z3g+UfB_oF;IB9M|>Xfdaq+(4mM^sH;#>g1a0JEy)J&ODE;eO`sJ# z)(KbHXg!|A2tiS!S{^v@i@=xg3LKr4{sX*12Sc{YeCUo4qK~!!F;rYj^rk@a zgoHwvD34>iZZyzSJFD1)F9D!oqL8sJ`kDAFuz21?d_)WH1oA8XzDa;(4S9+-BiT4^)~$bzDJ{syOOQB zS<>fB=J^73LD~lM2&i&5b*a=uk4JHGBD}Iu%eJFSY8);MzD9QXA*B75;}vM)@jPt! zmk>&R{aD7Ky9*q;ne!hZ8Y=!u5Jqv?GCOi++=fK|!0uJB3huh(J3H@*TaluPQKU@R z=V!ltOYlCg(yh6El`$15N^#+ZxcqU>_!THqgTvylK zvD-9p3hyLWs(64R!Mx(s3QKDDDiLM4Jn>jxD)GL23@|e8L_Eo}O6yKy14t)i5JPH# zwqVomxWPU82D$(F;rHIfIv@L#Y>Nx?LK#GX(1$p4IfO;7-0AupxsICN!Q+dc676NC zQh}d>nomR=Wbjuc5Jp!525%t*lT-{bH2M#ivC=kb&3E&dzY+I@4bEQgEg{la9nwN~ zQ29Y+T!8|I zr~y2&HI8^LY)-cPXHk~uhlE`gBrDI{fdW$l#mzCntPGf7rocVjN$nwn%-Yb93w;P7 z5+!qBPi|*uwV8QIkS0Dg<>&z-XV1gQ!ryf_RU#gB^M z9u6>Y4L@r-zL{>~ z)*}H$QN_y3!kv61tfTX(xgCji^UeqT>1`n@EHH9Y{D$~|n2a5eya>9jf;btk=v4yw ztryj?s(rFOp;{MHP^QhMBhzrNJY+ouKTgN3Co=e&Z$3KA*{fW#XF+1f5JbjC@X4e> zxsurOSe+?#kR&?GjPrn>KdWw3^f&_T_G6;+juUpLq$avhs~dznO~5MiM77`^eP3*Gp1v6on4*nJg=t@I_`d;A9peS}wHfmcr zFy_x&sfN3KBPsFA3OSXm<@g=gBs_`iqk`qkrR6cCF9K#)OIUz7yM{Bb*$w=Rnjv#A zYykj70DeLhI54v%Q(bVz0Z+D7e~s-p+NOx zerzT0O6E+@n&uE{4EyR~tpr^i$HQeYYV5i3*~r@N!-U$dm-I zl^CdSXr3W;)M5znm3GPaV$OdbN3!F6Y`5dq&iEG@eRSr+ns4K5JJ)V5Y9*jml@!w?*sG@7$b9G;06I30X>i>JcM!5i>P*> zBWK{ZKFFx4X-F!2MJq)JuOx+d`DgflfR7ITjA(UJM)3YNYJs6Rr-zHCKF(f5iN{U| zhIk29CJc3SvSEHHM+3Tv@jXLy8{&CrP_Zg0ggUGutz~B$>yC5C3KKlti1T>D5>zVe zACLV3z5q!yxV{@@!hnCW=XBh0u$-r#K^m4$hyijVdl_)W<9PsIH=Y}#>QDNVB4*cX0rDPTMOKd`K&I0%H!6oJlxZ3|e-Yh=TTPC@r3vBofRW*u< zi>cyI9yU3+)rj)~#sqkYpFfxl53_R&d42I+E&?Er++gwdGYu&LDQrcp0=a?O2V(-f zwTR@_jpw@Js$xl9D7D&TFvm=&i8oSI)Yy}laPs({v2FJe>E{nQ{2tCfHJpFS&pVoV zAIVL{J$KZCy2F-3g)dkbJ2WO&fR?S$VPk$nmCNzY%eMfk<4a7g-BR=+0pWlko9aGM z@Lv09HGTrk6a}ZSPi9r6V?X6OjxFTP-!r#h z;N21u!}c*0xV#6UYoV6CFI7j@z0T^6T91o9=OSBUFVT4rLg@OuMPggB@*4`9qpJN% zkv-sWfZf1Zb23B^VmPS6HtB^fWSZ_FS!F&Ix1&G+91_02G3!R7Anp0*>@&j|4F8$P zha-UTe?l_~@x~rpZs+kNLkuApP~($%A=Nkqag)%=V$dLio?;=OpTN*HrUUE>TCstY zV-kTZCXa8?p-NCuiD`)_u8^U%MMWd@$!b4?8NB%{+5GEfqR`suh@c?w17ASv$zJ!Pyu=$b8G0>XV#guQ)1a~Lb zsM1o!)})xdzSL?tsf#(f9Rd8M{QA+O2dg4#M9C3n2 zDMwDi=A~14G#wuPxlUYZG`R03AJ-{3RxB0}H)?Kzrir6A9OW2?D@1@AcY6sO3uGW7 z1$#(IPHtssS6Dp<@S3JX+;SAa#au8`FoH4Pf9=i-DNF>HefcND~z|4i{Fp z{0DhAg}CevU6Y~#m(TU&JtZ!d#e9X#i6P!O9P&#>asgvHy8GzJ0^%2_fa%yD1&$|Z zT;zQObo^2xtw(FE_ij8G0Uo<}0Y8bQDu)0@o+bL9sN!#2f*C6vfqgyc*$0)d;vIa_%3Mv;@ZB>u{XMF@ZbINKr0EPZ@{ubKXsj=MdeyxW6CB zEJQ>htez5J@f-$v$7$@jNXX6ZEu`-NkM&Ae5_BxbNMi#7XL0#VO?F@daxq93SRlG^ zZvz|8l_ECb3F#u@O?W?e37W(;hib#yEkW7A_QSQ`?H|4Y70nUU?n@Gchbm@GTt5g=-j$Na3v=hmIjy4X2$a7w59YxXby->2XY1@tTZF zb(YsnR)kY~$OZ5MP)#6*garEzyRws@I(zGWjWh*%J{#=-h zjNy9kvtCOcn>r5{@MKR-R03)~3=r>Ag(8V^HE0HRdja|V+GS^VAIcH9I| zPdT%K4W;H2$f1Zs$JED)GTRWJc99#28|AQ_-FY^M^U#QI!;SC0(`QdaEFXi3(}pTd zF`5Gm8gG__Xc?ZZ`yesMEl)wrPdXSEO-DK-Fiha@oX3jbz(W*niwsJ}nmHqx$7ke! z4|os~sP&o@K05#qmD#BQh)L&91OuSsa$ZY6{$4do5sU)h1s~2Ys+YDBEf(GEbEFu= z2KFQzQ)P|^-4DS$3hXNQ!cWMD(kMD=1f(2@vc9GB9m&pvZYZb_p~^ntFdCxDtvb9* zqgy0&RTsCM49EkP5E7}cV{2gLouuP6{Ijl1@Ml646V?NY;1shLyBBh}3UNiVz+oD< zsjl9BQ(w!On%oCWjVtf`lQr2L1XL&qCA@V(i`E5deUGnB8MM(Vf`?~0VsM`&d=yw9Ft3w< zUE0gF57cL8Ex^BUlg4mU6q{p%y#sRn&uXa&3@ycnh0P7^k+lo8y(_5pCf@Hbh_Igo zIv840Zm6a9j-q!x>@lcnFtR~S|MAsFD_$d8hRs&5^>1!#4; z-N;m~?)#a1f>JbXJz$Q|%#fy$tmsTQgjNdCtBsBvg^$rvo{kBX)N;!&zH(6?)K>CB zf??uNjvBbQusL|zZU-c$GwCzKFHG2gU@awFLTh}9CzrWTWw@s>p>Hzz83xQ1@INAm z_ZZzrbP(LQrPOE-kR$@F@<|WDt@7I7DY8>Tht49>HE!NJ{Yl_` zQUcXB5?hRiWVj9OOgRS{&R(EqrI77r2RfX_*WF6F&5McVz(DHDMwuABTOk+^sXReRmhS2 z8nL7{vX!U`5$}JVO)UtC0ClTey66W`8#-Tx&St;JKrbp@Qo&0r(Afi59mDcDMiucG zRL+R%g7fSwxfvxuAhJEZUjyCwB=jBoUL~#Kk(Uc+)xQCu4EaR>Orh+^qy<$;R5*sm zMjuBP`kUnL<_wU10NL@aUPJ~sxRLm~V>e1GjJa~8BX!uO&WBuPI?IU8b2$Ikt6>2> z4k$%{kfW%9Rs!|}#v8RaAvZEOoijR#3^;)g6_1J<+eEgJCtc?c!t5+QA@qzSys7}% zYyB(|XTO*r4lNDbM80MNRp(*3mI33av93A37BQUP1?SxW$l^_1LnLE5e;yH!WL6wZ zu>6otKt2oEZZ?z7nL)%03jIPd1`+kg5N>l{CR6dp>N8}{9_<#^r|b{dtLB11r5^$z zhmObY3Dj&5KJSRzx(0`jZ1@#e#-db8{$ftsvA`HjM^EmL0NOBOP6ce3C<8(`qT7+< zK_EJU)*kIZn0R$8gIXaqkpa_gCsHN=E1~0gstXlOfPrTaYUzg$WMt5Bj$B^BULQcm z0T2=@i7GwspNV4;Wp0_z#YYzd@P}P`fgfMkAspIQdDoEq{V-Nf*o| zdwBC2Fp8GehaBhKv=)(^7Um6f(bGA90N5LuDGo_H5Uy-BDyeIiYd1kIhmii0%|La3U<|gE6waVfmLDNLRR)I1_y3`j4Bu>6?~FLVL7l| zPi!-U!@3g*kdef`M+P_}I=7NEG>JKMDB{fBzpTevY$BzY>tkA$B0FqHa3z-FiO z{>SI)KTk57k{)o`zB^joUe1Id9xrJs@hu0k z`z(yWpxwgur2QemR1Kxej2AQWeD=a@v@y0|={jBB3{c7ksr~xN?Ab?ZDDV~q`6NP4lA zb=K0*w+PUyoJF;vLo(Q4W>`9V2163K+MuTfMJp)5+e0-YF`lydn%sLCL%pIK6i**c zAfIE{=5Hag^b?nG>)0lmS(u8Ya6fzqXBMY3$HU#j-9r=~@3wyqXG)FC$-UsRovy** z>tNag3j>jXM(|L{J@tH7b+iE=5T`5xKRsv3b0?s?0-XfS&Be$J?coqz2&!G>NZcP- zk&hMPkKGQ>55eIMpy>%o7#D%vgfHKW@3BBWB03TNwBu#sS?G)PqZ=JBQ{TF3A>h#{sctd2(zM+!i9djJ|z5n!o9faQH&hXy%C6^{u( z8AMfSP-SRGNljksv9c;ufVqPROt`%)E79&_k57eLSHQe8K>Ah5mJs`djktE zJRfZu)(Klp=Y5C2=wNUH!r?8fn78PHaiksb7sF8(kHi=wKKKHlMo~j1uIci-eEC%E zwQJkz)~?;|3*>&tkw=;@sr{G44FKI&e}gF@Rl=}9N#2K@M4u#f`;bOz&5and$0V;% z-4ap6!P;?>UyORDVt&H0#V_%yG_bj{3-T(a^MG&)!8xA)%gz5;iWPC*e!zKf71pQj z4+y5#?_lBw?xq?hF91*sEy2ME)60Fy4;L|}pNJc1l8am@^dRkat= zcKiX-V+VPX666~USl6Ro+-DRssU7ZT^(Nkgz_hwB-4n3Y&US&2e7Cr=u`RrOiB{h! zERGCu@hAxf6+AB2;BsaEivtjQG% zv{Iw6AUW8>7i$##_1xV3QU-D5U<<7+z!uTa(XYyrOJ@t@@BtKRV<4?Ggqf)>>ZY%Q zCBKzwwPcDyG~_Um#E({ead8$N)_xCZOUGZMNu=)+m;)e^@L;nG1hF5xSvMS|gX5l` zv70zsffxsJfUg?tcjE%?z>M0@1JWj!;0=b})!vlh*MXU>k-p>^IbfBszyo9okVsyX zH||8DWDiBf{}x}BEHu(z0-Xr+flciKl)wl!@dW;;(OS$ySP*GbbuZzs9nb_gbV?Gn zfPJ8K&2z>nfaCa(5%ZLd2NI2!eNfXf4i0K2`LQ%fzNm#8&7(riGViH~x*Vc4+{1+P$D#PhLm z&#i2y_$@G);7$Rg3?lgy4jw=S^g&y)7bB@~jpRjzWP|{jWrB{&v1x#LWws{(W-^iu z`Jupe?f;n#_7a?-I3Dxi_k--6I&K*W}{?-3qn-g-tf zREHQVT(bfI^w2jBk)F3Rnf!qjh{A+s}n&p+-KacG!h)HfE^!55)`r!ewG6 zP^ih1i-Kh2KHugQxsgOjPuQOxZz*l*MxY!W8kzH^kI}fh#Ca9_7W$XRm-7Y#?JSsz zAaIfs)Jl<5GN-dtfa#G_e5fEbUk36hv%Dc}$!Y*Ltyoln>;!OQq?2;A%DW)=b}Ed? z|3ot(ql}5P2QmXQVF-FS^SSUt!mP5fI=z0WR_`n<5ApT)%nQ1W&(?h{+}n`@kh{2L zni*0fILrV|jH35(UD*Bqq-Zf;JI4_d80e$)cSr^VZyIzrg4T(NAt7q2h7747y06n9 zGE~wTJA(Eys8p$wppApQkq4eYxyR zYOaECqN7p?Q=A2AQTk3JeHI-G8A;9BK=D=nidHOV52@90&T*LY&RKOWCb(*`1~~du zITJcbQJI!d^QI*hE-#%IAHGx2)QH-4VcU`FXpA5TZ2>5iC41G?0F+`XAwE1dA}Tb% zJ&%I~GmDp**-d7)RC*h8J!;W?GK(5vOWQt8XGT+=aR9X!F9X$r*O#Du+Xz9OgV-87 zupSg1vCcDgX|@FKccTu6G3E79HJ~pH211Irzgu1iHX+uL`8uM1ml~r+I7ZZsji*j> zAaX01jn?)vSAZyum%44o6o(@lr}O2R8^EyiUW2alMdG&!&_c(kHQX|v!o4S9R*w4= zUgj3vDDLi|iVCpo)M5*n!7ZM?a8maa1Oi%e4idheouTty#J#?!=sU&>5{@zrj89bd z-Mzo)Lp<;h=S>FH?!^Hosp&Bi$egyyND?R6XfDOZu+bc@l1%{i2SnNgQ96oBn9I1e z*Mvt|09lBw0tSQbEx6S|oQf&JO(rjVetV)WO7{{V?5^X-nyAB2{Q5L%y%AL_AW*>X zR8;0xqlIh0oQWwyZtU`=%VmO@( zcUa(DGopl8%Zt?HJ<=>9*_B&LC#>eH=r;m(1?sQ}Miq+ERtk5HbFjxsK-i3xul%1l zquAF*TA$95fm2Um{OCk<_V&RZJ%IK)K7>-C0rQm3orZO5AX_>philn9bO^Amik@Kv z)K%}L8RBO)|5G!1JBM7RlmqxH^?Pm4|*ZUK1$(o z#0NokVO^q!uNG`u%L!Ml3CLL4xCrkqGi^K`WS9~VnKaG_SY}PJI15OxL*#+ zwQF1u^0dSutT0~3W_kxxX~L$iU`}svv>+MvgNf^e6|JG|s7v)D%CA1n`Ry~9iJjG$ z5e}6ILu}}wGhh6mezN;3$x@sR|(BZ-0)2e}o&5VgJv$i8X#n597qRPBsF@CjO(!hldN2gkU;0hBCnbQ$}$;Wks5zPy8fK@Kru+fHrc@R=hj+29~*U5X`iJEyd+GVbUKh?(r0+_&#tNJqge zrK1qd{fIgY!=cs7XkFfS3ga^wJIrZ2wMOJ#wi+hT@vZ%bup~O@C|;D|Srl|fL+AJ4 zkVjRu-LEL_`IPK(_6q}$4cjYMHjQiJ)D@dDYa&}MsXcc*t$!x3QY$xfk5OZ!;fMZC zB=X^#fW+~w4N|swo=mH3tLfZZ<<=BlB>tDxigO2vaRtRGg_hu;t?rCyM%^)IzT+tQ znHn~m6M~4me|jENcXdP^9d(XFo1EZs2>5<(;TymXa=l;Vy5ZXw>guQH#jP8c0cK|c`C+ZD|8`D6hUdzn!?_2P5 zc_5sbdolG!X6nb(YhYS);CzmQt@dW7K1{uVTiZNzZ=HUn>pOK59eM<7#BfqCpR8^Y z?)i47A-*za8U6vC3fpuW9;egy)S;IvxC3xKJ{LqsSYml>hD!7g*bPDj4lxbPE6_jT z2YL6>w{T9VjXk+3O@|h=Fl9rq&nS=@g@}XEhhsGvF;VGJmLei^(rn@7I3G{GNqJt} zog+nh;=vIqScaQ(0>bbdHkLW94%&t)Npj&;qQ84+zjo zV|u?up$lq`zakd|5lof=GLz+gAWrbJy*OKE;I-Tm8}E6<`-pdWY(=tC+~FPTQn{xv zxIDHpP08V+R~jfrCQ{-=#9f6DWtTYMbpo_7g^Q0Mub8r{BKz6 z!D(&Q zL7|w!Ij3uT5tr(hw%J{aucO zfk^kK=ae_8YvZb4*YJL2vArVq(#48c7uVp}b#DjfB9VvmV$Z7(L2^qqm>!Y;7dwyV z06{0!fFzE^d)jcsgV9?NjD{nNkUVf^JBj$<`eMauwp4ZIkfP$~;e4Pzq3muUXhmVd z?t}>|GNaj_pI}C9>=!(u(J^XekG}$IksWQSg!8k;7Zrttct}nM<;tV>qM=ms&MYer_HUcwVqF0-r@YSB8M42+yx*qQOnN2pIi-Q zAhz*WmDDN^B@)VRk(imd=QH!#R@v^7IBS|wG$)Crb+%Y;iD zbdN)+vSDp(IJH11n3M0`8PPsg@5C;rf(bp9Pu2NOg%pWt4!$8rOVXt_>9 z&C>gI;0UUON>{laib&_F;lo%_)Ev3OTo0t@pcqjgu3_doWw`VVMR8`qxq}*p#E44T zs-WiP_(AKfLJKrs6Og4v5hFmpni&c^b4utC>^)-=@>9=pYz z5Zs_ z8zg?koX+)3z{3yR$O&Dr9&E|y92>HMS)c~`7~7>Vu=R+|Rr#lnXrBmMk5u!@JGd9A z)L!I&L#RE$(nS1m7cY)Sgu9we0XmNf4xQ>?4+8EDSv?-)fnDv`O4Z}IES9Chx4e;a zVD{g&k1ju2`^=9Gj2F?<+v--bb*o#LOd)MJFU5JN;oMT%E%f|E*ica;4VIzZT?>P5 zYGJrRhGU(ebdIl+Z8&&Xw~*|nGf|Ht%HZl8IJ`Er69BL*P8!Bp)nN3tu!$QC zkg?S4D%pu&H)UyTu^)5tK>aGW*L)y{BY@&;+KJGfk~w{fV-<6_dY4%q(*6!h(;V@B zrS5=A^m9|A?^s6V5%(r}Yxci&Gr9xDOXw~?jYn{50B1Q!Ka!r61t$CDk-+j~5&xiY zSD~gC3JKIB#?3s5d^VCgK=e?DqlkBj$nS0)3%lZvN6co=kFP%Do9pk6KcJtjq5MEe zHsk@bv43`21`Zs1flYKa5S73fWM+p>qQatCCQ7a&;y9Ae_H*YOfv;lx`=8(k=Nl7s zn=vdi!Al2h7OZnK{91dIdJJ(a3n2b7nOjbpC zdTRCqWs{m+z+OOf*nvlfx6=vvcpUWtq!MgKH02eY6jSMfg_s-U`(vZjK7ecY@D+1} z#3f$7n|Dw(60PWor-M>S{ciE;1NH*g8~(-(S>SQIlSn$7@+My@15E$cW6-9^s*x*xnlCJ6XDIN%HHKbU~X)w*?$6N$h`mqK(NUf2q&1? zqN;4i>Ad=UY>+K7=(rZ()_DeS>)N2>o?Qolbj$`cvlpzeno%^6j$F@x&)5v$B0B+m zea1kL$w=jhCUG*E0n{9&>#Jm5nU{W{YSglh#xn!;8_^aOp!#Kg7P;IN)%GtcFwVl1 zc2;w#-zpOmJU_ei{7lveHw_vAdX$1jm5G!0>CE9`l!lm!j!}V|UsY2ZBZ3M&Ul{f2 zd|;7Rh0PHooAa66oJXi?ryQzUM5GXiGlSp&6W2u5MKE!><$|^59l<@nKDGgnn?o@& z`|uwKuLg+!m+v?xA;eGZ$>Lyf$YtYia|2;CQpwQvhU(hiN?g=hv2>8or{&Iml*+(is^_T;^!*2h9K8 zEq=oP$IL>nZZ%ht&KH?B3;J z;8ZH{zJm*Zney6`i}Wn=Heo*Qu0`m*=Y$0N{Iq{aZ-bPCTB8&|Xe&zg_KrwKd{4Nx zCu_$x_XN&NaILfq0bUMb3X%ejGFi+65Qs%o)x{Obhk0dfb@6pvb>7V>M;x#e5aT#J zie8s0et0jts<}=ED3_}zT4L+CR(;ns3&5S=BSY70B(4LAFgsBZ&C3uzoF$sRNW%@ z7fwLgoy4}uOB}eIrWoouP|_0t(1uk%4_NaHmHou))L0aS#-aX-VN*%mH4_=AJvBT&$~Gyh^Fr2 zm+i3e*Rau?UfK#lN@-x8WZd+fnnQ^U+JHLyCe(h#04o?d5@1*FiJ;bq%S$#`u{JO% z_aiKvk#!NpR^0zYo%z6aC|K_8?S!|-uUY)Y=uH6NVi&9KFx3Md4;=apc?Qh=gU3uVGy?4@(al#XDdzJ#fwSe6pM*Gs*#Fr35x zu1JXq_E5X#27ic8IwDH@d;lQI;;GFFOo7v9aQx=7x!>R?<)9K6HFOYY@`;YyQ5;lV zegz1{x^XyBfh*88bJrsckg#wvTj6oU50*9H{n5;^e77RuKw>)TstjQwSxv|kL8V|u ztlEZ!a8be2=-d#|h-IlIhYNGtYcF1^JOdqAFEh;4yHtdW^#$SJkqq(ijV!B%-(Cv8 zy%oDFl*8O)L;R&8UTSdL&&m#Zzb$;nAo&c2@w+?sF!a5jWV-M{KC+L;fDxWH5?iM!S5r1D*$!Qb)^;J_sro7I%q%4+0OPfxR#+X1gEpN+4Nm}I2_ksxOnMp9`bjJ zhr*i0uQ?(4=_J1@ANk7H{J=4CKt6(L;B?RMzAXl)L8h?c9hjWT3PzQo!Ui=3qADPF z?^zAogjE$KjWpz*uwl#gmxdMscX~$GCT5zV=@QkEpj6uy667l`j0}Lw;RTmtVdP#H zuYw>Xl9x1qwWI+NR*{!5bkr2$$kiVO99o3CMOVe#GhWO(O(TL@MEX(KeK1^fOH%X` zX#~WFZQ2}HQVp+e3>(sI#Eb_ZW@k!{|0Np0<}Wsa;|ei>allfm6LoDVs!zKvf29>W z)8ea_S#p~2Gub}Nmc{#S(Sm~{-r0F?JgT5C7lKJIvIr9Fc3#c#W3i2p!aP1-7mG{- z(xP>Mb8m*c=I+jkuxhYFd{A5s$EO00&m-_Kpim348PrKo2NgK_Ft?$hEw%=)8oZ)9 z9Q}!~#tY3df#SdhHgKzqQ$6s80jC_R{VMjghC2!3kFrRFATX|GHd$~4;fzZ>4vRcG z8~%i8V2g1#@nihC!F@!|K(U9TS%WO8U3K}JwRqnM(+`;FUXGQfa#PY6#xHiS0335<%|Ypa(&(I!yBf2eSj3vppqiC+;9wSn z;JLtDH;;hWt(axSb^nrGyS&2I;6?kDB0#zo1zg|?{QXf&h8vO0NGEz5vqr$`=QW9`v zCcKZE z3rB9#__=x-%$rxE{5@Q=_|6iR)WcI_8&YAnaLjto>aZ?cy;KBVwTLS2%U>(@_4JAM zg=UuXPc^(+lN%(*V34$MgM?ZmBm!Hc9Bh@cHS;KwR{*R9@;*HLs`iG+2CQX8t4o=) zvrEVx%-^FtWD#R{7LiM8Tjo@W5yBz3o`yK#u2s#&na2^;`4RHtVogbNL!fLaTsDxs z$wq@iC5(o9*%hq~$c4d*xR+h?-$R+Ol+&|SV#^)H&%=-JOm8g6=|WHtTmy0G@rUiU>UWZ*d0 z)@3GiumNyTa@;)p;#Oe^p_KF%;DwB!$MyA>g~HUllpHK9!nrzN9$->g6m;PbTWD`Ls7KN!dSUj=V z0#jEQ5`2h&Fq;PcT~6(6PCE`vSvH#(#*+)ugMK|FCm6t`feuQ(xp0e1xf@$5h<40a z_bxLTAc9aq2Oh)$U_eStJdl)A$HUi!Xl|wh?j)d z(`#VwiMJ#Rfla!JhR6_bG5Ek3`@7d(#n$n*y8&J71!Ng9vETjGB9pq9FZg`C^RU(>>Ra|#M3}^?}bAE~#cR1!yB%-+nqN7n_R>9%o~db!L}idWMM zst(md4x|j~HclLeU zgTXa@XyDAliaW=q!MQ>_Fg^`vIhvGRD1|uS4>Q;S4+)B`=axY!M36itPGvaa_+ zUb99AcTwHX7lu-&&jshK>KM?Xaz5eboG1a_2B&Ic2&!Mp_MO*?Kyp^d#O*jT2832D z-=;AR!m_pyB)Om~y4b`g_>G(c*=gCDuig;Ea#Rt8$3*SZip{~tmcJ40;RUL(q9lL+ zh$MXWLRpfVTSyXD-c^OI4S{8lBsa3lYAfR_S8?TC4R@>p!sNDBsf&vbcyAGaD2G(3 z#I-xQ6>SEgau?N1yF5bRY2C|iLh(B1&btTVik%)Yu!Qf!_-&48T*4K9MN@}N3-vGy z*Y-EU1b9?bRo^Nz_3)0w}*rk2b{pbCCS&2u zj0SEOS-5<`Lz=_iW5Wl$&p!*4C(Q-$wqSy9!Re4cVal1~oKdG>Q0QRY1&Uf5a^gS0 zqhaGekj5)aR1Ivom%_yKK7`XiasI{q6}Q#mrm&!pf-Rz(d*oI~Nomw;Z%)&S?_RC_ z0Kg%CvOE=MRNoU^YikwaVn`d8Glfk92?Z%e3Bm#H z^NxU5Yp{tPBZ&Itc|^HY%Z*ojgN?~?8irSsBa)ftE$BL+tk%fjSt+e0pa zh9Z_O+lLJ&K0~<@`yT$EG02oE5`0k4DeBl@w>|W8)L5v(}hY#WAP>U-RZr+zY|&k_tJ=8_2<7TA`^m=dUTKk z5co_o1~bYg5l~W6f)Z`cJMa(h2|{F!)ToTSHaVOMD^SPXJ&#U8fg>HZn*6u=fsS(` zAW!^?PP8Kap~(r+S~K=lOKMp8V$1#xb%dNp0BLT>Qt-)0D^apClMZSiuOC{Fvb$Qz z#`d7dZi=ugc+r73C@>y=-^;t?{q`q?h0&2a06aix41U#kJ~4at6&==s(fNvw0ow%5 z#w^Dxnkdq?^OD!ueRRPjQg(V@&DN?-HJ+uxt;(eA0^zdhfCgO*<<5jw=(glk$=al(+f-MSkgZa$QJ`PKiy54|8)5*%0)9 zF|eo_hzX@SbE<+-PiJf(%Sr%nUZ6ZLS4ql%fH#ErsKDq&dkuhqux`$?JZkAoYN^Hq z=VjZA-7L?{1A zj@DAAJo08ra6%B&TRweh628pDf*i>^oSUAbq&C|?5QG?X-)Edpr4=bjT{N}$0{)M5 z_Bo!};u68^NqOHwV)nS48kqp&O#+B%2~L49{uwcQa$#5Rn*;zaW*=PGCGe}mmZ0#- z<|Kds1|T`#i|4#a>Y1Ys*;+d6Gqh^zwsRIMpnvM73BBllBRRZ#Htu=%Y&9FF(tBZ7 zd?Sy&f{fJz*rWy1U`2Q#GF{+E3a48Qxue^drH68>nqN7fd2)Lvn zceD?Mh>g4Vt?;I5NBq7T6@$BU#VRo+36ddEN$rd=1YNN38J_Uj8fi>jj|utCfJb8X ztd+y_nz1QB=>1;6%d!PQ2cJf63QRZ6J%fq6f$3(x%yjcIK*DrMDgged;6f;GQAGmz zSV)OXj!ul=k z+cgj-b4iOmE6K!OG!3;NEkmcH;=2Y>g(-XRDWg83p8{_loG?YK1La{+XHzuEu*_rC zhJYYi){EGXTom`@Xz1KIMEfxt3Lr^Q`Uc@a-I1PMC|N{#pxNJD)THtMkM+w2N~cw3 z-FOMUt_-a=>=Qui1y&~AP~MafC)Xp$cp3hU1@Kw;-}ObVTyQqa4cpjr=dTS!hzPD$3pMYu<=4l_uC+zs#KoXUYbI7w+C z0Mp9I8U>vaiBXzy2ulX#C2S=Wd$(yXz!7XwM9r~!rD4(ly&A`pY;n<^Jdu@D3FGZq zV7%Fo6Ic>mqPzyioBKjcSU}iYu?9Ar!RLv$|kuNCA z3}_HYm$LLi83VEKRBl~~YuV;wez~zl$>j%?&4&uBYt?xvxe3{tgq-;NxI*Qs?-GCE zL=|~C98QVj2Ag%iOs!prnf`(v;nWfOMG(_3gMW^{@RxxaURObf*bsfRG@?%jVg%GT zLig$Ppqpqop(`-p!0P{G?-E_{E`5QFlRafVk-o$8`w&)pl@8yB786KMOz3~m{^_s> zkTZSwy$jUVlbH2hkW?4+!DPK;t9Utz^BYqm=+_~ik*F;w>Z*)3&B|c;q z19gCm2sK&=kkf7*02^mxqa$-8D}ZnCiY<^frj4McN*oK8M4xv;ccj0x`XvR=tZDQ= z=utd)WS9O(&mp{N&?BNRY|-UV&3AxF(X|p?j+;J*nr_5%PGR>|->s*jEvOtUk&L86 zOo*=HBv9QUMlYgk%s^9fVw)go$=8&KypEs!(Q|_(;nmD2sYfo2x$k2xxtbZ0-Jw)NaU}o%2$i^93cKWfkud{RJv421u*0bM2&n z8FAF;86IUKc0;!{L!+OCzzadLIg?#H>bR^pHnA#aPg6oy?#2AB+UnX0pHJLtQ=3v7 zGcuZv9FI8amAkHVZ+1evvX#y%Bke+}Z%okA@Ga4iVc9#Bci3&!zP!NeNIFgSKqE#k zv=x{JwLn#W3m9h8InUr2q#C(UK9izJ0i2*Ll!H+*H2z+JK`3Wy&K}AupblsYK{y_Q z*?{m%hphvqvJ90;9hVR`4k6VFWO{2roB9E0Q~iD2IcF?$q_fe8Q-E?+>_jEtU^X0? zdt`dC>nSi7$^>z91=nrUBj$2~IFoK{dX3H*LIQA)98R_$|B-j?QcED=*c&XNsdmIM zXL_T@HU1@hRP*IgogsLxbn?S0bhL)zY6lp~}JJC74dOK>l$6niDl2L(bQnZf@;7x#gTI9s7&~ zEaY-sC#@BOxU!IW{p~)O|=*r-;5?m)5roW6P%h($zDKYzJNaiBtLS z(0LUATgG6OVX%;U(SMG?3c_H8V6bMr{<}oeN<6G8e{9xl4Z{3^u0(}{31Fg3r{@c z5Q?w|or08nL58oJ22_SYGO&>Jw$7eIt-2;y(==+-cwGoN8VzD|9!dxY%TA>3PDEwI?-w4=-_c6;=@Q=(#a@vppJkj z`=`)fBfYHi=ZO8iU4*&B07>{weEsV{&<9K^9s2_>S+P^d+=pU!MLl6YDFB&RLy)-k zvEgmk9Hs};gu0hFmW_I(9XPWC6sgl9peaaGROeeZ!vB4ZTH2EC3D73qL}wI@CgI*8 zgx!&@5(Mh?`R)Cn%>b*b10o-1GUM>fy@LQ!6|*-eXfvITue5W=-QZfI5EBx#Zxg?^ zwKS|e;i$5VBNGz@b&*qf#N3Iv0 zuCF{<9dO3$bV6G~>*4&BiV;Uok$nUVTy6UoQ&$`$b{q7hN(2K;ySt0okI;4`tvwEjBr^L?LA&XZ;pd z3wX`Op2z+0^y2}G6IgKhvH%6gp+V338sIs`e;-~cL}-cCz1;#RkzNQ zyzdfhA7zKu_M2u12S3m^V-9-W{xEN!vakn+TT)6~GT0YF1H9t5M6Qd73Our-Xw!w< zbNS~BZdJB5y|VpwNK>5UX7L~G&7!JmKG%)yDGIuY)ji&o&CQvUyZxl4fKu6~ln%T5 zEr^?+07~Yl?Dh2VztD)q8H6Qg`51*7e<{Ci3 zLyxTFBZMbo;Y9e@Nmydg@#zx;bJtIxIlv$mv*nNKKqQJ&>QF706PXf%U_$7(D~@U~ z!HVF|kh$BDWXY%j$gPPXVoE{4T&Lu>q)+iqi~L#K@pS-i}Oxzk}*fgYeT6sm}c zo)48-*6xRT#nl(Kd>7=2mB+;Mc zSonXG)199LWrAswSS}t;K&D*z8+dZ2bl0+<_>fNatir%59_S%=#(~*nJ!{X(E2%L; z4djk+BHKcww`wZ~?@L+C;3Lp;#TM^>lCpupSunCGSZ?cy0N(D@`y9F_b6SO!O6s^+ zC3WnANY=2?q*4m+fQ0sn2;=tq5cYw!rayOp|KL-{5)aM-h|Vlsqe0{E3auRO&!@h{ zY=8Nb*i^X1X%>DmuDC-dqw8)GANC^tvy6-w?5F7der_S26Cvx6|JFklncgM9{4OLpBOH7bd-b)0>3x=}?^D?G zKF)qv{3KVY{tps2714N~vGt(ZqW9VNkM|ko1Bq~YExiv}f33OpWN}@wAk(GXWy99| zc9$s%TH527aUEiEs2}rT=s6~8<{Xt;u3%j@pM+c3?Zc|j2(4fh;172OX}b3zMfi(qaHtit^hm7N|gzMD+4TyD?%wJ{YW} zJpyp3W`daizwiJ(x|D;)vuU(Vq;!TsWz2;88!j7n5uk=H7mIvm`*Wc(XoM9W1tMb7o z8M_3in=Cddv-kvBv}0%Bznw~HPcrUfM_7cHH6vz)IL&SWubfg~0WnsE64-hpx{sY)frTN!=;% zbXn&Bp0r2vuGcR8Zq4fT+cs}Qvml?afjskw-`Ql|_b!vm(kd)(kb36>Qtu2)+8^y1 z8t5Am8515I8rAs>>l^|>oX&|L)Ptm&=u7=uASo6fzXon6H1<7aFP44Ey-Q--EFY3) z^dV?!MD2SyfHb4c&9kIsLETzSU00Y_MT8QCYl?qPib@;LdQ@HOQB?8>U$+sT5r)$T zl9xyruvXA?RZinWseHsTC{EKV>hmkRrQG0ycflNjuOjPHW2SKdGrddFR~vAJxYb40 z!nFt_J4?ap3f*tlf2c{L?StB&1hiyu2m1Siijfsy zVd`=&Z8IMEKZ)Cql(-$)ZaPA?7F+P`lA_jzu+qh7C2ThCmAKO;TJ_+(Glu$_lDPg0 zyw$KMmIdEuhcwp37RT0ItgD?ZQGmICyvkJ6S;4K-aDMUz)|F*ENHHk4|D7o@fc_R4 zw^8oJ0VtbRYt<-Av1+FlDCEAQAPuQp9?3rX5A+Sh6=ybY;=9gxwZ$+*<0hA`?EsF0 zxrGmFH*M@B`{l$(Zh-yZqp^>W2y%@N`yaW!GFE(_MQB!U<^Am0-zpy1Hd6)L7nPb4 zW4I+6H3|TV&V6D&vfhs`#<6um59|J4U#9PR**JhZ1hDw1q{uL^!i^IeVw2{5zf_Up z3KqoWIK;$2Ql`{k*UnZBafYlKbcq2U!kN6pl>1#)>{SZ1z`fG`4>@6QS?B z!nnwb3jiHOD&eb`mf@_NXQXk?Ep z5VN&KQnG%T2B=|vkAkKUY2^gmxN%UN08HmZfem-v%QAzHMb{)f9?9n#Ur^s};I68` zyRxQZPj3Tr&}T+QX12X_XiFsIS;pT>k{# z9&Bu2emQnQ*RBi)ealB0KIJK#9w3;Rx~K1Y_T?^a2}_N@q^<*C7wyWa`Ai)bHSoE^ zKoT)ce1?-_KVk_aW`+<1DIC{y_QW>e_ys-WV)*OzbXf*$MyPD<%Q_v1m-f@QH7#u6 zyAsIOtb4+0jjiea^>M17`75@DK1sh{*rnF;yWFmhp`*g|Gi~^p=rvcYWk3yu=+NWb z3yWxP&t7%y0a@dD^Cey|2+`18gucV0 z(5z#l74u)m;EXI%lz|Wj=mSDzDBnw|AgHc*bcc!36p5x(-QcxkbryPG!}hvm7ZY1j z-nXpb+K=#t0~6W2W5HDk$1Jb#fd%ZAln@%ccaA?F@*UYWRC*>`BVzBP?J%y`q!An( zAQ#!%%9jsET(!KzU4BA6guA?sKa1J!9lbnceZ+nQw{mAA)LTtYm~BLl)u7~S;##%= zILMCA`7ng;W4~iGj>I)}V5LbAG29VL@i7Ytmyl(m0>^FEfQ-;eo3rlB;q%%56EA=- zeX@o-;#+hc;{%!~#@hPG1%EQ67sAdn2s?*7%S=(H`HYX4QmomYvzzD%(52XVJi7J; zkSq7&(-vT{7*|ML7!M>Ji9f|e?sSM2(BJ_eD`YTTB-xA|21x^Mj~(~jP1o)(ksczQ zU9kEc{KOT>V`xY7fzKQM0A6A6*EnZ|Dac8S@W3IL{lEuwv2A>yEmETpzy;Dwn4fYI zjnpS;Ren=fMf2L+A)6^)*u>hq6wK7(V4=nDeeTnQb>8zJ@{l z67HN2sb-gII$D~~oZWbF2j{Rh=9*Z`u8V<13B{>}8LB`h*l0?#OS7+4T&+ESqjk%z zE!Xz)StIabuoo-V_-)`U3o>{*P)f{2e8ioGvj*?h0FtFt#lQGWxFrL?>ds?YLj5oc&`3D%o`9tg++q zXs|1d26x#Yskz``>X5Rqo_mQ2KGozDvJrdCsTE+c#kU_EeB{9z=XuR(hyB zuF~?rk<#Q^J_1Xj{8(0QiQ!UpM=LF|OFLS)YZ5C3qFMq%@y#FwJpiv;7AQ&{+xdEl~?*o6B3(SJry4&_uEHo%_+~pqh+tbiQni3a-2IY0W zh}E%8!Cc-;LA?au`@$YBzmEX-4Y(HoKD$!bww}cs?)?JN1%z$NuamZB2>`4kc+X<+~FI;s!jx6}M6&NGpmUo#7b}3lR$= zdS-Uw$x@^TO^Ad7p^S=Q3s`1!DWGNZ4$k`(Fz_9krtneQ5Muq`2$M+S#iSqW5s3Tn zQQxuX5H%DBG&q8pUgU1EczQnFvo8U;=iM;o;n#TtA`fQ?xV!_FcM``8@%ObWqOm~w`lJrJDZ=ja^} zx7@G`8~fvVkkhnaXn{g8o_Z_sW!~X~FEY~{Zl8uf84Rws}Nk)z%hgwHmk!;(Y}og{HD= zy!Ril+8|)`0Rf}2%?#ad)XK*8zaEOw?N@#-O)5?-GMvuNEw0gVhrui`FXC*XAu%F3 zVxMl@nN?yG>IbO2iN?;YN*H*cNI^wHnRDAl2r#V0zuN`l_Zclin{YPz@VtDlW=rTu z@FDNx!hMNR?Yk|i9^!k5>m7jFfJ)^D=2;Ypbb0zU9C7US0+8-fLHuW)YDA#QI;Sg;FG^#Lna~$2LZ~d8(>TSg6+Vj zn}M84l7Er-fbC#jMl*6ex;agU8u%p2gh&1eYzI`+q>P4eKC5dlOVm8irG3 zr@&pjj!)e{e5F1e&>OJE<>OYfSgkNO)iy?zJa2%MVds-5@AGlM>L2^Je*!x&A-VMB zhl5{QYr3oz=)&TY-(q`$;`ba0LL(rwd90si-k@@cIUJ|Yh>blIgCc^2ipm~oMLa|Z zb3=*n%TM88n?OwPuhS>r$a~fDvXm4O9lp!wu^=-DYD?~=8@emm8Q%L9b_Y78lMz$H zu^9Vt4T34Zu24{gd9G$3b1O=a9uG|{L1ljryx;`(#gMQV;mth{`Uez7#{_*2kbry ziCf=HCVW!)M7T7`_T$BGh-b|#2Ym8VcuS`Z%=Wu-KKMg=+>D5_2sjE-DYu!ZE6hP% z0bG6v9zjwED(%h<*fG_x$G_v%7WaB|OIOlqK5;lJOD;?-&?glXrX9t8nEZepGaW;S zKJjJtx6BG1sbVau=nC&Yn`Pt=;_@Gi=yys;yk-fy`LQaeAg{(}A+_p{y5NTvEZ6?? z*sNY{^8!FF8L$TR7xf*(6j-=d@L?F%Cb|UV2g*RuRT>8lM`3KSkPtrVn#F7`N3&nB zi*z4~K;CCI`)sE4Sb2B*PE{qCj{xr|ga%UB(AFP8q9CPb9n8wsz0RRXWsoVUHHhax zT5M1E^0^_nDtDP^MjW}s7zm0HxG^pR742slVuFJls@4vVmsVXYJysZAxvqXM5H;L4 zt#Zor2^fmI;L4%iN{?Tp$Efa)Q+iDY8O~UwI#y^E;=^{6r@ zm6l3GGnR5uVMa-EnWZWs$w%C$SeQ?lgZ~abE)Vpz^N~O%NM`C7i-IJwtj!pCcwFKb z%XvvJ@0{s4Wn;q!DTdVV4t{s&d&~DS&NqMN`0*vLxfuqiMGrrP^l0Rhc)LE#R+&%V zP@0G@cylb9t4P>Q&e-GAenu+-*gN^4cBEhx zlv?1I76TgG25lt0l~G#xys(EpP>(WhuJ{zl?7J`&CI)U02kn{nx2iINE(*T)2!&}+ zzrPJP3iS^JS1pwF+%r>kpt8+>!gRa_GKKL2%qxbuS zSvyz7)Su~G&b(8-XL;+lgvI&h`xWkJNII2kY2Agkk`WUasG5El&t879oLy1IPhuB8 z<{W?c_VtBSrBDURu26w>==bm~FXKz&685ak>e8C3;tD>Et~gc(0y(XCDo3!QckVn~ zA*>?B6XtpBi`f@c@rw}!itgZtAWD&qZm49X;;W57cj6pRJeXyTRSl#jP+Aoiut(ku zNBS=NZL8N2JU4X+Yica5EIbKI>GTd|{!_ogX{VQwTk-)Wwobu)>>A4MJy;Iez47do z0xTdBi1!YPa!=52><(h*Jo#LN*4!ZqU3O5R4fgTM4S$t{;#a{RwX@aS5X!(xMdP&> z5kK!zEF0ALS-FTi*ww*;ac3uM>)!`uy+AN90@*1y*s`t#GZ?PpWnY9SBboI?_SV}g z*utBx_%ANFvR}T7$h3}~u0yl@r>9r2ZFh`lO+3@}9J{$*dQ`#Ab+w=@xQS#)(TA+? zlX7@e(%vv#93SxlE-Ifk77iC-k%Ue^OqkV_sTX`8tTdr$;y{#^PsiueDtCase0Y5( zVK8?HeSz5eaA|2ZAN2wYLZhnRKJ3h{W$YVw(u03yEa|~x`(Ybv@M5ihRxr4SrR1%V z`xp7~S^ZgC`N{ICW4Q7!vuVe|ijy&{V7$Ywww`J~dAe>#+xF{Q#dQ|Zx~&eg&f~9O zYptQoz|;*vNSxw*lA!-X z`xsqKe9)I{yXU@@(9+t#dv_vwRlx!K1_1V4Gm9DfU<{Hw1fwNlAR-gc3Udn!wg*DU zEl}{l@Hz1Y?NUAJTQ?U1kv##C4pq+XG5Rfko-DsCmj-@i^miBctw?(q)jI7Utmg8=Xh#i4p5vlwx2Bi zD`^6KN;2zc1~9PhvIiffLkk%s#@u1O4P?EITTJ(0r|z=yq1+AkO+p<^gQOSs?O*U%*WnEV1@$h-l^j%bSi9)aE&K2cpMCh*v(z-N8g5 z{NJaMWobv?ebB&F~G1|j&= zcElaHAfTroF`bqNP44rl00ZgaI&_{^Fw@wg8y2soR z{$%0$zkzZ`FnYdFN2%pjQS8~Zx0Fa`O*qRnXEts5Cs0u=WPd92;d0EniGK?aDAwR` z(xMpH)^&w#H$G%1%JWY9f=iV|7Naz+2g)%l`CK=y#mab6A|FN^k_V{w310%PO__`E zaXV6$NpM8Uq#7|tI$wOOz9p)3g+cL^79I$I@EnxT*E8`Aw}*i#q%3h2K1r6Q>%Z9K zC5RQKGUeq_Kx_{lOi$OptISkO4WFWJf4!)(CFujfIPvgGKMl3sUFe8d` zGgXQ*x}MKC%}BeK5BU)iLfClaU~lpD@hAaLi&*9>tqh9Tvs*6%+GfNnn!D>bKY3iH4I*O9xU-hFvqHvn4(%OpY$S72=$T0Aodxz&O~@2 z9)fYU3}Ov5%P>N-G@uns)D7v>NS<0#c1gAHZ1Wc;fhK`7S(6qXfU$lV)3Dtl+)|qXbe%={h!TOe} zRaO<=-d0iadb*x8o2V}@8zP>j*RE*0;SRXuI+FIifw%0bml1~q(r8K zW5SIJ>YMmTWLbAq0-kEvSCC%%tqbZ}r$ zQSfzxafR$yBrUJP{0$&D4?a(E$7$3aHv=6O0(A0`Za95ho zOfNm}_Zps>dXyPJFUFacHJ3J)`WFRq+XM#UVEE(1$|{xZ`irqF1l-qr2GF~?{b2}@ zs7(X0EHpAebLbLQAh^Vo+8NB9l#x~f zQiuPxe!HD5{2F6o%6l#h(B3M7_RgY(#ZZg)SoU`zsGal#`K&?~zAq&flXZNMC0mcE z5h)e&yWiO6eS4;a>NmzF`eu7&`c;4mSJv~n7VL8Qjke~~Cq3U? z@LAlt*p6V#$Lr;kIB>RchmV=zRcIfPCz{%Y2tksoH2FvY^YmvA=akp#8sp0N&^|28 zJ0K^tBiSIg4pVPWiCJKhl0}|m`_aQ9Fhbb=U{*w1EY?rJ|DB071KMdbbrABtY&7=7@X@ZYetdQr z#{7r^^l+oD5fq>hyMyt>XP1=0DPC@4b>q%WB`v&c(!$GDpH}oK-qXkF#&bzXh z`|XL{igd!}c_w%Z(!+nsej`;G&gy+p@-tBB7A*7JWg?=pzhI*ml=aw0bH*e6p&rlv z+MC7|tAWd*=VCM;_#X2l?M6QOF1l<;C0)IzlmF;xN_%1(m=69Sq>kzpUShoi6O{5l zg`7LTc}%eSF7Qb=m{QTJLky@Bdd2<`Vp2ywBpCb64czv8q?amWSqp=$G2X{;PUqKn z`2_Ju`;iB?4IqCqI08TK6mXq2&^E+BK>@)90e1{CS*cOGeMT)dHmdOo=&Njxv&{f? zda+I`@u#_Jrb@QFq)N6t`h+b~8oKg;dqN3Jyd<6?o3fhC`xm!940T=z4^I36+Mvaa zt)(Z*1Ij}As23TK~=zQ<8=v$Fg7FS|8la*gmL+!%| zA+|Cf;Rc6ThS^`KMM!1!-lR;e9Yn5d{?3>JM=3 z_k^|mCHZ#NiB`i0iI1LO0<;jppg;%?TbTyjKC#9a(z%*-B9Qh7+My>10C^W%EO_7_ zI|P5WhqKCP8O{e^U{Fj+{7W$fqx@*Fzm0Mu0Q?h(rMTt`m>Co`#-295&;Er=F?$Rn z;X;G_9_W$UqN*2=zz{?Ux0@NFgoEbBVQ#sc)o{yxEYOE4Ct{77^J!baY2A4r1BwUP zm)@AmHd;Z;FM#<}?>}px_U8Y|QGCEsVKC-WRS@h(VCqFV3j%(F_^1MAiUo#vU(D`$ zi~YG2>=jj@Xr*4u>bt7gGTq>Uv242=-Ai|m2=D_e<$dlUoR#G6x8cr>zu&~&7=?C9 zL@y8v+`ajP#$;kjgoaLlObeL{0nHm##%5JARom0YjmPh-+OTulTm^hV2<5feV zl(A||7wEr($6kLRe4jtJ|Culfu7y=s>2gmJU9Nb-4}7_8DXl50e#=p;NnOn~3UuED z4;O)5Vv*$a!sLia)SMaA`7G*#k9C33+@Vss0{y#)+m7BwuAj$VudV<~0-u3;?tNrl z!8F5W8^X4nc9$PvkYaO#Xok#I9fCq*dy({ZPJQF4gcA!5E4T!Vjq5)&SC8fM?`il* zEYpv~G5ED#G8kG_5akxBRTx}k3FsoR z9mBPfPT)7vU*hVhi4I`NM~b)$kOXe^8Z&Ox-1k;GnQ5c(bq0}u&Us;|FMBK_`N-vs zY3KCTd@AZ!7hTMf4F*2Sg{3Iv7DyR0M7afbh%+ZAf2Ss*x1Y2zDIp|;&yoN~oZx+4 z#=n}qjYXjg<;m{3>=u3xB6F!MLGAq!!kUSvcYPhF-;JO~ol*V9IGHN(M zBhBKFl$u+RcEnLm%gIa2cl;1?Fj`!oto)3^QeCMSVpa?`J{CgtgsvWt<|Qn*F*Y6i9=ksVtY+TU{53(O@YCl_Ta} zaI!+Uy)0Zeh{30E(h&rDd{!@NmHU*SQ6#O_TGLpKN?ZqkPj4IxUcMe`O*}J~q|uCX zNeP(uKEM{9p*S^0>4Nj0%_w2iE7v*$819#0A0*ldF6^9#!St?q^iaei8i-XNeS(w9 zCoW8L`|wey#ygLNQ@(qsu%=m8uksrSta(45o^$Hcloh_#eTONfU`#M;IMN74Igh*`#~%(SWtcBQwBcBf8X61VR1*i{Y!8v>azFL>V0OO)9bOOCZ)Y zRElxkXK_&heOVRK*K>JOEqF~MMtb8;c2Ue$0m!G}qM*FSM};!4;QgDx#8N8sh%;k_ zT?QZhDOhJF$el;T2(3rSx?}nt0iw=|JrwJ!VN3?%(6>SLhEmcRno< z8;)Cl1Uv+h9q-o&F`FLJUO|dy#{cpeH_R9e_6P3=tY8?&?pUW9_tkv*F4{QeXmMmG zbag3KoJZt-k1fAy_5~aOSNem+@+Z8$-|X%~3}yWaJRUTu`z;0y29@0M76`gWe#qXf z(D3P54AO6FSmgv}{Ru)5LkV;D!q%|nQ~rgLr~^J5`qI#vgpg}ho_H7O!4^KMja6=h z&=8GAm3s6@ih5JzY^A*_OE;4}OLjQ&LCYAZh4{2bbSGLd{wAXl$1`k2n%=ZRx|8Nn zwjGmKI7dA(o1|+=Ug{_oRzAXk6_W8wL|ecdY497JI~IM|FSv{isN_jSNf~kbbzzCI z;JM>dHnQtMN4)VdOT1eTP}@mcU1e+a&JI3jJ%!2BP+(P8jp&bzXx?Zvi4SK}H1Yx! z^Q#)mRw2$m#Yo=+z=^!MbYDecm1U`UI!>L?ad+Rp+`wlTne2=5-oV+c4Tx%R{+@Gs zJ~J9lDwp^9FKQ?I5IZkegV1^IST=9%yFdl0K95pzeN9V^cfC(bASuzv<>=L=aOOU& z2%m`xFy;#7n-y7u%HmBnG{d5*uzn`$`*^PB0mnwU*CVfH(d|qMY9L zFGWSL!_+ZT^4Iu^@BHV`dmrd-_K4sVbIq>|oLI*|22nQOF!#!z+~sGcpq-4{g#t+a ziK&#^K#)bR*G1oE2*z+W2^71u`mlr)SIEc1Jg$S2Op|OoV^N=pDZnSS-3#&Kh6o}C z6jMGa6vO`JeL3;788N{GniWU*p zEhbk^7mrK2nQd8)Nm1Cdej#-f}&`PM_M}V&L4vUMO6KO~TuVb4D}(N-pr-g&Bp}c|}J{b4v?ri-Pj^ zm4wv#HKtcq0AtayF8Kc*qQ`DNCx&F#@?qVGDjyJ^g7P7ntG<-q?Lwk@x|F>y zf3_-t8LgagdClD_dT#S8KB>x@>kT_7qobt1VyA6B;|ZmJWw^X&3TkUl##e7Lm~?!u zH}c=#nYFtzPQyoKGnnGtdmzcC?B@;*us@GFn5jS9X{ZNo%MCQYfsG7T*s>Rk8@-sx z8~CXHOy0q*FXE)3jaCP!e+7F{NPGVX5Yjr;HtJpfv}qUpuZRIxNn8jbytd!I2fVyj z_gBpJgZCZ*_2M@F>FGuBrg;My7UI>{x=x$BExF}dX4^DWStjOc|E;lYS}Yb##TLP| z;qOOoA2xGyg3&y7H$>*#G${vD(^K?{11zyb$h$5r%fkFSGczsIQGn19wU_`%3qY*^ zgL4Ku+~Qe$G?L>-2u$gtpa#4%o@?~)I%+aDz{Gz6Xt^1kg7vw8Ke~n}x zb=QS%jj|R#S&WoDP_*S!oNz6zG5$pDZ=yl&^k*xty!H)bRZV}e@5H!zG5++;)!b?| zzBAdNE2@8k`FR@tjSn0R)J{z5K+(evwBf>XjTqQ2dP6BpC|na-sT!Dd3k$l=HAa~2Us+b(;oS5b%cP?v$Pl?y z%#0`DVSwv>?hFKexj?6^HE!!%-1OJ;%jz0qOSnT9+vn|<4>lg$@X3GT89rkVe!h`Y zUe}NSF(^~Lv@9WPmtRgO1cJHY0kH%aZeu5*^3mLM98oS})O-G*mG_62mNiA$`A0ML znMaNts&hnZQQfiBq@(!5QC=*LiWYFqQY)mGE(Q3FkKr)8*hLp< z7m;RTU2DccaKa!$^C%97IG*iiB>xRM%&90d`?pL&Mfdn)!-j+=w_mNkC<9$XRFB zQ-c=#EU*zBlyms(uUG=NLj(wa+v_y9MJNkN>Ndd|jkLdK7O_jB4@Gv@`78trcyL?T zZ6V=1sNUJV1V%?BBXs%_pM{b?rkl)R3vA;oRC3fM@32wHVrzv1?~ke_o#9h)xqH-- z#aqy`S^{S&W6wem${(l)@jTTkFg4E$FRgrUw-leAke;C5z-^l_J)tTkPyWlVlb6@y z2<#N7m;Gzj?-#%2&e(10M(Hs9iB?&&qwa|fK?F_wv_zvYiU_4GgN0pz@dXC)`efQE z+*t>*3Xw!KbkAvxt`z3j-uf6`v49y%y-dZD?0OMthJz>iq9wU=H1C7!z6$rr%ilA1 zH`pFRAn+r*d+L35=WjX@UKI`oZfKx;rWXgj?N}YSM|vrEe<-MHL&C#7J<|gUL%JPc z`5yrXA7VrdOY>(QGtIx<@SD%}WMu{Um8ECpS3>z5Jxb>%kvqfWL%hj&g)N^z#B~1h z{rp<&pQ@z>P>(N-4f2NVdQ7G#w};3CsOn(9l|U)3gP5`3q4iARu`bYsd+1 zikc|f@Hr4hKr~ra<}vZB@+2r{haabs;Mb4P8h7lZ$mF(YUCadNGw(3AFbr(vyFpu7 zSH8Fh>gLF_=+roUYFtWG%051{KeHYOy05P`jqgG)+}kBJ<_2|qFwh_T-u(($`ozS8 zP%d84wUPZIp-a~0I76u)3IUCzP~6I;eddEwZv8#&!WrH6Oe_|6Y2}YX_^3OeS+nlo zmhGs748TzXaue>1)vTLz?m2Uz#R?pZnKh8V0{r4g$qMFzcpqQHIrx@;%fxGBV3YvR zos<>YyB}-h8OIRReb6Hv>Ai?sf5?;^ENB2{ahvm0;0#j=do{Bn117HIQZ}w!>cm6T zQHZx#ti1`A$kZv1k2h&Q`Aj3HCnJ63hHo*(1C%jRMH&At|D2`r&p-O*o>JfmkDaW3 zctO`l{d42HVf@Wp@KhO+HnP~Q#)r=55nDT>x}&)l1T+FY75^BW+I|r$L~SiHTw(U9X6{_+v_{WISK{&f*gzjo4AiF@l)=1TAyhSh+A3$);FC`qPFj+D&O7Kj4FQzu zhg+|CcC))WCarnSeeK3A@HjK6jRG+6BUiLp;0RO3G`VO@qG!;#fyY9F&Jr}kt$m+? z2in{g%w6s>BQ){s)eG&g56FeL4;HsMo8g1tdkExjFoXPF<`y-uk0Qh=W=bA+coR^l zYYaE|Vm-c0grVjf0l`(Ubgt-F&B@48;19vl=7@0M4`4fAk!Ij?wyr)_W2=x6TLSN2rrbh=;H{NUzprdnsR!PE^eZAiK5GUu;Fm4% zS1ZLv+tRdCLk?2q2&b+X|S6SHz1D#l?c0*0WTAR$!`M z0#Bt305q`yVt|Km!&pix5JoyTkLZ90z>jVd0}??>%jXQ>GWoe0NiA0jV<_D0-ip9oyt@61OLu)_C0d=^5%R48lb+`AC{TTciem#9T zzVwbRk2^&(5c95#45dqR_7EBWwYKCm&UPY>c^@{j8ZO3fm%($h2}Hw)qG}Idgt)(UKBVp$R@a@%{P56u0|T$LnL7BGDprGryXAT`eX33E&eYNl zZ1xjX+H|VAKOh7DB3g{xXk(nBVwlqz;k4XjM9(#b(T}*hdqiVJjWJ+<`fK=XWLDhG z7oA186V6_wD$ZS2F?UrYStC>EgNd1$#*dl&J$LyA9fjO(8d}IGQyI}AMtuJRLD3U_ zKm-ODo5HQFI36ixj-YU>*BRb_1i(4MP6&5{LQoZI`wqhGGH~z1fJ6HetYpR zT=p{KJIas9$)Ke^|NK@l1go!^TkXfBAE5=Jf5*SS{f@N7F?X~~vn2@vS~}0dy|?x9 zEo3Cz&X>g=hts<&#n1KeowV|CV~eu^0W%=`rw)jrX+moF!{(kC2$QfuNF{$h54}Qc z5?Pyh@NoKJz39Y!n-#z|<}^k%Ia=38HqX~Dya6~7q3^Cu+_^X1Gw!BgL3i~?cOU@l z>9-GZWQb*+M!1*5lykTm8u8SWKNYQB`9M+zO;et7qOAsSYVw;;18U_CHmt%oeRhT` zc3U1hD@I=wFcw;$99%r(80ldiaHcpC7w66&u#)*e1KW=sxy&65xN>M-$(oFd<+PkB zSl(O$E`Jtt5azZtFX;X;=>Bmi>@)JI?3iVZTJlh3hXI z?l^k2_zhKc^n)#zqveX?`lhm~(x7@TbOp+lyk7=$ljbG|tj8Ccxsa6xHImNLagR%T zxvQR)tQ*da6k88^K^Bb*?*HTThvxbpq-*fU)6Ve;+6<=h#N z3Xl@@_dO4QZO|^_65?jkR`OomNIm2q11j6cosgTUXw6PBekJK?jz=^xfcE4!|Ld}z zX!#MMU}kkeY(3mMh3z0;){`BneHIwtJcWpB5Fb4dAIZ6!9iUf0R~&$aG7ND!_A^u^ z&}GiukTC(EQ!k4t<0JwJs1Kc_MTQBRAqvvB9W`4D@91*S2EnHzd8uK>LB{obdJ}`} zpZ`sLjq(Ilz-osx55P&ytfS`4>BjTT>(6Y#v=*VkeEP>sof3T#3O>T-ksIqp%!&?T z+FQS#`qc%WOnOtrP%D=hOsA32r?@70k6zN#Lj#N#n#92L=0_sNBApkAj+F`5dt_{2 zFMdN)=)6fr^h6xIVgb+V8$e@f)a4+9s#?fUneB0XN_l~q= zw4Br3)|@#|vse!x+Jd|GhF|Lk!J>!~vpzA{YjEM5&Zwe9=0=cXNoDaR20km48a`D; zG8Dj?xlNUMvPao*c>;SW2m!0Yin;QgLGTErsY80 z0zFVhmv@md@1l&^4HQDJ7gZ|j*lv(AilN%v-1PJuLqt|lbc3Vpmlu9lZ$ue}V(vbj zz}&CW7v0RnX7YVH(4E|aZyI>^M?m;WCC=Gpv$MGsl{N9@TMTkSBuc!fsFxU@qAQeD zY4HF&R7;QVZ`Yf?CxY1SiHKk=u5$sM zS=K5^ZmP<>tSYm=6qMPdmz%3H^9l_4d09sabxkB6C%2$t=N*x_(*SPIoe|lM23U-I z$PJ*OrpI=e1>jZZ41fUvF}UFIkP4vK(8o^8;krwhdUr zSD-Rg{lnAT=~XPDr*@*}N60RJ`^%@eMgc-2ra&AJ>LG|0+_rEFWTD)pjlCr%lSJU( zSuah|P(Ng# zxacH(R8nY)f10Iu2`+ggcIw@7?O>vp zKK8}NkhYXFDdp8U2yl}$^mHhmhWMnW>GvA=>|UZb;s<#tyDs`gL$8A(CCHxs zCHsXHZNvWkweB0Q&y~T@5BwZ;^D&J61Xv&tZI@Gz!NQB&0?BLTUgHbxrH}En4$FeZNaB?{c4sRG#@TgvY09uuH0v7 zm5H@$=+3)Vu_aJrT0(}S8Z~0M{=kvqROnU+@{XT~iAH31$^?p5%_rQ9b8uoPpbYhd2sCxGU|O$F+WgQx2Z-R9pASAhZ|-V_en6>}4l#1QC|V1G1<0ZMP7&*5w;)||Z!M82?j#-9-V zUm*3ap#X07j>5`)Eyc4`tsQHw2OQZ&F`1q^7~Cqk)xX$qdMmd&(o>oW)i0>=Op{_l zLU)$CH$*#NJWbM<4*$rcY0w^fT#mxw?AN}$e4Ao!BYCmz)@y0nH zC>n9s7=~L*oM!}9o27~{clEab8=T0Uf})nXgoOY?d8k&)GJERkzUDoz}! zEf1&)EQ^s__f@S2uaVc`Jg}8 zI2afx!c{^M?l=_TAYH`mu#JVBDV)-wNd!S}T(dc%e>+4tDIF3+w~I;MVwgZ43(fcK zIqvS;6P6+}zL#6uSQFm>utbZ=m!(Y*vUylk40Birb72?tTkrAd>i5^uoJBcbfMp!K7Y%U=Yb>83cf1?({4uvsIPm*Ra(W z2z?{zH2 zh-jVUV-Oxf32z8C*iiYE%esyP3jAhCB;=*KNGIM8s0d65%#+oB8QMOvWv$+0M72I! zlHUrGLJmjdMCsF$akCud&%C9W^teL_-1T2f{_5&aF5kEPpemC06E3(22u`4UdLGm| zA81;WPt64^dP-WlKXBLXLjU^)<0-6-C+dFt^7oeCZ2lAzw$N(p)g9igq!?F(09VuwP<&18?T=G+4}qZUr51C$v^r zhT-h6X<|Z%umowg`Oph%c{I2B8Lyea&9C7@oNnGB`cb68Oi77NR2;0*Opk@}sx z{JSR++9li;AOjKuFmPaJBeJOx2pLQ-m{l9PEW^CSm@F~mbzD+D7jq~xXW&1~a#h;_ zD*Z3`Uff%pP?%DlUXfmTB)1~}#DS)*WyoqSu@n26Q%ak-#UX6XrL8n;)g7xQizLY% zxZ9vKafV)1FpS{)AR8(cIy##%yxW`-6B@+l;5!8~WtE2a2GC1*8>r&c{MaxH zz`r-X`W9BAXs9o|M_(}zt{MRscNxqoig5!5u!0$!=;Wj7FiT8fc}lsXg4;cJ94qYr zoz&wo#426={*uK+F1UFAJ|DT3&&kC*S)hsKHGEOToK30G<U~SF2Y+8044f@4jRiJ zm^B2qk&NE>YP=Y5#~f$_ty(MW3;ta7_{IGV#GO?Nz@wd#lDG3=^VSZU;V&1}UQ^{Oi@80@>f zL(ww~C$IT?imYeU@<(;j60QB6uG}F9OB@_}25kMz%+!>_+@S>zTT_Y>J#}7@i5~by zbd=<+^M)8Cj_5yn6zqGbhd6KY=-s=yDY=g}NwtoReH0*xun zH>xet;wSM;CFyBqEE?u2JSsgS-VmRWj7b7nn^bT#tToHB2(+LaJecWNTAGnd zN8D~8i)`3dCu^Il4(B4>&dn+)#^0D_K%pq^U$z}VNi`Fx@LI4^sJo~maPmm*REGt* zn$OwH8gM1n5x^0m>H^|uJss_o9_AM}8{}0{VdyvNrGdQj1HJtBT#ltoEncHg{uP5Y zuzjzm1)=-W9n1s<8D=L$&EFCz$wbG0&w{X&HwEwaD&KS@pgg`J4M!pSc9m@^2&jmo zBR*HcV%X+GE^er1hst3YOy%LZ=$+A^C-*C9-?r?4((tm7@`GiEF)mNqvs-Xq{e|I4q73D(SZs7H406=Lhy%OzacfWQ zJq2l2K65tyI%QN?(CE!(r8E5Xge5DK$$*SkVLlg*bm?+siWxx0wmpy(n;2pFTyTr` zd1&}e5`E76S%h9$3Hy?cPNDHp^l3vo2#x@*_No(>=aKM<4l&le@bii?z{X=qDuBg~ z@Xs!vF$mipNjel4&BwrLnVAPIe}A>=`@1^1tSBpAe^=rRF)FcCTFNy=6{pW04GG*6 z6}$yIW9~GZ<1SP2&6?xuBCGXB((?}G;10AmlCa}@ntR@5?)17vxX$Kr=Nsm6=N=8+ zkFaEl@Cs@Q^@vPIikS{j&wO+UM~859p|Y;JqOQ`jc~?zBy?n8?;jLruTb4b{dHa-3 zzQu9ZuP%31!DtDxBmU%+AWx ziw8(JkEZ7yMGXaRU0a@3dAK za|zac7!nQDqodYlTuOEuIo(aY7~}`y1{m?h8jpjaw^T7@ZOxTH%HL|PLhYKnhJoD- z$=HzfXt2bU*R5m~^9*1rG%n#TKLYxNyn(@}+e6}7sugupd`YwQg=F7AgR{o7AoK>V z;FFNm2-)FX5{R|6B`|Df#g@WAsIjJ*ht+1omXjv0x34FRq2ZL;rLYnkNfzm#o}mZk zW~s^@*R$&?WaD1$@)d-i{FC+;?k@5(x-K$GGBU)Tv>%w3X%qbJ}r3O*>@s z!7=l}x#IG==BUzV4aBd|0}`u67*u4qr8C>@18N)fQGgF37MmX7sCvj)QkC>J@5D-YyGV(3V!?_|?dLy&8(I9*@w*pFbs`O~jq@blg zi<*pm(Wj12Z9dr4b6^mAJ(WbzrtI~VF_eb8(cDDLzaX$$tIa-?nQ>ST{afzR1apjp z&IM? z*ZYXz-x_|i5o#}q)t5jknoxMA{1nqeh_`=4@*!-_^C3v81VUJ=OWfgCJk*{M{FoW% zL%i*cUF;%Uwg7oh5AZ-t71;N~Tf~@q$TEjM-r{!H-Y{`4sm)vb|978&ec$){{@>+wnUm*9Lh?N4{LXLOzxy7Z zf&Mj^J~^B5F*ikY0&*RS3ZbN6Pg)*)K~8$A!mXCg4-as3-{%DhY#8L$)-ryRK8W$~ zbk6ZZsPSQxpSv@c*ph7u_VwIR6nsoAb7tc|WFtE8sbQ6>j`ZB}YI9jUn1M&20+5oo zJ2%(-GU)hHH4%G4lzuk_D6_|VdBHUYTxGd%D$b>FyAtFC1v~=>A?&N!^~fh4$ggRx4{uncmf1z^baTq}xuZTn zCjg<_joJC9tj(PZFUd7$?5$g|S;g^m($j-?=l@;v_)VIScb%VR#!V5m0p)yL#EkQI z<;CTKdbf^QII`(3sE0~uzb8a%fes~^q&l*0IcHAV6pZ#Gg8)0x}sv6yC%=SaY5s9oDA<$18t~lN^ zHa3h=n}}ZB=+~0emLy9mRUW5BNo5;0=p;7F-Pm+79{d^hZ2{vn z1)JFLB{Nh1g(W*Y18BjwVSs-lanEcY$qM)GPu(y5Q%H_Wl4%X9^0M>%yQVCYH;d)nn0KVYb=0o3AQ=pT$%a zTKe{dxlAlcyP7kCYlP@{3fhw>!U zs85(}3!AYRpLwUC1_83{&-yl{Ow4^11sr-1|Yy zb~U>shQqWf?n5hsO`a>5Fp01ZV3lU2iLff8AsmwHbbKT!@jnt>S%ex!EuHf_G7{5x zP8!#%uOW*8Qx(MqG~#1Vv5B}AKpMfRH9Cd^d@_4XV$m;m8}zz9dz z;t(_gpymT3fKc8rgyvFqARjIb!CxzOzl0$jimaQkEEY~~rVl>=QV0+4yf?$0ZU$Q- ztd4MTbINl+jTdZ+4&14JAwuW|Yr)Ukttb>GMz~N=jG0mX5r!A@F$}L*j^X9py1FJ! z^-?}8tZXpsv}kO|u02#A4e2oq^9l~tM;G(U~@1$ z07j`2KD_2$yv<=V_eP;ISPGTF8vZ%gvdW>LUJO?d#LU>H3<`vX&Uv&R?6z&KP#i1DYpM<| z+Jxcd+`OjBnEkxagG}wg>X-^XypmJiToYCEAs-gJY2i~`DavTB4KBh(SA1DiRDUQq z|2+(E6_g%s3@L|^hOsMyk5~>L(e(1Z>ce$W4Y+M8G&8P>^u=_7!WO!5k5s|0>T#KS z1H&p?p$XaHl@Y}Ga+t55JVr6 z;jX^y9H(Hb+r`z`y?O(=!Zz?=kQtqT$Fl{_?XSW1_`nqQ>%GmKVVVm2#S0E5N?Abj}aSvkEU`ksQL7^jcfqGSn5Rg z6!$%DQrVbAJup4yK?m3%#O=q3^c&Kk8jJKeZ_j6FnlMAMcw-&kX&&JC|mFqQ^k$74Udt2I4zqmAo=Nr`{yED z&n~Ew6?j*~l_3LDr#ZT(^q^GsPQEy{)CE8*>zMW9-d8zzM0i$0W^&MdX1kTy zp1|EPRF6;WYb$Q7Z16b}bUXr;296#${ge4mPUl5)Xu00B1OQJ7`8L=*1t zw@o;}@{t$C9Gy^$RyrOovk6}UvBlvW!u#=#UTQH73&aKv{tTo=VWJ|DVMU=HBRgh- zUSw?hw!3fvMjDQ#fQ`u7F!}^5#gc!X?I)P`uH0p}oyPpc;fPHKu_>7G@c(#2S=beo z77MnAUZ=7UVY!>cC7sV%#TD(kr4rrnKIo^|i2aY#!%Sh=S_huPV(1MjCdB~y?ZI21EhL)nH>?SF| z8Lfx*UsR{CDMqrk?DDdz=n{J*`;bobj_?Oc>{wADtbQ4+{)zo%^@qZXVaPj56Si&* zNLZtmo!`0={)anUCmup}!*4G}2qCEv=^B;nV*Y`;=I}D?@t5+dYwAO*SE9QntEQpslU0d=vfBE7R50lfx8l?JsX^ z4K7-!mfbEaYiu#>?hv-FZU9TBT6QNF8+;Vs;P36Ls%r@^#om8!Ujs%}82#dO3F}Zl zPmul2PAbk76@t-`8UMuw=NW9g$pfsE@f-dH@KO;1oMtwn$`HZ~ z4rC^S>QP0*@n4wjbnaG~A)2)v$=zEs09YghKE>)$?Zb}of~Sows!`i9 zyhP;e7rrq; z*6_=x-Z>Yo+ka4LyyG z%RE!k=wG{3W`2b&T`B>$M@oGB?s%n4Exz4{3HN0-$c(`t%XqO|aM&&b3UvqXbOKw= zP1SFL%v;P*h~TUa+EM7x95GZDx2h(fB@xMgFzO=pjoefwsEsekcuJ>Ha!m#=%#hYB zjx7n!Q?<&5wf0H@!0DUu#cP%$2Oc# zw((BP){ze;e86V-8jLsaQ;FPm*Txn_=BbipLS1xGSgxu}21PNaTPp4Uz?g4iwo`eC zX1kEt9^`I^Z6>o#H(2f8naqt302D#&WFZ^W0oP_Kjtu%7He1TU7PEo=M_LGGx(qO( zuvQE%O5STkZcicp)idx+2~3Sl#}?R@SX{xIN_f$;j2Ar>my(iqU)sZW$6jov*b8YC zCQog<^^3X8JUCvnLp34^#=aSbDcDJ-+O@6Pw{e$LHdnZu=&^sB5-e`QWk5Am@Fu<2 ztck9?%6nvdcc?raWFHB!al4b_QM%eIH9!afBm@gGN?)?A9=sq0?i&NQqZ$$t5Rw5E z1@-8^LCT7zPWu`^%4vVKe;rz+5=nSat@xw8dIB!;2eE z3pX}nlfiutfY8WhMGwclm>+ZSAM!~W6Mw?vEgpeGMt62=| zKOH?fnB>2)Y2bcUxD^CqA}HLm0^yWxKC-#>0&?p8;b34cm@WEvDt;HOV<;S?poE)l zE<+zBkOt@q`0CV6e8nNX*@7?>M6|KAPa4QDL|}SP{6)yk@Du z)l_y24b_A3b7zS+$qmthS5^S1xew)Jt1bok+if&thTKV+)=&Q!osG~hl3v6Yo0w0^ zJgY#_`3z)k8}Q>65zo4W%H&!pozQ`6pw!N#(%ro2>9K+#;lfs9kJIVN5!?k1bKcHD zuE=uW(j(D7kLYZR8Idb5Ri-)L>J;xN7rINsm{_rtrt(2jm{*9vvT3Ne;Sa7SgOg0_|D&vV@o zD}D>vV67EZd_wX%p#p>rAlbMp0 z!`sC4&(X@ya%8=0pVfwUrG(fY4D; zmi=j^(_Z&Gs#bn>E$tUe9#ZLca#JP$>C1o56YiMMiOr;L`{7M=w37IiIn+r1SM@6( z4+i-1ixxpH=9e~O1=-4PX|!fMYj#6J`#dB;0uH2PretPANoHMVBzZDhh&J#d1b223 z{-S;}i1;sEYv%{(ag^WFc!AJD%TIK7m2&}ChI&gRKYLq4$&bKg2}B!BO(pi z(e(%Y*)BS4KDn*uw7DY(lZ*P{bOg%1*HxmGc9HG@sByOU68oJL=p(=1G9VOc7`k{t%BJ@)lZ)rLU@NfY)`$s%Jh7;;0$mB2R1k}6O_lhNg zs2H(L6vq7M^z)5ld9_E>^6yH#%9{MHDVh3n=uMfkIFyivign@yvA6xgh3bV1UB`&M z&BZddT$HfQzkHcr+^(w4+jk)7kfy{l+dD2W!9UJFP8#R$waZN@IxT0|DVX&Q0BvKL z)9(Pxtk%q48yvRpfSS#dJfHfxIAGbk5q_R3-{2VUV()Ud+C6po(#)cY%)`o)brBBu zLJoT@6d@A%DF>Gg-XD7e2+8_c;vR*3mA2y> zB0r$*=q$sWnwUdE#s0K!8|nNBD6qoL<#;}|kSdPj657hU*i~u_O3tg=RDBZI^r-Dr z@+FfP7($^yJ?H^Q#cqi5u|zu&LQlvIfjC(|5qi`&=tMO3bdx()(18KuzqNd&`W5d` z*(XHYnZs5SjU&(!0WRP=G0f`YwC@v`b=%+nJ(=yObsPQ^Cntn>64(hD>$EaVVU1x5 z|BqeI6jr$Bdw!Lyq693n18azss#cIrDNOiksC})5lqA6v#hs`?rj9Ox=2po!kp2$x%r^oD*0|Kq6f@%7XB=8sd-jNX$d zh1MAh+?({!s_&%2Si&M9&Sz=Zk96fwX3#GAg6J=h?>m1XD>~Z=bBV&9FQbz{p|xDQ zf{r*xbl>3)OnZ+h1c z7Lx8ysC0;d0&s@7MB&Qh`vm#32X)1nxw&fjiwAUg5;XQ6P;}kmFqNVTIk?v&`H@Fd zf{ozVBP!d539Da774u2kOND{BOCI=$4$6RP8~X?l_gKPvG}JQJO`UQge^3)F1}MZ+ z5^7z6XO(oyAbd~jw}`yq1s#wFMM{ICtI(AwU_HPPp_G5Kj%}cV5hqEb!zK|t2UDh( zE1dvc0CAvz!{8_TQ18pn=mAeP>VwhDFbQX^h3oOOM0c7khM9S7Nk+c^97g8FSn~3^ z2(c+uXWf@n*$L?_2dL|n&?y|WKKM4#9%B!w0vb7^l89Q~^a~wm3j(Jj`qcz2k*5@5 zG$>Jip@W_hx&1xbXCEGZfhr|i41r)!-a~rbCW@%fve|SZUyNlG3QSs9t64eI=r zO9oSG8{(sAYeo@~MFZY0*LAqKM>uHIp7%+`7T5daps+2cHmXr+qL43d;NDC_=THIz zw!DE#oYBs1s*r!$U~p<03{Fi0cWN5CxKq=x4F4TDs~ZP#O6v$f^>sCL0DKV9be~X^ zxDQ=o6}u`DE2!~VQo5%M1r%s{euJI}@f_6B(srjNr>W()+o|G~!5CO0ym)+=eZdmx z-~qU7li(EC;c>|O_#F#-Q7P7u4v+mtBU-_u@e8;XBSGgr858|OoX0HI;<5jh?&GFM z@VFQjPwq%MXGjvWc4fxnUtD%svO2Y(D6LGoHoYpWUa7xdK_~N`MO*oqUUVQ<@5Nr?_zpTSofr-*=LZ5qqJx9X z*hLrw(5lSGjX`5|18z(+XA#9Y^aOGuQ99by@De%Z!5we+6?8b( zCf+#!>;JJg!fg3<2bRlLDdg84(GhyGPk(jat{>dZhbkp5f^C!iDV0n#Rzo)#?>OIg=PY z6fWe{R5yfIf1+L`6#Xmvr2Ct{t1=+D+h~<151(rU z`O9`HEr$A(xLF~8-d>ftua(YwkEqx^Hc}zp7>fzt{_v_yyI!lp)t*2H2IC@6pi*4q zp9-mL91a#zsS`;QFd3-f$OMb)I3AFEr2wvi`rNCwG zj|V$CXgl#v3g6|cDvj_<_g3;VgR$^C?Ur5_uDRxJQ^{Z3WmKotlu={&+Q=5D(LzLr zy#R32862cX#J4+dbMxC9+(=YPw3gHige`Zd3ch_3o%9;?6d8n0;NHF|sA<`zmE`=w znP}&R_RPwq_%`ogzv-aHUPS(P2bHZdn5$Xv1#oa+zTw%(N4NwXGOD~sw2|xss)T!M zEC0<-Yr>EMq%u@gIVc?xm#dZ<_7GIVh2yV2c5 zcsR)cHdcyj5}aUjk1q|?^LM~qYKt`x+r0z?UK>-1t-ANA zk%eJunZh(8s(>z#T&KA%M$x_qieKAZ%!uUV;j|z2I31?hVwl4g(pj&of#3KVth{=B za5=}W8-bv0oUWA02t)=<#e>)>WVBBWVjJy3YKG1%rmfQra@yluKd zbP$dIWRgeMuRUm1+aKD|cF7z?`Tkc>M3jB)w4+W{;}Y%W6|~)ZTb@g~w9wD5Xq9r6 zbI|rJH`kqTR3kK>Y?M)-)>vm=lU7`ML3yg$%h56RJ^K&TRRP5jm4Symg4PFxxO>^B zyO+;DYqn+OOds(rB`D?H6&pU=^y1`ag{6%u(UDCxatheuz1G}>S~Za@iM&mdoRPUZ zlgch*>%FcK?V)=1HIc~gYG&0zy}%g(!?%hP5}cw#iwIs$E#UsN*|DQ+p+dZ(^TMx3 zjKuSGJX!aDNk&fGK&Uz79?gMqrn2dzN^j1020Cdw`GH+Ta-@Lwe}-!-an&DD>w&mM zkEn7NUOX}w@~ALPtOjI_lp<5M@?dPv7gPpyY2ypHxFd!yvE=?*l_P*9dL|OhJX#V z6U6lEM^nWw{=`HNsO{avq$w0GIu&en*dF?kDinGS z^TU0+VUyb4Y#E&x&0ji`Xa^!1e&J200Xo*fhAPA-2-BW`oRN|OraUM zR7ct_Bi(zv<#W=8nPO|BCMb5hewt53*0-I30u2CU2yhGU0*AEag zTp)kyKSHWWMinZ={Z^zC6mN#)b0^l^_NUYmd(ZY)bhH&F<}0K>Dqb0~A0xoQo2qX! z;4v=zWd8H^O8{Z$?F7hhE}PUXPo}10vY`MG0r?-W5^o{;N3AGO-D_rRe=yg=e2a2! zy6H?W!rJ{6=UW=TD!wBvzM00kxCd<_7St?()QU}l*gJkMhYc179f_zahM4bd&>@(E zxU&<6ZU~X;BrI5=f7=2<%1ENiqe^&Vo-C)^8dLLH!k$9`Xl@!=8uW$&7q{wmukQWw zW-mh!al6<5vR~fp#rMnmy~L}Cy(P%`|2r*@h6jZQs#)@;6J5he7qaWyFn$2OQ*HlU z3VGWDRzxlplpd0Ge?zqw_WCVAnzSex_SW?#qD)svl}K68Ig#Y3 zJ00o(TlJ7g-7;HZKmjL`r-#1!qWW^y{`CEOilpBQ5bBFdjEnZ%>9-l6b5qi}NBf#e zVqviU1v*HC+wd*^BSA!mf^Syf40ip+#QPS6-5&|VA%FFk?j%Y9SMqXO8X^E0 z(4Rc0JtAC6D?yLa07-Q$I!8PEfmOuM(W$6rU`BdB#S>&k3W?*Mpu0feOh-j! zD(G|?(ejJHh)!Nkcv{z1uC=3+;VZwe6O9q&>+I-k#4<1J=ot80TDw+pGZw&WJK5_H zJOta+{U%W9H3Ts;=^z_^gqltTc=?G!EQJDz{6+Asv;0H9V@n|Vq(#r|-?;Ikw7sb? zgvkG-qpGop`3wHku=zSQ(f1MdqK;R>^;5)KNbm*Iq2**V?St$No%lKS4haUR0PW*5 zX@ng){B71v=>s9bHN3Grorts<*VHta*!c(>3|sS5*i6v`q-5{~CQut| zQc}=%IIot{>7Yx&F_G$dP-829);-C7)49PTYD}nFk*4~ASwnk?8QW~*F4=FGVd8aN z^GkTIEYu!7K;L5hJ;IkWx^B1{)6U=_uZk^28h#i&1o*$WMtwYpqxdJ$MsSs}YdH3D z=93%vgT2`bc7$4|674PhYLE)tqpCy1Wd^xpD1w~mK$C9@F_bmr4>}_DM5RWfeQ44) zvQ$Xr6%RW44*s&$EhJELdnnhM)=2=Tw4%nyDo`nw+{73RCL&2NFcNdBlt{HkY{^P; zXbsXYklCB<1TR+HjBeR?iQXDD=dc~>0(Kn`g|Jh*RMdJZuFF#P6kVF&dKom2+>l{C z-xM83VaIMEq85`=JCaD~WMjl&8`;;dcS9!y66cWr2w2rkwi6;G22nX$93)%}_A+P7 z#f}NmFB!zbCR*X)GHI@^sV=K7^QiV}@S(G}6X&YHqNwJ?tfZXeJn2a~>?w&zj6$(z zhpVrH7af#EwBxAE0m>UPO6Ni&Ab?b-AIL1rl7?zDLEFuf!D2Q9PDJ02AqSfR)OzmL zsOaRxXw}`umX5p*V3{u}*x0L8z^tKMFItIK@9hu;Gy&~kW7IGEY>3XUq%slwa5F(& zYYGqe0X2O|Q0X42!}kS`vB!m-MDF=P*HS+0`T>E1p$|%4f5Db3(aIZEcD9nmB8+jv z7+iiUuDLWtG)J1~&tAGVYW#|HjzIQHKn@6MZtz*Z;y$ZAHBRTZ4gUCT4f|LRmc@zM zVN?mAFrDZ`*6~~!9@q0no)MT&C&v-?z!~D^zm>30zBAWK=`=LR(TUbX+W=ykq9ry2W z>@Qq4a)ro`)>F&zxFhwf40}U;>m|w0L=S~}sF^Mo-^b=QOOdyuF}f}PRAFN|Zz?_K zRTbNM&6aub zsWG5os3tbtF@jE(I*)i}`CdY$+4KpF$z?2E`J}8al-|>_y1plTd<^ObAY1<%+6Nyf zS)>srPySLvEe{c>5oECYREezMjdV(WMQy|M4RD5C>wJhJK@!pH zJK^>m*Cr}Fa=gA&ed{R1ro^ShtKwsmqN7Wh2XrJE<&zroE1{P4oE5=hi3dk7I(I26R4rj#D zT^wXU+2+DgwM4J+AjwId6=_7re|O8(^3fg>Ha&7r6{@QP@ol0OsG`UVJ9 z+`C$P>+VI5d8&BHM{74upZ)Q%iz@kV2iPOU+4cip;(a%c`hK!}v+tt!wj8*qHldU5 zVt*A?ogIYzu=IOO(;zKHVZRI#69|4j9o3~JKl`L0778V5*G zCy7>%%=d} zo7@HfGp~}t4%&#@y1NuZOOsR$>!6P#?Pos*6i;D_3r&d5j z>!R5Q5Ts&#sMTd6GIwOuH=m=!<{=%CC0wNDwq&^~MzoY{@rklmS3tZRsK-(Ppl`Gk~=w7keYq^h+3NP5^}sH_=>3P ze~4~cwgQL6T__0cg80RMVLYe>f?}Q*&^~0(Dd@;iBcb`Ev?;pWtIEA5J}WAZ4%Uzp zxmfJjs3n(4g8aT_ zo2Zn?A873G4Rr9bHI!nZ|EkF+&$%+peU0deU(weZzQ{;yFCBC^c9~Zl5mCuW(YPcX z2YFhN(n`F@6072QH64av?`3smcFBno*@52P(SaLOf3Xz4!D^S#ks2)E$>RLs zSJJtIup2LA+o*t=U>zINBi1@?ACdpOlFnL!G~vps+`3~YvOPRKqdoXXRZ=6ENIGLY zxq8z)CpjlEo6cNFG|8H1^Mx0dfR7s1kJd`H&~eoyJ~2MZ&ogMNr)&@DRJPJ7j_|Ae z6-@SH5!>{QAv7ApL!+`cq0zWA#DvZ0O-U&cvj|6#abC`1|y?D zt;b-#!gm`n6|_xKvM?UZopg9UkzwLKVZEn=pnf#bT8lPa#;B}5>qIj1MH)O`wn#S? ztJFe{bU{xsvx%CXB`uhyxc_78i1_35&Sh9oTj0sT`9fRVwVQ=Es5Rm{89P9glgO4j zkJ6y>=-k+S@%ah4iTO$Ur5C6$ja(IygLj1|L`Jy>`g-_yy`SFyD}}A!VaVd^eR1`j9PB zq8;A+GJ6+jG)aqp@n#ix+E#_Rd3ogTJfr?)i7;n_>r9(X4PWn4CH%hUY3dW{y|fdD z9x1;hbA)Gb%qBxkUuaKjHIE#e^)@B`UDj(WV zdLpIF4Q!6b&DchEq^z-ZcT3RFhS0LDKF;E7G0f})C(ROvEJrECFxV=rw)b~%a47p6 zUVJB3F!!GVky(G;~C=Sb0HEk99<$kCXhlz{rMc%kIY6CNNP=D=x!}C4! zWy^ft!4I~6pXeV_3paebiN`^nAc6)QwRm@8cVjb1x!GQUjta-p9)Xxe0n#}-?IE;M zwNV@<_!~4_h7Jab0a3F*@~M`+|Iq^ny_Xk{oE5+U4id@p$wWJ(m`ZoS(AjzuE-0R@ zg4uTN$pNUlu(J>M5-kMzciz-ehUl}|KzgS&4_Cbl%8JcS z$dO(_WOG|c(8OpWqQj&8VqD|spnpL~mr6YF>e25Y$79OYaG^FyJMD`^OutN2zgcg^ zjhl-kz}q~geUZPUN}KiY28qiLU=D$wHw7!<9fds9 zVMlDF2XA#h=~TK!B@cAi7O+m;DU&$V`JFhre_!!FmHbFx(fQ!dlx*hT_t=n{l$vC; z#?yGwrvQD$4d}l;oofuPLps zxO>d}h=`X=(yb&{Bk0^PqMe=A`4uL5uY#7$Qzy)i^0TvW_U=wP8i~l+XcIsnJcma+ zX4<~g=rPeRf(#G}gwV946Q5wzl+coq)tYs}yJ%g`s$AFNr~`?6lkraqe&M|0x~9ne zbg&f(+2NiC0jYcFnoW*ROo;Mx4f-T(UARw3&W4=z$5KAaIh%L2v9Ynj%B|X zwuZIrIws{J4s`Il~z1D^M zxzV}9$X}IzAH99=26&OzvwmLEqv9chmur0r>3nuaKMWHWw5sAcsx}5t;16;HT@r{8 zwA1jV=i(>O{#7Jdaij#ioPNdI5XbaAN1U=fuBdb8L%{0-T&%uYwE@pLssengypo<@ zkYAirMEg7<4h_M@u~pLRDfwk}RB|4xG+O^1VSPK}kR|9sckyjts_4XWUTHz*rWrb> zbm{@3{o3?O!G3hu><2}t(EPXPOGec*W8p+mJ{t4*!MSMW_5^$H?lK}aXloTRV7Hnc zkhUB4m}%*jWXYndD7mBqQ=z|fomxfVr`6*x0Y)YN+JTNbg|vhZs43-XemWYt`MZzksQd8K9!0qz^I!(Y4K;J9IIO=+ z1@0D_Y`}F4rqkBq5(c~b#H>xg{8@t}4!jse&bkw+$FtAm-7shad~5%C%)$7U+>C=+ zC%lT+XV2IlQoF0dU=2!Y|FH%hd4b#-B>IMU+xV`F@CnY@ApIKtkar;$?d765Sp8vK zw3mzKK(tqTbTwwoJBsz&!UA2@<>5XVUP^~;{_E8G+mg+7fhVq%R(-AlJw!xpb4;re z4gG_^W{NkaHbLyqO|AT%!Pq8eCTH&XPI@ZyFgLcXKC9BWvHfUM-D$&pfJUX_Ia%u* zv_VD9&Hy({^fSlnv4p9K8ObZzgLYkMQqI1l{YpA%H;zs^i!5_8k-1fbw5znAP_u=I zuXvFA9DexkxNaIc@Ffhu2rae|N1&@+47D+Joe&k^=eL25nL?x%@3Bwk@3D{J?`ev` zdz#Ym9;*5Q@rJiJLRZaod6BDn3~%-?5sUMcHGOPh4I3hS)FYLGPcjk zHqnwNzeLNG_F^Bg&v^%Csb+8Yu@B8|Y%I)f!?xz#Ru~P|_h@^ywhs&A z!vZ?vHa?FEiO3Uay3z>>@yAf@-buvsUB3Jn+i6c#>p)Jfke{%lmMx@NG1Z}_5#*=~ zKKA$9H2udh%oPDVVs?v*Y`(fs@?hKX_bRZ?JF07r{bY`Oxa16v*q?~OYem`OwWR@P z&sJ77*B@woe4}~ivPGNxmuwF6+UZ^3aTFi(;I#i@b*N;ngNHSouOYMGSUQ0Cqn!25 zOUy~gQJ+l9EvObZ5%qnh>_SF-DD*GnuNm}?>#Ib3f{Zle|YaQhJ9Y7+EnC;y@2lP;djDFEJZ!v_L2@zRW%uHesXLc6ZuRqCDOAH~|&`9yzRZ0G@|{;Hpi z$ao-Bv>eG{Dl0;&W?^Djzz!81CXmlgo9Ex#Li-#+s>m6qRVGLma=kyq+j%+!@q;8p zth6FuK3*)My!o2*P1C+pV3_Z7j3d@NNEXMlqg^R{Jgd79#E@%esA;DMo>fuv-hlj_ z?4a+Um=I|w-k#*eTlvWn)Ng-eP{5*t#=xWpAV2v?vArOaLwHIFwXKKYKq9V~4eM&h z`JsfxMN^q6k%44jE;IXzy_2e@Hpb+iz)JoXQ)$boWh#g9ehnD!8akpAmHazIQxpK! zDrsyfTB1?oua#IyxF0+IBa#AYKr!qWtzB%_#Dh zkI;`IfAxsBq@#z6Hy}~ywAsYFBI0CXUSdIVk+gzN=|V|r z8DOxH38b?eVZFZsJ|o@3Zpv=BKOb z_ts<|G;i3M?UAVpPYun;HAjC5>p%CtXrbCHY<3T`-%zV*SCXKN$5=W)|R_u5t zip~g-^fpp6q&M%?>?vv>0C?nA&0dg>u~8E^w$9bbLjeph+iOq@XA31 zP-{8N?IY+}z=;FCIU@uN2o07kh`1XD$Ww* zY$n>7PT>4Hd(fv79brSKq0rWPYB-?5gVzB4h;(R2&40i$=$}I zX9_QHxI;>z^b#8L&t*PnZfK}-edcs5;ZS0ATKb{%&%CqOW^T&xEeJ1^oK7HN6)RTLq>;S7r+);wKqo6152t(Wv zh`7Ul!}jo1-a2QZ8z~}}0uU@G+F66Za@7*lM8!cfpcRo%Cj-j_7Em3!u^stOy*8qArEFK1_~4sHfSfiJpyWG5Z<2Vp;&^6?HnMxr2|H2GFW zol4x7SeRTQt)%oR2}}w}jEG+C?zY-hhq@*u7l{5KRjJ8kJy?2?O6WNstbeT1ML*v&x3u26g(F{P|vu2U# z>I}IwEShF&lR>m=qP2&BtO80^whM-H;#l-&$yZP4paQA}t8F@dXGgnR_2sLlcF;lB z@hgnVQM&O(d{|E@9H6i3>9DK3Lglaw-Aki^vvA?1WE5$>&W1EvEH&ksm9d%RlNQ(d zkdB1B#KNQ^s`vwdlJ#$-m)5mm&!<8KmO76r6}kgl5nDEy36qJqboiS(UTx>+#Dk9g zh&VU7lt#5kTc~OzaVhrw4E4GwN9O?N9TZ}-NNh|RNm|Mpsw?YjT$*+s45Fh`@fG@= zA)b3dXhw~m5^1=0i#D6=*WX3{vbT#N7m2D4Os;97WV?Q^@L`hsPW1>n7+Hy#bkYPeN{k-;ZpfACPp{%_Nf7QD}DyU$8on$ zyJ5GZ0WASt8(@{=bWJ@-AjDKfkSk{O#nhP(Z86+L4^%`QbN)fWJEVfbkSzY-NHH(nEvVAIUT=9NH;5Lmuy>X@=KG}?2#SYbAvm4+R0%HI?| zbW?cPwmf$!mSsP(4qMY@L>pGLZwHYVTd{FagVkNg9i2g6F56@73FAjtg2xPQw4+GR-y8z<4%q%+0jl{4{B5tN8y)aGwV1Fo1xV%13 z8jsU~4rq@Rosf@|6)Qe*Eh=(R@7(8<9OMz|v&W;}U+UR%G5B{SoAl`H&#aeK-P98G ziu`SvmF7jcMd#Y8HL(?{6X7+Pxm9^JNp+6J(*4fMbCxN^cbCsvVEMQI&;IJlgj=Cb z=E32?%Ty*PB~JlR2@Q`wd_N6s&?Yd6|Ba)Ul@oTPCo5vY*%AjMBmL{WRP>u>nadyF zqej;JIS0(o5MvdtXnVcKmhY1s*q@$>zMIGGNwJ(`pZ7t@5sab{!Xn$y2W7q(zngPB zA}QF7sd=X1pqdvNIt7tGo``@IX)uw;!|8^OA;%}Y@}7O-7+0KLP{%;8Yw|5%;B0a& z+$L%Qkf!_@f5`^!6GSN-YQ0aYSezh#0WP0@U$EJToW5Uo{L>PZTzj&v{?uJ_&|nV! zOdP8cLj@<_5O+0OAm%98a-dUg9QONkh4YpL$RUaM6>JB!MY@!n&=JrT5-JCceXev zTAk#+n=@01COJ%I%f=|#SZ4B&4a`*UsY|I#te3KZ8d3gG9Ih1KrV|#E2I;Ac_PTS* z%SSv{rK$IL>~Y!UDqR)l;Qo%ipF7a8*kFElpz7~9 z%ky}A;|V%r5js%ag=%Vj(hyD{C>V_b9XdIrhE@(hXL!yK1_U7FLCE%b)v-22T)>nc za~It(`T|QH*uOtuAU}qIWXEXVZR8IsiNVtgXesx?NB&1qlA5gG=1TPzYerrdoJhUd zDQcBN#M~~#+Vdc0Aif>7^ux)J@ajkW8$w%(rCP6GeFDD5I|vkBZ%2ehCFmm_0$QYO zf5a?f4xx6@g7bgUDI#{qDAc@?5&OL$UBTqcAK;eJ=N$&&1T}*Y^Y2uSBw^ML{VqBP zC40=iS8S=)6vpvq)VE&KzF3W2e*m{at#kPIG}N|6={}-T6eVyKzEfy_eZ3kf)lsxh z8Ic_)+R0+Fh#b6bw+@{qk<^l)a~g=Y|0Rw?xil2rdq_P{DYCxQdOhi;b4RenATXX}3pSe{Sj0n4-IHv!A@$=v}9a90P3UZcyU19y^oy%mt4 zG9)TT{fjrK(!S&I`P0xhqPR-fEmNxS;6LRf;+xK=S-6H>eW3UV(skCT_oo7jx3H_3 zsIKs?xKIs8tcoi-F9K4k@hAY(3XEU3^o4|3UIw}!oJHbTW`(A0I;kBNM5?u=lJ&%+ z*tIb7p!7C1d7q>R38CR38+|F+M(C_f_~S;vuD+2MhIIRyZJ;~@`^o`!f*LpCd{3LD zurjSG?2xkS1`y4LZp$8Ens{J1(P!)SDMT+UDQDUb$Vn9(jkigiery-edjC(N1*l4D z=1Usv)cWIhw5>+~)e*4it>o%z=yRY`HCUgvbb2Al@Nc44xL{%mLT}9>?KF=3kWsTt zOrBo)yz4-Q$ze3`c4<;9jw}Pf zb1Xr-@>p$LCng(MEvo3KAIbd8m1&>WMP|n2#N|uTwQYs^+!Dck`J{wNbnGJh%_$k= z^|$1Utt*80!+dros>Aam^EA0qN;~i&BeckHJCaRBAtiC8(&8>95hFVTNxcvipjw7Y}!EbNKx#Z8sbUXY*7LpbF-72H%5;K!QiX%NkhxLQDc2aC&T$JaIoht+B zoPDIVuCCJUyGuUnc2yYUZ zfONti9WcknqlkEs?d?1SH@E|w4jixW8kBs4P{&%+*^BhpANfnR0j)-6NZz2;$s4At zNDFwQ(O1~)#I65svR29eKn55SOJDX(_!8`>D*4^DM4UeY;)FmJgS_yq%Ltb5>mj0Y9WdleKIWn`i0hejmHs2L(ft|Sy_&T?t+?}_+hHVA(Et7W= z?R`22{qJ+=;9UH)NiT``9$+n~|I4(S#r_JGMszdvZxeCNDz?V(xSFk@rdJ>uJPfTX zNSb7ewy*!hTcwxa*vAvF6uQxAi9oBQ!*)&v{&Aq9yKnyzVYPaWh?|7Y2h%(F16GQW zqI47!JnJ3OxBCor^4?`%?FLC&m6rw)8f2NvSPr#CZuPzk9RjSvV;5?2fjHozeB=PN&XOJ$EqstsLH{7B>52lkAVTH8oqVao2m%-eplC!tW zu_)+}^F;jv#2g?&3<0xKWLM_NjFa9uYcuS!R%EZLjLgtv#^p%aBC0}j$0vf@rm^9Z zqoMJlfyT?ahO~Alj8N-zekaXiB`Y1H)^EBg>Q;|=VEDm4+fDx4-o~Wa7Q@C6W@p;oo+UbXs;$KWcr$e+FYJ`%RQ{k zT}SkP(19jI_Sdnam+qVI?P25%-mFTL?8u6kX|Hk(j`S^b#%Gv)j6n7>KFyI18^%9W zh4u9NBad9)J1Vfk+VyhUW*$*s%4m*yo(&r>*%r0jLsOu`iaTmK{0|9#>#@LN5c2qzVDN+PQ6k+LzbfgHV zR4LK~LB)nfqp>HkCPvL0O-wU+Q#=E+kpDi5n!Mlj|NoUMvO6<7J3DjEbDnbF_d^xs z9jpi_qD_3j-MrD%2q8@i`tPV4%5rXNUtAmoA^`j11{t^H$qiuS5nDi*!v^%kw}dYI z#!D!`GF~bli&FV_2>(xz!+Dhz1&8wk5ALKwcT^(#;ffp8a=vi<=bzV z(t@c+Je4QNedO-bWaDM^I~vZFl$2jQU2^_m`f``|$4(hBRx@|;tod%sm0>%ggF`G_ zOEj=3A?b8V_Fr0k(dx}gOf7cJWm9Ha~2c^`2LWafb z=VrP0$>;5Glpz7}((wvQRnWfQAWHeUc`a4_#JTKZ>np;-*{nOHrg(o-D*U~tv~LWY zyr*>VMEGn!Jza~zK4=PQ)3N`ehpR&MWzJZG_{NtN+;RaOXP_-2dg9gvxB*xf)FFWL z>qE)==?jvVmxrc>XGZN;vMIECAYn5Fucf0R?ZSD7M%Vx_N4moXFm9aC0PLOW%V)I< z&$3RN>G1hva#Fvzz>`Ym0Kvq3h)oyI(dz%p^|L!fv!Ook>pE!96l&I$pMiEgf=>*O zqxQd(j*T*f>G>U+B3A7K@MS}4usk!ouo*@66Meszc&R}i_`Eb*pGtD{SKB9dpm+AR zlygdzMWDcL;+QeFq2D34SCHTJmqEH)YQ=()gaPV$sg-cR}bg&I4Jii+M3;52s`9DKNaUAUmQ1u-cMD?;`q`otzjCq-kx z#wNrj>_{?~C)dEppC+TSCgyKCqlETIpBSP)-eybK`-1zjDLaRT(P8j~spcKBv@WK(H=YfR=d7-Gz+-e4rbYBpjN|a0svaK zORf5;nVOG7LHbWb-hVZ3*c7}tR=Yt^{n6|g=CehMO`chP_cZkoJ81UU!1b?vQ2FZM z5bzo-D*G@nJhuw9&gSA{6-9N2-A>S{A}Mpt3)~l+1?(iWIAXGt<-DFG$MJeni4e9u zAaD)-i}awwlW`MgK*7BQ6m&K>3lJhyMk9o{EF@%G9kPMFJ0`Zjf|t+$ClBVp6LJn6Y5kEvqLvGm@~-P` zmiXk@Br=o^ehGKnngmWP#6lSW@)#G48rX=t$wJ8K(vywVP1UXqn@)I9@oTaRosZr- zv#~JxJLM&6bb&;Va`7Cuj0%X<3i3LDLMFSHYIvg6djmHh9ytSbgxx1z}@;mT52^?t2g?QUxg~-2E|4P z(x{JuL3kIbl66S^U?92Rp}FDJGl)YqB7fJ>BzB~1#8WJaZ;;g6fL^@4VG@3b-+*?o zyza*>Ysb>>kZ!Zjqlu&W_@X~e*tm9V`1r_EgfLyb|T9x@c*@ZQJ}e#b6R%PC}1!^VQRUz90^9@x&C!WYD$YBg%R*O1mL zB-6JjtPonl67X$|Y$@|C-Bkgm7Vj`(sP>!}LcG0wxA=5adsvA2E2#I&B?ay$f|^SD zG!`YD+*${AMGtNP^`@r0xXSzRi8LA}S7U(2oIF``uFk2+#Q=>NbjL40?xnJ!J%~6_#V*`odyr7MoF}z^ zkBBeWWfWLKi0#MemABqT_{>PqpCf6Me2e-1k~9jT8{IuNd3pGzuP+TO3_85KuBa%t z=J=uFgIjCX7wtkX+5Lrw${-r-S*qMvxw$H=PI;ORdrss$VO7v}SC}g55DRY%9#t7x zoxkI76v#W#;c;Q{I%?@l)*suD9CvBPNwt(XG- zWd_e_zC>mbg$ADY)4s|3`EUZM_MfDnMy?zCe|V;l6r{>*L|lfah7mH38f_=#_pd*g z!;H5;+sL*Xk-CUr2xCzm4CKvOEcbJv5X(C6gC6AFi+7hW!HG_8LIBYvOEziv$s$)# zE4?6_G$9IW+1OQ9glY^)U(S}04Pfc(g&Z=mPLaGfB_Wv(JwZ5R0#J$@`Fy(@pb_!1 z^1eLSa>dF}UoUM%(6$s{01b$Ujx#o!l@Ojz&lil+jzz*Bs4xwr@chGJ|JSm?wDN= zUV62NzKh53s`hz$L&c942cz@C_iHXCX74Yj0(@!O4Iliq6Se71^nZz9vq=@qsof0& z|2))IiXr3&CxD67GY@`Iv^q`SM?ng~NA+oV`2!D7{!v&Vo`Wayucq435TDu_800cU6uv|ImYh2U-xr7*ijp+rpQ%2d(|e>x?R zh&gn$7W-^1p=^WvU;(Q|bP5CCQXt!5q82_v`fyl(GL5xjTB62$Lri1jcq&^a(>^(T7jKscFGW>(`g0I*zv2PxR}s+75%=H)NYTJde-%NEQEU6knoU`U ztL*z1^6Xrm4+fM{m+{;X>i(dkLl+__($OIp z)IaFf8RFX?bz*xtQaDf9d_9tN-#wKzJcz172H6$gG}ei*t7^{a09{XPDQ5`OUi@}7 zrJ|0SA_j&Wm*xxsD7gSTMEz2<%>--fmz4q)?!0;bL)n}67`NTj-!G5}GP2Teo|{#D z`UmZHlHcse|B+8nx1zWy1{2k7+6x|)8L$P=<{trXy%!%4HXmWH0h2I@63-)E6}zjI z)o)=71}0v-Jw-f&;7Mv6en zFF&CmH7`SKIYrH4a1&_PFg)5Gq^-~(c!iIt*RYYo@qnZA=LNbsYvhsgCSUmn_-&S%P=&y3aq)JyD^#bxLt5yR520SH2L4nITZw zTBo4aNkklZBPu`cD`jqnNs167C7ZvXCgDN*J|t(LCvQk9SsXu$74*~fFq*KJol5V1 ze0M2($Cq`EsE=!ik%Gzo6{#JZ{ErQsd}=!jMb?3DLGFGc+@JP*{WVy#eA*4qy**z9 z<^Y&{)-N`JBs23^MVlS|_CnDEak5_bxeRrc2>^?HD;SDh$jH+|cvT?jlyEde0B?jm zRDJR>Pl9k;28~GAGEoK$;%}HW#2|?0YCxO~;na61e<95PaliwT6%_#xTSw`y@TXeF z5VrP?yjCHhYaUA6FkD(X_`et#n%PkF-V+)Z9v885qffB2k3lP75Z4M2c{b$K>uksx zgW{Z50umyLhJ*;Ueg;xECn|L$L87KmqcKE;5{oJh707$U(D!zg=)H!iNU_up3LWgg zI@WfaK>PVmcb6aavgv|D(KrcxL3#R&zR5H!9(B&RYWPn^LfmJz)ZwZN3}mm}Jr68$NdpjF&uR&hds&2S*QJ<%d9TFr=p zZIS0y_#NL{cDPSL{{E(omGV3`(i{bp?njC{AF&d)(y^?%7XF(;A(XeQ&s#V9DBop;7%PIMiG*f#-F+CuH zlGy4%kO)_`Q?iE)65D`;_!$z~lp<0YQb883_gKA_Co+^de6=z7VygUG>1zY_Xmvo? zP6;NV%-pCbF5~1aON@kdY8`*OC-vlQ%bWXNA7$hlw>xN^<@#L(C2<(_KmtK`P2Kh( z?k{M=baWjNXPnu7I_|$1{$oN&%lz51h&05Gh;v&8c7R$k+Xhn0k8sCU!>54%T~CeE z$a*#j>=L;iR{5?E+(qy{=tK~%On2gX9jf6{`wD(BZgfZ^`J&RIf{r)m+iS4h58Jo% z?dK~k$~(5(jwMT5x8Q)$Uy=NMnFkViHe@T}%y3e&3sU*x@SNT!yn8&FZyUUt>if6u z&1NQ>SSMW878-)$lweq12DO%4crM{@#cmQd(+XN^zNynRP8?t+AIvGT> z)^wWB6{d_ZgIuH+YCT_DRc)Q76UkgbhnW({Ct=E|A@X8qHLbY01&vF5&@(QfZJN={ z(jAQ&d1;pVPWF+SW4p`V*E&3-X5GjbE5-Y}eO%qMz3ynYv&X!sgFx>EnVB>`ZgYi_ z4R3{c9xrV4ow9efVP?OTo*eQM>Xpce4l$U3y9N_rk38t;unrS|1CO?jAscCr&7_7J zug8$NOgu)%cI>4#xdg`|J<81mPvgxOF7ENncC~c%3|+DZLXC#1EZ42c*i;pHBJ+Iq ziPDnN!YyBJqEiOq8XbPd0QiNp48t?aAuXejRrMr^>E}?pIJhEhK$iS&0iE!eY?YOv zZNY}aCNAS0ARF6UNeaYcdRP-i^#?gOXI5pmf$6Z_My!EXla1~?C=Zemo1y-Mt-f# zX%k4aV!YfVKlwL+N4 zG?wTcY4;ukJxADpInc2Ds~bDt)|F5W}%;E6VyC*rCS45gyn6=_PjYo;t#Qil(S57qkO*E!_l@xaZeqZGwhEEX-jyWo!``vowwfzKDlJ4e+1bZJun~#*aH4~9bStUV2zJclL_P_0cG_I zE+4@g_43r}6mRP4C<)+RAw(UC-Y%4y;8J36DT9FSk)E`_B>JQq)aXAbzC!=|9r#G; z9P2au0-e}^ZA?d`pL2f~gmT?t^~u|I@snfa@UybM(fZ(niQ zjq^g-KZ}>x?n&xo3xQJz4~!&n`t|r=7P4>BBr2dV2?QeDwA{|_#A?8WTBj*f_qjE-#ORv7a}**{zEU3mBPXc;ee528jd_&vh#w1#v3FXelC+IKwm z*dU*^7f7$RDgB7J14=QRg7^`Z1j{-ABv4G{C^IT*AiDx+lW{NTu54&E;UBC**UUV?$r7l>e)BlY-vRc<}4=cV}cb9y=XE`s@@cXn{M+w%i1-Mlm zouVZKimGkPI?-Wp>b9-aQB7B(+PSoo3<}oCRh^KX`C}Mj0W@{^9JI<~vMItYyro(Y z%YHmY`!& zjvqSC@q^(Ubo8${xE9gG4Wc@$quqd!=01+JvON~QtOSkDd7|_YN;NP*zZm3S&>%9TEU%JseX8WiUQiu9mg=oZ(bkslcU2w$FO~f@6j6E zMa>%#R6k(ORDtrB=Tx~y{}97x>o46W9}$g=x}N#}h2 zEbaa=`E;4^;hqS8Ukmq)Z8rcVTSRSuNgE-qzIK$Fy-S7y9AV#|NbiBVTU=KYTr^)x z9ge`|AFRHhtKOI1m{uEHx-@fiuKAT?Wv3JFaPCo(Z1H#oBLN8B6b&f2ctSgK3{N?Y z1-MJIm^l5aK=DxDoqa<9oynJXpim_FBA|aq`U+-{eW2F+4fl+ zUrnfK6@f?|ZYVJAu)gR`GwCg%#A{2&s`8-19i^eQhYRw`PxC}v+3LcbwSHyUg$K(I zWt{OUSBhzrA_W3?G9BFQ4ND$sfQ!gr1s#F3N1v{Neu-DL42`|0)`aM%B~z=;$o*dI zcydEzvn8l8vF&uk0;seojz!i?*T`OAcv#4DWQ3^0AW;L8M0-DjV}_-N%>zuPrAr%n zuGF27j=%us9ZAr#;neItY3qXKD^+g#BSI<;{E*{mp7H~etLemNgbn1!=%3=4crSgN z&@u<79ka`z9V)`73l8=P4T_Ma{n;$k>aC z=n(}_%`$(KCz+KE`Ggg!AD^e=R^oJa4^fXeNEpxmX|5wV?3Wj`KO|GPAAl?n-##F9 zLQH5xSZLVJ5dU2sArv(aberXEniio=lVsZKts^M5uY(SPFMpO32a>r_D950^Dp>A) z)Ow_yrO9}lf2mQCIw|+f-cTKMHm6Tx*1^NOn&d5#Y<@Vxr+lI1LNFmCwDJ~xi_SB{ z_3W-br*w7snMah*Sg!gj7WtJNX@opWsk3X2{~gUmIvj=F(iG&1z8y*CD^y>kEElGS zZ1LcGwasen%~0Dp94&)^hBUtezGF+UTQKcejRS3gB+<0Ng-G@wz*o;9dZTA_unigG ziVB_-WIhs=OzVrWS zvPW%m330+2hix1fAm>o)edM#I4-^+=6ZMs}vSak!r`XD8Jhj_K9yCog>;a65ozkQ7 zcFM4446f*e6yH7@M_DHmy-k{e_BaV=Y-rmRr2J)`JhDT*-vQ|<@Es?=y2+*{YuO0} z;1ciLl&8v$1Ln!^Y2;Jj`TRZ){GNf}j<8dTFAr8fd2+yWViMowxNXz;aXYFWB5}NMwIZxHw>qI59j9i{SFhp9>`;}u)rs~6p0cKs4EOj} zC)AA5)nKfyos)#{2j%LGV|8n>K;un4cFKPbBM%fRHuMH!lbu*&nN1t)aHlN(a_IX& zV9a3yxrOYG`f^vmBmCMf05~^|$m38BwV{KPya?EIOartYjurP!2i` zfh@W6J=&eSQ??5Xy36SQhv=!dHEHLO5$4hDq>mdOLp=(lR83uSR(`PsojQ>OWN)hj zH8ngwTxT)vW7k`8nj)0yIif27cg&<7gipcKt4Ra*b)EM5Q znvR1VX>}*nHGGY#b}!s1N{~-zZ{E;A={@4In%@BA>!cJPbTn1{ZKa2KfZ=m&A40AR zs>AuAh9LdrNTRx$Kev4bxjvU~r3yr&6LugkXYRgl$0dkC4qt%0-O3g6uUrVyM~xjn z^88{CB|b>)u0bDPKT$28oCFV02B*0jI$Je8U|nVS(p`_U-jY3$XBm8I4nQ#QY%c@y zr6wtIS1yjirr|L&XSnueHGdjnq~-r~w_}|=`wOq5SUe#&vWS|x5pjB4(6-IA2cpBc zSanAuNZJ=O-hIE%7n+yyRH`x~3~jtdOeCQ?u094*PTD?;awNg&gblDr%W2;#qW_2v zLQ0^W_K3tovOdm+2vfIW*;~M->7GqA)~zV@^|hJvyaPhvz(mZ^=O-%X}pw%Ob@ErGgvsrPF~Ffja@cYWM5WhS*|BtkKM_bP|^8mgU*9>8SOE3g0oNXW^J1isltG!#s*%g{k$tyHI?GY zryf(ayA_jHxlh!X%22c1E=`nDQJNJfEhY$|*a~T)4od`KE2XvVfbrRFB=Q;5Sg>4_?#oOycLbjc+@2DO#WK&RvQ*;hTLh3Q`PtrzOZj5B)T` zWghVhT@jBKfZztt1-!0hXw4e%?1sYpIVq@p4)7AF@SB1n{-@vbm7r08YNW;LHv$sJ zPun2mu%rz|P|!1mNI2uu8XSVg32MNB=-8JC51Q$aBj^s&JZC;t&Lh!8^{nL!vT&92 zI%kc@2Xar6(yxOUgf;pix%s^1M}Pw_@)C%U2$eJ{G$Jk1-iv%Ydk8W?K5ioa{7U|L zKuJNdrdBGxzV|C56}u{yrf8*OiceCqOKU8ugDc?{{z7}NKz@g5)Kc9YeVsxK*}9b~ zu!2D*M9FyxsB0Hx!_mb1w5;UJgbXF^zZR8`Gz_`!`6N{V!xg(vlbV^Fd&uHCHC=@5 zHqu+J3UVrHc3AOaNzI)aO=WIkYJ#qX z9Ih=WPd@5WqMX?nQ1XL?sr8+WNB^`q&4<}$McmN6A6n0%b~kx3>xSW703>?1I?F2CW-$a z4KdjR_)_?Z4tsD9Dq!+T50=&E7gE!7$kv$9lV)M&mT={F{v)Kp}hO4C$RRyfB_Ech9?Hkw{I(ka)%Wvgrpz#LzT?HyIHDRS?dGCDZF!%^+;2v1MB zPlalyV(OaBW3f>%ZBHk_LDpQh$cCL@(7t@BKE1Jc?Yi~cKQd9siIU2wd9(lik1QAZ zK!ZTfs5NR?CJI)lA8F)$s3!Ed<@9>55wesY|pkP3h(Wr$wUI^Yk-xw{ZasQv;XUA44AE(Z{+6`GaHDq4rY+Q5bE zEbl{>x}voQDW@jr`;k%6-kYE$D7DFk5qOHAX^R2wZu9}LJFPr8>-G*7$>t6zXE$LE zM731O59ri!z`WKkNN#>@#jcVCTK$14X|HfCC+BEoNFg-j_f~TK)&1DJy9D~HkA->R zevn;lkg%4~F{qAz^?FOaVGBX@h-#R)H6-nlHa=zV{=&k&B^AHzeeu+yaPR(vRQPY% z@ZXBF!j~m!qxbGk*ro~G>*sADS1JPc?v9VrEZGwh83_LlEA0cK@63hFGxu9?7~J;c zU1{i^&b2l(MJ_$zaDh|@R?q!#x4Q@QX&0>8wa$glzrkiuRSrB6ucf_r6MYC3<1j{x zLEfYN5NKrAzNK%YIfNH;`AfKDwD)K-BL>Qu2-_CQ-yq!5phg04ZZ2+p1YrZDV^qDGKXV&gh=dVk z8;f_Wd(z3LFD!oA_>^~A_y~!|xJ~0mT8yNM1d>`~QJb4um!x6sLS0;L^d<}UUD0lF z8qr3uEar<}|6qN~>*T3+InyQ4TZH7+eKO?jMW9gYccp^;&sI%FMoZ{F>+Bg{8#Gmf^LreCj!aT$&QOXKIs7Dn;o>c^Y!j=;ynC zTLYj?;%%b8jA>mA0v!ABDq+O{f65;U-@T4ZWYrb4sQg}!Wr$8Pfg$iOj^3xs;#$5U zodWT{*D;8A@jpv=9sMFh{Y&GA=YhoO^QWzA3;>J3mTu}6tPv1IbW<0yTzbWqgpRZ)D`vl2-z*68qT!54ovMH zak1mMbYdIuIV}~`<}_x6j46u&J)BS3G2O`<>17;ULE6qsV-1;86%V!sk0Ko43@-sI z!6oKH$6mo#&PAf}S*TiXB>_V+77>7jp&p_X^;Mkl)cP)Ry^hofJ|O5%;HmI1y}cz% z9*Z=H%q<;ALiB5pKbx2ejELwX|H0qLYz*^(AGDE$UV_p13c}q^#r4Y_QN9b zw+-#Nom|T*Jfh_*KRiBb;NdfU-^yZHAl7%_ z?eegL3P^!7Z-LrQA-r+1S|hxBb=RR&>Gyq{j4N>TH!=;pkV)2eg7cT_x&$&R`W8>?8Pnm-jV0ehZ$0NK19#4&mc=EqkU8feUoMR!!24Vm}LwO>iw zsP-)K#jtiLD|bPa8$W&s0%z5!JO)DY;z z)1(2Cn;KT7xEXlJMN~_7vGgF(x<o z6q>7fhUrE=tQtC5+|1``JXK>pP-DmSaY!>YMVFR?m}PV5;5DdW@Cuqz-6YL(0?-oEh0_ddPw|Pv+p^hSz^k|N zW(BduOSIIihFq79pc(e|1O)(-XIoa{0fj6{}3?VphK@<{jqY?QDpwI<}KCF-IKNSDVT zXp9o*BoViD3<7@SW$BoAQDbv&=R*F2};hOKw_R3&DZdEL63~X%VykfRh zVd^HR9yvc^%ZchyejC7=V7~mEPpwDr_lKf`b5~osJgO8#{UoU#;y(GT8vib$z47M9 zdDt{t>FG&>f_S9_55zrUf+JMxxCXeNw6G-bFa-+)PkQ0%=e@Ix;PHH-nuzDaw+ff{~HWZFAi{}x^ zcn_vUZ*(qqk=uaM%zXg}qyzGePg$+Hu^QU3qM_V%jcvk$sE>VsD5wvfC3BkxVRS=m z4^=kw*=jtEC66^i&s`UGj(nkp`P+y4peVMc7meMil0$jmA% zkDRa*0v-+*Rgo_Y<_as|>4-dq+68hY;iRoIS!YcJM=a+L5%~wWdFJ?i4xbg}7m;<1 zpu#MyIbQ#O4XG&)D{KW~I`m9X=1q%aDl8;1)NT_Qao(@sCKb!b!k8U&_=lvi@I=Mo z>LQ<$TX^#+Q|Xm@r+$u+YyA2hz5tyZJQlAFk>(gR#2>D`W_h`MlZ#ef)CN1zi?G?! z`|5&3{FkJR)Wz)DPOW|=?uUX;YDGr^n8Hb>K*nnZF*w{?K=-6(z=W_rh@2p_mEvdi zCy!X4*1w3j9IcyW7t9o8gW{&!cH}L6&Qi^B-s{c-ZyE?I9ZKel|L{IqyAh9NRRGfs z#Qmr~DmMd9WBx5H{Q-lq`A!F8gIxDLT}Q6_J2Z7m!0qS(##g&eN<-UeN2)1+wnv1} zwh(mj`>+(MSItxjdg@=8AXw6F5Z@`3Y(jSX)o+xb@?prdi)?5^DeICGmkssHrj*QF zI&2Z~%iMVa;<8hbUf47lT_r^M9prLQ<7Tvn4Hl#mBuj8jFAl4+Y$)N6zqqA^Y>H5L zZ;y!#)$R`qNLxoosCf0%b|aAn>{7q>pwAzZY78E<4Ls;JCHJ7~T=g{q?KTv1MIolr z!Hkput>25MC~PkyF`WONRyKnpP)*_?otkcWMV`s)U+(~gX=VxD4k&MYq-CSduYvi$ zOlW_U37!*8s{*I!T>+nEE6brh4)P{lR~q9zwUN}c8cDq_Y_pn<8G&SBUp&IWlC>IH zX`?A0ReMeXfHofSzKA-Rq0g+jqV6s zF67r;xcoXKZ?qqheND+fR$A0>b#@Fs<2nNr$+%eegEG&6MFLzIq(EgT)y~AFU_Tv$ z3PVB~eRn%S7ah%#xCq~^7F2*=pO&7yFEL9A_Fl3DD^sNJg{p*RPddtf$$OIHlH!v3 z#3t=Yj7x}1>=OgFX=H*jAwGWZ9^h?36nraGDDIao047hzaCi6`)ly=SngGp|D@I3QsE8hE!`iL{lx zS_GD_FZ!*)Y0*NN=qe@w_agk_$c9ZjmY^&#C==mg$3`bQ7^{;LcP+r@!q5)Jg58bC z7m*Zdy##%EITOFP1W%WiGS&7*h7Ds{#mTTl+g@wDl zeLN5Oo!7Ftf@!+iYg^{x+m=$eaQQ%ONmW1v=GE7NWGvMfW#lC8&$6i4esGyaze-rR z({qbwf%gS1I}O1g2SDF}l86P=4?)^K*%Rq4Bqdrg&V{CX*YD5$#i`{_QVQHRAgX_+ zGgZyQoA>9w+JptEKJHj%dNWXmWh;`E;>kmKbrnHHkj4(qZaqpumnhZ-1)z~C9oIs1 zyJCX310}qUp#DMo!+dh=_6S!}4Dkg*$7}J}=$GWjhg1zv02is-NXI_Hv=I3`V(;#G zNxOcEUMaMn1=)60OU)7Tf=u()&6YdC&+d(b|?u_Ny&rOuA=FgDr5PBoDmFp;UFX+H& zxS+mpYQp8=v@auKd=a%hhD3Znw6tg<^{h(P!K>PU(VGwzkq|+<4Z}oDrJ_BF!t`4~ zwYYFP){{i-X!|nN2FM)MFQd()%3tQ`*lIt;W&$={XT@gTq7cW>lYmnzb7J^ zJcVoemOC2pZJI!zSS#%oZtlw|Gvo;LyX)u>RO}*nW&q|52=v5ye3Ln7M!TyGyJFj_ z-t1|lQ;bO+o$!drsfMk6zQyqXnoUQ;vRIroA7?#?t9>9$kM!FBF@AV9|Kk{~?jX@1 zbTt&IZ@@4}ov;|csYnmMN;Vb;S4At^#*6i#jgfiB@*1|6xaPU#24;lrR~86j8-8yo zH8RH1tw&t$M2*Yvr;-|_@^|bN35OR;*JcQMJ!}Iq5Dv>x9eVNGTRDF}ko&*LK}BBm zm#!kRa29fL=5!o97;|ccIJa#!srD7<0HoOt%H6RDcE-sqItJ;(!=VOB`!`^TI2Yjno(!sFnoTuJt6*KcT(ti8O1h zLjRbRfc_*l)MZ`M9%Xa4gwwu@iC;PEjLMS>u1(Uj>5m9&!);aUq&wKx%1KfzQxaHIiyrn;M z?Oyn4=xuHZn1-I5hmy=wrsQ*SAyhD(m*QYsr1jZ6Iz&e0(t+rw3ZKMnIQ<*^w1EIR&G4VeA`jL5L1VAOb%SJ(e0HYe&hi-4P z58d8mA8>~!kz2MLztar-&MdK*r*`NVGa??&I@&}>OeJW11@`7SeqXVi!39;1Bm@4P zLy-p=k|=?HqWbh5?tmK?Rx=aHX%rR_Cl?~g`F6E+-nf0PMn8{Q|3qXzzQT5+M(}rn z#T)9PfMelxb;_S#7PDDjTd?n)h&3868HV-+ZH1hTKER=~cpqS;co)3{PiN#Grl!Z? zdT&3imHSGOoUbEQ^5PO6>H@y_9o%wJ%f1%?gFF=Mm9qTA_C800n-dC-vFpUGhAOTT))yp4L5*eVer91^ z)$YOtNm_0fFd|_W%89r=IVZTDnsgzN(ed%o+LH#054YnzINFV9(hO&dV-2cscG_bg z@em@=yf8}hAs(Oz2j&B6or$p}G(~vixLf9d3dC=!vAR*3;H=;WcthZ67tznAQ|=OH z&p@Paz4;N{)n}wxs|-i%qzWYKK{9P2^3INZA4`i+2m{GV1uUM@n}BuIPbL+qMF*NK z_Hp*4Dd6HFyGm{0rpc~nVQ;uO#uAoGoa9A9`e>(g>*v_$6ydM&b^S^`O)x+fi>>Yv@ zR!^t{LO#|VY;kI>LgBJmA4rzV0~C(C{oFkF!z7N7hC(1nWPNTleyXM2;fM>~WZq3z z*2mzSp^4X6n@uOr#cAr|HfA`ZV7PmQ+>NOxvN>!Lvw*{ax|FPk4%eB5&Uld2XYg#E{s762KeTf3>`=AD6dOwdfFGZ<9VHa}dqys0z62 zR-M@9B_T$A?Dz?qHgNbV?~eq1W7Jw%>JF(B%p`=97OV_>5J^IDxnl ztFDedmYm(FJn%`z_s^h8j;7I5b(7<7f-B7@>0iOKvqWF@?GAQT)|5rVJD%l2emrc9lhF(PqcU$dzxU`<0 z+H=So&IBmGj0!0IoP?qdc!i(vu6$nz@d0r`F}s2nulHWyMm4zq=!Z&;P}@Nb9crv6 zr3q^l_c1r@-o^9%`Em^tA5**Yq$<3w2;+g;{zgQQmCO%4wa8G^x)(Y*xd(32#41M0 z-FI=q!%MaUOlU(PY3xyg<$F5v7$aa8mJp_*=CB4TYH5p`g4R(DMLh4_thl)qd2hooTouDFF&^d_AS_<8#=00mh4tt>cv#hK^^b18hFUfWXAHOmD&q>)$vZ#849u z<@Z~!LBKXj*yEb*-yD50x`6h%NDz#d7(DpGJoHE0K;B||#FEC4D(8wx7rpBvA1beK z^70wrUMbYP#p&c)%TzLtFT&6tz6}V!a5pm#vN%&lqSbd8HEAZf=pztTK_`5MD|<#& z&bW?x)bws-94xI0&%w^fYF^XZ&OLm`Xw>weCLvtTIC!KuykMbLK6Heo2v;%=mXwF* zeyD|BBg=uHd4BPc$OCx$gD^AN-<{_Ve#LC1Ca^SPe1LUhUSwk`6yXMsYnNd=k{W6qo>>Xia8H&4!Z;i26ThSwy~F*|ri}0L9JIA5*j66=#nXHa0dOV3&WVQ$cSS zdY#mj=YEo`scPRtJdfjtH$i9*q6jkj2tnk+gB(Dk|oiOHE-6?67V%-Q~#W#$$gcp>XB zJ)Nn5#3(=LWphEtL9d#(B<1b6NzL0yoFXbcd|z0)5_@@!i`cBF3R2EXWFSrcm`jzw zr~EaS5*u8VA`vAU8#?4NJlxx(l3V_ZZ7N78-qJODEiIZrdtyEqM{)8#%fkAMT-WYc zU37%Mo7b*Y0o1~t@ z$EpwKRaYIXphHd(=X^8gm(0sP7z0PY>uLp!P{1E?0a zQhnzyI_@gzSKgLx;5xT}_;ZIT`8HYkW+PBoW3bZnqJ2IAYaZZ4)D&SBtBD&%0lFW{ zYhW?$E`x%C&bCID*k_mfuWr{sUX3wO;vL?>E`UMUZbTfC5316p$QK~Tj?#NQ=r6Lx zlK^Oq3=Sn4HmMiYZX@EE*RlEvssUPq+AksAzWY{|Z!6l-VbNas@|;DXA!pIixA5rO zzJ=R(-$DhQG?icM7MKlcHUktYEPD-D{GjDKxFdz>W&kCtTC?Bal9981u9J@eMd*mi z4kxKWZp|4aHNfRiTQ)-$_rE$g6V#{-DJ3wG8FZZhk*x76sVa53b6QF z0B_{AM(zdJqSdF17f_S$2^KXvaX!*L$Y$FiuRz6L5SUj{`$)W|FUdATcZ`pGwk+9X zu<+E*4>*bVpgp_dLc>>h`!3tuv6(9iYg#1CoC#QwQOE2e=hNOiShb5HIT8i;9d1Gb z+*x1Yo{=gcm&a>C9uFYD4TJ zp5!(1F$MV1)5*P`tpSOYxHu@VhrXzxhpwI zA^u)IIli~FQl79dp_YS@0>mPPp~ofFKpcau6xzIq9`k{1X+`2?Xua5QnEgzA54iyO zgBvrQ!`=G#XVw^h-f$Dz96_o5i)*Kb;U7oNlQg`3v%6ErZoQ80?qt}_UyFk3>mItJ zRIr8eWty&iG?I5dYdW$lEu-kzOboXqeGVvHFwc{dN$vX6ZtNo2dDQucQZ{+M?-$nD zLR5eHaDn2QOf~?aO@-YgWC}#ly9U8;>W^RIO?&AHsHnoV-2G_f$T4WKNSXhMFHuxO zA*s!u@R?7A^#sm1JnQ$#su_R{%V{Xx>#v^gkV+i;Gnq%V08Y$Mh+|O@LWXmgyjgwc z&U8dVCe<8jZ}*Mi<8CE*g!}qyv!q~fZ4FX2wedTPrdv|cft1>g;*K62dI}JVb5auR z2Po5Bjz=8XpCBdp&w^}w1PJSY7Uce=z#sp!Ap5^rknI5ce3bw=Jhyt4q$#p&hDJt3 zpSh!j%a|wlvKaVLV}E9Lvbyal>Dfi6`rEN3r5#fq=?euaWz0tI1hl^+vv29!J_kff zB`g6ToK*1{$1m!AF3{fa_@{DOtznz2`=4!+LjoPsgyXs7?04~W=y*IZc%{F8`(`rO z-84uI4^AiX(R-sJ+u8_244|%3i0hdJ9fH6q9pgp}y*d6ar`#2FLk{ENi=NC-evi-5gpNbC+ zTSuzJ+#QDOiNU$t9I$(Rv?drFW=~JIET4PYT$f$Kyx;)eSglyWFldcSpwjRHYm>|~ zYAM8kuQOooAo9SyMS&Av8c@~J8hl2)Edmdwc$!vDr<1?J^nS#t18%U|jvYY~ZyPM? z6OZw1DTm}-%|1k}j}Sx8Ql3oJ^CDsg$GJ_(ZojS|auNgzh^$j2Ra%fkAgXg;JK?DT z|Jx^YK)41%#xQK}LX50O3lUdrLb)l77mv%Dk}o$~G(}P}ldKHd>gjf9`&sR`=pDT^ zY=QN5A&93Gda9d5#is2+L#dUPd|Ys;zPcI?7^r6aUkO8E zGBp`Z^mC+H<1g5h)+q0PqsWPBqt{%q-f<_j5nLlZlMxpKl7n%E8EVTBHPRMx2IC$ z%jD?In;A!&n=>}fo*lk%If4m$Nc&8ZKA=WHxFG!im}6sf3)LM1rtdW2^%afj2Z-zW zJ+kkNNmYcO-MN;IVc0O0kjh=fbG39Vs9bbRCV_DtE0b|R@C@mTDWN2C$hU%Yp4g51 zR>A++fDGd0BSr`840PH`yKW-~U7AwLvtKsVe3o%Tc}Z*I-R2kNoA13JUFi=c9!$te%E_*+K2V%nd9W_?3W%>zmn&7Y{!HZX_S>W}K!_fq zb9PwLG8YAy%=}6)ZU@t0?Zg|1@;98M<^R_(dMQNdd;T~(65<8xQh;>>>R|}kvVZ&0 z7~Cg2XC5ROsYR};2M1!lG?5x%*mKMTdP-41E2y>&#(EL@S)src0YZumHT|n&_{ZRx zi~wv4ygZPZ#r9O9Z_3dt$Ycc57{P#s<>%FE(Gc}xNPkqbo*j!R6^;?n#VcqY;3r1& zNC2950J8cqvhBLHgB4igQN79ldD_5){|j&fpddS8u1|-N-c0TA%oHEQxl%jBSnH(N zl)ZsEg2 zTwJ^?IpiA3E7n+w&QR;?_2=(C(!q+CO;fGGD{z zusJ`v>h)&doqiCY<)>Sju{AIBfbtURJ~xH1VE>&MP<~ajvH58fu=6Ti(wZmF+|%Y+S+5byAd6n4VDtc}YY}z!^_&w?m^B=vtUgf&Ov>Rm>sl1?Q-!t=l>Z{EZxk)MH8q zna#=_V#=a^7|?FC_XFT|x(F?}k$l4tI-$m}L63M^l)E5&a0K>MQ65?iDE3d(4C+ez zT=WT4gV+plS$~D}oFw|^1Xss^-0}oheJ$${x;Vbh6qs)yZglt!;`IQj7%QA|2{pZi za8(i!z`Q;C2j!$9P}Fwe?5b$;=+-+|8@H(5i! z<7L}_$rtkk>M$NC>SChPSvVaJtw$O@|tkl?f*lRQekx)ZdywZ zc_<}F(N67(SZ-eHb#iSt*O|uH4dbgvboAc%7@&NvACtq0DZkf;hmr+y=PT3!;U1UC zG0xR}|5`CZL8+{1p_z-sk zu`e~Nf;%4ADX0itYr`;OkzZMLa3!YpF5i8UZ(_B3JI|>dt)rQ*~I9UyHdMp z{7jguMuu8D)}sy0He@+U2I#osnd^5?JB>*^D!!Lng+}ycz!hdw0XYF1Vbjuy{tn^P z=oMyLAvO9MDwph(v<9#-YMu;b1pvN_mk@b|^rP?sFysD61yH+H#5B7;<-}o&!Pe_dBcBf2>&1u=V8sL)Le|MRjdo+qg5HOHe5a!*K?%f?Z)KqCriJ zEs0TMY*9d(bU_8_AYB9ty$e!A1VOMD?6I4~mSW;ded&oNIfEC-x6U0jzW?{~dopvT z-+S&ZYp=bwei+RK=A^h(R=>X~q7JpdEa9E4K|%f%!51_}$V%&FT3l~wgO01^9f^FC ze4^S*?fxcl3XdJY9Uqg8OjxD!#~Ba9FoO_Nf2qI1BqSN>m!L3dPOb&C^qL4p{8+>% z0~36u(TGoGB_Kfhd~X6@w?{-{(#Kbtiil=gAFOxI>4QpXBFC%TLu2M?#w$#o+?o8d z@C@pKKi)wo^5a8^LF>Smq)p6$QYjdFW(>0>bR93miMvcV*rPMyBA@8% z#%Pl-q@e=)l)Wlrkc?k_GhUQ@zvfIBUh)-fAZlB})xWk%%2=p>!Nc>Ip#L6bnE&9? zgLnRd)z}6x$344G>^>g4@4X#%IWzNoo4F>nh`}p;r8NA)kx; zjN&C<3%7zXNX$q#hfd=$C>PE@2jTqAC)gJ0BPGN4B_WO)48=qJ^CiQhKdMTkb`ql) zCn-Vuj8pTkLj|B;^QhxY!rth@_M!GljKpTBHsv+%`Ji3(8@BJY9w;PjoJUOYlS zl!hSu6ST+@MmRx>+CuL8Z!2lC^A<^?5e1HTyulFIq%K=Ymn(I~rB1b@5IoN3H&9suAsvSrq6ji1@hYH~32l#c z*bG`-Bhmdev~vp)AxlI>c-#ct6f^0v;@|uFfdla1PYEle2BXQ_vlM!Bf34>>-WL*h zo|E=a5nzWJQ{Opv0dB$JDcXL3<^nF0j1XJ=GLmQ`F-QG~^n=-^yzrv{1VWkCZ1x(x& zKpoGPdp;L@j1aPo9+z{b$Tz*S&TRGCqendmJAK8Xx;Kb?j| z;MqwIO;6P2LaQ*yOhP#RBFqYt?V#BC5z&oGLZ3bzl*J=D@Pz!SK^F0mlmhH&U!*pe zr^qMXDX3KtX^ZA#XL~SH4jRfsNJ7`-e8o+#%C+vk$e5&fRR11||99{oG--?>F7SNl zU+?1q`oVySAj%B?5aY8!vu+a#GV(KW)ivd{`%0^*BR3#}fQx+v3nf$X*|_5i$@_l` zcYMn{+yn86T_Y898z8Uhkr`!(EjhN7@OAr>T6y4TXls>vsof^lLqlVsP`m1Kj6_FF9UH)n`=fvk;`mR*=NY2xHAE|{-2zrhW#BPQB{~hjN|`Z#tUBB; zXgbfQx4-nrsajG#O(wg_xP}`4* z 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 literal 0 HcmV?d00001 From 4e6635fa2afa5367a08160a368233f4f0dbcf1cf Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Thu, 1 Aug 2024 16:50:49 +0900 Subject: [PATCH 162/263] =?UTF-8?q?feat:=20BBNavigationBar=EC=97=90=20?= =?UTF-8?q?=EC=83=88=EB=A1=9C=EC=9A=B4=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#587)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FamilyManagementViewController.swift | 8 +- .../Home/Views/ContributorView.swift | 2 +- .../Base/BBNavigationViewController.swift | 5 + .../Sources/Base/ReactorViewController.swift | 4 +- .../BibbiAlertViewController.swift | 4 +- .../{BibbiButton.swift => BBButton.swift} | 15 ++- .../BBNavigationBarButtonStyle.swift | 11 +- .../BBNavigationBarView.swift | 122 +++++++++++++++++- .../familyNameChange.imageset/Contents.json | 12 ++ .../familyNameChange.imageset/icon_change.svg | 3 + 10 files changed, 168 insertions(+), 18 deletions(-) rename 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBButton/{BibbiButton.swift => BBButton.swift} (80%) create mode 100644 14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/familyNameChange.imageset/Contents.json create mode 100644 14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/familyNameChange.imageset/icon_change.svg diff --git a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/ViewController/FamilyManagementViewController.swift b/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/ViewController/FamilyManagementViewController.swift index c925e1d9c..f9eee0061 100644 --- a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/ViewController/FamilyManagementViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/ViewController/FamilyManagementViewController.swift @@ -17,7 +17,7 @@ import SnapKit import Then fileprivate typealias _Str = FamilyManagementStrings -public final class FamilyManagementViewController: BaseViewController { +public final class FamilyManagementViewController: BBNavigationViewController { // MARK: - Views private let shareContainerview: InvitationUrlContainerView = InvitationUrlContainerDIContainer().makeView() @@ -60,7 +60,7 @@ public final class FamilyManagementViewController: BaseViewController { private let secondProfileView: ContributorProfileView = ContributorProfileView(reactor: ContributorProfileReactor(initialState: .init(rank: .second))) private let thirdProfileView: ContributorProfileView = ContributorProfileView(reactor: ContributorProfileReactor(initialState: .init(rank: .third))) - private let nextButton: BibbiButton = BibbiButton() + private let nextButton: BBButton = BBButton() let contributorRelay: BehaviorRelay = .init(value: FamilyRankData.empty) diff --git a/14th-team5-iOS/Core/Sources/Base/BBNavigationViewController.swift b/14th-team5-iOS/Core/Sources/Base/BBNavigationViewController.swift index 50ddf4f96..ab049d5f7 100644 --- a/14th-team5-iOS/Core/Sources/Base/BBNavigationViewController.swift +++ b/14th-team5-iOS/Core/Sources/Base/BBNavigationViewController.swift @@ -16,6 +16,7 @@ import ReactorKit /// /// 이 ViewController를 상속하는 ViewController는 `View`가 아닌 `ContentView`에 UI를 배치해야 합니다. /// `ContentView`는 NavigationBar 영역을 제외한 나머지 공간을 차지하는 View입니다. 이 View는 NavigationBar의 높이에 따라 동적으로 변합니다. +/// 물론 `ContentView`가 아니라 `View`에 UI를 배치해도 상관없습니다. /// /// 삐삐 스타일의 NavigationBar의 UI나 스타일을 변경해야 한다면, 직접 관련 메서드나 프로퍼티를 통해 변경할 수 있습니다. /// 가령, NavigationBar의 높이를 바꿔야 한다면. `setNavigationBarHeight(_:)` 메서드를 호출하면 됩니다. @@ -95,6 +96,8 @@ open class BBNavigationViewController: ReactorViewController where R: Reac open override func setupAttributes() { super.setupAttributes() + + navigationBarView.layer.zPosition = 888 } } @@ -119,8 +122,10 @@ extension BBNavigationViewController { } } + /// NavigationBar를 View의 맨 앞으로 가져옵니다. /// contentView가 아닌 view에 새로운 UI를 배치할 때, 꼭 호출해주어야 합니다. + @available(*, deprecated) public func bringNavigationBarViewToFront() { view.bringSubviewToFront(navigationBarView) } diff --git a/14th-team5-iOS/Core/Sources/Base/ReactorViewController.swift b/14th-team5-iOS/Core/Sources/Base/ReactorViewController.swift index 206887f69..8c83e9b24 100644 --- a/14th-team5-iOS/Core/Sources/Base/ReactorViewController.swift +++ b/14th-team5-iOS/Core/Sources/Base/ReactorViewController.swift @@ -48,5 +48,7 @@ open class ReactorViewController: UIViewController, ReactorKit.View where R: open func setupAutoLayout() { } - open func setupAttributes() { } + open func setupAttributes() { + view.backgroundColor = .bibbiBlack + } } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertBuilder/BibbiAlertViewController.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertBuilder/BibbiAlertViewController.swift index cd81bff95..9d21fbca2 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertBuilder/BibbiAlertViewController.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertBuilder/BibbiAlertViewController.swift @@ -21,8 +21,8 @@ public final class BibbiAlertViewController: UIViewController { private var mainTitleLabel: BBLabel = BBLabel(.body2Regular, textAlignment: .center) private var imageView: UIImageView = UIImageView() private var buttonStackView: UIStackView = UIStackView() - private var confirmButton: BibbiButton = BibbiButton(type: .system) - private var cancelButton: BibbiButton = BibbiButton(type: .system) + private var confirmButton: BBButton = BBButton(type: .system) + private var cancelButton: BBButton = BBButton(type: .system) // MARK: - Properties var subTitle: BibbiAlertTitle? { diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBButton/BibbiButton.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBButton/BBButton.swift similarity index 80% rename from 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBButton/BibbiButton.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBButton/BBButton.swift index f57bc8895..acddc90d4 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBButton/BibbiButton.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBButton/BBButton.swift @@ -9,20 +9,21 @@ import UIKit import SnapKit -final public class BibbiButton: UIButton { +final public class BBButton: UIButton { // MARK: - Views + public var bibbiTitleLabel: BBLabel = BBLabel() // MARK: - Properties + public override var titleLabel: UILabel? { - get { - return bibbiTitleLabel - } + get { bibbiTitleLabel } set { } } // MARK: - Intializer + public override init(frame: CGRect) { super.init(frame: frame) setupUI() @@ -33,7 +34,8 @@ final public class BibbiButton: UIButton { } - // MARK: - Intializer + // MARK: - Helpers + private func setupUI() { addSubview(bibbiTitleLabel) @@ -42,14 +44,17 @@ final public class BibbiButton: UIButton { } } + /// 버튼의 타이틀을 변경합니다. public override func setTitle(_ title: String?, for state: UIControl.State) { titleLabel?.text = title } + /// 버튼의 타이틀 색상을 변경합니다. public override func setTitleColor(_ color: UIColor?, for state: UIControl.State) { titleLabel?.textColor = color } + /// 버튼의 타이틀 폰트 스타일을 변경합니다. public func setTitleFontStyle(_ fontStyle: BBFontStyle) { if let titleLabel = titleLabel as? BBLabel { titleLabel.fontStyle = fontStyle diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBarView/BBNavigationBarButtonStyle.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBarView/BBNavigationBarButtonStyle.swift index 644284261..6c351d9bf 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBarView/BBNavigationBarButtonStyle.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBarView/BBNavigationBarButtonStyle.swift @@ -21,15 +21,16 @@ public extension BBNavigationBarView { enum TitleStyle { case bibbi - case newBibbi } enum TopBarButtonStyle { case addPerson case arrowLeft + case change case heartCalendar case setting case xmark + case none } } @@ -40,8 +41,6 @@ extension TitleStyle { switch self { case .bibbi: return DesignSystemAsset.bibbiLogo.image - case .newBibbi: - return DesignSystemAsset.bibbiLogo.image } } @@ -54,13 +53,17 @@ extension TopBarButtonStyle { case .addPerson: return DesignSystemAsset.addPerson.image case .arrowLeft: - return UIImage(systemName: "chevron.backward") + return DesignSystemAsset.arrowLeft.image + case .change: + return DesignSystemAsset.familyNameChange.image case .heartCalendar: return DesignSystemAsset.heartCalendar.image case .setting: return DesignSystemAsset.setting.image case .xmark: return DesignSystemAsset.xmark.image + @unknown default: + return nil } } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBarView/BBNavigationBarView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBarView/BBNavigationBarView.swift index 28c627a03..32834b518 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBarView/BBNavigationBarView.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBarView/BBNavigationBarView.swift @@ -41,7 +41,7 @@ public final class BBNavigationBarView: UIView { } // NavigationBar의 FontStyle을 바꿉니다. - public var navigationTitleFontStyle: BBFontStyle = .head2Bold { + public var navigationTitleFontStyle: BBFontStyle = .homeTitle { didSet { navigationTitleLabel.fontStyle = navigationTitleFontStyle } @@ -76,7 +76,7 @@ public final class BBNavigationBarView: UIView { rightBarButtonItem?.image, for: .normal ) - setupButtonBackground(rightBarButton, type: leftBarButtonItem) + setupButtonBackground(rightBarButton, type: rightBarButtonItem) } } @@ -142,6 +142,11 @@ public final class BBNavigationBarView: UIView { } // MARK: - Intializer + public convenience init() { + self.init(frame: .zero) + setNavigationBar() + } + public override init(frame: CGRect) { super.init(frame: .zero) setupUI() @@ -233,6 +238,116 @@ public final class BBNavigationBarView: UIView { // MARK: - Extensions extension BBNavigationBarView { + + /// NavigationBar의 속성을 바꿉니다. + /// + /// - Parameters: + /// - title: 네비게이션 바의 타이틀 문자열 + /// - titleColor: 네비게이션 바의 타이틀 색상 + /// - titleFontStyle: 네비게이션 바의 타이틀 폰트 스타일 + /// - leftBarButtonItem: 네비게이션 바의 왼쪽 버튼 스타일 + /// - leftBarButtonTint: 네비게이션 바의 왼쪽 버튼 강조 색상 + /// - leftBarButtonItemScale: 네비게이션 바의 왼쪽 버튼 크기 + /// - leftBarButtonYOffset: 네비게이션 바의 왼쪽 버튼 Y 위치 + /// - rightBarButtonItem: 네비게이션 바의 오른쪽 버튼 스타일 + /// - rightBarButtonTint: 네비게이션 바의 오른쪽 버튼 강조 색상 + /// - rightBarButtonItemScale: 네비게이션 바의 오른쪽 버튼 크기 + /// - rightBarButtonYOffset: 네비게이션 바의 오른쪽 버튼 Y 위치 + public func setNavigationBar( + _ title: String? = nil, + titleColor: UIColor = .gray200, + titleFontStyle: BBFontStyle = .homeTitle, + leftBarButtonItem: TopBarButtonStyle = .none, + leftBarButtonTint: UIColor = .gray300, + leftBarButtonItemScale: CGFloat = 1.0, + leftBarButtonYOffset: CGFloat = 0.0, + rightBarButtonItem: TopBarButtonStyle = .none, + rightBarButtonTint: UIColor = .gray300, + rightBarButtonItemScale: CGFloat = 1.0, + rightBarButtonYOffset: CGFloat = 0.0 + ) { + self.navigationTitle = title + self.navigationTitleTextColor = titleColor + self.navigationTitleFontStyle = titleFontStyle + + setNavigationBarAttributes( + leftBarButtonItem: leftBarButtonItem, + leftBarButtonTint: leftBarButtonTint, + leftBarButtonItemScale: leftBarButtonItemScale, + leftBarButtonYOffset: leftBarButtonYOffset, + rightBarButtonItem: rightBarButtonItem, + rightBarButtonTint: rightBarButtonTint, + rightBarButtonItemScale: rightBarButtonItemScale, + rightBarButtonYOffset: rightBarButtonYOffset + ) + } + + /// NavigationBar의 속성을 바꿉니다. + /// + /// - Parameters: + /// - title: 네비게이션 바의 타이틀 문자열 + /// - titleColor: 네비게이션 바의 타이틀 색상 + /// - titleFontStyle: 네비게이션 바의 타이틀 폰트 스타일 + /// - leftBarButtonItem: 네비게이션 바의 왼쪽 버튼 스타일 + /// - leftBarButtonTint: 네비게이션 바의 왼쪽 버튼 강조 색상 + /// - leftBarButtonItemScale: 네비게이션 바의 왼쪽 버튼 크기 + /// - leftBarButtonYOffset: 네비게이션 바의 왼쪽 버튼 Y 위치 + /// - rightBarButtonItem: 네비게이션 바의 오른쪽 버튼 스타일 + /// - rightBarButtonTint: 네비게이션 바의 오른쪽 버튼 강조 색상 + /// - rightBarButtonItemScale: 네비게이션 바의 오른쪽 버튼 크기 + /// - rightBarButtonYOffset: 네비게이션 바의 오른쪽 버튼 Y 위치 + public func setNavigationBar( + _ image: TopBarButtonStyle? = nil, + imageScale: CGFloat = 1.0, + leftBarButtonItem: TopBarButtonStyle = .none, + leftBarButtonTint: UIColor = .gray300, + leftBarButtonItemScale: CGFloat = 1.0, + leftBarButtonYOffset: CGFloat = 0.0, + rightBarButtonItem: TopBarButtonStyle = .none, + rightBarButtonTint: UIColor = .gray300, + rightBarButtonItemScale: CGFloat = 1.0, + rightBarButtonYOffset: CGFloat = 0.0 + ) { + self.navigationImage = image + self.navigationImageScale = imageScale + + setNavigationBarAttributes( + leftBarButtonItem: leftBarButtonItem, + leftBarButtonTint: leftBarButtonTint, + leftBarButtonItemScale: leftBarButtonItemScale, + leftBarButtonYOffset: leftBarButtonYOffset, + rightBarButtonItem: rightBarButtonItem, + rightBarButtonTint: rightBarButtonTint, + rightBarButtonItemScale: rightBarButtonItemScale, + rightBarButtonYOffset: rightBarButtonYOffset + ) + } + + private func setNavigationBarAttributes( + leftBarButtonItem: TopBarButtonStyle = .none, + leftBarButtonTint: UIColor = .gray300, + leftBarButtonItemScale: CGFloat = 1.0, + leftBarButtonYOffset: CGFloat = 0.0, + rightBarButtonItem: TopBarButtonStyle = .none, + rightBarButtonTint: UIColor = .gray300, + rightBarButtonItemScale: CGFloat = 1.0, + rightBarButtonYOffset: CGFloat = 0.0 + ) { + self.leftBarButtonItem = leftBarButtonItem + self.leftBarButtonItemTintColor = leftBarButtonTint + self.leftBarButtonItemScale = leftBarButtonItemScale + self.leftBarButtonItemYOffset = leftBarButtonYOffset + + self.rightBarButtonItem = rightBarButtonItem + self.rightBarButtonItemTintColor = rightBarButtonTint + self.rightBarButtonItemScale = rightBarButtonItemScale + self.rightBarButtonItemYOffset = rightBarButtonYOffset + } + +} + +extension BBNavigationBarView { + private func setupNavigationImageScale(_ scale: CGFloat) { navigationImageView.layer.transform = CATransform3DMakeScale( scale, scale, scale @@ -258,9 +373,11 @@ extension BBNavigationBarView { button.backgroundColor = .clear } } + } extension BBNavigationBarView { + @objc func didTapLeftButton(_ button: UIButton, event: UIButton.Event) { guard let _ = button.currentImage else { return } delegate?.navigationBarView?(button, didTapLeftBarButton: event) @@ -270,4 +387,5 @@ extension BBNavigationBarView { guard let _ = button.currentImage else { return } delegate?.navigationBarView?(button, didTapRightBarButton: event) } + } diff --git a/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/familyNameChange.imageset/Contents.json b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/familyNameChange.imageset/Contents.json new file mode 100644 index 000000000..cbf97adcb --- /dev/null +++ b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/familyNameChange.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "icon_change.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/familyNameChange.imageset/icon_change.svg b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/familyNameChange.imageset/icon_change.svg new file mode 100644 index 000000000..ee7fd9af3 --- /dev/null +++ b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/familyNameChange.imageset/icon_change.svg @@ -0,0 +1,3 @@ + + + From def01b41e56e1e9a8423806f763063543754e67c Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Fri, 2 Aug 2024 07:44:16 +0900 Subject: [PATCH 163/263] =?UTF-8?q?move:=20BibbiNavigationBarView=20?= =?UTF-8?q?=ED=8F=B4=EB=8D=94=20=EC=9D=B4=EB=8F=99=20(#587)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BibbiNavigationBarView/BibbiNavigationBarView.swift | 0 .../DelegateProxy/RxBibbiNavigationDelegateProxy.swift | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename 14th-team5-iOS/Core/Sources/{Bibbi/BBCommons => Trash}/BibbiNavigationBarView/BibbiNavigationBarView.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi/BBCommons => Trash}/BibbiNavigationBarView/DelegateProxy/RxBibbiNavigationDelegateProxy.swift (100%) diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BibbiNavigationBarView/BibbiNavigationBarView.swift b/14th-team5-iOS/Core/Sources/Trash/BibbiNavigationBarView/BibbiNavigationBarView.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BibbiNavigationBarView/BibbiNavigationBarView.swift rename to 14th-team5-iOS/Core/Sources/Trash/BibbiNavigationBarView/BibbiNavigationBarView.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BibbiNavigationBarView/DelegateProxy/RxBibbiNavigationDelegateProxy.swift b/14th-team5-iOS/Core/Sources/Trash/BibbiNavigationBarView/DelegateProxy/RxBibbiNavigationDelegateProxy.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BibbiNavigationBarView/DelegateProxy/RxBibbiNavigationDelegateProxy.swift rename to 14th-team5-iOS/Core/Sources/Trash/BibbiNavigationBarView/DelegateProxy/RxBibbiNavigationDelegateProxy.swift From 05c110885002b940c9c3bac8a57bb901d2a33f60 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Fri, 2 Aug 2024 21:26:27 +0900 Subject: [PATCH 164/263] =?UTF-8?q?feat:=20BBNavigationBar=EC=9D=98=20set?= =?UTF-8?q?=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=9D=B4=EB=A6=84=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#587)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FamilyManagementViewController.swift | 1 + .../BBNavigationBarView/BBNavigationBarView.swift | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/ViewController/FamilyManagementViewController.swift b/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/ViewController/FamilyManagementViewController.swift index f9eee0061..55ffdf80e 100644 --- a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/ViewController/FamilyManagementViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/ViewController/FamilyManagementViewController.swift @@ -225,6 +225,7 @@ public final class FamilyManagementViewController: BBNavigationViewController Date: Sun, 4 Aug 2024 10:05:47 +0900 Subject: [PATCH 165/263] =?UTF-8?q?move:=20BibbiToastMessageView=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=9D=B4=EB=8F=99=20(#591)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BibbiToastMessageView/BibbiToastMessageView.swift | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename 14th-team5-iOS/Core/Sources/{Bibbi/BBCommons => Trash}/BibbiToastMessageView/BibbiToastMessageView.swift (100%) diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BibbiToastMessageView/BibbiToastMessageView.swift b/14th-team5-iOS/Core/Sources/Trash/BibbiToastMessageView/BibbiToastMessageView.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BibbiToastMessageView/BibbiToastMessageView.swift rename to 14th-team5-iOS/Core/Sources/Trash/BibbiToastMessageView/BibbiToastMessageView.swift From d21e36833cc183a327ace6283d22e9bd65035ee8 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Mon, 5 Aug 2024 11:26:14 +0900 Subject: [PATCH 166/263] =?UTF-8?q?feat:=20BBToast=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?(#591)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FamilyManagementViewController.swift | 19 +- .../Bibbi/BBCommons/BBToast/Animation.swift | 56 ++++ .../Bibbi/BBCommons/BBToast/BBToast.swift | 302 ++++++++++++++++++ .../BBToast/BBToastConfiguration.swift | 88 +++++ .../BBCommons/BBToast/BBToastDelegate.swift | 26 ++ .../BBToast/BBToastViewConfiguration.swift | 43 +++ .../Bibbi/BBCommons/BBToast/Background.swift | 17 + .../Bibbi/BBCommons/BBToast/Direction.swift | 42 +++ .../Bibbi/BBCommons/BBToast/Dismissable.swift | 19 ++ .../BBToast/ToastViews/BBToastView.swift | 12 + .../DefaultToastView/DefaultToastView.swift | 115 +++++++ .../DefaultToastView/IconToastView.swift | 71 ++++ .../DefaultToastView/TextToastView.swift | 45 +++ .../Core/Sources/Bibbi/BBHelper.swift | 45 +++ .../Sources/Utilities/MulticaseDelegate.swift | 31 ++ 15 files changed, 924 insertions(+), 7 deletions(-) create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/Animation.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToast.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastConfiguration.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastDelegate.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastViewConfiguration.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/Background.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/Direction.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/Dismissable.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/BBToastView.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/DefaultToastView.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/IconToastView.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/TextToastView.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBHelper.swift create mode 100644 14th-team5-iOS/Core/Sources/Utilities/MulticaseDelegate.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/ViewController/FamilyManagementViewController.swift b/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/ViewController/FamilyManagementViewController.swift index 55ffdf80e..4aaa87f67 100644 --- a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/ViewController/FamilyManagementViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/ViewController/FamilyManagementViewController.swift @@ -133,18 +133,23 @@ public final class FamilyManagementViewController: BBNavigationViewController() + + public private(set) var config: BBToastConfiguration + + + // MARK: - Toast + + public static func text( + _ title: String, + titleColor: UIColor? = nil, + titleFontStyle: BBFontStyle? = nil, + viewConfig: BBToastViewConfiguration = BBToastViewConfiguration(), + config: BBToastConfiguration = BBToastConfiguration() + ) -> BBToast { + let view = DefaultToastView( + child: TextToastView( + title, + titleColor: titleColor, + titleFontStyle: titleFontStyle, + viewConfig: viewConfig + ), + viewConfig: viewConfig + ) + return BBToast(view: view, config: config) + } + + public static func `default`( + image: UIImage, + imageTint: UIColor = defaultImageTint, + title: String, + titleColor: UIColor? = nil, + titleFontStyle: BBFontStyle? = nil, + viewConfig: BBToastViewConfiguration = BBToastViewConfiguration(), + config: BBToastConfiguration = BBToastConfiguration() + ) -> BBToast { + let view = DefaultToastView( + child: IconToastView( + image: image, + imageTint: imageTint, + title: title, + titleColor: titleColor, + titleFontStyle: titleFontStyle, + viewConfig: viewConfig + ), + viewConfig: viewConfig + ) + return BBToast(view: view, config: config) + } + + public static func custom( + view: BBToastView, + config: BBToastConfiguration = BBToastConfiguration() + ) -> BBToast { + return BBToast(view: view, config: config) + } + + + // MARK: - Show + + public func show( + haptic type: UINotificationFeedbackGenerator.FeedbackType, + after time: TimeInterval = 0 + ) { + UINotificationFeedbackGenerator().notificationOccurred(type) + show(after: time) + } + + public func show(after delay: TimeInterval = 0) { + if let backgroundView = self.createBackgroundView() { + self.backgroundView = backgroundView + config.view?.addSubview(backgroundView) ?? BBHelper.topController()?.view.addSubview(backgroundView) + } + + config.view?.addSubview(view) ?? BBHelper.topController()?.view.addSubview(view) + view.createView(for: self) + + multicast.invoke { $0.willShowToast(self) } + + config.enteringAnimation.apply(to: self.view) + let endBackgroundColor = backgroundView?.backgroundColor + backgroundView?.backgroundColor = .clear + UIView.animate( + withDuration: config.animationTime, + delay: delay, + usingSpringWithDamping: 0.8, + initialSpringVelocity: 0.4 + ) { + self.config.enteringAnimation.undo(from: self.view) + self.backgroundView?.backgroundColor = endBackgroundColor + } completion: { [self] _ in + multicast.invoke { $0.didShowToast(self) } + + configureCloseTimer() + if !config.allowToastOverlap { + closeOverlappedToasts() + } + BBToast.activeToasts.append(self) + } + + } + + private func closeOverlappedToasts() { + BBToast.activeToasts.forEach { + $0.closeTimer?.invalidate() + $0.close(animated: false) + } + } + + + // MARK: - Close + + public func close( + animated: Bool = true, + completion: (() -> Void)? = nil + ) { + multicast.invoke { $0.willCloseToast(self) } + + UIView.animate( + withDuration: config.animationTime, + delay: 0, + usingSpringWithDamping: 0.8, + initialSpringVelocity: 0.4 + ) { + if animated { + self.config.exitingAnimation.apply(to: self.view) + } + self.backgroundView?.backgroundColor = .clear + } completion: { [self] _ in + self.backgroundView?.removeFromSuperview() + self.view.removeFromSuperview() + if let index = BBToast.activeToasts.firstIndex(where: { $0 === self }) { + BBToast.activeToasts.remove(at: index) + } + completion?() + self.multicast.invoke { $0.didCloseToast(self) } + } + } + + + // MARK: - Intializer + + public required init( + view: BBToastView, + config: BBToastConfiguration + ) { + self.view = view + self.config = config + + for dismissable in config.dismissables { + switch dismissable { + case .tap: + enableTapToClose() + case .longPress: + enableLongPressToClose() + case .swipe: + enablePanToClose() + default: + break + } + } + } + +} + + +// MARK: - Extensions + +extension BBToast { + + public func addDelegate(_ delegate: BBToastDelegate) { + multicast.add(delegate) + } + + private func createBackgroundView() -> UIView? { + switch config.background { + case .none: + return nil + + case let .color(color): + let backgroundView = UIView(frame: config.view?.frame ?? BBHelper.topController()?.view.frame ?? .zero) + backgroundView.backgroundColor = color + backgroundView.layer.zPosition = 998 + return backgroundView + } + } + + private func configureCloseTimer() { + for dismissable in config.dismissables { + if case let .time(displayTime) = dismissable { + closeTimer = Timer.scheduledTimer(withTimeInterval: displayTime, repeats: false) { [self] _ in + close() + } + } + } + } + +} + +private extension BBToast { + + func enablePanToClose() { + let pan = UIPanGestureRecognizer(target: self, action: #selector(toastOnPan(_:))) + self.view.addGestureRecognizer(pan) + } + + @objc func toastOnPan(_ gesture: UIPanGestureRecognizer) { + guard + let topVc = BBHelper.topController() + else { return } + + switch gesture.state { + case .began: + startY = self.view.frame.origin.y + startShiftY = gesture.location(in: topVc.view).y + closeTimer?.invalidate() + + case .changed: + let delta = gesture.location(in: topVc.view).y - startShiftY + + for dismissable in config.dismissables { + if case let .swipe(dismissSwipeDirection) = dismissable { + let shouldApply = dismissSwipeDirection.shouldApply(delta, direction: config.direction) + + if shouldApply { + self.view.frame.origin.y = startY + delta + } + } + } + + case .ended: + let threshold = 15.0 + let ammountOfUserDragged = abs(startY - self.view.frame.origin.y) + let shouldDismissToast = ammountOfUserDragged > threshold + + if shouldDismissToast { + close() + } else { + UIView.animate(withDuration: config.animationTime, delay: 0, options: [.curveEaseOut, .allowUserInteraction]) { + self.view.frame.origin.y = self.startY + } completion: { [self] _ in + configureCloseTimer() + } + } + + case .cancelled, .failed: + configureCloseTimer() + + default: + break + } + } + + func enableTapToClose() { + let tap = UITapGestureRecognizer(target: self, action: #selector(toastOnTap)) + self.view.addGestureRecognizer(tap) + } + + func enableLongPressToClose() { + let longPress = UILongPressGestureRecognizer(target: self, action: #selector(toastOnTap)) + self.view.addGestureRecognizer(longPress) + } + + @objc func toastOnTap(_ gesture: UITapGestureRecognizer) { + closeTimer?.invalidate() + close() + } + +} + +extension BBToast: Equatable { + + public static func == (lhs: BBToast, rhs: BBToast) -> Bool { + return ObjectIdentifier(lhs) == ObjectIdentifier(rhs) + } + +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastConfiguration.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastConfiguration.swift new file mode 100644 index 000000000..331309e5b --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastConfiguration.swift @@ -0,0 +1,88 @@ +// +// BBToastConfiguration.swift +// BBToast +// +// Created by 김건우 on 7/8/24. +// + +import UIKit + +public struct BBToastConfiguration { + + // MARK: - Properties + + public let direction: BBToast.Direction + public let dismissables: [BBToast.Dismissable] + public let animationTime: TimeInterval + public let enteringAnimation: BBToast.Animation + public let exitingAnimation: BBToast.Animation + public let background: BBToast.Background + public let allowToastOverlap: Bool + + public let view: UIView? + + + // MARK: - Intializer + + public init( + direction: BBToast.Direction = .bottom(yOffset: 0), + dismissables: [BBToast.Dismissable] = [.time(time: 2.5), .swipe(direction: .natural)], + animationTime: TimeInterval = 0.6, + enteringAnimation: BBToast.Animation = .default, + exitingAnimation: BBToast.Animation = .default, + attachTo view: UIView? = nil, + background: BBToast.Background = .none, + allowToastOverlap: Bool = false + ) { + self.direction = direction + self.dismissables = dismissables + self.animationTime = animationTime + self.enteringAnimation = enteringAnimation.isDefualt ? Self.defaultEnteringAnimation(with: direction) : enteringAnimation + self.exitingAnimation = exitingAnimation.isDefualt ? Self.defaultExitingAnimation(with: direction) : exitingAnimation + self.background = background + self.allowToastOverlap = allowToastOverlap + self.view = view + } + +} + + +// MARK: - Extensions + +private extension BBToastConfiguration { + + private static func defaultEnteringAnimation(with direction: BBToast.Direction) -> BBToast.Animation { + switch direction { + case .top: + return .custom( + transformation: CGAffineTransform(scaleX: 0.9, y: 0.9).translatedBy(x: 0, y: -100) + ) + + case .bottom: + return .custom( + transformation: CGAffineTransform(scaleX: 0.9, y: 0.9).translatedBy(x: 0, y: 100) + ) + + case .center: + return .custom( + transformation: CGAffineTransform(scaleX: 0.5, y: 0.5) + ) + } + } + + private static func defaultExitingAnimation(with direction: BBToast.Direction) -> BBToast.Animation { + self.defaultEnteringAnimation(with: direction) + } + +} + +fileprivate extension BBToast.Animation { + + var isDefualt: Bool { + if case .default = self { + return true + } + return false + } + +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastDelegate.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastDelegate.swift new file mode 100644 index 000000000..3085a019d --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastDelegate.swift @@ -0,0 +1,26 @@ +// +// ToastDelegate.swift +// BBToast +// +// Created by 김건우 on 7/8/24. +// + +import Foundation + +public protocol BBToastDelegate: AnyObject { + + func willShowToast(_ toast: BBToast) + func didShowToast(_ toast: BBToast) + func willCloseToast(_ toast: BBToast) + func didCloseToast(_ toast: BBToast) + +} + +extension BBToastDelegate { + + func willShowToast(_ toast: BBToast) { } + func didShowToast(_ toast: BBToast) { } + func willCloseToast(_ toast: BBToast) { } + func didCloseToast(_ toast: BBToast) { } + +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastViewConfiguration.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastViewConfiguration.swift new file mode 100644 index 000000000..cc72c40c6 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastViewConfiguration.swift @@ -0,0 +1,43 @@ +// +// ToastViewConfiguration.swift +// BBToast +// +// Created by 김건우 on 7/8/24. +// + +import UIKit + +public struct BBToastViewConfiguration { + + // MARK: - Properties + + public let minWidth: CGFloat + public let minHeight: CGFloat + + public let backgroundColor: UIColor + + public let titleNumberOfLines: Int + public let subtitleNumberOfLines: Int + + public let cornerRadius: CGFloat? + + + // MARK: - Intializer + + public init( + minWidth: CGFloat = 300, + minHeight: CGFloat = 56, + backgroundColor: UIColor = .gray900, + titleNumberOfLines: Int = 1, + subtitleNumberOfLines: Int = 1, + cornerRadius: CGFloat? = nil + ) { + self.minWidth = minWidth + self.minHeight = minHeight + self.backgroundColor = backgroundColor + self.titleNumberOfLines = titleNumberOfLines + self.subtitleNumberOfLines = subtitleNumberOfLines + self.cornerRadius = cornerRadius + } + +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/Background.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/Background.swift new file mode 100644 index 000000000..a23c2a2b6 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/Background.swift @@ -0,0 +1,17 @@ +// +// Background.swift +// BBToast +// +// Created by 김건우 on 7/8/24. +// + +import UIKit + +extension BBToast { + + public enum Background { + case none + case color(color: UIColor = defaultImageTint.withAlphaComponent(0.25)) + } + +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/Direction.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/Direction.swift new file mode 100644 index 000000000..69dc42c6a --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/Direction.swift @@ -0,0 +1,42 @@ +// +// Direction.swift +// BBToast +// +// Created by 김건우 on 7/8/24. +// + +import UIKit + +extension BBToast { + + public enum Direction { + case top(yOffset: CGFloat = 0) + case bottom(yOffset: CGFloat = 0) + case center(xOffset: CGFloat = 0, yOffset: CGFloat = 0) + } + + public enum DismissSwipeDirection: Equatable { + case toTop + case toBottom + case natural + + func shouldApply(_ delta: CGFloat, direction: Direction) -> Bool { + switch self { + case .toTop: + return delta <= 0 + case .toBottom: + return delta >= 0 + case .natural: + switch direction { + case .top: + return delta <= 0 + case .bottom: + return delta >= 0 + case .center: + return delta <= 0 + } + } + } + } + +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/Dismissable.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/Dismissable.swift new file mode 100644 index 000000000..ae456c42f --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/Dismissable.swift @@ -0,0 +1,19 @@ +// +// Dismissable.swift +// BBToast +// +// Created by 김건우 on 7/8/24. +// + +import Foundation + +extension BBToast { + + public enum Dismissable { + case tap + case longPress + case time(time: TimeInterval) + case swipe(direction: DismissSwipeDirection) + } + +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/BBToastView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/BBToastView.swift new file mode 100644 index 000000000..e6f610f58 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/BBToastView.swift @@ -0,0 +1,12 @@ +// +// ToastView.swift +// BBToast +// +// Created by 김건우 on 7/4/24. +// + +import UIKit + +public protocol BBToastView: UIView { + func createView(for toast: BBToast) +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/DefaultToastView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/DefaultToastView.swift new file mode 100644 index 000000000..48259d0db --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/DefaultToastView.swift @@ -0,0 +1,115 @@ +// +// BBToastView.swift +// BBToast +// +// Created by 김건우 on 7/4/24. +// + +import UIKit + +import SnapKit +import Then + +public class DefaultToastView: UIView, BBToastView { + + // MARK: - Views + + private let child: UIView + + + // MARK: - Properties + + private weak var toast: BBToast? + private let viewConfig: BBToastViewConfiguration + + + // MARK: - Intializer + + public init( + child: UIView, + viewConfig: BBToastViewConfiguration = BBToastViewConfiguration() + ) { + self.viewConfig = viewConfig + self.child = child + super.init(frame: .zero) + + addSubview(child) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Create + + public func createView(for toast: BBToast) { + self.toast = toast + + setupConstraints(for: toast) + setupAttributes() + } + + private func setupConstraints(for toast: BBToast) { + guard let superview = superview else { return } + translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + widthAnchor.constraint(greaterThanOrEqualToConstant: viewConfig.minWidth), + heightAnchor.constraint(greaterThanOrEqualToConstant: viewConfig.minHeight), + leadingAnchor.constraint(greaterThanOrEqualTo: superview.leadingAnchor, constant: 10), + trailingAnchor.constraint(lessThanOrEqualTo: superview.trailingAnchor, constant: -10), + centerXAnchor.constraint(equalTo: superview.centerXAnchor) + ]) + + switch toast.config.direction { + case let .bottom(yOffset): + bottomAnchor.constraint(equalTo: superview.layoutMarginsGuide.bottomAnchor, constant: yOffset).isActive = true + case let .top(yOffset): + topAnchor.constraint(equalTo: superview.layoutMarginsGuide.topAnchor, constant: yOffset).isActive = true + case let .center(xOffset, yOffset): + centerYAnchor.constraint(equalTo: superview.layoutMarginsGuide.centerYAnchor, constant: yOffset).isActive = true + } + + setupSubviewConstraints() + } + + private func setupAttributes() { + layoutIfNeeded() + clipsToBounds = true + layer.zPosition = 999 + layer.cornerRadius = viewConfig.cornerRadius ?? frame.height / 2 + backgroundColor = viewConfig.backgroundColor + + addShadow() + } + + + // MARK: - Helpers + + private func setupSubviewConstraints() { + child.snp.makeConstraints { + $0.verticalEdges.equalToSuperview().inset(10) + $0.horizontalEdges.equalToSuperview().inset(25) + } + } + + private func addShadow() { + layer.masksToBounds = false + layer.shadowOffset = CGSize(width: 0, height: 4) + layer.shadowColor = UIColor.black.withAlphaComponent(0.08).cgColor + layer.shadowOpacity = 1 + layer.shadowRadius = 8 + } + + public override func removeFromSuperview() { + super.removeFromSuperview() + self.toast = nil + } + + public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + UIView.animate(withDuration: 0.5) { + self.setupAttributes() + } + } + +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/IconToastView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/IconToastView.swift new file mode 100644 index 000000000..a7a350cfd --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/IconToastView.swift @@ -0,0 +1,71 @@ +// +// IconTextView.swift +// BBToast +// +// Created by 김건우 on 7/4/24. +// + +import UIKit + +import Then + +public class IconToastView: UIStackView { + + // MARK: - Views + + private let vStack = UIStackView() + private let imageView = UIImageView() + private let titleLabel = BBLabel(textAlignment: .center) + + + // MARK: - Intializer + + public init( + image: UIImage, + imageTint: UIColor? = nil, + title: String, + titleColor: UIColor? = nil, + titleFontStyle: BBFontStyle? = nil, + viewConfig: BBToastViewConfiguration + ) { + super.init(frame: .zero) + commonInit() + + titleLabel.text = title + titleLabel.textColor = titleColor ?? .bibbiWhite + titleLabel.fontStyle = titleFontStyle ?? .body1Regular + titleLabel.numberOfLines = viewConfig.titleNumberOfLines + vStack.addArrangedSubview(titleLabel) + + imageView.image = image + imageView.tintColor = imageTint + + addArrangedSubview(imageView) + addArrangedSubview(vStack) + } + + required init(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func commonInit() { + axis = .horizontal + spacing = 15 + alignment = .center + distribution = .fill + + setupAttributes() + } + + + // MARK: - Helpers + + private func setupAttributes() { + vStack.do { + $0.axis = .vertical + $0.spacing = 2 + $0.alignment = .leading + } + } + +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/TextToastView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/TextToastView.swift new file mode 100644 index 000000000..6427768c8 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/TextToastView.swift @@ -0,0 +1,45 @@ +// +// TextToastView.swift +// BBToast +// +// Created by 김건우 on 7/4/24. +// + +import UIKit + +public class TextToastView: UIStackView { + + // MARK: - Views + + private let titleLabel = BBLabel(.body1Regular) + + + // MARK: - Intializer + + public init( + _ title: String, + titleColor: UIColor? = nil, + titleFontStyle: BBFontStyle? = nil, + viewConfig: BBToastViewConfiguration + ) { + super.init(frame: .zero) + commonInit() + + titleLabel.text = title + titleLabel.textColor = titleColor ?? .bibbiWhite + titleLabel.fontStyle = titleFontStyle ?? .body1Regular + titleLabel.numberOfLines = viewConfig.titleNumberOfLines + addArrangedSubview(titleLabel) + } + + required init(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func commonInit() { + axis = .vertical + alignment = .leading + distribution = .fillEqually + } + +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBHelper.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBHelper.swift new file mode 100644 index 000000000..19734662e --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBHelper.swift @@ -0,0 +1,45 @@ +// +// BBHelper.swift +// BBToast +// +// Created by 김건우 on 7/4/24. +// + +import UIKit + +final class BBHelper { + + public static func topController() -> UIViewController? { + if var topController = keyWindow()?.rootViewController { + + while var presentedController = topController.presentedViewController { + topController = presentedController + } + return topController + + } + + return nil + } + + private static func keyWindow() -> UIWindow? { + for scene in UIApplication.shared.connectedScenes { + guard + let windowScene = scene as? UIWindowScene + else { continue } + + if windowScene.windows.isEmpty { + continue + } + + guard + let window = windowScene.windows.first(where: { $0.isKeyWindow }) + else { continue } + + return window + } + + return nil + } + +} diff --git a/14th-team5-iOS/Core/Sources/Utilities/MulticaseDelegate.swift b/14th-team5-iOS/Core/Sources/Utilities/MulticaseDelegate.swift new file mode 100644 index 000000000..55b8a221b --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Utilities/MulticaseDelegate.swift @@ -0,0 +1,31 @@ +// +// MulticaseDelegate.swift +// BBToast +// +// Created by 김건우 on 7/8/24. +// + +import Foundation + +final class MulticaseDelegate { + + private let delegates: NSHashTable = NSHashTable() + + func add(_ delegate: T) { + delegates.add(delegates as AnyObject) + } + + func remove(_ delegateToRemove: T) { + for delegate in delegates.allObjects.reversed() { + if delegate === delegateToRemove as AnyObject { + delegates.remove(delegate) + } + } + } + + func invoke(_ invocation: (T) -> Void) { + for delegate in delegates.allObjects.reversed() { + invocation(delegate as! T) + } + } +} From 5936c944071dc1b4e3dec7cb5f68fe215e8b3f69 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Mon, 5 Aug 2024 11:49:58 +0900 Subject: [PATCH 167/263] =?UTF-8?q?feat:=20=EA=B5=AC=20ToastMessage=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20Deprecated=20=EC=B2=98=EB=A6=AC=20(#591)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Core/Sources/Extensions/UIViewController+Ext.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/14th-team5-iOS/Core/Sources/Extensions/UIViewController+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/UIViewController+Ext.swift index c96f3a850..b93c13883 100644 --- a/14th-team5-iOS/Core/Sources/Extensions/UIViewController+Ext.swift +++ b/14th-team5-iOS/Core/Sources/Extensions/UIViewController+Ext.swift @@ -17,6 +17,7 @@ extension UIViewController { case down } + @available(*, deprecated, message: "BBToast를 사용하세요.") public func makeErrorBibbiToastView( delay: CGFloat = 0.6, duration: CGFloat = 0.6, @@ -31,6 +32,7 @@ extension UIViewController { ) } + @available(*, deprecated, message: "BBToast를 사용하세요.") public func makeActionBibbiToastView( text: String = "", transtionText: String = "", @@ -48,6 +50,7 @@ extension UIViewController { ) } + @available(*, deprecated, message: "BBToast를 사용하세요.") public func makeTranstionToastView( text: String = "", transtionText: String = "", @@ -71,6 +74,7 @@ extension UIViewController { } } + @available(*, deprecated, message: "BBToast를 사용하세요.") public func makeBibbiToastView( text: String, image: DesignSystemImages.Image? = nil, From de3855bd406dbf12d53805eb90a2a7b8091ff407 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Tue, 6 Aug 2024 09:38:31 +0900 Subject: [PATCH 168/263] =?UTF-8?q?feat:=20ToastButton=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#591)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FamilyManagementViewController.swift | 15 +++- .../Bibbi/BBCommons/BBToast/BBToast.swift | 55 ++++++++++-- .../BBCommons/BBToast/BBToastDelegate.swift | 6 +- .../BBToast/BBToastViewConfiguration.swift | 7 ++ .../BBToast/ToastViews/BBToastStackView.swift | 12 +++ .../DefaultToastView/ButtonToastView.swift | 89 +++++++++++++++++++ .../DefaultToastView/DefaultToastView.swift | 8 +- .../DefaultToastView/IconToastView.swift | 6 +- .../DefaultToastView/TextToastView.swift | 7 +- .../Sources/Utilities/MulticaseDelegate.swift | 2 +- 10 files changed, 189 insertions(+), 18 deletions(-) create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/BBToastStackView.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/ButtonToastView.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/ViewController/FamilyManagementViewController.swift b/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/ViewController/FamilyManagementViewController.swift index 4aaa87f67..88c9dc000 100644 --- a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/ViewController/FamilyManagementViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/ViewController/FamilyManagementViewController.swift @@ -145,10 +145,17 @@ public final class FamilyManagementViewController: BBNavigationViewController() + public static var multicast = MulticaseDelegate() public private(set) var config: BBToastConfiguration @@ -73,6 +73,35 @@ public class BBToast { return BBToast(view: view, config: config) } + public static func button( + image: UIImage, + imageTint: UIColor = defaultImageTint, + title: String, + titleColor: UIColor? = nil, + titleFontStyle: BBFontStyle? = nil, + buttonTitle: String, + buttonTitleFontStyle: BBFontStyle? = nil, + buttonTint: UIColor? = nil, + viewConfig: BBToastViewConfiguration = BBToastViewConfiguration(), + config: BBToastConfiguration = BBToastConfiguration() + ) -> BBToast { + let view = DefaultToastView( + child: ButtonToastView( + image: image, + imageTint: imageTint, + title: title, + titleColor: titleColor, + titleFontStyle: titleFontStyle, + buttonTitle: buttonTitle, + buttonTitleFontStlye: buttonTitleFontStyle, + buttonTint: buttonTint, + viewConfig: viewConfig + ), + viewConfig: viewConfig + ) + return BBToast(view: view, config: config) + } + public static func custom( view: BBToastView, config: BBToastConfiguration = BBToastConfiguration() @@ -100,7 +129,7 @@ public class BBToast { config.view?.addSubview(view) ?? BBHelper.topController()?.view.addSubview(view) view.createView(for: self) - multicast.invoke { $0.willShowToast(self) } + Self.multicast.invoke { $0.willShowToast(self) } config.enteringAnimation.apply(to: self.view) let endBackgroundColor = backgroundView?.backgroundColor @@ -114,7 +143,7 @@ public class BBToast { self.config.enteringAnimation.undo(from: self.view) self.backgroundView?.backgroundColor = endBackgroundColor } completion: { [self] _ in - multicast.invoke { $0.didShowToast(self) } + Self.multicast.invoke { $0.didShowToast(self) } configureCloseTimer() if !config.allowToastOverlap { @@ -139,7 +168,7 @@ public class BBToast { animated: Bool = true, completion: (() -> Void)? = nil ) { - multicast.invoke { $0.willCloseToast(self) } + Self.multicast.invoke { $0.willCloseToast(self) } UIView.animate( withDuration: config.animationTime, @@ -158,7 +187,7 @@ public class BBToast { BBToast.activeToasts.remove(at: index) } completion?() - self.multicast.invoke { $0.didCloseToast(self) } + Self.multicast.invoke { $0.didCloseToast(self) } } } @@ -194,9 +223,23 @@ public class BBToast { extension BBToast { public func addDelegate(_ delegate: BBToastDelegate) { - multicast.add(delegate) + Self.multicast.add(delegate) } + public func addTapAction( + _ action: ((BBToast?) -> Void)? = nil + ) { + if let view = view as? DefaultToastView { + if let subview = view.child as? ButtonToastView { + subview.tapAction = action + } + } + } + +} + +extension BBToast { + private func createBackgroundView() -> UIView? { switch config.background { case .none: diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastDelegate.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastDelegate.swift index 3085a019d..0c3b1c9bd 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastDelegate.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastDelegate.swift @@ -14,13 +14,17 @@ public protocol BBToastDelegate: AnyObject { func willCloseToast(_ toast: BBToast) func didCloseToast(_ toast: BBToast) + func didTapToastButton(_ toast: BBToast) + } -extension BBToastDelegate { +public extension BBToastDelegate { func willShowToast(_ toast: BBToast) { } func didShowToast(_ toast: BBToast) { } func willCloseToast(_ toast: BBToast) { } func didCloseToast(_ toast: BBToast) { } + func didTapToastButton(_ toast: BBToast) { } + } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastViewConfiguration.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastViewConfiguration.swift index cc72c40c6..dc9a3284e 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastViewConfiguration.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastViewConfiguration.swift @@ -14,6 +14,9 @@ public struct BBToastViewConfiguration { public let minWidth: CGFloat public let minHeight: CGFloat + public let minButtonWidth: CGFloat + public let minButtonHeight: CGFloat + public let backgroundColor: UIColor public let titleNumberOfLines: Int @@ -27,6 +30,8 @@ public struct BBToastViewConfiguration { public init( minWidth: CGFloat = 300, minHeight: CGFloat = 56, + minButtonWidth: CGFloat = 50, + minButtonHeight: CGFloat = 36, backgroundColor: UIColor = .gray900, titleNumberOfLines: Int = 1, subtitleNumberOfLines: Int = 1, @@ -34,6 +39,8 @@ public struct BBToastViewConfiguration { ) { self.minWidth = minWidth self.minHeight = minHeight + self.minButtonWidth = minButtonWidth + self.minButtonHeight = minButtonHeight self.backgroundColor = backgroundColor self.titleNumberOfLines = titleNumberOfLines self.subtitleNumberOfLines = subtitleNumberOfLines diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/BBToastStackView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/BBToastStackView.swift new file mode 100644 index 000000000..6ea3e140d --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/BBToastStackView.swift @@ -0,0 +1,12 @@ +// +// BBToastSubView.swift +// Core +// +// Created by 김건우 on 8/5/24. +// + +import UIKit + +public protocol BBToastStackView: UIStackView { + var toast: BBToast? { get set } +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/ButtonToastView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/ButtonToastView.swift new file mode 100644 index 000000000..78b67ca60 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/ButtonToastView.swift @@ -0,0 +1,89 @@ +// +// ButtonToastView.swift +// Core +// +// Created by 김건우 on 8/5/24. +// + +import UIKit + +final public class ButtonToastView: UIStackView, BBToastStackView { + + // MARK: - Views + + private let button: BBButton = BBButton() + + // MARK: - Properties + + public var toast: BBToast? + public var tapAction: ((BBToast?) -> Void)? + + private let viewConfig: BBToastViewConfiguration + + // MARK: - Intializer + + public init( + image: UIImage, + imageTint: UIColor? = nil, + title: String, + titleColor: UIColor? = nil, + titleFontStyle: BBFontStyle? = nil, + buttonTitle: String, + buttonTitleFontStlye: BBFontStyle? = nil, + buttonTint: UIColor? = nil, + viewConfig: BBToastViewConfiguration + ) { + self.toast = nil + self.tapAction = nil + + self.viewConfig = viewConfig + super.init(frame: .zero) + commonInit() + + let iconView = IconToastView( + image: image, + imageTint: imageTint, + title: title, + titleColor: titleColor, + titleFontStyle: titleFontStyle, + viewConfig: viewConfig + ) + + button.setTitle(buttonTitle, for: .normal) + button.setTitleFontStyle(buttonTitleFontStlye ?? .body1Regular) + button.setTitleColor(buttonTint ?? .gray100, for: .normal) + + addArrangedSubview(iconView) + addArrangedSubview(button) + } + + required init(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Helpers + + private func commonInit() { + axis = .horizontal + spacing = 6 + alignment = .center + distribution = .fillProportionally + + setupAttributes() + } + + private func setupAttributes() { + button.addTarget( + self, + action: #selector(didTapToastButton), + for: .touchUpInside + ) + } + + // TODO: - 세세한 버튼 UI 수정하기 + + @objc public func didTapToastButton(_ button: UIButton) { + tapAction?(toast) + } + +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/DefaultToastView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/DefaultToastView.swift index 48259d0db..e31b47c79 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/DefaultToastView.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/DefaultToastView.swift @@ -14,19 +14,20 @@ public class DefaultToastView: UIView, BBToastView { // MARK: - Views - private let child: UIView + public let child: BBToastStackView // MARK: - Properties - private weak var toast: BBToast? + public weak var toast: BBToast? + private let viewConfig: BBToastViewConfiguration // MARK: - Intializer public init( - child: UIView, + child: BBToastStackView, viewConfig: BBToastViewConfiguration = BBToastViewConfiguration() ) { self.viewConfig = viewConfig @@ -44,6 +45,7 @@ public class DefaultToastView: UIView, BBToastView { public func createView(for toast: BBToast) { self.toast = toast + self.child.toast = toast setupConstraints(for: toast) setupAttributes() diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/IconToastView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/IconToastView.swift index a7a350cfd..02c312c29 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/IconToastView.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/IconToastView.swift @@ -9,13 +9,17 @@ import UIKit import Then -public class IconToastView: UIStackView { +public class IconToastView: UIStackView, BBToastStackView { // MARK: - Views private let vStack = UIStackView() private let imageView = UIImageView() private let titleLabel = BBLabel(textAlignment: .center) + + // MARK: - Properties + + public var toast: BBToast? // MARK: - Intializer diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/TextToastView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/TextToastView.swift index 6427768c8..7c5ea7dce 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/TextToastView.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/TextToastView.swift @@ -7,12 +7,15 @@ import UIKit -public class TextToastView: UIStackView { - +public class TextToastView: UIStackView, BBToastStackView { + // MARK: - Views private let titleLabel = BBLabel(.body1Regular) + // MARK: - Properties + + public var toast: BBToast? // MARK: - Intializer diff --git a/14th-team5-iOS/Core/Sources/Utilities/MulticaseDelegate.swift b/14th-team5-iOS/Core/Sources/Utilities/MulticaseDelegate.swift index 55b8a221b..5d14ba9e8 100644 --- a/14th-team5-iOS/Core/Sources/Utilities/MulticaseDelegate.swift +++ b/14th-team5-iOS/Core/Sources/Utilities/MulticaseDelegate.swift @@ -7,7 +7,7 @@ import Foundation -final class MulticaseDelegate { +final public class MulticaseDelegate { private let delegates: NSHashTable = NSHashTable() From 0abfe50f709e529ae0f0ec20eff0f612ca49c1f1 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Fri, 9 Aug 2024 12:41:23 +0900 Subject: [PATCH 169/263] =?UTF-8?q?feat:=20BBAlert=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?(#598)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AlertViews/BBAlertStackView.swift | 12 ++ .../BBCommons/AlertViews/BBAlertView.swift | 12 ++ .../DefaultToastView/DefaultAlertView.swift | 189 ++++++++++++++++++ .../DefaultToastView/ImageAlertView.swift | 69 +++++++ .../DefaultToastView/TextAlertView.swift | 8 + .../Sources/Bibbi/BBCommons/BBAlert.swift | 173 ++++++++++++++++ .../Bibbi/BBCommons/BBAlertAnimation.swift | 53 +++++ .../Bibbi/BBCommons/BBAlertBackground.swift | 17 ++ .../Bibbi/BBCommons/BBAlertButton.swift | 17 ++ .../Bibbi/BBCommons/BBAlertButtonsAxis.swift | 17 ++ .../BBCommons/BBAlertConfiguration.swift | 63 ++++++ .../Bibbi/BBCommons/BBAlertDelegate.swift | 26 +++ .../Bibbi/BBCommons/BBAlertStyle.swift | 20 ++ .../BBCommons/BBAlertViewConfiguration.swift | 55 +++++ 14 files changed, 731 insertions(+) create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/AlertViews/BBAlertStackView.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/AlertViews/BBAlertView.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/AlertViews/DefaultToastView/DefaultAlertView.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/AlertViews/DefaultToastView/ImageAlertView.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/AlertViews/DefaultToastView/TextAlertView.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertAnimation.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertBackground.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertButton.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertButtonsAxis.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertConfiguration.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertDelegate.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertStyle.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertViewConfiguration.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/AlertViews/BBAlertStackView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/AlertViews/BBAlertStackView.swift new file mode 100644 index 000000000..107b10adc --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/AlertViews/BBAlertStackView.swift @@ -0,0 +1,12 @@ +// +// BBAlertStackView.swift +// BBAlert +// +// Created by 김건우 on 8/7/24. +// + +import UIKit + +public protocol BBAlertStackView: UIStackView { + var alert: BBAlert? { get set } +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/AlertViews/BBAlertView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/AlertViews/BBAlertView.swift new file mode 100644 index 000000000..dc006c54d --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/AlertViews/BBAlertView.swift @@ -0,0 +1,12 @@ +// +// BBAlertView.swift +// BBAlert +// +// Created by 김건우 on 8/6/24. +// + +import UIKit + +public protocol BBAlertView: UIView { + func createView(for alert: BBAlert) +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/AlertViews/DefaultToastView/DefaultAlertView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/AlertViews/DefaultToastView/DefaultAlertView.swift new file mode 100644 index 000000000..e0ef6984b --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/AlertViews/DefaultToastView/DefaultAlertView.swift @@ -0,0 +1,189 @@ +// +// DefaultAlertView.swift +// BBAlert +// +// Created by 김건우 on 8/6/24. +// + +import UIKit + +public class DefaultAlertView: UIView, BBAlertView { + + // MARK: - Views + + private let child: BBAlertStackView + + private let buttonStack: UIStackView = UIStackView() + + private let alertStack: UIStackView = UIStackView() + + // MARK: - Properties + + private var alert: BBAlert? + private let viewConfig: BBAlertViewConfiguration + + private let buttonSpacing: CGFloat = 8 + + + // MARK: - Intializer + + public init( + child: BBAlertStackView, + viewConfig: BBAlertViewConfiguration = BBAlertViewConfiguration() + ) { + self.viewConfig = viewConfig + self.child = child + super.init(frame: .zero) + + addSubview(child) + addSubview(buttonStack) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + // MARK: - Create + + public func createView(for alert: BBAlert) { + self.alert = alert + self.child.alert = alert + + setupUI() + setupConstraints(for: alert) + setupAttributes() + } + + + // MARK: - Helpers + + private func setupUI() { + + for type in viewConfig.buttons { + let button = createAlertButton(for: type) + buttonStack.addArrangedSubview(button) + } + + } + + private func setupConstraints(for alert: BBAlert) { + guard let superview = superview else { return } + translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + widthAnchor.constraint(greaterThanOrEqualToConstant: viewConfig.minWidth), + heightAnchor.constraint(greaterThanOrEqualToConstant: viewConfig.minHeight), + topAnchor.constraint(greaterThanOrEqualTo: superview.layoutMarginsGuide.topAnchor), + leadingAnchor.constraint(greaterThanOrEqualTo: superview.layoutMarginsGuide.leadingAnchor), + trailingAnchor.constraint(lessThanOrEqualTo: superview.layoutMarginsGuide.trailingAnchor), + bottomAnchor.constraint(lessThanOrEqualTo: superview.layoutMarginsGuide.bottomAnchor), + centerXAnchor.constraint(equalTo: superview.centerXAnchor), + centerYAnchor.constraint(equalTo: superview.centerYAnchor) + ]) + + setupSubviewConstraints() + } + + 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), + ]) + + if case .vertical = viewConfig.buttonsAxis { + let count = CGFloat(buttonStack.arrangedSubviews.count) + buttonStack.heightAnchor.constraint(equalToConstant: count * viewConfig.buttonHeight + (buttonSpacing * count)).isActive = true + } else { + buttonStack.heightAnchor.constraint(equalToConstant: viewConfig.buttonHeight).isActive = true + } + + child.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + child.bottomAnchor.constraint(equalTo: buttonStack.topAnchor, constant: -10), + child.leadingAnchor.constraint(equalTo: self.layoutMarginsGuide.leadingAnchor), + child.trailingAnchor.constraint(equalTo: self.layoutMarginsGuide.trailingAnchor), + child.topAnchor.constraint(equalTo: self.layoutMarginsGuide.topAnchor) + ]) + } + + private func setupButtonConstraints() { + for button in buttonStack.arrangedSubviews { + NSLayoutConstraint.activate([ + button.heightAnchor.constraint(equalToConstant: viewConfig.buttonHeight) + ]) + } + } + + private func setupAttributes() { + layoutIfNeeded() + clipsToBounds = true + layer.zPosition = 888 + layer.cornerRadius = viewConfig.cornerRadius ?? 16 + backgroundColor = viewConfig.backgroundColor + + buttonStack.axis = viewConfig.buttonsAxis + buttonStack.spacing = buttonSpacing + buttonStack.alignment = .fill + buttonStack.distribution = .fillEqually + + addShadow() + } + + private func createAlertButton(for type: BBAlert.Button) -> UIButton { + let button = UIButton(type: .system) // TODO: - BBButton으로 교체 + + if case let .normal(title, tint, action) = type { + setupAlertButtotAttribute( + button, + title: title, + tint: tint, + action: action + ) + } else { + setupAlertButtotAttribute( + button, + title: "취소", + tint: .gray // TODO: - BBColor로 교체 + ) + } + + return button + } + + private func setupAlertButtotAttribute( + _ button: UIButton, + title: String?, + tint: UIColor?, + action: ((BBAlert?) -> Void)? = nil + ) { + let action = UIAction { [weak self] _ in + action?(self?.alert) ?? self?.alert?.close() + } + + button.setTitle(title, for: .normal) + button.backgroundColor = tint + + button.layer.cornerRadius = 8 + button.layer.masksToBounds = false + + button.addAction(action, for: .touchUpInside) + } + + private func addShadow() { + layer.masksToBounds = false + layer.shadowOffset = CGSize(width: 0, height: 4) + layer.shadowColor = UIColor.black.withAlphaComponent(0.08).cgColor + layer.shadowOpacity = 1 + layer.shadowRadius = 8 + } + + + public override func removeFromSuperview() { + super.removeFromSuperview() + self.alert = nil + } + +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/AlertViews/DefaultToastView/ImageAlertView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/AlertViews/DefaultToastView/ImageAlertView.swift new file mode 100644 index 000000000..a16776a89 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/AlertViews/DefaultToastView/ImageAlertView.swift @@ -0,0 +1,69 @@ +// +// ImageAlertView.swift +// BBAlert +// +// Created by 김건우 on 8/6/24. +// + +import UIKit + +public class ImageAlertView: UIStackView, BBAlertStackView { + + // MARK: - Views + + private let titleLabel = UILabel() // TODO: - BBLabel로 교체 + private let subtitleLabel = UILabel() + private let imageView = UIImageView() + + + // MARK: - Properties + + public var alert: BBAlert? + + + // MARK: - Intializer + + public init( + image: UIImage? = nil, + imageTint: UIColor? = nil, + title: String, + subtitle: String? = nil, + viewConfig: BBAlertViewConfiguration + ) { + super.init(frame: .zero) + commonInit() + + titleLabel.text = title + titleLabel.numberOfLines = viewConfig.titleNumberOfLines + addArrangedSubview(titleLabel) + + if let subtitle = subtitle { + subtitleLabel.text = subtitle + subtitleLabel.numberOfLines = viewConfig.subtitleNumberOfLines + addArrangedSubview(subtitleLabel) + } + + if let image = image { + imageView.image = image + imageView.contentMode = .scaleAspectFit + imageView.tintColor = imageTint + addArrangedSubview(imageView) + } + } + + required init(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + // MARK: - Helpers + + private func commonInit() { + axis = .vertical + spacing = 3 + alignment = .center + distribution = .fillProportionally + } + +} + diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/AlertViews/DefaultToastView/TextAlertView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/AlertViews/DefaultToastView/TextAlertView.swift new file mode 100644 index 000000000..c3bed8911 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/AlertViews/DefaultToastView/TextAlertView.swift @@ -0,0 +1,8 @@ +// +// TextAlertView.swift +// BBAlert +// +// Created by 김건우 on 8/6/24. +// + +import Foundation diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert.swift new file mode 100644 index 000000000..cdc20d2a9 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert.swift @@ -0,0 +1,173 @@ +// +// BBAlert.swift +// BBAlert +// +// Created by 김건우 on 8/6/24. +// + +import UIKit + +public class BBAlert { + + // MARK: - Properties + + public static var activeAlerts: [BBAlert] = [] + + public let view: BBAlertView + private var backgroundView: UIView? + + public static var defaultImageTint: UIColor = .black + + private var multicast = MulticaseDelegate() + + public private(set) var config: BBAlertConfiguration + + + // MARK: - Alert + + public static func `image`( + image: UIImage? = nil, + imageTint: UIColor? = nil, + title: String, + subtitle: String? = nil, + viewConfig: BBAlertViewConfiguration = BBAlertViewConfiguration(), + config: BBAlertConfiguration = BBAlertConfiguration() + ) -> BBAlert { + let view = DefaultAlertView( + child: ImageAlertView( + image: image, + imageTint: imageTint, + title: title, + subtitle: subtitle, + viewConfig: viewConfig + ), + viewConfig: viewConfig + ) + + return BBAlert(view: view, config: config) + } + + public static func custom( + _ child: BBAlertStackView, + viewConfig: BBAlertViewConfiguration = BBAlertViewConfiguration(), + config: BBAlertConfiguration = BBAlertConfiguration() + ) -> BBAlert { + let view = DefaultAlertView( + child: child, + viewConfig: viewConfig + ) + + return BBAlert(view: view, config: config) + } + + // MARK: - Show + + public func show(haptic type: UINotificationFeedbackGenerator.FeedbackType, after time: TimeInterval = 0) { + UINotificationFeedbackGenerator().notificationOccurred(type) + show(after: time) + } + + public func show(after delay: TimeInterval = 0) { + if let backgroundView = self.createBackgroundView() { + self.backgroundView = backgroundView + config.view?.addSubview(backgroundView) ?? BBHelper.topController()?.view.addSubview(backgroundView) + } + + config.view?.addSubview(view) ?? BBHelper.topController()?.view.addSubview(view) + view.createView(for: self) + + multicast.invoke { $0.willShowAlert(self) } + + config.enteringAnimation.apply(to: self.view) + let endBackgroundColor = backgroundView?.backgroundColor + backgroundView?.backgroundColor = .clear + UIView.animate(withDuration: 0.15) { + self.config.enteringAnimation.undo(from: self.view) + self.backgroundView?.backgroundColor = endBackgroundColor + } completion: { [self] _ in + multicast.invoke { $0.didShowAlert(self) } + + if !config.allowOverlapAlert { + closeOverlappedAlerts() + } + BBAlert.activeAlerts.append(self) + } + + } + + + // MARK: - Close + + public func close( + animated: Bool = true, + completion: (() -> Void)? = nil + ) { + multicast.invoke { $0.willCloseAlert(self) } + + UIView.animate(withDuration: 0.15) { + if animated { + self.config.exitingAnimation.apply(to: self.view) + } + self.backgroundView?.backgroundColor = .clear + } completion: { [self] _ in + self.backgroundView?.removeFromSuperview() + self.view.removeFromSuperview() + if let index = BBAlert.activeAlerts.firstIndex(where: { $0 === self }) { + BBAlert.activeAlerts.remove(at: index) + } + completion?() + self.multicast.invoke { $0.didCloseAlert(self) } + } + } + + + // MARK: - Intializer + + public required init( + view: BBAlertView, + config: BBAlertConfiguration + ) { + self.view = view + self.config = config + } + +} + + +// MARK: - Extensions + +extension BBAlert { + + private func createBackgroundView() -> UIView? { + switch config.background { + case .none: + return nil + + case let .color(color): + let backgroundView = UIView(frame: config.view?.frame ?? BBHelper.topController()?.view.frame ?? .zero) + backgroundView.backgroundColor = color + backgroundView.layer.zPosition = 887 + return backgroundView + } + } + +} + +extension BBAlert { + + private func closeOverlappedAlerts() { + BBAlert.activeAlerts.forEach { alert in + alert.close(animated: false) + } + } + +} + + +extension BBAlert: Equatable { + + public static func == (lhs: BBAlert, rhs: BBAlert) -> Bool { + ObjectIdentifier(lhs) == ObjectIdentifier(rhs) + } + +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertAnimation.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertAnimation.swift new file mode 100644 index 000000000..94ee53fa5 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertAnimation.swift @@ -0,0 +1,53 @@ +// +// Animation.swift +// BBAlert +// +// Created by 김건우 on 8/7/24. +// + +import UIKit + +extension BBAlert { + + public enum Animation { + case fade(alpha: CGFloat) + case scaleAndFade(scaleX: CGFloat, scaleY: CGFloat, alpha: CGFloat) + case custom(transformation: CGAffineTransform) + case `default` + + func apply(to view: UIView) { + switch self { + case let .fade(alpha): + view.alpha = alpha + + case let .scaleAndFade(scaleX, scaleY, alpha): + view.alpha = alpha + view.transform = CGAffineTransform(scaleX: scaleX, y: scaleY) + + case let .custom(transformation): + view.transform = transformation + + case .default: + break + } + } + + func undo(from view: UIView) { + switch self { + case .fade: + view.alpha = 1 + + case .scaleAndFade: + view.alpha = 1 + view.transform = .identity + + case .custom: + view.transform = .identity + + case .default: + break + } + } + } + +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertBackground.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertBackground.swift new file mode 100644 index 000000000..5bd28f5ff --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertBackground.swift @@ -0,0 +1,17 @@ +// +// Background.swift +// BBAlert +// +// Created by 김건우 on 8/7/24. +// + +import UIKit + +extension BBAlert { + + public enum Background { + case none + case color(color: UIColor = defaultImageTint.withAlphaComponent(0.25)) + } + +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertButton.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertButton.swift new file mode 100644 index 000000000..0311a41f2 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertButton.swift @@ -0,0 +1,17 @@ +// +// Button.swift +// BBAlert +// +// Created by 김건우 on 8/7/24. +// + +import UIKit + +extension BBAlert { + + public enum Button { + case normal(title: String? = nil, tint: UIColor? = nil, action: ((BBAlert?) -> Void)? = nil) + case cancel + } + +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertButtonsAxis.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertButtonsAxis.swift new file mode 100644 index 000000000..1dc26faef --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertButtonsAxis.swift @@ -0,0 +1,17 @@ +// +// ButtonAxis.swift +// BBAlert +// +// Created by 김건우 on 8/7/24. +// + +import Foundation + +extension BBAlert { + + public enum ButtonsAxis { + case vertical + case horizontal + } + +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertConfiguration.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertConfiguration.swift new file mode 100644 index 000000000..415afa9e5 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertConfiguration.swift @@ -0,0 +1,63 @@ +// +// BBAlertConfiguration.swift +// BBAlert +// +// Created by 김건우 on 8/6/24. +// + +import UIKit + +public struct BBAlertConfiguration { + + // MARK: - Properties + + public let enteringAnimation: BBAlert.Animation + public let exitingAnimation: BBAlert.Animation + public let background: BBAlert.Background + public let allowOverlapAlert: Bool + + public let view: UIView? + + + // MARK: - Intializer + + public init( + attachedTo view: UIView? = nil, + enteringAnimation: BBAlert.Animation = .default, + exitingAnimation: BBAlert.Animation = .default, + background: BBAlert.Background = .color(), + allowOverlapAlert: Bool = false + ) { + self.view = view + self.enteringAnimation = enteringAnimation.isDefault ? Self.defaultEnteringAnimation() : enteringAnimation + self.exitingAnimation = exitingAnimation.isDefault ? Self.defaultExitingAnimation() : exitingAnimation + self.background = background + self.allowOverlapAlert = allowOverlapAlert + } + +} + +// MARK: - Extensions + +private extension BBAlertConfiguration { + + static func defaultEnteringAnimation() -> BBAlert.Animation { + return .scaleAndFade(scaleX: 1.1, scaleY: 1.1, alpha: 0) + } + + static func defaultExitingAnimation() -> BBAlert.Animation { + return .fade(alpha: 0) + } + +} + +fileprivate extension BBAlert.Animation { + + var isDefault: Bool { + if case .default = self { + return true + } + return false + } + +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertDelegate.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertDelegate.swift new file mode 100644 index 000000000..e76779e0b --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertDelegate.swift @@ -0,0 +1,26 @@ +// +// BBAlertDelegate.swift +// BBAlert +// +// Created by 김건우 on 8/9/24. +// + +import Foundation + +public protocol BBAlertDelegate: AnyObject { + + func willShowAlert(_ alert: BBAlert) + func didShowAlert(_ alert: BBAlert) + func willCloseAlert(_ alert: BBAlert) + func didCloseAlert(_ alert: BBAlert) + +} + +extension BBAlertDelegate { + + func willShowAlert(_ alert: BBAlert) { } + func didShowAlert(_ alert: BBAlert) { } + func willCloseAlert(_ alert: BBAlert) { } + func didCloseAlert(_ alert: BBAlert) { } + +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertStyle.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertStyle.swift new file mode 100644 index 000000000..175df6811 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertStyle.swift @@ -0,0 +1,20 @@ +// +// BBAlertStyle.swift +// Core +// +// Created by 김건우 on 8/9/24. +// + +import Foundation + +extension BBAlert { + + public enum Style { + case logout + case widget + case mission + case picking + case camera + } + +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertViewConfiguration.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertViewConfiguration.swift new file mode 100644 index 000000000..b512cc1a7 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertViewConfiguration.swift @@ -0,0 +1,55 @@ +// +// BBAlertViewConfiguration.swift +// BBAlert +// +// Created by 김건우 on 8/7/24. +// + +import UIKit + +// MARK: - Typealias + +public typealias BBAlertButtonAxis = NSLayoutConstraint.Axis + +public struct BBAlertViewConfiguration { + + // MARK: - Properties + + public let minWidth: CGFloat + public let minHeight: CGFloat + + public let buttons: [BBAlert.Button] + public let buttonsAxis: BBAlertButtonAxis + public let buttonHeight: CGFloat + + public let titleNumberOfLines: Int + public let subtitleNumberOfLines: Int + + public let backgroundColor: UIColor? + public let cornerRadius: CGFloat? + + + // MARK: - Intializer + + public init( + minWidth: CGFloat = 280, + minHeight: CGFloat = 384, + buttons: [BBAlert.Button] = [.normal(title: "확인", tint: .yellow), .cancel], + buttonsAxis: BBAlertButtonAxis = .horizontal, + buttonHeight: CGFloat = 44, + titleNumberOfLines: Int = 1, + subtitleNumberOfLines: Int = 10, + backgroundColor: UIColor? = .black, + cornerRadius: CGFloat? = nil + ) { + self.minWidth = minWidth + self.minHeight = minHeight + self.buttons = buttons + self.buttonsAxis = buttonsAxis + self.buttonHeight = buttonHeight + self.titleNumberOfLines = titleNumberOfLines + self.subtitleNumberOfLines = subtitleNumberOfLines + self.backgroundColor = backgroundColor + self.cornerRadius = cornerRadius + } +} From 15c0fbf73bb74df239a274b9813a06942fcb98ee Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Fri, 9 Aug 2024 13:42:06 +0900 Subject: [PATCH 170/263] =?UTF-8?q?feat:=20ImageAlertView=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#598)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ViewControllers/MainViewController.swift | 24 ++++++-- .../DefaultToastView/DefaultAlertView.swift | 24 +++++--- .../DefaultToastView/ImageAlertView.swift | 57 ++++++++++++++++--- .../Sources/Bibbi/BBCommons/BBAlert.swift | 4 ++ .../Bibbi/BBCommons/BBAlertButton.swift | 8 ++- .../BBCommons/BBAlertViewConfiguration.swift | 4 +- 6 files changed, 97 insertions(+), 24 deletions(-) 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 871a15082..a9ed6ce34 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift @@ -294,6 +294,11 @@ extension MainViewController { MPEvent.Home.cameraTapped.track(with: nil) navigationController?.pushViewController(CameraViewControllerWrapper(cameraType: type).viewController, animated: true) case .survivalAlert: +// BBAlert.image( +// image: DesignSystemAsset.key.image, +// title: "미션 열쇠 획득!", +// subtitle: "열쇠를 획득해 잠금이 해제되었어요.\n미션 사진을 찍을 수 있어요!" +// ).show() BibbiAlertBuilder(self) .alertStyle(.takeSurvival) .setConfirmAction { [weak self] in @@ -302,12 +307,19 @@ extension MainViewController { } .present() case .pickAlert(let name, let id): - BibbiAlertBuilder(self) - .alertStyle(.pickMember(name)) - .setConfirmAction { [weak self] in - guard let self else { return } - self.alertConfirmRelay.accept((name, id)) } - .present() + let viewConfig = BBAlertViewConfiguration(buttonsAxis: .vertical) + BBAlert.image( + image: DesignSystemAsset.missionKeyGraphic.image, + title: "생존 확인하기", + subtitle: "[닉네임]님의 생존 여부를 물어볼까요?\n지금 알림이 전송됩니다.", + viewConfig: viewConfig + ).show() +// BibbiAlertBuilder(self) +// .alertStyle(.pickMember(name)) +// .setConfirmAction { [weak self] in +// guard let self else { return } +// self.alertConfirmRelay.accept((name, id)) } +// .present() case .missionUnlockedAlert: BibbiAlertBuilder(self) .alertStyle(.missionKey) diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/AlertViews/DefaultToastView/DefaultAlertView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/AlertViews/DefaultToastView/DefaultAlertView.swift index e0ef6984b..01182015b 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/AlertViews/DefaultToastView/DefaultAlertView.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/AlertViews/DefaultToastView/DefaultAlertView.swift @@ -132,21 +132,25 @@ public class DefaultAlertView: UIView, BBAlertView { addShadow() } - private func createAlertButton(for type: BBAlert.Button) -> UIButton { - let button = UIButton(type: .system) // TODO: - BBButton으로 교체 + private func createAlertButton(for type: BBAlert.Button) -> BBButton { + let button = BBButton(type: .system) - if case let .normal(title, tint, action) = type { + if case let .normal(title, titleFontStyle, titleColor, backgroundColor, action) = type { setupAlertButtotAttribute( button, title: title, - tint: tint, + titleFontStlye: titleFontStyle, + titleColor: titleColor, + backgroundColor: backgroundColor, action: action ) } else { setupAlertButtotAttribute( button, title: "취소", - tint: .gray // TODO: - BBColor로 교체 + titleFontStlye: .body1Bold, + titleColor: .gray400, + backgroundColor: .gray700 ) } @@ -154,9 +158,11 @@ public class DefaultAlertView: UIView, BBAlertView { } private func setupAlertButtotAttribute( - _ button: UIButton, + _ button: BBButton, title: String?, - tint: UIColor?, + titleFontStlye: BBFontStyle?, + titleColor: UIColor?, + backgroundColor: UIColor?, action: ((BBAlert?) -> Void)? = nil ) { let action = UIAction { [weak self] _ in @@ -164,7 +170,9 @@ public class DefaultAlertView: UIView, BBAlertView { } button.setTitle(title, for: .normal) - button.backgroundColor = tint + button.setTitleColor(titleColor, for: .normal) + button.setTitleFontStyle(titleFontStlye ?? .body1Bold) + button.backgroundColor = backgroundColor button.layer.cornerRadius = 8 button.layer.masksToBounds = false diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/AlertViews/DefaultToastView/ImageAlertView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/AlertViews/DefaultToastView/ImageAlertView.swift index a16776a89..fc99618eb 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/AlertViews/DefaultToastView/ImageAlertView.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/AlertViews/DefaultToastView/ImageAlertView.swift @@ -11,8 +11,9 @@ public class ImageAlertView: UIStackView, BBAlertStackView { // MARK: - Views - private let titleLabel = UILabel() // TODO: - BBLabel로 교체 - private let subtitleLabel = UILabel() + private let titleLabel = BBLabel(textAlignment: .center) + private let subtitleLabel = BBLabel(textAlignment: .center) + private let titleStack = UIStackView() private let imageView = UIImageView() @@ -26,29 +27,38 @@ public class ImageAlertView: UIStackView, BBAlertStackView { public init( image: UIImage? = nil, imageTint: UIColor? = nil, - title: String, + title: String?, + titleFontStyle: BBFontStyle? = nil, subtitle: String? = nil, + subtitleFontStyle: BBFontStyle? = nil, viewConfig: BBAlertViewConfiguration ) { super.init(frame: .zero) commonInit() titleLabel.text = title + titleLabel.fontStyle = titleFontStyle ?? .head2Bold titleLabel.numberOfLines = viewConfig.titleNumberOfLines - addArrangedSubview(titleLabel) + titleStack.addArrangedSubview(titleLabel) if let subtitle = subtitle { subtitleLabel.text = subtitle + subtitleLabel.fontStyle = subtitleFontStyle ?? .body2Regular subtitleLabel.numberOfLines = viewConfig.subtitleNumberOfLines - addArrangedSubview(subtitleLabel) + titleStack.addArrangedSubview(subtitleLabel) } + addArrangedSubview(titleStack) + if let image = image { imageView.image = image imageView.contentMode = .scaleAspectFit imageView.tintColor = imageTint addArrangedSubview(imageView) } + + setupCostraints() + setupAttributes() } required init(coder: NSCoder) { @@ -57,11 +67,44 @@ public class ImageAlertView: UIStackView, BBAlertStackView { // MARK: - Helpers + + private func setupCostraints() { + + titleLabel.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + titleLabel.topAnchor.constraint(equalTo: titleStack.topAnchor, constant: 20), + titleLabel.leadingAnchor.constraint(equalTo: titleStack.leadingAnchor), + titleLabel.trailingAnchor.constraint(equalTo: titleStack.trailingAnchor), + titleLabel.heightAnchor.constraint(lessThanOrEqualToConstant: 25) + ]) + + subtitleLabel.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + subtitleLabel.leadingAnchor.constraint(equalTo: titleStack.leadingAnchor), + subtitleLabel.trailingAnchor.constraint(equalTo: titleStack.trailingAnchor), + subtitleLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: 40) + ]) + + imageView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + imageView.leadingAnchor.constraint(equalTo: self.leadingAnchor), + imageView.trailingAnchor.constraint(equalTo: self.trailingAnchor), + imageView.heightAnchor.constraint(lessThanOrEqualToConstant: 151) + ]) + + } + + private func setupAttributes() { + titleStack.axis = .vertical + titleStack.spacing = 8 + titleStack.alignment = .fill + titleStack.distribution = .fillProportionally + } private func commonInit() { axis = .vertical - spacing = 3 - alignment = .center + spacing = 16 + alignment = .fill distribution = .fillProportionally } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert.swift index cdc20d2a9..2e1a67a05 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert.swift @@ -138,6 +138,10 @@ public class BBAlert { extension BBAlert { + public func addDelegate(_ delegate: BBAlertDelegate) { + multicast.add(delegate) + } + private func createBackgroundView() -> UIView? { switch config.background { case .none: diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertButton.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertButton.swift index 0311a41f2..9ea5c6c7d 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertButton.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertButton.swift @@ -10,7 +10,13 @@ import UIKit extension BBAlert { public enum Button { - case normal(title: String? = nil, tint: UIColor? = nil, action: ((BBAlert?) -> Void)? = nil) + case normal( + title: String? = nil, + titleFontStyle: BBFontStyle? = nil, + titleColor: UIColor? = nil, + backgroundColor: UIColor? = nil, + action: ((BBAlert?) -> Void)? = nil + ) case cancel } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertViewConfiguration.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertViewConfiguration.swift index b512cc1a7..bc981eaaa 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertViewConfiguration.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertViewConfiguration.swift @@ -34,12 +34,12 @@ public struct BBAlertViewConfiguration { public init( minWidth: CGFloat = 280, minHeight: CGFloat = 384, - buttons: [BBAlert.Button] = [.normal(title: "확인", tint: .yellow), .cancel], + buttons: [BBAlert.Button] = [.normal(title: "확인", titleColor: .bibbiBlack, backgroundColor: .mainYellow), .cancel], buttonsAxis: BBAlertButtonAxis = .horizontal, buttonHeight: CGFloat = 44, titleNumberOfLines: Int = 1, subtitleNumberOfLines: Int = 10, - backgroundColor: UIColor? = .black, + backgroundColor: UIColor? = .gray900, cornerRadius: CGFloat? = nil ) { self.minWidth = minWidth From d3f1f8daf30008308851bf566c3a22d639042073 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Fri, 9 Aug 2024 13:57:12 +0900 Subject: [PATCH 171/263] =?UTF-8?q?feat:=20BBAlertButtonLayout=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#598)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ViewControllers/MainViewController.swift | 2 +- .../DefaultToastView/DefaultAlertView.swift | 12 +++--- .../DefaultToastView/ImageAlertView.swift | 2 +- .../Bibbi/BBCommons/BBAlertButtonLayout.swift | 38 +++++++++++++++++++ .../BBCommons/BBAlertViewConfiguration.swift | 20 +++------- 5 files changed, 51 insertions(+), 23 deletions(-) create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertButtonLayout.swift 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 a9ed6ce34..44b7f012d 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift @@ -307,7 +307,7 @@ extension MainViewController { } .present() case .pickAlert(let name, let id): - let viewConfig = BBAlertViewConfiguration(buttonsAxis: .vertical) + let viewConfig = BBAlertViewConfiguration() BBAlert.image( image: DesignSystemAsset.missionKeyGraphic.image, title: "생존 확인하기", diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/AlertViews/DefaultToastView/DefaultAlertView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/AlertViews/DefaultToastView/DefaultAlertView.swift index 01182015b..e8f8fd18f 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/AlertViews/DefaultToastView/DefaultAlertView.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/AlertViews/DefaultToastView/DefaultAlertView.swift @@ -60,7 +60,7 @@ public class DefaultAlertView: UIView, BBAlertView { private func setupUI() { - for type in viewConfig.buttons { + for type in viewConfig.buttonLayout.buttons { let button = createAlertButton(for: type) buttonStack.addArrangedSubview(button) } @@ -93,11 +93,11 @@ public class DefaultAlertView: UIView, BBAlertView { buttonStack.trailingAnchor.constraint(equalTo: self.layoutMarginsGuide.trailingAnchor), ]) - if case .vertical = viewConfig.buttonsAxis { + if case .vertical = viewConfig.buttonLayout.axis { let count = CGFloat(buttonStack.arrangedSubviews.count) - buttonStack.heightAnchor.constraint(equalToConstant: count * viewConfig.buttonHeight + (buttonSpacing * count)).isActive = true + buttonStack.heightAnchor.constraint(equalToConstant: count * viewConfig.buttonLayout.height + (buttonSpacing * count)).isActive = true } else { - buttonStack.heightAnchor.constraint(equalToConstant: viewConfig.buttonHeight).isActive = true + buttonStack.heightAnchor.constraint(equalToConstant: viewConfig.buttonLayout.height).isActive = true } child.translatesAutoresizingMaskIntoConstraints = false @@ -112,7 +112,7 @@ public class DefaultAlertView: UIView, BBAlertView { private func setupButtonConstraints() { for button in buttonStack.arrangedSubviews { NSLayoutConstraint.activate([ - button.heightAnchor.constraint(equalToConstant: viewConfig.buttonHeight) + button.heightAnchor.constraint(equalToConstant: viewConfig.buttonLayout.height) ]) } } @@ -124,7 +124,7 @@ public class DefaultAlertView: UIView, BBAlertView { layer.cornerRadius = viewConfig.cornerRadius ?? 16 backgroundColor = viewConfig.backgroundColor - buttonStack.axis = viewConfig.buttonsAxis + buttonStack.axis = viewConfig.buttonLayout.axis buttonStack.spacing = buttonSpacing buttonStack.alignment = .fill buttonStack.distribution = .fillEqually diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/AlertViews/DefaultToastView/ImageAlertView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/AlertViews/DefaultToastView/ImageAlertView.swift index fc99618eb..3053aaae5 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/AlertViews/DefaultToastView/ImageAlertView.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/AlertViews/DefaultToastView/ImageAlertView.swift @@ -89,7 +89,7 @@ public class ImageAlertView: UIStackView, BBAlertStackView { NSLayoutConstraint.activate([ imageView.leadingAnchor.constraint(equalTo: self.leadingAnchor), imageView.trailingAnchor.constraint(equalTo: self.trailingAnchor), - imageView.heightAnchor.constraint(lessThanOrEqualToConstant: 151) + imageView.heightAnchor.constraint(greaterThanOrEqualToConstant: 151) ]) } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertButtonLayout.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertButtonLayout.swift new file mode 100644 index 000000000..1efe0813a --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertButtonLayout.swift @@ -0,0 +1,38 @@ +// +// BBAlertButtonConfiguration.swift +// Core +// +// Created by 김건우 on 8/9/24. +// + +import UIKit + +// MARK: - Typealias + +public typealias BBAlertButtonAxis = NSLayoutConstraint.Axis + +public struct BBAlertButtonLayout { + + // MARK: - Properties + + public let buttons: [BBAlert.Button] + public let axis: BBAlertButtonAxis + public let height: CGFloat + + + // MARK: - Intializer + + public init( + buttons: [BBAlert.Button] = [ + .normal(title: "확인", titleColor: .bibbiBlack, backgroundColor: .mainYellow), + .cancel + ], + axis: BBAlertButtonAxis = .vertical, + height: CGFloat = 44 + ) { + self.buttons = buttons + self.axis = axis + self.height = height + } + +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertViewConfiguration.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertViewConfiguration.swift index bc981eaaa..4324a9cd7 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertViewConfiguration.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertViewConfiguration.swift @@ -7,10 +7,6 @@ import UIKit -// MARK: - Typealias - -public typealias BBAlertButtonAxis = NSLayoutConstraint.Axis - public struct BBAlertViewConfiguration { // MARK: - Properties @@ -18,38 +14,32 @@ public struct BBAlertViewConfiguration { public let minWidth: CGFloat public let minHeight: CGFloat - public let buttons: [BBAlert.Button] - public let buttonsAxis: BBAlertButtonAxis - public let buttonHeight: CGFloat - public let titleNumberOfLines: Int public let subtitleNumberOfLines: Int public let backgroundColor: UIColor? public let cornerRadius: CGFloat? + public let buttonLayout: BBAlertButtonLayout + // MARK: - Intializer public init( minWidth: CGFloat = 280, minHeight: CGFloat = 384, - buttons: [BBAlert.Button] = [.normal(title: "확인", titleColor: .bibbiBlack, backgroundColor: .mainYellow), .cancel], - buttonsAxis: BBAlertButtonAxis = .horizontal, - buttonHeight: CGFloat = 44, titleNumberOfLines: Int = 1, subtitleNumberOfLines: Int = 10, backgroundColor: UIColor? = .gray900, - cornerRadius: CGFloat? = nil + cornerRadius: CGFloat? = nil, + buttonLayout: BBAlertButtonLayout = BBAlertButtonLayout() ) { self.minWidth = minWidth self.minHeight = minHeight - self.buttons = buttons - self.buttonsAxis = buttonsAxis - self.buttonHeight = buttonHeight self.titleNumberOfLines = titleNumberOfLines self.subtitleNumberOfLines = subtitleNumberOfLines self.backgroundColor = backgroundColor self.cornerRadius = cornerRadius + self.buttonLayout = buttonLayout } } From c118a268ac3685cb21300ab95380b02e371d486f Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Fri, 9 Aug 2024 14:50:35 +0900 Subject: [PATCH 172/263] =?UTF-8?q?feat:=20TextAlertView=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#598)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ViewControllers/MainViewController.swift | 15 ++-- .../DefaultToastView/DefaultAlertView.swift | 5 +- .../DefaultToastView/ImageAlertView.swift | 8 +- .../DefaultToastView/TextAlertView.swift | 74 ++++++++++++++++++- .../Sources/Bibbi/BBCommons/BBAlert.swift | 26 +++++++ 5 files changed, 110 insertions(+), 18 deletions(-) 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 44b7f012d..84c7dc539 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift @@ -307,13 +307,14 @@ extension MainViewController { } .present() case .pickAlert(let name, let id): - let viewConfig = BBAlertViewConfiguration() - BBAlert.image( - image: DesignSystemAsset.missionKeyGraphic.image, - title: "생존 확인하기", - subtitle: "[닉네임]님의 생존 여부를 물어볼까요?\n지금 알림이 전송됩니다.", - viewConfig: viewConfig - ).show() + let viewConfig = BBAlertViewConfiguration(minHeight: 181, buttonLayout: BBAlertButtonLayout(buttons: [.cancel, .normal(title: "확인", titleColor: .bibbiBlack, backgroundColor: .mainYellow)], axis: .horizontal)) + BBAlert.text(title: "가족 방 이름을 초기화 하겠습니까?", subtitle: "홈 화면의 가족방 이름이 사라지고\nBibbi 로고로 바뀌어요", viewConfig: viewConfig).show() +// BBAlert.image( +// image: DesignSystemAsset.missionKeyGraphic.image, +// title: "생존 확인하기", +// subtitle: "[닉네임]님의 생존 여부를 물어볼까요?\n지금 알림이 전송됩니다.", +// viewConfig: viewConfig +// ).show() // BibbiAlertBuilder(self) // .alertStyle(.pickMember(name)) // .setConfirmAction { [weak self] in diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/AlertViews/DefaultToastView/DefaultAlertView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/AlertViews/DefaultToastView/DefaultAlertView.swift index e8f8fd18f..7fefea466 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/AlertViews/DefaultToastView/DefaultAlertView.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/AlertViews/DefaultToastView/DefaultAlertView.swift @@ -15,7 +15,6 @@ public class DefaultAlertView: UIView, BBAlertView { private let buttonStack: UIStackView = UIStackView() - private let alertStack: UIStackView = UIStackView() // MARK: - Properties @@ -79,7 +78,7 @@ public class DefaultAlertView: UIView, BBAlertView { trailingAnchor.constraint(lessThanOrEqualTo: superview.layoutMarginsGuide.trailingAnchor), bottomAnchor.constraint(lessThanOrEqualTo: superview.layoutMarginsGuide.bottomAnchor), centerXAnchor.constraint(equalTo: superview.centerXAnchor), - centerYAnchor.constraint(equalTo: superview.centerYAnchor) + centerYAnchor.constraint(equalTo: superview.centerYAnchor), ]) setupSubviewConstraints() @@ -102,7 +101,7 @@ public class DefaultAlertView: UIView, BBAlertView { child.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - child.bottomAnchor.constraint(equalTo: buttonStack.topAnchor, constant: -10), + child.bottomAnchor.constraint(equalTo: buttonStack.topAnchor, constant: -16), child.leadingAnchor.constraint(equalTo: self.layoutMarginsGuide.leadingAnchor), child.trailingAnchor.constraint(equalTo: self.layoutMarginsGuide.trailingAnchor), child.topAnchor.constraint(equalTo: self.layoutMarginsGuide.topAnchor) diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/AlertViews/DefaultToastView/ImageAlertView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/AlertViews/DefaultToastView/ImageAlertView.swift index 3053aaae5..0a5b141e0 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/AlertViews/DefaultToastView/ImageAlertView.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/AlertViews/DefaultToastView/ImageAlertView.swift @@ -73,22 +73,16 @@ public class ImageAlertView: UIStackView, BBAlertStackView { titleLabel.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ titleLabel.topAnchor.constraint(equalTo: titleStack.topAnchor, constant: 20), - titleLabel.leadingAnchor.constraint(equalTo: titleStack.leadingAnchor), - titleLabel.trailingAnchor.constraint(equalTo: titleStack.trailingAnchor), - titleLabel.heightAnchor.constraint(lessThanOrEqualToConstant: 25) + titleLabel.heightAnchor.constraint(equalToConstant: 25) ]) subtitleLabel.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - subtitleLabel.leadingAnchor.constraint(equalTo: titleStack.leadingAnchor), - subtitleLabel.trailingAnchor.constraint(equalTo: titleStack.trailingAnchor), subtitleLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: 40) ]) imageView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - imageView.leadingAnchor.constraint(equalTo: self.leadingAnchor), - imageView.trailingAnchor.constraint(equalTo: self.trailingAnchor), imageView.heightAnchor.constraint(greaterThanOrEqualToConstant: 151) ]) diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/AlertViews/DefaultToastView/TextAlertView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/AlertViews/DefaultToastView/TextAlertView.swift index c3bed8911..fcd7940fc 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/AlertViews/DefaultToastView/TextAlertView.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/AlertViews/DefaultToastView/TextAlertView.swift @@ -5,4 +5,76 @@ // Created by 김건우 on 8/6/24. // -import Foundation +import UIKit + +public class TextAlertView: UIStackView, BBAlertStackView { + + // MARK: - Views + + private let titleLabel = BBLabel(textAlignment: .center) + private let subtitleLabel = BBLabel(textAlignment: .center) + + // MARK: - Properties + + public var alert: BBAlert? + + + // MARK: - Intializer + + public init( + _ title: String?, + titleFontStyle: BBFontStyle? = nil, + subtitle: String? = nil, + subtitleFontStyle: BBFontStyle? = nil, + viewConfig: BBAlertViewConfiguration + ) { + super.init(frame: .zero) + commonInit() + + titleLabel.text = title + titleLabel.fontStyle = titleFontStyle ?? .head2Bold + titleLabel.numberOfLines = viewConfig.titleNumberOfLines + addArrangedSubview(titleLabel) + + if let subtitle = subtitle { + subtitleLabel.text = subtitle + subtitleLabel.textColor = .gray300 + subtitleLabel.fontStyle = titleFontStyle ?? .body1Regular + subtitleLabel.numberOfLines = viewConfig.subtitleNumberOfLines + addArrangedSubview(subtitleLabel) + } + + setupConstraints() + } + + required init(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + // MARK: - Helpers + + private func setupConstraints() { + + titleLabel.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + titleLabel.topAnchor.constraint(equalTo: self.topAnchor, constant: 20), + titleLabel.heightAnchor.constraint(equalToConstant: 25) + ]) + + subtitleLabel.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + subtitleLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: 40) + ]) + + } + + private func commonInit() { + axis = .vertical + spacing = 8 + alignment = .fill + distribution = .fillProportionally + } + +} + diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert.swift index 2e1a67a05..cced2e163 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert.swift @@ -25,11 +25,35 @@ public class BBAlert { // MARK: - Alert + public static func text( + title: String, + titleFontStyle: BBFontStyle? = nil, + subtitle: String? = nil, + subtitleFontStyle: BBFontStyle? = nil, + viewConfig: BBAlertViewConfiguration = BBAlertViewConfiguration(), + config: BBAlertConfiguration = BBAlertConfiguration() + ) -> BBAlert { + let view = DefaultAlertView( + child: TextAlertView( + title, + titleFontStyle: titleFontStyle, + subtitle: subtitle, + subtitleFontStyle: subtitleFontStyle, + viewConfig: viewConfig + ), + viewConfig: viewConfig + ) + + return BBAlert(view: view, config: config) + } + public static func `image`( image: UIImage? = nil, imageTint: UIColor? = nil, title: String, + titleFontStyle: BBFontStyle? = nil, subtitle: String? = nil, + subtitleFontStyle: BBFontStyle? = nil, viewConfig: BBAlertViewConfiguration = BBAlertViewConfiguration(), config: BBAlertConfiguration = BBAlertConfiguration() ) -> BBAlert { @@ -38,7 +62,9 @@ public class BBAlert { image: image, imageTint: imageTint, title: title, + titleFontStyle: titleFontStyle, subtitle: subtitle, + subtitleFontStyle: subtitleFontStyle, viewConfig: viewConfig ), viewConfig: viewConfig From 575fbe3b3b26900cb29deecbcadd0dc36499c1e4 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Fri, 9 Aug 2024 18:30:07 +0900 Subject: [PATCH 173/263] =?UTF-8?q?move:=20BibbiAlertBuilder=20Deprecated?= =?UTF-8?q?=20=EC=B2=98=EB=A6=AC=20(#598)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BBCommons/{ => BBAlert}/AlertViews/BBAlertStackView.swift | 0 .../Bibbi/BBCommons/{ => BBAlert}/AlertViews/BBAlertView.swift | 0 .../AlertViews/DefaultToastView/DefaultAlertView.swift | 0 .../AlertViews/DefaultToastView/ImageAlertView.swift | 0 .../AlertViews/DefaultToastView/TextAlertView.swift | 0 .../Core/Sources/Bibbi/BBCommons/{ => BBAlert}/BBAlert.swift | 0 .../Sources/Bibbi/BBCommons/{ => BBAlert}/BBAlertAnimation.swift | 0 .../Bibbi/BBCommons/{ => BBAlert}/BBAlertBackground.swift | 0 .../Sources/Bibbi/BBCommons/{ => BBAlert}/BBAlertButton.swift | 0 .../Bibbi/BBCommons/{ => BBAlert}/BBAlertButtonLayout.swift | 0 .../Bibbi/BBCommons/{ => BBAlert}/BBAlertButtonsAxis.swift | 0 .../Bibbi/BBCommons/{ => BBAlert}/BBAlertConfiguration.swift | 0 .../Sources/Bibbi/BBCommons/{ => BBAlert}/BBAlertDelegate.swift | 0 .../Sources/Bibbi/BBCommons/{ => BBAlert}/BBAlertStyle.swift | 0 .../Bibbi/BBCommons/{ => BBAlert}/BBAlertViewConfiguration.swift | 0 .../BBCommons => Trash}/BBAlertBuilder/BibbiAlertAction.swift | 0 .../BBCommons => Trash}/BBAlertBuilder/BibbiAlertBuilder.swift | 1 + .../BBCommons => Trash}/BBAlertBuilder/BibbiAlertStyle.swift | 0 .../BBCommons => Trash}/BBAlertBuilder/BibbiAlertTitle.swift | 0 .../BBAlertBuilder/BibbiAlertViewController.swift | 0 20 files changed, 1 insertion(+) rename 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/{ => BBAlert}/AlertViews/BBAlertStackView.swift (100%) rename 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/{ => BBAlert}/AlertViews/BBAlertView.swift (100%) rename 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/{ => BBAlert}/AlertViews/DefaultToastView/DefaultAlertView.swift (100%) rename 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/{ => BBAlert}/AlertViews/DefaultToastView/ImageAlertView.swift (100%) rename 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/{ => BBAlert}/AlertViews/DefaultToastView/TextAlertView.swift (100%) rename 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/{ => BBAlert}/BBAlert.swift (100%) rename 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/{ => BBAlert}/BBAlertAnimation.swift (100%) rename 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/{ => BBAlert}/BBAlertBackground.swift (100%) rename 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/{ => BBAlert}/BBAlertButton.swift (100%) rename 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/{ => BBAlert}/BBAlertButtonLayout.swift (100%) rename 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/{ => BBAlert}/BBAlertButtonsAxis.swift (100%) rename 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/{ => BBAlert}/BBAlertConfiguration.swift (100%) rename 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/{ => BBAlert}/BBAlertDelegate.swift (100%) rename 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/{ => BBAlert}/BBAlertStyle.swift (100%) rename 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/{ => BBAlert}/BBAlertViewConfiguration.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi/BBCommons => Trash}/BBAlertBuilder/BibbiAlertAction.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi/BBCommons => Trash}/BBAlertBuilder/BibbiAlertBuilder.swift (98%) rename 14th-team5-iOS/Core/Sources/{Bibbi/BBCommons => Trash}/BBAlertBuilder/BibbiAlertStyle.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi/BBCommons => Trash}/BBAlertBuilder/BibbiAlertTitle.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi/BBCommons => Trash}/BBAlertBuilder/BibbiAlertViewController.swift (100%) diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/AlertViews/BBAlertStackView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/AlertViews/BBAlertStackView.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/AlertViews/BBAlertStackView.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/AlertViews/BBAlertStackView.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/AlertViews/BBAlertView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/AlertViews/BBAlertView.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/AlertViews/BBAlertView.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/AlertViews/BBAlertView.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/AlertViews/DefaultToastView/DefaultAlertView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/AlertViews/DefaultToastView/DefaultAlertView.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/AlertViews/DefaultToastView/DefaultAlertView.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/AlertViews/DefaultToastView/DefaultAlertView.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/AlertViews/DefaultToastView/ImageAlertView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/AlertViews/DefaultToastView/ImageAlertView.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/AlertViews/DefaultToastView/ImageAlertView.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/AlertViews/DefaultToastView/ImageAlertView.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/AlertViews/DefaultToastView/TextAlertView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/AlertViews/DefaultToastView/TextAlertView.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/AlertViews/DefaultToastView/TextAlertView.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/AlertViews/DefaultToastView/TextAlertView.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlert.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlert.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertAnimation.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertAnimation.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertAnimation.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertAnimation.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertBackground.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertBackground.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertBackground.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertBackground.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertButton.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertButton.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertButton.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertButton.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertButtonLayout.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertButtonLayout.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertButtonLayout.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertButtonLayout.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertButtonsAxis.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertButtonsAxis.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertButtonsAxis.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertButtonsAxis.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertConfiguration.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertConfiguration.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertConfiguration.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertConfiguration.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertDelegate.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertDelegate.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertDelegate.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertDelegate.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertStyle.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertStyle.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertStyle.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertStyle.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertViewConfiguration.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertViewConfiguration.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertViewConfiguration.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertViewConfiguration.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertBuilder/BibbiAlertAction.swift b/14th-team5-iOS/Core/Sources/Trash/BBAlertBuilder/BibbiAlertAction.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertBuilder/BibbiAlertAction.swift rename to 14th-team5-iOS/Core/Sources/Trash/BBAlertBuilder/BibbiAlertAction.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertBuilder/BibbiAlertBuilder.swift b/14th-team5-iOS/Core/Sources/Trash/BBAlertBuilder/BibbiAlertBuilder.swift similarity index 98% rename from 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertBuilder/BibbiAlertBuilder.swift rename to 14th-team5-iOS/Core/Sources/Trash/BBAlertBuilder/BibbiAlertBuilder.swift index d57314e73..a5b9f6acf 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertBuilder/BibbiAlertBuilder.swift +++ b/14th-team5-iOS/Core/Sources/Trash/BBAlertBuilder/BibbiAlertBuilder.swift @@ -8,6 +8,7 @@ import DesignSystem import UIKit +@available(*, deprecated, renamed: "BBAlert") public final class BibbiAlertBuilder { // MARK: - Properties diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertBuilder/BibbiAlertStyle.swift b/14th-team5-iOS/Core/Sources/Trash/BBAlertBuilder/BibbiAlertStyle.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertBuilder/BibbiAlertStyle.swift rename to 14th-team5-iOS/Core/Sources/Trash/BBAlertBuilder/BibbiAlertStyle.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertBuilder/BibbiAlertTitle.swift b/14th-team5-iOS/Core/Sources/Trash/BBAlertBuilder/BibbiAlertTitle.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertBuilder/BibbiAlertTitle.swift rename to 14th-team5-iOS/Core/Sources/Trash/BBAlertBuilder/BibbiAlertTitle.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertBuilder/BibbiAlertViewController.swift b/14th-team5-iOS/Core/Sources/Trash/BBAlertBuilder/BibbiAlertViewController.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlertBuilder/BibbiAlertViewController.swift rename to 14th-team5-iOS/Core/Sources/Trash/BBAlertBuilder/BibbiAlertViewController.swift From f60f3622c76c801d91f9c257c202048155b6bf00 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Fri, 9 Aug 2024 19:36:25 +0900 Subject: [PATCH 174/263] =?UTF-8?q?feat:=20BBAlertStyle=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#598)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ViewControllers/MainViewController.swift | 25 +-- .../DefaultToastView/DefaultAlertView.swift | 27 ++-- .../Bibbi/BBCommons/BBAlert/BBAlert.swift | 149 +++++++++++++++++- .../BBCommons/BBAlert/BBAlertButton.swift | 11 +- .../BBAlert/BBAlertButtonLayout.swift | 38 ----- .../BBCommons/BBAlert/BBAlertStyle.swift | 10 +- .../BBCommons/BBAlert/ButtonLayout.swift | 44 ++++++ 7 files changed, 231 insertions(+), 73 deletions(-) delete mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertButtonLayout.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/ButtonLayout.swift 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 84c7dc539..871a15082 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift @@ -294,11 +294,6 @@ extension MainViewController { MPEvent.Home.cameraTapped.track(with: nil) navigationController?.pushViewController(CameraViewControllerWrapper(cameraType: type).viewController, animated: true) case .survivalAlert: -// BBAlert.image( -// image: DesignSystemAsset.key.image, -// title: "미션 열쇠 획득!", -// subtitle: "열쇠를 획득해 잠금이 해제되었어요.\n미션 사진을 찍을 수 있어요!" -// ).show() BibbiAlertBuilder(self) .alertStyle(.takeSurvival) .setConfirmAction { [weak self] in @@ -307,20 +302,12 @@ extension MainViewController { } .present() case .pickAlert(let name, let id): - let viewConfig = BBAlertViewConfiguration(minHeight: 181, buttonLayout: BBAlertButtonLayout(buttons: [.cancel, .normal(title: "확인", titleColor: .bibbiBlack, backgroundColor: .mainYellow)], axis: .horizontal)) - BBAlert.text(title: "가족 방 이름을 초기화 하겠습니까?", subtitle: "홈 화면의 가족방 이름이 사라지고\nBibbi 로고로 바뀌어요", viewConfig: viewConfig).show() -// BBAlert.image( -// image: DesignSystemAsset.missionKeyGraphic.image, -// title: "생존 확인하기", -// subtitle: "[닉네임]님의 생존 여부를 물어볼까요?\n지금 알림이 전송됩니다.", -// viewConfig: viewConfig -// ).show() -// BibbiAlertBuilder(self) -// .alertStyle(.pickMember(name)) -// .setConfirmAction { [weak self] in -// guard let self else { return } -// self.alertConfirmRelay.accept((name, id)) } -// .present() + BibbiAlertBuilder(self) + .alertStyle(.pickMember(name)) + .setConfirmAction { [weak self] in + guard let self else { return } + self.alertConfirmRelay.accept((name, id)) } + .present() case .missionUnlockedAlert: BibbiAlertBuilder(self) .alertStyle(.missionKey) 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 7fefea466..3d4880d09 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 @@ -134,7 +134,8 @@ public class DefaultAlertView: UIView, BBAlertView { private func createAlertButton(for type: BBAlert.Button) -> BBButton { let button = BBButton(type: .system) - if case let .normal(title, titleFontStyle, titleColor, backgroundColor, action) = type { + switch type { + case let .normal(title, titleFontStyle, titleColor, backgroundColor, action): setupAlertButtotAttribute( button, title: title, @@ -143,11 +144,17 @@ public class DefaultAlertView: UIView, BBAlertView { backgroundColor: backgroundColor, action: action ) - } else { + + case let .confirm(title, action): setupAlertButtotAttribute( button, - title: "취소", - titleFontStlye: .body1Bold, + title: title, + action: action + ) + case let .cancel(title): + setupAlertButtotAttribute( + button, + title: title, titleColor: .gray400, backgroundColor: .gray700 ) @@ -159,19 +166,19 @@ public class DefaultAlertView: UIView, BBAlertView { private func setupAlertButtotAttribute( _ button: BBButton, title: String?, - titleFontStlye: BBFontStyle?, - titleColor: UIColor?, - backgroundColor: UIColor?, - action: ((BBAlert?) -> Void)? = nil + titleFontStlye: BBFontStyle? = nil, + titleColor: UIColor? = nil, + backgroundColor: UIColor? = nil, + action: BBAlertAction = nil ) { let action = UIAction { [weak self] _ in action?(self?.alert) ?? self?.alert?.close() } button.setTitle(title, for: .normal) - button.setTitleColor(titleColor, for: .normal) + button.setTitleColor(titleColor ?? .bibbiBlack, for: .normal) button.setTitleFontStyle(titleFontStlye ?? .body1Bold) - button.backgroundColor = backgroundColor + button.backgroundColor = backgroundColor ?? .mainYellow button.layer.cornerRadius = 8 button.layer.masksToBounds = false diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlert.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlert.swift index cced2e163..a20d67a15 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlert.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlert.swift @@ -5,6 +5,7 @@ // Created by 김건우 on 8/6/24. // +import DesignSystem import UIKit public class BBAlert { @@ -47,7 +48,7 @@ public class BBAlert { return BBAlert(view: view, config: config) } - public static func `image`( + public static func image( image: UIImage? = nil, imageTint: UIColor? = nil, title: String, @@ -73,6 +74,152 @@ public class BBAlert { return BBAlert(view: view, config: config) } + public static func style( + _ style: BBAlertStyle, + primaryAction action: BBAlertAction, + config: BBAlertConfiguration = BBAlertConfiguration() + ) -> BBAlert { + switch style { + case .logout: + let layout = BBAlertButtonLayout( + buttons: [.cancel(), .confirm(action: action)], + axis: .horizontal + ) + let viewConfig = BBAlertViewConfiguration( + minHeight: 145, + buttonLayout: layout + ) + let view = DefaultAlertView( + child: TextAlertView( + "로그아웃", + subtitle: "로그아웃 하시겠어요?", + viewConfig: viewConfig + ), + viewConfig: viewConfig + ) + return BBAlert(view: view, config: config) + + case .makeNewFamily: + let layout = BBAlertButtonLayout( + buttons: [.cancel(), .confirm(action: action)], + axis: .horizontal + ) + let viewConfig = BBAlertViewConfiguration( + minHeight: 181, + buttonLayout: layout + ) + let view = DefaultAlertView( + child: TextAlertView( + "새 가족 방 만들기", + subtitle: "초대 받은 가족이 없어\n새 가족 방으로 입장할래요", + viewConfig: viewConfig + ), + viewConfig: viewConfig + ) + return BBAlert(view: view, config: config) + + case .resetFamilyName: + let layout = BBAlertButtonLayout( + buttons: [.cancel(), .confirm(action: action)], + axis: .horizontal + ) + let viewConfig = BBAlertViewConfiguration( + minHeight: 181, + buttonLayout: layout + ) + let view = DefaultAlertView( + child: TextAlertView( + "가족 방 이름을 초기화 하겠습니까?", + subtitle: "홈 화면의 가족방 이름이 사라지고\nBibbi 로고로 바뀌어요", + viewConfig: viewConfig + ), + viewConfig: viewConfig + ) + return BBAlert(view: view, config: config) + + case .widget: + let layout = BBAlertButtonLayout( + buttons: [.confirm(title: "확인하기", action: action), .cancel(title: "닫기")], + axis: .vertical + ) + let viewConfig = BBAlertViewConfiguration( + minHeight: 384, + buttonLayout: layout + ) + let view = DefaultAlertView( + child: ImageAlertView( + image: DesignSystemAsset.widgetGraphic.image, + title: "위젯 추가하셨나요?", + subtitle: "홈 화면에서 위젯으로\n가족의 소식을 한눈에 파악할 수 있어요", + viewConfig: viewConfig + ), + viewConfig: viewConfig + ) + return BBAlert(view: view, config: config) + + case .mission: + let layout = BBAlertButtonLayout( + buttons: [.confirm(title: "미션 사진 찍기", action: action), .cancel(title: "닫기")], + axis: .vertical + ) + let viewConfig = BBAlertViewConfiguration( + minHeight: 384, + buttonLayout: layout + ) + let view = DefaultAlertView( + child: ImageAlertView( + image: DesignSystemAsset.missionKeyGraphic.image, + title: "미션 열쇠 획득!", + subtitle: "열쇠를 획득해 잠금이 해제되었어요.\n미션 사진을 찍을 수 있어요!", + viewConfig: viewConfig + ), + viewConfig: viewConfig + ) + return BBAlert(view: view, config: config) + + case let .picking(name): + let layout = BBAlertButtonLayout( + buttons: [.confirm(title: "지금 하기", action: action), .cancel(title: "다음에 하기")], + axis: .vertical + ) + let viewConfig = BBAlertViewConfiguration( + minHeight: 384, + buttonLayout: layout + ) + let view = DefaultAlertView( + child: ImageAlertView( + image: DesignSystemAsset.exhaustedBibbiGraphic.image, + title: "생존 확인하기", + subtitle: "\(name)님의 생존 여부를 물어볼까요?\n지금 알림이 전송됩니다.", + viewConfig: viewConfig + ), + viewConfig: viewConfig + ) + return BBAlert(view: view, config: config) + + case .takePhoto: + let layout = BBAlertButtonLayout( + buttons: [.confirm(title: "생존 신고 먼저하기", action: action), .cancel(title: "다음에 하기")], + axis: .vertical + ) + let viewConfig = BBAlertViewConfiguration( + minHeight: 384, + buttonLayout: layout + ) + let view = DefaultAlertView( + child: ImageAlertView( + image: DesignSystemAsset.takeSurvivalGraphic.image, + title: "생존신고 사진을 먼저 찍으세요!", + subtitle: "미션 사진을 올리려면\n생존신고 사진을 먼저 업로드해야해요.", + viewConfig: viewConfig + ), + viewConfig: viewConfig + ) + return BBAlert(view: view, config: config) + + } + } + public static func custom( _ child: BBAlertStackView, viewConfig: BBAlertViewConfiguration = BBAlertViewConfiguration(), diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertButton.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertButton.swift index 9ea5c6c7d..d792958bf 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertButton.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertButton.swift @@ -7,17 +7,22 @@ import UIKit +// MARK: - Typelias + +public typealias BBAlertAction = ((BBAlert?) -> Void)? + extension BBAlert { public enum Button { case normal( - title: String? = nil, + title: String? = "확인", titleFontStyle: BBFontStyle? = nil, titleColor: UIColor? = nil, backgroundColor: UIColor? = nil, - action: ((BBAlert?) -> Void)? = nil + action: BBAlertAction = nil ) - case cancel + case confirm(title: String? = "확인", action: BBAlertAction = nil) + case cancel(title: String = "취소") } } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertButtonLayout.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertButtonLayout.swift deleted file mode 100644 index 1efe0813a..000000000 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertButtonLayout.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// BBAlertButtonConfiguration.swift -// Core -// -// Created by 김건우 on 8/9/24. -// - -import UIKit - -// MARK: - Typealias - -public typealias BBAlertButtonAxis = NSLayoutConstraint.Axis - -public struct BBAlertButtonLayout { - - // MARK: - Properties - - public let buttons: [BBAlert.Button] - public let axis: BBAlertButtonAxis - public let height: CGFloat - - - // MARK: - Intializer - - public init( - buttons: [BBAlert.Button] = [ - .normal(title: "확인", titleColor: .bibbiBlack, backgroundColor: .mainYellow), - .cancel - ], - axis: BBAlertButtonAxis = .vertical, - height: CGFloat = 44 - ) { - self.buttons = buttons - self.axis = axis - self.height = height - } - -} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertStyle.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertStyle.swift index 175df6811..59076ad56 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertStyle.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertStyle.swift @@ -7,14 +7,20 @@ import Foundation +// MARK: - Typealias + +public typealias BBAlertStyle = BBAlert.Style + extension BBAlert { public enum Style { case logout + case makeNewFamily + case resetFamilyName case widget case mission - case picking - case camera + case picking(name: String) + case takePhoto } } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/ButtonLayout.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/ButtonLayout.swift new file mode 100644 index 000000000..380df6125 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/ButtonLayout.swift @@ -0,0 +1,44 @@ +// +// BBAlertButtonConfiguration.swift +// Core +// +// Created by 김건우 on 8/9/24. +// + +import UIKit + +// MARK: - Typealias + +public typealias BBAlertButtonLayout = BBAlertViewConfiguration.ButtonLayout + +extension BBAlertViewConfiguration { + + public typealias BBAlertButtonAxis = NSLayoutConstraint.Axis + + public struct ButtonLayout { + + // MARK: - Properties + + public let buttons: [BBAlert.Button] + public let axis: BBAlertButtonAxis + public let height: CGFloat + + + // MARK: - Intializer + + public init( + buttons: [BBAlert.Button] = [ + .confirm(title: "확인"), + .cancel() + ], + axis: BBAlertButtonAxis = .vertical, + height: CGFloat = 44 + ) { + self.buttons = buttons + self.axis = axis + self.height = height + } + + } + +} From 0ca16fb86f5b3ee5b642598eab986247870a966f Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Fri, 9 Aug 2024 20:42:30 +0900 Subject: [PATCH 175/263] =?UTF-8?q?feat:=20BBButton=20=ED=95=98=EC=9D=B4?= =?UTF-8?q?=EB=9D=BC=EC=9D=B4=ED=8A=B8=20=ED=9A=A8=EA=B3=BC=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#598)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Bibbi/BBCommons/BBAlert/BBAlert.swift | 2 +- .../BBCommons/BBAlert/BBAlertButton.swift | 5 ++- .../Bibbi/BBCommons/BBButton/BBButton.swift | 35 ++++++++++++++++--- 3 files changed, 35 insertions(+), 7 deletions(-) diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlert.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlert.swift index a20d67a15..829aa6bd6 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlert.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlert.swift @@ -17,7 +17,7 @@ public class BBAlert { public let view: BBAlertView private var backgroundView: UIView? - public static var defaultImageTint: UIColor = .black + public static var defaultImageTint: UIColor = .bibbiBlack private var multicast = MulticaseDelegate() diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertButton.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertButton.swift index d792958bf..1fd84519e 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertButton.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertButton.swift @@ -21,7 +21,10 @@ extension BBAlert { backgroundColor: UIColor? = nil, action: BBAlertAction = nil ) - case confirm(title: String? = "확인", action: BBAlertAction = nil) + case confirm( + title: String? = "확인", + action: BBAlertAction = nil + ) case cancel(title: String = "취소") } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBButton/BBButton.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBButton/BBButton.swift index acddc90d4..5418534cb 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBButton/BBButton.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBButton/BBButton.swift @@ -13,20 +13,35 @@ final public class BBButton: UIButton { // MARK: - Views - public var bibbiTitleLabel: BBLabel = BBLabel() + public var mainTitleLabel: BBLabel = BBLabel() // MARK: - Properties public override var titleLabel: UILabel? { - get { bibbiTitleLabel } + get { mainTitleLabel } set { } } + public override var isHighlighted: Bool { + didSet { + guard + oldValue != self.isHighlighted + else { return } + + UIView.animate(withDuration: 0.1, delay: 0, options: [.beginFromCurrentState]) { + self.alpha = self.isHighlighted ? 0.5 : 1 + } + } + } + // MARK: - Intializer public override init(frame: CGRect) { super.init(frame: frame) + setupUI() + setupConstraints() + setupAttributes() } public required init?(coder aDecoder: NSCoder) { @@ -37,13 +52,23 @@ final public class BBButton: UIButton { // MARK: - Helpers private func setupUI() { - addSubview(bibbiTitleLabel) + addSubview(mainTitleLabel) - bibbiTitleLabel.snp.makeConstraints { make in - make.center.equalToSuperview() + } + + private func setupConstraints() { + mainTitleLabel.snp.makeConstraints { + $0.center.equalToSuperview() } } + private func setupAttributes() { + mainTitleLabel.do { + $0.numberOfLines = 1 + } + } + + /// 버튼의 타이틀을 변경합니다. public override func setTitle(_ title: String?, for state: UIControl.State) { titleLabel?.text = title From 2f3692479b00aa82d6cbda3e1aefcdd475f5556d Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Sat, 10 Aug 2024 10:15:24 +0900 Subject: [PATCH 176/263] =?UTF-8?q?feat:=20BBAlert=EC=9D=98=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20=EB=8F=99=EC=9E=91=20=EA=B5=AC=ED=98=84=20(#598)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ViewControllers/MainViewController.swift | 30 ++++++++++++++----- .../DefaultToastView/DefaultAlertView.swift | 25 ++++++++++++++-- .../Bibbi/BBCommons/BBAlert/BBAlert.swift | 14 ++++----- .../BBCommons/BBAlert/BBAlertDelegate.swift | 6 +++- .../Bibbi/BBCommons/BBButton/BBButton.swift | 8 +++++ .../Bibbi/BBCommons/BBToast/BBToast.swift | 2 +- ...Delegate.swift => MulticastDelegate.swift} | 8 +++-- 7 files changed, 71 insertions(+), 22 deletions(-) rename 14th-team5-iOS/Core/Sources/Utilities/{MulticaseDelegate.swift => MulticastDelegate.swift} (75%) 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 871a15082..820a10b26 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift @@ -15,7 +15,7 @@ import RxDataSources import RxCocoa import RxSwift -final class MainViewController: BaseViewController, UICollectionViewDelegateFlowLayout { +final class MainViewController: BaseViewController, UICollectionViewDelegateFlowLayout, BBAlertDelegate { private let familyViewController: MainFamilyViewController = MainFamilyViewControllerWrapper().makeViewController() private let timerView: TimerView = TimerView(reactor: TimerReactor()) @@ -302,12 +302,15 @@ extension MainViewController { } .present() case .pickAlert(let name, let id): - BibbiAlertBuilder(self) - .alertStyle(.pickMember(name)) - .setConfirmAction { [weak self] in - guard let self else { return } - self.alertConfirmRelay.accept((name, id)) } - .present() + let alert = BBAlert.style(.logout) + alert.addDelegate(self) + alert.show() +// BibbiAlertBuilder(self) +// .alertStyle(.pickMember(name)) +// .setConfirmAction { [weak self] in +// guard let self else { return } +// self.alertConfirmRelay.accept((name, id)) } +// .present() case .missionUnlockedAlert: BibbiAlertBuilder(self) .alertStyle(.missionKey) @@ -326,3 +329,16 @@ extension MainViewController: UIPopoverPresentationControllerDelegate { return .none } } + +extension MainViewController { + + func willShowAlert(_ alert: BBAlert) { + print("=============== ") + print(#function) + } + + func didTapAlertButton(_ alert: BBAlert?, index: Int?, button: BBButton) { + print("버튼이 클릭됨! \(index)") + } + +} 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 3d4880d09..2e8b292d1 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 @@ -18,6 +18,8 @@ public class DefaultAlertView: UIView, BBAlertView { // MARK: - Properties + public var id: Int = -1 + private var alert: BBAlert? private let viewConfig: BBAlertViewConfiguration @@ -171,9 +173,8 @@ public class DefaultAlertView: UIView, BBAlertView { backgroundColor: UIColor? = nil, action: BBAlertAction = nil ) { - let action = UIAction { [weak self] _ in - action?(self?.alert) ?? self?.alert?.close() - } + self.id += 1 + button.setId(id) button.setTitle(title, for: .normal) button.setTitleColor(titleColor ?? .bibbiBlack, for: .normal) @@ -183,6 +184,24 @@ public class DefaultAlertView: UIView, BBAlertView { button.layer.cornerRadius = 8 button.layer.masksToBounds = false + let action = UIAction { [weak self] _ in + guard let self = self else { return } + // 델리게이트 실행 + BBAlert.multicast.invoke { + $0.didTapAlertButton( + self.alert, + index: button.id, + button: button + ) + } + // 액션 클로저 실행 + if let action = action { + action(self.alert) + } else { + self.alert?.close() + } + } + button.addAction(action, for: .touchUpInside) } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlert.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlert.swift index 829aa6bd6..97a2961c5 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlert.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlert.swift @@ -19,7 +19,7 @@ public class BBAlert { public static var defaultImageTint: UIColor = .bibbiBlack - private var multicast = MulticaseDelegate() + public static var multicast = MulticastDelegate() public private(set) var config: BBAlertConfiguration @@ -76,7 +76,7 @@ public class BBAlert { public static func style( _ style: BBAlertStyle, - primaryAction action: BBAlertAction, + primaryAction action: BBAlertAction = nil, config: BBAlertConfiguration = BBAlertConfiguration() ) -> BBAlert { switch style { @@ -249,7 +249,7 @@ public class BBAlert { config.view?.addSubview(view) ?? BBHelper.topController()?.view.addSubview(view) view.createView(for: self) - multicast.invoke { $0.willShowAlert(self) } + Self.multicast.invoke { $0.willShowAlert(self) } config.enteringAnimation.apply(to: self.view) let endBackgroundColor = backgroundView?.backgroundColor @@ -258,7 +258,7 @@ public class BBAlert { self.config.enteringAnimation.undo(from: self.view) self.backgroundView?.backgroundColor = endBackgroundColor } completion: { [self] _ in - multicast.invoke { $0.didShowAlert(self) } + Self.multicast.invoke { $0.didShowAlert(self) } if !config.allowOverlapAlert { closeOverlappedAlerts() @@ -275,7 +275,7 @@ public class BBAlert { animated: Bool = true, completion: (() -> Void)? = nil ) { - multicast.invoke { $0.willCloseAlert(self) } + Self.multicast.invoke { $0.willCloseAlert(self) } UIView.animate(withDuration: 0.15) { if animated { @@ -289,7 +289,7 @@ public class BBAlert { BBAlert.activeAlerts.remove(at: index) } completion?() - self.multicast.invoke { $0.didCloseAlert(self) } + Self.multicast.invoke { $0.didCloseAlert(self) } } } @@ -312,7 +312,7 @@ public class BBAlert { extension BBAlert { public func addDelegate(_ delegate: BBAlertDelegate) { - multicast.add(delegate) + Self.multicast.add(delegate) } private func createBackgroundView() -> UIView? { diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertDelegate.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertDelegate.swift index e76779e0b..1ab9b0575 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertDelegate.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertDelegate.swift @@ -14,13 +14,17 @@ public protocol BBAlertDelegate: AnyObject { func willCloseAlert(_ alert: BBAlert) func didCloseAlert(_ alert: BBAlert) + func didTapAlertButton(_ alert: BBAlert?, index: Int?, button: BBButton) + } -extension BBAlertDelegate { +public extension BBAlertDelegate { func willShowAlert(_ alert: BBAlert) { } func didShowAlert(_ alert: BBAlert) { } func willCloseAlert(_ alert: BBAlert) { } func didCloseAlert(_ alert: BBAlert) { } + func didTapAlertButton(_ alert: BBAlert?, index: Int?, button: BBButton) { } + } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBButton/BBButton.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBButton/BBButton.swift index 5418534cb..175e8ead7 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBButton/BBButton.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBButton/BBButton.swift @@ -17,6 +17,8 @@ final public class BBButton: UIButton { // MARK: - Properties + public var id: Int? + public override var titleLabel: UILabel? { get { mainTitleLabel } set { } @@ -37,6 +39,7 @@ final public class BBButton: UIButton { // MARK: - Intializer public override init(frame: CGRect) { + self.id = nil super.init(frame: frame) setupUI() @@ -69,6 +72,11 @@ final public class BBButton: UIButton { } + /// 버튼이 가진 고유한 ID값을 변경합니다. + public func setId(_ id: Int) { + self.id = id + } + /// 버튼의 타이틀을 변경합니다. public override func setTitle(_ title: String?, for state: UIControl.State) { titleLabel?.text = title diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToast.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToast.swift index 0720f4b9c..6cb2c868b 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToast.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToast.swift @@ -24,7 +24,7 @@ public class BBToast { public static var defaultImageTint: UIColor = .bibbiWhite - public static var multicast = MulticaseDelegate() + public static var multicast = MulticastDelegate() public private(set) var config: BBToastConfiguration diff --git a/14th-team5-iOS/Core/Sources/Utilities/MulticaseDelegate.swift b/14th-team5-iOS/Core/Sources/Utilities/MulticastDelegate.swift similarity index 75% rename from 14th-team5-iOS/Core/Sources/Utilities/MulticaseDelegate.swift rename to 14th-team5-iOS/Core/Sources/Utilities/MulticastDelegate.swift index 5d14ba9e8..bc428554c 100644 --- a/14th-team5-iOS/Core/Sources/Utilities/MulticaseDelegate.swift +++ b/14th-team5-iOS/Core/Sources/Utilities/MulticastDelegate.swift @@ -7,12 +7,12 @@ import Foundation -final public class MulticaseDelegate { +final public class MulticastDelegate { private let delegates: NSHashTable = NSHashTable() func add(_ delegate: T) { - delegates.add(delegates as AnyObject) + delegates.add(delegate as AnyObject) } func remove(_ delegateToRemove: T) { @@ -25,7 +25,9 @@ final public class MulticaseDelegate { func invoke(_ invocation: (T) -> Void) { for delegate in delegates.allObjects.reversed() { - invocation(delegate as! T) + if let delegate = delegate as? T { + invocation(delegate) + } } } } From 6cb5f868b49a927951be3128c26d1707f094bc8e Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Sat, 10 Aug 2024 11:57:56 +0900 Subject: [PATCH 177/263] =?UTF-8?q?feat:=20BBToastStyle=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#598)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FamilyManagementViewController.swift | 19 ++++------ .../ViewControllers/MainViewController.swift | 30 ++++----------- .../Bibbi/BBCommons/BBAlert/BBAlert.swift | 4 ++ .../BBCommons/BBAlert/BBAlertButton.swift | 4 -- .../Bibbi/BBCommons/BBToast/BBToast.swift | 37 ++++++++++++++++--- ...Animation.swift => BBToastAnimation.swift} | 0 ...ckground.swift => BBToastBackground.swift} | 0 .../BBCommons/BBToast/BBToastDelegate.swift | 4 +- ...Direction.swift => BBToastDirection.swift} | 0 ...issable.swift => BBToastDismissable.swift} | 0 .../BBCommons/BBToast/BBToastStyle.swift | 20 ++++++++++ .../DefaultToastView/ButtonToastView.swift | 27 ++++++++++---- 12 files changed, 91 insertions(+), 54 deletions(-) rename 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/{Animation.swift => BBToastAnimation.swift} (100%) rename 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/{Background.swift => BBToastBackground.swift} (100%) rename 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/{Direction.swift => BBToastDirection.swift} (100%) rename 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/{Dismissable.swift => BBToastDismissable.swift} (100%) create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastStyle.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/ViewController/FamilyManagementViewController.swift b/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/ViewController/FamilyManagementViewController.swift index 88c9dc000..7336456aa 100644 --- a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/ViewController/FamilyManagementViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/ViewController/FamilyManagementViewController.swift @@ -145,17 +145,14 @@ public final class FamilyManagementViewController: BBNavigationViewController, UICollectionViewDelegateFlowLayout, BBAlertDelegate { +final class MainViewController: BaseViewController, UICollectionViewDelegateFlowLayout { private let familyViewController: MainFamilyViewController = MainFamilyViewControllerWrapper().makeViewController() private let timerView: TimerView = TimerView(reactor: TimerReactor()) @@ -302,15 +302,12 @@ extension MainViewController { } .present() case .pickAlert(let name, let id): - let alert = BBAlert.style(.logout) - alert.addDelegate(self) - alert.show() -// BibbiAlertBuilder(self) -// .alertStyle(.pickMember(name)) -// .setConfirmAction { [weak self] in -// guard let self else { return } -// self.alertConfirmRelay.accept((name, id)) } -// .present() + BibbiAlertBuilder(self) + .alertStyle(.pickMember(name)) + .setConfirmAction { [weak self] in + guard let self else { return } + self.alertConfirmRelay.accept((name, id)) } + .present() case .missionUnlockedAlert: BibbiAlertBuilder(self) .alertStyle(.missionKey) @@ -329,16 +326,3 @@ extension MainViewController: UIPopoverPresentationControllerDelegate { return .none } } - -extension MainViewController { - - func willShowAlert(_ alert: BBAlert) { - print("=============== ") - print(#function) - } - - func didTapAlertButton(_ alert: BBAlert?, index: Int?, button: BBButton) { - print("버튼이 클릭됨! \(index)") - } - -} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlert.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlert.swift index 97a2961c5..f6ddaad80 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlert.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlert.swift @@ -8,6 +8,10 @@ import DesignSystem import UIKit +// MARK: - Typelias + +public typealias BBAlertAction = ((BBAlert?) -> Void)? + public class BBAlert { // MARK: - Properties diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertButton.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertButton.swift index 1fd84519e..5f934c1ce 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertButton.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertButton.swift @@ -7,10 +7,6 @@ import UIKit -// MARK: - Typelias - -public typealias BBAlertAction = ((BBAlert?) -> Void)? - extension BBAlert { public enum Button { diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToast.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToast.swift index 6cb2c868b..024224605 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToast.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToast.swift @@ -5,8 +5,13 @@ // Created by 김건우 on 7/4/24. // +import DesignSystem import UIKit +// MARK: - Typealias + +public typealias BBToastAction = ((BBToast?) -> Void)? + public class BBToast { // MARK: - Properties @@ -102,6 +107,27 @@ public class BBToast { return BBToast(view: view, config: config) } + public static func style( + _ style: BBToastStyle, + config: BBToastConfiguration = BBToastConfiguration() + ) -> BBToast { + switch style { + case .error: + let viewConfig = BBToastViewConfiguration( + minWidth: 250 + ) + let view = DefaultToastView( + child: IconToastView( + image: DesignSystemAsset.warning.image, + title: "잠시 후에 다시 시도해주세요", + viewConfig: viewConfig + ), + viewConfig: viewConfig + ) + return BBToast(view: view, config: config) + } + } + public static func custom( view: BBToastView, config: BBToastConfiguration = BBToastConfiguration() @@ -226,13 +252,12 @@ extension BBToast { Self.multicast.add(delegate) } - public func addTapAction( - _ action: ((BBToast?) -> Void)? = nil + public func addButtonAction( + _ action: BBToastAction = nil ) { - if let view = view as? DefaultToastView { - if let subview = view.child as? ButtonToastView { - subview.tapAction = action - } + if let view = view as? DefaultToastView, + let subview = view.child as? ButtonToastView { + subview.buttonAction = action } } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/Animation.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastAnimation.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/Animation.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastAnimation.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/Background.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastBackground.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/Background.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastBackground.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastDelegate.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastDelegate.swift index 0c3b1c9bd..a0c7e1386 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastDelegate.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastDelegate.swift @@ -14,7 +14,7 @@ public protocol BBToastDelegate: AnyObject { func willCloseToast(_ toast: BBToast) func didCloseToast(_ toast: BBToast) - func didTapToastButton(_ toast: BBToast) + func didTapToastButton(_ toast: BBToast?, index: Int?, button: BBButton) } @@ -25,6 +25,6 @@ public extension BBToastDelegate { func willCloseToast(_ toast: BBToast) { } func didCloseToast(_ toast: BBToast) { } - func didTapToastButton(_ toast: BBToast) { } + func didTapToastButton(_ toast: BBToast, index: Int?, button: BBButton) { } } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/Direction.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastDirection.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/Direction.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastDirection.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/Dismissable.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastDismissable.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/Dismissable.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastDismissable.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastStyle.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastStyle.swift new file mode 100644 index 000000000..5265744e5 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastStyle.swift @@ -0,0 +1,20 @@ +// +// BBToastStyle.swift +// Core +// +// Created by 김건우 on 8/10/24. +// + +import Foundation + +// MARK: - Typealias + +public typealias BBToastStyle = BBToast.Style + +extension BBToast { + + public enum Style { + case error + } + +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/ButtonToastView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/ButtonToastView.swift index 78b67ca60..f572075a9 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/ButtonToastView.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/ButtonToastView.swift @@ -16,7 +16,7 @@ final public class ButtonToastView: UIStackView, BBToastStackView { // MARK: - Properties public var toast: BBToast? - public var tapAction: ((BBToast?) -> Void)? + public var buttonAction: BBToastAction private let viewConfig: BBToastViewConfiguration @@ -34,7 +34,7 @@ final public class ButtonToastView: UIStackView, BBToastStackView { viewConfig: BBToastViewConfiguration ) { self.toast = nil - self.tapAction = nil + self.buttonAction = nil self.viewConfig = viewConfig super.init(frame: .zero) @@ -49,10 +49,27 @@ final public class ButtonToastView: UIStackView, BBToastStackView { viewConfig: viewConfig ) + button.setId(0) button.setTitle(buttonTitle, for: .normal) button.setTitleFontStyle(buttonTitleFontStlye ?? .body1Regular) button.setTitleColor(buttonTint ?? .gray100, for: .normal) + let action = UIAction { [weak self] _ in + guard let self = self else { return } + // 델리게이트 실행 + BBToast.multicast.invoke { + $0.didTapToastButton( + self.toast, + index: self.button.id, + button: self.button + ) + } + // 액션 클로저 실행 + buttonAction?(self.toast) + } + + button.addAction(action, for: .touchUpInside) + addArrangedSubview(iconView) addArrangedSubview(button) } @@ -80,10 +97,4 @@ final public class ButtonToastView: UIStackView, BBToastStackView { ) } - // TODO: - 세세한 버튼 UI 수정하기 - - @objc public func didTapToastButton(_ button: UIButton) { - tapAction?(toast) - } - } From dcc5fa32509cd1a48a5a402cfe94063bb63a51ce Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Sat, 10 Aug 2024 12:18:28 +0900 Subject: [PATCH 178/263] =?UTF-8?q?docs:=20BBAlert,=20BBToast=EC=97=90=20?= =?UTF-8?q?=EB=AC=B8=EC=84=9C=20=EC=A3=BC=EC=84=9D=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?(#598)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Bibbi/BBCommons/BBAlert/BBAlert.swift | 27 +++++++++++++ .../Bibbi/BBCommons/BBToast/BBToast.swift | 38 +++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlert.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlert.swift index f6ddaad80..1b6aed376 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlert.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlert.swift @@ -30,6 +30,15 @@ public class BBAlert { // MARK: - Alert + /// 텍스트와 서브 텍스트가 포함된 Alert를 생성합니다. + /// - Parameters: + /// - title: 타이틀 텍스트 + /// - titleFontStyle: 타이틀의 폰트 스타일 + /// - subtitle: 서브 타이틀 텍스트 + /// - subtitleFontStyle: 서브 타이틀의 폰트 스타일 + /// - viewConfig: AlertView 설정값 + /// - config: Alert 설정값 + /// - Returns: BBAlert public static func text( title: String, titleFontStyle: BBFontStyle? = nil, @@ -52,6 +61,18 @@ public class BBAlert { return BBAlert(view: view, config: config) } + + /// 텍스트, 서브 텍스트와 이미지가 포함된 Alert를 생성합니다. + /// - Parameters: + /// - image: 이미지 + /// - imageTint: 이미지 강조 색상 + /// - title: 타이틀 텍스트 + /// - titleFontStyle: 타이틀의 폰트 스타일 + /// - subtitle: 서브 타이틀 텍스트 + /// - subtitleFontStyle: 서브 타이틀의 폰트 스타일 + /// - viewConfig: AlertView 설정값 + /// - config: Alert 설정값 + /// - Returns: BBAlert public static func image( image: UIImage? = nil, imageTint: UIColor? = nil, @@ -78,6 +99,12 @@ public class BBAlert { return BBAlert(view: view, config: config) } + /// 정해진 Style의 Alert를 생성합니다. + /// - Parameters: + /// - style: 스타일 + /// - primaryAction: 버튼 액션 클로저 + /// - config: Alert 설정값 + /// - Returns: BBAlert public static func style( _ style: BBAlertStyle, primaryAction action: BBAlertAction = nil, diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToast.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToast.swift index 024224605..3d8f097c6 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToast.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToast.swift @@ -36,6 +36,14 @@ public class BBToast { // MARK: - Toast + /// 텍스트가 포함된 Toast를 생성합니다. + /// - Parameters: + /// - title: 타이틀 텍스트 + /// - titleColor: 타이틀 색상 + /// - titleFontStyle: 타이틀의 폰트 스타일 + /// - viewConfig: ToastView 설정값 + /// - config: Toast 설정값 + /// - Returns: BBToast public static func text( _ title: String, titleColor: UIColor? = nil, @@ -55,6 +63,17 @@ public class BBToast { return BBToast(view: view, config: config) } + + /// 이미지와 텍스트가 포함된 Toast를 생성합니다, + /// - Parameters: + /// - image: 이미지 + /// - imageTint: 이미지 강조 색상 + /// - title: 타이틀 텍스트 + /// - titleColor: 타이틀 색상 + /// - titleFontStyle: 타이틀의 폰트 스타일 + /// - viewConfig: ToastView 설정값 + /// - config: Toast 설정값 + /// - Returns: BBToast public static func `default`( image: UIImage, imageTint: UIColor = defaultImageTint, @@ -78,6 +97,19 @@ public class BBToast { return BBToast(view: view, config: config) } + /// 이미지, 텍스트와 버튼이 포함된 Toast를 생성합니다. + /// - Parameters: + /// - image: 이미지 + /// - imageTint: 이미지 강조 색상 + /// - title: 타이틀 텍스트 + /// - titleColor: 타이틀 색상 + /// - titleFontStyle: 타이틀의 폰트 스타일 + /// - buttonTitle: 버튼 타이틀 텍스트 + /// - buttonTitleFontStyle: 버튼 타이틀의 폰트 스타일 + /// - buttonTint: 버튼 강조 색상 + /// - viewConfig: ToastView 설정값 + /// - config: Toast 설정값 + /// - Returns: BBToast public static func button( image: UIImage, imageTint: UIColor = defaultImageTint, @@ -107,6 +139,12 @@ public class BBToast { return BBToast(view: view, config: config) } + + /// 정해진 Style의 Toast를 생성합니다. + /// - Parameters: + /// - style: 스타일 + /// - config: Toast 설정값 + /// - Returns: BBToast public static func style( _ style: BBToastStyle, config: BBToastConfiguration = BBToastConfiguration() From 2d5578ec3541db5c1479e2f0d40a578a308fcf61 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Sat, 10 Aug 2024 12:54:51 +0900 Subject: [PATCH 179/263] =?UTF-8?q?feat:=20MyRepository,=20MyUseCase=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#600)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../App/Sources/Application/AppDelegate.swift | 3 +- .../DIContainer/MyDIContainer.swift | 89 +++++++++++++++++++ .../APIs/My/Repository/MyRepository.swift | 46 ++++++++++ ...rotocol.swift => MainViewRepository.swift} | 0 .../Sources/Repositories/MyRepository.swift | 15 ++++ ...rotocol.swift => PostListRepository.swift} | 0 ...rotocol.swift => ReactionRepository.swift} | 0 ...otocol.swift => RealEmojiRepository.swift} | 0 ...yProtocol.swift => WidgetRepository.swift} | 0 .../UseCases/My/CheckIsMeUseCase.swift | 29 ++++++ .../My/CheckIsVaildMemberUseCase.swift | 29 ++++++ .../UseCases/My/FetchMyMemberIdUseCase.swift | 29 ++++++ .../UseCases/My/FetchMyUserNameUseCase.swift | 29 ++++++ .../My/FetchProfileImageUrlUseCase.swift | 30 +++++++ .../UseCases/My/FetchUserNameUseCase.swift | 29 ++++++ 15 files changed, 327 insertions(+), 1 deletion(-) create mode 100644 14th-team5-iOS/App/Sources/Application/DIContainer/MyDIContainer.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/My/Repository/MyRepository.swift rename 14th-team5-iOS/Domain/Sources/Repositories/{MainViewRepositoryProtocol.swift => MainViewRepository.swift} (100%) create mode 100644 14th-team5-iOS/Domain/Sources/Repositories/MyRepository.swift rename 14th-team5-iOS/Domain/Sources/Repositories/{PostListRepositoryProtocol.swift => PostListRepository.swift} (100%) rename 14th-team5-iOS/Domain/Sources/Repositories/{ReactionRepositoryProtocol.swift => ReactionRepository.swift} (100%) rename 14th-team5-iOS/Domain/Sources/Repositories/{RealEmojiRepositoryProtocol.swift => RealEmojiRepository.swift} (100%) rename 14th-team5-iOS/Domain/Sources/Repositories/{WidgetRepositoryProtocol.swift => WidgetRepository.swift} (100%) create mode 100644 14th-team5-iOS/Domain/Sources/UseCases/My/CheckIsMeUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/UseCases/My/CheckIsVaildMemberUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/UseCases/My/FetchMyMemberIdUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/UseCases/My/FetchMyUserNameUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/UseCases/My/FetchProfileImageUrlUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/UseCases/My/FetchUserNameUseCase.swift diff --git a/14th-team5-iOS/App/Sources/Application/AppDelegate.swift b/14th-team5-iOS/App/Sources/Application/AppDelegate.swift index 61cb371cd..c145c71d3 100644 --- a/14th-team5-iOS/App/Sources/Application/AppDelegate.swift +++ b/14th-team5-iOS/App/Sources/Application/AppDelegate.swift @@ -68,7 +68,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { RealEmojiDIContainer(), PickDIContainer(), MissionDIContainer(), - MemberDIContainer() + MemberDIContainer(), + MyDIContainer() ] containers.forEach { $0.registerDependencies() diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/MyDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/MyDIContainer.swift new file mode 100644 index 000000000..35ba2c326 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/MyDIContainer.swift @@ -0,0 +1,89 @@ +// +// MyDIContainer.swift +// App +// +// Created by 김건우 on 8/10/24. +// + +import Core +import Data +import Domain + +final class MyDIContainer: BaseContainer { + + // MARK: - Make UseCcase + + private func makeFetchMyMemberIdUseCase() -> FetchMyMemberIdUseCaseProtocol { + FetchMyMemberIdUseCase( + myRepository: makeMyRepository() + ) + } + + private func makeFetchMyUserNameUseCase() -> FetchMyUserNameUseCaseProtocol { + FetchMyUserNameUseCase( + myRepository: makeMyRepository() + ) + } + + private func makeFetchUserNameUseCase() -> FetchUserNameUseCaseProtocol { + FetchUserNameUseCase( + myRepository: makeMyRepository() + ) + } + + private func makeFetchProfileImageUrlUseCase() -> FetchProfileImageUrlUseCaseProtocol { + FetchProfileImageUrlUseCase( + myRepository: makeMyRepository() + ) + } + + private func makeCheckIsMeUseCase() -> CheckIsMeUseCaseProtocol { + CheckIsMeUseCase( + myRepository: makeMyRepository() + ) + } + + private func makeCheckIsVaildMemberUseCase() -> CheckIsVaildMemberUseCaseProtocol { + CheckIsVaildMemberUseCase( + myRepository: makeMyRepository() + ) + } + + + // MARK: - Make Repository + + private func makeMyRepository() -> MyRepositoryProtocol { + return MyRepository() + } + + // MARK: - Register + + func registerDependencies() { + + container.register(type: FetchMyMemberIdUseCaseProtocol.self) { _ in + self.makeFetchMyMemberIdUseCase() + } + + container.register(type: FetchMyUserNameUseCaseProtocol.self) { _ in + self.makeFetchMyUserNameUseCase() + } + + container.register(type: FetchUserNameUseCaseProtocol.self) { _ in + self.makeFetchUserNameUseCase() + } + + container.register(type: FetchProfileImageUrlUseCaseProtocol.self) { _ in + self.makeFetchProfileImageUrlUseCase() + } + + container.register(type: CheckIsMeUseCaseProtocol.self) { _ in + self.makeCheckIsMeUseCase() + } + + container.register(type: CheckIsVaildMemberUseCaseProtocol.self) { _ in + self.makeCheckIsVaildMemberUseCase() + } + + } + +} diff --git a/14th-team5-iOS/Data/Sources/APIs/My/Repository/MyRepository.swift b/14th-team5-iOS/Data/Sources/APIs/My/Repository/MyRepository.swift new file mode 100644 index 000000000..c8c79e88f --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/My/Repository/MyRepository.swift @@ -0,0 +1,46 @@ +// +// MyRepository.swift +// Data +// +// Created by 김건우 on 8/10/24. +// + +import Domain +import Foundation + +public final class MyRepository: MyRepositoryProtocol { + + // MARK: - Properties + + // private let familyUserDefaults = FamilyUserDefaults() + + // MARK: - Intializer + + public init() { } + +} + +extension MyRepository { + + public func fetchMyMemberId() -> String { + // TODO: - 리팩토링된 FamilyUserDefaults로 바꾸기 + FamilyUserDefaults.returnMyMemberId() + } + + public func fetchMyUserName() -> String { + // TODO: - 리팩토링된 FamilyUserDefaults로 바꾸기 + let myMemberId = FamilyUserDefaults.returnMyMemberId() + return FamilyUserDefaults.load(memberId: myMemberId)?.name ?? "" + } + + public func fetchUserName(memberId: String) -> String { + // TODO: - 리팩토링된 FamilyUserDefaults로 바꾸기 + FamilyUserDefaults.load(memberId: memberId)?.name ?? "" + } + + public func fetchProfileImageUrl(memberId: String) -> String { + // TODO: - 리팩토링된 FamilyUserDefaults로 바꾸기 + FamilyUserDefaults.load(memberId: memberId)?.profileImageURL ?? "" + } + +} diff --git a/14th-team5-iOS/Domain/Sources/Repositories/MainViewRepositoryProtocol.swift b/14th-team5-iOS/Domain/Sources/Repositories/MainViewRepository.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Repositories/MainViewRepositoryProtocol.swift rename to 14th-team5-iOS/Domain/Sources/Repositories/MainViewRepository.swift diff --git a/14th-team5-iOS/Domain/Sources/Repositories/MyRepository.swift b/14th-team5-iOS/Domain/Sources/Repositories/MyRepository.swift new file mode 100644 index 000000000..4b3f97ee3 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Repositories/MyRepository.swift @@ -0,0 +1,15 @@ +// +// MyRepository.swift +// Domain +// +// Created by 김건우 on 8/10/24. +// + +import Foundation + +public protocol MyRepositoryProtocol { + func fetchMyMemberId() -> String + func fetchMyUserName() -> String + func fetchUserName(memberId: String) -> String + func fetchProfileImageUrl(memberId: String) -> String +} diff --git a/14th-team5-iOS/Domain/Sources/Repositories/PostListRepositoryProtocol.swift b/14th-team5-iOS/Domain/Sources/Repositories/PostListRepository.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Repositories/PostListRepositoryProtocol.swift rename to 14th-team5-iOS/Domain/Sources/Repositories/PostListRepository.swift diff --git a/14th-team5-iOS/Domain/Sources/Repositories/ReactionRepositoryProtocol.swift b/14th-team5-iOS/Domain/Sources/Repositories/ReactionRepository.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Repositories/ReactionRepositoryProtocol.swift rename to 14th-team5-iOS/Domain/Sources/Repositories/ReactionRepository.swift diff --git a/14th-team5-iOS/Domain/Sources/Repositories/RealEmojiRepositoryProtocol.swift b/14th-team5-iOS/Domain/Sources/Repositories/RealEmojiRepository.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Repositories/RealEmojiRepositoryProtocol.swift rename to 14th-team5-iOS/Domain/Sources/Repositories/RealEmojiRepository.swift diff --git a/14th-team5-iOS/Domain/Sources/Repositories/WidgetRepositoryProtocol.swift b/14th-team5-iOS/Domain/Sources/Repositories/WidgetRepository.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Repositories/WidgetRepositoryProtocol.swift rename to 14th-team5-iOS/Domain/Sources/Repositories/WidgetRepository.swift diff --git a/14th-team5-iOS/Domain/Sources/UseCases/My/CheckIsMeUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/My/CheckIsMeUseCase.swift new file mode 100644 index 000000000..c9da2161e --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/UseCases/My/CheckIsMeUseCase.swift @@ -0,0 +1,29 @@ +// +// CheckIsMeUseCase.swift +// Domain +// +// Created by 김건우 on 8/10/24. +// + +import Foundation + +public protocol CheckIsMeUseCaseProtocol { + func execute(memberId: String) -> Bool +} + +public class CheckIsMeUseCase: CheckIsMeUseCaseProtocol { + + // MARK: - Repositories + let myRepository: MyRepositoryProtocol + + // MARK: - Intializer + public init(myRepository: MyRepositoryProtocol) { + self.myRepository = myRepository + } + + // MARK: - Execute + public func execute(memberId: String) -> Bool { + memberId == myRepository.fetchMyMemberId() + } + +} diff --git a/14th-team5-iOS/Domain/Sources/UseCases/My/CheckIsVaildMemberUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/My/CheckIsVaildMemberUseCase.swift new file mode 100644 index 000000000..7e025e1e1 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/UseCases/My/CheckIsVaildMemberUseCase.swift @@ -0,0 +1,29 @@ +// +// CheckIsVaildMemberUseCase.swift +// Domain +// +// Created by 김건우 on 8/10/24. +// + +import Foundation + +public protocol CheckIsVaildMemberUseCaseProtocol { + func execute(memberId: String) -> Bool +} + +public class CheckIsVaildMemberUseCase: CheckIsVaildMemberUseCaseProtocol { + + // MARK: - Repositories + let myRepository: MyRepositoryProtocol + + // MARK: - Intializer + public init(myRepository: MyRepositoryProtocol) { + self.myRepository = myRepository + } + + // MARK: - Execute + public func execute(memberId: String) -> Bool { + myRepository.fetchUserName(memberId: memberId) != "" + } + +} diff --git a/14th-team5-iOS/Domain/Sources/UseCases/My/FetchMyMemberIdUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/My/FetchMyMemberIdUseCase.swift new file mode 100644 index 000000000..1cad4ab38 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/UseCases/My/FetchMyMemberIdUseCase.swift @@ -0,0 +1,29 @@ +// +// FetchMyMemberIdUseCase.swift +// Domain +// +// Created by 김건우 on 8/10/24. +// + +import Foundation + +public protocol FetchMyMemberIdUseCaseProtocol { + func execute() -> String +} + +public class FetchMyMemberIdUseCase: FetchMyMemberIdUseCaseProtocol { + + // MARK: - Properties + let myRepository: MyRepositoryProtocol + + // MARK: - Intializer + public init(myRepository: MyRepositoryProtocol) { + self.myRepository = myRepository + } + + // MARK: - Execute + public func execute() -> String { + myRepository.fetchMyMemberId() + } + +} diff --git a/14th-team5-iOS/Domain/Sources/UseCases/My/FetchMyUserNameUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/My/FetchMyUserNameUseCase.swift new file mode 100644 index 000000000..beb228eea --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/UseCases/My/FetchMyUserNameUseCase.swift @@ -0,0 +1,29 @@ +// +// FetchMyUserNameUseCase.swift +// Domain +// +// Created by 김건우 on 8/10/24. +// + +import Foundation + +public protocol FetchMyUserNameUseCaseProtocol { + func execute() -> String +} + +public class FetchMyUserNameUseCase: FetchMyUserNameUseCaseProtocol { + + // MARK: - Repositories + let myRepository: MyRepositoryProtocol + + // MARK: - Intialzier + public init(myRepository: MyRepositoryProtocol) { + self.myRepository = myRepository + } + + // MARK: - Execute + public func execute() -> String { + myRepository.fetchMyUserName() + } + +} diff --git a/14th-team5-iOS/Domain/Sources/UseCases/My/FetchProfileImageUrlUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/My/FetchProfileImageUrlUseCase.swift new file mode 100644 index 000000000..5d132ffec --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/UseCases/My/FetchProfileImageUrlUseCase.swift @@ -0,0 +1,30 @@ +// +// FetchProfileImageUrlUseCase.swift +// Domain +// +// Created by 김건우 on 8/10/24. +// + +import Foundation + +public protocol FetchProfileImageUrlUseCaseProtocol { + func execute(memberId: String) -> String +} + +public class FetchProfileImageUrlUseCase: FetchProfileImageUrlUseCaseProtocol { + + // MARK: - Repositories + let myRepository: MyRepositoryProtocol + + // MARK: - Intializer + public init(myRepository: MyRepositoryProtocol) { + self.myRepository = myRepository + } + + // MARK: - Execute + public func execute(memberId: String) -> String { + myRepository.fetchProfileImageUrl(memberId: memberId) + } + +} + diff --git a/14th-team5-iOS/Domain/Sources/UseCases/My/FetchUserNameUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/My/FetchUserNameUseCase.swift new file mode 100644 index 000000000..273381b0a --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/UseCases/My/FetchUserNameUseCase.swift @@ -0,0 +1,29 @@ +// +// FetchUserNameProtocol.swift +// Domain +// +// Created by 김건우 on 8/10/24. +// + +import Foundation + +public protocol FetchUserNameUseCaseProtocol { + func execute(memberId: String) -> String +} + +public class FetchUserNameUseCase: FetchUserNameUseCaseProtocol { + + // MARK: - Repositories + let myRepository: MyRepositoryProtocol + + // MARK: - Intializer + public init(myRepository: MyRepositoryProtocol) { + self.myRepository = myRepository + } + + // MARK: - Execute + public func execute(memberId: String) -> String { + myRepository.fetchUserName(memberId: memberId) + } + +} From 064034841e64e08fa07c763665b0522d1a2b4892 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Sun, 11 Aug 2024 11:26:09 +0900 Subject: [PATCH 180/263] =?UTF-8?q?feat:=20=EA=B0=80=EC=A1=B1=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=EB=B3=80=EA=B2=BDAPI=ED=86=B5=EC=8B=A0,=20UpdateFamil?= =?UTF-8?q?yNameUseCase=20=EA=B5=AC=ED=98=84=20(#603)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DIContainer/FamilyDIContainer.swift | 8 ++++ .../DataMapping/FamilyNameResponseDTO.swift | 33 +++++++++++++++ .../UpdateFamilyNameRequestDTO.swift | 12 ++++++ .../Family/FamilyAPI/FamilyAPIWorker.swift | 19 +++++++++ .../APIs/Family/FamilyAPI/FamilyAPIs.swift | 3 ++ .../Family/Repository/FamilyRepository.swift | 40 +++++++++++++++---- .../Entities/Family/FamilyNameEntity.swift | 27 +++++++++++++ .../Family/UpdateFamilyNameRequest.swift | 16 ++++++++ .../Repositories/FamilyRepository.swift | 1 + .../Family/UpdateFamilyNameUseCase.swift | 31 ++++++++++++++ 10 files changed, 182 insertions(+), 8 deletions(-) create mode 100644 14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/FamilyNameResponseDTO.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/UpdateFamilyNameRequestDTO.swift create mode 100644 14th-team5-iOS/Domain/Sources/Entities/Family/FamilyNameEntity.swift create mode 100644 14th-team5-iOS/Domain/Sources/Entities/Family/UpdateFamilyNameRequest.swift create mode 100644 14th-team5-iOS/Domain/Sources/UseCases/Family/UpdateFamilyNameUseCase.swift diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/FamilyDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/FamilyDIContainer.swift index 962b8204d..37b10b95f 100644 --- a/14th-team5-iOS/App/Sources/Application/DIContainer/FamilyDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/FamilyDIContainer.swift @@ -45,6 +45,10 @@ final class FamilyDIContainer: BaseContainer { ResignFamilyUseCase(familyRepository: repository) } + private func makeUpdateFamilyNameUseCase() -> UpdateFamilyNameUseCaseProtocol { + UpdateFamilyNameUseCase(familyRepository: repository) + } + // Deprecated private func makeFamilyUseCase() -> FamilyUseCaseProtocol { FamilyUseCase(familyRepository: repository) @@ -81,6 +85,10 @@ final class FamilyDIContainer: BaseContainer { makeResignFamilyUseCase() } + container.register(type: UpdateFamilyNameUseCaseProtocol.self) { _ in + makeUpdateFamilyNameUseCase() + } + // Deprecated container.register(type: FamilyUseCaseProtocol.self) { _ in makeFamilyUseCase() diff --git a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/FamilyNameResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/FamilyNameResponseDTO.swift new file mode 100644 index 000000000..e34520cc7 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/FamilyNameResponseDTO.swift @@ -0,0 +1,33 @@ +// +// FamilyNameResponseDTO.swift +// Data +// +// Created by 김건우 on 8/11/24. +// + +import Domain +import Foundation + +public struct FamilyNameResponseDTO: Decodable { + private enum CodingKeys: String, CodingKey { + case familyId + case familyName + case familyIdEditorId + case createdAt + } + var familyId: String + var familyName: String + var familyIdEditorId: String + var createdAt: String +} + +extension FamilyNameResponseDTO { + func toDomain() -> FamilyNameEntity { + return .init( + familyId: self.familyId, + familyName: self.familyName, + familyIdEditorId: self.familyIdEditorId, + createdAt: self.createdAt.iso8601ToDate() + ) + } +} diff --git a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/UpdateFamilyNameRequestDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/UpdateFamilyNameRequestDTO.swift new file mode 100644 index 000000000..b6eff45fe --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/UpdateFamilyNameRequestDTO.swift @@ -0,0 +1,12 @@ +// +// UpdateFamilyNameRequestDTO.swift +// Data +// +// Created by 김건우 on 8/11/24. +// + +import Foundation + +public struct UpdateFamilyNameRequestDTO: Encodable { + let familyName: String +} diff --git a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIWorker.swift index 1ebf1bce4..37e6adb21 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIWorker.swift @@ -111,4 +111,23 @@ extension FamilyAPIWorker { .asSingle() } + + // MARK: - Change Family Name + + public func updateFamilyName( + familyId: String, + body: UpdateFamilyNameRequestDTO + ) -> Single { + let spec = FamilyAPIs.updateFamilyName(familyId).spec + + return request(spec: spec, jsonEncodable: body) + .subscribe(on: Self.queue) + .map(FamilyNameResponseDTO.self) + .catchAndReturn(nil) + .asSingle() + } + } + + + diff --git a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIs.swift index 118436624..9cfb026ee 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIs.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIs.swift @@ -15,6 +15,7 @@ enum FamilyAPIs: API { case fetchInvitationLink(String) case fetchFamilyCreatedAt(String) case fetchPaginationFamilyMembers(Int, Int) + case updateFamilyName(String) var spec: APISpec { switch self { @@ -30,6 +31,8 @@ enum FamilyAPIs: API { return APISpec(method: .get, url: "\(BibbiAPI.hostApi)/families/\(familyId)/created-at") case let .fetchPaginationFamilyMembers(page, size): return APISpec(method: .get, url: "\(BibbiAPI.hostApi)/members?type=FAMILY&page=\(page)&size=\(size)") + case let .updateFamilyName(familyId): + return APISpec(method: .put, url: "\(BibbiAPI.hostApi)/families/\(familyId)/name") } } } diff --git a/14th-team5-iOS/Data/Sources/APIs/Family/Repository/FamilyRepository.swift b/14th-team5-iOS/Data/Sources/APIs/Family/Repository/FamilyRepository.swift index 430d25c4a..559cdee59 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Family/Repository/FamilyRepository.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Family/Repository/FamilyRepository.swift @@ -12,13 +12,18 @@ import Foundation import RxSwift public final class FamilyRepository: FamilyRepositoryProtocol { + + // MARK: - Properties + public let disposeBag: DisposeBag = DisposeBag() private let familyApiWorker: FamilyAPIWorker = FamilyAPIWorker() - // TODO: - UserDefaults로 바꾸기 + // TODO: - 리팩토링된 FamilyUserDefaults로 바꾸기 private var familyId: String = App.Repository.member.familyId.value ?? "" + // MARK: - Intializer + public init() { } } @@ -33,45 +38,54 @@ extension FamilyRepository { .map { $0?.toDomain() } .do(onSuccess: { [weak self] response in guard let self else { return } - App.Repository.member.familyId.accept(response?.familyId) // TODO: - UserDefaults로 바꾸기 + App.Repository.member.familyId.accept(response?.familyId) // TODO: - 리팩토링된 FamilyUserDefaults로 바꾸기 App.Repository.member.familyCreatedAt.accept(response?.createdAt) fetchPaginationFamilyMembers(query: .init()) // TODO: - 로직 분리하기 }) .asObservable() } + // MARK: - Resign Family + public func resignFamily() -> Observable { return familyApiWorker.resignFamily() .map { $0?.toDomain() } .asObservable() } + // MARK: - Create Family + public func createFamily() -> Observable { return familyApiWorker.createFamily() .map { $0?.toDomain() } .do(onSuccess: { - App.Repository.member.familyId.accept($0?.familyId) // TODO: - UserDefaults로 바꾸기 - App.Repository.member.familyCreatedAt.accept($0?.createdAt) + App.Repository.member.familyId.accept($0?.familyId) // TODO: - 리팩토링된 FamilyUserDefaults로 바꾸기 + App.Repository.member.familyCreatedAt.accept($0?.createdAt) // TODO: - 리팩토링된 FamilyUserDefaults로 바꾸기 }) .asObservable() } + // MARK: - Fetch Family CreatedAt + public func fetchFamilyCreatedAt() -> Observable { return familyApiWorker.fetchFamilyCreatedAt(familyId: familyId) .map { $0?.toDomain() } .do(onSuccess: { - App.Repository.member.familyCreatedAt.accept($0?.createdAt) // TODO: - UserDefaults로 바꾸기 + App.Repository.member.familyCreatedAt.accept($0?.createdAt) // TODO: - 리팩토링된 FamilyUserDefaults로 바꾸기 }) .asObservable() } + // MARK: - Fetch Invitation Url + public func fetchInvitationLink() -> Observable { return familyApiWorker.fetchInvitationLink(familyId: familyId) .map { $0?.toDomain() } .asObservable() } - // TODO: - 반환 타입 확인하기 + // MARK: - Fetch Family Members + public func fetchPaginationFamilyMembers(query: FamilyPaginationQuery) -> Observable { return familyApiWorker.fetchPaginationFamilyMember(familyId: familyId, query: query) .map { $0?.toDomain() } @@ -81,8 +95,18 @@ extension FamilyRepository { .asObservable() } - public func fetchPaginationFamilyMembers(memberIds: [String]) -> [FamilyMemberProfileEntity] { // TODO: - 반환 타입 바꾸기 - // TODO: - 리팩토링된 UserDefaults로 바꾸기 + public func fetchPaginationFamilyMembers(memberIds: [String]) -> [FamilyMemberProfileEntity] { + // TODO: - 리팩토링된 FamilyUserDefaults로 바꾸기 return FamilyUserDefaults.loadMembersFromUserDefaults(memberIds: memberIds) } + + // MARK: - Update Family Name + + public func updateFamilyName(body: UpdateFamilyNameRequest) -> Observable { + let body = UpdateFamilyNameRequestDTO(familyName: body.familyName) + + return familyApiWorker.updateFamilyName(familyId: familyId, body: body) + .map { $0?.toDomain() } + .asObservable() + } } diff --git a/14th-team5-iOS/Domain/Sources/Entities/Family/FamilyNameEntity.swift b/14th-team5-iOS/Domain/Sources/Entities/Family/FamilyNameEntity.swift new file mode 100644 index 000000000..ef281d57d --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Entities/Family/FamilyNameEntity.swift @@ -0,0 +1,27 @@ +// +// FamilyNameEntity.swift +// Domain +// +// Created by 김건우 on 8/11/24. +// + +import Foundation + +public struct FamilyNameEntity { + public var familyId: String + public var familyName: String + public var familyIdEditorId: String + public var createdAt: Date + + public init( + familyId: String, + familyName: String, + familyIdEditorId: String, + createdAt: Date + ) { + self.familyId = familyId + self.familyName = familyName + self.familyIdEditorId = familyIdEditorId + self.createdAt = createdAt + } +} diff --git a/14th-team5-iOS/Domain/Sources/Entities/Family/UpdateFamilyNameRequest.swift b/14th-team5-iOS/Domain/Sources/Entities/Family/UpdateFamilyNameRequest.swift new file mode 100644 index 000000000..6213ac0f6 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Entities/Family/UpdateFamilyNameRequest.swift @@ -0,0 +1,16 @@ +// +// UpdateFamilyNameRequest.swift +// Domain +// +// Created by 김건우 on 8/11/24. +// + +import Foundation + +public struct UpdateFamilyNameRequest { + public let familyName: String + + public init(familyName: String) { + self.familyName = familyName + } +} diff --git a/14th-team5-iOS/Domain/Sources/Repositories/FamilyRepository.swift b/14th-team5-iOS/Domain/Sources/Repositories/FamilyRepository.swift index 014b392ac..e12a231eb 100644 --- a/14th-team5-iOS/Domain/Sources/Repositories/FamilyRepository.swift +++ b/14th-team5-iOS/Domain/Sources/Repositories/FamilyRepository.swift @@ -17,4 +17,5 @@ public protocol FamilyRepositoryProtocol { func fetchInvitationLink() -> Observable func fetchPaginationFamilyMembers(query: FamilyPaginationQuery) -> Observable func fetchPaginationFamilyMembers(memberIds: [String]) -> [FamilyMemberProfileEntity] + func updateFamilyName(body: UpdateFamilyNameRequest) -> Observable } diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Family/UpdateFamilyNameUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Family/UpdateFamilyNameUseCase.swift new file mode 100644 index 000000000..7f1763af6 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/UseCases/Family/UpdateFamilyNameUseCase.swift @@ -0,0 +1,31 @@ +// +// UpdateFamilyNameUseCase.swift +// Domain +// +// Created by 김건우 on 8/11/24. +// + +import Foundation + +import RxSwift + +public protocol UpdateFamilyNameUseCaseProtocol { + func execute(body: UpdateFamilyNameRequest) -> Observable +} + +public class UpdateFamilyNameUseCase: UpdateFamilyNameUseCaseProtocol { + + // MARK: - Repositories + let familyRepository: FamilyRepositoryProtocol + + // MARK: - Intializer + public init(familyRepository: FamilyRepositoryProtocol) { + self.familyRepository = familyRepository + } + + // MARK: - Execute + public func execute(body: UpdateFamilyNameRequest) -> Observable { + familyRepository.updateFamilyName(body: body) + } + +} From 4fccb0db7caa1369c4afeee8647fe5ef929aeda0 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Sun, 11 Aug 2024 11:38:35 +0900 Subject: [PATCH 181/263] =?UTF-8?q?feat:=20FetchFamilyIdUseCase=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#603)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DIContainer/FamilyDIContainer.swift | 8 +++++ .../Family/Repository/FamilyRepository.swift | 8 +++++ .../Repositories/FamilyRepository.swift | 1 + .../Family/FetchFamilyIdUseCase.swift | 29 +++++++++++++++++++ 4 files changed, 46 insertions(+) create mode 100644 14th-team5-iOS/Domain/Sources/UseCases/Family/FetchFamilyIdUseCase.swift diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/FamilyDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/FamilyDIContainer.swift index 37b10b95f..7c6a48c2d 100644 --- a/14th-team5-iOS/App/Sources/Application/DIContainer/FamilyDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/FamilyDIContainer.swift @@ -21,6 +21,10 @@ final class FamilyDIContainer: BaseContainer { CreateFamilyUseCase(familyRepository: repository) } + private func makeFetchFamilyIdUseCase() -> FetchFamilyIdUseCaseProtocol { + FetchFamilyIdUseCase(familyRepository: repository) + } + private func makeFetchFamilyCreatedAtUseCase() -> FetchFamilyCreatedAtUseCaseProtocol { FetchFamilyCreatedAtUseCase(familyRepository: repository) } @@ -61,6 +65,10 @@ final class FamilyDIContainer: BaseContainer { makeCreateFamilyUseCase() } + container.register(type: FetchFamilyIdUseCaseProtocol.self) { _ in + makeFetchFamilyIdUseCase() + } + container.register(type: FetchFamilyCreatedAtUseCaseProtocol.self) { _ in makeFetchFamilyCreatedAtUseCase() } diff --git a/14th-team5-iOS/Data/Sources/APIs/Family/Repository/FamilyRepository.swift b/14th-team5-iOS/Data/Sources/APIs/Family/Repository/FamilyRepository.swift index 559cdee59..abeaf5011 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Family/Repository/FamilyRepository.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Family/Repository/FamilyRepository.swift @@ -65,6 +65,14 @@ extension FamilyRepository { .asObservable() } + // MARK: - Fetch Family ID + + public func fetchFamilyId() -> String? { + // TODO: - 리팩토링된 FamilyUserDefaults로 바꾸기 + App.Repository.member.familyId.value + } + + // MARK: - Fetch Family CreatedAt public func fetchFamilyCreatedAt() -> Observable { diff --git a/14th-team5-iOS/Domain/Sources/Repositories/FamilyRepository.swift b/14th-team5-iOS/Domain/Sources/Repositories/FamilyRepository.swift index e12a231eb..c1e4367fd 100644 --- a/14th-team5-iOS/Domain/Sources/Repositories/FamilyRepository.swift +++ b/14th-team5-iOS/Domain/Sources/Repositories/FamilyRepository.swift @@ -13,6 +13,7 @@ public protocol FamilyRepositoryProtocol { func joinFamily(body: JoinFamilyRequest) -> Observable func resignFamily() -> Observable func createFamily() -> Observable + func fetchFamilyId() -> String? func fetchFamilyCreatedAt() -> Observable func fetchInvitationLink() -> Observable func fetchPaginationFamilyMembers(query: FamilyPaginationQuery) -> Observable diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Family/FetchFamilyIdUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Family/FetchFamilyIdUseCase.swift new file mode 100644 index 000000000..4ad893869 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/UseCases/Family/FetchFamilyIdUseCase.swift @@ -0,0 +1,29 @@ +// +// FetchFamilyIdUseCase.swift +// Domain +// +// Created by 김건우 on 8/11/24. +// + +import Foundation + +public protocol FetchFamilyIdUseCaseProtocol { + func execute() -> String? +} + +public class FetchFamilyIdUseCase: FetchFamilyIdUseCaseProtocol { + + // MARK: - Repositories + var familyRepository: FamilyRepositoryProtocol + + // MARK: - Intializer + public init(familyRepository: FamilyRepositoryProtocol) { + self.familyRepository = familyRepository + } + + // MARK: - Execute + public func execute() -> String? { + familyRepository.fetchFamilyId() + } + +} From c14805dab3694906e18708e2767001286db4c23c Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Sun, 11 Aug 2024 11:45:31 +0900 Subject: [PATCH 182/263] =?UTF-8?q?feat:=20=EB=A9=A4=EB=B2=84ID,=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=EC=9D=84=20=EC=98=B5=EC=85=94=EB=84=90?= =?UTF-8?q?=EB=A1=9C=20=EB=B0=98=ED=99=98=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95=20(#600)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/APIs/My/Repository/MyRepository.swift | 14 +++++++------- .../Domain/Sources/Repositories/MyRepository.swift | 8 ++++---- .../UseCases/My/FetchMyMemberIdUseCase.swift | 4 ++-- .../UseCases/My/FetchMyUserNameUseCase.swift | 4 ++-- .../UseCases/My/FetchProfileImageUrlUseCase.swift | 4 ++-- .../Sources/UseCases/My/FetchUserNameUseCase.swift | 4 ++-- 6 files changed, 19 insertions(+), 19 deletions(-) diff --git a/14th-team5-iOS/Data/Sources/APIs/My/Repository/MyRepository.swift b/14th-team5-iOS/Data/Sources/APIs/My/Repository/MyRepository.swift index c8c79e88f..50ee5055f 100644 --- a/14th-team5-iOS/Data/Sources/APIs/My/Repository/MyRepository.swift +++ b/14th-team5-iOS/Data/Sources/APIs/My/Repository/MyRepository.swift @@ -22,25 +22,25 @@ public final class MyRepository: MyRepositoryProtocol { extension MyRepository { - public func fetchMyMemberId() -> String { + public func fetchMyMemberId() -> String? { // TODO: - 리팩토링된 FamilyUserDefaults로 바꾸기 FamilyUserDefaults.returnMyMemberId() } - public func fetchMyUserName() -> String { + public func fetchMyUserName() -> String? { // TODO: - 리팩토링된 FamilyUserDefaults로 바꾸기 let myMemberId = FamilyUserDefaults.returnMyMemberId() - return FamilyUserDefaults.load(memberId: myMemberId)?.name ?? "" + return FamilyUserDefaults.load(memberId: myMemberId)?.name } - public func fetchUserName(memberId: String) -> String { + public func fetchUserName(memberId: String) -> String? { // TODO: - 리팩토링된 FamilyUserDefaults로 바꾸기 - FamilyUserDefaults.load(memberId: memberId)?.name ?? "" + FamilyUserDefaults.load(memberId: memberId)?.name } - public func fetchProfileImageUrl(memberId: String) -> String { + public func fetchProfileImageUrl(memberId: String) -> String? { // TODO: - 리팩토링된 FamilyUserDefaults로 바꾸기 - FamilyUserDefaults.load(memberId: memberId)?.profileImageURL ?? "" + FamilyUserDefaults.load(memberId: memberId)?.profileImageURL } } diff --git a/14th-team5-iOS/Domain/Sources/Repositories/MyRepository.swift b/14th-team5-iOS/Domain/Sources/Repositories/MyRepository.swift index 4b3f97ee3..670424add 100644 --- a/14th-team5-iOS/Domain/Sources/Repositories/MyRepository.swift +++ b/14th-team5-iOS/Domain/Sources/Repositories/MyRepository.swift @@ -8,8 +8,8 @@ import Foundation public protocol MyRepositoryProtocol { - func fetchMyMemberId() -> String - func fetchMyUserName() -> String - func fetchUserName(memberId: String) -> String - func fetchProfileImageUrl(memberId: String) -> String + func fetchMyMemberId() -> String? + func fetchMyUserName() -> String? + func fetchUserName(memberId: String) -> String? + func fetchProfileImageUrl(memberId: String) -> String? } diff --git a/14th-team5-iOS/Domain/Sources/UseCases/My/FetchMyMemberIdUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/My/FetchMyMemberIdUseCase.swift index 1cad4ab38..4dfecc97c 100644 --- a/14th-team5-iOS/Domain/Sources/UseCases/My/FetchMyMemberIdUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/My/FetchMyMemberIdUseCase.swift @@ -8,7 +8,7 @@ import Foundation public protocol FetchMyMemberIdUseCaseProtocol { - func execute() -> String + func execute() -> String? } public class FetchMyMemberIdUseCase: FetchMyMemberIdUseCaseProtocol { @@ -22,7 +22,7 @@ public class FetchMyMemberIdUseCase: FetchMyMemberIdUseCaseProtocol { } // MARK: - Execute - public func execute() -> String { + public func execute() -> String? { myRepository.fetchMyMemberId() } diff --git a/14th-team5-iOS/Domain/Sources/UseCases/My/FetchMyUserNameUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/My/FetchMyUserNameUseCase.swift index beb228eea..e8d911375 100644 --- a/14th-team5-iOS/Domain/Sources/UseCases/My/FetchMyUserNameUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/My/FetchMyUserNameUseCase.swift @@ -8,7 +8,7 @@ import Foundation public protocol FetchMyUserNameUseCaseProtocol { - func execute() -> String + func execute() -> String? } public class FetchMyUserNameUseCase: FetchMyUserNameUseCaseProtocol { @@ -22,7 +22,7 @@ public class FetchMyUserNameUseCase: FetchMyUserNameUseCaseProtocol { } // MARK: - Execute - public func execute() -> String { + public func execute() -> String? { myRepository.fetchMyUserName() } diff --git a/14th-team5-iOS/Domain/Sources/UseCases/My/FetchProfileImageUrlUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/My/FetchProfileImageUrlUseCase.swift index 5d132ffec..c22fbcb6e 100644 --- a/14th-team5-iOS/Domain/Sources/UseCases/My/FetchProfileImageUrlUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/My/FetchProfileImageUrlUseCase.swift @@ -8,7 +8,7 @@ import Foundation public protocol FetchProfileImageUrlUseCaseProtocol { - func execute(memberId: String) -> String + func execute(memberId: String) -> String? } public class FetchProfileImageUrlUseCase: FetchProfileImageUrlUseCaseProtocol { @@ -22,7 +22,7 @@ public class FetchProfileImageUrlUseCase: FetchProfileImageUrlUseCaseProtocol { } // MARK: - Execute - public func execute(memberId: String) -> String { + public func execute(memberId: String) -> String? { myRepository.fetchProfileImageUrl(memberId: memberId) } diff --git a/14th-team5-iOS/Domain/Sources/UseCases/My/FetchUserNameUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/My/FetchUserNameUseCase.swift index 273381b0a..307763673 100644 --- a/14th-team5-iOS/Domain/Sources/UseCases/My/FetchUserNameUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/My/FetchUserNameUseCase.swift @@ -8,7 +8,7 @@ import Foundation public protocol FetchUserNameUseCaseProtocol { - func execute(memberId: String) -> String + func execute(memberId: String) -> String? } public class FetchUserNameUseCase: FetchUserNameUseCaseProtocol { @@ -22,7 +22,7 @@ public class FetchUserNameUseCase: FetchUserNameUseCaseProtocol { } // MARK: - Execute - public func execute(memberId: String) -> String { + public func execute(memberId: String) -> String? { myRepository.fetchUserName(memberId: memberId) } From 50f99225e8158d5c679a20563d6aa91cb1a70dce Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Sun, 11 Aug 2024 16:56:58 +0900 Subject: [PATCH 183/263] =?UTF-8?q?feat:=20BBNavigationBar=EC=97=90=20New?= =?UTF-8?q?=20=EB=A7=88=ED=81=AC=20=ED=91=9C=EC=8B=9C=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84=20(#606)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Base/BBNavigationViewController.swift | 4 +- .../BBNavigationBar.swift} | 97 +++++++++++-------- .../BBNavigationBarButtonStyle.swift | 51 ++++++---- .../BBNavigationBarDelegate.swift | 13 +++ .../BBNavigationBarDelegateProxy.swift | 45 +++++++++ .../BBNavigationBar+DelegateProxy.swift | 57 ----------- .../icons/new.imageset/Contents.json | 12 +++ .../icons/new.imageset/new.svg | 5 + 8 files changed, 167 insertions(+), 117 deletions(-) rename 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/{BBNavigationBarView/BBNavigationBarView.swift => BBNavigationBar/BBNavigationBar.swift} (83%) rename 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/{BBNavigationBarView => BBNavigationBar}/BBNavigationBarButtonStyle.swift (50%) create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBar/BBNavigationBarDelegate.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBar/BBNavigationBarDelegateProxy.swift delete mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBarView/BBNavigationBar+DelegateProxy.swift create mode 100644 14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/new.imageset/Contents.json create mode 100644 14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/new.imageset/new.svg diff --git a/14th-team5-iOS/Core/Sources/Base/BBNavigationViewController.swift b/14th-team5-iOS/Core/Sources/Base/BBNavigationViewController.swift index ab049d5f7..1451aaa5a 100644 --- a/14th-team5-iOS/Core/Sources/Base/BBNavigationViewController.swift +++ b/14th-team5-iOS/Core/Sources/Base/BBNavigationViewController.swift @@ -33,7 +33,7 @@ open class BBNavigationViewController: ReactorViewController where R: Reac // MARK: - Views - public let navigationBarView = BBNavigationBarView() + public let navigationBarView = BBNavigationBar() public let contentView = UIView() // MARK: - Properties @@ -135,7 +135,7 @@ extension BBNavigationViewController { extension BBNavigationViewController { - private func popViewController(_ ifTypeIsXMark: TopBarButtonStyle?) { + private func popViewController(_ ifTypeIsXMark: BBNavigationButtonStyle?) { if enableAutoPopViewController { switch ifTypeIsXMark { case .arrowLeft, .xmark: diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBarView/BBNavigationBarView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBar/BBNavigationBar.swift similarity index 83% rename from 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBarView/BBNavigationBarView.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBar/BBNavigationBar.swift index abf898919..95e0b9c5b 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBarView/BBNavigationBarView.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBar/BBNavigationBar.swift @@ -14,7 +14,7 @@ import RxSwift import RxCocoa ///. 삐삐 스타일의 NavigationBar가 구현된 View입니다. -public final class BBNavigationBarView: UIView { +public final class BBNavigationBar: UIView { // MARK: - Views private let containerView: UIView = UIView() @@ -25,8 +25,10 @@ public final class BBNavigationBarView: UIView { private let leftBarButton: UIButton = UIButton(type: .system) private let rightBarButton: UIButton = UIButton(type: .system) + private let newMarkImageView: UIImageView = UIImageView() + // MARK: - Properties - public weak var delegate: BBNavigationBarViewDelegate? + public weak var delegate: BBNavigationBarDelegate? /// NavigationBar의 Title을 바꿉니다. @@ -49,7 +51,7 @@ public final class BBNavigationBarView: UIView { /// NavigationBar의 Image를 바꿉니다. /// Image를 적용하면 Title이 사라집니다. - public var navigationImage: TopBarButtonStyle? { + public var navigationImage: BBNavigationButtonStyle? { didSet { navigationImageView.isHidden = false navigationTitleLabel.isHidden = true @@ -59,27 +61,28 @@ public final class BBNavigationBarView: UIView { } /// 왼쪽 버튼의 스타일을 설정합니다. - public var leftBarButtonItem: TopBarButtonStyle? { + public var leftBarButtonItem: BBNavigationButtonStyle? { didSet { - leftBarButton.setImage( - leftBarButtonItem?.image, - for: .normal - ) + setupButtonImage(leftBarButton, type: leftBarButtonItem) setupButtonBackground(leftBarButton, type: leftBarButtonItem) } } /// 오른쪽 버튼의 스타일을 설정합니다. - public var rightBarButtonItem: TopBarButtonStyle? { + public var rightBarButtonItem: BBNavigationButtonStyle? { didSet { - rightBarButton.setImage( - rightBarButtonItem?.image, - for: .normal - ) + setupButtonImage(rightBarButton, type: rightBarButtonItem) setupButtonBackground(rightBarButton, type: rightBarButtonItem) } } + /// 왼쪽 버튼에 New 표시를 숨깁니다. + public var isHiddenleftBarButtonNewMark: Bool = true { + didSet { + newMarkImageView.isHidden = isHiddenleftBarButtonNewMark + } + } + /// Navigation Image의 크기를 설정합니다. 기본값은 1.0입니다. public var navigationImageScale: CGFloat = 1.0 { didSet { @@ -142,13 +145,11 @@ public final class BBNavigationBarView: UIView { } // MARK: - Intializer - public convenience init() { - self.init(frame: .zero) - set("Bibbi") - } - + public override init(frame: CGRect) { super.init(frame: .zero) + set() + setupUI() setupAutolayout() setupAttributes() @@ -164,6 +165,8 @@ public final class BBNavigationBarView: UIView { containerView.addSubviews( leftBarButton, navigationImageView, navigationTitleLabel, rightBarButton ) + + leftBarButton.addSubview(newMarkImageView) } func setupAutolayout() { @@ -192,6 +195,11 @@ public final class BBNavigationBarView: UIView { $0.centerY.equalTo(self.snp.centerY) $0.width.height.equalTo(52.0) } + + newMarkImageView.snp.makeConstraints { + $0.top.equalToSuperview().offset(7) + $0.trailing.equalToSuperview().offset(10) + } } func setupAttributes() { @@ -204,9 +212,9 @@ public final class BBNavigationBarView: UIView { } leftBarButton.do { - $0.layer.masksToBounds = true - $0.layer.cornerRadius = 10.0 - $0.tintColor = DesignSystemAsset.gray300.color + $0.clipsToBounds = false + $0.layer.cornerRadius = 10 + $0.tintColor = .gray300 $0.addTarget( self, @@ -217,9 +225,9 @@ public final class BBNavigationBarView: UIView { } rightBarButton.do { - $0.layer.masksToBounds = true - $0.layer.cornerRadius = 10.0 - $0.tintColor = DesignSystemAsset.gray300.color + $0.clipsToBounds = false + $0.layer.cornerRadius = 10 + $0.tintColor = .gray300 $0.addTarget( self, @@ -228,6 +236,12 @@ public final class BBNavigationBarView: UIView { ) } + newMarkImageView.do { + $0.isHidden = true + $0.image = DesignSystemAsset.new.image + $0.contentMode = .scaleAspectFit + } + setupNavigationImageScale(navigationImageScale) setupLeftButtonImageScale(leftBarButtonItemScale) setupRightButtonImageScale(rightBarButtonItemScale) @@ -237,7 +251,7 @@ public final class BBNavigationBarView: UIView { // MARK: - Extensions -extension BBNavigationBarView { +extension BBNavigationBar { /// NavigationBar의 속성을 바꿉니다. /// @@ -254,14 +268,14 @@ extension BBNavigationBarView { /// - rightBarButtonItemScale: 네비게이션 바의 오른쪽 버튼 크기 /// - rightBarButtonYOffset: 네비게이션 바의 오른쪽 버튼 Y 위치 public func set( - _ title: String? = nil, + _ title: String? = "Bibbi", titleColor: UIColor = .gray200, titleFontStyle: BBFontStyle = .homeTitle, - leftBarButtonItem: TopBarButtonStyle = .none, + leftBarButtonItem: BBNavigationButtonStyle? = nil, leftBarButtonTint: UIColor = .gray300, leftBarButtonItemScale: CGFloat = 1.0, leftBarButtonYOffset: CGFloat = 0.0, - rightBarButtonItem: TopBarButtonStyle = .none, + rightBarButtonItem: BBNavigationButtonStyle? = nil, rightBarButtonTint: UIColor = .gray300, rightBarButtonItemScale: CGFloat = 1.0, rightBarButtonYOffset: CGFloat = 0.0 @@ -297,13 +311,13 @@ extension BBNavigationBarView { /// - rightBarButtonItemScale: 네비게이션 바의 오른쪽 버튼 크기 /// - rightBarButtonYOffset: 네비게이션 바의 오른쪽 버튼 Y 위치 public func set( - _ image: TopBarButtonStyle? = nil, + _ image: BBNavigationButtonStyle? = nil, imageScale: CGFloat = 1.0, - leftBarButtonItem: TopBarButtonStyle = .none, + leftBarButtonItem: BBNavigationButtonStyle? = nil, leftBarButtonTint: UIColor = .gray300, leftBarButtonItemScale: CGFloat = 1.0, leftBarButtonYOffset: CGFloat = 0.0, - rightBarButtonItem: TopBarButtonStyle = .none, + rightBarButtonItem: BBNavigationButtonStyle? = nil, rightBarButtonTint: UIColor = .gray300, rightBarButtonItemScale: CGFloat = 1.0, rightBarButtonYOffset: CGFloat = 0.0 @@ -324,11 +338,11 @@ extension BBNavigationBarView { } private func setAttributes( - leftBarButtonItem: TopBarButtonStyle = .none, + leftBarButtonItem: BBNavigationButtonStyle? = nil, leftBarButtonTint: UIColor = .gray300, leftBarButtonItemScale: CGFloat = 1.0, leftBarButtonYOffset: CGFloat = 0.0, - rightBarButtonItem: TopBarButtonStyle = .none, + rightBarButtonItem: BBNavigationButtonStyle? = nil, rightBarButtonTint: UIColor = .gray300, rightBarButtonItemScale: CGFloat = 1.0, rightBarButtonYOffset: CGFloat = 0.0 @@ -346,7 +360,7 @@ extension BBNavigationBarView { } -extension BBNavigationBarView { +extension BBNavigationBar { private func setupNavigationImageScale(_ scale: CGFloat) { navigationImageView.layer.transform = CATransform3DMakeScale( @@ -365,8 +379,15 @@ extension BBNavigationBarView { scale, scale, scale ) } + + private func setupButtonImage(_ button: UIButton, type: BBNavigationButtonStyle?) { + button.setImage(type?.image, for: .normal) + if case let .person(new) = type { + isHiddenleftBarButtonNewMark = !new + } + } - private func setupButtonBackground(_ button: UIButton, type: TopBarButtonStyle?) { + private func setupButtonBackground(_ button: UIButton, type: BBNavigationButtonStyle?) { if type == .arrowLeft || type == .xmark { button.backgroundColor = .gray900 } else { @@ -376,16 +397,16 @@ extension BBNavigationBarView { } -extension BBNavigationBarView { +extension BBNavigationBar { @objc func didTapLeftButton(_ button: UIButton, event: UIButton.Event) { guard let _ = button.currentImage else { return } - delegate?.navigationBarView?(button, didTapLeftBarButton: event) + delegate?.navigationBar?(button, didTapLeftBarButton: event) } @objc func didTapRightButton(_ button: UIButton, event: UIButton.Event) { guard let _ = button.currentImage else { return } - delegate?.navigationBarView?(button, didTapRightBarButton: event) + delegate?.navigationBar?(button, didTapRightBarButton: event) } } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBarView/BBNavigationBarButtonStyle.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBar/BBNavigationBarButtonStyle.swift similarity index 50% rename from 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBarView/BBNavigationBarButtonStyle.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBar/BBNavigationBarButtonStyle.swift index 6c351d9bf..356f95ab6 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBarView/BBNavigationBarButtonStyle.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBar/BBNavigationBarButtonStyle.swift @@ -11,31 +11,37 @@ import UIKit // MARK: - Typealias -public typealias TitleStyle = BBNavigationBarView.TitleStyle -public typealias TopBarButtonStyle = BBNavigationBarView.TopBarButtonStyle +public typealias BBNavigationTitleStyle = BBNavigationBar.Style.Title +public typealias BBNavigationButtonStyle = BBNavigationBar.Style.Button // MARK: - Extensions -public extension BBNavigationBarView { +extension BBNavigationBar { - enum TitleStyle { - case bibbi - } - - enum TopBarButtonStyle { - case addPerson - case arrowLeft - case change - case heartCalendar - case setting - case xmark - case none + public enum Style { + + public enum Title { + case bibbi + } + + public enum Button { + @available(*, deprecated, renamed: "person") + case addPerson + + case person(new: Bool) + case arrowLeft + case refresh + case calendar + case setting + case xmark + } + } } -extension TitleStyle { +extension BBNavigationTitleStyle { var image: UIImage? { switch self { @@ -46,17 +52,17 @@ extension TitleStyle { } -extension TopBarButtonStyle { +extension BBNavigationButtonStyle { var image: UIImage? { switch self { - case .addPerson: + case .addPerson, .person: return DesignSystemAsset.addPerson.image case .arrowLeft: return DesignSystemAsset.arrowLeft.image - case .change: + case .refresh: return DesignSystemAsset.familyNameChange.image - case .heartCalendar: + case .calendar: return DesignSystemAsset.heartCalendar.image case .setting: return DesignSystemAsset.setting.image @@ -68,3 +74,8 @@ extension TopBarButtonStyle { } } + + +extension BBNavigationTitleStyle: Equatable { } + +extension BBNavigationButtonStyle: Equatable { } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBar/BBNavigationBarDelegate.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBar/BBNavigationBarDelegate.swift new file mode 100644 index 000000000..dc34ff1c6 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBar/BBNavigationBarDelegate.swift @@ -0,0 +1,13 @@ +// +// BBNavigationBarDelegate.swift +// Core +// +// Created by 김건우 on 8/11/24. +// + +import UIKit + +@objc public protocol BBNavigationBarDelegate { + @objc optional func navigationBar(_ button: UIButton, didTapRightBarButton event: UIControl.Event) + @objc optional func navigationBar(_ button: UIButton, didTapLeftBarButton event: UIControl.Event) +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBar/BBNavigationBarDelegateProxy.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBar/BBNavigationBarDelegateProxy.swift new file mode 100644 index 000000000..012f25227 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBar/BBNavigationBarDelegateProxy.swift @@ -0,0 +1,45 @@ +// +// BBNavigationBarDelegateProxy.swift +// Core +// +// Created by 김건우 on 6/5/24. +// + +import UIKit + +import RxSwift +import RxCocoa + + +final class RxBBNavigationBarViewDelegateProxy: DelegateProxy, DelegateProxyType, BBNavigationBarDelegate { + static func registerKnownImplementations() { + self.register { + RxBBNavigationBarViewDelegateProxy(parentObject: $0, delegateProxy: self) + } + } +} + +extension BBNavigationBar: HasDelegate { + public typealias Delegate = BBNavigationBarDelegate +} + + +extension Reactive where Base: BBNavigationBar { + public var delegate: DelegateProxy { + return RxBBNavigationBarViewDelegateProxy.proxy(for: self.base) + } + + public var didTapLeftBarButton: ControlEvent { + let source = delegate.sentMessage(#selector(BBNavigationBarDelegate.navigationBar(_:didTapLeftBarButton:))) + .map { $0[0] as! UIButton } + + return ControlEvent(events: source) + } + + public var didTapRightBarButton: ControlEvent { + let source = delegate.sentMessage(#selector(BBNavigationBarDelegate.navigationBar(_:didTapRightBarButton:))) + .map { $0[0] as! UIButton } + + return ControlEvent(events: source) + } +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBarView/BBNavigationBar+DelegateProxy.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBarView/BBNavigationBar+DelegateProxy.swift deleted file mode 100644 index 697f52f0f..000000000 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBarView/BBNavigationBar+DelegateProxy.swift +++ /dev/null @@ -1,57 +0,0 @@ -// -// BBNavigationBarDelegateProxy.swift -// Core -// -// Created by 김건우 on 6/5/24. -// - -import UIKit - -import RxSwift -import RxCocoa - - -// MARK: - Delegate - -@objc public protocol BBNavigationBarViewDelegate { - @objc optional func navigationBarView(_ button: UIButton, didTapRightBarButton event: UIControl.Event) - @objc optional func navigationBarView(_ button: UIButton, didTapLeftBarButton event: UIControl.Event) -} - - -// MARK: - DelgateProxy - -final class RxBBNavigationBarViewDelegateProxy: DelegateProxy, DelegateProxyType, BBNavigationBarViewDelegate { - static func registerKnownImplementations() { - self.register { - RxBBNavigationBarViewDelegateProxy(parentObject: $0, delegateProxy: self) - } - } -} - -extension BBNavigationBarView: HasDelegate { - public typealias Delegate = BBNavigationBarViewDelegate -} - - -extension Reactive where Base: BBNavigationBarView { - public var delegate: DelegateProxy { - return RxBBNavigationBarViewDelegateProxy.proxy(for: self.base) - } - - public var didTapLeftBarButton: ControlEvent { - let source = delegate.sentMessage(#selector(BBNavigationBarViewDelegate.navigationBarView(_:didTapLeftBarButton:))) - .debug("navigationBarView(_:didTapLeftBarButton:) 메서드 호출 성공") - .map { $0[0] as! UIButton } - - return ControlEvent(events: source) - } - - public var didTapRightBarButton: ControlEvent { - let source = delegate.sentMessage(#selector(BBNavigationBarViewDelegate.navigationBarView(_:didTapRightBarButton:))) - .debug("navigationBarView(_:didTapRightBarButton:) 메서드 호출 성공") - .map { $0[0] as! UIButton } - - return ControlEvent(events: source) - } -} diff --git a/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/new.imageset/Contents.json b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/new.imageset/Contents.json new file mode 100644 index 000000000..b9bbb213d --- /dev/null +++ b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/new.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "new.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/new.imageset/new.svg b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/new.imageset/new.svg new file mode 100644 index 000000000..f4a60f6f8 --- /dev/null +++ b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/icons/new.imageset/new.svg @@ -0,0 +1,5 @@ + + + + + From 6a736b4ecd3d37fbef243008b25ef81b2ca5ccd5 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Sun, 11 Aug 2024 17:11:30 +0900 Subject: [PATCH 184/263] =?UTF-8?q?feat:=20NavigationBar=EC=97=90=20Rx+Ext?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84=20(#606)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DailyCalendarViewController.swift | 8 +++---- .../FamilyManagementViewController.swift | 6 ++--- .../Base/BBNavigationViewController.swift | 22 +++++++++---------- .../BBNavigationBar/BBNavigationBar.swift | 6 ++--- .../BBNavigationBarReactive.swift | 20 +++++++++++++++++ 5 files changed, 41 insertions(+), 21 deletions(-) create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBar/BBNavigationBarReactive.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/DailyCalendarViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/DailyCalendarViewController.swift index 9766337a7..a40b26a12 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/DailyCalendarViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/DailyCalendarViewController.swift @@ -71,7 +71,7 @@ public final class DailyCalendarViewController: BBNavigationViewController: ReactorViewController where R: Reac // MARK: - Views - public let navigationBarView = BBNavigationBar() + public let navigationBar = BBNavigationBar() public let contentView = UIView() // MARK: - Properties @@ -62,11 +62,11 @@ open class BBNavigationViewController: ReactorViewController where R: Reac super.bind(reactor: reactor) // 왼쪽 버튼이 특정 타입시, popViewController 기본 구현 제공 - navigationBarView.rx.didTapLeftBarButton + navigationBar.rx.didTapLeftBarButton .bind(with: self) { owner, _ in - let buttonItem = owner.navigationBarView.leftBarButtonItem + let item = owner.navigationBar.leftBarButtonItem - owner.popViewController(buttonItem) + owner.popViewController(item) } .disposed(by: disposeBag) } @@ -74,20 +74,20 @@ open class BBNavigationViewController: ReactorViewController where R: Reac open override func setupUI() { super.setupUI() - view.addSubviews(navigationBarView, contentView) + view.addSubviews(navigationBar, contentView) } open override func setupAutoLayout() { super.setupAutoLayout() - navigationBarView.snp.makeConstraints { + navigationBar.snp.makeConstraints { $0.top.equalTo(view.safeAreaLayoutGuide) $0.horizontalEdges.equalToSuperview() $0.height.equalTo(42) // 내비게이션 바 기본 높이 42 } contentView.snp.makeConstraints { - $0.top.equalTo(navigationBarView.snp.bottom) + $0.top.equalTo(navigationBar.snp.bottom) $0.horizontalEdges.equalToSuperview() $0.bottom.equalToSuperview() } @@ -97,7 +97,7 @@ open class BBNavigationViewController: ReactorViewController where R: Reac open override func setupAttributes() { super.setupAttributes() - navigationBarView.layer.zPosition = 888 + navigationBar.layer.zPosition = 888 } } @@ -109,14 +109,14 @@ extension BBNavigationViewController { /// NavigationBar의 높이를 바꿉니다. public func setNavigationBarHeight(_ height: CGFloat) { - navigationBarView.snp.updateConstraints { + navigationBar.snp.updateConstraints { $0.top.equalTo(view.safeAreaLayoutGuide) $0.horizontalEdges.equalToSuperview() $0.height.equalTo(height) } contentView.snp.makeConstraints { - $0.top.equalTo(navigationBarView.snp.bottom) + $0.top.equalTo(navigationBar.snp.bottom) $0.horizontalEdges.equalToSuperview() $0.bottom.equalToSuperview() } @@ -127,7 +127,7 @@ extension BBNavigationViewController { /// contentView가 아닌 view에 새로운 UI를 배치할 때, 꼭 호출해주어야 합니다. @available(*, deprecated) public func bringNavigationBarViewToFront() { - view.bringSubviewToFront(navigationBarView) + view.bringSubviewToFront(navigationBar) } } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBar/BBNavigationBar.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBar/BBNavigationBar.swift index 95e0b9c5b..24e129497 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBar/BBNavigationBar.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBar/BBNavigationBar.swift @@ -77,9 +77,9 @@ public final class BBNavigationBar: UIView { } /// 왼쪽 버튼에 New 표시를 숨깁니다. - public var isHiddenleftBarButtonNewMark: Bool = true { + public var isHiddenLeftBarButtonNewMark: Bool = true { didSet { - newMarkImageView.isHidden = isHiddenleftBarButtonNewMark + newMarkImageView.isHidden = isHiddenLeftBarButtonNewMark } } @@ -383,7 +383,7 @@ extension BBNavigationBar { private func setupButtonImage(_ button: UIButton, type: BBNavigationButtonStyle?) { button.setImage(type?.image, for: .normal) if case let .person(new) = type { - isHiddenleftBarButtonNewMark = !new + isHiddenLeftBarButtonNewMark = !new } } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBar/BBNavigationBarReactive.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBar/BBNavigationBarReactive.swift new file mode 100644 index 000000000..f31e22b14 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBar/BBNavigationBarReactive.swift @@ -0,0 +1,20 @@ +// +// BBNavigationBarRx.swift +// Core +// +// Created by 김건우 on 8/11/24. +// + +import Foundation + +import RxSwift + +public extension Reactive where Base: BBNavigationBar { + + var isHiddenLeftBarButtonNewMark: Binder { + Binder(self.base) { navigationBar, isHidden in + navigationBar.isHiddenLeftBarButtonNewMark = isHidden + } + } + +} From bf681adec79f98457839066ad8f0580dfd7f7227 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Sun, 11 Aug 2024 20:44:41 +0900 Subject: [PATCH 185/263] =?UTF-8?q?fix:=20Constraints=20=EC=B6=A9=EB=8F=8C?= =?UTF-8?q?=20=EC=97=90=EB=9F=AC=20=EC=88=98=EC=A0=95=20(#606)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BBNavigationBar/BBNavigationBar.swift | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBar/BBNavigationBar.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBar/BBNavigationBar.swift index 24e129497..c17096086 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBar/BBNavigationBar.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBar/BBNavigationBar.swift @@ -146,9 +146,13 @@ public final class BBNavigationBar: UIView { // MARK: - Intializer + public convenience init() { + self.init(frame: .zero) + commonInit() + } + public override init(frame: CGRect) { - super.init(frame: .zero) - set() + super.init(frame: frame) setupUI() setupAutolayout() @@ -160,7 +164,12 @@ public final class BBNavigationBar: UIView { } // MARK: - Helpers - func setupUI() { + + private func commonInit() { + set() + } + + private func setupUI() { addSubview(containerView) containerView.addSubviews( leftBarButton, navigationImageView, navigationTitleLabel, rightBarButton @@ -169,7 +178,7 @@ public final class BBNavigationBar: UIView { leftBarButton.addSubview(newMarkImageView) } - func setupAutolayout() { + private func setupAutolayout() { containerView.snp.makeConstraints { $0.edges.equalToSuperview() } @@ -202,7 +211,7 @@ public final class BBNavigationBar: UIView { } } - func setupAttributes() { + private func setupAttributes() { containerView.do { $0.backgroundColor = UIColor.clear } From d106a00084fbff0269613a2d1c3350e586fe068e Mon Sep 17 00:00:00 2001 From: Kyoungmi <1mmgm.dev@gmail.com> Date: Sun, 11 Aug 2024 21:00:54 +0900 Subject: [PATCH 186/263] feat: modify view and rename(#608) --- .../DIContainer/NavigatorDIContainer.swift | 4 + .../Navigator/FamilyEntranceNavigator.swift | 39 ++++ .../Navigator/SplashNavigator.swift | 2 +- .../FamilyEntranceControllerWrapper.swift | 38 ++++ .../JoinedFamilyViewControllerWrapper.swift | 38 ---- .../Dependency/JoinedFamilyDIContainer.swift | 8 +- .../Reactor/FamilyEntranceReactor.swift | 63 ++++++ .../Reactor/JoinedFamilyReactor.swift | 55 ----- .../FamilyEntranceViewController.swift | 205 ++++++++++++++++++ .../JoinedFamilyViewController.swift | 152 ------------- 10 files changed, 354 insertions(+), 250 deletions(-) create mode 100644 14th-team5-iOS/App/Sources/Application/Navigator/FamilyEntranceNavigator.swift create mode 100644 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/JoinFamily/FamilyEntranceControllerWrapper.swift delete mode 100644 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/JoinFamily/JoinedFamilyViewControllerWrapper.swift create mode 100644 14th-team5-iOS/App/Sources/Presentation/JoinFamily/Reactor/FamilyEntranceReactor.swift delete mode 100644 14th-team5-iOS/App/Sources/Presentation/JoinFamily/Reactor/JoinedFamilyReactor.swift create mode 100644 14th-team5-iOS/App/Sources/Presentation/JoinFamily/ViewController/FamilyEntranceViewController.swift delete mode 100644 14th-team5-iOS/App/Sources/Presentation/JoinFamily/ViewController/JoinedFamilyViewController.swift diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/NavigatorDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/NavigatorDIContainer.swift index 5a2c4c673..46844b5e2 100644 --- a/14th-team5-iOS/App/Sources/Application/DIContainer/NavigatorDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/NavigatorDIContainer.swift @@ -54,6 +54,10 @@ final class NavigatorDIContainer: BaseContainer { navigationController: makeUINavigationController() ) } + + container.register(type: FamilyEntranceNavigatorProtocol.self) { _ in + FamilyEntranceNavigator(navigationController: makeUINavigationController()) + } } } diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/FamilyEntranceNavigator.swift b/14th-team5-iOS/App/Sources/Application/Navigator/FamilyEntranceNavigator.swift new file mode 100644 index 000000000..84ab31643 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/Navigator/FamilyEntranceNavigator.swift @@ -0,0 +1,39 @@ +// +// FamilyEntranceNavigator.swift +// App +// +// Created by 마경미 on 11.08.24. +// + +import Core +import UIKit + +protocol FamilyEntranceNavigatorProtocol: BaseNavigator { + func toHome() + func toInputCode() +} + +final class FamilyEntranceNavigator: FamilyEntranceNavigatorProtocol { + + // MARK: - Properties + + var navigationController: UINavigationController + + // MARK: - Intializer + + init(navigationController: UINavigationController) { + self.navigationController = navigationController + } + + // MARK: - To + + func toHome() { + let vc = MainViewControllerWrapper().viewController + navigationController.pushViewController(vc, animated: true) + } + + func toInputCode() { + let vc = InputFamilyLinkViewControllerWrapper().viewController + navigationController.pushViewController(vc, animated: true) + } +} diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/SplashNavigator.swift b/14th-team5-iOS/App/Sources/Application/Navigator/SplashNavigator.swift index ba1a664f1..024940b7f 100644 --- a/14th-team5-iOS/App/Sources/Application/Navigator/SplashNavigator.swift +++ b/14th-team5-iOS/App/Sources/Application/Navigator/SplashNavigator.swift @@ -34,7 +34,7 @@ final class SplashNavigator: SplashNavigatorProtocol { } func toJoined() { - let vc = JoinedFamilyViewControllerWrapper().viewController + let vc = FamilyEntranceControllerWrapper().viewController navigationController.setViewControllers([vc], animated: false) } diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/JoinFamily/FamilyEntranceControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/JoinFamily/FamilyEntranceControllerWrapper.swift new file mode 100644 index 000000000..4c91e560a --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/JoinFamily/FamilyEntranceControllerWrapper.swift @@ -0,0 +1,38 @@ +// +// JoinedFamilyViewControllerWrapper.swift +// App +// +// Created by 김건우 on 6/26/24. +// + +import Core +import Foundation + +final class FamilyEntranceControllerWrapper: BaseWrapper { + + // MARK: - Typealias + + typealias R = FamilyEntranceReactor + typealias V = FamilyEntranceViewController + + // MARK: - Properties + + var reactor: FamilyEntranceReactor { + makeReactor() + } + + var viewController: FamilyEntranceViewController { + makeViewController() + } + + // MARK: - Make + + func makeReactor() -> FamilyEntranceReactor { + FamilyEntranceReactor() + } + + func makeViewController() -> FamilyEntranceViewController { + FamilyEntranceViewController(reactor: makeReactor()) + } + +} diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/JoinFamily/JoinedFamilyViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/JoinFamily/JoinedFamilyViewControllerWrapper.swift deleted file mode 100644 index 914804609..000000000 --- a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/JoinFamily/JoinedFamilyViewControllerWrapper.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// JoinedFamilyViewControllerWrapper.swift -// App -// -// Created by 김건우 on 6/26/24. -// - -import Core -import Foundation - -final class JoinedFamilyViewControllerWrapper: BaseWrapper { - - // MARK: - Typealias - - typealias R = JoinedFamilyReactor - typealias V = JoinedFamilyViewController - - // MARK: - Properties - - var reactor: JoinedFamilyReactor { - makeReactor() - } - - var viewController: JoinedFamilyViewController { - makeViewController() - } - - // MARK: - Make - - func makeReactor() -> JoinedFamilyReactor { - JoinedFamilyReactor() - } - - func makeViewController() -> JoinedFamilyViewController { - JoinedFamilyViewController(reactor: makeReactor()) - } - -} diff --git a/14th-team5-iOS/App/Sources/Presentation/JoinFamily/Dependency/JoinedFamilyDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/JoinFamily/Dependency/JoinedFamilyDIContainer.swift index 166c81d7b..f968a3dcf 100644 --- a/14th-team5-iOS/App/Sources/Presentation/JoinFamily/Dependency/JoinedFamilyDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/JoinFamily/Dependency/JoinedFamilyDIContainer.swift @@ -13,11 +13,11 @@ import Domain @available(*, deprecated, renamed: "JoinedFamilyViewControllerWrapper") final class JoinedFamilyDIContainer { - public func makeViewController() -> JoinedFamilyViewController { - return JoinedFamilyViewController(reactor: makeReactor()) + public func makeViewController() -> FamilyEntranceViewController { + return FamilyEntranceViewController(reactor: makeReactor()) } - public func makeReactor() -> JoinedFamilyReactor { - return JoinedFamilyReactor() + public func makeReactor() -> FamilyEntranceReactor { + return FamilyEntranceReactor() } } diff --git a/14th-team5-iOS/App/Sources/Presentation/JoinFamily/Reactor/FamilyEntranceReactor.swift b/14th-team5-iOS/App/Sources/Presentation/JoinFamily/Reactor/FamilyEntranceReactor.swift new file mode 100644 index 000000000..d408e5797 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/JoinFamily/Reactor/FamilyEntranceReactor.swift @@ -0,0 +1,63 @@ +// +// JoinedFamilyReactor.swift +// App +// +// Created by geonhui Yu on 2/8/24. +// + +import Foundation + +import Core +import Domain + +import ReactorKit + +public final class FamilyEntranceReactor: Reactor { + public enum Action { + case loadFamily + case showHome + case joinFamily + } + + public enum Mutation { + case setProfiles([FamilyMemberProfileEntity]?) + } + + public struct State { + var profiles: [FamilyMemberProfileEntity]? + } + + public var initialState: State = State() + @Navigator var navigator: FamilyEntranceNavigatorProtocol + @Injected var provider: GlobalStateProviderProtocol + @Injected var familyUseCase: FamilyUseCaseProtocol +} + +extension FamilyEntranceReactor { + public func mutate(action: Action) -> Observable { + switch action { + case .showHome: + navigator.toHome() + return .empty() + case .joinFamily: + UserDefaults.standard.clearInviteCode() + navigator.toInputCode() + return .empty() + case .loadFamily: + return familyUseCase.executeFetchPaginationFamilyMembers(query: .init()) + .flatMap { + return Observable.just(.setProfiles($0?.results)) + } + + } + } + + public func reduce(state: State, mutation: Mutation) -> State { + var newState = state + switch mutation { + case .setProfiles(let profiles): + newState.profiles = profiles + } + return newState + } +} diff --git a/14th-team5-iOS/App/Sources/Presentation/JoinFamily/Reactor/JoinedFamilyReactor.swift b/14th-team5-iOS/App/Sources/Presentation/JoinFamily/Reactor/JoinedFamilyReactor.swift deleted file mode 100644 index 49a335515..000000000 --- a/14th-team5-iOS/App/Sources/Presentation/JoinFamily/Reactor/JoinedFamilyReactor.swift +++ /dev/null @@ -1,55 +0,0 @@ -// -// JoinedFamilyReactor.swift -// App -// -// Created by geonhui Yu on 2/8/24. -// - -import Core -import Domain - -import ReactorKit - -public final class JoinedFamilyReactor: Reactor { - public enum Action { - case enterFamily - case joinFamily - } - - public enum Mutation { - case setHomeView(Bool) - case setShowJoinFamily(Bool) - } - - public struct State { - var isShowHome: Bool = false - var isShowJoinFamily: Bool = false - } - - public var initialState: State - init() { - self.initialState = State() - } -} - -extension JoinedFamilyReactor { - public func mutate(action: Action) -> Observable { - switch action { - case .enterFamily: - return Observable.just(.setHomeView(true)) - case .joinFamily: - return Observable.just(.setShowJoinFamily(true)) - } - } - - public func reduce(state: State, mutation: Mutation) -> State { - var newState = state - switch mutation { - case .setHomeView(let isShow): - newState.isShowHome = isShow - case .setShowJoinFamily(let isShow): - newState.isShowJoinFamily = isShow - } - return newState - } -} diff --git a/14th-team5-iOS/App/Sources/Presentation/JoinFamily/ViewController/FamilyEntranceViewController.swift b/14th-team5-iOS/App/Sources/Presentation/JoinFamily/ViewController/FamilyEntranceViewController.swift new file mode 100644 index 000000000..4ab11b94b --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/JoinFamily/ViewController/FamilyEntranceViewController.swift @@ -0,0 +1,205 @@ +import UIKit +import Core +import DesignSystem +import Domain + +import ReactorKit +import RxCocoa +import RxDataSources +import RxSwift +import SnapKit +import Then + +final class FamilyEntranceViewController: BaseViewController { + private let profileStackView = UIStackView() + private let familyCountLabel = BBLabel(.body1Regular, textColor: .gray300) + + private let titleLabel = BBLabel(.head1, textColor: .gray100) + private let captionLabel = BBLabel(.body1Regular, textColor: .gray300) + + private let showHomeButton = BBButton() + private let showCodeButton = BBButton() + + override public func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + navigationController?.navigationBar.isHidden = true + } + + override func setupUI() { + super.setupUI() + + view.addSubviews(profileStackView, familyCountLabel, titleLabel, + captionLabel, showHomeButton, showCodeButton) + } + + override func setupAttributes() { + super.setupAttributes() + + profileStackView.do { + $0.axis = .horizontal + $0.distribution = .fillEqually + $0.spacing = -40 + } + + titleLabel.do { + $0.text = "이미 가입된 가족이 있어요" + } + + captionLabel.do { + $0.text = "하나의 가족에만 소속될 수 있어요." + } + + showHomeButton.do { + $0.setTitle("홈으로 돌아가기", for: .normal) + $0.backgroundColor = .mainYellow + $0.layer.cornerRadius = 28 + $0.setTitleColor(.bibbiBlack, for: .normal) + $0.setTitleFontStyle(.body1Bold) + } + + showCodeButton.do { + $0.setTitle("그룹 탈퇴 후 초대링크로 입장하기", for: .normal) + $0.backgroundColor = .mainYellow + $0.layer.cornerRadius = 28 + $0.setTitleColor(.bibbiBlack, for: .normal) + $0.setTitleFontStyle(.body1Bold) + } + } + + override func setupAutoLayout() { + super.setupAutoLayout() + + profileStackView.snp.makeConstraints { + $0.top.equalTo(view.safeAreaLayoutGuide).inset(100) + $0.centerX.equalToSuperview() + $0.height.equalTo(80) + } + + familyCountLabel.snp.makeConstraints { + $0.top.equalTo(profileStackView.snp.bottom).offset(16) + $0.centerX.equalToSuperview() + } + + titleLabel.snp.makeConstraints { + $0.top.equalTo(familyCountLabel.snp.bottom).offset(32) + $0.centerX.equalToSuperview() + } + + captionLabel.snp.makeConstraints { + $0.top.equalTo(titleLabel.snp.bottom).offset(16) + $0.centerX.equalToSuperview() + } + + showHomeButton.snp.makeConstraints { + $0.horizontalEdges.equalToSuperview().inset(12) + $0.bottom.equalTo(showCodeButton.snp.top).offset(-22) + $0.height.equalTo(56) + } + + showCodeButton.snp.makeConstraints { + $0.horizontalEdges.equalToSuperview().inset(12) + $0.height.equalTo(56) + $0.bottom.equalTo(view.safeAreaLayoutGuide).offset(-17) + } + } + + override func bind(reactor: FamilyEntranceReactor) { + super.bind(reactor: reactor) + bindInput(reactor: reactor) + bindOutput(reactor: reactor) + } +} + +extension FamilyEntranceViewController { + private func bindInput(reactor: FamilyEntranceReactor) { + Observable.just(()) + .map { Reactor.Action.loadFamily } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + showHomeButton.rx.tap + .throttle(RxConst.milliseconds300Interval, scheduler: MainScheduler.instance) + .map { Reactor.Action.showHome } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + showCodeButton.rx.tap + .throttle(RxConst.milliseconds300Interval, scheduler: MainScheduler.instance) + .map { Reactor.Action.joinFamily } + .bind(to: reactor.action) + .disposed(by: disposeBag) + } + + private func bindOutput(reactor: FamilyEntranceReactor) { + reactor.state + .compactMap { $0.profiles } + .distinctUntilChanged() + .observe(on: RxSchedulers.main) + .withUnretained(self) + .bind(onNext: { $0.0.setProfileStackView(profiles: $0.1) }) + .disposed(by: disposeBag) + } +} + +extension FamilyEntranceViewController { + private func updateProfileStackViewWidth() { + let profileWidth: CGFloat = 80 + let profileSpacing: CGFloat = -40 + let itemCount = profileStackView.arrangedSubviews.count + + let totalWidth = profileWidth * CGFloat(itemCount) + profileSpacing * CGFloat(itemCount - 1) + + profileStackView.snp.remakeConstraints { + $0.top.equalTo(view.safeAreaLayoutGuide).inset(100) + $0.centerX.equalToSuperview() + $0.height.equalTo(80) + $0.width.equalTo(totalWidth) + } + } + + private func setProfileStackView(profiles: [FamilyMemberProfileEntity]) { + profileStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } + + let displayProfiles = profiles.prefix(3) + + displayProfiles.enumerated().forEach { index, profile in + let profileView: UIImageView + if index == 2, profiles.count > 3 { + profileView = createProfileImageView(with: nil, name: "+\(profiles.count - 2)") + } else { + profileView = createProfileImageView(with: profile.profileImageURL, name: profile.name) + } + + profileStackView.addArrangedSubview(profileView) + } + + updateProfileStackViewWidth() + } + + private func createProfileImageView(with url: String?, name: String) -> UIImageView { + let imageView = UIImageView(frame: .init(x: 0, y: 0, width: 80, height: 80)).then { + $0.clipsToBounds = true + $0.layer.cornerRadius = 40 + $0.layer.borderWidth = 6 + $0.layer.borderColor = DesignSystemColors.Color.bibbiBlack.cgColor + + if let urlString = url, let imageUrl = URL(string: urlString) { + $0.kf.setImage(with: imageUrl) + } else { + $0.backgroundColor = .gray800 + addLabeltoImage(to: $0, withText: name) + } + } + return imageView + } + + private func addLabeltoImage(to view: UIImageView, withText text: String) { + let label = BBLabel(.head2Bold, textAlignment: .center, textColor: .gray200) + label.text = text + + view.addSubview(label) + label.snp.makeConstraints { + $0.edges.equalToSuperview() + } + } +} diff --git a/14th-team5-iOS/App/Sources/Presentation/JoinFamily/ViewController/JoinedFamilyViewController.swift b/14th-team5-iOS/App/Sources/Presentation/JoinFamily/ViewController/JoinedFamilyViewController.swift deleted file mode 100644 index 9271a3724..000000000 --- a/14th-team5-iOS/App/Sources/Presentation/JoinFamily/ViewController/JoinedFamilyViewController.swift +++ /dev/null @@ -1,152 +0,0 @@ -// -// JoinedFamilyViewController.swift -// App -// -// Created by geonhui Yu on 2/8/24. -// - -import UIKit -import Core -import DesignSystem - -import ReactorKit -import RxCocoa -import RxDataSources -import RxSwift -import SnapKit -import Then - -fileprivate typealias _Str = JoinedFamilyStrings -final class JoinedFamilyViewController: BaseViewController { - private let titleLabel = BBLabel(.head1, textColor: .gray100) - private let captionLabel = BBLabel(.body1Regular, textColor: .gray300) - private let labelStack = UIStackView() - - private let imageView = UIImageView() - - private let showHomeButton = UIButton() - private let showJoinedFamilyButton = UIButton() - - override func setupUI() { - super.setupUI() - - view.addSubviews(titleLabel, captionLabel, imageView, showHomeButton, showJoinedFamilyButton) - } - - override public func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - navigationController?.navigationBar.isHidden = true - } - - override func setupAttributes() { - super.setupAttributes() - - titleLabel.do { - $0.text = _Str.title - } - - captionLabel.do { - $0.text = _Str.caption - } - - imageView.do { - $0.image = DesignSystemAsset.joinedFamilyCharacter.image - $0.contentMode = .scaleAspectFit - } - - showHomeButton.do { - $0.setTitle(_Str.homeBtnTitle, for: .normal) - $0.titleLabel?.font = UIFont(font: DesignSystemFontFamily.Pretendard.semiBold, size: 16) - $0.setTitleColor(DesignSystemAsset.black.color, for: .normal) - $0.backgroundColor = DesignSystemAsset.mainYellow.color - $0.layer.cornerRadius = 28 - } - - showJoinedFamilyButton.do { - $0.setTitle(_Str.joinFamilyBtnTitle, for: .normal) - $0.titleLabel?.font = UIFont(font: DesignSystemFontFamily.Pretendard.semiBold, size: 16) - $0.setTitleColor(DesignSystemAsset.black.color, for: .normal) - $0.backgroundColor = DesignSystemAsset.mainYellow.color - $0.layer.cornerRadius = 28 - } - } - - override func setupAutoLayout() { - super.setupAutoLayout() - - titleLabel.snp.makeConstraints { - $0.top.equalTo(view.safeAreaLayoutGuide).inset(44) - $0.horizontalEdges.equalToSuperview().inset(20) - } - - captionLabel.snp.makeConstraints { - $0.top.equalTo(titleLabel.snp.bottom).offset(8) - $0.horizontalEdges.equalToSuperview().inset(20) - } - - imageView.snp.makeConstraints { - $0.centerX.centerY.equalToSuperview() - } - - showHomeButton.snp.makeConstraints { - $0.horizontalEdges.equalToSuperview().inset(12) - $0.bottom.equalTo(showJoinedFamilyButton.snp.top).offset(-22) - $0.height.equalTo(56) - } - - showJoinedFamilyButton.snp.makeConstraints { - $0.horizontalEdges.equalToSuperview().inset(12) - $0.height.equalTo(56) - $0.bottom.equalTo(view.safeAreaLayoutGuide).offset(-17) - } - } - - override func bind(reactor: JoinedFamilyReactor) { - super.bind(reactor: reactor) - - showHomeButton.rx.tap - .throttle(RxConst.milliseconds300Interval, scheduler: MainScheduler.instance) - .map { Reactor.Action.enterFamily } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - showJoinedFamilyButton.rx.tap - .throttle(RxConst.milliseconds300Interval, scheduler: MainScheduler.instance) - .map { Reactor.Action.joinFamily } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - reactor.state - .map { $0.isShowHome } - .observe(on: RxSchedulers.main) - .distinctUntilChanged() - .withUnretained(self) - .bind(onNext: { $0.0.showHomeViewController($0.1) }) - .disposed(by: disposeBag) - - reactor.state - .map { $0.isShowJoinFamily } - .observe(on: RxSchedulers.main) - .withUnretained(self) - .bind(onNext: { $0.0.showInputLinkViewController($0.1) }) - .disposed(by: disposeBag) - } -} - -extension JoinedFamilyViewController { - private func showHomeViewController(_ isShow: Bool) { - guard isShow else { return } - - UserDefaults.standard.clearInviteCode() - - guard let sceneDelegate = UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate else { return } - sceneDelegate.window?.rootViewController = UINavigationController(rootViewController: MainViewControllerWrapper().makeViewController()) - sceneDelegate.window?.makeKeyAndVisible() - } - - private func showInputLinkViewController(_ isShow: Bool) { - guard isShow else { return } - let inputFamilyLinkViewController = InputFamilyLinkDIContainer().makeViewController() - self.navigationController?.pushViewController(inputFamilyLinkViewController, animated: true) - } -} From d34976322823eefe51b927ce388cd8c7becc7f87 Mon Sep 17 00:00:00 2001 From: Kyoungmi <1mmgm.dev@gmail.com> Date: Sun, 11 Aug 2024 21:25:32 +0900 Subject: [PATCH 187/263] feat: rename folder(#608) --- .../Dependency/InputFamilyLinkDIContainer.swift | 0 .../Dependency/JoinFamilyDIContainer.swift | 0 .../Dependency/JoinedFamilyDIContainer.swift | 0 .../Reactor/FamilyEntranceReactor.swift | 0 .../Reactor/InputFamilyLinkReactor.swift | 0 .../Reactor/JoinFamilyReactor.swift | 0 .../Strings/JoinFamilyStrings.swift | 0 .../Strings/inviteFamilyLinkStrings.swift | 0 .../Strings/joinedFamilyStrings.swift | 0 .../ViewController/FamilyEntranceViewController.swift | 0 .../ViewController/InputFamilyLinkViewController.swift | 0 .../ViewController/JoinFamilyViewController.swift | 0 .../{JoinFamily => FamilyEntrance}/Views/MakeNewFamilyView.swift | 0 13 files changed, 0 insertions(+), 0 deletions(-) rename 14th-team5-iOS/App/Sources/Presentation/{JoinFamily => FamilyEntrance}/Dependency/InputFamilyLinkDIContainer.swift (100%) rename 14th-team5-iOS/App/Sources/Presentation/{JoinFamily => FamilyEntrance}/Dependency/JoinFamilyDIContainer.swift (100%) rename 14th-team5-iOS/App/Sources/Presentation/{JoinFamily => FamilyEntrance}/Dependency/JoinedFamilyDIContainer.swift (100%) rename 14th-team5-iOS/App/Sources/Presentation/{JoinFamily => FamilyEntrance}/Reactor/FamilyEntranceReactor.swift (100%) rename 14th-team5-iOS/App/Sources/Presentation/{JoinFamily => FamilyEntrance}/Reactor/InputFamilyLinkReactor.swift (100%) rename 14th-team5-iOS/App/Sources/Presentation/{JoinFamily => FamilyEntrance}/Reactor/JoinFamilyReactor.swift (100%) rename 14th-team5-iOS/App/Sources/Presentation/{JoinFamily => FamilyEntrance}/Strings/JoinFamilyStrings.swift (100%) rename 14th-team5-iOS/App/Sources/Presentation/{JoinFamily => FamilyEntrance}/Strings/inviteFamilyLinkStrings.swift (100%) rename 14th-team5-iOS/App/Sources/Presentation/{JoinFamily => FamilyEntrance}/Strings/joinedFamilyStrings.swift (100%) rename 14th-team5-iOS/App/Sources/Presentation/{JoinFamily => FamilyEntrance}/ViewController/FamilyEntranceViewController.swift (100%) rename 14th-team5-iOS/App/Sources/Presentation/{JoinFamily => FamilyEntrance}/ViewController/InputFamilyLinkViewController.swift (100%) rename 14th-team5-iOS/App/Sources/Presentation/{JoinFamily => FamilyEntrance}/ViewController/JoinFamilyViewController.swift (100%) rename 14th-team5-iOS/App/Sources/Presentation/{JoinFamily => FamilyEntrance}/Views/MakeNewFamilyView.swift (100%) diff --git a/14th-team5-iOS/App/Sources/Presentation/JoinFamily/Dependency/InputFamilyLinkDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Dependency/InputFamilyLinkDIContainer.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/JoinFamily/Dependency/InputFamilyLinkDIContainer.swift rename to 14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Dependency/InputFamilyLinkDIContainer.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/JoinFamily/Dependency/JoinFamilyDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Dependency/JoinFamilyDIContainer.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/JoinFamily/Dependency/JoinFamilyDIContainer.swift rename to 14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Dependency/JoinFamilyDIContainer.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/JoinFamily/Dependency/JoinedFamilyDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Dependency/JoinedFamilyDIContainer.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/JoinFamily/Dependency/JoinedFamilyDIContainer.swift rename to 14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Dependency/JoinedFamilyDIContainer.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/JoinFamily/Reactor/FamilyEntranceReactor.swift b/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Reactor/FamilyEntranceReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/JoinFamily/Reactor/FamilyEntranceReactor.swift rename to 14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Reactor/FamilyEntranceReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/JoinFamily/Reactor/InputFamilyLinkReactor.swift b/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Reactor/InputFamilyLinkReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/JoinFamily/Reactor/InputFamilyLinkReactor.swift rename to 14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Reactor/InputFamilyLinkReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/JoinFamily/Reactor/JoinFamilyReactor.swift b/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Reactor/JoinFamilyReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/JoinFamily/Reactor/JoinFamilyReactor.swift rename to 14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Reactor/JoinFamilyReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/JoinFamily/Strings/JoinFamilyStrings.swift b/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Strings/JoinFamilyStrings.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/JoinFamily/Strings/JoinFamilyStrings.swift rename to 14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Strings/JoinFamilyStrings.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/JoinFamily/Strings/inviteFamilyLinkStrings.swift b/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Strings/inviteFamilyLinkStrings.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/JoinFamily/Strings/inviteFamilyLinkStrings.swift rename to 14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Strings/inviteFamilyLinkStrings.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/JoinFamily/Strings/joinedFamilyStrings.swift b/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Strings/joinedFamilyStrings.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/JoinFamily/Strings/joinedFamilyStrings.swift rename to 14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Strings/joinedFamilyStrings.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/JoinFamily/ViewController/FamilyEntranceViewController.swift b/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/ViewController/FamilyEntranceViewController.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/JoinFamily/ViewController/FamilyEntranceViewController.swift rename to 14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/ViewController/FamilyEntranceViewController.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/JoinFamily/ViewController/InputFamilyLinkViewController.swift b/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/ViewController/InputFamilyLinkViewController.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/JoinFamily/ViewController/InputFamilyLinkViewController.swift rename to 14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/ViewController/InputFamilyLinkViewController.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/JoinFamily/ViewController/JoinFamilyViewController.swift b/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/ViewController/JoinFamilyViewController.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/JoinFamily/ViewController/JoinFamilyViewController.swift rename to 14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/ViewController/JoinFamilyViewController.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/JoinFamily/Views/MakeNewFamilyView.swift b/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Views/MakeNewFamilyView.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/JoinFamily/Views/MakeNewFamilyView.swift rename to 14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Views/MakeNewFamilyView.swift From c88cec97272029f23737c825b6ee993d80501264 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Sun, 11 Aug 2024 22:12:56 +0900 Subject: [PATCH 188/263] =?UTF-8?q?feat:=20BBNavigationBarButton=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#606)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FamilyManagementViewController.swift | 2 +- .../Bibbi/BBCommons/BBButton/BBButton.swift | 2 +- .../BBNavigationBar/BBNavigationBar.swift | 61 +++++---- .../BBNavigationBarButton.swift | 123 ++++++++++++++++++ .../BBNavigationBarReactive.swift | 4 +- 5 files changed, 162 insertions(+), 30 deletions(-) create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBar/BBNavigationBarButton.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/ViewController/FamilyManagementViewController.swift b/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/ViewController/FamilyManagementViewController.swift index 8c6186b7f..3b0726025 100644 --- a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/ViewController/FamilyManagementViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/ViewController/FamilyManagementViewController.swift @@ -238,7 +238,7 @@ public final class FamilyManagementViewController: BBNavigationViewController { + var isHiddenLeftBarButtonMark: Binder { Binder(self.base) { navigationBar, isHidden in - navigationBar.isHiddenLeftBarButtonNewMark = isHidden + navigationBar.isHiddenLeftBarButtonMark = isHidden } } From 6dc30afd64550a94a90b5e7f27cde077f81219dd Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Fri, 16 Aug 2024 19:25:39 +0900 Subject: [PATCH 189/263] =?UTF-8?q?feat:=20BBProgressHUD=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84,=20ButtonToastView=20=EB=B9=8C=EB=93=9C=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EC=88=98=EC=A0=95=20(#610)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BBProgressHUD/BBProgressHUD.swift | 184 ++++++++++++++++++ .../BBProgressHUDAnimation.swift | 44 +++++ .../BBProgressHUDBackground.swift | 17 ++ .../BBProgressHUD/BBProgressHUDCAType.swift | 25 +++ .../BBProgressHUDConfiguration.swift | 68 +++++++ .../BBProgressHUD/BBProgressHUDDelegate.swift | 26 +++ .../BBProgressHUDViewConfiguration.swift | 46 +++++ .../BBProgressHUDSubView.swift | 12 ++ .../ProgressHUDView/BBProgressHUDView.swift | 12 ++ .../Animations/ActivityIndicator.swift | 25 +++ .../Animations/HorizontalDotScaling.swift | 62 ++++++ .../CoreAnimationProgressHUDView.swift | 41 ++++ .../DefaultProgressHUDView.swift | 95 +++++++++ .../LottieProgressHUDView.swift | 8 + .../DefaultToastView/ButtonToastView.swift | 12 +- 15 files changed, 666 insertions(+), 11 deletions(-) create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUD.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUDAnimation.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUDBackground.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUDCAType.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUDConfiguration.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUDDelegate.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUDViewConfiguration.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/ProgressHUDView/BBProgressHUDSubView.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/ProgressHUDView/BBProgressHUDView.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/ProgressHUDView/DefaultProgressHUDView/Animations/ActivityIndicator.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/ProgressHUDView/DefaultProgressHUDView/Animations/HorizontalDotScaling.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/ProgressHUDView/DefaultProgressHUDView/CoreAnimationProgressHUDView.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/ProgressHUDView/DefaultProgressHUDView/DefaultProgressHUDView.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/ProgressHUDView/DefaultProgressHUDView/LottieProgressHUDView.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUD.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUD.swift new file mode 100644 index 000000000..d02c23e12 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUD.swift @@ -0,0 +1,184 @@ +// +// ProgressHUD.swift +// BBProgressHUD +// +// Created by 김건우 on 8/16/24. +// + +import UIKit + +public class BBProgressHUD { + + // MARK: - Properties + + public static var activeProgressHUDs = [BBProgressHUD]() + + public let view: BBProgressHUDView + private var backgroundView: UIView? + + public static var defaultImageTint: UIColor = .bibbiBlack + + public static var multicast = MulticastDelegate() + + public private(set) var config: BBProgressHUDConfiguration + + + // MARK: - ProgressHUD + + public static func lottie() { + + } + + public static func animation( + _ type: BBProgressHUDCAType, + viewConfig: BBProgressHUDViewConfiguration = BBProgressHUDViewConfiguration(), + config: BBProgressHUDConfiguration = BBProgressHUDConfiguration() + ) -> BBProgressHUD { + let view = DefaultProgressHUDView( + child: CoreAnimationProgressHUDView( + of: type, + viewConfig: viewConfig + ), + viewConfig: viewConfig + ) + + return BBProgressHUD(view: view, config: config) + } + + + // MARK: - Show + + public func show( + after delay: TimeInterval = 0, + animated: Bool = true + ) { + if let backgroundView = self.createBackgroundView() { + self.backgroundView = backgroundView + config.view?.addSubview(backgroundView) ?? BBHelper.topController()?.view.addSubview(backgroundView) + } + + config.view?.addSubview(view) ?? BBHelper.topController()?.view.addSubview(view) + view.createView(for: self) + + Self.multicast.invoke { $0.willShowProgressHUD(self) } + + config.enteringAnimation.apply(to: self.view) + let endBackgroundColor = backgroundView?.backgroundColor + backgroundView?.backgroundColor = .clear + UIView.animate( + withDuration: animated ? config.animationTime : 0, + delay: delay, + options: [.curveEaseOut] + ) { + self.config.enteringAnimation.undo(from: self.view) + self.backgroundView?.backgroundColor = endBackgroundColor + } completion: { [self] _ in + Self.multicast.invoke { $0.didShowProgressHUD(self) } + + if !config.allowProgressHUDOverlap { + closeOverlappedProgressHUDs() + } + Self.activeProgressHUDs.append(self) + } + } + + + // MARK: - Close + + public func close( + animated: Bool = true, + completion: (() -> Void)? = nil + ) { + Self.multicast.invoke { $0.willHideProgressHUD(self) } + + UIView.animate( + withDuration: config.animationTime, + delay: 0, + options: [.curveEaseOut] + ) { + if animated { + self.config.exitingAnimation.apply(to: self.view) + } + self.backgroundView?.backgroundColor = .clear + } completion: { [self] _ in + self.backgroundView?.removeFromSuperview() + self.view.removeFromSuperview() + if let index = Self.activeProgressHUDs.firstIndex(where: { $0 === self }) { + Self.activeProgressHUDs.remove(at: index) + } + completion?() + Self.multicast.invoke { $0.didHideProgressHUD(self) } + } + + } + + public static func close( + _ closeToProgressHud: BBProgressHUD, + animated: Bool = true + ) { + if let index = Self.activeProgressHUDs.firstIndex(where: { $0 === closeToProgressHud }) { + Self.activeProgressHUDs[index].close(animated: animated) + } + } + + public static func closeAll(animated: Bool = true) { + Self.activeProgressHUDs.forEach { + $0.close(animated: animated) + } + } + + private func closeOverlappedProgressHUDs() { + Self.activeProgressHUDs.forEach { + $0.close(animated: false) + } + } + + + + + + // MARK: - Intializer + + public required init( + view: BBProgressHUDView, + config: BBProgressHUDConfiguration + ) { + self.view = view + self.config = config + } + +} + + +// MARK: - Extensions + +extension BBProgressHUD { + + public func addDelegate(_ delegate: BBProgressHUDDelegate) { + Self.multicast.add(delegate) + } + + private func createBackgroundView() -> UIView? { + switch config.background { + case .none: + return nil + + case let .color(color): + let backgroundView = UIView(frame: config.view?.frame ?? BBHelper.topController()?.view.frame ?? .zero) + backgroundView.backgroundColor = color + backgroundView.layer.zPosition = 998 + return backgroundView + } + } + + + +} + +extension BBProgressHUD: Equatable { + + public static func == (lhs: BBProgressHUD, rhs: BBProgressHUD) -> Bool { + ObjectIdentifier(lhs) == ObjectIdentifier(rhs) + } + +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUDAnimation.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUDAnimation.swift new file mode 100644 index 000000000..6c18ff397 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUDAnimation.swift @@ -0,0 +1,44 @@ +// +// BBProgressHUDAnimation.swift +// BBProgressHUD +// +// Created by 김건우 on 8/16/24. +// + +import UIKit + +extension BBProgressHUD { + + public enum Animation { + case fade(alpha: CGFloat) + case fadeAndScale(scaleX: CGFloat, scaleY: CGFloat, alpha: CGFloat) + case custom(transformation: CGAffineTransform) + case `default` + + func apply(to view: UIView) { + switch self { + case let .fade(alpha): + view.alpha = alpha + + case let .fadeAndScale(scaleX, scaleY, alpha): + view.alpha = alpha + view.transform = CGAffineTransform(scaleX: scaleX, y: scaleY) + + case let .custom(transformation): + view.transform = transformation + + case .default: + break + } + } + + func undo(from view: UIView) { + switch self { + @unknown default: + view.alpha = 1 + view.transform = .identity + } + } + } + +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUDBackground.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUDBackground.swift new file mode 100644 index 000000000..8424ed663 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUDBackground.swift @@ -0,0 +1,17 @@ +// +// BBProgressHUDBackground.swift +// BBProgressHUD +// +// Created by 김건우 on 8/16/24. +// + +import UIKit + +extension BBProgressHUD { + + public enum Background { + case none + case color(color: UIColor = defaultImageTint.withAlphaComponent(0.25)) + } + +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUDCAType.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUDCAType.swift new file mode 100644 index 000000000..0c56917d8 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUDCAType.swift @@ -0,0 +1,25 @@ +// +// BBProgressHUDStyle.swift +// BBProgressHUD +// +// Created by 김건우 on 8/16/24. +// + +import UIKit + +// MARK: - Typealias + +public typealias BBProgressHUDCAType = BBProgressHUD.CAType + + +// MARK: - Extensions + +extension BBProgressHUD { + + public enum CAType { + case spinner + case circleRippleMultiple + case horizontalDotScaling + } + +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUDConfiguration.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUDConfiguration.swift new file mode 100644 index 000000000..064c14c71 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUDConfiguration.swift @@ -0,0 +1,68 @@ +// +// BBProgressHUDConfiguration.swift +// BBProgressHUD +// +// Created by 김건우 on 8/16/24. +// + +import UIKit + +public struct BBProgressHUDConfiguration { + + // MARK: - Properties + + public let animationTime: TimeInterval + public let enteringAnimation: BBProgressHUD.Animation + public let exitingAnimation: BBProgressHUD.Animation + public let background: BBProgressHUD.Background + + public let view: UIView? + + public let allowProgressHUDOverlap: Bool + + + // MARK: - Initalizer + + public init( + animationTime: TimeInterval = 0.2, + enteringAnimation: BBProgressHUD.Animation = .default, + exitingAnimation: BBProgressHUD.Animation = .default, + background: BBProgressHUD.Background = .none, + attachedTo view: UIView? = nil, + allowProgressHUDOverlap: Bool = false + ) { + self.animationTime = animationTime + self.enteringAnimation = enteringAnimation.isDefault ? Self.defaultEnteringAnimation() : enteringAnimation + self.exitingAnimation = exitingAnimation.isDefault ? Self.defaultExitingAnimation() : exitingAnimation + self.background = background + self.view = view + self.allowProgressHUDOverlap = allowProgressHUDOverlap + } + +} + + +// MARK: - Extensions + +extension BBProgressHUDConfiguration { + + private static func defaultEnteringAnimation() -> BBProgressHUD.Animation { + return .fadeAndScale(scaleX: 0.9, scaleY: 0.9, alpha: 0) + } + + private static func defaultExitingAnimation() -> BBProgressHUD.Animation { + return .fade(alpha: 0) + } + +} + +fileprivate extension BBProgressHUD.Animation { + + var isDefault: Bool { + if case .default = self { + return true + } + return false + } + +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUDDelegate.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUDDelegate.swift new file mode 100644 index 000000000..62f818675 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUDDelegate.swift @@ -0,0 +1,26 @@ +// +// BBProgressHUDDelegate.swift +// BBProgressHUD +// +// Created by 김건우 on 8/16/24. +// + +import Foundation + +public protocol BBProgressHUDDelegate { + + func willShowProgressHUD(_ progress: BBProgressHUD) + func didShowProgressHUD(_ progress: BBProgressHUD) + func willHideProgressHUD(_ progress: BBProgressHUD) + func didHideProgressHUD(_ progess: BBProgressHUD) + +} + +extension BBProgressHUDDelegate { + + func willShowProgressHUD(_ progress: BBProgressHUD) { } + func didShowProgressHUD(_ progress: BBProgressHUD) { } + func willHideProgressHUD(_ progress: BBProgressHUD) { } + func didHideProgressHUd(_ progress: BBProgressHUD) { } + +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUDViewConfiguration.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUDViewConfiguration.swift new file mode 100644 index 000000000..9ba899273 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUDViewConfiguration.swift @@ -0,0 +1,46 @@ +// +// BBProgressHUDViewConfiguration.swift +// BBProgressHUD +// +// Created by 김건우 on 8/16/24. +// + +import UIKit + +public struct BBProgressHUDViewConfiguration { + + // MARK: - Properties + + public let minWidth: CGFloat + public let minHeight: CGFloat + + public let xOffset: CGFloat + public let yOffset: CGFloat + + public let animationColor: UIColor? + public let backgroundColor: UIColor? + + public let cornerRadius: CGFloat? + + + // MARK: - Intializer + + public init( + minWidth: CGFloat = 130, + minHeight: CGFloat = 130, + offsetFromCenterX xOffset: CGFloat = 0, + offsetFromCenterY yOffset: CGFloat = 0, + animationColor: UIColor? = .mainYellow, + backgroundColor: UIColor? = nil, + cornerRadius: CGFloat? = 30 + ) { + self.minWidth = minWidth + self.minHeight = minHeight + self.xOffset = xOffset + self.yOffset = yOffset + self.animationColor = animationColor + self.backgroundColor = backgroundColor + self.cornerRadius = cornerRadius + } + +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/ProgressHUDView/BBProgressHUDSubView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/ProgressHUDView/BBProgressHUDSubView.swift new file mode 100644 index 000000000..b99a446df --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/ProgressHUDView/BBProgressHUDSubView.swift @@ -0,0 +1,12 @@ +// +// BBProgressHUDStackView.swift +// BBProgressHUD +// +// Created by 김건우 on 8/16/24. +// + +import UIKit + +public protocol BBProgressHUDSubView: UIView { + func applyAnimation(for progressHud: BBProgressHUD?) +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/ProgressHUDView/BBProgressHUDView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/ProgressHUDView/BBProgressHUDView.swift new file mode 100644 index 000000000..85b8d0f55 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/ProgressHUDView/BBProgressHUDView.swift @@ -0,0 +1,12 @@ +// +// ProgressHUDView.swift +// BBProgressHUD +// +// Created by 김건우 on 8/16/24. +// + +import UIKit + +public protocol BBProgressHUDView: UIView { + func createView(for progress: BBProgressHUD) +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/ProgressHUDView/DefaultProgressHUDView/Animations/ActivityIndicator.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/ProgressHUDView/DefaultProgressHUDView/Animations/ActivityIndicator.swift new file mode 100644 index 000000000..ae8ae09f2 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/ProgressHUDView/DefaultProgressHUDView/Animations/ActivityIndicator.swift @@ -0,0 +1,25 @@ +// +// ActivityIndicator.swift +// BBProgressHUD +// +// Created by 김건우 on 8/16/24. +// + +import UIKit + +extension CoreAnimationProgressHUDView { + + func animationActivityIndicator(for view: UIView) { + + let spinner = UIActivityIndicatorView(style: .large) + let scale = view.frame.size.width / spinner.frame.size.width / 2 + spinner.transform = CGAffineTransform(scaleX: scale, y: scale).translatedBy(x: 1, y: 0) + spinner.frame = view.bounds + spinner.color = viewConfig.animationColor + spinner.hidesWhenStopped = true + spinner.startAnimating() + view.addSubview(spinner) + + } + +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/ProgressHUDView/DefaultProgressHUDView/Animations/HorizontalDotScaling.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/ProgressHUDView/DefaultProgressHUDView/Animations/HorizontalDotScaling.swift new file mode 100644 index 000000000..6411b8f5f --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/ProgressHUDView/DefaultProgressHUDView/Animations/HorizontalDotScaling.swift @@ -0,0 +1,62 @@ +// +// HorizontalDotScaling.swift +// BBProgressHUD +// +// Created by 김건우 on 8/16/24. +// + +import UIKit + +extension CoreAnimationProgressHUDView { + + func animationHorizontalDotScaling(for view: UIView) { + + let width = view.frame.size.width + let height = view.frame.size.height + + let spacing = 3.0 + let radius = (width - spacing * 2) / 3 + let center = CGPoint(x: radius / 2, y: radius / 2) + let positionY = (height - radius) / 2 + + let beginTime = CACurrentMediaTime() + let beginTimes = [0.36, 0.24, 0.12] + let timingFunction = CAMediaTimingFunction(controlPoints: 0.2, 0.68, 0.18, 1.08) + + let animation = CAKeyframeAnimation(keyPath: "transform.scale") + animation.keyTimes = [0, 0.5, 1] + animation.timingFunctions = [timingFunction, timingFunction] + animation.values = [1, 0.3, 1] + animation.duration = 1 + animation.repeatCount = .infinity + animation.fillMode = .backwards + animation.isRemovedOnCompletion = false + + let path = UIBezierPath( + arcCenter: center, + radius: radius / 2, + startAngle: 0, + endAngle: 2 * .pi, + clockwise: false + ) + + for i in 0..<3 { + let layer = CAShapeLayer() + layer.frame = CGRect( + x: (radius + spacing) * CGFloat(i), + y: positionY, + width: radius, + height: radius + ) + layer.path = path.cgPath + layer.fillColor = viewConfig.animationColor?.cgColor + + animation.beginTime = beginTime + beginTimes[i] + + layer.add(animation, forKey: "animation") + view.layer.addSublayer(layer) + } + + } + +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/ProgressHUDView/DefaultProgressHUDView/CoreAnimationProgressHUDView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/ProgressHUDView/DefaultProgressHUDView/CoreAnimationProgressHUDView.swift new file mode 100644 index 000000000..ade7f7d0e --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/ProgressHUDView/DefaultProgressHUDView/CoreAnimationProgressHUDView.swift @@ -0,0 +1,41 @@ +// +// CAProgressView.swift +// BBProgressHUD +// +// Created by 김건우 on 8/16/24. +// + +import UIKit + +public class CoreAnimationProgressHUDView: UIView, BBProgressHUDSubView { + + // MARK: - Properties + + public var viewConfig: BBProgressHUDViewConfiguration + + private var type: BBProgressHUDCAType + + + // MARK: - Intializer + + public init( + of type: BBProgressHUDCAType, + viewConfig: BBProgressHUDViewConfiguration + ) { + self.type = type + self.viewConfig = viewConfig + super.init(frame: .zero) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + // MARK: - Helpers + + public func applyAnimation(for progressHud: BBProgressHUD?) { + if type == .spinner { animationActivityIndicator(for: self) } + } + +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/ProgressHUDView/DefaultProgressHUDView/DefaultProgressHUDView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/ProgressHUDView/DefaultProgressHUDView/DefaultProgressHUDView.swift new file mode 100644 index 000000000..9eb219bb3 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/ProgressHUDView/DefaultProgressHUDView/DefaultProgressHUDView.swift @@ -0,0 +1,95 @@ +// +// DefaultProgressView.swift +// BBProgressHUD +// +// Created by 김건우 on 8/16/24. +// + +import UIKit + +public class DefaultProgressHUDView: UIView, BBProgressHUDView { + + // MARK: - Views + + private let child: BBProgressHUDSubView + + // MARK: - Properties + + private weak var progressHud: BBProgressHUD? + private var viewConfig: BBProgressHUDViewConfiguration + + // MARK: - Intializer + + public init( + child: BBProgressHUDSubView, + viewConfig: BBProgressHUDViewConfiguration + ) { + self.child = child + self.viewConfig = viewConfig + super.init(frame: .zero) + + addSubview(child) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + // MARK: - Helpers + + public func createView(for progressHud: BBProgressHUD) { + self.progressHud = progressHud + + setupConstraints() + setupAttributes() + } + + private func setupConstraints() { + guard let superview = superview else { return } + + translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + self.widthAnchor.constraint(greaterThanOrEqualToConstant: viewConfig.minWidth), + self.heightAnchor.constraint(greaterThanOrEqualToConstant: viewConfig.minHeight), + self.centerXAnchor.constraint(equalTo: superview.centerXAnchor, constant: viewConfig.xOffset), + self.centerYAnchor.constraint(equalTo: superview.centerYAnchor, constant: viewConfig.yOffset) + ]) + + child.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + child.widthAnchor.constraint(equalTo: self.widthAnchor, multiplier: 0.75), + child.heightAnchor.constraint(equalTo: self.heightAnchor, multiplier: 0.75), + child.centerXAnchor.constraint(equalTo: self.centerXAnchor), + child.centerYAnchor.constraint(equalTo: self.centerYAnchor) + ]) + + } + + private func setupAttributes() { + layoutIfNeeded() + clipsToBounds = true + layer.zPosition = 999 + layer.cornerCurve = .continuous + layer.cornerRadius = viewConfig.cornerRadius ?? 0 + backgroundColor = viewConfig.backgroundColor + + addShadow() + + child.applyAnimation(for: progressHud) + } + + private func addShadow() { + layer.masksToBounds = false + layer.shadowOffset = CGSize(width: 0, height: 4) + layer.shadowColor = UIColor.black.withAlphaComponent(0.08).cgColor + layer.shadowOpacity = 1 + layer.shadowRadius = 8 + } + + public override func removeFromSuperview() { + super.removeFromSuperview() + self.progressHud = nil + } + +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/ProgressHUDView/DefaultProgressHUDView/LottieProgressHUDView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/ProgressHUDView/DefaultProgressHUDView/LottieProgressHUDView.swift new file mode 100644 index 000000000..a628358fe --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/ProgressHUDView/DefaultProgressHUDView/LottieProgressHUDView.swift @@ -0,0 +1,8 @@ +// +// LottieProgressView.swift +// BBProgressHUD +// +// Created by 김건우 on 8/16/24. +// + +import Foundation diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/ButtonToastView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/ButtonToastView.swift index f572075a9..5430a569d 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/ButtonToastView.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/ButtonToastView.swift @@ -33,8 +33,8 @@ final public class ButtonToastView: UIStackView, BBToastStackView { buttonTint: UIColor? = nil, viewConfig: BBToastViewConfiguration ) { - self.toast = nil self.buttonAction = nil + self.toast = nil self.viewConfig = viewConfig super.init(frame: .zero) @@ -85,16 +85,6 @@ final public class ButtonToastView: UIStackView, BBToastStackView { spacing = 6 alignment = .center distribution = .fillProportionally - - setupAttributes() - } - - private func setupAttributes() { - button.addTarget( - self, - action: #selector(didTapToastButton), - for: .touchUpInside - ) } } From c36459eb8fa3c4073649389b9e3b1af3e58f3797 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Fri, 16 Aug 2024 20:29:52 +0900 Subject: [PATCH 190/263] =?UTF-8?q?feat:=20BBLottieView=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#610)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BBCommons/BBLottieView/BBLottieView.swift | 72 +++++++++++++++++++ .../BBLottieView/BBLottieViewKind.swift | 31 ++++++++ .../BBProgressHUDViewConfiguration.swift | 2 +- .../Lottie/BibbiLoadingView.swift | 0 .../Lottie/BibbiLottileIndicator.swift | 0 .../Lottie/LottieType.swift | 0 .../Lottie/LottieView.swift | 0 7 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBLottieView/BBLottieView.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBLottieView/BBLottieViewKind.swift rename 14th-team5-iOS/Core/Sources/{Bibbi/BBCommons => Trash}/Lottie/BibbiLoadingView.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi/BBCommons => Trash}/Lottie/BibbiLottileIndicator.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi/BBCommons => Trash}/Lottie/LottieType.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi/BBCommons => Trash}/Lottie/LottieView.swift (100%) diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBLottieView/BBLottieView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBLottieView/BBLottieView.swift new file mode 100644 index 000000000..6fa519115 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBLottieView/BBLottieView.swift @@ -0,0 +1,72 @@ +// +// BBLottieView.swift +// Core +// +// Created by 김건우 on 8/16/24. +// + +import UIKit + +import Lottie + +public class BBLottieView: LottieAnimationView { + + // MARK: - Intializer + + public init( + of kind: BBLottieViewKind + ) { + super.init(frame: .zero) + animation = LottieAnimation.named(kind.name) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + // MARK: - Helpers + + /// Plays the animation from a start frame to an end frame in the animation’s framerate. + public func startAnimating( + fromFrame startFrame: AnimationFrameTime? = nil, + toFrame endFrame: AnimationFrameTime, + loopMode: LottieLoopMode? = .loop, + completion: LottieCompletionBlock? = nil + ) { + play( + fromFrame: startFrame, + toFrame: endFrame, + loopMode: loopMode, + completion: completion + ) + } + + /// Plays the animation from a progress (0-1) to a progress (0-1). + public func startAnimating( + fromProgress startTime: AnimationProgressTime? = nil, + toProgress endTime: AnimationProgressTime = 1, + loopMode: LottieLoopMode? = .loop, + completion: LottieCompletionBlock? = nil + ) { + play( + fromProgress: startTime, + toProgress: endTime, + loopMode: loopMode, + completion: completion + ) + } + + /// Pauses the animation in its current state. + /// The completion closure will be called with false + public func pauseAnimating() { + pause() + } + + /// Stops the animation and resets the view to its start frame. + /// The completion closure will be called with false + public func stopAnimating() { + stop() + } + +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBLottieView/BBLottieViewKind.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBLottieView/BBLottieViewKind.swift new file mode 100644 index 000000000..d4d0132ef --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBLottieView/BBLottieViewKind.swift @@ -0,0 +1,31 @@ +// +// BBLottieViewType.swift +// Core +// +// Created by 김건우 on 8/16/24. +// + +import UIKit + +// MARK: - Typealias + +public typealias BBLottieViewKind = BBLottieView.Kind + + +// MARK: - Extensions + +extension BBLottieView { + + public enum `Kind`: String { + case fire + case airplane + + var name: String { + switch self { + case .fire: return "fire" + case .airplane: return "loading" + } + } + } + +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUDViewConfiguration.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUDViewConfiguration.swift index 9ba899273..85ab73514 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUDViewConfiguration.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUDViewConfiguration.swift @@ -31,7 +31,7 @@ public struct BBProgressHUDViewConfiguration { offsetFromCenterX xOffset: CGFloat = 0, offsetFromCenterY yOffset: CGFloat = 0, animationColor: UIColor? = .mainYellow, - backgroundColor: UIColor? = nil, + backgroundColor: UIColor? = .gray900, cornerRadius: CGFloat? = 30 ) { self.minWidth = minWidth diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/Lottie/BibbiLoadingView.swift b/14th-team5-iOS/Core/Sources/Trash/Lottie/BibbiLoadingView.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/Lottie/BibbiLoadingView.swift rename to 14th-team5-iOS/Core/Sources/Trash/Lottie/BibbiLoadingView.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/Lottie/BibbiLottileIndicator.swift b/14th-team5-iOS/Core/Sources/Trash/Lottie/BibbiLottileIndicator.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/Lottie/BibbiLottileIndicator.swift rename to 14th-team5-iOS/Core/Sources/Trash/Lottie/BibbiLottileIndicator.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/Lottie/LottieType.swift b/14th-team5-iOS/Core/Sources/Trash/Lottie/LottieType.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/Lottie/LottieType.swift rename to 14th-team5-iOS/Core/Sources/Trash/Lottie/LottieType.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/Lottie/LottieView.swift b/14th-team5-iOS/Core/Sources/Trash/Lottie/LottieView.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/Lottie/LottieView.swift rename to 14th-team5-iOS/Core/Sources/Trash/Lottie/LottieView.swift From 4131764aed637a984f1209e4f1dbf4ff13777311 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Fri, 16 Aug 2024 21:08:47 +0900 Subject: [PATCH 191/263] =?UTF-8?q?feat:=20LottieProgressHUDView=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#610)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ViewControllers/MainViewController.swift | 6 +++ .../Bibbi/BBCommons/BBLottie/BBLottie.swift | 14 +++++ .../BBLottieKind.swift} | 4 +- .../BBLottieView/BBLottieView.swift | 6 ++- .../BBProgressHUD/BBProgressHUD.swift | 14 ++++- .../BBProgressHUDViewConfiguration.swift | 29 +++++++++++ .../DefaultProgressHUDView.swift | 2 +- .../LottieProgressHUDView.swift | 52 ++++++++++++++++++- 8 files changed, 121 insertions(+), 6 deletions(-) create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBLottie/BBLottie.swift rename 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/{BBLottieView/BBLottieViewKind.swift => BBLottie/BBLottieKind.swift} (83%) rename 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/{ => BBLottie}/BBLottieView/BBLottieView.swift (92%) 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 871a15082..4e00aebd0 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift @@ -35,6 +35,12 @@ final class MainViewController: BaseViewController, UICollectio UserDefaults.standard.inviteCode = nil } + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + BBProgressHUD.lottie(.airplane).show() + } + override func bind(reactor: MainViewReactor) { super.bind(reactor: reactor) bindInput(reactor: reactor) diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBLottie/BBLottie.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBLottie/BBLottie.swift new file mode 100644 index 000000000..c20b9ed19 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBLottie/BBLottie.swift @@ -0,0 +1,14 @@ +// +// BBLottie.swift +// Core +// +// Created by 김건우 on 8/16/24. +// + +import Foundation + +public class BBLottie { + + // TODO: - BBLottie.show(.fire) 등 구현하기 + +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBLottieView/BBLottieViewKind.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBLottie/BBLottieKind.swift similarity index 83% rename from 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBLottieView/BBLottieViewKind.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBLottie/BBLottieKind.swift index d4d0132ef..c9ccf5e10 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBLottieView/BBLottieViewKind.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBLottie/BBLottieKind.swift @@ -9,12 +9,12 @@ import UIKit // MARK: - Typealias -public typealias BBLottieViewKind = BBLottieView.Kind +public typealias BBLottieKind = BBLottie.Kind // MARK: - Extensions -extension BBLottieView { +extension BBLottie { public enum `Kind`: String { case fire diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBLottieView/BBLottieView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBLottie/BBLottieView/BBLottieView.swift similarity index 92% rename from 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBLottieView/BBLottieView.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBLottie/BBLottieView/BBLottieView.swift index 6fa519115..4a3d58bc7 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBLottieView/BBLottieView.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBLottie/BBLottieView/BBLottieView.swift @@ -14,7 +14,7 @@ public class BBLottieView: LottieAnimationView { // MARK: - Intializer public init( - of kind: BBLottieViewKind + of kind: BBLottieKind ) { super.init(frame: .zero) animation = LottieAnimation.named(kind.name) @@ -27,6 +27,10 @@ public class BBLottieView: LottieAnimationView { // MARK: - Helpers + public func startAnimating(completion: LottieCompletionBlock? = nil) { + play(completion: completion) + } + /// Plays the animation from a start frame to an end frame in the animation’s framerate. public func startAnimating( fromFrame startFrame: AnimationFrameTime? = nil, diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUD.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUD.swift index d02c23e12..c8891feac 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUD.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUD.swift @@ -25,8 +25,20 @@ public class BBProgressHUD { // MARK: - ProgressHUD - public static func lottie() { + public static func lottie( + _ kind: BBLottieKind, + viewConfig: BBProgressHUDViewConfiguration = BBProgressHUDViewConfiguration(), + config: BBProgressHUDConfiguration = BBProgressHUDConfiguration() + ) -> BBProgressHUD { + let view = DefaultProgressHUDView( + child: LottieProgressHUDView( + of: kind, + viewConfig: viewConfig + ), + viewConfig: viewConfig + ) + return BBProgressHUD(view: view, config: config) } public static func animation( diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUDViewConfiguration.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUDViewConfiguration.swift index 85ab73514..94d368a8c 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUDViewConfiguration.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUDViewConfiguration.swift @@ -20,6 +20,11 @@ public struct BBProgressHUDViewConfiguration { public let animationColor: UIColor? public let backgroundColor: UIColor? + public let lottieFromProgress: CGFloat + public let lottieToProgress: CGFloat + public let lottieAnimationSpeed: CGFloat + public let lottieAnimationScale: CGFloat + public let cornerRadius: CGFloat? @@ -32,6 +37,10 @@ public struct BBProgressHUDViewConfiguration { offsetFromCenterY yOffset: CGFloat = 0, animationColor: UIColor? = .mainYellow, backgroundColor: UIColor? = .gray900, + lottieFromProgress fromProgress: CGFloat = 0.15, + lottieToProgress toProgress: CGFloat = 0.95, + lottieAnimationSpeed speed: CGFloat = 1.35, + lottieAnimationScale scale: CGFloat = 0.8, cornerRadius: CGFloat? = 30 ) { self.minWidth = minWidth @@ -40,7 +49,27 @@ public struct BBProgressHUDViewConfiguration { self.yOffset = yOffset self.animationColor = animationColor self.backgroundColor = backgroundColor + self.lottieFromProgress = fromProgress + self.lottieToProgress = toProgress + self.lottieAnimationSpeed = speed + self.lottieAnimationScale = scale self.cornerRadius = cornerRadius } } + + +// MARK: - Extensions + +public extension BBProgressHUDViewConfiguration { + + static var airplaneLottie: Self = { + Self( + lottieFromProgress: 0.15, + lottieToProgress: 0.95, + lottieAnimationSpeed: 1.35, + lottieAnimationScale: 0.8 + ) + }() + +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/ProgressHUDView/DefaultProgressHUDView/DefaultProgressHUDView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/ProgressHUDView/DefaultProgressHUDView/DefaultProgressHUDView.swift index 9eb219bb3..92910be11 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/ProgressHUDView/DefaultProgressHUDView/DefaultProgressHUDView.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/ProgressHUDView/DefaultProgressHUDView/DefaultProgressHUDView.swift @@ -75,7 +75,7 @@ public class DefaultProgressHUDView: UIView, BBProgressHUDView { backgroundColor = viewConfig.backgroundColor addShadow() - + child.applyAnimation(for: progressHud) } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/ProgressHUDView/DefaultProgressHUDView/LottieProgressHUDView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/ProgressHUDView/DefaultProgressHUDView/LottieProgressHUDView.swift index a628358fe..6464e52c3 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/ProgressHUDView/DefaultProgressHUDView/LottieProgressHUDView.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/ProgressHUDView/DefaultProgressHUDView/LottieProgressHUDView.swift @@ -5,4 +5,54 @@ // Created by 김건우 on 8/16/24. // -import Foundation +import UIKit + +public class LottieProgressHUDView: UIView, BBProgressHUDSubView { + + // MARK: - Properties + + private let kind: BBLottieKind + + public let viewConfig: BBProgressHUDViewConfiguration + + // MARK: - Intializer + + public init( + of kind: BBLottieKind, + viewConfig: BBProgressHUDViewConfiguration + ) { + self.viewConfig = viewConfig + self.kind = kind + + super.init(frame: .zero) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + // MARK: - Helpers + + public func applyAnimation(for progressHud: BBProgressHUD?) { + + let lottieView = BBLottieView(of: kind) + addSubview(lottieView) + + lottieView.frame = self.bounds + lottieView.contentMode = .scaleAspectFit + lottieView.animationSpeed = viewConfig.lottieAnimationSpeed + lottieView.transform = CGAffineTransform( + scaleX: viewConfig.lottieAnimationScale, + y: viewConfig.lottieAnimationScale + ) + + lottieView.startAnimating( + fromProgress: viewConfig.lottieFromProgress, + toProgress: viewConfig.lottieToProgress, + loopMode: .loop + ) + + } + +} From 4a461eef0b4c0023a4690c9cb8d11f724502b679 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Fri, 16 Aug 2024 21:22:14 +0900 Subject: [PATCH 192/263] =?UTF-8?q?feat:=20BBProgressHUDStyle=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#610)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ViewControllers/MainViewController.swift | 2 -- .../BBProgressHUD/BBProgressHUD.swift | 17 ++++++++++++++ .../BBProgressHUD/BBProgressHUDStyle.swift | 23 +++++++++++++++++++ 3 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUDStyle.swift 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 4e00aebd0..57c6e181e 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift @@ -37,8 +37,6 @@ final class MainViewController: BaseViewController, UICollectio override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - - BBProgressHUD.lottie(.airplane).show() } override func bind(reactor: MainViewReactor) { diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUD.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUD.swift index c8891feac..699333c9e 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUD.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUD.swift @@ -57,6 +57,23 @@ public class BBProgressHUD { return BBProgressHUD(view: view, config: config) } + public static func style( + _ style: BBProgressHUDStyle, + config: BBProgressHUDConfiguration = BBProgressHUDConfiguration() + ) -> BBProgressHUD { + switch style { + case .airplane: + let view = DefaultProgressHUDView( + child: LottieProgressHUDView( + of: .airplane, + viewConfig: .airplaneLottie + ), + viewConfig: .airplaneLottie + ) + return BBProgressHUD(view: view, config: config) + } + } + // MARK: - Show diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUDStyle.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUDStyle.swift new file mode 100644 index 000000000..ac7629024 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUDStyle.swift @@ -0,0 +1,23 @@ +// +// BBProgressHUDStyle.swift +// Core +// +// Created by 김건우 on 8/16/24. +// + +import Foundation + +// MARK: - Typealias + +public typealias BBProgressHUDStyle = BBProgressHUD.Style + + +// MARK: - Extensions + +extension BBProgressHUD { + + public enum Style { + case airplane + } + +} From 104d2e4f76d443eb80b75e850dac2dff2c6cfa7b Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Sat, 17 Aug 2024 23:36:56 +0900 Subject: [PATCH 193/263] =?UTF-8?q?docs:=20=EB=AC=B8=EC=84=9C=20=EC=A3=BC?= =?UTF-8?q?=EC=84=9D=20=EC=9E=91=EC=84=B1=20(#610)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BBProgressHUD/BBProgressHUD.swift | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUD.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUD.swift index 699333c9e..2256d5a70 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUD.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUD.swift @@ -25,6 +25,12 @@ public class BBProgressHUD { // MARK: - ProgressHUD + /// 로띠가 포함된 ProgressHUD를 생성합니다. + /// - Parameters: + /// - kind: 로띠 종류 + /// - viewConfig: ProgressHUDView 설정값 + /// - config: ProgressHUD 설정값 + /// - Returns: BBProgressHUD public static func lottie( _ kind: BBLottieKind, viewConfig: BBProgressHUDViewConfiguration = BBProgressHUDViewConfiguration(), @@ -41,6 +47,12 @@ public class BBProgressHUD { return BBProgressHUD(view: view, config: config) } + /// 코어 애니메이션이 포함된 ProgressHUD를 생성합니다. + /// - Parameters: + /// - type: 코어 애니메이션 타입 + /// - viewConfig: ProgressHUDView 설정값 + /// - config: ProgressHUD 설정값 + /// - Returns: BBProgressHUD public static func animation( _ type: BBProgressHUDCAType, viewConfig: BBProgressHUDViewConfiguration = BBProgressHUDViewConfiguration(), @@ -57,6 +69,11 @@ public class BBProgressHUD { return BBProgressHUD(view: view, config: config) } + /// 정해진 Style의 ProgressHUD를 생성합니다. + /// - Parameters: + /// - style: 스타일 + /// - config: ProgressHUD 설정값 + /// - Returns: BBProgressHUD public static func style( _ style: BBProgressHUDStyle, config: BBProgressHUDConfiguration = BBProgressHUDConfiguration() @@ -74,6 +91,19 @@ public class BBProgressHUD { } } + public static func custom( + _ child: BBProgressHUDSubView, + viewConfig: BBProgressHUDViewConfiguration = BBProgressHUDViewConfiguration(), + config: BBProgressHUDConfiguration = BBProgressHUDConfiguration() + ) -> BBProgressHUD { + let view = DefaultProgressHUDView( + child: child, + viewConfig: viewConfig + ) + + return BBProgressHUD(view: view, config: config) + } + // MARK: - Show From 51420f8f5a049e1e5466fbc29e7c3ffce1d5c9a0 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Sun, 18 Aug 2024 00:01:23 +0900 Subject: [PATCH 194/263] =?UTF-8?q?feat:=20=EA=B5=AC=20LottieView=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20Deprecated=20=EC=B2=98=EB=A6=AC=20(#610)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 14th-team5-iOS/Core/Sources/Trash/BBRx/RxUtils.swift | 6 ++++++ .../Core/Sources/Trash/Lottie/BibbiLoadingView.swift | 2 +- .../Core/Sources/Trash/Lottie/BibbiLottileIndicator.swift | 1 + 14th-team5-iOS/Core/Sources/Trash/Lottie/LottieType.swift | 1 + 14th-team5-iOS/Core/Sources/Trash/Lottie/LottieView.swift | 1 + 14th-team5-iOS/Core/Sources/Utilities/Haptic.swift | 2 ++ 6 files changed, 12 insertions(+), 1 deletion(-) diff --git a/14th-team5-iOS/Core/Sources/Trash/BBRx/RxUtils.swift b/14th-team5-iOS/Core/Sources/Trash/BBRx/RxUtils.swift index 5fed12199..8505079b8 100644 --- a/14th-team5-iOS/Core/Sources/Trash/BBRx/RxUtils.swift +++ b/14th-team5-iOS/Core/Sources/Trash/BBRx/RxUtils.swift @@ -9,7 +9,9 @@ import Foundation import RxSwift +@available(*, deprecated, renamed: "RxInterval") public enum RxConst { + static public var milliseconds100Interval: RxTimeInterval { return .milliseconds(100) } @@ -17,9 +19,12 @@ public enum RxConst { static public var milliseconds300Interval: RxTimeInterval { return .milliseconds(300) } + } +@available(*, deprecated, renamed: "RxSchedulers") public enum RxSchedulers { + public static let main = { MainScheduler.instance }() @@ -55,4 +60,5 @@ public enum RxSchedulers { public static let userInteractive = { ConcurrentDispatchQueueScheduler(queue: DispatchQueue(label: "userInteractive", qos: .userInteractive)) }() + } diff --git a/14th-team5-iOS/Core/Sources/Trash/Lottie/BibbiLoadingView.swift b/14th-team5-iOS/Core/Sources/Trash/Lottie/BibbiLoadingView.swift index f1f141e2e..44a99bb7e 100644 --- a/14th-team5-iOS/Core/Sources/Trash/Lottie/BibbiLoadingView.swift +++ b/14th-team5-iOS/Core/Sources/Trash/Lottie/BibbiLoadingView.swift @@ -9,7 +9,7 @@ import UIKit import Lottie - +@available(*, deprecated, renamed: "BBLottieView") public final class BibbiLoadingView: UIView { public let loadingView: LottieAnimationView = LottieAnimationView() diff --git a/14th-team5-iOS/Core/Sources/Trash/Lottie/BibbiLottileIndicator.swift b/14th-team5-iOS/Core/Sources/Trash/Lottie/BibbiLottileIndicator.swift index 4f55ee268..3b1107be9 100644 --- a/14th-team5-iOS/Core/Sources/Trash/Lottie/BibbiLottileIndicator.swift +++ b/14th-team5-iOS/Core/Sources/Trash/Lottie/BibbiLottileIndicator.swift @@ -10,6 +10,7 @@ import RxSwift import RxCocoa import Lottie +@available(*, deprecated, renamed: "BBProgressHUD") public class BibbiLoadIndicator { private var disposeBag = DisposeBag() diff --git a/14th-team5-iOS/Core/Sources/Trash/Lottie/LottieType.swift b/14th-team5-iOS/Core/Sources/Trash/Lottie/LottieType.swift index 67092be7a..d62a44948 100644 --- a/14th-team5-iOS/Core/Sources/Trash/Lottie/LottieType.swift +++ b/14th-team5-iOS/Core/Sources/Trash/Lottie/LottieType.swift @@ -8,6 +8,7 @@ import Foundation import DesignSystem +@available(*, deprecated) public enum LottieType: Equatable { static let keyLoading = "loading" static let keyfire = "fire" diff --git a/14th-team5-iOS/Core/Sources/Trash/Lottie/LottieView.swift b/14th-team5-iOS/Core/Sources/Trash/Lottie/LottieView.swift index c56037ed0..6afbc60be 100644 --- a/14th-team5-iOS/Core/Sources/Trash/Lottie/LottieView.swift +++ b/14th-team5-iOS/Core/Sources/Trash/Lottie/LottieView.swift @@ -11,6 +11,7 @@ import SnapKit import RxSwift import RxCocoa +@available(*, deprecated, renamed: "BBLottieView") final public class LottieView: UIView { // MARK: SubView diff --git a/14th-team5-iOS/Core/Sources/Utilities/Haptic.swift b/14th-team5-iOS/Core/Sources/Utilities/Haptic.swift index 6e46268c7..75370ea3f 100644 --- a/14th-team5-iOS/Core/Sources/Utilities/Haptic.swift +++ b/14th-team5-iOS/Core/Sources/Utilities/Haptic.swift @@ -10,6 +10,7 @@ import Foundation import UIKit public final class Haptic { + // MARK: - Notification public static func notification(type: UINotificationFeedbackGenerator.FeedbackType) { let generator = UINotificationFeedbackGenerator() @@ -27,4 +28,5 @@ public final class Haptic { let generator = UISelectionFeedbackGenerator() generator.selectionChanged() } + } From 90ca193b0c40558ccfeb9429423a29a2ea92ad6c Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Mon, 19 Aug 2024 14:15:56 +0900 Subject: [PATCH 195/263] =?UTF-8?q?fix:=20BBProgressHUDSubView=EB=A5=BC=20?= =?UTF-8?q?BBProgressHudStackView=EB=A1=9C=20=EC=88=98=EC=A0=95=20(#610)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ViewControllers/MainViewController.swift | 5 ++ .../BBProgressHUD/BBProgressHUD.swift | 20 +++++-- .../BBProgressHUDViewConfiguration.swift | 2 +- .../BBProgressHUDSubView.swift | 4 +- .../CoreAnimationProgressHUDView.swift | 45 ++++++++++++--- .../DefaultProgressHUDView.swift | 6 +- .../LottieProgressHUDView.swift | 57 +++++++++++++++---- .../BibbiToastMessageView.swift | 1 + 8 files changed, 110 insertions(+), 30 deletions(-) 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 57c6e181e..c344a3933 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift @@ -37,6 +37,11 @@ final class MainViewController: BaseViewController, UICollectio override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) + + let viewConfig = BBProgressHUDViewConfiguration(minWidth: 160, minHeight: 160, lottieAnimationScale: 1.1) + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + BBProgressHUD.lottie(.airplane, title: "열심히 불러오는 중..", viewConfig: viewConfig).show() + } } override func bind(reactor: MainViewReactor) { diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUD.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUD.swift index 2256d5a70..3f766b3db 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUD.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUD.swift @@ -33,12 +33,18 @@ public class BBProgressHUD { /// - Returns: BBProgressHUD public static func lottie( _ kind: BBLottieKind, + title: String? = nil, + titleFontStyle: BBFontStyle? = nil, + titleColor: UIColor? = nil, viewConfig: BBProgressHUDViewConfiguration = BBProgressHUDViewConfiguration(), config: BBProgressHUDConfiguration = BBProgressHUDConfiguration() ) -> BBProgressHUD { let view = DefaultProgressHUDView( child: LottieProgressHUDView( of: kind, + title: title, + titleFontStyle: titleFontStyle, + titleColor: titleColor, viewConfig: viewConfig ), viewConfig: viewConfig @@ -55,12 +61,18 @@ public class BBProgressHUD { /// - Returns: BBProgressHUD public static func animation( _ type: BBProgressHUDCAType, + title: String? = nil, + titleFontStyle: BBFontStyle? = nil, + titleColor: UIColor? = nil, viewConfig: BBProgressHUDViewConfiguration = BBProgressHUDViewConfiguration(), config: BBProgressHUDConfiguration = BBProgressHUDConfiguration() ) -> BBProgressHUD { let view = DefaultProgressHUDView( child: CoreAnimationProgressHUDView( - of: type, + type, + title: title, + titleFontStyle: titleFontStyle, + titleColor: titleColor, viewConfig: viewConfig ), viewConfig: viewConfig @@ -83,16 +95,16 @@ public class BBProgressHUD { let view = DefaultProgressHUDView( child: LottieProgressHUDView( of: .airplane, - viewConfig: .airplaneLottie + viewConfig: .lottie ), - viewConfig: .airplaneLottie + viewConfig: .lottie ) return BBProgressHUD(view: view, config: config) } } public static func custom( - _ child: BBProgressHUDSubView, + _ child: BBProgressHUDStackView, viewConfig: BBProgressHUDViewConfiguration = BBProgressHUDViewConfiguration(), config: BBProgressHUDConfiguration = BBProgressHUDConfiguration() ) -> BBProgressHUD { diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUDViewConfiguration.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUDViewConfiguration.swift index 94d368a8c..9f813c582 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUDViewConfiguration.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUDViewConfiguration.swift @@ -63,7 +63,7 @@ public struct BBProgressHUDViewConfiguration { public extension BBProgressHUDViewConfiguration { - static var airplaneLottie: Self = { + static var lottie: Self = { Self( lottieFromProgress: 0.15, lottieToProgress: 0.95, diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/ProgressHUDView/BBProgressHUDSubView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/ProgressHUDView/BBProgressHUDSubView.swift index b99a446df..bf8ca4989 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/ProgressHUDView/BBProgressHUDSubView.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/ProgressHUDView/BBProgressHUDSubView.swift @@ -7,6 +7,6 @@ import UIKit -public protocol BBProgressHUDSubView: UIView { - func applyAnimation(for progressHud: BBProgressHUD?) +public protocol BBProgressHUDStackView: UIStackView { + var progressHud: BBProgressHUD? { get set } } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/ProgressHUDView/DefaultProgressHUDView/CoreAnimationProgressHUDView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/ProgressHUDView/DefaultProgressHUDView/CoreAnimationProgressHUDView.swift index ade7f7d0e..14e6c457d 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/ProgressHUDView/DefaultProgressHUDView/CoreAnimationProgressHUDView.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/ProgressHUDView/DefaultProgressHUDView/CoreAnimationProgressHUDView.swift @@ -7,35 +7,66 @@ import UIKit -public class CoreAnimationProgressHUDView: UIView, BBProgressHUDSubView { +public class CoreAnimationProgressHUDView: UIStackView, BBProgressHUDStackView { + + // MARK: - Views + + private let caView = UIView() + private let titleLabel = BBLabel() // MARK: - Properties public var viewConfig: BBProgressHUDViewConfiguration - + public var progressHud: BBProgressHUD? + private var type: BBProgressHUDCAType // MARK: - Intializer public init( - of type: BBProgressHUDCAType, + _ type: BBProgressHUDCAType, + title: String? = nil, + titleFontStyle: BBFontStyle? = nil, + titleColor: UIColor? = nil, viewConfig: BBProgressHUDViewConfiguration ) { - self.type = type self.viewConfig = viewConfig + self.type = type + super.init(frame: .zero) + commonInit() + + addArrangedSubview(caView) + applyAnimation(for: caView) + + if let title = title { + titleLabel.text = title + titleLabel.fontStyle = titleFontStyle ?? .body1Regular + titleLabel.textColor = titleColor ?? .bibbiWhite + titleLabel.textAlignment = .center + titleLabel.numberOfLines = 1 + addArrangedSubview(titleLabel) + } + } - required init?(coder: NSCoder) { + required init(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } // MARK: - Helpers - public func applyAnimation(for progressHud: BBProgressHUD?) { - if type == .spinner { animationActivityIndicator(for: self) } + private func commonInit() { + axis = .vertical + spacing = 10 + alignment = .fill + distribution = .fillProportionally + } + + private func applyAnimation(for view: UIView) { + if type == .spinner { animationActivityIndicator(for: view) } } } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/ProgressHUDView/DefaultProgressHUDView/DefaultProgressHUDView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/ProgressHUDView/DefaultProgressHUDView/DefaultProgressHUDView.swift index 92910be11..6b6cb8667 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/ProgressHUDView/DefaultProgressHUDView/DefaultProgressHUDView.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/ProgressHUDView/DefaultProgressHUDView/DefaultProgressHUDView.swift @@ -11,7 +11,7 @@ public class DefaultProgressHUDView: UIView, BBProgressHUDView { // MARK: - Views - private let child: BBProgressHUDSubView + private let child: BBProgressHUDStackView // MARK: - Properties @@ -21,7 +21,7 @@ public class DefaultProgressHUDView: UIView, BBProgressHUDView { // MARK: - Intializer public init( - child: BBProgressHUDSubView, + child: BBProgressHUDStackView, viewConfig: BBProgressHUDViewConfiguration ) { self.child = child @@ -75,8 +75,6 @@ public class DefaultProgressHUDView: UIView, BBProgressHUDView { backgroundColor = viewConfig.backgroundColor addShadow() - - child.applyAnimation(for: progressHud) } private func addShadow() { diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/ProgressHUDView/DefaultProgressHUDView/LottieProgressHUDView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/ProgressHUDView/DefaultProgressHUDView/LottieProgressHUDView.swift index 6464e52c3..79e14a07d 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/ProgressHUDView/DefaultProgressHUDView/LottieProgressHUDView.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/ProgressHUDView/DefaultProgressHUDView/LottieProgressHUDView.swift @@ -7,47 +7,80 @@ import UIKit -public class LottieProgressHUDView: UIView, BBProgressHUDSubView { +public class LottieProgressHUDView: UIStackView, BBProgressHUDStackView { + + // MARK: - Views + + private let lottieView = BBLottieView(of: .airplane) + private let titleLabel = BBLabel() + // MARK: - Properties + + public let viewConfig: BBProgressHUDViewConfiguration + public var progressHud: BBProgressHUD? private let kind: BBLottieKind - public let viewConfig: BBProgressHUDViewConfiguration // MARK: - Intializer public init( of kind: BBLottieKind, + title: String? = nil, + titleFontStyle: BBFontStyle? = nil, + titleColor: UIColor? = nil, viewConfig: BBProgressHUDViewConfiguration ) { self.viewConfig = viewConfig self.kind = kind super.init(frame: .zero) + commonInit() + + addArrangedSubview(lottieView) + applyAnimation(for: lottieView) + + if let title = title { + titleLabel.text = title + titleLabel.fontStyle = titleFontStyle ?? .body1Regular + titleLabel.textColor = titleColor ?? .bibbiWhite + titleLabel.textAlignment = .center + titleLabel.numberOfLines = 1 + addArrangedSubview(titleLabel) + } + } - required init?(coder: NSCoder) { + required init(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + public override func layoutSubviews() { + super.layoutSubviews() + applyAnimation(for: lottieView) + } + // MARK: - Helpers - public func applyAnimation(for progressHud: BBProgressHUD?) { + private func commonInit() { + axis = .vertical + spacing = 6 + alignment = .fill + distribution = .fillProportionally + } + + private func applyAnimation(for view: BBLottieView) { - let lottieView = BBLottieView(of: kind) - addSubview(lottieView) - - lottieView.frame = self.bounds - lottieView.contentMode = .scaleAspectFit - lottieView.animationSpeed = viewConfig.lottieAnimationSpeed - lottieView.transform = CGAffineTransform( + view.contentMode = .scaleAspectFit + view.animationSpeed = viewConfig.lottieAnimationSpeed + view.transform = CGAffineTransform( scaleX: viewConfig.lottieAnimationScale, y: viewConfig.lottieAnimationScale ) - lottieView.startAnimating( + view.startAnimating( fromProgress: viewConfig.lottieFromProgress, toProgress: viewConfig.lottieToProgress, loopMode: .loop diff --git a/14th-team5-iOS/Core/Sources/Trash/BibbiToastMessageView/BibbiToastMessageView.swift b/14th-team5-iOS/Core/Sources/Trash/BibbiToastMessageView/BibbiToastMessageView.swift index a4851500c..3a158f3b8 100644 --- a/14th-team5-iOS/Core/Sources/Trash/BibbiToastMessageView/BibbiToastMessageView.swift +++ b/14th-team5-iOS/Core/Sources/Trash/BibbiToastMessageView/BibbiToastMessageView.swift @@ -11,6 +11,7 @@ import UIKit import SnapKit import Then +@available(*, deprecated) final public class BibbiToastMessageView: UIView { // MARK: - Views private let capsuleView: UIView = UIView() From 00da623772e2bb4185d497f327d326c62e9f839d Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Mon, 19 Aug 2024 14:26:37 +0900 Subject: [PATCH 196/263] =?UTF-8?q?feat:=20BBProgressHUD=EC=97=90=20airpla?= =?UTF-8?q?neWithTitle=20Style=20=EA=B5=AC=ED=98=84=20(#610)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ViewControllers/MainViewController.swift | 2 +- .../BBProgressHUD/BBProgressHUD.swift | 13 +++++++++++ .../BBProgressHUD/BBProgressHUDStyle.swift | 7 +++++- .../BBProgressHUDViewConfiguration.swift | 22 ++++++++++++++----- 4 files changed, 37 insertions(+), 7 deletions(-) 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 c344a3933..eb6964636 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift @@ -40,7 +40,7 @@ final class MainViewController: BaseViewController, UICollectio let viewConfig = BBProgressHUDViewConfiguration(minWidth: 160, minHeight: 160, lottieAnimationScale: 1.1) DispatchQueue.main.asyncAfter(deadline: .now() + 2) { - BBProgressHUD.lottie(.airplane, title: "열심히 불러오는 중..", viewConfig: viewConfig).show() + BBProgressHUD.style(.airplaneWithTitle()).show() } } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUD.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUD.swift index 3f766b3db..656980977 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUD.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUD.swift @@ -100,6 +100,19 @@ public class BBProgressHUD { viewConfig: .lottie ) return BBProgressHUD(view: view, config: config) + + case let .airplaneWithTitle(title, titleFontStyle, titleColor): + let view = DefaultProgressHUDView( + child: LottieProgressHUDView( + of: .airplane, + title: title, + titleFontStyle: titleFontStyle, + titleColor: titleColor, + viewConfig: .lottieWithTitle + ), + viewConfig: .lottieWithTitle + ) + return BBProgressHUD(view: view, config: config) } } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUDStyle.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUDStyle.swift index ac7629024..eabcf967d 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUDStyle.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUDStyle.swift @@ -5,7 +5,7 @@ // Created by 김건우 on 8/16/24. // -import Foundation +import UIKit // MARK: - Typealias @@ -18,6 +18,11 @@ extension BBProgressHUD { public enum Style { case airplane + case airplaneWithTitle( + title: String? = "열심히 불러오는 중..", + titleFontStyle: BBFontStyle? = .body1Regular, + titleColor: UIColor? = .gray400 + ) } } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUDViewConfiguration.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUDViewConfiguration.swift index 9f813c582..743cd5832 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUDViewConfiguration.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBProgressHUD/BBProgressHUDViewConfiguration.swift @@ -37,10 +37,10 @@ public struct BBProgressHUDViewConfiguration { offsetFromCenterY yOffset: CGFloat = 0, animationColor: UIColor? = .mainYellow, backgroundColor: UIColor? = .gray900, - lottieFromProgress fromProgress: CGFloat = 0.15, - lottieToProgress toProgress: CGFloat = 0.95, - lottieAnimationSpeed speed: CGFloat = 1.35, - lottieAnimationScale scale: CGFloat = 0.8, + lottieFromProgress fromProgress: CGFloat = 0, + lottieToProgress toProgress: CGFloat = 1, + lottieAnimationSpeed speed: CGFloat = 1, + lottieAnimationScale scale: CGFloat = 1, cornerRadius: CGFloat? = 30 ) { self.minWidth = minWidth @@ -68,7 +68,19 @@ public extension BBProgressHUDViewConfiguration { lottieFromProgress: 0.15, lottieToProgress: 0.95, lottieAnimationSpeed: 1.35, - lottieAnimationScale: 0.8 + lottieAnimationScale: 1.1 + ) + }() + + static var lottieWithTitle: Self = { + Self( + minWidth: 160, + minHeight: 160, + backgroundColor: .clear, + lottieFromProgress: 0.15, + lottieToProgress: 0.95, + lottieAnimationSpeed: 1.35, + lottieAnimationScale: 1.1 ) }() From 6772f136a83322c4613b1243ab73dbb1e72ac228 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Sat, 24 Aug 2024 12:03:00 +0900 Subject: [PATCH 197/263] =?UTF-8?q?fix:=20FamilyRepository=EC=97=90?= =?UTF-8?q?=EC=84=9C=20FamilyID=EB=A5=BC=20=EC=A0=9C=EB=8C=80=EB=A1=9C=20?= =?UTF-8?q?=EB=AA=BB=20=EB=B6=88=EB=9F=AC=EC=98=A4=EB=8A=94=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=EC=88=98=EC=A0=95=20(#613)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FamilyManagementViewController.swift | 9 +------ .../ViewControllers/MainViewController.swift | 9 ------- .../Splash/SplashViewController.swift | 27 ++++++++++--------- .../UserDefaultsWrapper.swift | 2 +- .../Family/Repository/FamilyRepository.swift | 14 +++++++--- 5 files changed, 27 insertions(+), 34 deletions(-) diff --git a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/ViewController/FamilyManagementViewController.swift b/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/ViewController/FamilyManagementViewController.swift index 4051bb3cc..8f3ace404 100644 --- a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/ViewController/FamilyManagementViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/ViewController/FamilyManagementViewController.swift @@ -145,13 +145,6 @@ public final class FamilyManagementViewController: BBNavigationViewController, UICollectio UserDefaults.standard.inviteCode = nil } - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - let viewConfig = BBProgressHUDViewConfiguration(minWidth: 160, minHeight: 160, lottieAnimationScale: 1.1) - DispatchQueue.main.asyncAfter(deadline: .now() + 2) { - BBProgressHUD.style(.airplaneWithTitle()).show() - } - } - override func bind(reactor: MainViewReactor) { super.bind(reactor: reactor) bindInput(reactor: reactor) diff --git a/14th-team5-iOS/App/Sources/Presentation/Splash/SplashViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Splash/SplashViewController.swift index 1deb17ee5..b0da7977b 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Splash/SplashViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Splash/SplashViewController.swift @@ -99,28 +99,29 @@ public final class SplashViewController: BaseViewController { @Navigator var splashNavigator: SplashNavigatorProtocol guard let member = member else { - container = UINavigationController(rootViewController: AccountSignInDIContainer().makeViewController()) - sceneDelegate.window?.rootViewController = container - sceneDelegate.window?.makeKeyAndVisible() + splashNavigator.toSignIn() +// container = UINavigationController(rootViewController: AccountSignInDIContainer().makeViewController()) +// sceneDelegate.window?.rootViewController = container +// sceneDelegate.window?.makeKeyAndVisible() return } if let _ = member.familyId { if UserDefaults.standard.inviteCode != nil { -// splashNavigator.toJoined() - container = UINavigationController(rootViewController: JoinedFamilyDIContainer().makeViewController()) + splashNavigator.toJoined() +// container = UINavigationController(rootViewController: JoinedFamilyDIContainer().makeViewController()) } else { -// splashNavigator.toHome() - container = UINavigationController(rootViewController: MainViewControllerWrapper().makeViewController()) + splashNavigator.toHome() +// container = UINavigationController(rootViewController: MainViewControllerWrapper().makeViewController()) } - sceneDelegate.window?.rootViewController = container - sceneDelegate.window?.makeKeyAndVisible() +// sceneDelegate.window?.rootViewController = container +// sceneDelegate.window?.makeKeyAndVisible() return } else { -// splashNavigator.toOnboarding() - container = UINavigationController(rootViewController: OnBoardingDIContainer().makeViewController()) - sceneDelegate.window?.rootViewController = container - sceneDelegate.window?.makeKeyAndVisible() + splashNavigator.toOnboarding() +// container = UINavigationController(rootViewController: OnBoardingDIContainer().makeViewController()) +// sceneDelegate.window?.rootViewController = container +// sceneDelegate.window?.makeKeyAndVisible() return } } 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 f7c152e0b..fffd9640b 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/UserDefaultsWrapper/UserDefaultsWrapper.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/UserDefaultsWrapper/UserDefaultsWrapper.swift @@ -17,7 +17,7 @@ final public class UserDefaultsWrapper { private(set) public var suitName: String private static let defaultSuitName: String = { - Bundle.main.bundleIdentifier ?? "UserDefaultsWrapper" + "UserDefaultsWrapper" }() // MARK: - Intializer diff --git a/14th-team5-iOS/Data/Sources/APIs/Family/Repository/FamilyRepository.swift b/14th-team5-iOS/Data/Sources/APIs/Family/Repository/FamilyRepository.swift index abeaf5011..fa0e655bd 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Family/Repository/FamilyRepository.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Family/Repository/FamilyRepository.swift @@ -19,9 +19,6 @@ public final class FamilyRepository: FamilyRepositoryProtocol { private let familyApiWorker: FamilyAPIWorker = FamilyAPIWorker() - // TODO: - 리팩토링된 FamilyUserDefaults로 바꾸기 - private var familyId: String = App.Repository.member.familyId.value ?? "" - // MARK: - Intializer public init() { } @@ -76,6 +73,9 @@ extension FamilyRepository { // MARK: - Fetch Family CreatedAt public func fetchFamilyCreatedAt() -> Observable { + // TODO: - 리팩토링된 FamilyUserDefaults로 바꾸기 + var familyId: String = App.Repository.member.familyId.value ?? "" + return familyApiWorker.fetchFamilyCreatedAt(familyId: familyId) .map { $0?.toDomain() } .do(onSuccess: { @@ -87,6 +87,9 @@ extension FamilyRepository { // MARK: - Fetch Invitation Url public func fetchInvitationLink() -> Observable { + // TODO: - 리팩토링된 FamilyUserDefaults로 바꾸기 + var familyId: String = App.Repository.member.familyId.value ?? "" + return familyApiWorker.fetchInvitationLink(familyId: familyId) .map { $0?.toDomain() } .asObservable() @@ -95,6 +98,9 @@ extension FamilyRepository { // MARK: - Fetch Family Members public func fetchPaginationFamilyMembers(query: FamilyPaginationQuery) -> Observable { + // TODO: - 리팩토링된 FamilyUserDefaults로 바꾸기 + var familyId: String = App.Repository.member.familyId.value ?? "" + return familyApiWorker.fetchPaginationFamilyMember(familyId: familyId, query: query) .map { $0?.toDomain() } .do(onSuccess: { @@ -111,6 +117,8 @@ extension FamilyRepository { // MARK: - Update Family Name public func updateFamilyName(body: UpdateFamilyNameRequest) -> Observable { + // TODO: - 리팩토링된 FamilyUserDefaults로 바꾸기 + var familyId: String = App.Repository.member.familyId.value ?? "" let body = UpdateFamilyNameRequestDTO(familyName: body.familyName) return familyApiWorker.updateFamilyName(familyId: familyId, body: body) From 1509a04e975fb8df06ddd5d04c86f3265d1712fb Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Sat, 24 Aug 2024 12:27:28 +0900 Subject: [PATCH 198/263] =?UTF-8?q?feat:=20App,=20My=20=EB=B0=8F=20FamilyU?= =?UTF-8?q?serDefaults=20=EA=B5=AC=ED=98=84=20(#613)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UserDefaultsWrapper/UserDefaultsKey.swift | 13 +++ .../TokenKeychain/Models/OldAccessToken.swift | 8 ++ .../TokenKeychain.swift} | 0 .../AppUserDefaults/AppUserDefaults.swift | 93 +++++++++++++++++++ .../FamilyUserDefaults.swift | 72 ++++++++++++++ .../MyUserDefaults/MyUserDefaults.swift | 55 +++++++++++ .../UserDefaults/UserDefaults+Family.swift | 41 -------- .../UserDefaults/FamilyUserDefautls.swift | 1 + 8 files changed, 242 insertions(+), 41 deletions(-) create mode 100644 14th-team5-iOS/Data/Sources/Storages/Keychain/TokenKeychain/Models/OldAccessToken.swift rename 14th-team5-iOS/Data/Sources/Storages/Keychain/{Keychain+Token.swift => TokenKeychain/TokenKeychain.swift} (100%) create mode 100644 14th-team5-iOS/Data/Sources/Storages/UserDefaults/AppUserDefaults/AppUserDefaults.swift create mode 100644 14th-team5-iOS/Data/Sources/Storages/UserDefaults/FamilyUserDefaults/FamilyUserDefaults.swift create mode 100644 14th-team5-iOS/Data/Sources/Storages/UserDefaults/MyUserDefaults/MyUserDefaults.swift delete mode 100644 14th-team5-iOS/Data/Sources/Storages/UserDefaults/UserDefaults+Family.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/UserDefaultsWrapper/UserDefaultsKey.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/UserDefaultsWrapper/UserDefaultsKey.swift index 178158f38..88ffa1537 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/UserDefaultsWrapper/UserDefaultsKey.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/UserDefaultsWrapper/UserDefaultsKey.swift @@ -9,9 +9,22 @@ import Foundation public extension UserDefaultsWrapper.Key { + // MARK: - App UserDefaults + + static let isFirstLaunchApp: Self = "isFirstLaunchApp" + static let isFirstChangeFamilyName: Self = "isFirstChangeFamilyName" + static let isFirstShowWidgetAlert: Self = "isFirstShowWidgetAlert" + static let inviteCode: Self = "inviteCode" + + // MARK: - Family UserDefaults + static let familyId: Self = "familyId" static let familyCreatedAt: Self = "familyCreatedAt" + static let familyName: Self = "familyName" + + // MARK: - My UserDefaults + static let userName: Self = "userName" static let memberId: Self = "memberId" } 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 new file mode 100644 index 000000000..4d00cada3 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/Storages/Keychain/TokenKeychain/Models/OldAccessToken.swift @@ -0,0 +1,8 @@ +// +// AccessToken.swift +// Data +// +// Created by 김건우 on 8/24/24. +// + +import Foundation diff --git a/14th-team5-iOS/Data/Sources/Storages/Keychain/Keychain+Token.swift b/14th-team5-iOS/Data/Sources/Storages/Keychain/TokenKeychain/TokenKeychain.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Storages/Keychain/Keychain+Token.swift rename to 14th-team5-iOS/Data/Sources/Storages/Keychain/TokenKeychain/TokenKeychain.swift diff --git a/14th-team5-iOS/Data/Sources/Storages/UserDefaults/AppUserDefaults/AppUserDefaults.swift b/14th-team5-iOS/Data/Sources/Storages/UserDefaults/AppUserDefaults/AppUserDefaults.swift new file mode 100644 index 000000000..7ae5eb5dc --- /dev/null +++ b/14th-team5-iOS/Data/Sources/Storages/UserDefaults/AppUserDefaults/AppUserDefaults.swift @@ -0,0 +1,93 @@ +// +// AppUserDefaults.swift +// Data +// +// Created by 김건우 on 8/24/24. +// + +import Core +import Foundation + + +public protocol AppUserDefaultsType: UserDefaultsType { + func saveIsFirstLaunchApp(_ value: Bool?) + func loadIsFirstLaunchApp() -> Bool? + + func saveIsFirstChangeFamilyName(_ value: Bool?) + func loadIsFirstChangeFamilyName() -> Bool? + + func saveIsFirstShowWidgetAlert(_ value: Bool?) + func loadIsFirstShowWidgetAlert() -> Bool? + + func saveInviteCode(_ inviteCode: String?) + func loadInviteCode() -> String? +} + +final public class AppUserDefaults: AppUserDefaultsType { + + // MARK: - Intializer + + public init() { } + + + // MARK: - Is First Launch App + + public func saveIsFirstLaunchApp(_ value: Bool?) { + userDefaults[.isFirstLaunchApp] = value + } + + public func loadIsFirstLaunchApp() -> Bool? { + guard + let value: Bool? = userDefaults[.isFirstLaunchApp] + else { return nil } + return value + } + + + // MARK: - Is First Change Family Name + + public func saveIsFirstChangeFamilyName(_ value: Bool?) { + userDefaults[.isFirstChangeFamilyName] = value + } + + public func loadIsFirstChangeFamilyName() -> Bool? { + guard + let value: Bool? = userDefaults[.isFirstChangeFamilyName] + else { return nil } + return value + } + + + // MARK: - Is First Show Widget Alert + + public func saveIsFirstShowWidgetAlert(_ value: Bool?) { + userDefaults[.isFirstShowWidgetAlert] = value + } + + public func loadIsFirstShowWidgetAlert() -> Bool? { + guard + let value: Bool? = userDefaults[.isFirstShowWidgetAlert] + else { return nil } + return value + } + + + // MARK: - Invite Code + + public func saveInviteCode(_ inviteCode: String?) { + userDefaults[.inviteCode] = inviteCode + } + + public func loadInviteCode() -> String? { + guard + let inviteCode: String? = userDefaults[.inviteCode] + else { return nil } + return inviteCode + } + + + + + + +} diff --git a/14th-team5-iOS/Data/Sources/Storages/UserDefaults/FamilyUserDefaults/FamilyUserDefaults.swift b/14th-team5-iOS/Data/Sources/Storages/UserDefaults/FamilyUserDefaults/FamilyUserDefaults.swift new file mode 100644 index 000000000..67aa6d50c --- /dev/null +++ b/14th-team5-iOS/Data/Sources/Storages/UserDefaults/FamilyUserDefaults/FamilyUserDefaults.swift @@ -0,0 +1,72 @@ +// +// File.swift +// Data +// +// Created by 김건우 on 6/2/24. +// + +import Core +import Foundation + + +public protocol FamilyInfoUserDefaultsType: UserDefaultsType { + func saveFamilyId(_ familyId: String?) + func loadFamilyId() -> String? + + func saveFamilyCreatedAt(_ familyCreatedAt: Date?) + func loadFamilyCreatedAt() -> Date? + + func saveFamilyName(_ familyName: String?) + func loadFamilyName() -> String? +} + + +final public class FamilyInfoUserDefaults: FamilyInfoUserDefaultsType { + + // MARK: - Intializer + + public init() { } + + + // MARK: - Family Id + + public func saveFamilyId(_ familyId: String?) { + userDefaults[.familyId] = familyId + } + + public func loadFamilyId() -> String? { + guard + let familyId: String? = userDefaults[.familyId] + else { return nil } + return familyId + } + + + // MARK: - Family Created At + + public func saveFamilyCreatedAt(_ familyCreatedAt: Date?) { + userDefaults[.familyCreatedAt] = familyCreatedAt + } + + public func loadFamilyCreatedAt() -> Date? { + guard + let familyCreatedAt: Date? = userDefaults[.familyCreatedAt] + else { return nil } + return familyCreatedAt + } + + + // MARK: - Family Name + + public func saveFamilyName(_ familyName: String?) { + userDefaults[.familyName] = familyName + } + + public func loadFamilyName() -> String? { + guard + let familyName: String? = userDefaults[.familyName] + else { return nil } + return familyName + } + +} diff --git a/14th-team5-iOS/Data/Sources/Storages/UserDefaults/MyUserDefaults/MyUserDefaults.swift b/14th-team5-iOS/Data/Sources/Storages/UserDefaults/MyUserDefaults/MyUserDefaults.swift new file mode 100644 index 000000000..c9ec3fb14 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/Storages/UserDefaults/MyUserDefaults/MyUserDefaults.swift @@ -0,0 +1,55 @@ +// +// MyUserDefaults.swift +// Data +// +// Created by 김건우 on 8/24/24. +// + +import Core +import Foundation + + +public protocol MyUserDefaultsType: UserDefaultsType { + func saveMemberId(_ memberId: String?) + func loadMemberId() -> String? + + func saveUserName(_ userName: String?) + func loadUserName() -> String? +} + +final public class MyUserDefaults: MyUserDefaultsType { + + // MARK: - Intializer + + public init() { } + + + // MARK: - Member Id + + public func saveMemberId(_ memberId: String?) { + userDefaults[.memberId] = memberId + } + + public func loadMemberId() -> String? { + guard + let memberId: String? = userDefaults[.memberId] + else { return nil } + return memberId + } + + + // MARK: - UserName + + public func saveUserName(_ userName: String?) { + userDefaults[.userName] = userName + } + + public func loadUserName() -> String? { + guard + let userName: String? = userDefaults[.userName] + else { return nil } + return userName + } + +} + diff --git a/14th-team5-iOS/Data/Sources/Storages/UserDefaults/UserDefaults+Family.swift b/14th-team5-iOS/Data/Sources/Storages/UserDefaults/UserDefaults+Family.swift deleted file mode 100644 index ee4017197..000000000 --- a/14th-team5-iOS/Data/Sources/Storages/UserDefaults/UserDefaults+Family.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// File.swift -// Data -// -// Created by 김건우 on 6/2/24. -// - -import Core -import Foundation - - -// NOTE: - 예시 코드 - -public protocol MemberUserDefaultsType: UserDefaultsType { - func save(_ memberId: String) - func loadMemberId() -> String? -} - - -final public class MemberUserDefaults: MemberUserDefaultsType { - - // MARK: - Intializer - public init() { } - - - // MARK: - Member Id - - public func save(_ memberId: String) { - userDefaults[.memberId] = memberId - } - - public func loadMemberId() -> String? { - guard - let value: String = userDefaults[.memberId] - else { return nil } - return value - } - - // ... - -} diff --git a/14th-team5-iOS/Data/Sources/Trash/UserDefaults/FamilyUserDefautls.swift b/14th-team5-iOS/Data/Sources/Trash/UserDefaults/FamilyUserDefautls.swift index 8562e63d7..bdaf259b8 100644 --- a/14th-team5-iOS/Data/Sources/Trash/UserDefaults/FamilyUserDefautls.swift +++ b/14th-team5-iOS/Data/Sources/Trash/UserDefaults/FamilyUserDefautls.swift @@ -11,6 +11,7 @@ import Domain import RxSwift import RxCocoa +@available(*, deprecated, renamed: "FamilyInfoUserDefaults") public class FamilyUserDefaults { /// familyIdKey - familyId 저장 /// familyId - memberId를 배열로 저장 From ba4b5a6ade2a64372fd35f2070e0d563dac5f3d6 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Sat, 24 Aug 2024 12:39:13 +0900 Subject: [PATCH 199/263] =?UTF-8?q?feat:=20CommentUserDefaults=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#613)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UserDefaultsWrapper/UserDefaultsKey.swift | 7 ++++ .../CommentUserDefaults.swift | 37 +++++++++++++++++++ .../Models/CommentSnapshot.swift | 13 +++++++ 3 files changed, 57 insertions(+) create mode 100644 14th-team5-iOS/Data/Sources/Storages/UserDefaults/CommentUserDefaults/CommentUserDefaults.swift create mode 100644 14th-team5-iOS/Data/Sources/Storages/UserDefaults/CommentUserDefaults/Models/CommentSnapshot.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/UserDefaultsWrapper/UserDefaultsKey.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/UserDefaultsWrapper/UserDefaultsKey.swift index 88ffa1537..ca3c3454b 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/UserDefaultsWrapper/UserDefaultsKey.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/UserDefaultsWrapper/UserDefaultsKey.swift @@ -16,15 +16,22 @@ public extension UserDefaultsWrapper.Key { static let isFirstShowWidgetAlert: Self = "isFirstShowWidgetAlert" static let inviteCode: Self = "inviteCode" + // MARK: - Family UserDefaults static let familyId: Self = "familyId" static let familyCreatedAt: Self = "familyCreatedAt" static let familyName: Self = "familyName" + // MARK: - My UserDefaults static let userName: Self = "userName" static let memberId: Self = "memberId" + + // MARK: - Comment UserDefaults + + static let commentSnapshot: Self = "commentSnapshot" + } diff --git a/14th-team5-iOS/Data/Sources/Storages/UserDefaults/CommentUserDefaults/CommentUserDefaults.swift b/14th-team5-iOS/Data/Sources/Storages/UserDefaults/CommentUserDefaults/CommentUserDefaults.swift new file mode 100644 index 000000000..472fc649c --- /dev/null +++ b/14th-team5-iOS/Data/Sources/Storages/UserDefaults/CommentUserDefaults/CommentUserDefaults.swift @@ -0,0 +1,37 @@ +// +// CommentUserDefaults.swift +// Data +// +// Created by 김건우 on 8/24/24. +// + +import Core +import Foundation + + +public protocol CommentUserDefaultsType: UserDefaultsType { + func saveCommentSnapshot(_ snapshot: CommentSnapshot?) + func loadCommentSnapshot() -> CommentSnapshot? +} + +public final class CommentUserDefaults: CommentUserDefaultsType { + + // MARK: - Intializer + + public init() { } + + + // MARK: - Comment Snapshot + + public func saveCommentSnapshot(_ snapshot: CommentSnapshot?) { + userDefaults[.commentSnapshot] = snapshot + } + + public func loadCommentSnapshot() -> CommentSnapshot? { + guard + let snapshot: CommentSnapshot? = userDefaults[.commentSnapshot] + else { return nil } + return snapshot + } + +} diff --git a/14th-team5-iOS/Data/Sources/Storages/UserDefaults/CommentUserDefaults/Models/CommentSnapshot.swift b/14th-team5-iOS/Data/Sources/Storages/UserDefaults/CommentUserDefaults/Models/CommentSnapshot.swift new file mode 100644 index 000000000..ca3d97758 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/Storages/UserDefaults/CommentUserDefaults/Models/CommentSnapshot.swift @@ -0,0 +1,13 @@ +// +// CommentSnapshot.swift +// Data +// +// Created by 김건우 on 8/24/24. +// + +import Foundation + +public struct CommentSnapshot: Codable { + public var postId: String + public var contents: String +} From 82211414a5182ef0292d21b44bedd3237a269d63 Mon Sep 17 00:00:00 2001 From: Kyoungmi <1mmgm.dev@gmail.com> Date: Fri, 30 Aug 2024 12:24:24 +0900 Subject: [PATCH 200/263] feat: add userdefaults apps and family(#615) --- .../UserDefaultsWrapper/UserDefaultsKey.swift | 6 ++- .../AppUserDefaults/AppUserDefaults.swift | 43 +++++++++++++++++-- .../FamilyUserDefaults.swift | 43 ++++++++++++++++++- 3 files changed, 85 insertions(+), 7 deletions(-) diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/UserDefaultsWrapper/UserDefaultsKey.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/UserDefaultsWrapper/UserDefaultsKey.swift index ca3c3454b..d089a08a3 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/UserDefaultsWrapper/UserDefaultsKey.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/UserDefaultsWrapper/UserDefaultsKey.swift @@ -8,12 +8,13 @@ import Foundation public extension UserDefaultsWrapper.Key { - // MARK: - App UserDefaults static let isFirstLaunchApp: Self = "isFirstLaunchApp" static let isFirstChangeFamilyName: Self = "isFirstChangeFamilyName" static let isFirstShowWidgetAlert: Self = "isFirstShowWidgetAlert" + static let isFirstFamilyManagement: Self = "isFirstFamilyManagement" + static let isFirstOnboarding: Self = "isFirstOnboarding" static let inviteCode: Self = "inviteCode" @@ -22,6 +23,8 @@ public extension UserDefaultsWrapper.Key { static let familyId: Self = "familyId" static let familyCreatedAt: Self = "familyCreatedAt" static let familyName: Self = "familyName" + static let familyMembers: Self = "familyMembers" + static let familyMemberIds: Self = "familyMemberIds" // MARK: - My UserDefaults @@ -33,5 +36,4 @@ public extension UserDefaultsWrapper.Key { // MARK: - Comment UserDefaults static let commentSnapshot: Self = "commentSnapshot" - } 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 7ae5eb5dc..2a6d36660 100644 --- a/14th-team5-iOS/Data/Sources/Storages/UserDefaults/AppUserDefaults/AppUserDefaults.swift +++ b/14th-team5-iOS/Data/Sources/Storages/UserDefaults/AppUserDefaults/AppUserDefaults.swift @@ -19,12 +19,19 @@ public protocol AppUserDefaultsType: UserDefaultsType { func saveIsFirstShowWidgetAlert(_ value: Bool?) func loadIsFirstShowWidgetAlert() -> Bool? - func saveInviteCode(_ inviteCode: String?) + func saveIsFirstOnboarding(_ value: Bool) + func loadIsFirstOnboarding() -> Bool + + func saveIsFirstFamilyManagement(_ value: Bool) + func loadIsFirstFamilyManagement() -> Bool + + func saveInviteCode(_ inviteCode: String) func loadInviteCode() -> String? + func deleteInvitedCode() } final public class AppUserDefaults: AppUserDefaultsType { - + // MARK: - Intializer public init() { } @@ -74,7 +81,7 @@ final public class AppUserDefaults: AppUserDefaultsType { // MARK: - Invite Code - public func saveInviteCode(_ inviteCode: String?) { + public func saveInviteCode(_ inviteCode: String) { userDefaults[.inviteCode] = inviteCode } @@ -85,9 +92,39 @@ final public class AppUserDefaults: AppUserDefaultsType { return inviteCode } + public func deleteInvitedCode() { + userDefaults.remove(forKey: .inviteCode) + } + + // MARK: - Onboarding + public func saveIsFirstOnboarding(_ value: Bool) { + userDefaults[.isFirstOnboarding] = value + } + public func loadIsFirstOnboarding() -> Bool { + guard let isFirstOnboarding: Bool = userDefaults[.isFirstOnboarding] else { + return false + } + return isFirstOnboarding + } + + // MARK: - FamilyManagement + + public func saveIsFirstFamilyManagement(_ value: Bool) { + userDefaults[.isFirstFamilyManagement] = value + } + + public func loadIsFirstFamilyManagement() -> Bool { + guard let isFirstFamilyManagement: Bool = userDefaults[.isFirstFamilyManagement] else { + return false + } + + return isFirstFamilyManagement + } + + } diff --git a/14th-team5-iOS/Data/Sources/Storages/UserDefaults/FamilyUserDefaults/FamilyUserDefaults.swift b/14th-team5-iOS/Data/Sources/Storages/UserDefaults/FamilyUserDefaults/FamilyUserDefaults.swift index 67aa6d50c..864550839 100644 --- a/14th-team5-iOS/Data/Sources/Storages/UserDefaults/FamilyUserDefaults/FamilyUserDefaults.swift +++ b/14th-team5-iOS/Data/Sources/Storages/UserDefaults/FamilyUserDefaults/FamilyUserDefaults.swift @@ -5,11 +5,21 @@ // Created by 김건우 on 6/2/24. // -import Core import Foundation +import Core +import Domain + public protocol FamilyInfoUserDefaultsType: UserDefaultsType { + typealias Profile = FamilyMemberProfileEntity + + func loadFamilyMember(_ memberId: String) -> Profile? + + func saveFamilyMembers(_ members: [Profile]) + func loadFamilyMembers() -> [Profile]? + func deleteFamilyMembers() + func saveFamilyId(_ familyId: String?) func loadFamilyId() -> String? @@ -22,11 +32,40 @@ public protocol FamilyInfoUserDefaultsType: UserDefaultsType { final public class FamilyInfoUserDefaults: FamilyInfoUserDefaultsType { - + + // MARK: - Intializer public init() { } + // MARK: - FamilyMember + + public func loadFamilyMember(_ memberId: String) -> Profile? { + guard let familyMembers = loadFamilyMembers() else { + return nil + } + + let member = familyMembers.filter { $0.memberId == memberId } + return member.first + } + + // MARK: - FamilyMembers + + public func saveFamilyMembers(_ members: [Profile]) { + userDefaults[.familyMembers] = members + } + + public func loadFamilyMembers() -> [Profile]? { + guard let value: [Profile] = userDefaults[.familyMembers] else { + return nil + } + return value + } + + public func deleteFamilyMembers() { + userDefaults.remove(forKey: .familyMembers) + } + // MARK: - Family Id From 2d6df60c5735f9ec6d2ee40cc5e32af57c472de1 Mon Sep 17 00:00:00 2001 From: Kyoungmi <1mmgm.dev@gmail.com> Date: Sun, 1 Sep 2024 11:38:26 +0900 Subject: [PATCH 201/263] feat: add remove method(#615) --- .../BBStorages/UserDefaultsWrapper/UserDefaultsType.swift | 5 +++++ .../UserDefaults/AppUserDefaults/AppUserDefaults.swift | 5 +++-- .../UserDefaults/FamilyUserDefaults/FamilyUserDefaults.swift | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/UserDefaultsWrapper/UserDefaultsType.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/UserDefaultsWrapper/UserDefaultsType.swift index 1ede28fd8..b530c5817 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/UserDefaultsWrapper/UserDefaultsType.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/UserDefaultsWrapper/UserDefaultsType.swift @@ -9,10 +9,15 @@ import Foundation public protocol UserDefaultsType { var userDefaults: UserDefaultsWrapper { get } + func remove(forKey key: UserDefaultsWrapper.Key) } extension UserDefaultsType { public var userDefaults: UserDefaultsWrapper { UserDefaultsWrapper.standard } + + public func remove(forKey key: UserDefaultsWrapper.Key) { + UserDefaultsWrapper.standard.remove(forKey: key) + } } 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 2a6d36660..b10dfeb3f 100644 --- a/14th-team5-iOS/Data/Sources/Storages/UserDefaults/AppUserDefaults/AppUserDefaults.swift +++ b/14th-team5-iOS/Data/Sources/Storages/UserDefaults/AppUserDefaults/AppUserDefaults.swift @@ -92,8 +92,9 @@ final public class AppUserDefaults: AppUserDefaultsType { return inviteCode } - public func deleteInvitedCode() { - userDefaults.remove(forKey: .inviteCode) + + public func deleteInviteCode() { + remove(forKey: .inviteCode) } diff --git a/14th-team5-iOS/Data/Sources/Storages/UserDefaults/FamilyUserDefaults/FamilyUserDefaults.swift b/14th-team5-iOS/Data/Sources/Storages/UserDefaults/FamilyUserDefaults/FamilyUserDefaults.swift index 864550839..58bcc5233 100644 --- a/14th-team5-iOS/Data/Sources/Storages/UserDefaults/FamilyUserDefaults/FamilyUserDefaults.swift +++ b/14th-team5-iOS/Data/Sources/Storages/UserDefaults/FamilyUserDefaults/FamilyUserDefaults.swift @@ -63,7 +63,7 @@ final public class FamilyInfoUserDefaults: FamilyInfoUserDefaultsType { } public func deleteFamilyMembers() { - userDefaults.remove(forKey: .familyMembers) + remove(forKey: .familyMembers) } From 4960835da3607f5cc5d7f594b93a9df350b1e7c9 Mon Sep 17 00:00:00 2001 From: Kyoungmi <1mmgm.dev@gmail.com> Date: Sun, 1 Sep 2024 11:39:40 +0900 Subject: [PATCH 202/263] rename method(#615) --- .../Storages/UserDefaults/AppUserDefaults/AppUserDefaults.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 b10dfeb3f..32edbb73f 100644 --- a/14th-team5-iOS/Data/Sources/Storages/UserDefaults/AppUserDefaults/AppUserDefaults.swift +++ b/14th-team5-iOS/Data/Sources/Storages/UserDefaults/AppUserDefaults/AppUserDefaults.swift @@ -93,7 +93,7 @@ final public class AppUserDefaults: AppUserDefaultsType { } - public func deleteInviteCode() { + public func deleteInvitedCode() { remove(forKey: .inviteCode) } From 3b14d8fc9931e1fa5dcb380305607caa52609252 Mon Sep 17 00:00:00 2001 From: g_mi <62610032+akrudal@users.noreply.github.com> Date: Wed, 4 Sep 2024 03:25:06 +0900 Subject: [PATCH 203/263] =?UTF-8?q?=EB=B0=A4=EC=97=90=20=EC=BB=A8=ED=8A=B8?= =?UTF-8?q?=EB=A6=AC=EB=B7=B0=ED=84=B0=20=EB=B7=B0=EA=B0=80=20=EB=B3=B4?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EB=AC=B8=EC=A0=9C?= =?UTF-8?q?=EB=A5=BC=20=ED=95=B4=EA=B2=B0=ED=95=A9=EB=8B=88=EB=8B=A4.=20(#?= =?UTF-8?q?624)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: response dto(#620) * fix: bind sync(#620) * fix: bind sync(#620) * fix: button background style(#620) * fix: present contributor view(#620) * remove comment(#620) * remove comment(#620) * setup reactor(#620) --- .../Home/Reactor/ContributorReactor.swift | 10 ++------ .../Home/Reactor/MainViewReactor.swift | 5 +++- .../ViewControllers/MainViewController.swift | 2 +- .../Home/Views/ContributorView.swift | 22 ++++++++++------ .../Sources/Base/BaseViewController.swift | 18 +++++++++---- .../Sources/Base/ReactorViewController.swift | 16 +++++++++--- .../Bibbi/BBCommons/BBButton/BBButton.swift | 25 +++++++++++++++++++ .../DataMapping/MainNightResponseDTO.swift | 2 +- .../MainView/NightMainViewEntity.swift | 6 ++--- 9 files changed, 76 insertions(+), 30 deletions(-) diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/ContributorReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/ContributorReactor.swift index 038016b08..de71693c9 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/ContributorReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/ContributorReactor.swift @@ -21,10 +21,7 @@ final class ContributorReactor: Reactor { } struct State { - var month: Int = Date().month - @Pulse var firstRanker: RankerData? = nil - @Pulse var secondRanker: RankerData? = nil - @Pulse var thirdRanker: RankerData? = nil + @Pulse var rank: FamilyRankData = .empty } let initialState: State = State() @@ -43,10 +40,7 @@ extension ContributorReactor { switch mutation { case .updateState(let contributor): - newState.month = contributor.month - newState.firstRanker = contributor.firstRanker - newState.secondRanker = contributor.secondRanker - newState.thirdRanker = contributor.thirdRanker + newState.rank = contributor } return newState diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift index 1043d7f3f..746bfbcb9 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift @@ -225,7 +225,10 @@ extension MainViewReactor { case .navigationLeftButtonTap: return Observable.just(.showNextView(.familyManagementViewController)) case .contributorNextButtonTap: - return Observable.just(.showNextView(.weeklycalendarViewController(currentState.contributor.recentPostDate))) + guard let date = currentState.contributor.recentPostDate else { + return .empty() + } + return Observable.just(.showNextView(.weeklycalendarViewController(date))) } case .checkMissionAlert(let isUnlocked, let isMeSurvivalUploadedToday): if isUnlocked && isMeSurvivalUploadedToday { 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 871a15082..64b385d13 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift @@ -36,6 +36,7 @@ final class MainViewController: BaseViewController, UICollectio } override func bind(reactor: MainViewReactor) { + print("bind main reactor") super.bind(reactor: reactor) bindInput(reactor: reactor) bindOutput(reactor: reactor) @@ -172,7 +173,6 @@ extension MainViewController { private func bindOutput(reactor: MainViewReactor) { reactor.state.map { $0.isInTime }.compactMap { $0 } - .debug("isInTime") .distinctUntilChanged() .withUnretained(self) .observe(on: MainScheduler.instance) diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Views/ContributorView.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Views/ContributorView.swift index 4ab22de7f..8038e129e 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Views/ContributorView.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Views/ContributorView.swift @@ -115,8 +115,9 @@ final class ContributorView: BaseView { $0.layer.cornerRadius = 8 $0.setTitle("지난 날 생존신고 보기", for: .normal) $0.setTitleColor(.bibbiBlack, for: .normal) - $0.backgroundColor = .mainYellow $0.setTitleFontStyle(.body1Bold) + $0.setButtonBackgroundColor(.mainYellow, for: .normal) + $0.setButtonBackgroundColor(.gray400, for: .disabled) } } } @@ -129,22 +130,29 @@ extension ContributorView { } private func bindOutput(reactor: ContributorReactor) { - reactor.state.map { $0.month } - .distinctUntilChanged() - .map { "\($0)월 생존신고 횟수" } + reactor.pulse(\.$rank) + .map { "\($0.month)월 생존신고 횟수" } .bind(to: subTitleLabel.rx.text) .disposed(by: disposeBag) - reactor.pulse(\.$firstRanker) + reactor.pulse(\.$rank) + .map { $0.firstRanker } .bind(to: firstProfileView.rankerRelay) .disposed(by: disposeBag) - reactor.pulse(\.$secondRanker) + reactor.pulse(\.$rank) + .map { $0.secondRanker } .bind(to: secondProfileView.rankerRelay) .disposed(by: disposeBag) - reactor.pulse(\.$thirdRanker) + reactor.pulse(\.$rank) + .map { $0.thirdRanker } .bind(to: thirdProfileView.rankerRelay) .disposed(by: disposeBag) + + reactor.pulse(\.$rank) + .map { $0.recentPostDate != nil } + .bind(to: nextButton.rx.isEnabled) + .disposed(by: disposeBag) } } diff --git a/14th-team5-iOS/Core/Sources/Base/BaseViewController.swift b/14th-team5-iOS/Core/Sources/Base/BaseViewController.swift index 152bb8754..a04876e5b 100644 --- a/14th-team5-iOS/Core/Sources/Base/BaseViewController.swift +++ b/14th-team5-iOS/Core/Sources/Base/BaseViewController.swift @@ -14,11 +14,12 @@ import RxSwift @available(*, deprecated, renamed: "ReactorViewController") open class BaseViewController: UIViewController, ReactorKit.View where R: Reactor { public typealias Reactor = R - + // MARK: - Properties + private var initialReactor: Reactor? public var disposeBag: RxSwift.DisposeBag = DisposeBag() public let navigationBarView: BibbiNavigationBarView = BibbiNavigationBarView() - + // MARK: - Intializer public init() { super.init(nibName: nil, bundle: nil) @@ -26,7 +27,7 @@ open class BaseViewController: UIViewController, ReactorKit.View where R: Rea public convenience init(reactor: Reactor? = nil) { self.init() - self.reactor = reactor + self.initialReactor = reactor } required public init?(coder: NSCoder) { @@ -39,6 +40,7 @@ open class BaseViewController: UIViewController, ReactorKit.View where R: Rea setupUI() setupAutoLayout() setupAttributes() + setupReactor() } // MARK: - Helpers @@ -53,15 +55,21 @@ open class BaseViewController: UIViewController, ReactorKit.View where R: Rea self.navigationController?.popViewController(animated: true) case .xmark: self.navigationController?.popViewController(animated: true) -// self.dismiss(animated: true) { self.dismissCompletion() } + // self.dismiss(animated: true) { self.dismissCompletion() } default: break } }) .disposed(by: disposeBag) } + open func setupReactor() { + if let reactor = initialReactor { + self.reactor = reactor + } + } + /// 서브 뷰 추가를 위한 메서드 - open func setupUI() { + open func setupUI() { view.addSubview(navigationBarView) } diff --git a/14th-team5-iOS/Core/Sources/Base/ReactorViewController.swift b/14th-team5-iOS/Core/Sources/Base/ReactorViewController.swift index 8c83e9b24..e4e106dbc 100644 --- a/14th-team5-iOS/Core/Sources/Base/ReactorViewController.swift +++ b/14th-team5-iOS/Core/Sources/Base/ReactorViewController.swift @@ -15,10 +15,11 @@ open class ReactorViewController: UIViewController, ReactorKit.View where R: // MARK: - Typealias public typealias Reactor = R - + // MARK: - Properties + private var initialReactor: Reactor? public var disposeBag: RxSwift.DisposeBag = DisposeBag() - + // MARK: - Intializer public init() { super.init(nibName: nil, bundle: nil) @@ -26,7 +27,7 @@ open class ReactorViewController: UIViewController, ReactorKit.View where R: public convenience init(reactor: Reactor? = nil) { self.init() - self.reactor = reactor + self.initialReactor = reactor } required public init?(coder: NSCoder) { @@ -39,6 +40,7 @@ open class ReactorViewController: UIViewController, ReactorKit.View where R: setupUI() setupAutoLayout() setupAttributes() + setupReactor() } // MARK: - Helpers @@ -48,7 +50,13 @@ open class ReactorViewController: UIViewController, ReactorKit.View where R: open func setupAutoLayout() { } - open func setupAttributes() { + open func setupAttributes() { view.backgroundColor = .bibbiBlack } + + open func setupReactor() { + if let reactor = initialReactor { + self.reactor = reactor + } + } } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBButton/BBButton.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBButton/BBButton.swift index 178a0a04f..a70cf66f8 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBButton/BBButton.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBButton/BBButton.swift @@ -9,6 +9,10 @@ import UIKit import SnapKit +extension UIControl.State: Hashable { + +} + public class BBButton: UIButton { // MARK: - Views @@ -18,12 +22,19 @@ public class BBButton: UIButton { // MARK: - Properties public var id: Int? + private var backgroundColors: [UIControl.State: UIColor] = [:] public override var titleLabel: UILabel? { get { mainTitleLabel } set { } } + public override var isEnabled: Bool { + didSet { + updateBackgroundColor() + } + } + public override var isHighlighted: Bool { didSet { guard @@ -36,6 +47,7 @@ public class BBButton: UIButton { } } + // MARK: - Intializer public override init(frame: CGRect) { @@ -71,6 +83,19 @@ public class BBButton: UIButton { } } + private func updateBackgroundColor() { + if let color = backgroundColors[state] { + self.backgroundColor = color + } else if let normalColor = backgroundColors[.normal] { + self.backgroundColor = normalColor + } + } + + public func setButtonBackgroundColor(_ backgroundColor: UIColor, for state: UIControl.State) { + backgroundColors[state] = backgroundColor + updateBackgroundColor() + } + /// 버튼이 가진 고유한 ID값을 변경합니다. public func setId(_ id: Int) { diff --git a/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/DataMapping/MainNightResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/DataMapping/MainNightResponseDTO.swift index e67bf15a4..94634cd54 100644 --- a/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/DataMapping/MainNightResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/DataMapping/MainNightResponseDTO.swift @@ -24,7 +24,7 @@ struct FamilyMemberMonthlyRanking: Codable { let firstRanker: Ranker? let secondRanker: Ranker? let thirdRanker: Ranker? - let mostRecentSurvivalPostDate: String + let mostRecentSurvivalPostDate: String? func toDomain() -> FamilyRankData { return .init(month: month, diff --git a/14th-team5-iOS/Domain/Sources/Entities/MainView/NightMainViewEntity.swift b/14th-team5-iOS/Domain/Sources/Entities/MainView/NightMainViewEntity.swift index fa25cd74b..c91dd491e 100644 --- a/14th-team5-iOS/Domain/Sources/Entities/MainView/NightMainViewEntity.swift +++ b/14th-team5-iOS/Domain/Sources/Entities/MainView/NightMainViewEntity.swift @@ -57,12 +57,12 @@ public struct RankerData { public struct FamilyRankData { public let month: Int - public let recentPostDate: String + public let recentPostDate: String? public let firstRanker: RankerData? public let secondRanker: RankerData? public let thirdRanker: RankerData? - public init(month: Int, recentPostDate: String, firstRanker: RankerData?, secondRanker: RankerData?, thirdRanker: RankerData?) { + public init(month: Int, recentPostDate: String?, firstRanker: RankerData?, secondRanker: RankerData?, thirdRanker: RankerData?) { self.month = month self.recentPostDate = recentPostDate self.firstRanker = firstRanker @@ -73,7 +73,7 @@ public struct FamilyRankData { extension FamilyRankData { public static var empty: FamilyRankData { - return .init(month: 0, recentPostDate: "", firstRanker: nil, secondRanker: nil, thirdRanker: nil) + return .init(month: 0, recentPostDate: nil, firstRanker: nil, secondRanker: nil, thirdRanker: nil) } } From f2df551f54e5a86794a32e44e2de5dc50786964e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=84?= Date: Tue, 10 Sep 2024 12:10:41 +0900 Subject: [PATCH 204/263] =?UTF-8?q?feat:=20JoinFamilyGroupNameViewControll?= =?UTF-8?q?er=20=ED=99=94=EB=A9=B4=20=EA=B5=AC=ED=98=84=20(#611)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 14th-team5-iOS/App/Project.swift | 2 +- .../DIContainer/FamilyDIContainer.swift | 9 + ...milyNameSettingViewControllerWrapper.swift | 33 +++ .../FamilyNameSettingViewReactor.swift | 132 +++++++++ .../View/JoinFamilyGroupEditorView.swift | 76 +++++ .../FamilyManagementViewController.swift | 22 +- .../FamilyNameSettingViewController.swift | 278 ++++++++++++++++++ .../FamilyGroupInfoResponseDTO.swift | 28 ++ .../DataMapping/FamilyNameResponseDTO.swift | 10 +- .../UpdateFamilyNameRequestDTO.swift | 2 +- .../Family/FamilyAPI/FamilyAPIWorker.swift | 16 +- .../APIs/Family/FamilyAPI/FamilyAPIs.swift | 3 + .../Family/Repository/FamilyRepository.swift | 9 + .../Member/Repository/MemberRepository.swift | 4 + .../UserDefaults/FamilyUserDefautls.swift | 9 + .../Family/FamilyGroupInfoEntity.swift | 28 ++ .../Entities/Family/FamilyNameEntity.swift | 6 +- .../Family/UpdateFamilyNameRequest.swift | 4 +- .../Repositories/FamilyRepository.swift | 1 + .../Repositories/MemberRepository.swift | 1 + .../Trash/Member/UseCases/MemberUseCase.swift | 5 + .../Family/FetchFamilyGroupInfoUseCase.swift | 27 ++ 22 files changed, 690 insertions(+), 15 deletions(-) create mode 100644 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/JoinFamily/FamilyNameSettingViewControllerWrapper.swift create mode 100644 14th-team5-iOS/App/Sources/Presentation/FamilyManagement/Reactor/FamilyNameSettingViewReactor.swift create mode 100644 14th-team5-iOS/App/Sources/Presentation/FamilyManagement/View/JoinFamilyGroupEditorView.swift create mode 100644 14th-team5-iOS/App/Sources/Presentation/FamilyManagement/ViewController/FamilyNameSettingViewController.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/FamilyGroupInfoResponseDTO.swift create mode 100644 14th-team5-iOS/Domain/Sources/Entities/Family/FamilyGroupInfoEntity.swift create mode 100644 14th-team5-iOS/Domain/Sources/UseCases/Family/FetchFamilyGroupInfoUseCase.swift diff --git a/14th-team5-iOS/App/Project.swift b/14th-team5-iOS/App/Project.swift index 9bd40d87e..8a32b42f4 100644 --- a/14th-team5-iOS/App/Project.swift +++ b/14th-team5-iOS/App/Project.swift @@ -22,7 +22,7 @@ private let targets: [Target] = [ "CFBundleShortVersionString": .string("1.2"), "UILaunchStoryboardName": .string("LaunchScreen.storyboard"), "UISupportedInterfaceOrientations": .array([.string("UIInterfaceOrientationPortrait")]), - "UIUserInterfaceStyle": .string("Light"), + "UIUserInterfaceStyle": .string("Dark"), "NSPhotoLibraryAddUsageDescription" : .string("프로필 사진, 피드 업로드를 위한 사진 촬영을 위해 Bibbi가 앨범에 접근할 수 있도록 허용해 주세요"), "NSCameraUsageDescription": .string("프로필 사진, 피드 업로드를 위한 사진 촬영을 위해 Bibbi가 카메라에 접근할 수 있도록 허용해 주세요"), "UIApplicationSceneManifest" : .dictionary([ diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/FamilyDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/FamilyDIContainer.swift index 7c6a48c2d..69f90cba7 100644 --- a/14th-team5-iOS/App/Sources/Application/DIContainer/FamilyDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/FamilyDIContainer.swift @@ -53,11 +53,16 @@ final class FamilyDIContainer: BaseContainer { UpdateFamilyNameUseCase(familyRepository: repository) } + private func makeFetchFamilyGroupInfoUseCase() -> FetchFamilyGroupInfoUseCaseProtocol { + FetchFamilyGroupInfoUseCase(familyRepository: repository) + } + // Deprecated private func makeFamilyUseCase() -> FamilyUseCaseProtocol { FamilyUseCase(familyRepository: repository) } + // MARK: - Register func registerDependencies() { @@ -97,6 +102,10 @@ final class FamilyDIContainer: BaseContainer { makeUpdateFamilyNameUseCase() } + container.register(type: FetchFamilyGroupInfoUseCaseProtocol.self) { _ in + makeFetchFamilyGroupInfoUseCase() + } + // Deprecated container.register(type: FamilyUseCaseProtocol.self) { _ in makeFamilyUseCase() diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/JoinFamily/FamilyNameSettingViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/JoinFamily/FamilyNameSettingViewControllerWrapper.swift new file mode 100644 index 000000000..186900cf2 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/JoinFamily/FamilyNameSettingViewControllerWrapper.swift @@ -0,0 +1,33 @@ +// +// FamilyNameSettingViewControllerWrapper.swift +// App +// +// Created by Kim dohyun on 7/23/24. +// + +import Core +import Foundation + + +final class FamilyNameSettingViewControllerWrapper: BaseWrapper { + + typealias R = FamilyNameSettingViewReactor + typealias V = FamilyNameSettingViewController + + + func makeReactor() -> FamilyNameSettingViewReactor { + return FamilyNameSettingViewReactor() + } + + func makeViewController() -> FamilyNameSettingViewController { + return FamilyNameSettingViewController(reactor: reactor) + } + + var reactor: FamilyNameSettingViewReactor { + return makeReactor() + } + + var viewController: FamilyNameSettingViewController { + return makeViewController() + } +} diff --git a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/Reactor/FamilyNameSettingViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/Reactor/FamilyNameSettingViewReactor.swift new file mode 100644 index 000000000..9b29d3b86 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/Reactor/FamilyNameSettingViewReactor.swift @@ -0,0 +1,132 @@ +// +// FamilyNameSettingViewReactor.swift +// App +// +// Created by Kim dohyun on 7/23/24. +// + +import Foundation + +import Core +import Domain +import ReactorKit + +public final class FamilyNameSettingViewReactor: Reactor { + public var initialState: State + @Injected private var fetchFamilyGroupInfoUseCase: any FetchFamilyGroupInfoUseCaseProtocol + @Injected private var updateFamilyNameUseCase: any UpdateFamilyNameUseCaseProtocol + @Injected private var fetchFamilyEditerUseCase: any FetchMembersProfileUseCaseProtocol + + public enum FamilyNameUpdateType { + case initial + case update + } + + public enum Action { + case viewDidLoad + case didChangeFamilyGroupNickname(String) + case didTapUpdateFamilyGroupNickname(FamilyNameUpdateType) + } + + public enum Mutation { + case setFamilyGroupNameEditerItem(MembersProfileEntity) + case setFamilyGroupInfoItem(FamilyGroupInfoEntity) + case setFamilyGroupNickName(String) + case setFamilyNickNameVaildation(Bool) + case setFamilyGroupEditValidation(Bool) + case setFamilyNickNameMaximumValidation(Bool) + case setUpdateFamilyNameItem(FamilyNameEntity) + } + + public struct State { + @Pulse var familyNameEntity: FamilyNameEntity? + @Pulse var familyNameEditorEntity: MembersProfileEntity? + @Pulse var familyGroupInfoEntity: FamilyGroupInfoEntity? + var familyGroupNickName: String + var isNickNameVaildation: Bool + var isEdit: Bool + var isNickNameMaximumValidation: Bool + } + + init() { + self.initialState = State( + familyGroupNickName: "", + isNickNameVaildation: false, + isEdit: false, + isNickNameMaximumValidation: true + ) + } + + public func mutate(action: Action) -> Observable { + switch action { + case .viewDidLoad: + + return fetchFamilyGroupInfoUseCase + .execute() + .asObservable() + .compactMap { $0 } + .withUnretained(self) + .flatMap { owner, familyGroupInfo -> Observable in + if familyGroupInfo.familyNameEditorId.isEmpty { + return .concat( + .just(.setFamilyNickNameVaildation(false)), + .just(.setFamilyGroupEditValidation(true)), + .just(.setFamilyGroupInfoItem(familyGroupInfo)) + ) + } + let editorId = familyGroupInfo.familyNameEditorId + return owner.fetchFamilyEditerUseCase.execute(memberId: editorId) + .asObservable() + .compactMap { $0 } + .flatMap { editorInfo -> Observable in + return .concat( + .just(.setFamilyGroupNameEditerItem(editorInfo)), + .just(.setFamilyGroupEditValidation(false)) + ) + } + + } + case let .didChangeFamilyGroupNickname(familyGroupNickName): + let isValidation = familyGroupNickName.count > 0 && familyGroupNickName.count < 10 ? true : false + let isMaximumValidation = familyGroupNickName.count < 10 ? true : false + + return .concat( + .just(.setFamilyGroupNickName(familyGroupNickName)), + .just(.setFamilyNickNameVaildation(isValidation)), + .just(.setFamilyNickNameMaximumValidation(isMaximumValidation)) + ) + case let .didTapUpdateFamilyGroupNickname(type): + let familyName = type == .initial ? nil : currentState.familyGroupNickName + let updateFamilyBody = UpdateFamilyNameRequest(familyName: familyName) + return updateFamilyNameUseCase.execute(body: updateFamilyBody) + .asObservable() + .compactMap { $0 } + .flatMap { familyGroupNameEntity -> Observable in + return .just(.setUpdateFamilyNameItem(familyGroupNameEntity)) + } + } + } + + public func reduce(state: State, mutation: Mutation) -> State { + var newState = state + + switch mutation { + case let .setFamilyGroupNickName(familyGroupNickName): + newState.familyGroupNickName = familyGroupNickName + case let .setFamilyNickNameVaildation(isNickNameVaildation): + newState.isNickNameVaildation = isNickNameVaildation + case let .setFamilyNickNameMaximumValidation(isNickNameMaximumValidation): + newState.isNickNameMaximumValidation = isNickNameMaximumValidation + case let .setUpdateFamilyNameItem(familyNameEntity): + newState.familyNameEntity = familyNameEntity + case let .setFamilyGroupNameEditerItem(familyNameEditorEntity): + newState.familyNameEditorEntity = familyNameEditorEntity + case let .setFamilyGroupEditValidation(isEdit): + newState.isEdit = isEdit + case let .setFamilyGroupInfoItem(familyGroupInfoEntity): + newState.familyGroupInfoEntity = familyGroupInfoEntity + } + return newState + } + +} diff --git a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/View/JoinFamilyGroupEditorView.swift b/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/View/JoinFamilyGroupEditorView.swift new file mode 100644 index 000000000..54c3a24d5 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/View/JoinFamilyGroupEditorView.swift @@ -0,0 +1,76 @@ +// +// JoinFamilyGroupEdtiorView.swift +// App +// +// Created by Kim dohyun on 8/11/24. +// + +import Core +import DesignSystem +import UIKit + + +final class JoinFamilyGroupEdtiorView: UIView { + + //MARK: Properties + private let descrptionLabel: BBLabel = BBLabel(.body1Regular, textAlignment: .center, textColor: .gray400) + let profileNameLabel: BBLabel = BBLabel(.head2Bold, textAlignment: .center, textColor: .gray200) + let profileView: UIImageView = UIImageView() + let userNameLabel: BBLabel = BBLabel(.body1Regular, textAlignment: .center, textColor: .gray400) + + + + override init(frame: CGRect) { + super.init(frame: frame) + setupUI() + setupAutoLayout() + setupAttributes() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + //MARK: Configure + private func setupUI() { + addSubviews(descrptionLabel, profileView, userNameLabel, profileNameLabel) + } + + private func setupAutoLayout() { + descrptionLabel.snp.makeConstraints { + $0.width.equalTo(80) + $0.top.left.bottom.equalToSuperview() + } + + profileNameLabel.snp.makeConstraints { + $0.center.equalTo(profileView) + } + + profileView.snp.makeConstraints { + $0.size.equalTo(24) + $0.left.equalTo(descrptionLabel.snp.right).offset(8) + $0.centerY.equalToSuperview() + } + + userNameLabel.snp.makeConstraints { + $0.left.equalTo(profileView.snp.right).offset(4) + $0.height.equalTo(24) + $0.right.equalToSuperview() + $0.centerY.equalToSuperview() + } + + } + + private func setupAttributes() { + descrptionLabel.do { + $0.text = "마지막 수정 :" + } + + profileView.do { + $0.layer.cornerRadius = 24 / 2 + $0.clipsToBounds = true + $0.backgroundColor = DesignSystemAsset.gray800.color + } + } +} diff --git a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/ViewController/FamilyManagementViewController.swift b/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/ViewController/FamilyManagementViewController.swift index 8f3ace404..a62d76712 100644 --- a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/ViewController/FamilyManagementViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/ViewController/FamilyManagementViewController.swift @@ -25,6 +25,7 @@ public final class FamilyManagementViewController: BBNavigationViewController { + + //MARK: Properties + private let groupNameLabel: BBLabel = BBLabel(.head2Bold, textAlignment: .center, textColor: .gray300) + private let groupConfirmButton: BBButton = BBButton() + private let groupTextField: UITextField = UITextField() + private let groupDescrptionLabel: BBLabel = BBLabel(.body1Regular, textColor: .gray400) + private let groupErrorStackView: UIStackView = UIStackView() + private let groupErrorImageView: UIImageView = UIImageView() + private let groupErrorLabel: BBLabel = BBLabel(.body1Regular, textColor: .warningRed) + private let groupEditerView: JoinFamilyGroupEdtiorView = JoinFamilyGroupEdtiorView() + + + //MARK: Configures + override func setupUI() { + super.setupUI() + groupErrorStackView.addArrangedSubviews(groupErrorImageView, groupErrorLabel) + view.addSubviews(groupNameLabel, groupDescrptionLabel, groupTextField, groupConfirmButton, groupErrorStackView, groupEditerView) + } + + override func setupAutoLayout() { + super.setupAutoLayout() + groupNameLabel.snp.makeConstraints { + $0.top.equalTo(view.safeAreaLayoutGuide).offset(130) + $0.height.equalTo(25) + $0.centerX.equalToSuperview().inset(20) + } + + groupTextField.snp.makeConstraints { + $0.top.equalTo(groupNameLabel.snp.bottom).offset(8) + $0.centerX.equalToSuperview().inset(20) + } + + groupErrorStackView.snp.makeConstraints { + $0.top.equalTo(groupTextField.snp.bottom).offset(12) + $0.height.equalTo(24) + $0.centerX.equalToSuperview() + } + + groupErrorImageView.snp.makeConstraints { + $0.size.equalTo(20) + $0.left.equalToSuperview() + } + + groupDescrptionLabel.snp.makeConstraints { + $0.centerX.equalToSuperview() + $0.bottom.equalTo(groupConfirmButton.snp.top).offset(-14) + } + + groupEditerView.snp.makeConstraints { + $0.width.equalTo(171) + $0.height.equalTo(24) + $0.bottom.equalTo(groupConfirmButton.snp.top).offset(-14) + $0.centerX.equalToSuperview() + } + + groupConfirmButton.snp.makeConstraints { + $0.horizontalEdges.equalToSuperview().inset(12) + $0.bottom.equalTo(view.keyboardLayoutGuide.snp.top).offset(-10) + $0.height.equalTo(56) + } + } + + override func setupAttributes() { + super.setupAttributes() + + groupNameLabel.do { + $0.text = "가족 방 이름을 입력해주세요" + } + + groupErrorStackView.do { + $0.spacing = 4 + $0.axis = .horizontal + $0.distribution = .fill + $0.isHidden = true + } + + groupErrorLabel.do { + $0.text = "10자 이내로 입력해주세요" + } + + groupErrorImageView.do { + $0.contentMode = .scaleAspectFill + $0.image = DesignSystemAsset.warning.image + } + + groupTextField.do { + $0.makePlaceholderAttributedString("나의 가족", attributed: [ + .font: DesignSystemFontFamily.Pretendard.bold.font(size: 36), + .foregroundColor: DesignSystemAsset.gray700.color + ]) + $0.font = DesignSystemFontFamily.Pretendard.bold.font(size: 36) + $0.textColor = DesignSystemAsset.gray200.color + $0.autocorrectionType = .no + $0.spellCheckingType = .no + } + + groupDescrptionLabel.do { + $0.text = "홈 화면에 가족 이름이 추가돼요 " + } + + groupEditerView.do { + $0.isHidden = true + } + + groupConfirmButton.do { + $0.setTitle("저장", for: .normal) + $0.setTitleFontStyle(.body1Bold) + $0.setTitleColor(.bibbiBlack, for: .normal) + $0.backgroundColor = .mainYellow.withAlphaComponent(0.2) + $0.isEnabled = false + $0.layer.cornerRadius = 28 + } + } + + override func bind(reactor: FamilyNameSettingViewReactor) { + super.bind(reactor: reactor) + + + Observable.just(()) + .map { Reactor.Action.viewDidLoad } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + navigationBar.rx.didTapRightBarButton + .bind(with: self) { owner, _ in + let alert = BBAlert.style(.resetFamilyName) + alert.addDelegate(owner) + alert.show() + } + .disposed(by: disposeBag) + + groupTextField.rx.text + .orEmpty + .skip(1) + .debounce(.milliseconds(300), scheduler: MainScheduler.instance) + .map { Reactor.Action.didChangeFamilyGroupNickname($0)} + .bind(to: reactor.action) + .disposed(by: disposeBag) + + groupConfirmButton.rx + .tap + .throttle(.microseconds(300), scheduler: MainScheduler.instance) + .map { Reactor.Action.didTapUpdateFamilyGroupNickname(.update) } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + groupConfirmButton + .rx.tap + .withLatestFrom(reactor.pulse(\.$familyNameEntity)) + .filter { $0 != nil } + .bind(with: self) { owner, _ in + owner.navigationController?.popViewController(animated: true) + } + .disposed(by: disposeBag) + + reactor.pulse(\.$familyNameEntity) + .filter { $0 != nil } + .bind(with: self) { owner, _ in + owner.navigationController?.popViewController(animated: true) + } + .disposed(by: disposeBag) + + reactor.pulse(\.$familyNameEditorEntity) + .compactMap { $0?.memberImage } + .distinctUntilChanged() + .bind(with: self) { owner, image in + owner.groupEditerView.profileView.kf.setImage(with: image) + } + .disposed(by: disposeBag) + + reactor.state + .map { $0.isEdit } + .distinctUntilChanged() + .bind(to: groupEditerView.rx.isHidden) + .disposed(by: disposeBag) + + reactor.state + .map { !$0.isEdit } + .distinctUntilChanged() + .bind(to: groupDescrptionLabel.rx.isHidden) + .disposed(by: disposeBag) + + reactor.pulse(\.$familyNameEditorEntity) + .compactMap { $0?.memberName } + .distinctUntilChanged() + .bind(to: groupEditerView.userNameLabel.rx.text) + .disposed(by: disposeBag) + + reactor.pulse(\.$familyNameEditorEntity) + .compactMap { $0 } + .filter { $0.memberImage.isFileURL } + .compactMap { $0.memberName.first } + .compactMap { "\($0)"} + .bind(to: groupEditerView.profileNameLabel.rx.text) + .disposed(by: disposeBag) + + reactor.pulse(\.$familyNameEditorEntity) + .compactMap { $0?.memberImage } + .filter { $0.isFileURL == false } + .bind(with: self) { owner, imageURL in + owner.groupEditerView.profileView.kf.setImage(with: imageURL) + } + .disposed(by: disposeBag) + + reactor.state + .map { $0.isEdit } + .distinctUntilChanged() + .withUnretained(self) + .observe(on: MainScheduler.asyncInstance) + .bind(onNext: { $0.0.updateNavigationBarLayout(isUpdate: $0.1) }) + .disposed(by: disposeBag) + + reactor.state + .map { $0.isNickNameVaildation } + .distinctUntilChanged() + .withUnretained(self) + .bind(onNext: { $0.0.updateVaildationLayout(isEnabled: $0.1) }) + .disposed(by: disposeBag) + + reactor.state + .map { $0.isNickNameMaximumValidation } + .distinctUntilChanged() + .withUnretained(self) + .bind(onNext: { $0.0.updateMaximumValidationLayout(isUpdate: $0.1) }) + .disposed(by: disposeBag) + } +} + +//MARK: Layout Extenions +extension FamilyNameSettingViewController { + private func updateVaildationLayout(isEnabled: Bool) { + groupConfirmButton.backgroundColor = isEnabled ? DesignSystemAsset.mainYellow.color : DesignSystemAsset.mainYellow.color.withAlphaComponent(0.2) + groupConfirmButton.isEnabled = isEnabled + } + + private func updateMaximumValidationLayout(isUpdate: Bool) { + groupErrorStackView.isHidden = isUpdate + groupTextField.textColor = isUpdate ? DesignSystemAsset.gray200.color : DesignSystemAsset.warningRed.color + } + + private func updateNavigationBarLayout(isUpdate: Bool) { + navigationBar.leftBarButtonItem = .arrowLeft + navigationBar.rightBarButtonItem = isUpdate == true ? .none : .refresh + } +} + +//MARK: Delegate Extensions +extension FamilyNameSettingViewController: BBAlertDelegate { + func willShowAlert(_ alert: Core.BBAlert) { } + + func didShowAlert(_ alert: Core.BBAlert) { } + + func willCloseAlert(_ alert: Core.BBAlert) { } + + func didCloseAlert(_ alert: Core.BBAlert) { } + + func didTapAlertButton(_ alert: BBAlert?, index: Int?, button: BBButton) { + if index == 0 { + alert?.close() + } else { + self.reactor?.action.onNext(.didTapUpdateFamilyGroupNickname(.initial)) + } + } +} 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 new file mode 100644 index 000000000..3ce302eea --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/FamilyGroupInfoResponseDTO.swift @@ -0,0 +1,28 @@ +// +// FamilyGroupInfoResponseDTO.swift +// Data +// +// Created by Kim dohyun on 9/6/24. +// + +import Foundation +import Domain + + +public struct FamilyGroupInfoResponseDTO: Decodable { + let familyId: String + let familyName: String? + let familyNameEditorId: String? + let createdAt: String +} + +extension FamilyGroupInfoResponseDTO { + func toDomain() -> FamilyGroupInfoEntity { + return .init( + familyId: familyId, + familyName: familyName ?? "", + familyNameEditorId: familyNameEditorId ?? "", + createdAt: createdAt.iso8601ToDate() + ) + } +} diff --git a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/FamilyNameResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/FamilyNameResponseDTO.swift index e34520cc7..5b412e5d7 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/FamilyNameResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/FamilyNameResponseDTO.swift @@ -12,12 +12,12 @@ public struct FamilyNameResponseDTO: Decodable { private enum CodingKeys: String, CodingKey { case familyId case familyName - case familyIdEditorId + case familyNameEditorId case createdAt } var familyId: String - var familyName: String - var familyIdEditorId: String + let familyName: String? + let familyNameEditorId: String? var createdAt: String } @@ -25,8 +25,8 @@ extension FamilyNameResponseDTO { func toDomain() -> FamilyNameEntity { return .init( familyId: self.familyId, - familyName: self.familyName, - familyIdEditorId: self.familyIdEditorId, + familyName: self.familyName ?? "", + familyNameEditorId: self.familyNameEditorId ?? "", createdAt: self.createdAt.iso8601ToDate() ) } diff --git a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/UpdateFamilyNameRequestDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/UpdateFamilyNameRequestDTO.swift index b6eff45fe..c125f23d9 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/UpdateFamilyNameRequestDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/UpdateFamilyNameRequestDTO.swift @@ -8,5 +8,5 @@ import Foundation public struct UpdateFamilyNameRequestDTO: Encodable { - let familyName: String + let familyName: String? } diff --git a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIWorker.swift index 37e6adb21..84fa2d44a 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIWorker.swift @@ -111,7 +111,6 @@ extension FamilyAPIWorker { .asSingle() } - // MARK: - Change Family Name public func updateFamilyName( @@ -120,13 +119,26 @@ extension FamilyAPIWorker { ) -> Single { let spec = FamilyAPIs.updateFamilyName(familyId).spec - return request(spec: spec, jsonEncodable: body) + let accessToken = App.Repository.token.accessToken.value?.accessToken ?? "" + //TODO: Interceptor에서 CommonHedaer 넣어주지 않고 있음 + return request(spec: spec, headers: [BibbiHeader.acceptJson, BibbiHeader.xAppKey, BibbiHeader.xAuthToken(accessToken)], jsonEncodable: body) .subscribe(on: Self.queue) .map(FamilyNameResponseDTO.self) .catchAndReturn(nil) .asSingle() } + // MARK: - Fetch Family Info + + public func fetchFamilyGroupInfo() -> Single { + let spec = FamilyAPIs.fetchFamilyInfo.spec + + return request(spec: spec) + .subscribe(on: Self.queue) + .map(FamilyGroupInfoResponseDTO.self) + .catchAndReturn(nil) + .asSingle() + } } diff --git a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIs.swift index 9cfb026ee..a546453ab 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIs.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIs.swift @@ -16,6 +16,7 @@ enum FamilyAPIs: API { case fetchFamilyCreatedAt(String) case fetchPaginationFamilyMembers(Int, Int) case updateFamilyName(String) + case fetchFamilyInfo var spec: APISpec { switch self { @@ -33,6 +34,8 @@ enum FamilyAPIs: API { return APISpec(method: .get, url: "\(BibbiAPI.hostApi)/members?type=FAMILY&page=\(page)&size=\(size)") case let .updateFamilyName(familyId): return APISpec(method: .put, url: "\(BibbiAPI.hostApi)/families/\(familyId)/name") + case .fetchFamilyInfo: + return APISpec(method: .get, url: "\(BibbiAPI.hostApi)/me/family-info") } } } diff --git a/14th-team5-iOS/Data/Sources/APIs/Family/Repository/FamilyRepository.swift b/14th-team5-iOS/Data/Sources/APIs/Family/Repository/FamilyRepository.swift index fa0e655bd..4a510af0e 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Family/Repository/FamilyRepository.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Family/Repository/FamilyRepository.swift @@ -122,6 +122,15 @@ extension FamilyRepository { let body = UpdateFamilyNameRequestDTO(familyName: body.familyName) return familyApiWorker.updateFamilyName(familyId: familyId, body: body) + .map { $0?.toDomain() } + .do(onSuccess: { + FamilyUserDefaults.saveFamilyEditorId(familyEditorId: $0?.familyNameEditorId ?? "") + }) + .asObservable() + } + + public func fetchFamilyGroupInfo() -> Observable { + return familyApiWorker.fetchFamilyGroupInfo() .map { $0?.toDomain() } .asObservable() } diff --git a/14th-team5-iOS/Data/Sources/Trash/Member/Repository/MemberRepository.swift b/14th-team5-iOS/Data/Sources/Trash/Member/Repository/MemberRepository.swift index 02a186b31..bee456c54 100644 --- a/14th-team5-iOS/Data/Sources/Trash/Member/Repository/MemberRepository.swift +++ b/14th-team5-iOS/Data/Sources/Trash/Member/Repository/MemberRepository.swift @@ -13,6 +13,10 @@ public final class MemberRepository: MemberRepositoryProtocol { } extension MemberRepository { + public func fetchFamilyNameEditorId() -> String { + return FamilyUserDefaults.loadFamilyNameEditorId() + } + public func fetchUserName(memberId: String) -> String { return FamilyUserDefaults.load(memberId: memberId)?.name ?? .unknown } diff --git a/14th-team5-iOS/Data/Sources/Trash/UserDefaults/FamilyUserDefautls.swift b/14th-team5-iOS/Data/Sources/Trash/UserDefaults/FamilyUserDefautls.swift index bdaf259b8..688416657 100644 --- a/14th-team5-iOS/Data/Sources/Trash/UserDefaults/FamilyUserDefautls.swift +++ b/14th-team5-iOS/Data/Sources/Trash/UserDefaults/FamilyUserDefautls.swift @@ -18,6 +18,7 @@ public class FamilyUserDefaults { /// 각 memberId - familymember 객체 저장 private static let familyIdKey = "familyId" + private static let familyEditorIdKey = "familyEditorId" private static let myMemberIdKey = "memberId" private static let memberIdsKey = "memberIds" private static let dayOfBirths = "dayOfBirths" @@ -47,6 +48,10 @@ public class FamilyUserDefaults { UserDefaults.standard.setValue(memberId, forKey: myMemberIdKey) } + public static func saveFamilyEditorId(familyEditorId: String) { + UserDefaults.standard.setValue(familyEditorId, forKey: familyEditorIdKey) + } + public static func getMyMemberId() -> String { return UserDefaults.standard.string(forKey: myMemberIdKey) ?? "" } @@ -87,6 +92,10 @@ public class FamilyUserDefaults { public static func loadMemberIds() -> [String] { return userDefaults.array(forKey: memberIdsKey) as! [String] } + + public static func loadFamilyNameEditorId() -> String { + return userDefaults.string(forKey: familyEditorIdKey) ?? "" + } } extension FamilyUserDefaults { diff --git a/14th-team5-iOS/Domain/Sources/Entities/Family/FamilyGroupInfoEntity.swift b/14th-team5-iOS/Domain/Sources/Entities/Family/FamilyGroupInfoEntity.swift new file mode 100644 index 000000000..12c26e081 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Entities/Family/FamilyGroupInfoEntity.swift @@ -0,0 +1,28 @@ +// +// FamilyGroupInfoEntity.swift +// Domain +// +// Created by Kim dohyun on 9/6/24. +// + +import Foundation + +public struct FamilyGroupInfoEntity { + public let familyId: String + public let familyName: String + public let familyNameEditorId: String + public let createdAt: Date + + + public init( + familyId: String, + familyName: String, + familyNameEditorId: String, + createdAt: Date + ) { + self.familyId = familyId + self.familyName = familyName + self.familyNameEditorId = familyNameEditorId + self.createdAt = createdAt + } +} diff --git a/14th-team5-iOS/Domain/Sources/Entities/Family/FamilyNameEntity.swift b/14th-team5-iOS/Domain/Sources/Entities/Family/FamilyNameEntity.swift index ef281d57d..1b04ecf52 100644 --- a/14th-team5-iOS/Domain/Sources/Entities/Family/FamilyNameEntity.swift +++ b/14th-team5-iOS/Domain/Sources/Entities/Family/FamilyNameEntity.swift @@ -10,18 +10,18 @@ import Foundation public struct FamilyNameEntity { public var familyId: String public var familyName: String - public var familyIdEditorId: String + public var familyNameEditorId: String public var createdAt: Date public init( familyId: String, familyName: String, - familyIdEditorId: String, + familyNameEditorId: String, createdAt: Date ) { self.familyId = familyId self.familyName = familyName - self.familyIdEditorId = familyIdEditorId + self.familyNameEditorId = familyNameEditorId self.createdAt = createdAt } } diff --git a/14th-team5-iOS/Domain/Sources/Entities/Family/UpdateFamilyNameRequest.swift b/14th-team5-iOS/Domain/Sources/Entities/Family/UpdateFamilyNameRequest.swift index 6213ac0f6..eb010653c 100644 --- a/14th-team5-iOS/Domain/Sources/Entities/Family/UpdateFamilyNameRequest.swift +++ b/14th-team5-iOS/Domain/Sources/Entities/Family/UpdateFamilyNameRequest.swift @@ -8,9 +8,9 @@ import Foundation public struct UpdateFamilyNameRequest { - public let familyName: String + public let familyName: String? - public init(familyName: String) { + public init(familyName: String?) { self.familyName = familyName } } diff --git a/14th-team5-iOS/Domain/Sources/Repositories/FamilyRepository.swift b/14th-team5-iOS/Domain/Sources/Repositories/FamilyRepository.swift index c1e4367fd..7dcfb5ec9 100644 --- a/14th-team5-iOS/Domain/Sources/Repositories/FamilyRepository.swift +++ b/14th-team5-iOS/Domain/Sources/Repositories/FamilyRepository.swift @@ -18,5 +18,6 @@ public protocol FamilyRepositoryProtocol { func fetchInvitationLink() -> Observable func fetchPaginationFamilyMembers(query: FamilyPaginationQuery) -> Observable func fetchPaginationFamilyMembers(memberIds: [String]) -> [FamilyMemberProfileEntity] + func fetchFamilyGroupInfo() -> Observable func updateFamilyName(body: UpdateFamilyNameRequest) -> Observable } diff --git a/14th-team5-iOS/Domain/Sources/Trash/Member/Interfaces/Repositories/MemberRepository.swift b/14th-team5-iOS/Domain/Sources/Trash/Member/Interfaces/Repositories/MemberRepository.swift index 11eb86dc9..f9d0b3fb1 100644 --- a/14th-team5-iOS/Domain/Sources/Trash/Member/Interfaces/Repositories/MemberRepository.swift +++ b/14th-team5-iOS/Domain/Sources/Trash/Member/Interfaces/Repositories/MemberRepository.swift @@ -8,6 +8,7 @@ import Foundation public protocol MemberRepositoryProtocol { + func fetchFamilyNameEditorId() -> String func fetchUserName(memberId: String) -> String func fetchProfileImageUrlString(memberId: String) -> String func checkIsMe(memberId: String) -> Bool diff --git a/14th-team5-iOS/Domain/Sources/Trash/Member/UseCases/MemberUseCase.swift b/14th-team5-iOS/Domain/Sources/Trash/Member/UseCases/MemberUseCase.swift index a92c41f53..9f3f7a6f8 100644 --- a/14th-team5-iOS/Domain/Sources/Trash/Member/UseCases/MemberUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/Trash/Member/UseCases/MemberUseCase.swift @@ -14,6 +14,7 @@ public protocol MemberUseCaseProtocol { func executeCheckIsMe(memberId: String) -> Bool func executeCheckIsValidMember(memberId: String) -> Bool func executeFetchMyMemberId() -> String + func executeFetchFamilyNameEditorId() -> String } public final class MemberUseCase: MemberUseCaseProtocol { @@ -47,4 +48,8 @@ public final class MemberUseCase: MemberUseCaseProtocol { public func executeFetchMyMemberId() -> String { return memberRepository.fetchMyMemberId() } + + public func executeFetchFamilyNameEditorId() -> String { + return memberRepository.fetchFamilyNameEditorId() + } } diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Family/FetchFamilyGroupInfoUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Family/FetchFamilyGroupInfoUseCase.swift new file mode 100644 index 000000000..1884e7b9b --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/UseCases/Family/FetchFamilyGroupInfoUseCase.swift @@ -0,0 +1,27 @@ +// +// FetchFamilyGroupInfoUseCase.swift +// Domain +// +// Created by Kim dohyun on 9/6/24. +// + +import Foundation + +import RxSwift + +public protocol FetchFamilyGroupInfoUseCaseProtocol { + func execute() -> Observable +} + +public final class FetchFamilyGroupInfoUseCase: FetchFamilyGroupInfoUseCaseProtocol { + private let familyRepository: any FamilyRepositoryProtocol + + public init(familyRepository: any FamilyRepositoryProtocol) { + self.familyRepository = familyRepository + } + + public func execute() -> Observable { + return familyRepository.fetchFamilyGroupInfo() + } + +} From cceee6195d3c6d42a0ebad6515435c8a09ba4397 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Thu, 12 Sep 2024 00:00:17 +0900 Subject: [PATCH 205/263] =?UTF-8?q?refactor:=20ManagementViewController=20?= =?UTF-8?q?=EB=B0=8F=20=EB=B6=80=EA=B0=80=20=EC=BD=94=EB=93=9C=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A7=81=20(#626)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 컴파일 에러 수정 (#602) * refactor: ManagementViewController 코드 리팩토링 (#602) * refactor: Table RefreshControl 동작 수정 (#602) * refactor: FamilyMemberCell 코드 수정 (#602) * refactor: FamilyMemberCell 코드 수정 --- .../App/Sources/Application/AppDelegate.swift | 2 +- .../DIContainer/AppDIContainer.swift | 4 +- .../DIContainer/NavigatorDIContainer.swift | 6 + .../Navigator/ManagementNavigator.swift | 56 ++++ ... DailyCalendarViewControllerWrapper.swift} | 0 .../Management/ManagementViewController.swift | 38 --- .../ManagementViewControllerWrapper.swift | 38 +++ .../MonthlyCalendarDIConatainer.swift | 4 +- .../Reactor/CalendarImageCellReactor.swift | 2 +- .../Reactor/CalendarPageViewCellReactor.swift | 2 +- .../Reactor/CalendarPostCellReactor.swift | 2 +- .../Reactor/DailyCalendarViewReactor.swift | 2 +- .../Reactor/MonthlyCalendarViewReactor.swift | 2 +- .../Calendar/View/Cell/CalendarPostCell.swift | 2 +- .../BibbiRealEmojiCellReactor.swift | 4 +- .../Reactor/CameraDisplayViewReactor.swift | 2 +- .../Camera/Reactor/CameraViewReactor.swift | 2 +- .../Reactor/FamilyEntranceReactor.swift | 2 +- .../Reactor/FamilyManagementViewReactor.swift | 239 -------------- .../FamilyMemberProfileCellReactor.swift | 49 --- .../Strings/FamilyManagementStrings.swift | 24 -- .../View/Cell/FamilyMemberProfileCell.swift | 190 ----------- .../FamilyManagementViewController.swift | 301 ------------------ .../Reactor/Cell/MainFamilyCellReactor.swift | 4 +- .../Home/Reactor/MainFamilyViewReactor.swift | 2 +- .../Home/Reactor/MainPostViewReactor.swift | 2 +- .../Home/Reactor/MainViewReactor.swift | 6 +- .../Config/FamilyMemberCellKind.swift | 24 ++ .../FamilyMemberSectionModel.swift} | 2 +- .../Delegate/ManagementTableDelegate.swift | 14 + .../ManagementTableHeaderDelegate.swift | 14 + .../Delegate/SharingContainerDelegate.swift | 14 + .../RxManagementTableDelegateProxy.swift | 51 +++ ...RxManagementTableHeaderDelegateProxy.swift | 46 +++ .../RxSharingContainerDelegateProxy.swift | 46 +++ .../Cell/FamilyMemberCellReactor.swift | 100 ++++++ .../FamilyNameSettingViewReactor.swift | 0 .../Reactor/ManagementReactor.swift | 179 +++++++++++ .../ManagementTableHeaderReactor.swift | 94 ++++++ .../Reactor/ManagementTableReactor.swift | 96 ++++++ .../Reactor/SharingContainerReactor.swift} | 37 ++- .../View/Cell/FamilyMemberCell.swift | 209 ++++++++++++ .../View/JoinFamilyGroupEditorView.swift | 0 .../View/ManagementTableHeaderView.swift | 111 +++++++ .../Management/View/ManagementTableView.swift | 150 +++++++++ .../View/SharingContainerView.swift} | 96 +++--- .../FamilyNameSettingViewController.swift | 0 .../ManagementViewController.swift | 183 +++++++++++ .../Dependency/PostCommentDIContainer.swift | 4 +- .../Reactor/CommentCellReactor.swift | 4 +- .../Reactor/CommentViewReactor.swift | 2 +- .../PostComment/View/Cell/CommentCell.swift | 2 +- .../Reactor/PostDetailViewReactor.swift | 2 +- .../PostDetail/Reactor/PostReactor.swift | 2 +- .../Reactor/ReactionMemberViewReactor.swift | 12 +- .../Reactor/ReactionViewReactor.swift | 2 +- .../ReactionMembersViewController.swift | 8 +- .../Reactor/ProfileFeedPageViewReactor.swift | 2 +- .../Profile/Reactor/ProfileViewReactor.swift | 2 +- .../View => Trash}/AirplaneLottieView.swift | 0 .../BibbiFetchFailureView.swift | 1 + .../BlurAiraplaneLottieView.swift | 4 + .../FamilyManagementDIContainer.swift | 8 +- .../InvitationUrlContainerDIContainer.swift | 12 +- .../Trash}/Manager/DeepLinkManager.swift | 0 .../Base/BBNavigationViewController.swift | 5 + .../Core/Sources/Base/BaseService.swift | 4 +- .../Core/Sources/Base/BaseTableView.swift | 51 +++ .../Sources/Base/ReactorViewController.swift | 6 + .../Activity/CopyInvitationUrlActivity.swift | 8 +- .../BBServices/ActivityGlobalState.swift | 36 --- .../Bibbi/BBServices/ManagementService.swift | 73 +++++ ...teProvider.swift => ServiceProvider.swift} | 12 +- .../Sources/Extensions/Reactive+Ext.swift | 41 +-- .../Extensions/UIViewController+Ext.swift | 8 +- .../AppUserDefaults/AppUserDefaults.swift | 9 - 76 files changed, 1733 insertions(+), 1040 deletions(-) rename 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Calendar/{DailyCalendarDIContainer.swift => DailyCalendarViewControllerWrapper.swift} (100%) delete mode 100644 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Management/ManagementViewController.swift create mode 100644 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Management/ManagementViewControllerWrapper.swift delete mode 100644 14th-team5-iOS/App/Sources/Presentation/FamilyManagement/Reactor/FamilyManagementViewReactor.swift delete mode 100644 14th-team5-iOS/App/Sources/Presentation/FamilyManagement/Reactor/FamilyMemberProfileCellReactor.swift delete mode 100644 14th-team5-iOS/App/Sources/Presentation/FamilyManagement/Strings/FamilyManagementStrings.swift delete mode 100644 14th-team5-iOS/App/Sources/Presentation/FamilyManagement/View/Cell/FamilyMemberProfileCell.swift delete mode 100644 14th-team5-iOS/App/Sources/Presentation/FamilyManagement/ViewController/FamilyManagementViewController.swift create mode 100644 14th-team5-iOS/App/Sources/Presentation/Management/Config/FamilyMemberCellKind.swift rename 14th-team5-iOS/App/Sources/Presentation/{FamilyManagement/DataSource/FamilyMemberProfileSectionModel.swift => Management/Reactive/DataSource/FamilyMemberSectionModel.swift} (59%) create mode 100644 14th-team5-iOS/App/Sources/Presentation/Management/Reactive/DelegateProxy/Delegate/ManagementTableDelegate.swift create mode 100644 14th-team5-iOS/App/Sources/Presentation/Management/Reactive/DelegateProxy/Delegate/ManagementTableHeaderDelegate.swift create mode 100644 14th-team5-iOS/App/Sources/Presentation/Management/Reactive/DelegateProxy/Delegate/SharingContainerDelegate.swift create mode 100644 14th-team5-iOS/App/Sources/Presentation/Management/Reactive/DelegateProxy/RxManagementTableDelegateProxy.swift create mode 100644 14th-team5-iOS/App/Sources/Presentation/Management/Reactive/DelegateProxy/RxManagementTableHeaderDelegateProxy.swift create mode 100644 14th-team5-iOS/App/Sources/Presentation/Management/Reactive/DelegateProxy/RxSharingContainerDelegateProxy.swift create mode 100644 14th-team5-iOS/App/Sources/Presentation/Management/Reactor/Cell/FamilyMemberCellReactor.swift rename 14th-team5-iOS/App/Sources/Presentation/{FamilyManagement => Management}/Reactor/FamilyNameSettingViewReactor.swift (100%) create mode 100644 14th-team5-iOS/App/Sources/Presentation/Management/Reactor/ManagementReactor.swift create mode 100644 14th-team5-iOS/App/Sources/Presentation/Management/Reactor/ManagementTableHeaderReactor.swift create mode 100644 14th-team5-iOS/App/Sources/Presentation/Management/Reactor/ManagementTableReactor.swift rename 14th-team5-iOS/App/Sources/Presentation/{FamilyManagement/Reactor/InvitationUrlContainerViewReactor.swift => Management/Reactor/SharingContainerReactor.swift} (60%) create mode 100644 14th-team5-iOS/App/Sources/Presentation/Management/View/Cell/FamilyMemberCell.swift rename 14th-team5-iOS/App/Sources/Presentation/{FamilyManagement => Management}/View/JoinFamilyGroupEditorView.swift (100%) create mode 100644 14th-team5-iOS/App/Sources/Presentation/Management/View/ManagementTableHeaderView.swift create mode 100644 14th-team5-iOS/App/Sources/Presentation/Management/View/ManagementTableView.swift rename 14th-team5-iOS/App/Sources/Presentation/{FamilyManagement/View/InvitationUrlContainerView.swift => Management/View/SharingContainerView.swift} (50%) rename 14th-team5-iOS/App/Sources/Presentation/{FamilyManagement => Management}/ViewController/FamilyNameSettingViewController.swift (100%) create mode 100644 14th-team5-iOS/App/Sources/Presentation/Management/ViewController/ManagementViewController.swift rename 14th-team5-iOS/App/Sources/Presentation/{FamilyManagement/View => Trash}/AirplaneLottieView.swift (100%) rename 14th-team5-iOS/App/Sources/Presentation/{FamilyManagement/View => Trash}/BibbiFetchFailureView.swift (99%) rename 14th-team5-iOS/App/Sources/Presentation/{FamilyManagement/View => Trash}/BlurAiraplaneLottieView.swift (99%) rename 14th-team5-iOS/App/Sources/Presentation/{FamilyManagement => Trash}/Dependency/FamilyManagementDIContainer.swift (76%) rename 14th-team5-iOS/App/Sources/Presentation/{FamilyManagement => Trash}/Dependency/InvitationUrlContainerDIContainer.swift (54%) rename 14th-team5-iOS/App/Sources/{ => Presentation/Trash}/Manager/DeepLinkManager.swift (100%) create mode 100644 14th-team5-iOS/Core/Sources/Base/BaseTableView.swift delete mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBServices/ActivityGlobalState.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBServices/ManagementService.swift rename 14th-team5-iOS/Core/Sources/Bibbi/BBServices/{GlobalStateProvider.swift => ServiceProvider.swift} (82%) diff --git a/14th-team5-iOS/App/Sources/Application/AppDelegate.swift b/14th-team5-iOS/App/Sources/Application/AppDelegate.swift index c145c71d3..69421c73b 100644 --- a/14th-team5-iOS/App/Sources/Application/AppDelegate.swift +++ b/14th-team5-iOS/App/Sources/Application/AppDelegate.swift @@ -34,7 +34,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { let disposeBag = DisposeBag() @available(*, deprecated, message: "@Injected var provider: ServiceProviderProtocol") - let globalStateProvider: GlobalStateProviderProtocol = GlobalStateProvider() + let globalStateProvider: ServiceProviderProtocol = ServiceProvider() diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/AppDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/AppDIContainer.swift index 88f28ab39..582387cc1 100644 --- a/14th-team5-iOS/App/Sources/Application/DIContainer/AppDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/AppDIContainer.swift @@ -36,8 +36,8 @@ final class AppDIContainer: BaseContainer { } // ServiceProvider 등록 - container.register(type: GlobalStateProviderProtocol.self) { _ in - return GlobalStateProvider() + container.register(type: ServiceProviderProtocol.self) { _ in + return ServiceProvider() } } diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/NavigatorDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/NavigatorDIContainer.swift index 46844b5e2..d659c926a 100644 --- a/14th-team5-iOS/App/Sources/Application/DIContainer/NavigatorDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/NavigatorDIContainer.swift @@ -55,6 +55,12 @@ final class NavigatorDIContainer: BaseContainer { ) } + container.register(type: ManagementNavigatorProtocol.self) { _ in + ManagementNavigator( + navigationController: makeUINavigationController() + ) + } + container.register(type: FamilyEntranceNavigatorProtocol.self) { _ in FamilyEntranceNavigator(navigationController: makeUINavigationController()) } diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/ManagementNavigator.swift b/14th-team5-iOS/App/Sources/Application/Navigator/ManagementNavigator.swift index d2489cc76..d0aa79c1e 100644 --- a/14th-team5-iOS/App/Sources/Application/Navigator/ManagementNavigator.swift +++ b/14th-team5-iOS/App/Sources/Application/Navigator/ManagementNavigator.swift @@ -8,8 +8,17 @@ import Core import UIKit +import DesignSystem + protocol ManagementNavigatorProtocol: BaseNavigator { func toProfile(memberId: String) + func toSetting(memberId: String) + func toFamilyNameSetting() + + func presentSharingSheet(url: URL?) + + func showSuccessToast() + func showErrorToast() } final class ManagementNavigator: ManagementNavigatorProtocol { @@ -31,4 +40,51 @@ final class ManagementNavigator: ManagementNavigatorProtocol { navigationController.pushViewController(vc, animated: true) } + func toSetting(memberId: String) { + // TODO: - Wrapper로 바꾸기 + let vc = PrivacyDIContainer(memberId: memberId).makeViewController() + navigationController.pushViewController(vc, animated: true) + } + + func toFamilyNameSetting() { + let vc = FamilyNameSettingViewControllerWrapper().viewController + navigationController.pushViewController(vc, animated: true) + } + + + // MARK: - Present + + func presentSharingSheet(url: URL?) { + // TODO: - 리팩토링하기 + guard let url else { return } + + let itemSource = UrlActivityItemSource( + title: "삐삐! 가족에게 보내는 하루 한 번 생존 신고", + url: url + ) + let copyToPastboard = CopyInvitationUrlActivity(url) + + let items: [Any] = [itemSource] + let activityVC = UIActivityViewController( + activityItems: items, + applicationActivities: [copyToPastboard] + ) + activityVC.excludedActivityTypes = [.addToReadingList, .copyToPasteboard] + navigationController.present(activityVC, animated: true) + } + + + // MARK: - Show + + func showSuccessToast() { + BBToast.default( + image: DesignSystemAsset.link.image, + title: "링크가 복사되었어요" + ).show() + } + + func showErrorToast() { + BBToast.style(.error).show() + } + } diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Calendar/DailyCalendarDIContainer.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Calendar/DailyCalendarViewControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Calendar/DailyCalendarDIContainer.swift rename to 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Calendar/DailyCalendarViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Management/ManagementViewController.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Management/ManagementViewController.swift deleted file mode 100644 index fd1ae6c64..000000000 --- a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Management/ManagementViewController.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// ManagementViewController.swift -// App -// -// Created by 김건우 on 6/25/24. -// - -import Core -import Foundation - -final class ManagementViewController: BaseWrapper { - - // MARK: - Typealias - - typealias R = FamilyManagementViewReactor - typealias V = FamilyManagementViewController - - // MARK: - Properties - - var reactor: FamilyManagementViewReactor { - makeReactor() - } - - var viewController: FamilyManagementViewController { - makeViewController() - } - - // MARK: - Make - - func makeReactor() -> FamilyManagementViewReactor { - FamilyManagementViewReactor() - } - - func makeViewController() -> FamilyManagementViewController { - FamilyManagementViewController(reactor: makeReactor()) - } - -} diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Management/ManagementViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Management/ManagementViewControllerWrapper.swift new file mode 100644 index 000000000..6187fedd4 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Management/ManagementViewControllerWrapper.swift @@ -0,0 +1,38 @@ +// +// ManagementViewController.swift +// App +// +// Created by 김건우 on 6/25/24. +// + +import Core +import Foundation + +final class ManagementViewControllerWrapper: BaseWrapper { + + // MARK: - Typealias + + typealias R = ManagementReactor + typealias V = ManagementViewController + + // MARK: - Properties + + var reactor: ManagementReactor { + makeReactor() + } + + var viewController: ManagementViewController { + makeViewController() + } + + // MARK: - Make + + func makeReactor() -> ManagementReactor { + ManagementReactor() + } + + func makeViewController() -> ManagementViewController { + ManagementViewController(reactor: makeReactor()) + } + +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/DIContainer/MonthlyCalendarDIConatainer.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/DIContainer/MonthlyCalendarDIConatainer.swift index 637794ff9..906254ee1 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/DIContainer/MonthlyCalendarDIConatainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/DIContainer/MonthlyCalendarDIConatainer.swift @@ -14,9 +14,9 @@ import Domain @available(*, deprecated, renamed: "MonthlyCalendarViewControllerWrapper") public final class MonthlyCalendarDIConatainer { // MARK: - Properties - private var globalState: GlobalStateProviderProtocol { + private var globalState: ServiceProviderProtocol { guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { - return GlobalStateProvider() + return ServiceProvider() } return appDelegate.globalStateProvider } diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarImageCellReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarImageCellReactor.swift index ac942ae44..0a70b1001 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarImageCellReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarImageCellReactor.swift @@ -41,7 +41,7 @@ final public class CalendarImageCellReactor: Reactor { public var initialState: State @Injected var calendarUseCase: CalendarUseCaseProtocol - @Injected var provider: GlobalStateProviderProtocol + @Injected var provider: ServiceProviderProtocol public let type: CalendarType diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPageViewCellReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPageViewCellReactor.swift index 0db98dea5..a4f3c84e1 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPageViewCellReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPageViewCellReactor.swift @@ -41,7 +41,7 @@ public final class CalendarCellReactor: Reactor { // MARK: - Properties public var initialState: State - @Injected var provider: GlobalStateProviderProtocol + @Injected var provider: ServiceProviderProtocol @Injected var calendarUseCase: CalendarUseCaseProtocol @Navigator var navigator: MonthlyCalendarNavigatorProtocol diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPostCellReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPostCellReactor.swift index 471ad58b6..22e1be498 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPostCellReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPostCellReactor.swift @@ -40,7 +40,7 @@ public final class CalendarPostCellReactor: Reactor { public var initialState: State @Injected var meUseCase: MemberUseCaseProtocol - @Injected var provider: GlobalStateProviderProtocol + @Injected var provider: ServiceProviderProtocol // MARK: - Intializer public init( diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/DailyCalendarViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/DailyCalendarViewReactor.swift index 257e0c8b9..9ab7e18d8 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/DailyCalendarViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/DailyCalendarViewReactor.swift @@ -59,7 +59,7 @@ public final class DailyCalendarViewReactor: Reactor { } // MARK: - Properties - @Injected var provider: GlobalStateProviderProtocol + @Injected var provider: ServiceProviderProtocol @Injected var calendarUseCase: CalendarUseCaseProtocol public var initialState: State diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/MonthlyCalendarViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/MonthlyCalendarViewReactor.swift index 9763db5b5..e27bfd078 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/MonthlyCalendarViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/MonthlyCalendarViewReactor.swift @@ -40,7 +40,7 @@ public final class MonthlyCalendarViewReactor: Reactor { // MARK: - Properties public var initialState: State - @Injected var provider: GlobalStateProviderProtocol + @Injected var provider: ServiceProviderProtocol @Injected var calendarUseCase: CalendarUseCaseProtocol @Navigator var navigator: MonthlyCalendarNavigatorProtocol diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarPostCell.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarPostCell.swift index 32cfba83b..f6b22c684 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarPostCell.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarPostCell.swift @@ -110,7 +110,7 @@ final class CalendarPostCell: BaseCollectionViewCell { reactor.state.compactMap { $0.authorName } .distinctUntilChanged() - .bind(to: authorFirstNameLabel.rx.firtNameText) + .bind(with: self) { $0.authorNameLabel.text = $1[0] } .disposed(by: disposeBag) reactor.state.compactMap { $0.authorImageUrl } diff --git a/14th-team5-iOS/App/Sources/Presentation/Camera/Cell/CellReactor/BibbiRealEmojiCellReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Camera/Cell/CellReactor/BibbiRealEmojiCellReactor.swift index 9127869fb..11a3beb05 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Camera/Cell/CellReactor/BibbiRealEmojiCellReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Camera/Cell/CellReactor/BibbiRealEmojiCellReactor.swift @@ -12,7 +12,7 @@ import ReactorKit public final class BibbiRealEmojiCellReactor: Reactor { public typealias Action = NoAction - private let provider: GlobalStateProviderProtocol + private let provider: ServiceProviderProtocol public var initialState: State @@ -32,7 +32,7 @@ public final class BibbiRealEmojiCellReactor: Reactor { } public init( - provider: GlobalStateProviderProtocol, + provider: ServiceProviderProtocol, realEmojiImage: URL?, isSelected: Bool, indexPath: Int, diff --git a/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraDisplayViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraDisplayViewReactor.swift index 615027334..95f03fca1 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraDisplayViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraDisplayViewReactor.swift @@ -15,7 +15,7 @@ import Core public final class CameraDisplayViewReactor: Reactor { public var initialState: State - @Injected private var provider: GlobalStateProviderProtocol + @Injected private var provider: ServiceProviderProtocol @Injected private var createPresignedCameraUseCase: CreateCameraUseCaseProtocol @Injected private var uploadImageUseCase: FetchCameraUploadImageUseCaseProtocol @Injected private var fetchCameraImageUseCase: CreateCameraImageUseCaseProtocol diff --git a/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraViewReactor.swift index fa738873f..f4d5ec864 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraViewReactor.swift @@ -27,7 +27,7 @@ public final class CameraViewReactor: Reactor { @Injected private var editProfileImageUseCase: EditCameraProfileImageUseCaseProtocol @Injected private var fetchRealEmojiListUseCase: FetchCameraRealEmojiListUseCaseProtocol @Injected private var fetchRealEmojiPreSignedUseCase: FetchCameraRealEmojiUseCaseProtocol - @Injected private var provider: GlobalStateProviderProtocol + @Injected private var provider: ServiceProviderProtocol public var cameraType: UploadLocation diff --git a/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Reactor/FamilyEntranceReactor.swift b/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Reactor/FamilyEntranceReactor.swift index d408e5797..8960ae7cf 100644 --- a/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Reactor/FamilyEntranceReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Reactor/FamilyEntranceReactor.swift @@ -29,7 +29,7 @@ public final class FamilyEntranceReactor: Reactor { public var initialState: State = State() @Navigator var navigator: FamilyEntranceNavigatorProtocol - @Injected var provider: GlobalStateProviderProtocol + @Injected var provider: ServiceProviderProtocol @Injected var familyUseCase: FamilyUseCaseProtocol } diff --git a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/Reactor/FamilyManagementViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/Reactor/FamilyManagementViewReactor.swift deleted file mode 100644 index 94b657718..000000000 --- a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/Reactor/FamilyManagementViewReactor.swift +++ /dev/null @@ -1,239 +0,0 @@ -// -// LinkShareViewReactor.swift -// App -// -// Created by 김건우 on 12/11/23. -// - -import UIKit - -import Core -import Domain -import Differentiator -import ReactorKit -import RxSwift - -public final class FamilyManagementViewReactor: Reactor { - // MARK: - Action - public enum Action { - case fetchPaginationFamilyMemebers(Bool) - case didTapShareContainer - case didTapPrivacyBarButton - case didSelectTableCell(IndexPath) - } - - // MARK: - Mutate - public enum Mutation { - case setSharePanel(String?) - case setCopySuccessToastMessageView - case setUrlFetchFailureToastMessageView - case setFamilyFetchFailureToastMessageView - case setHiddenPaperAirplaneLottieView(Bool) - case setHiddenFamilyFetchFailureView(Bool) - case pushProfileVC(String) - case pushPrivacyVC(String) - case injectFamilyId(String?) - case injectFamilyMembers([FamilyMemberProfileCellReactor]) - case generateErrorHapticNotification - } - - // MARK: - State - public struct State { - var familyId: String? - @Pulse var familyInvitationUrl: URL? - @Pulse var shouldPushProfileVC: String - @Pulse var shouldPushPrivacyVC: String - @Pulse var shouldPresentCopySuccessToastMessageView: Bool - @Pulse var shouldPresentUrlFetchFailureToastMessageView: Bool - @Pulse var shouldPresentFamilyFetchFailureToastMessageView: Bool - @Pulse var shouldGenerateErrorHapticNotification: Bool - @Pulse var shouldPresentPaperAirplaneLottieView: Bool - @Pulse var shouldPresentFamilyFetchFailureView: Bool - @Pulse var displayFamilyMember: [FamilyMemberProfileSectionModel] - var displayFamilyMemberCount: Int - } - - // MARK: - Properties - public let initialState: State - - @Injected var memberUseCase: MemberUseCaseProtocol - @Injected var familyUseCase: FamilyUseCaseProtocol - @Injected var provider: GlobalStateProviderProtocol - - // MARK: - Intializer - init() { - self.initialState = State( - familyId: nil, - familyInvitationUrl: nil, - shouldPushProfileVC: .none, - shouldPushPrivacyVC: .none, - shouldPresentCopySuccessToastMessageView: false, - shouldPresentUrlFetchFailureToastMessageView: false, - shouldPresentFamilyFetchFailureToastMessageView: false, - shouldGenerateErrorHapticNotification: false, - shouldPresentPaperAirplaneLottieView: false, - shouldPresentFamilyFetchFailureView: false, - displayFamilyMember: [.init(model: (), items: [])], - displayFamilyMemberCount: 0 - ) - - } - - // MARK: - Transform - public func transform(action: Observable) -> Observable { - let eventAction = provider.profileGlobalState.event - .flatMap { event -> Observable in - switch event { - case .refreshFamilyMembers: - return Observable.just(.fetchPaginationFamilyMemebers(false)) - } - } - - return Observable.merge(action, eventAction) - } - - public func transform(mutation: Observable) -> Observable { - let eventMutation = provider.activityGlobalState.event - .flatMap { event -> Observable in - switch event { - case .didTapCopyInvitationUrlAction: - return Observable.just(.setCopySuccessToastMessageView) - default: - return Observable.empty() - } - } - - return Observable.merge(mutation, eventMutation) - } - - // MARK: - Mutate - public func mutate(action: Action) -> Observable { - switch action { - case .didTapShareContainer: - MPEvent.Family.shareLink.track(with: nil) - return Observable.concat( - provider.activityGlobalState.hiddenInvitationUrlIndicatorView(true) - .flatMap({ _ in Observable.empty() }), - - familyUseCase.executeFetchInvitationUrl() - .withUnretained(self) - .concatMap({ - guard let invitationLink = $0.1?.url else { - return Observable.concat( - Observable.just(.generateErrorHapticNotification), - Observable.just(.setUrlFetchFailureToastMessageView), - $0.0.provider.activityGlobalState.hiddenInvitationUrlIndicatorView(false) - .flatMap({ _ in Observable.empty() }) - ) - } - return Observable.concat( - Observable.just(.setSharePanel(invitationLink)), - $0.0.provider.activityGlobalState.hiddenInvitationUrlIndicatorView(false) - .flatMap({ _ in Observable.empty() }) - ) - }) - ) - - case .didTapPrivacyBarButton: - let memberId = memberUseCase.executeFetchMyMemberId() - return Observable.just(.pushPrivacyVC(memberId)) - - case let .fetchPaginationFamilyMemebers(refresh): - let query = FamilyPaginationQuery() - - return Observable.concat( - Observable.just(.setHiddenPaperAirplaneLottieView(refresh ? true : false)), - - familyUseCase.executeFetchPaginationFamilyMembers(query: query) - .withUnretained(self) - .concatMap { - guard let familyResponse = $0.1?.results else { - return Observable.concat( - Observable.just(.injectFamilyMembers([])), - Observable.just(.setFamilyFetchFailureToastMessageView), - Observable.just(.generateErrorHapticNotification), - Observable.just(.setHiddenFamilyFetchFailureView(false)), - Observable.just(.setHiddenPaperAirplaneLottieView(true)) - ) - - } - - return Observable.concat( - Observable.just( - .injectFamilyMembers( - familyResponse.map { - FamilyMemberProfileCellReactor( - $0, isMe: self.memberUseCase.executeCheckIsMe(memberId: $0.memberId), cellType: .family - ) - } - ) - ), - Observable.just(.setHiddenFamilyFetchFailureView(true)), - Observable.just(.setHiddenPaperAirplaneLottieView(true)) - ) - } - ) - - case let .didSelectTableCell(indexPath): - guard let dataSource = currentState.displayFamilyMember.first else { - return Observable.empty() - } - let memberId = dataSource.items[indexPath.row].initialState.memberId - - return Observable.just(.pushProfileVC(memberId)) - } - } - - // MARK: - Reduce - public func reduce(state: State, mutation: Mutation) -> State { - var newState = state - switch mutation { - case let .setSharePanel(urlString): - newState.familyInvitationUrl = URL(string: urlString ?? "") - - case .setCopySuccessToastMessageView: - newState.shouldPresentCopySuccessToastMessageView = true - - case .setUrlFetchFailureToastMessageView: - newState.shouldPresentUrlFetchFailureToastMessageView = true - - case .setFamilyFetchFailureToastMessageView: - newState.shouldPresentFamilyFetchFailureToastMessageView = true - - case let .setHiddenPaperAirplaneLottieView(hidden): - newState.shouldPresentPaperAirplaneLottieView = hidden - - case let .setHiddenFamilyFetchFailureView(hidden): - newState.shouldPresentFamilyFetchFailureView = hidden - - case let .pushProfileVC(memberId): - newState.shouldPushProfileVC = memberId - - case let .pushPrivacyVC(memberId): - newState.shouldPushPrivacyVC = memberId - - case let .injectFamilyId(familyId): - newState.familyId = familyId - - case let .injectFamilyMembers(familyMembers): - guard var dataSource = currentState.displayFamilyMember.first else { - break - } - dataSource.items = familyMembers - dataSource.items.sort { $0.currentState.isMe && !$1.currentState.isMe} - - newState.displayFamilyMember = [dataSource] - newState.displayFamilyMemberCount = familyMembers.count - - case .generateErrorHapticNotification: - newState.shouldGenerateErrorHapticNotification = true - } - return newState - } -} - -extension FamilyManagementViewController { - private func shouldFetchNextPage(contentOffsetY: CGFloat, frameHehgit: CGFloat) -> Bool { - return false - } -} diff --git a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/Reactor/FamilyMemberProfileCellReactor.swift b/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/Reactor/FamilyMemberProfileCellReactor.swift deleted file mode 100644 index 4b3736976..000000000 --- a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/Reactor/FamilyMemberProfileCellReactor.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// YourFamilProfileCellReactor.swift -// App -// -// Created by 김건우 on 12/12/23. -// - -import Foundation - -import Core -import Domain -import ReactorKit -import RxDataSources -import RxSwift - -final public class FamilyMemberProfileCellReactor: Reactor { - public enum CellType { - case family - case emoji - } - - // MARK: - Action - public typealias Action = NoAction - - // MARK: - State - public struct State { - let cellType: CellType - var memberId: String - var name: String - var imageUrl: String? - var dayOfBirth: Date - var isMe: Bool - } - - // MARK: - Properties - public var initialState: State - - // MARK: - Intializer - public init(_ memberResponse: FamilyMemberProfileEntity, isMe: Bool, cellType: CellType) { - self.initialState = State( - cellType: cellType, - memberId: memberResponse.memberId, - name: memberResponse.name, - imageUrl: memberResponse.profileImageURL, - dayOfBirth: memberResponse.dayOfBirth ?? Date(), - isMe: isMe - ) - } -} diff --git a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/Strings/FamilyManagementStrings.swift b/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/Strings/FamilyManagementStrings.swift deleted file mode 100644 index aba5c5818..000000000 --- a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/Strings/FamilyManagementStrings.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// LinkShareVC.swift -// App -// -// Created by 김건우 on 12/11/23. -// - -import UIKit - -typealias FamilyManagementStrings = String.FamilyManagement -extension String { - enum FamilyManagement {} -} - -extension FamilyManagementStrings { - static let mainTitle: String = "가족" - static let inviteDescText: String = "삐삐에 가족 초대하기" - static let invitationUrlText: String = "https://no5ing.kr/" - static let headerTitle: String = "당신의 가족" - static let headerCount: String = "0" - static let sucessCopyInvitationUrlText = "링크가 복사되었어요" - static let fetchFailInvitationUrlText = "잠시 후에 다시 시도해주세요" - static let fetchFailFamilyText = "가족을 불러오는데 실패했어요" -} diff --git a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/View/Cell/FamilyMemberProfileCell.swift b/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/View/Cell/FamilyMemberProfileCell.swift deleted file mode 100644 index fba1aac5b..000000000 --- a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/View/Cell/FamilyMemberProfileCell.swift +++ /dev/null @@ -1,190 +0,0 @@ -// -// FamilyProfileCell.swift -// App -// -// Created by 김건우 on 12/12/23. -// - -import UIKit - -import Core -import DesignSystem -import ReactorKit -import RxSwift -import SnapKit -import Then - -final class FamilyMemberProfileCell: BaseTableViewCell { - // MARK: - Views - private let containerView: UIView = UIView() - private let firstNameLabel: BBLabel = BBLabel(.head2Bold, textAlignment: .center, textColor: .gray200) - private let profileImageView: UIImageView = UIImageView() - private let dayOfBirthBadgeView: UIImageView = UIImageView() - - private let labelStack: UIStackView = UIStackView() - private let nameLabel: BBLabel = BBLabel(.body1Regular, textColor: .gray200) - private let isMeLabel: BBLabel = BBLabel(.body2Regular, textColor: .gray500) - private let rightArrowImageView: UIImageView = UIImageView() - - - // MARK: - Properties - static let id: String = "FamilyProfileCell" - - private var containerSize: CGFloat { - guard let reactor = reactor else { return 52 } - return reactor.currentState.cellType == .family ? 52 : 44 - } - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func prepareForReuse() { - nameLabel.text = String.none - isMeLabel.text = String.none - profileImageView.image = nil - } - - // MARK: - Helpers - override func bind(reactor: FamilyMemberProfileCellReactor) { - super.bind(reactor: reactor) - bindInput(reactor: reactor) - bindOutput(reactor: reactor) - } - - private func bindInput(reactor: FamilyMemberProfileCellReactor) { } - - private func bindOutput(reactor: FamilyMemberProfileCellReactor) { - reactor.state.compactMap { $0.imageUrl } - .distinctUntilChanged() - .bind(to: profileImageView.rx.kingfisherImage) - .disposed(by: disposeBag) - - let name = reactor.state.map({ $0.name }).asDriver(onErrorJustReturn: .none) - - name - .distinctUntilChanged() - .drive(nameLabel.rx.text) - .disposed(by: disposeBag) - - name - .distinctUntilChanged() - .drive(firstNameLabel.rx.firtNameText) - .disposed(by: disposeBag) - - let isMe = reactor.state.map({ $0.isMe }).asDriver(onErrorJustReturn: false) - - isMe - .distinctUntilChanged() - .drive(isMeLabel.rx.isMeText) - .disposed(by: disposeBag) - - isMe - .distinctUntilChanged() - .drive(labelStack.rx.isMeSpacing) - .disposed(by: disposeBag) - - reactor.state.map { $0.dayOfBirth } - .distinctUntilChanged() - .map { !$0.isEqual([.month, .day], with: .now) } - .bind(to: dayOfBirthBadgeView.rx.isHidden) - .disposed(by: disposeBag) - - reactor.state.map { $0.cellType } - .map { $0 != .family } - .distinctUntilChanged() - .bind(to: rightArrowImageView.rx.isHidden) - .disposed(by: disposeBag) - } - - override func setupUI() { - super.setupUI() - containerView.addSubviews( - firstNameLabel, profileImageView - ) - contentView.addSubviews( - containerView, dayOfBirthBadgeView, - labelStack, rightArrowImageView - ) - labelStack.addArrangedSubviews( - nameLabel, isMeLabel - ) - } - - override func setupAutoLayout() { - super.setupAutoLayout() - - containerView.snp.makeConstraints { - $0.size.equalTo(containerSize) - $0.leading.equalTo(contentView.snp.leading).offset(20) - $0.verticalEdges.equalToSuperview().inset(12) - } - - dayOfBirthBadgeView.snp.makeConstraints { - $0.size.equalTo(20) - $0.top.equalTo(containerView.snp.top).offset(-5) - $0.trailing.equalTo(containerView.snp.trailing).offset(5) - } - - firstNameLabel.snp.makeConstraints { - $0.center.equalToSuperview() - } - - profileImageView.snp.makeConstraints { - $0.edges.equalToSuperview() - } - - labelStack.snp.makeConstraints { - $0.leading.equalTo(containerView.snp.trailing).offset(16) - $0.centerY.equalTo(containerView.snp.centerY) - } - - rightArrowImageView.snp.makeConstraints { - $0.size.equalTo(20) - $0.centerY.equalToSuperview() - $0.trailing.equalToSuperview().offset(-20) - } - } - - override func setupAttributes() { - super.setupAttributes() - - self.selectionStyle = .none - self.backgroundColor = .clear - - containerView.do { - $0.layer.masksToBounds = true - $0.layer.cornerRadius = containerSize / 2 - $0.backgroundColor = UIColor.gray800 - } - - dayOfBirthBadgeView.do { - $0.image = DesignSystemAsset.birthday.image - $0.contentMode = .scaleAspectFit - } - - profileImageView.do { - $0.contentMode = .scaleAspectFill - $0.layer.masksToBounds = true - $0.layer.cornerRadius = containerSize / 2 - } - - labelStack.do { - $0.axis = .vertical - $0.spacing = 3 - $0.alignment = .fill - $0.distribution = .fillProportionally - } - - rightArrowImageView.do { - let arrowRight = DesignSystemAsset.arrowRight.image - $0.image = arrowRight.withRenderingMode(.alwaysTemplate) - $0.tintColor = UIColor.gray500 - $0.contentMode = .scaleAspectFill - } - } -} diff --git a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/ViewController/FamilyManagementViewController.swift b/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/ViewController/FamilyManagementViewController.swift deleted file mode 100644 index a62d76712..000000000 --- a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/ViewController/FamilyManagementViewController.swift +++ /dev/null @@ -1,301 +0,0 @@ -// -// LinkShareViewController.swift -// App -// -// Created by 김건우 on 12/11/23. -// - -import Core -import DesignSystem -import UIKit - -import ReactorKit -import RxCocoa -import RxDataSources -import RxSwift -import SnapKit -import Then - -fileprivate typealias _Str = FamilyManagementStrings -public final class FamilyManagementViewController: BBNavigationViewController { - // MARK: - Views - - private let shareContainerview: InvitationUrlContainerView = InvitationUrlContainerDIContainer().makeView() - private let dividerView: UIView = UIView() - - private let headerStack: UIStackView = UIStackView() - private let tableTitleLabel: BBLabel = BBLabel(.head1, textColor: .gray200) - private let tableEditButton: BBButton = BBButton() - private let tableCountLabel: BBLabel = BBLabel(.body1Regular, textColor: .gray400) - - private let familyTableView: UITableView = UITableView() - private let refreshControl: UIRefreshControl = UIRefreshControl() - - private let bibbiLottieView: AirplaneLottieView = AirplaneLottieView() - - private let fetchFailureView: BibbiFetchFailureView = BibbiFetchFailureView(type: .family) - - // MARK: - Properties - private lazy var dataSource: RxTableViewSectionedReloadDataSource = prepareDatasource() - - // MARK: - Lifecycles - public override func viewDidLoad() { - super.viewDidLoad() - } - - public override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - navigationController?.navigationBar.isHidden = true - } - - // MARK: - Helpers - public override func bind(reactor: FamilyManagementViewReactor) { - super.bind(reactor: reactor) - bindInput(reactor: reactor) - bindOutput(reactor: reactor) - } - - private func bindInput(reactor: FamilyManagementViewReactor) { - Observable.just(()) - .map { Reactor.Action.fetchPaginationFamilyMemebers(false) } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - navigationBar.rx.didTapRightBarButton - .map { _ in Reactor.Action.didTapPrivacyBarButton } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - shareContainerview.rx.tap - .throttle(RxConst.milliseconds300Interval, scheduler: MainScheduler.instance) - .map { Reactor.Action.didTapShareContainer } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - familyTableView.rx.itemSelected - .map { Reactor.Action.didSelectTableCell($0) } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - refreshControl.rx.controlEvent(.valueChanged) - .map { Reactor.Action.fetchPaginationFamilyMemebers(true) } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - tableEditButton.rx.tap - .throttle(RxConst.milliseconds300Interval, scheduler: MainScheduler.instance) - .bind(with: self) { owner, _ in - let familyGroupSettingViewController = FamilyNameSettingViewControllerWrapper().viewController - owner.navigationController?.pushViewController(familyGroupSettingViewController, animated: true) - } - .disposed(by: disposeBag) - } - - private func bindOutput(reactor: FamilyManagementViewReactor) { - reactor.pulse(\.$shouldPushPrivacyVC) - .filter { !$0.isEmpty } - .withUnretained(self) - .subscribe { - let privacyVC = PrivacyDIContainer(memberId: $0.1).makeViewController() - $0.0.navigationController?.pushViewController(privacyVC, animated: true) - } - .disposed(by: disposeBag) - - reactor.pulse(\.$familyInvitationUrl) - .withUnretained(self) - .subscribe { - $0.0.makeInvitationUrlSharePanel( - $0.1, - provider: reactor.provider - ) - } - .disposed(by: disposeBag) - - reactor.state.map { "\($0.displayFamilyMemberCount)" } - .distinctUntilChanged() - .bind(to: tableCountLabel.rx.text) - .disposed(by: disposeBag) - - let familyMember = reactor.pulse(\.$displayFamilyMember).asDriver(onErrorJustReturn: []) - - familyMember - .drive(familyTableView.rx.items(dataSource: dataSource)) - .disposed(by: disposeBag) - - familyMember - .drive(with: self, onNext: { owner, _ in - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - owner.refreshControl.endRefreshing() - } - }) - .disposed(by: disposeBag) - - reactor.pulse(\.$shouldPushProfileVC) - .filter { !$0.isEmpty } - .withUnretained(self) - .subscribe { - let profileViewController = ProfileViewControllerWrapper(memberId: $0.1).viewController - $0.0.navigationController?.pushViewController(profileViewController, animated: true) - } - .disposed(by: disposeBag) - - reactor.pulse(\.$shouldPresentCopySuccessToastMessageView) - .filter { $0 } - .withUnretained(self) - .subscribe(onNext: { _ in - BBToast.default( - image: DesignSystemAsset.link.image, - title: "링크가 복사되었어요" - ).show() - }) - .disposed(by: disposeBag) - - reactor.pulse(\.$shouldPresentUrlFetchFailureToastMessageView) - .filter { $0 } - .withUnretained(self) - .subscribe(onNext: { _ in - BBToast.style(.error).show() - }) - .disposed(by: disposeBag) - - reactor.pulse(\.$shouldPresentFamilyFetchFailureToastMessageView) - .filter { $0 } - .withUnretained(self) - .subscribe { - $0.0.makeBibbiToastView( - text: _Str.fetchFailFamilyText, - image: DesignSystemAsset.warning.image - ) - } - .disposed(by: disposeBag) - - reactor.pulse(\.$shouldPresentPaperAirplaneLottieView) - .bind(to: bibbiLottieView.rx.isHidden) - .disposed(by: disposeBag) - - reactor.pulse(\.$shouldPresentFamilyFetchFailureView) - .bind(to: fetchFailureView.rx.isHidden) - .disposed(by: disposeBag) - - reactor.pulse(\.$shouldGenerateErrorHapticNotification) - .filter { $0 } - .subscribe(onNext: { _ in Haptic.notification(type: .error) }) - .disposed(by: disposeBag) - } - - public override func setupUI() { - super.setupUI() - view.addSubviews( - shareContainerview, - dividerView, headerStack, tableEditButton ,familyTableView - ) - headerStack.addArrangedSubviews( - tableTitleLabel, tableCountLabel - ) - familyTableView.addSubviews(bibbiLottieView, fetchFailureView) - } - - public override func setupAutoLayout() { - super.setupAutoLayout() - shareContainerview.snp.makeConstraints { - $0.top.equalTo(navigationBar.snp.bottom).offset(24) - $0.horizontalEdges.equalToSuperview().inset(20) - $0.height.equalTo(90) - } - - dividerView.snp.makeConstraints { - $0.height.equalTo(1) - $0.top.equalTo(shareContainerview.snp.bottom).offset(24) - $0.horizontalEdges.equalToSuperview() - } - - headerStack.snp.makeConstraints { - $0.leading.equalTo(view).inset(24) - $0.top.equalTo(dividerView.snp.bottom).offset(28) - } - - tableEditButton.snp.makeConstraints { - $0.centerY.equalTo(headerStack) - $0.height.width.equalTo(24) - $0.right.equalToSuperview().inset(20) - } - - familyTableView.snp.makeConstraints { - $0.horizontalEdges.equalToSuperview() - $0.top.equalTo(headerStack.snp.bottom).offset(8) - $0.bottom.equalToSuperview() - } - - bibbiLottieView.snp.makeConstraints { - $0.centerX.equalToSuperview() - $0.horizontalEdges.equalToSuperview() - $0.top.equalToSuperview().offset(140) - } - - fetchFailureView.snp.makeConstraints { - $0.top.equalToSuperview().offset(70) - $0.centerX.equalToSuperview() - } - } - - public override func setupAttributes() { - super.setupAttributes() - navigationBar.do { - $0.navigationTitle = "가족" - $0.navigationTitleFontStyle = .head2Bold - $0.leftBarButtonItem = .arrowLeft - $0.rightBarButtonItem = .setting - } - - dividerView.do { - $0.backgroundColor = UIColor.gray600 - } - - tableEditButton.do { - $0.setBackgroundImage(DesignSystemAsset.edit.image, for: .normal) - $0.setTitle("", for: .normal) - } - - headerStack.do { - $0.axis = .horizontal - $0.spacing = 10 - $0.alignment = .fill - $0.distribution = .fillProportionally - } - - tableTitleLabel.do { - $0.text = _Str.headerTitle - } - - tableCountLabel.do { - $0.text = _Str.headerCount - } - - familyTableView.do { - $0.separatorStyle = .none - $0.estimatedRowHeight = UITableView.automaticDimension - $0.backgroundColor = UIColor.clear - $0.contentInset = UIEdgeInsets(top: 10, left: 0, bottom: 0, right: 0) - - $0.refreshControl = refreshControl - $0.refreshControl?.tintColor = UIColor.bibbiWhite - - $0.register(FamilyMemberProfileCell.self, forCellReuseIdentifier: FamilyMemberProfileCell.id) - } - - fetchFailureView.do { - $0.isHidden = true - } - } -} - -// MARK: - Extensions -extension FamilyManagementViewController { - private func prepareDatasource() -> RxTableViewSectionedReloadDataSource { - return RxTableViewSectionedReloadDataSource { datasource, tableView, indexPath, reactor in - let cell = tableView.dequeueReusableCell(withIdentifier: FamilyMemberProfileCell.id, for: indexPath) as! FamilyMemberProfileCell - cell.reactor = reactor - return cell - } - } -} diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/Cell/MainFamilyCellReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/Cell/MainFamilyCellReactor.swift index e94970b00..9cd5334cc 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/Cell/MainFamilyCellReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/Cell/MainFamilyCellReactor.swift @@ -43,10 +43,10 @@ final class MainFamilyCellReactor: Reactor { // MARK: - Properties let initialState: State - let provider: GlobalStateProviderProtocol + let provider: ServiceProviderProtocol // MARK: - Intializer - init(_ profileData: FamilyMemberProfileEntity, service provider: GlobalStateProviderProtocol) { + init(_ profileData: FamilyMemberProfileEntity, service provider: ServiceProviderProtocol) { self.initialState = State(profileData: profileData) self.provider = provider } diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainFamilyViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainFamilyViewReactor.swift index a7c801c22..f814520a6 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainFamilyViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainFamilyViewReactor.swift @@ -37,7 +37,7 @@ final class MainFamilyViewReactor: Reactor { } let initialState: State = State() - @Injected var provider: GlobalStateProviderProtocol + @Injected var provider: ServiceProviderProtocol @Injected var familyUseCase: FamilyUseCaseProtocol } diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainPostViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainPostViewReactor.swift index 893672ab3..8197ed100 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainPostViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainPostViewReactor.swift @@ -34,7 +34,7 @@ final class MainPostViewReactor: Reactor { } let initialState: State - @Injected var provider: GlobalStateProviderProtocol + @Injected var provider: ServiceProviderProtocol @Injected var postUseCase: FetchPostListUseCaseProtocol init(initialState: State) { diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift index 746bfbcb9..58cf0ed96 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift @@ -90,7 +90,7 @@ final class MainViewReactor: Reactor { } let initialState: State = State() - @Injected var provider: GlobalStateProviderProtocol + @Injected var provider: ServiceProviderProtocol @Injected var fetchMainUseCase: FetchMainUseCaseProtocol @Injected var fetchMainNightUseCase: FetchNightMainViewUseCaseProtocol @Injected var pickUseCase: PickUseCaseProtocol @@ -111,10 +111,10 @@ extension MainViewReactor { } } - let eventMutation = provider.activityGlobalState.event + let eventMutation = provider.managementService.event .flatMap { event -> Observable in switch event { - case .didTapCopyInvitationUrlAction: + case .didTapCopyUrlAction: return Observable.just(.setCopySuccessToastMessage) default: return Observable.empty() diff --git a/14th-team5-iOS/App/Sources/Presentation/Management/Config/FamilyMemberCellKind.swift b/14th-team5-iOS/App/Sources/Presentation/Management/Config/FamilyMemberCellKind.swift new file mode 100644 index 000000000..e6177581c --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Management/Config/FamilyMemberCellKind.swift @@ -0,0 +1,24 @@ +// +// MemberProfileCellType.swift +// App +// +// Created by 김건우 on 9/1/24. +// + +import Foundation + +// MARK: - Typealias + +public typealias FamilyMemberCellKind = FamilyMemberCell.Kind + + +// MARK: - Extensions + +public extension FamilyMemberCell { + + enum Kind { + case emoji + case management + } + +} diff --git a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/DataSource/FamilyMemberProfileSectionModel.swift b/14th-team5-iOS/App/Sources/Presentation/Management/Reactive/DataSource/FamilyMemberSectionModel.swift similarity index 59% rename from 14th-team5-iOS/App/Sources/Presentation/FamilyManagement/DataSource/FamilyMemberProfileSectionModel.swift rename to 14th-team5-iOS/App/Sources/Presentation/Management/Reactive/DataSource/FamilyMemberSectionModel.swift index 75c7a563b..132bbcb61 100644 --- a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/DataSource/FamilyMemberProfileSectionModel.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Management/Reactive/DataSource/FamilyMemberSectionModel.swift @@ -9,4 +9,4 @@ import Foundation import Differentiator -typealias FamilyMemberProfileSectionModel = SectionModel +typealias FamilyMemberSectionModel = SectionModel diff --git a/14th-team5-iOS/App/Sources/Presentation/Management/Reactive/DelegateProxy/Delegate/ManagementTableDelegate.swift b/14th-team5-iOS/App/Sources/Presentation/Management/Reactive/DelegateProxy/Delegate/ManagementTableDelegate.swift new file mode 100644 index 000000000..0a0bdce8b --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Management/Reactive/DelegateProxy/Delegate/ManagementTableDelegate.swift @@ -0,0 +1,14 @@ +// +// ManagementTableDelegate.swift +// App +// +// Created by 김건우 on 9/10/24. +// + +import UIKit + +@objc public protocol ManagementTableDelegate { + + @objc optional func table(_ refreshControl: UIRefreshControl, didPullDownRefreshControl: UIRefreshControl.Event) + +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Management/Reactive/DelegateProxy/Delegate/ManagementTableHeaderDelegate.swift b/14th-team5-iOS/App/Sources/Presentation/Management/Reactive/DelegateProxy/Delegate/ManagementTableHeaderDelegate.swift new file mode 100644 index 000000000..c29711534 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Management/Reactive/DelegateProxy/Delegate/ManagementTableHeaderDelegate.swift @@ -0,0 +1,14 @@ +// +// ManagementTableViewDelegateProxy.swift +// App +// +// Created by 김건우 on 9/1/24. +// + +import UIKit + +@objc public protocol ManagementTableHeaderDelegate { + + @objc optional func tableHeader(_ button: UIButton, didTapFamilyNameEidtButton: UIButton.Event) + +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Management/Reactive/DelegateProxy/Delegate/SharingContainerDelegate.swift b/14th-team5-iOS/App/Sources/Presentation/Management/Reactive/DelegateProxy/Delegate/SharingContainerDelegate.swift new file mode 100644 index 000000000..46f9b3ccd --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Management/Reactive/DelegateProxy/Delegate/SharingContainerDelegate.swift @@ -0,0 +1,14 @@ +// +// SharingDelegate.swift +// App +// +// Created by 김건우 on 9/1/24. +// + +import UIKit + +@objc public protocol SharingContainerDlegate { + + @objc optional func sharing(_ button: UIButton, didTapSharingButton event: UIButton.Event) + +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Management/Reactive/DelegateProxy/RxManagementTableDelegateProxy.swift b/14th-team5-iOS/App/Sources/Presentation/Management/Reactive/DelegateProxy/RxManagementTableDelegateProxy.swift new file mode 100644 index 000000000..704ee9c0b --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Management/Reactive/DelegateProxy/RxManagementTableDelegateProxy.swift @@ -0,0 +1,51 @@ +// +// RxManagementTableDelegateProxy.swift +// App +// +// Created by 김건우 on 9/10/24. +// + +import UIKit + +import RxCocoa +import RxSwift + +final public class RxManagementTableDelegateProxy: DelegateProxy, ManagementTableDelegate, DelegateProxyType { + + public static func registerKnownImplementations() { + self.register { + RxManagementTableDelegateProxy( + parentObject: $0, + delegateProxy: self + ) + } + } + + public static func setCurrentDelegate( + _ delegate: (any ManagementTableDelegate)?, + to object: ManagementTableView + ) { + object.control = delegate + } + + public static func currentDelegate(for object: ManagementTableView) -> (any ManagementTableDelegate)? { + object.control + } + +} + + +extension Reactive where Base: ManagementTableView { + + public var control: DelegateProxy { + RxManagementTableDelegateProxy.proxy(for: self.base) + } + + public var didPullDownRefreshControl: ControlEvent { + let source = control.sentMessage(#selector(ManagementTableDelegate.table(_:didPullDownRefreshControl:))) + .map { $0[0] as! UIRefreshControl } + + return ControlEvent(events: source) + } + +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Management/Reactive/DelegateProxy/RxManagementTableHeaderDelegateProxy.swift b/14th-team5-iOS/App/Sources/Presentation/Management/Reactive/DelegateProxy/RxManagementTableHeaderDelegateProxy.swift new file mode 100644 index 000000000..6210df017 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Management/Reactive/DelegateProxy/RxManagementTableHeaderDelegateProxy.swift @@ -0,0 +1,46 @@ +// +// RxManagementTableHeaderDelegateProxy.swift +// App +// +// Created by 김건우 on 9/1/24. +// + +import UIKit + +import RxCocoa +import RxSwift + +public final class RxManagementTableHeaderDelegateProxy: DelegateProxy, ManagementTableHeaderDelegate, DelegateProxyType { + + static public func registerKnownImplementations() { + self.register { + RxManagementTableHeaderDelegateProxy( + parentObject: $0, + delegateProxy: self + ) + } + } + +} + +extension ManagementTableHeaderView: HasDelegate { + + public typealias Delegate = ManagementTableHeaderDelegate + +} + + +extension Reactive where Base: ManagementTableHeaderView { + + public var delegate: DelegateProxy { + RxManagementTableHeaderDelegateProxy.proxy(for: self.base) + } + + public var didTapFamilyNameEditButton: ControlEvent { + let source = delegate.sentMessage(#selector(ManagementTableHeaderDelegate.tableHeader(_:didTapFamilyNameEidtButton:))) + .map { $0[0] as! UIButton } + + return ControlEvent(events: source) + } + +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Management/Reactive/DelegateProxy/RxSharingContainerDelegateProxy.swift b/14th-team5-iOS/App/Sources/Presentation/Management/Reactive/DelegateProxy/RxSharingContainerDelegateProxy.swift new file mode 100644 index 000000000..ab5a02476 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Management/Reactive/DelegateProxy/RxSharingContainerDelegateProxy.swift @@ -0,0 +1,46 @@ +// +// RxSharingRoundedRectView+DelegateProxy.swift +// App +// +// Created by 김건우 on 9/1/24. +// + +import UIKit + +import RxCocoa +import RxSwift + +public final class RxSharingContainerDelegateProxy: DelegateProxy, DelegateProxyType, SharingContainerDlegate { + + static public func registerKnownImplementations() { + self.register { + RxSharingContainerDelegateProxy( + parentObject: $0, + delegateProxy: self + ) + } + } + +} + +extension SharingContainerView: HasDelegate { + + public typealias Delegate = SharingContainerDlegate + +} + + +extension Reactive where Base: SharingContainerView { + + public var delegate: DelegateProxy { + RxSharingContainerDelegateProxy.proxy(for: self.base) + } + + public var didTapSharingButton: ControlEvent { + let source = delegate.sentMessage(#selector(SharingContainerDlegate.sharing(_:didTapSharingButton:))) + .map { $0[0] as! UIButton } + + return ControlEvent(events: source) + } + +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Management/Reactor/Cell/FamilyMemberCellReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Management/Reactor/Cell/FamilyMemberCellReactor.swift new file mode 100644 index 000000000..ee589f8b4 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Management/Reactor/Cell/FamilyMemberCellReactor.swift @@ -0,0 +1,100 @@ +// +// YourFamilProfileCellReactor.swift +// App +// +// Created by 김건우 on 12/12/23. +// + +import Core +import Domain +import Foundation + +import ReactorKit +import RxDataSources +import RxSwift + +final public class FamilyMemberCellReactor: Reactor { + + // MARK: - Action + + public enum Action { + case checkIsMe + case checkIsMemberBirth + } + + + // MARK: - Mutate + + public enum Mutation { + case setIsHiddenIsMeMark(Bool) + case setIsHiddenBirthBadge(Bool) + } + + + // MARK: - State + + public struct State { + let kind: FamilyMemberCellKind + var member: FamilyMemberProfileEntity + + var isHiddenIsMeMark: Bool = true + var isHiddenBirthBadge: Bool = true + } + + + // MARK: - Properties + + public var initialState: State + + @Injected var checkIsMeUseCase: CheckIsMeUseCaseProtocol + + + // MARK: - Intializer + + public init( + _ kind: FamilyMemberCellKind, + member: FamilyMemberProfileEntity + ) { + self.initialState = State( + kind: kind, + member: member + ) + } + + + // MARK: - Mutate + + public func mutate(action: Action) -> Observable { + let member = initialState.member + + switch action { + case .checkIsMe: + let checkIsMe = checkIsMeUseCase.execute(memberId: member.memberId) + let isHidden = initialState.kind == .management ? !checkIsMe : true + return Observable.just(.setIsHiddenIsMeMark(isHidden)) + + case .checkIsMemberBirth: + let checkBirth = member.dayOfBirth?.isEqual([.day, .month], with: Date.now) ?? false + let isHidden = initialState.kind == .management ? !checkBirth : true + return Observable.just(.setIsHiddenBirthBadge(isHidden)) + } + } + + + // MARK: - Reduce + + public func reduce(state: State, mutation: Mutation) -> State { + var newState = state + + switch mutation { + case let .setIsHiddenIsMeMark(isHidden): + newState.isHiddenIsMeMark = isHidden + + case let .setIsHiddenBirthBadge(isHidden): + newState.isHiddenBirthBadge = isHidden + } + + return newState + } + +} diff --git a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/Reactor/FamilyNameSettingViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Management/Reactor/FamilyNameSettingViewReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/FamilyManagement/Reactor/FamilyNameSettingViewReactor.swift rename to 14th-team5-iOS/App/Sources/Presentation/Management/Reactor/FamilyNameSettingViewReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Management/Reactor/ManagementReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Management/Reactor/ManagementReactor.swift new file mode 100644 index 000000000..134c8b3f5 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Management/Reactor/ManagementReactor.swift @@ -0,0 +1,179 @@ +// +// LinkShareViewReactor.swift +// App +// +// Created by 김건우 on 12/11/23. +// + +import UIKit + +import Core +import Domain +import Differentiator +import ReactorKit +import RxSwift + +public final class ManagementReactor: Reactor { + + // MARK: - Action + + public enum Action { + case fetchPaginationFamilyMemebers + case didTapSharingContainer + case didTapSettingBarButton + case didTapFamilyNameEditButton + case didSelectTableCell(IndexPath) + } + + + // MARK: - Mutate + + public enum Mutation { + case setMemberDatasource([FamilyMemberCellReactor]) + } + + + // MARK: - State + + public struct State { + @Pulse var memberDatasource: [FamilyMemberSectionModel] = [.init(model: (), items: [])] + } + + + // MARK: - Properties + + public let initialState: State + + @Navigator var navigator: ManagementNavigatorProtocol + + @Injected var fetchMyMemberIdIUseCase: FetchMyMemberIdUseCaseProtocol + @Injected var fetchFamilyMemberUseCase: FetchFamilyMembersUseCaseProtocol + @Injected var fetchSharingUrlUseCase: FetchInvitationLinkUseCaseProtocol + @Injected var checkIsMeUseCase: CheckIsMeUseCaseProtocol + + @Injected var provider: ServiceProviderProtocol + + + // MARK: - Intializer + + init() { + self.initialState = State() + } + + + // MARK: - Transform + + + public func transform(mutation: Observable) -> Observable { + let eventMutation = provider.managementService.event + .withUnretained(self) + .flatMap { owner, event -> Observable in + switch event { + case .didTapCopyUrlAction: + owner.navigator.showSuccessToast() + return Observable.empty() + default: + return Observable.empty() + } + } + + return Observable.merge(mutation, eventMutation) + } + + + // MARK: - Mutate + + public func mutate(action: Action) -> Observable { + let managementService = provider.managementService + + switch action { + case .didTapSharingContainer: + // Mixpanel + MPEvent.Family.shareLink.track(with: nil) + + managementService.hiddenSharingProgressHud(hidden: false) + return Observable.concat( + + fetchSharingUrlUseCase.execute() + .withUnretained(self) + .concatMap { + guard let sharingUrl = $0.1?.url else { + Haptic.notification(type: .error) + managementService.hiddenSharingProgressHud(hidden: true) + $0.0.navigator.showErrorToast() + + return Observable.empty() + } + + managementService.hiddenSharingProgressHud(hidden: true) + $0.0.navigator.presentSharingSheet(url: URL(string: sharingUrl)) + return Observable.empty() + } + ) + + case .didTapSettingBarButton: + if let memberId = fetchMyMemberIdIUseCase.execute() { + navigator.toSetting(memberId: memberId) + } + return Observable.empty() + + case .didTapFamilyNameEditButton: + navigator.toFamilyNameSetting() + return Observable.empty() + + case .fetchPaginationFamilyMemebers: + let query = FamilyPaginationQuery() + + return Observable.concat( + fetchFamilyMemberUseCase.execute(query: query) + .withUnretained(self) + .concatMap { + guard let results = $0.1?.results else { + Haptic.notification(type: .error) + managementService.hiddenTableProgressHud(hidden: true) + managementService.hiddenMemberFetchFailureView(hidden: false) + $0.0.navigator.showErrorToast() + managementService.endTableRefreshing() + + return Observable.just(.setMemberDatasource([])) + } + + managementService.hiddenTableProgressHud(hidden: true) + managementService.hiddenMemberFetchFailureView(hidden: true) + // TODO: - 새로운 가족 이름 반영하기 + managementService.setTableHeaderInfo(familyName: "나의 가족", memberCount: results.count) + managementService.endTableRefreshing() + + let items = results.sorted { [unowned self] in + self.checkIsMeUseCase.execute(memberId: $0.memberId) && + !self.checkIsMeUseCase.execute(memberId: $1.memberId) + }.map { FamilyMemberCellReactor(.management, member: $0) } + + return Observable.just(.setMemberDatasource(items)) + } + ) + + case let .didSelectTableCell(indexPath): + if let dataSource = currentState.memberDatasource.first { + let memberId = dataSource.items[indexPath.row].initialState.member.memberId + navigator.toProfile(memberId: memberId) + } + + return Observable.empty() + } + } + + + // MARK: - Reduce + + public func reduce(state: State, mutation: Mutation) -> State { + var newState = state + switch mutation { + + case let .setMemberDatasource(items): + let dataSource = FamilyMemberSectionModel(model: (), items: items) + newState.memberDatasource = [dataSource] + } + return newState + } +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Management/Reactor/ManagementTableHeaderReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Management/Reactor/ManagementTableHeaderReactor.swift new file mode 100644 index 000000000..500c1f408 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Management/Reactor/ManagementTableHeaderReactor.swift @@ -0,0 +1,94 @@ +// +// ManagementTableHeaderViewReactor.swift +// App +// +// Created by 김건우 on 9/1/24. +// + +import Core +import MacrosInterface + +import ReactorKit + +@Reactor +public final class ManagementTableHeaderReactor { + + // MARK: - Action + + public enum Action { } + + + // MARK: - Mutation + + public enum Mutation { + case setFamilyName(String) + case setMemberCount(Int) + } + + + // MARK: - State + + public struct State { + var familyName: String = "나의 가족" + var memberCount: String = "0" + } + + // MARK: - Properties + + @Injected var provider: ServiceProviderProtocol + + public var initialState: State + + + // MARK: - Intializer + + public init() { + self.initialState = State() + } + + + // MARK: - Transform + + public func transform(mutation: Observable) -> Observable { + let eventMutation = provider.managementService.event + .flatMap { event in + switch event { + case let .setTableHeaderInfo(familyName, memberCount): + return Observable.concat( + Observable.just(.setFamilyName(familyName)), + Observable.just(.setMemberCount(memberCount)) + ) + + default: + return Observable.empty() + } + } + + return Observable.merge(eventMutation, mutation) + } + + // MARK: - Mutate + + public func mutate(action: Action) -> Observable { + return .empty() + } + + + // MARK: - Reduce + + public func reduce(state: State, mutation: Mutation) -> State { + var newState = state + + switch mutation { + case let .setFamilyName(name): + newState.familyName = name + + case let .setMemberCount(count): + newState.memberCount = String(count) + } + + return newState + } + + +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Management/Reactor/ManagementTableReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Management/Reactor/ManagementTableReactor.swift new file mode 100644 index 000000000..619fdfbe6 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Management/Reactor/ManagementTableReactor.swift @@ -0,0 +1,96 @@ +// +// ManagementTableReactor.swift +// App +// +// Created by 김건우 on 9/10/24. +// + +import Core +import MacrosInterface + +import ReactorKit + +@Reactor +final public class ManagementTableReactor { + + // MARK: - Action + + public enum Action { } + + + // MARK: - Mutation + + public enum Mutation { + case setHiddenTableProgresssHud(Bool) + case setHiddenFetchFailureView(Bool) + case setEndRefreshing(Bool) + } + + + // MARK: - State + + public struct State { + var hiddenTableProgressHud: Bool = false + var hiddenFetchFailureView: Bool = true + @Pulse var isRefreshing: Bool = true + } + + + // MARK: - Prperties + + public let initialState: State + + @Injected var provider: ServiceProviderProtocol + + + // MARK: - Intializer + + public init() { + self.initialState = State() + } + + + // MARK: - Transform + + public func transform(mutation: Observable) -> Observable { + let eventMutation = provider.managementService.event + .flatMap { event in + switch event { + case let .hiddenTableProgressHud(hidden): + return Observable.just(.setHiddenTableProgresssHud(hidden)) + + case let .hiddenMemberFetchFailureView(hidden): + return Observable.just(.setHiddenFetchFailureView(hidden)) + + case .endTableRefreshing: + return Observable.just(.setEndRefreshing(false)) + + default: + return Observable.empty() + } + } + + return Observable.merge(eventMutation, mutation) + } + + + // MARK: - Reduce + + public func reduce(state: State, mutation: Mutation) -> State { + var newState = state + + switch mutation { + case let .setHiddenTableProgresssHud(hidden): + newState.hiddenTableProgressHud = hidden + + case let .setHiddenFetchFailureView(hidden): + newState.hiddenFetchFailureView = hidden + + case let .setEndRefreshing(isRefreshing): + newState.isRefreshing = isRefreshing + } + + return newState + } + +} diff --git a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/Reactor/InvitationUrlContainerViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Management/Reactor/SharingContainerReactor.swift similarity index 60% rename from 14th-team5-iOS/App/Sources/Presentation/FamilyManagement/Reactor/InvitationUrlContainerViewReactor.swift rename to 14th-team5-iOS/App/Sources/Presentation/Management/Reactor/SharingContainerReactor.swift index 776480f90..6da9a3fa9 100644 --- a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/Reactor/InvitationUrlContainerViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Management/Reactor/SharingContainerReactor.swift @@ -11,38 +11,49 @@ import Foundation import ReactorKit import RxSwift -final public class InvitationUrlContainerViewReactor: Reactor { +final public class SharingContainerReactor: Reactor { + // MARK: - Action + public enum Action { } + // MARK: - Mutation + public enum Mutation { - case setIndicatorView(Bool) + case setProgressHud(Bool) } + // MARK: - State + public struct State { - var shouldHiddenIndicatorView: Bool + var hiddenProgresHud: Bool = true } + // MARK: - Properties + public var initialState: State - public var provider: GlobalStateProviderProtocol + @Injected var provider: ServiceProviderProtocol + // MARK: - Intializer - public init(provider: GlobalStateProviderProtocol) { - self.initialState = State(shouldHiddenIndicatorView: false) - self.provider = provider + + public init() { + self.initialState = State() } + // MARK: - Transform + public func transform(mutation: Observable) -> Observable { - let eventMutation = provider.activityGlobalState.event + let eventMutation = provider.managementService.event .flatMap { event -> Observable in switch event { - case let .hiddenInvitationUrlIndicatorView(hidden): - return Observable.just(.setIndicatorView(hidden)) + case let .hiddenSharingProgressHud(hidden): + return Observable.just(.setProgressHud(hidden)) default: return Observable.empty() } @@ -51,12 +62,14 @@ final public class InvitationUrlContainerViewReactor: Reactor { return Observable.merge(mutation, eventMutation) } + // MARK: - Reduce + public func reduce(state: State, mutation: Mutation) -> State { var newState = state switch mutation { - case let .setIndicatorView(hidden): - newState.shouldHiddenIndicatorView = hidden + case let .setProgressHud(hidden): + newState.hiddenProgresHud = hidden } return newState } diff --git a/14th-team5-iOS/App/Sources/Presentation/Management/View/Cell/FamilyMemberCell.swift b/14th-team5-iOS/App/Sources/Presentation/Management/View/Cell/FamilyMemberCell.swift new file mode 100644 index 000000000..c3788c7e0 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Management/View/Cell/FamilyMemberCell.swift @@ -0,0 +1,209 @@ +// +// FamilyProfileCell.swift +// App +// +// Created by 김건우 on 12/12/23. +// + +import Core +import DesignSystem +import UIKit + +import ReactorKit +import RxSwift +import SnapKit +import Then + +final public class FamilyMemberCell: BaseTableViewCell { + + // MARK: - Views + + private let profileBackground: UIView = UIView() + private let profileImage: UIImageView = UIImageView() + private let profilePlaceholder: BBLabel = BBLabel(.head2Bold, textAlignment: .center, textColor: .gray200) + private let birthBadge: UIImageView = UIImageView() + + private let labelStack: UIStackView = UIStackView() + private let nameLabel: BBLabel = BBLabel(.body1Regular, textColor: .gray200) + private let isMeLabel: BBLabel = BBLabel(.body2Regular, textColor: .gray500) + + private let rightArrowSymbol: UIImageView = UIImageView() + + + // MARK: - Properties + + static let id: String = "FamilyMemberCell" + + private var containerSize: CGFloat { + guard let reactor else { return 52 } + + let size: CGFloat = switch reactor.initialState.kind { + case .emoji: 44 + case .management: 52 + } + + return size + } + + + // MARK: - Intializer + + override init( + style: UITableViewCell.CellStyle, + reuseIdentifier: String? + ) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + // MARK: - Helpers + + public override func prepareForReuse() { + super.prepareForReuse() + + nameLabel.text = "" + isMeLabel.text = "" + profileImage.image = nil + } + + public override func bind(reactor: FamilyMemberCellReactor) { + super.bind(reactor: reactor) + bindInput(reactor: reactor) + bindOutput(reactor: reactor) + } + + private func bindInput(reactor: FamilyMemberCellReactor) { + Observable.merge([ + Observable.just(.checkIsMe), + Observable.just(.checkIsMemberBirth) + ]) + .bind(to: reactor.action) + .disposed(by: disposeBag) + } + + private func bindOutput(reactor: FamilyMemberCellReactor) { + let memberName = reactor.state + .map { $0.member.name } + .asDriver(onErrorJustReturn: .none) + + memberName + .distinctUntilChanged() + .drive(nameLabel.rx.text) + .disposed(by: disposeBag) + + memberName + .distinctUntilChanged() + .drive(profilePlaceholder.rx.firstLetterText) + .disposed(by: disposeBag) + + reactor.state.map { $0.isHiddenIsMeMark } + .distinctUntilChanged() + .bind(with: self) { $0.isMeLabel.text = $1 ? "" : "Me" } + .disposed(by: disposeBag) + + reactor.state.map { $0.isHiddenBirthBadge} + .distinctUntilChanged() + .bind(to: birthBadge.rx.isHidden) + .disposed(by: disposeBag) + + reactor.state.compactMap { $0.member.profileImageURL } + .distinctUntilChanged() + .bind(to: profileImage.rx.kingfisherImage) + .disposed(by: disposeBag) + + let kind = reactor.state + .map { $0.kind } + .map { $0 == .emoji } + .asDriver(onErrorJustReturn: false) + + kind + .distinctUntilChanged() + .drive(rightArrowSymbol.rx.isHidden) + .disposed(by: disposeBag) + + } + + public override func setupUI() { + super.setupUI() + profileBackground.addSubviews(profilePlaceholder, profileImage) + contentView.addSubviews(profileBackground, birthBadge,labelStack, rightArrowSymbol) + labelStack.addArrangedSubviews(nameLabel, isMeLabel) + } + + public override func setupAutoLayout() { + super.setupAutoLayout() + + profileBackground.snp.makeConstraints { + $0.size.equalTo(containerSize) + $0.leading.equalTo(contentView.snp.leading).offset(20) + $0.verticalEdges.equalToSuperview().inset(12) + } + + birthBadge.snp.makeConstraints { + $0.size.equalTo(20) + $0.top.equalTo(profileBackground.snp.top).offset(-5) + $0.trailing.equalTo(profileBackground.snp.trailing).offset(5) + } + + profilePlaceholder.snp.makeConstraints { + $0.center.equalToSuperview() + } + + profileImage.snp.makeConstraints { + $0.edges.equalToSuperview() + } + + labelStack.snp.makeConstraints { + $0.leading.equalTo(profileBackground.snp.trailing).offset(16) + $0.centerY.equalTo(profileBackground.snp.centerY) + } + + rightArrowSymbol.snp.makeConstraints { + $0.size.equalTo(20) + $0.centerY.equalToSuperview() + $0.trailing.equalToSuperview().offset(-20) + } + } + + public override func setupAttributes() { + super.setupAttributes() + + self.selectionStyle = .none + self.backgroundColor = .clear + + profileBackground.do { + $0.layer.masksToBounds = true + $0.layer.cornerRadius = containerSize / 2 + $0.backgroundColor = UIColor.gray800 + } + + birthBadge.do { + $0.image = DesignSystemAsset.birthday.image + $0.contentMode = .scaleAspectFit + } + + profileImage.do { + $0.contentMode = .scaleAspectFill + $0.layer.cornerRadius = containerSize / 2 + } + + labelStack.do { + $0.axis = .vertical + $0.spacing = 3 + $0.alignment = .fill + $0.distribution = .fillProportionally + } + + rightArrowSymbol.do { + let arrowRight = DesignSystemAsset.arrowRight.image + $0.image = arrowRight.withRenderingMode(.alwaysTemplate) + $0.tintColor = UIColor.gray500 + $0.contentMode = .scaleAspectFill + } + } + +} diff --git a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/View/JoinFamilyGroupEditorView.swift b/14th-team5-iOS/App/Sources/Presentation/Management/View/JoinFamilyGroupEditorView.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/FamilyManagement/View/JoinFamilyGroupEditorView.swift rename to 14th-team5-iOS/App/Sources/Presentation/Management/View/JoinFamilyGroupEditorView.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Management/View/ManagementTableHeaderView.swift b/14th-team5-iOS/App/Sources/Presentation/Management/View/ManagementTableHeaderView.swift new file mode 100644 index 000000000..ae42dcc36 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Management/View/ManagementTableHeaderView.swift @@ -0,0 +1,111 @@ +// +// ManagementTableHeaderView.swift +// App +// +// Created by 김건우 on 9/1/24. +// + +import DesignSystem +import Core +import UIKit + +import SnapKit +import Then + +public final class ManagementTableHeaderView: BaseView { + + // MARK: - Views + + private let titleStack: UIStackView = UIStackView() + private let familyNameLabel: BBLabel = BBLabel(.head1, textColor: .gray200) + private let memberCountLabel: BBLabel = BBLabel(.body1Regular, textColor: .gray400) + private let familyNameEditButton: BBButton = BBButton() + + + // MARK: - Properties + + public weak var delegate: ManagementTableHeaderDelegate? + + + // MARK: - Helpers + + public override func bind(reactor: ManagementTableHeaderReactor) { + super.bind(reactor: reactor) + bindInput(reactor: reactor) + bindOutput(reactor: reactor) + } + + private func bindInput(reactor: ManagementTableHeaderReactor) { + + } + + private func bindOutput(reactor: ManagementTableHeaderReactor) { + reactor.state.map { $0.familyName } + .distinctUntilChanged() + .bind(to: familyNameLabel.rx.text) + .disposed(by: disposeBag) + + reactor.state.map { $0.memberCount } + .distinctUntilChanged() + .bind(to: memberCountLabel.rx.text) + .disposed(by: disposeBag) + } + + public override func setupUI() { + super.setupUI() + + self.addSubviews(titleStack, familyNameEditButton) + titleStack.addArrangedSubviews(familyNameLabel, memberCountLabel) + } + + public override func setupAttributes() { + super.setupAttributes() + + titleStack.do { + $0.axis = .horizontal + $0.spacing = 10 + $0.alignment = .fill + $0.distribution = .fillProportionally + } + + familyNameLabel.do { + $0.text = "나의 가족" + } + + memberCountLabel.do { + $0.text = "0" + } + + familyNameEditButton.do { + $0.setImage(DesignSystemAsset.edit.image, for: .normal) + $0.addTarget(self, action: #selector(didTapFamilyNameEditButton(_:event:)), for: .touchUpInside) + } + } + + public override func setupAutoLayout() { + super.setupAutoLayout() + + titleStack.snp.makeConstraints { + $0.centerY.equalToSuperview() + $0.leading.equalToSuperview().inset(24) + } + + familyNameEditButton.snp.makeConstraints { + $0.size.equalTo(24) + $0.centerY.equalTo(titleStack) + $0.trailing.equalToSuperview().offset(-28) + } + } + +} + + +// MARK: - Extensions + +extension ManagementTableHeaderView { + + @objc func didTapFamilyNameEditButton(_ button: UIButton, event: UIButton.Event) { + delegate?.tableHeader?(button, didTapFamilyNameEidtButton: event) + } + +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Management/View/ManagementTableView.swift b/14th-team5-iOS/App/Sources/Presentation/Management/View/ManagementTableView.swift new file mode 100644 index 000000000..6a94cb835 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Management/View/ManagementTableView.swift @@ -0,0 +1,150 @@ +// +// ManagementTableView.swift +// App +// +// Created by 김건우 on 9/1/24. +// + +import Core +import UIKit + +import SnapKit +import Then + +public final class ManagementTableView: BaseTableView { + + // MARK: - Views + + private let basicRefreshControl: UIRefreshControl = UIRefreshControl() + // TODO: - 리팩토링된 FetchFailure로 바꾸기 + private let fetchFailureView: BibbiFetchFailureView = BibbiFetchFailureView(type: .family) + + // MARK: - Properties + + public weak var control: ManagementTableDelegate? + + private lazy var progressHud: BBProgressHUD = { + let config = BBProgressHUDConfiguration(attachedTo: self) + let viewConfig = BBProgressHUDViewConfiguration( + offsetFromCenterY: -100, + backgroundColor: UIColor.clear + ) + let hud = BBProgressHUD.lottie( + .airplane, + title: "열심히 불러오는 중..", + titleFontStyle: .body1Regular, + titleColor: .gray400, + viewConfig: viewConfig, + config: config + ) + return hud + }() + + + // MARK: - Intializer + + public convenience init() { + self.init(frame: .zero, style: .plain) + } + + public override init(frame: CGRect, style: UITableView.Style) { + super.init(frame: frame, style: style) + + setupAttributes() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + // MARK: - Helpers + + public override func bind(reactor: ManagementTableReactor) { + super.bind(reactor: reactor) + + bindOutput(reactor: reactor) + } + + private func bindOutput(reactor: ManagementTableReactor) { + + reactor.state.map { $0.hiddenTableProgressHud } + .distinctUntilChanged() + .bind(with: self) { owner, hidden in + hidden ? owner.progressHud.close() : owner.progressHud.show() + } + .disposed(by: disposeBag) + + reactor.state.map { $0.hiddenFetchFailureView } + .distinctUntilChanged() + .bind(to: fetchFailureView.rx.isHidden) + .disposed(by: disposeBag) + + reactor.pulse(\.$isRefreshing) + .delay(RxInterval._700milliseconds, scheduler: RxScheduler.main) + .bind(to: basicRefreshControl.rx.isRefreshing) + .disposed(by: disposeBag) + + + } + + + public override func setupUI() { + super.setupUI() + + self.addSubview(fetchFailureView) + } + + public override func setupAttributes() { + super.setupAttributes() + + self.do { + $0.separatorStyle = .none + $0.estimatedRowHeight = UITableView.automaticDimension + $0.backgroundColor = UIColor.clear + + $0.refreshControl = basicRefreshControl + + $0.register(FamilyMemberCell.self, forCellReuseIdentifier: FamilyMemberCell.id) + } + + basicRefreshControl.do { + $0.tintColor = UIColor.bibbiWhite + $0.addTarget(self, action: #selector(didPullDownRefreshControl(_:event:)), for: .valueChanged) + } + + fetchFailureView.do { + $0.isHidden = true + } + } + + public override func setupAutoLayout() { + super.setupAutoLayout() + + fetchFailureView.snp.makeConstraints { + $0.top.equalToSuperview().offset(70) + $0.centerX.equalToSuperview() + } + } + +} + + +// MARK: - Extensions + +extension ManagementTableView { + + // TODO: - 임시 코드 삭제하기 + func endRefreshing() { + basicRefreshControl.endRefreshing() + } + +} + +extension ManagementTableView { + + @objc func didPullDownRefreshControl(_ refreshControl: UIRefreshControl, event: UIRefreshControl.Event) { + control?.table?(refreshControl, didPullDownRefreshControl: event) + } + +} diff --git a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/View/InvitationUrlContainerView.swift b/14th-team5-iOS/App/Sources/Presentation/Management/View/SharingContainerView.swift similarity index 50% rename from 14th-team5-iOS/App/Sources/Presentation/FamilyManagement/View/InvitationUrlContainerView.swift rename to 14th-team5-iOS/App/Sources/Presentation/Management/View/SharingContainerView.swift index 9d52c8a6a..34bc97011 100644 --- a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/View/InvitationUrlContainerView.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Management/View/SharingContainerView.swift @@ -9,19 +9,28 @@ import Core import DesignSystem import UIKit -fileprivate typealias _Str = FamilyManagementStrings -public final class InvitationUrlContainerView: BaseView { +public final class SharingContainerView: BaseView { + // MARK: - Views - private let shareContainerView: UIView = UIView() + + private let containerView: UIView = UIView() private let envelopeImageView: UIImageView = UIImageView() private let labelStack: UIStackView = UIStackView() - private let invitationDescLabel: BBLabel = BBLabel(.head2Bold, textColor: .gray200) - private let invitationUrlLabel: BBLabel = BBLabel(.body2Regular, textColor: .gray300) - private let shareLineImageView: UIImageView = UIImageView() - private let indicatorView: UIActivityIndicatorView = UIActivityIndicatorView() + private let sharingDescriptionLabel: BBLabel = BBLabel(.head2Bold, textColor: .gray200) + private let sharingUrlLabel: BBLabel = BBLabel(.body2Regular, textColor: .gray300) + private let sharingImageView: UIImageView = UIImageView() + + private let basicProgressHud: UIActivityIndicatorView = UIActivityIndicatorView() + + + // MARK: - Properties + + public weak var delegate: SharingContainerDlegate? + // MARK: - Intialzier + public override init(frame: CGRect) { super.init(frame: .zero) } @@ -30,81 +39,84 @@ public final class InvitationUrlContainerView: BaseView { + + // MARK: - Typealias + + private typealias RxDataSource = RxTableViewSectionedReloadDataSource + + + // MARK: - Views + + private lazy var sharingContainerView: SharingContainerView = makeSharingContainerView() + private let divider: UIView = UIView() + + private lazy var memberTableHeaderView: ManagementTableHeaderView = makeMengementTableHeaderView() + private lazy var memberTableView: ManagementTableView = makeManagementTableView() + + + // MARK: - Properties + + private lazy var dataSource: RxDataSource = { + prepareDatasource() + }() + + // MARK: - Helpers + + public override func bind(reactor: ManagementReactor) { + super.bind(reactor: reactor) + bindInput(reactor: reactor) + bindOutput(reactor: reactor) + } + + private func bindInput(reactor: ManagementReactor) { + Observable.just(()) + .map { Reactor.Action.fetchPaginationFamilyMemebers } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + navigationBar.rx.didTapRightBarButton + .map { _ in Reactor.Action.didTapSettingBarButton } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + sharingContainerView.rx.tap + .throttle(RxInterval._300milliseconds, scheduler: MainScheduler.instance) + .map { Reactor.Action.didTapSharingContainer } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + memberTableView.rx.itemSelected + .map { Reactor.Action.didSelectTableCell($0) } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + memberTableView.rx.didPullDownRefreshControl + .throttle(RxInterval._300milliseconds, scheduler: RxScheduler.main) + .map { _ in Reactor.Action.fetchPaginationFamilyMemebers } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + memberTableHeaderView.rx.didTapFamilyNameEditButton + .throttle(RxInterval._300milliseconds, scheduler: RxScheduler.main) + .map { _ in Reactor.Action.didTapFamilyNameEditButton } + .bind(to: reactor.action) + .disposed(by: disposeBag) + } + + private func bindOutput(reactor: ManagementReactor) { + + reactor.pulse(\.$memberDatasource) + .bind(to: memberTableView.rx.items(dataSource: dataSource)) + .disposed(by: disposeBag) + + } + + public override func setupUI() { + super.setupUI() + + view.addSubviews(sharingContainerView, divider, memberTableHeaderView, memberTableView) + } + + public override func setupAutoLayout() { + super.setupAutoLayout() + + sharingContainerView.snp.makeConstraints { + $0.top.equalTo(navigationBar.snp.bottom).offset(24) + $0.horizontalEdges.equalToSuperview().inset(20) + $0.height.equalTo(90) + } + + divider.snp.makeConstraints { + $0.height.equalTo(1) + $0.top.equalTo(sharingContainerView.snp.bottom).offset(24) + $0.horizontalEdges.equalToSuperview() + } + + memberTableHeaderView.snp.makeConstraints { + $0.height.equalTo(73) + $0.top.equalTo(divider.snp.bottom) + $0.horizontalEdges.equalToSuperview() + } + + memberTableView.snp.makeConstraints { + $0.horizontalEdges.equalToSuperview() + $0.top.equalTo(memberTableHeaderView.snp.bottom).offset(8) + $0.bottom.equalToSuperview() + } + } + + public override func setupAttributes() { + super.setupAttributes() + + navigationBar.do { + $0.navigationTitle = "가족" + $0.navigationTitleFontStyle = .head2Bold + $0.leftBarButtonItem = .arrowLeft + $0.rightBarButtonItem = .setting + } + + divider.do { + $0.backgroundColor = UIColor.gray600 + } + + } +} + + +// MARK: - Extensions + +extension ManagementViewController { + + // TODO: - 코드 리팩토링하기 + private func makeSharingContainerView() -> SharingContainerView { + return SharingContainerView( + reactor: SharingContainerReactor() + ) + } + + private func makeMengementTableHeaderView() -> ManagementTableHeaderView { + return ManagementTableHeaderView( + reactor: ManagementTableHeaderReactor() + ) + } + + private func makeManagementTableView() -> ManagementTableView { + return ManagementTableView( + reactor: ManagementTableReactor() + ) + } + +} + +extension ManagementViewController { + + private func prepareDatasource() -> RxDataSource { + return RxDataSource { datasource, tableView, indexPath, reactor in + let cell = tableView.dequeueReusableCell( + withIdentifier: FamilyMemberCell.id, + for: indexPath + ) as! FamilyMemberCell + cell.reactor = reactor + return cell + } + } + +} diff --git a/14th-team5-iOS/App/Sources/Presentation/PostComment/Dependency/PostCommentDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/PostComment/Dependency/PostCommentDIContainer.swift index 64c5fce2a..329dfc2c9 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostComment/Dependency/PostCommentDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostComment/Dependency/PostCommentDIContainer.swift @@ -13,9 +13,9 @@ import UIKit @available(*, deprecated, renamed: "CommentViewControllerWrapper") public final class PostCommentDIContainer { // MARK: - Properties - private var globalState: GlobalStateProviderProtocol { + private var globalState: ServiceProviderProtocol { guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { - return GlobalStateProvider() + return ServiceProvider() } return appDelegate.globalStateProvider } diff --git a/14th-team5-iOS/App/Sources/Presentation/PostComment/Reactor/CommentCellReactor.swift b/14th-team5-iOS/App/Sources/Presentation/PostComment/Reactor/CommentCellReactor.swift index c252ec1e8..44f3030bb 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostComment/Reactor/CommentCellReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostComment/Reactor/CommentCellReactor.swift @@ -44,14 +44,14 @@ final public class CommentCellReactor: Reactor { public var memberUseCase: MemberUseCaseProtocol public var postCommentUseCase: PostCommentUseCaseProtocol - public var provider: GlobalStateProviderProtocol + public var provider: ServiceProviderProtocol // MARK: - Intializer public init( _ commentResponse: PostCommentEntity, memberUseCase: MemberUseCaseProtocol, postCommentUseCase: PostCommentUseCaseProtocol, - provider: GlobalStateProviderProtocol + provider: ServiceProviderProtocol ) { self.initialState = State( commentId: commentResponse.commentId, diff --git a/14th-team5-iOS/App/Sources/Presentation/PostComment/Reactor/CommentViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/PostComment/Reactor/CommentViewReactor.swift index 087c447d0..488dfc10c 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostComment/Reactor/CommentViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostComment/Reactor/CommentViewReactor.swift @@ -72,7 +72,7 @@ final public class CommentViewReactor: Reactor { @Injected var memberUseCase: MemberUseCaseProtocol @Injected var postCommentUseCase: PostCommentUseCaseProtocol - @Injected var provider: GlobalStateProviderProtocol + @Injected var provider: ServiceProviderProtocol private var postComentCount: Int = 0 private var hasReceivedInputEvent: Bool = false diff --git a/14th-team5-iOS/App/Sources/Presentation/PostComment/View/Cell/CommentCell.swift b/14th-team5-iOS/App/Sources/Presentation/PostComment/View/Cell/CommentCell.swift index 069fc951c..d11ee7201 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostComment/View/Cell/CommentCell.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostComment/View/Cell/CommentCell.swift @@ -81,7 +81,7 @@ final public class CommentCell: BaseTableViewCell { userName .distinctUntilChanged() - .drive(firstNameLabel.rx.firtNameText) + .drive(with: self) { $0.userNameLabel.text = $1[0] } .disposed(by: disposeBag) reactor.state.map { $0.profileImageUrlString } diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostDetailViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostDetailViewReactor.swift index 7c15598ba..68f86d1ee 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostDetailViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostDetailViewReactor.swift @@ -39,7 +39,7 @@ final class PostDetailViewReactor: Reactor { } let initialState: State - @Injected var provider: GlobalStateProviderProtocol + @Injected var provider: ServiceProviderProtocol @Injected var memberUseCase: MemberUseCaseProtocol init(initialState: State) { diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostReactor.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostReactor.swift index 723f7810c..1e6a99198 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostReactor.swift @@ -39,7 +39,7 @@ final class PostReactor: Reactor { let initialState: State @Injected var fetchMissionUseCase: FetchMissionContentUseCaseProtocol - @Injected var provider: GlobalStateProviderProtocol + @Injected var provider: ServiceProviderProtocol init(initialState: State) { diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/ReactionMemberViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/ReactionMemberViewReactor.swift index 76aa4009b..118342558 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/ReactionMemberViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/ReactionMemberViewReactor.swift @@ -18,12 +18,12 @@ final class ReactionMemberViewReactor: Reactor { } enum Mutation { - case setMemberDataSource([FamilyMemberProfileSectionModel]) + case setMemberDataSource([FamilyMemberSectionModel]) } struct State { let emojiData: EmojiEntity - var memberDataSource: [FamilyMemberProfileSectionModel] = [] + var memberDataSource: [FamilyMemberSectionModel] = [] } let initialState: State @@ -40,14 +40,14 @@ extension ReactionMemberViewReactor { case .makeDataSource: let profiles: [FamilyMemberProfileEntity] = familyUseCase.executeFetchPaginationFamilyMembers(memberIds: currentState.emojiData.memberIds) - var items: [FamilyMemberProfileCellReactor] = [] + var items: [FamilyMemberCellReactor] = [] profiles.forEach { let member = FamilyMemberProfileEntity( memberId: $0.memberId, profileImageURL: $0.profileImageURL, name: $0.name ) - items.append(FamilyMemberProfileCellReactor(member, isMe: false, cellType: .emoji)) + items.append(FamilyMemberCellReactor(.emoji, member: member)) } if profiles.count != currentState.emojiData.memberIds.count { @@ -57,11 +57,11 @@ extension ReactionMemberViewReactor { memberId: .none, name: .unknown ) - items.append(FamilyMemberProfileCellReactor(member, isMe: false, cellType: .emoji)) + items.append(FamilyMemberCellReactor(.emoji, member: member)) } } - let dataSource: FamilyMemberProfileSectionModel = .init(model: (), items: items) + let dataSource: FamilyMemberSectionModel = .init(model: (), items: items) return Observable.just(Mutation.setMemberDataSource([dataSource])) } } diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/ReactionViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/ReactionViewReactor.swift index 888ac4b54..b0fbb1a01 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/ReactionViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/ReactionViewReactor.swift @@ -54,7 +54,7 @@ final class ReactionViewReactor: Reactor { } let initialState: State - @Injected var provider: GlobalStateProviderProtocol + @Injected var provider: ServiceProviderProtocol @Injected var fetchReactionListUseCase: FetchReactionListUseCaseProtocol @Injected var createReactionUseCase: CreateReactionUseCaseProtocol @Injected var removeReactionUseCase: RemoveReactionUseCaseProtocol diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/ReactionMembersViewController.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/ReactionMembersViewController.swift index 34b6e4144..e7b5f6bb4 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/ReactionMembersViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/ReactionMembersViewController.swift @@ -135,7 +135,7 @@ final class ReactionMembersViewController: BaseViewController RxTableViewSectionedReloadDataSource { - return RxTableViewSectionedReloadDataSource { datasource, tableView, indexPath, reactor in - let cell = tableView.dequeueReusableCell(withIdentifier: FamilyMemberProfileCell.id, for: indexPath) as! FamilyMemberProfileCell + private func createDataSource() -> RxTableViewSectionedReloadDataSource { + return RxTableViewSectionedReloadDataSource { datasource, tableView, indexPath, reactor in + let cell = tableView.dequeueReusableCell(withIdentifier: FamilyMemberCell.id, for: indexPath) as! FamilyMemberCell cell.reactor = reactor return cell } diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileFeedPageViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileFeedPageViewReactor.swift index 4feb09dde..3fabe6cf0 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileFeedPageViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileFeedPageViewReactor.swift @@ -14,7 +14,7 @@ final class ProfileFeedPageViewReactor: Reactor { //MAKR: Property public var initialState: State - @Injected private var provider: GlobalStateProviderProtocol + @Injected private var provider: ServiceProviderProtocol enum Action { case updatePageViewController(Int) diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift index 48c6ee2d9..7ee8ea6f8 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift @@ -24,7 +24,7 @@ public final class ProfileViewReactor: Reactor { private let memberId: String private let isUser: Bool - @Injected private var provider: GlobalStateProviderProtocol + @Injected private var provider: ServiceProviderProtocol public enum Action { case viewDidLoad diff --git a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/View/AirplaneLottieView.swift b/14th-team5-iOS/App/Sources/Presentation/Trash/AirplaneLottieView.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/FamilyManagement/View/AirplaneLottieView.swift rename to 14th-team5-iOS/App/Sources/Presentation/Trash/AirplaneLottieView.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/View/BibbiFetchFailureView.swift b/14th-team5-iOS/App/Sources/Presentation/Trash/BibbiFetchFailureView.swift similarity index 99% rename from 14th-team5-iOS/App/Sources/Presentation/FamilyManagement/View/BibbiFetchFailureView.swift rename to 14th-team5-iOS/App/Sources/Presentation/Trash/BibbiFetchFailureView.swift index 5cba1d616..a75d4b5e5 100644 --- a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/View/BibbiFetchFailureView.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Trash/BibbiFetchFailureView.swift @@ -12,6 +12,7 @@ import UIKit import Then import SnapKit +@available(*, deprecated) final public class BibbiFetchFailureView: UIView { // MARK: - Type public enum FailureType: String { diff --git a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/View/BlurAiraplaneLottieView.swift b/14th-team5-iOS/App/Sources/Presentation/Trash/BlurAiraplaneLottieView.swift similarity index 99% rename from 14th-team5-iOS/App/Sources/Presentation/FamilyManagement/View/BlurAiraplaneLottieView.swift rename to 14th-team5-iOS/App/Sources/Presentation/Trash/BlurAiraplaneLottieView.swift index e8560e306..1539e69c6 100644 --- a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/View/BlurAiraplaneLottieView.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Trash/BlurAiraplaneLottieView.swift @@ -13,14 +13,18 @@ import Then import SnapKit final public class BlurAiraplaneLottieView: UIView { + // MARK: - Views + private let blurContainerView: UIView = UIView() private let lottieStack: UIStackView = UIStackView() private let lottieView: LottieView = LottieView(with: .loading) private let loadingLabel: BBLabel = BBLabel(.body1Regular, textAlignment: .center, textColor: .gray500) + // MARK: - Properties + public var containerSize: CGFloat = 125 { didSet { blurContainerView.snp.updateConstraints { diff --git a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/Dependency/FamilyManagementDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/Trash/Dependency/FamilyManagementDIContainer.swift similarity index 76% rename from 14th-team5-iOS/App/Sources/Presentation/FamilyManagement/Dependency/FamilyManagementDIContainer.swift rename to 14th-team5-iOS/App/Sources/Presentation/Trash/Dependency/FamilyManagementDIContainer.swift index 8850dfba3..d0327481d 100644 --- a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/Dependency/FamilyManagementDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Trash/Dependency/FamilyManagementDIContainer.swift @@ -15,8 +15,8 @@ import Domain public final class FamilyManagementDIContainer { // MARK: - Make - public func makeViewController() -> FamilyManagementViewController { - return FamilyManagementViewController(reactor: makeReactor()) + public func makeViewController() -> ManagementViewController { + return ManagementViewController(reactor: makeReactor()) } public func makeMemberUseCase() -> MemberUseCaseProtocol { @@ -35,7 +35,7 @@ public final class FamilyManagementDIContainer { return FamilyRepository() } - public func makeReactor() -> FamilyManagementViewReactor { - return FamilyManagementViewReactor() + public func makeReactor() -> ManagementReactor { + return ManagementReactor() } } diff --git a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/Dependency/InvitationUrlContainerDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/Trash/Dependency/InvitationUrlContainerDIContainer.swift similarity index 54% rename from 14th-team5-iOS/App/Sources/Presentation/FamilyManagement/Dependency/InvitationUrlContainerDIContainer.swift rename to 14th-team5-iOS/App/Sources/Presentation/Trash/Dependency/InvitationUrlContainerDIContainer.swift index b8267ba64..862f3f64d 100644 --- a/14th-team5-iOS/App/Sources/Presentation/FamilyManagement/Dependency/InvitationUrlContainerDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Trash/Dependency/InvitationUrlContainerDIContainer.swift @@ -12,19 +12,19 @@ import UIKit @available(*, deprecated) public final class InvitationUrlContainerDIContainer { // MARK: - Properties - private var globalState: GlobalStateProviderProtocol { + private var globalState: ServiceProviderProtocol { guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { - return GlobalStateProvider() + return ServiceProvider() } return appDelegate.globalStateProvider } // MARK: - Make - public func makeView() -> InvitationUrlContainerView { - return InvitationUrlContainerView(reactor: makeReactor()) + public func makeView() -> SharingContainerView { + return SharingContainerView(reactor: makeReactor()) } - public func makeReactor() -> InvitationUrlContainerViewReactor { - return InvitationUrlContainerViewReactor(provider: globalState) + public func makeReactor() -> SharingContainerReactor { + return SharingContainerReactor() } } diff --git a/14th-team5-iOS/App/Sources/Manager/DeepLinkManager.swift b/14th-team5-iOS/App/Sources/Presentation/Trash/Manager/DeepLinkManager.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Manager/DeepLinkManager.swift rename to 14th-team5-iOS/App/Sources/Presentation/Trash/Manager/DeepLinkManager.swift diff --git a/14th-team5-iOS/Core/Sources/Base/BBNavigationViewController.swift b/14th-team5-iOS/Core/Sources/Base/BBNavigationViewController.swift index 313940a44..689ddefd6 100644 --- a/14th-team5-iOS/Core/Sources/Base/BBNavigationViewController.swift +++ b/14th-team5-iOS/Core/Sources/Base/BBNavigationViewController.swift @@ -57,6 +57,11 @@ open class BBNavigationViewController: ReactorViewController where R: Reac } + open override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + navigationController?.navigationBar.isHidden = true + } + // MARK: - Helpers open override func bind(reactor: R) { super.bind(reactor: reactor) diff --git a/14th-team5-iOS/Core/Sources/Base/BaseService.swift b/14th-team5-iOS/Core/Sources/Base/BaseService.swift index edd4c22fe..52dd34d0e 100644 --- a/14th-team5-iOS/Core/Sources/Base/BaseService.swift +++ b/14th-team5-iOS/Core/Sources/Base/BaseService.swift @@ -8,9 +8,9 @@ import ReactorKit public class BaseService { - unowned let provider: GlobalStateProviderProtocol + unowned let provider: ServiceProviderProtocol - init(provider: GlobalStateProviderProtocol) { + init(provider: ServiceProviderProtocol) { self.provider = provider } } diff --git a/14th-team5-iOS/Core/Sources/Base/BaseTableView.swift b/14th-team5-iOS/Core/Sources/Base/BaseTableView.swift new file mode 100644 index 000000000..2300d853a --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Base/BaseTableView.swift @@ -0,0 +1,51 @@ +// +// BaseTableView.swift +// Core +// +// Created by 김건우 on 9/10/24. +// + +import UIKit + +import ReactorKit +import RxSwift + +open class BaseTableView: UITableView, ReactorKit.View where R: Reactor { + public typealias Reactor = R + + // MARK: - Properties + + public var disposeBag: RxSwift.DisposeBag = DisposeBag() + + // MARK: - Intializer + + public convenience init(reactor: Reactor? = nil) { + self.init(frame: .zero) + self.reactor = reactor + } + + public override init(frame: CGRect, style: UITableView.Style) { + super.init(frame: .zero, style: style) + setupUI() + setupAutoLayout() + setupAttributes() + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + // MARK: - Helpers + + // 리액터와 바인딩을 위한 메서드 + open func bind(reactor: R) { } + + // 서브 뷰 추가를 위한 메서드 + open func setupUI() { } + + // 오토레이아웃 설정을 위한 메서드 + open func setupAutoLayout() { } + + // 뷰의 속성 설정을 위한 메서드 + open func setupAttributes() { } +} diff --git a/14th-team5-iOS/Core/Sources/Base/ReactorViewController.swift b/14th-team5-iOS/Core/Sources/Base/ReactorViewController.swift index e4e106dbc..15f8f0135 100644 --- a/14th-team5-iOS/Core/Sources/Base/ReactorViewController.swift +++ b/14th-team5-iOS/Core/Sources/Base/ReactorViewController.swift @@ -14,13 +14,17 @@ import RxSwift open class ReactorViewController: UIViewController, ReactorKit.View where R: Reactor { // MARK: - Typealias + public typealias Reactor = R // MARK: - Properties + private var initialReactor: Reactor? + public var disposeBag: RxSwift.DisposeBag = DisposeBag() // MARK: - Intializer + public init() { super.init(nibName: nil, bundle: nil) } @@ -35,6 +39,7 @@ open class ReactorViewController: UIViewController, ReactorKit.View where R: } // MARK: - Lifecycles + override open func viewDidLoad() { super.viewDidLoad() setupUI() @@ -44,6 +49,7 @@ open class ReactorViewController: UIViewController, ReactorKit.View where R: } // MARK: - Helpers + open func bind(reactor: R) { } open func setupUI() { } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/Activity/CopyInvitationUrlActivity.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/Activity/CopyInvitationUrlActivity.swift index eda4181dd..43e83064b 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/Activity/CopyInvitationUrlActivity.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/Activity/CopyInvitationUrlActivity.swift @@ -14,14 +14,14 @@ public class CopyInvitationUrlActivity: UIActivity { } let url: URL - let provider: GlobalStateProviderProtocol? + + @Injected var provider: ServiceProviderProtocol let bundleId: String = Bundle.main.bundleIdentifier! var typeName: String = String(describing: CopyInvitationUrlActivity.self) - public init(_ url: URL, provider: GlobalStateProviderProtocol?) { + public init(_ url: URL) { self.url = url - self.provider = provider } public override class var activityCategory: UIActivity.Category { @@ -46,7 +46,7 @@ public class CopyInvitationUrlActivity: UIActivity { public override func perform() { UIPasteboard.general.string = url.description - provider?.activityGlobalState.didTapCopyInvitationUrlAction() + provider.managementService.didTapCopUrlAction() } } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ActivityGlobalState.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ActivityGlobalState.swift deleted file mode 100644 index eee0cfdf1..000000000 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ActivityGlobalState.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// ActivityGlobalState.swift -// Core -// -// Created by 김건우 on 12/13/23. -// - -import UIKit - -import RxSwift - -public enum ActivityEvent { - case didTapCopyInvitationUrlAction - case hiddenInvitationUrlIndicatorView(Bool) -} - -public protocol ActivityGlobalStateType { - var event: PublishSubject { get } - func didTapCopyInvitationUrlAction() -> Observable - func hiddenInvitationUrlIndicatorView(_ hidden: Bool) -> Observable -} - -final public class ActivityGlobalState: BaseService, ActivityGlobalStateType { - public var event: PublishSubject = PublishSubject() - - public func didTapCopyInvitationUrlAction() -> Observable { - event.onNext(.didTapCopyInvitationUrlAction) - return Observable.just(()) - } - - public func hiddenInvitationUrlIndicatorView(_ hidden: Bool) -> Observable { - event.onNext(.hiddenInvitationUrlIndicatorView(hidden)) - return Observable.just(hidden) - } -} - diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ManagementService.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ManagementService.swift new file mode 100644 index 000000000..9d4d33ace --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ManagementService.swift @@ -0,0 +1,73 @@ +// +// ActivityGlobalState.swift +// Core +// +// Created by 김건우 on 12/13/23. +// + +import UIKit + +import RxSwift + +public enum ManagementEvent { + case didTapCopyUrlAction + case hiddenSharingProgressHud(hidden: Bool) + case hiddenTableProgressHud(hidden: Bool) + case hiddenMemberFetchFailureView(hidden: Bool) + case setTableHeaderInfo(familyName: String, memberCount: Int) + case endTableRefreshing +} + +public protocol ManagementServiceType { + var event: PublishSubject { get } + + @discardableResult + func didTapCopUrlAction() -> Observable + @discardableResult + func hiddenSharingProgressHud(hidden: Bool) -> Observable + @discardableResult + func hiddenTableProgressHud(hidden: Bool) -> Observable + @discardableResult + func hiddenMemberFetchFailureView(hidden: Bool) -> Observable + @discardableResult + func setTableHeaderInfo(familyName: String, memberCount: Int) -> Observable<(String, Int)> + @discardableResult + func endTableRefreshing() -> Observable +} + +final public class ManagementService: BaseService, ManagementServiceType { + + public var event: PublishSubject = PublishSubject() + + public func didTapCopUrlAction() -> Observable { + event.onNext(.didTapCopyUrlAction) + return Observable.just(()) + } + + public func hiddenSharingProgressHud(hidden: Bool) -> Observable { + event.onNext(.hiddenSharingProgressHud(hidden: hidden)) + return Observable.just(hidden) + } + + public func hiddenTableProgressHud(hidden: Bool) -> Observable { + event.onNext(.hiddenTableProgressHud(hidden: hidden)) + return Observable.just(hidden) + } + + public func hiddenMemberFetchFailureView(hidden: Bool) -> Observable { + event.onNext(.hiddenMemberFetchFailureView(hidden: hidden)) + return Observable.just(hidden) + } + + public func setTableHeaderInfo(familyName: String, memberCount: Int) -> Observable<(String, Int)> { + event.onNext(.setTableHeaderInfo(familyName: familyName, memberCount: memberCount)) + return Observable<(String, Int)>.just((familyName, memberCount)) + } + + public func endTableRefreshing() -> Observable { + event.onNext(.endTableRefreshing) + return Observable.just(()) + } + +} + diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBServices/GlobalStateProvider.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ServiceProvider.swift similarity index 82% rename from 14th-team5-iOS/Core/Sources/Bibbi/BBServices/GlobalStateProvider.swift rename to 14th-team5-iOS/Core/Sources/Bibbi/BBServices/ServiceProvider.swift index 4cd4e9a76..7616bb9a9 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBServices/GlobalStateProvider.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ServiceProvider.swift @@ -7,9 +7,10 @@ import Foundation -public protocol GlobalStateProviderProtocol: AnyObject { +public protocol ServiceProviderProtocol: AnyObject { + var managementService: ManagementServiceType { get } + var postGlobalState: PostGlobalStateType { get } - var activityGlobalState: ActivityGlobalStateType { get } var calendarGlabalState: CalendarGlobalStateType { get } var toastGlobalState: ToastMessageGlobalStateType { get } var profileGlobalState: ProfileGlobalStateType { get } @@ -19,9 +20,12 @@ public protocol GlobalStateProviderProtocol: AnyObject { var mainService: MainServiceType { get } } -final public class GlobalStateProvider: GlobalStateProviderProtocol { +final public class ServiceProvider: ServiceProviderProtocol { + + public lazy var managementService: any ManagementServiceType = ManagementService(provider: self) + + public lazy var postGlobalState: PostGlobalStateType = PostGlobalState(provider: self) - public lazy var activityGlobalState: ActivityGlobalStateType = ActivityGlobalState(provider: self) public lazy var calendarGlabalState: CalendarGlobalStateType = CalendarGlobalState(provider: self) public lazy var toastGlobalState: ToastMessageGlobalStateType = ToastMessageGlobalState(provider: self) public lazy var profileGlobalState: ProfileGlobalStateType = ProfileGlobalState(provider: self) diff --git a/14th-team5-iOS/Core/Sources/Extensions/Reactive+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/Reactive+Ext.swift index 742c10137..873c63e19 100644 --- a/14th-team5-iOS/Core/Sources/Extensions/Reactive+Ext.swift +++ b/14th-team5-iOS/Core/Sources/Extensions/Reactive+Ext.swift @@ -51,22 +51,6 @@ extension Reactive where Base: UIView { } } -extension Reactive where Base: UIScrollView { - public func reachedBottom(from space: CGFloat = 200.0) -> ControlEvent { - let source = contentOffset.map { contentOffset in - let visibleHeight = self.base.frame.height - self.base.contentInset.top - self.base.contentInset.bottom - let y = contentOffset.y + self.base.contentInset.top - let threshold = self.base.contentSize.height - visibleHeight - space - return y >= threshold - } - .distinctUntilChanged() - .filter { $0 } - .map { _ in () } - - return ControlEvent(events: source) - } -} - extension Reactive where Base: UITapGestureRecognizer { public var tapGesture: ControlEvent { let tapEvent = self.methodInvoked(#selector(Base.touchesBegan(_:with:))).map { _ in } @@ -75,12 +59,16 @@ extension Reactive where Base: UITapGestureRecognizer { } extension Reactive where Base: UILabel { - public var isMeText: Binder { - Binder(self.base) { label, isMe in - label.text = isMe ? "ME" : "" + + public var firstLetterText: Binder { + Binder(self.base) { label, text in + if let firstLetter = text.first { + label.text = String(firstLetter) + } } } + @available(*, deprecated, message: "삭제") public var calendarTitleText: Binder { Binder(self.base) { label, date in var formatString: String = .none @@ -93,12 +81,7 @@ extension Reactive where Base: UILabel { } } - public var firtNameText: Binder { - Binder(self.base) { label, text in - label.text = text[0] - } - } - + @available(*, deprecated, message: "삭제") public var memoryCountText: Binder { Binder(self.base) { label, count in label.text = "\(count)개의 추억" @@ -106,14 +89,6 @@ extension Reactive where Base: UILabel { } } -extension Reactive where Base: UIStackView { - public var isMeSpacing: Binder { - Binder(self.base) { stackView, isMe in - stackView.spacing = isMe ? 3.0 : 0.0 - } - } -} - extension Reactive where Base: WKWebView { public var loadURL: Binder { return Binder(self.base) { webView, url in diff --git a/14th-team5-iOS/Core/Sources/Extensions/UIViewController+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/UIViewController+Ext.swift index b93c13883..b09eef584 100644 --- a/14th-team5-iOS/Core/Sources/Extensions/UIViewController+Ext.swift +++ b/14th-team5-iOS/Core/Sources/Extensions/UIViewController+Ext.swift @@ -148,6 +148,8 @@ extension UIViewController { } extension UIViewController { + + @available(*, deprecated, message: "Navigator 안에서 호출하세요.") public func makeSharePanel( _ activityItemSources: [UIActivityItemSource], activities: [UIActivity], @@ -166,13 +168,15 @@ extension UIViewController { /// - Parameters: /// - url: 공유할 URL /// - globalState: GlobalState (선택) - public func makeInvitationUrlSharePanel(_ url: URL?, provider globalState: GlobalStateProviderProtocol? = nil) { + /// + @available(*, deprecated, message: "Navigator 안에서 호출하세요.") + public func makeInvitationUrlSharePanel(_ url: URL?, provider globalState: ServiceProviderProtocol? = nil) { guard let url = url else { return } let itemSource = UrlActivityItemSource( title: "삐삐! 가족에게 보내는 하루 한 번 생존 신고", url: url ) - let copyToPastboard = CopyInvitationUrlActivity(url, provider: globalState) + let copyToPastboard = CopyInvitationUrlActivity(url) makeSharePanel([itemSource], activities: [copyToPastboard]) } 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 32edbb73f..6fb7dc26b 100644 --- a/14th-team5-iOS/Data/Sources/Storages/UserDefaults/AppUserDefaults/AppUserDefaults.swift +++ b/14th-team5-iOS/Data/Sources/Storages/UserDefaults/AppUserDefaults/AppUserDefaults.swift @@ -27,7 +27,6 @@ public protocol AppUserDefaultsType: UserDefaultsType { func saveInviteCode(_ inviteCode: String) func loadInviteCode() -> String? - func deleteInvitedCode() } final public class AppUserDefaults: AppUserDefaultsType { @@ -92,11 +91,6 @@ final public class AppUserDefaults: AppUserDefaultsType { return inviteCode } - - public func deleteInvitedCode() { - remove(forKey: .inviteCode) - } - // MARK: - Onboarding @@ -124,8 +118,5 @@ final public class AppUserDefaults: AppUserDefaultsType { return isFirstFamilyManagement } - - - } From a3f25d33a5553445dacf2b78121cd460b1c39453 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Sun, 15 Sep 2024 12:02:12 +0900 Subject: [PATCH 206/263] =?UTF-8?q?refactor:=20CommentViewController=20?= =?UTF-8?q?=EB=B0=8F=20=EB=B6=80=EA=B0=80=20=EC=BD=94=EB=93=9C=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A7=81=20(#628)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: CommentTable, CommentTextFieldView 구현 및 부가 코드 리팩토링 (#627) * refactor: CommentViewController 코드 수정 (#627) * fix: 댓글삭제Toast위치 수정 (#627) * refactor: CommentViewReactor 코드 수정 (#627) * delete: CommentService 코드 삭제 (#627) * refactor: ManagementViewController 및 부가 코드 수정 (#627) * move: UrlSharingActivity 관련 코드 이동 (#627) --- .../DIContainer/NavigatorDIContainer.swift | 6 + .../Navigator/CommentNavigator.swift | 33 ++ .../Application/Navigator/HomeNavigator.swift | 50 +++ .../CommentViewControllerWrapper.swift | 8 +- .../DataSource/CommentSectionModel.swift | 0 .../Delegate/CommentTextFieldDelegate.swift | 13 + .../RxCommentTextFieldDelegateProxy.swift | 52 +++ .../Reactor/Cell/CommentCellReactor.swift | 127 ++++++ .../Comment/Reactor/CommentTableReactor.swift | 43 ++ .../Reactor/CommentTextFieldReactor.swift | 83 ++++ .../Comment/Reactor/CommentViewReactor.swift | 284 +++++++++++++ .../View/Cell/CommentCell.swift | 89 ++-- .../Comment/View/CommentTableView.swift | 126 ++++++ .../Comment/View/CommentTextFieldView.swift | 159 ++++++++ .../View/CommentTopBarView.swift} | 24 +- .../View/NoneCommentView.swift} | 25 +- .../CommentViewController.swift | 240 +++++++++++ .../Reactor/InputFamilyLinkReactor.swift | 4 +- .../Home/Reactor/MainFamilyViewReactor.swift | 30 +- .../MainFamilyViewController.swift | 20 +- .../Activity/CopyInvitationUrlActivity.swift | 5 +- .../Activity/UrlActivityItemSource.swift | 2 + .../Reactor/ManagementReactor.swift | 79 ++-- .../ManagementTableHeaderReactor.swift | 58 +-- .../Reactor/ManagementTableReactor.swift | 56 +-- .../Reactor/SharingContainerReactor.swift | 38 +- .../View/ManagementTableHeaderView.swift | 32 +- .../Management/View/ManagementTableView.swift | 38 +- .../View/SharingContainerView.swift | 51 +-- .../ManagementViewController.swift | 31 +- .../Reactor/CommentCellReactor.swift | 117 ------ .../Reactor/CommentViewReactor.swift | 344 ---------------- .../Strings/PostCommentStrings.swift | 18 - .../PostCommentViewController.swift | 384 ------------------ .../PostCommentDIContainer.swift | 4 +- .../Core/Sources/Base/BaseTableView.swift | 12 +- .../BBToast/BBToastConfiguration.swift | 10 +- .../DefaultToastView/DefaultToastView.swift | 1 + .../Bibbi/BBServices/ManagementService.swift | 40 -- .../Bibbi/BBServices/ServiceProvider.swift | 5 +- .../Extensions/UIViewController+Ext.swift | 37 +- .../Family/FamilyAPI/FamilyAPIWorker.swift | 2 +- 42 files changed, 1478 insertions(+), 1302 deletions(-) create mode 100644 14th-team5-iOS/App/Sources/Application/Navigator/HomeNavigator.swift rename 14th-team5-iOS/App/Sources/Presentation/{PostComment => Comment/Reactive}/DataSource/CommentSectionModel.swift (100%) create mode 100644 14th-team5-iOS/App/Sources/Presentation/Comment/Reactive/DelegateProxy/Delegate/CommentTextFieldDelegate.swift create mode 100644 14th-team5-iOS/App/Sources/Presentation/Comment/Reactive/DelegateProxy/RxCommentTextFieldDelegateProxy.swift create mode 100644 14th-team5-iOS/App/Sources/Presentation/Comment/Reactor/Cell/CommentCellReactor.swift create mode 100644 14th-team5-iOS/App/Sources/Presentation/Comment/Reactor/CommentTableReactor.swift create mode 100644 14th-team5-iOS/App/Sources/Presentation/Comment/Reactor/CommentTextFieldReactor.swift create mode 100644 14th-team5-iOS/App/Sources/Presentation/Comment/Reactor/CommentViewReactor.swift rename 14th-team5-iOS/App/Sources/Presentation/{PostComment => Comment}/View/Cell/CommentCell.swift (60%) create mode 100644 14th-team5-iOS/App/Sources/Presentation/Comment/View/CommentTableView.swift create mode 100644 14th-team5-iOS/App/Sources/Presentation/Comment/View/CommentTextFieldView.swift rename 14th-team5-iOS/App/Sources/Presentation/{PostComment/View/PostCommentTopBarView.swift => Comment/View/CommentTopBarView.swift} (79%) rename 14th-team5-iOS/App/Sources/Presentation/{PostComment/View/NoCommentLabel.swift => Comment/View/NoneCommentView.swift} (67%) create mode 100644 14th-team5-iOS/App/Sources/Presentation/Comment/ViewController/CommentViewController.swift rename 14th-team5-iOS/{Core/Sources/Bibbi/BBCommons => App/Sources/Presentation/Management}/Activity/CopyInvitationUrlActivity.swift (90%) rename 14th-team5-iOS/{Core/Sources/Bibbi/BBCommons => App/Sources/Presentation/Management}/Activity/UrlActivityItemSource.swift (97%) delete mode 100644 14th-team5-iOS/App/Sources/Presentation/PostComment/Reactor/CommentCellReactor.swift delete mode 100644 14th-team5-iOS/App/Sources/Presentation/PostComment/Reactor/CommentViewReactor.swift delete mode 100644 14th-team5-iOS/App/Sources/Presentation/PostComment/Strings/PostCommentStrings.swift delete mode 100644 14th-team5-iOS/App/Sources/Presentation/PostComment/ViewController/PostCommentViewController.swift rename 14th-team5-iOS/App/Sources/Presentation/{PostComment/Dependency => Trash/Dependencygg}/PostCommentDIContainer.swift (91%) diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/NavigatorDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/NavigatorDIContainer.swift index d659c926a..c3b18560d 100644 --- a/14th-team5-iOS/App/Sources/Application/DIContainer/NavigatorDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/NavigatorDIContainer.swift @@ -37,6 +37,12 @@ final class NavigatorDIContainer: BaseContainer { ) } + container.register(type: HomeNavigatorProtocol.self) { _ in + HomeNavigator( + navigationController: makeUINavigationController() + ) + } + container.register(type: MonthlyCalendarNavigatorProtocol.self) { _ in MonthlyCalendarNavigator( navigationController: makeUINavigationController() diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/CommentNavigator.swift b/14th-team5-iOS/App/Sources/Application/Navigator/CommentNavigator.swift index 26e599fcb..5683bd977 100644 --- a/14th-team5-iOS/App/Sources/Application/Navigator/CommentNavigator.swift +++ b/14th-team5-iOS/App/Sources/Application/Navigator/CommentNavigator.swift @@ -6,11 +6,16 @@ // import Core +import DesignSystem import UIKit protocol CommentNavigatorProtocol: BaseNavigator { func toProfile(memberId: String) func dismiss(completion: (() -> Void)?) + + func showErrorToast() + func showCommentDeleteToast() + func showFetchFailureToast() } final class CommentNavigator: CommentNavigatorProtocol { @@ -40,4 +45,32 @@ final class CommentNavigator: CommentNavigatorProtocol { } } + // MARK: - Show + + func showErrorToast() { + BBToast.style(.error).show() + } + + func showCommentDeleteToast() { + let config = BBToastConfiguration(direction: .top(yOffset: 75)) + let viewConfig = BBToastViewConfiguration(minWidth: 100) + BBToast.default( + image: DesignSystemAsset.warning.image, + title: "댓글이 삭제되었습니다", + viewConfig: viewConfig, + config: config + ).show() + } + + func showFetchFailureToast() { + let config = BBToastConfiguration(direction: .top(yOffset: 75)) + let viewConfig = BBToastViewConfiguration(minWidth: 100) + BBToast.default( + image: DesignSystemAsset.warning.image, + title: "댓글을 불러오는데 실패했어요", + viewConfig: viewConfig, + config: config + ).show() + } + } diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/HomeNavigator.swift b/14th-team5-iOS/App/Sources/Application/Navigator/HomeNavigator.swift new file mode 100644 index 000000000..1eb2d7dbb --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/Navigator/HomeNavigator.swift @@ -0,0 +1,50 @@ +// +// HomeNavigator.swift +// App +// +// Created by 김건우 on 9/14/24. +// + +import Core +import DesignSystem +import UIKit + +protocol HomeNavigatorProtocol: BaseNavigator { + func presentSharingSheet(url: URL?) +} + +final class HomeNavigator: HomeNavigatorProtocol { + + // MARK: - Properties + + var navigationController: UINavigationController + + // MARK: - Intializer + + init(navigationController: UINavigationController) { + self.navigationController = navigationController + } + + // MARK: - Present + + func presentSharingSheet(url: URL?) { + // TODO: - 리팩토링하기 + guard let url else { return } + + let itemSource = UrlActivityItemSource( + title: "삐삐! 가족에게 보내는 하루 한 번 생존 신고", + url: url + ) + let copyToPastboard = CopyInvitationUrlActivity(url) + + let items: [Any] = [itemSource] + let activityVC = UIActivityViewController( + activityItems: items, + applicationActivities: [copyToPastboard] + ) + activityVC.excludedActivityTypes = [.addToReadingList, .copyToPasteboard] + navigationController.present(activityVC, animated: true) + } + +} + diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Comment/CommentViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Comment/CommentViewControllerWrapper.swift index d145a51cf..8cea0349b 100644 --- a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Comment/CommentViewControllerWrapper.swift +++ b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Comment/CommentViewControllerWrapper.swift @@ -13,7 +13,7 @@ final class CommentViewControllerWrapper: BaseWrapper { // MARK: - Typealias typealias R = CommentViewReactor - typealias V = PostCommentViewController + typealias V = CommentViewController // MARK: - Properties @@ -23,7 +23,7 @@ final class CommentViewControllerWrapper: BaseWrapper { makeReactor() } - var viewController: PostCommentViewController { + var viewController: CommentViewController { makeViewController() } @@ -39,8 +39,8 @@ final class CommentViewControllerWrapper: BaseWrapper { CommentViewReactor(postId: postId) } - func makeViewController() -> PostCommentViewController { - PostCommentViewController(reactor: makeReactor()) + func makeViewController() -> CommentViewController { + CommentViewController(reactor: makeReactor()) } } diff --git a/14th-team5-iOS/App/Sources/Presentation/PostComment/DataSource/CommentSectionModel.swift b/14th-team5-iOS/App/Sources/Presentation/Comment/Reactive/DataSource/CommentSectionModel.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/PostComment/DataSource/CommentSectionModel.swift rename to 14th-team5-iOS/App/Sources/Presentation/Comment/Reactive/DataSource/CommentSectionModel.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Comment/Reactive/DelegateProxy/Delegate/CommentTextFieldDelegate.swift b/14th-team5-iOS/App/Sources/Presentation/Comment/Reactive/DelegateProxy/Delegate/CommentTextFieldDelegate.swift new file mode 100644 index 000000000..973c7637b --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Comment/Reactive/DelegateProxy/Delegate/CommentTextFieldDelegate.swift @@ -0,0 +1,13 @@ +// +// CommentTextFieldDelegate.swift +// App +// +// Created by 김건우 on 9/12/24. +// + +import UIKit + +@objc public protocol CommentTextFieldDelegate: AnyObject { + @objc optional func didTapConfirmButton(_ button: UIButton, text: String?, event: UITextField.Event) + @objc optional func didTapDoneButton(text: String?) +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Comment/Reactive/DelegateProxy/RxCommentTextFieldDelegateProxy.swift b/14th-team5-iOS/App/Sources/Presentation/Comment/Reactive/DelegateProxy/RxCommentTextFieldDelegateProxy.swift new file mode 100644 index 000000000..cc5d06ece --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Comment/Reactive/DelegateProxy/RxCommentTextFieldDelegateProxy.swift @@ -0,0 +1,52 @@ +// +// CommentTextFieldDelegateProxy.swift +// App +// +// Created by 김건우 on 9/12/24. +// + +import UIKit + +import RxCocoa +import RxSwift + +public class RxCommentTextFieldDelegateProxy: DelegateProxy, DelegateProxyType, CommentTextFieldDelegate { + + static public func registerKnownImplementations() { + self.register { + RxCommentTextFieldDelegateProxy( + parentObject: $0, + delegateProxy: self + ) + } + } + +} + +extension CommentTextFieldView: HasDelegate { + + public typealias Delegate = CommentTextFieldDelegate + +} + +extension Reactive where Base: CommentTextFieldView { + + public var delegate: DelegateProxy { + RxCommentTextFieldDelegateProxy.proxy(for: self.base) + } + + public var didTapConfirmButton: Observable { + let source = delegate.sentMessage(#selector(CommentTextFieldDelegate.didTapConfirmButton(_:text:event:))) + .map { $0[1] as! String } + + return source + } + + public var didTapDoneButton: Observable { + let source = delegate.sentMessage(#selector(CommentTextFieldDelegate.didTapDoneButton(text:))) + .map { $0[0] as! String } + + return source + } + +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Comment/Reactor/Cell/CommentCellReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Comment/Reactor/Cell/CommentCellReactor.swift new file mode 100644 index 000000000..b8a39ea38 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Comment/Reactor/Cell/CommentCellReactor.swift @@ -0,0 +1,127 @@ +// +// CommentCellReactor.swift +// App +// +// Created by 김건우 on 1/18/24. +// + +import Core +import Domain +import Foundation + +import Differentiator +import ReactorKit +import RxSwift + +final public class CommentCellReactor: Reactor { + + // MARK: - Action + + public enum Action { + case fetchUserName + case fetchProfileImage + case didTapProfileButton + } + + + // MARK: - Mutation + + public enum Mutation { + case setMemberName(String) + case setProfileImageUrl(String?) + } + + + // MARK: - State + + public struct State { + let comment: PostCommentEntity + + var memberName: String? = nil + var profileImageUrl: String? = nil + } + + + // MARK: - Properties + + public var initialState: State + + @Injected var fetchUserNameUseCase: FetchUserNameUseCaseProtocol + @Injected var fetchProfileImageUrlUseCase: FetchProfileImageUrlUseCaseProtocol + @Injected var checkIsVaildMemberUseCase: CheckIsVaildMemberUseCaseProtocol + + @Injected var provider: ServiceProviderProtocol + + @Navigator var navigator: CommentNavigatorProtocol + + + // MARK: - Intializer + + public init(_ comment: PostCommentEntity) { + self.initialState = State(comment: comment) + } + + + // MARK: - Mutate + + public func mutate(action: Action) -> Observable { + let memberId = initialState.comment.memberId + + switch action { + case .fetchUserName: + let memberName = fetchUserNameUseCase.execute(memberId: memberId) ?? "알 수 없음" + return Observable.just(.setMemberName(memberName)) + + case .fetchProfileImage: + let url = fetchProfileImageUrlUseCase.execute(memberId: memberId) + return Observable.just(.setProfileImageUrl(url)) + + case .didTapProfileButton: + let isValid = checkIsVaildMemberUseCase.execute(memberId: memberId) + if isValid { + navigator.dismiss { [weak self] in + self?.navigator.toProfile(memberId: memberId) + } +// provider.postGlobalState.pushProfileViewController(memberId) + } + return Observable.empty() + } + } + + + // MARK: - Reduce + + public func reduce(state: State, mutation: Mutation) -> State { + var newState = state + switch mutation { + case let .setMemberName(memberName): + newState.memberName = memberName + + case let .setProfileImageUrl(url): + newState.profileImageUrl = url + } + + return newState + } + +} + + +// MARK: - Extensions + +extension CommentCellReactor: IdentifiableType, Equatable { + + // MARK: - IdentifiableType + + public typealias Identity = String + public var identity: Identity { + return initialState.comment.commentId + } + + + // MARK: - Equatable + + public static func == (lhs: CommentCellReactor, rhs: CommentCellReactor) -> Bool { + return lhs.initialState.comment.commentId == rhs.initialState.comment.commentId + } +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Comment/Reactor/CommentTableReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Comment/Reactor/CommentTableReactor.swift new file mode 100644 index 000000000..71767ef3b --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Comment/Reactor/CommentTableReactor.swift @@ -0,0 +1,43 @@ +// +// CommentTableReactor.swift +// App +// +// Created by 김건우 on 9/12/24. +// + +import Core +import MacrosInterface + +import ReactorKit + +@Reactor +public final class CommentTableReactor { + + // MARK: - Action + + public enum Action { } + + + // MARK: - Mutation + + public enum Mutation { } + + + // MARK: - State + + public struct State { } + + + // MARK: - Properties + + public let initialState: State + + + // MARK: - Intializer + + public init() { + self.initialState = State() + } + +} + diff --git a/14th-team5-iOS/App/Sources/Presentation/Comment/Reactor/CommentTextFieldReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Comment/Reactor/CommentTextFieldReactor.swift new file mode 100644 index 000000000..e45579c25 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Comment/Reactor/CommentTextFieldReactor.swift @@ -0,0 +1,83 @@ +// +// CommentTextFieldReactor.swift +// App +// +// Created by 김건우 on 9/12/24. +// + +import Core +import Domain +import Foundation + +import ReactorKit +import MacrosInterface + +@Reactor +public final class CommentTextFieldReactor { + + // MARK: - Action + + public enum Action { + case inputText(String) + } + + + // MARK: - Mutation + + public enum Mutation { + case setEnableConfirmButton(Bool) + case setEnableTextField(Bool) + } + + + // MARK: - State + + public struct State { + @Pulse var inputText: String? = nil + var enableTextField: Bool = true + var enableConfirmButton: Bool = false + } + + + // MARK: - Properties + + public let initialState: State + + @Injected var provider: ServiceProviderProtocol + + // MARK: - Intializer + + public init() { + self.initialState = State() + } + + + // MARK: - Mutate + + public func mutate(action: Action) -> Observable { + switch action { + case let .inputText(text): + // TODO: - 댓글 내용 임시 저장 코드 구현하기 + let enable = text.count == 0 ? false : true + return Observable.just(.setEnableConfirmButton(enable)) + } + } + + + // MARK: - Reduce + + public func reduce(state: State, mutation: Mutation) -> State { + var newState = state + + switch mutation { + case let .setEnableConfirmButton(enable): + newState.enableConfirmButton = enable + + case let .setEnableTextField(enable): + newState.enableTextField = enable + } + + return newState + } + +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Comment/Reactor/CommentViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Comment/Reactor/CommentViewReactor.swift new file mode 100644 index 000000000..ffec93d78 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Comment/Reactor/CommentViewReactor.swift @@ -0,0 +1,284 @@ +// +// PostCommentReactor.swift +// App +// +// Created by 김건우 on 1/18/24. +// + +import Core +import Domain +import Foundation + +import ReactorKit +import RxSwift + +final public class CommentViewReactor: Reactor { + + // MARK: - Action + + public enum Action { + case fetchComment + case createComment(String) + case deleteComment(String) + } + + + // MARK: - Mutation + + public enum Mutation { + case setComments([CommentCellReactor]) + case appendComment(CommentCellReactor) + case deleteComment(String) + + case setHiddenTablePrgressHud(Bool) + case setHiddenFetchFailureView(Bool) + case setHiddenNoneCommentView(Bool) + + case setEnableConfirmButton(Bool) + case setEnableCommentTextField(Bool) + + case setText(String?) + case setBecomeFirstResponder(Bool) + + case scrollTableToLast(Bool) + } + + + // MARK: - State + + public struct State { + @Pulse var commentDatasource: [CommentSectionModel] = [.init(model: "", items: [])] + + var hiddenTableProgressHud: Bool = false + var hiddenFetchFailureView: Bool = true + var hiddenNoneCommentView: Bool = true + + var enableConfirmButton: Bool = false + var enableCommentTextField: Bool = false + + @Pulse var text: String? = nil + @Pulse var makeTextFieldFirstResponder: Bool = false + + @Pulse var scrollTableToLast: Bool = false + } + + + // MARK: - Properties + + public var initialState: State + + @Injected var fetchCommentUseCase: FetchCommentUseCaseProtocol + @Injected var createCommentUseCase: CreateCommentUseCaseProtocol + @Injected var deleteCommentUseCase: DeleteCommentUseCaseProtocol + @Injected var provider: ServiceProviderProtocol + + @Navigator var navigator: CommentNavigatorProtocol + + private let postId: String + + private var commentCount: Int? { + if let count = currentState.commentDatasource.first?.items.count { + return count + } + return nil + } + + private var commentDatasource: CommentSectionModel? { + if let dataSource = currentState.commentDatasource.first { + return dataSource + } + return nil + } + + // MARK: - Intializer + + public init(postId: String) { + self.postId = postId + self.initialState = State() + } + + + // MARK: - Mutate + public func mutate(action: Action) -> Observable { +// let commentService = provider.commentService + + switch action { + case .fetchComment: + let query = PostCommentPaginationQuery() + + return Observable.concat( + Observable.just(.setEnableConfirmButton(false)), + Observable.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 { + return Observable.concat( + Observable.just(.setComments([])), + Observable.just(.setBecomeFirstResponder(true)), + Observable.just(.setHiddenTablePrgressHud(true)), + Observable.just(.setHiddenNoneCommentView(false)), + Observable.just(.setHiddenFetchFailureView(true)), + Observable.just(.setEnableConfirmButton(true)), + Observable.just(.setEnableCommentTextField(true)) + ) + } + + let cells = comments.results + .map { CommentCellReactor($0) } + + return Observable.concat( + Observable.just(.setComments(cells)), + Observable.just(.setBecomeFirstResponder(true)), + Observable.just(.setHiddenTablePrgressHud(true)), + Observable.just(.setHiddenNoneCommentView(true)), + Observable.just(.setHiddenFetchFailureView(true)), + Observable.just(.setEnableConfirmButton(true)), + Observable.just(.setEnableCommentTextField(true)), + Observable.just(.scrollTableToLast(true)) + ) + } + ) + + case let .createComment(content): + guard + !content.trimmingCharacters(in: .whitespaces).isEmpty + else { + return Observable.just(.setText(nil)) + } + + let body = CreatePostCommentRequest(content: content) + + Haptic.impact(style: .rigid) + + return Observable.concat( + Observable.just(.setEnableConfirmButton(false)), + Observable.just(.setEnableCommentTextField(false)), + + createCommentUseCase.execute(postId: postId, body: body) + .withUnretained(self) + .concatMap { + guard let comment = $0.1 else { + Haptic.notification(type: .error) + $0.0.navigator.showErrorToast() + return Observable.concat( + Observable.just(.setEnableConfirmButton(true)), + Observable.just(.setEnableCommentTextField(true)) + ) + } + + let reactor = CommentCellReactor(comment) + + // TODO: - Provider 바꾸기 + if let count = $0.0.commentCount { + $0.0.provider.postGlobalState.renewalPostCommentCount(count + 1) + } + + return Observable.concat( + Observable.just(.appendComment(reactor)), + Observable.just(.setEnableConfirmButton(true)), + Observable.just(.setEnableCommentTextField(true)), + Observable.just(.scrollTableToLast(true)), + Observable.just(.setText(nil)) + ) + } + ) + + case let .deleteComment(commentId): + return deleteCommentUseCase.execute(postId: postId, commentId: commentId) + .withUnretained(self) + .flatMap { + guard + let delete = $0.1, delete.success + else { + Haptic.notification(type: .error) + $0.0.navigator.showErrorToast() + return Observable.empty() + } + + // TODO: - Provider 바꾸기 + if let count = $0.0.commentCount { + $0.0.provider.postGlobalState.renewalPostCommentCount(count - 1) + } + + $0.0.navigator.showCommentDeleteToast() + return Observable.just(.deleteComment(commentId)) + } + } + } + + // MARK: - Reduce + public func reduce(state: State, mutation: Mutation) -> State { + var newState = state + + switch mutation { + case let .setComments(comments): + let dataSource: CommentSectionModel = .init(model: "", items: comments) + newState.commentDatasource = [dataSource] + + case let .appendComment(comment): + guard + var dataSource = commentDatasource + else { break } + dataSource.items.append(comment) + newState.commentDatasource = [dataSource] + + case let .deleteComment(commentId): + guard + var dataSource = commentDatasource + else { break } + dataSource.items.removeAll { $0.currentState.comment.commentId == commentId } + newState.commentDatasource = [dataSource] + + case let .setHiddenTablePrgressHud(hidden): + newState.hiddenTableProgressHud = hidden + + case let .setHiddenNoneCommentView(hidden): + newState.hiddenNoneCommentView = hidden + + case let .setHiddenFetchFailureView(hidden): + newState.hiddenFetchFailureView = hidden + + case let .setEnableConfirmButton(enable): + newState.enableConfirmButton = enable + + case let .setEnableCommentTextField(enable): + newState.enableCommentTextField = enable + + case let .setText(text): + newState.text = text + + case let .setBecomeFirstResponder(responder): + newState.makeTextFieldFirstResponder = responder + + case let .scrollTableToLast(scroll): + newState.scrollTableToLast = scroll + } + + return newState + } + +} + + +// MARK: - Extensions + +extension CommentViewReactor { + + + +} diff --git a/14th-team5-iOS/App/Sources/Presentation/PostComment/View/Cell/CommentCell.swift b/14th-team5-iOS/App/Sources/Presentation/Comment/View/Cell/CommentCell.swift similarity index 60% rename from 14th-team5-iOS/App/Sources/Presentation/PostComment/View/Cell/CommentCell.swift rename to 14th-team5-iOS/App/Sources/Presentation/Comment/View/Cell/CommentCell.swift index d11ee7201..cea16f36c 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostComment/View/Cell/CommentCell.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Comment/View/Cell/CommentCell.swift @@ -15,39 +15,36 @@ import SnapKit import Then final public class CommentCell: BaseTableViewCell { + // MARK: - Views - private let containerView: UIView = UIView() - private let firstNameLabel: UILabel = BBLabel(.head2Bold, textAlignment: .center) - private let profileImageView: UIImageView = UIImageView() + + private let profileBackground: UIView = UIView() + private let profilePlaceholder: UILabel = BBLabel(.head2Bold, textAlignment: .center) + private let profileImage: UIImageView = UIImageView() private let profileButton: UIButton = UIButton() - private let userNameStack: UIStackView = UIStackView() - private let userNameLabel: BBLabel = BBLabel(.body2Bold, textColor: .gray100) + private let labelStack: UIStackView = UIStackView() + private let nameLabel: BBLabel = BBLabel(.body2Bold, textColor: .gray100) private let createdAtLabel: BBLabel = BBLabel(.body2Regular, textColor: .gray500) private let commentLabel: BBLabel = BBLabel(.body1Regular, textColor: .gray100) + // MARK: - Properties + static var id: String = "CommentCell" - // MARK: - Intialzier - public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - } - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } + // MARK: - Helpers - // MARK: - Helerps public override func prepareForReuse() { super.prepareForReuse() - disposeBag = DisposeBag() + nameLabel.text = "" + createdAtLabel.text = "" + profileImage.image = nil - userNameLabel.text = String.none - createdAtLabel.text = String.none - profileImageView.image = nil + disposeBag = DisposeBag() // TODO: - 코드 삭제 테스트하기 } public override func bind(reactor: CommentCellReactor) { @@ -59,43 +56,45 @@ final public class CommentCell: BaseTableViewCell { private func bindInput(reactor: CommentCellReactor) { Observable.merge( Observable.just(Reactor.Action.fetchUserName), - Observable.just(Reactor.Action.fetchProfileImageUrlString) + Observable.just(Reactor.Action.fetchProfileImage) ) .bind(to: reactor.action) .disposed(by: disposeBag) profileButton.rx.tap - .throttle(RxConst.milliseconds300Interval, scheduler: RxSchedulers.main) + .throttle(RxInterval._300milliseconds, scheduler: RxScheduler.main) .map { Reactor.Action.didTapProfileButton } .bind(to: reactor.action) .disposed(by: disposeBag) } private func bindOutput(reactor: CommentCellReactor) { - let userName = reactor.state.map({ $0.userName }).asDriver(onErrorJustReturn: .none) + let mamberName = reactor.state + .compactMap { $0.memberName } + .asDriver(onErrorJustReturn: "") - userName + mamberName .distinctUntilChanged() - .drive(userNameLabel.rx.text) + .drive(nameLabel.rx.text) .disposed(by: disposeBag) - userName + mamberName .distinctUntilChanged() - .drive(with: self) { $0.userNameLabel.text = $1[0] } + .drive(profilePlaceholder.rx.firstLetterText) .disposed(by: disposeBag) - reactor.state.map { $0.profileImageUrlString } + reactor.state.compactMap { $0.profileImageUrl } .distinctUntilChanged() - .bind(to: profileImageView.rx.kingfisherImage) + .bind(to: profileImage.rx.kingfisherImage) .disposed(by: disposeBag) - reactor.state.map { $0.createdAt } + reactor.state.map { $0.comment.createdAt } .distinctUntilChanged() - .map { $0.relativeFormatter() } + .map { $0.relativeFormatter() } // Reactor 안으로 집어넣기 .bind(to: createdAtLabel.rx.text) .disposed(by: disposeBag) - reactor.state.map { $0.comment } + reactor.state.map { $0.comment.comment } .distinctUntilChanged() .bind(to: commentLabel.rx.text) .disposed(by: disposeBag) @@ -104,15 +103,15 @@ final public class CommentCell: BaseTableViewCell { public override func setupUI() { super.setupUI() - containerView.addSubviews(firstNameLabel, profileImageView, profileButton) - contentView.addSubviews(containerView, userNameStack, commentLabel) - userNameStack.addArrangedSubviews(userNameLabel, createdAtLabel) + profileBackground.addSubviews(profilePlaceholder, profileImage, profileButton) + contentView.addSubviews(profileBackground, labelStack, commentLabel) + labelStack.addArrangedSubviews(nameLabel, createdAtLabel) } public override func setupAutoLayout() { super.setupUI() - containerView.snp.makeConstraints { + profileBackground.snp.makeConstraints { $0.size.equalTo(44) $0.top.equalTo(contentView.snp.top).offset(8) $0.leading.equalTo(contentView.snp.leading).offset(20) @@ -122,22 +121,22 @@ final public class CommentCell: BaseTableViewCell { $0.edges.equalToSuperview() } - firstNameLabel.snp.makeConstraints { + profilePlaceholder.snp.makeConstraints { $0.size.equalToSuperview() } - profileImageView.snp.makeConstraints { + profileImage.snp.makeConstraints { $0.size.equalToSuperview() } - userNameStack.snp.makeConstraints { + labelStack.snp.makeConstraints { $0.top.equalToSuperview().offset(10) - $0.leading.equalTo(containerView.snp.trailing).offset(18) + $0.leading.equalTo(profileBackground.snp.trailing).offset(18) } commentLabel.snp.makeConstraints { - $0.top.equalTo(userNameStack.snp.bottom).offset(8) - $0.leading.equalTo(userNameStack.snp.leading) + $0.top.equalTo(labelStack.snp.bottom).offset(8) + $0.leading.equalTo(labelStack.snp.leading) $0.trailing.equalToSuperview().offset(-8) $0.bottom.equalToSuperview().offset(-10) } @@ -151,23 +150,21 @@ final public class CommentCell: BaseTableViewCell { } profileButton.do { - $0.setTitle(.none, for: .normal) + $0.setTitle("", for: .normal) $0.backgroundColor = UIColor.clear } - containerView.do { - $0.layer.masksToBounds = true + profileBackground.do { $0.layer.cornerRadius = 44 / 2 $0.backgroundColor = UIColor.gray800 } - profileImageView.do { - $0.contentMode = .scaleAspectFill - $0.layer.masksToBounds = true + profileImage.do { $0.layer.cornerRadius = 44 / 2 + $0.contentMode = .scaleAspectFill } - userNameStack.do { + labelStack.do { $0.axis = .horizontal $0.spacing = 8 $0.alignment = .fill diff --git a/14th-team5-iOS/App/Sources/Presentation/Comment/View/CommentTableView.swift b/14th-team5-iOS/App/Sources/Presentation/Comment/View/CommentTableView.swift new file mode 100644 index 000000000..43a823582 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Comment/View/CommentTableView.swift @@ -0,0 +1,126 @@ +// +// CommentTableView.swift +// App +// +// Created by 김건우 on 9/12/24. +// + +import Core +import UIKit + +import SnapKit +import Then + +public final class CommentTableView: BaseTableView { + + // MARK: - Views + + private let basicRefreshControl: UIRefreshControl = UIRefreshControl() + // TODO: - 리팩토링된 FetchFailureView로 바꾸기 + private let fetchFailureView: BibbiFetchFailureView = BibbiFetchFailureView(type: .comment) + private let noneCommentView: NoneCommentView = NoneCommentView() + + + // MARK: - Properties + + private lazy var progressHud: BBProgressHUD = { + let config = BBProgressHUDConfiguration(attachedTo: self) + let viewConfig = BBProgressHUDViewConfiguration( + offsetFromCenterY: -50, + backgroundColor: UIColor.clear + ) + let hud = BBProgressHUD.lottie( + .airplane, + title: "열심히 불러오는 중..", + titleFontStyle: .body1Regular, + titleColor: .gray400, + viewConfig: viewConfig, + config: config + ) + return hud + }() + + + // MARK: - Intializer + + public override init( + frame: CGRect, + style: UITableView.Style + ) { + super.init(frame: .zero, style: .plain) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + // MARK: - Helpers + + public override func setupUI() { + super.setupUI() + + addSubviews(noneCommentView, fetchFailureView) + } + + public override func setupAutoLayout() { + super.setupAutoLayout() + + self.do { + $0.separatorStyle = .none + $0.estimatedRowHeight = 250 + $0.rowHeight = UITableView.automaticDimension + + $0.allowsSelection = false + + $0.backgroundColor = UIColor.bibbiBlack + $0.contentInset = UIEdgeInsets(top: 10, left: 0, bottom: 0, right: 0) + + $0.register(CommentCell.self, forCellReuseIdentifier: CommentCell.id) + } + + fetchFailureView.do { + $0.isHidden = true + } + + noneCommentView.do { + $0.isHidden = true + } + } + + public override func setupAttributes() { + super.setupAttributes() + + noneCommentView.snp.makeConstraints { + $0.top.equalTo(self.snp.top).offset(75) + $0.bottom.equalTo(self.snp.bottom).offset(-5) + $0.horizontalEdges.equalToSuperview() + $0.centerX.equalToSuperview() + } + + fetchFailureView.snp.makeConstraints { + $0.top.equalToSuperview().offset(70) + $0.centerX.equalToSuperview() + } + } + +} + + +// MARK: - Extensions + +extension CommentTableView { + + func hiddenTableProgressHud(hidden: Bool) { + hidden ? progressHud.close() : progressHud.show() + } + + func hiddenFetchFailureView(hidden: Bool) { + fetchFailureView.isHidden = hidden + } + + func hiddenNoneCommentView(hidden: Bool) { + noneCommentView.isHidden = hidden + } + +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Comment/View/CommentTextFieldView.swift b/14th-team5-iOS/App/Sources/Presentation/Comment/View/CommentTextFieldView.swift new file mode 100644 index 000000000..84c00a78d --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Comment/View/CommentTextFieldView.swift @@ -0,0 +1,159 @@ +// +// CommentTextFieldView.swift +// App +// +// Created by 김건우 on 9/12/24. +// + +import Core +import UIKit + +import SnapKit +import Then + +public final class CommentTextFieldView: BaseView { + + // MARK: - Views + + private let container: UIView = UIView() + private let textFieldView: UITextField = UITextField() + private let confirmButton: UIButton = UIButton(type: .system) + + // MARK: - Properties + + public weak var delegate: CommentTextFieldDelegate? + + + // MARK: - Intializer + + public override init(frame: CGRect) { + super.init(frame: .zero) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + // MARK: - Helpers + + public override func bind(reactor: CommentTextFieldReactor) { + super.bind(reactor: reactor) + bindInput(reactor: reactor) + bindOutput(reactor: reactor) + } + + private func bindInput(reactor: CommentTextFieldReactor) { + textFieldView.rx.text + .orEmpty + .map { Reactor.Action.inputText($0) } + .bind(to: reactor.action) + .disposed(by: disposeBag) + } + + private func bindOutput(reactor: CommentTextFieldReactor) { + reactor.pulse(\.$inputText) + .bind(to: textFieldView.rx.text) + .disposed(by: disposeBag) + + reactor.state.map { $0.enableTextField } + .distinctUntilChanged() + .bind(to: textFieldView.rx.isEnabled) + .disposed(by: disposeBag) + + reactor.state.map { $0.enableConfirmButton } + .distinctUntilChanged() + .bind(to: confirmButton.rx.isEnabled) + .disposed(by: disposeBag) + } + + public override func setupUI() { + super.setupUI() + addSubviews(container, textFieldView) + } + + public override func setupAutoLayout() { + super.setupAutoLayout() + + container.snp.makeConstraints { + $0.edges.equalToSuperview() + } + + textFieldView.snp.makeConstraints { + $0.verticalEdges.equalToSuperview() + $0.horizontalEdges.equalToSuperview().inset(15) + } + } + + public override func setupAttributes() { + super.setupAttributes() + + self.do { + $0.backgroundColor = UIColor.gray900 + } + + textFieldView.do { + $0.textColor = UIColor.bibbiWhite + $0.backgroundColor = UIColor.clear + $0.attributedPlaceholder = NSAttributedString( + string: "댓글 달기...", + attributes: [.foregroundColor: UIColor.gray300] + ) + + $0.rightView = confirmButton + $0.rightViewMode = .always + $0.returnKeyType = .done + + $0.delegate = self + } + + confirmButton.do { + $0.isEnabled = false + $0.setTitle("등록", for: .normal) + $0.tintColor = UIColor.mainYellow + + $0.addTarget(self, action: #selector(didTapConfirmButton(_:event:)), for: .touchUpInside) + } + } + +} + + +// MARK: - Extensions + +extension CommentTextFieldView { + + func enableConfirmButton(enable: Bool) { + confirmButton.isEnabled = enable + } + + func enableCommentTextField(enable: Bool) { + textFieldView.isEnabled = enable + } + + func makeTextFieldFirstResponder() { + textFieldView.becomeFirstResponder() + } + + func assignText(_ toTextField: String? = nil) { + textFieldView.text = toTextField + } + +} + +extension CommentTextFieldView { + + @objc func didTapConfirmButton(_ button: UIButton, event: UIButton.Event) { + delegate?.didTapConfirmButton?(button, text: textFieldView.text, event: event) + } + +} + +extension CommentTextFieldView: UITextFieldDelegate { + + public func textFieldShouldReturn(_ textField: UITextField) -> Bool { + delegate?.didTapDoneButton?(text: textField.text) + return true + } + +} diff --git a/14th-team5-iOS/App/Sources/Presentation/PostComment/View/PostCommentTopBarView.swift b/14th-team5-iOS/App/Sources/Presentation/Comment/View/CommentTopBarView.swift similarity index 79% rename from 14th-team5-iOS/App/Sources/Presentation/PostComment/View/PostCommentTopBarView.swift rename to 14th-team5-iOS/App/Sources/Presentation/Comment/View/CommentTopBarView.swift index 5b6ada112..99d6fc7bb 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostComment/View/PostCommentTopBarView.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Comment/View/CommentTopBarView.swift @@ -11,14 +11,17 @@ import UIKit import Then import SnapKit -final class PostCommentTopBarView: UIView { +final class CommentTopBarView: UIView { + // MARK: - Views - private let grabber: UIView = UIView() + private let grabber: UIView = UIView() private let titleLabel: BBLabel = BBLabel(.body1Bold) - private let barDividerView: UIView = UIView() + private let divider: UIView = UIView() + // MARK: - Intializer + override init(frame: CGRect) { super.init(frame: .zero) setupUI() @@ -30,15 +33,17 @@ final class PostCommentTopBarView: UIView { fatalError("init(coder:) has not been implemented") } + // MARK: - Helpers + func setupUI() { - self.addSubviews(grabber, titleLabel, barDividerView) + addSubviews(grabber, titleLabel, divider) } func setupAutoLayout() { grabber.snp.makeConstraints { $0.width.equalTo(36) - $0.height.equalTo(5.08) + $0.height.equalTo(5) $0.centerX.equalToSuperview() $0.top.equalToSuperview().offset(6) } @@ -48,17 +53,16 @@ final class PostCommentTopBarView: UIView { $0.bottom.equalToSuperview().offset(-12) } - barDividerView.snp.makeConstraints { + divider.snp.makeConstraints { $0.height.equalTo(1) - $0.bottom.equalToSuperview() $0.horizontalEdges.equalToSuperview() + $0.bottom.equalToSuperview() } } func setupAttributes() { grabber.do { - $0.layer.masksToBounds = true - $0.layer.cornerRadius = 5.08 / 2.0 + $0.layer.cornerRadius = 2.5 $0.backgroundColor = UIColor.gray500 } @@ -66,7 +70,7 @@ final class PostCommentTopBarView: UIView { $0.text = "댓글" } - barDividerView.do { + divider.do { $0.backgroundColor = UIColor.gray700 } } diff --git a/14th-team5-iOS/App/Sources/Presentation/PostComment/View/NoCommentLabel.swift b/14th-team5-iOS/App/Sources/Presentation/Comment/View/NoneCommentView.swift similarity index 67% rename from 14th-team5-iOS/App/Sources/Presentation/PostComment/View/NoCommentLabel.swift rename to 14th-team5-iOS/App/Sources/Presentation/Comment/View/NoneCommentView.swift index e5e56f794..fa7264956 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostComment/View/NoCommentLabel.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Comment/View/NoneCommentView.swift @@ -11,13 +11,19 @@ import UIKit import Then import SnapKit -final class NoCommentLabel: UIView { +// NoneCommentView + +final class NoneCommentView: UIView { + // MARK: - Views + private let labelStack: UIStackView = UIStackView() - private let mainLabel: BBLabel = BBLabel(.body1Bold, textAlignment: .center) - private let subLabel: BBLabel = BBLabel(.body2Regular, textAlignment: .center, textColor: .gray500) + private let mainTextLabel: BBLabel = BBLabel(.body1Bold, textAlignment: .center) + private let subTextLabel: BBLabel = BBLabel(.body2Regular, textAlignment: .center, textColor: .gray500) + // MARK: - Intializer + override init(frame: CGRect) { super.init(frame: .zero) setupUI() @@ -29,16 +35,17 @@ final class NoCommentLabel: UIView { fatalError("init(coder:) has not been implemented") } + // MARK: - Helpers + func setupUI() { - labelStack.addArrangedSubviews(mainLabel, subLabel) - self.addSubview(labelStack) + addSubview(labelStack) + labelStack.addArrangedSubviews(mainTextLabel, subTextLabel) } func setupAutoLayout() { labelStack.snp.makeConstraints { - $0.top.equalToSuperview() - $0.centerX.equalToSuperview() + $0.center.equalToSuperview() } } @@ -50,11 +57,11 @@ final class NoCommentLabel: UIView { $0.distribution = .fillProportionally } - mainLabel.do { + mainTextLabel.do { $0.text = "아직 댓글이 없습니다" } - subLabel.do { + subTextLabel.do { $0.text = "댓글을 남겨보세요" } } diff --git a/14th-team5-iOS/App/Sources/Presentation/Comment/ViewController/CommentViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Comment/ViewController/CommentViewController.swift new file mode 100644 index 000000000..1e1489f4d --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Comment/ViewController/CommentViewController.swift @@ -0,0 +1,240 @@ +// +// PostCommentViewController.swift +// App +// +// Created by 김건우 on 1/18/24. +// + +import Core +import DesignSystem +import UIKit + +import RxCocoa +import RxDataSources +import RxSwift +import Then +import SnapKit + +final public class CommentViewController: ReactorViewController { + + // MARK: - Typealias + + private typealias RxDataSource = RxTableViewSectionedAnimatedDataSource + + + // MARK: - Views + + private let topBarView: CommentTopBarView = CommentTopBarView() + private lazy var commentTableView: CommentTableView = makeCommentTableView() + private lazy var textFieldView: CommentTextFieldView = makeCommentTextFieldView() + + + // MARK: - Properties + + private lazy var dataSource: RxDataSource = prepareDatasource() + + + // MARK: - LifeCycles + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + navigationController?.navigationBar.isHidden = true + } + + + // MARK: - Helpers + + public override func bind(reactor: CommentViewReactor) { + super.bind(reactor: reactor) + bindInput(reactor: reactor) + bindOutput(reactor: reactor) + } + + private func bindInput(reactor: CommentViewReactor) { + Observable.just(()) + .delay(RxInterval._900milliseconds, scheduler: RxScheduler.main) + .map { Reactor.Action.fetchComment } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + Observable.merge( + textFieldView.rx.didTapDoneButton, + textFieldView.rx.didTapConfirmButton + ) + .throttle(RxInterval._300milliseconds, scheduler: RxScheduler.main) + .map { Reactor.Action.createComment($0) } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + commentTableView.rx.itemDeleted + .withUnretained(self) + .bind { $0.0.reactor?.action.onNext(.deleteComment($0.0.dataSource[$0.1].currentState.comment.commentId)) } + .disposed(by: disposeBag) + + // TODO: - 테이블 등 다른 화면 터치 시 키보드 내리기 + + } + + private func bindOutput(reactor: CommentViewReactor) { + reactor.pulse(\.$commentDatasource) + .bind(to: commentTableView.rx.items(dataSource: dataSource)) + .disposed(by: disposeBag) + + reactor.state.map { $0.hiddenTableProgressHud } + .distinctUntilChanged() + .bind(with: self) { $0.commentTableView.hiddenTableProgressHud(hidden: $1) } + .disposed(by: disposeBag) + + reactor.state.map { $0.hiddenFetchFailureView } + .distinctUntilChanged() + .bind(with: self) { $0.commentTableView.hiddenFetchFailureView(hidden: $1) } + .disposed(by: disposeBag) + + reactor.state.map { $0.hiddenNoneCommentView } + .distinctUntilChanged() + .bind(with: self) { $0.commentTableView.hiddenNoneCommentView(hidden: $1) } + .disposed(by: disposeBag) + + reactor.state.map { $0.enableConfirmButton } + .distinctUntilChanged() + .bind(with: self) { $0.textFieldView.enableConfirmButton(enable: $1) } + .disposed(by: disposeBag) + + reactor.state.map { $0.enableCommentTextField } + .distinctUntilChanged() + .bind(with: self) { $0.textFieldView.enableCommentTextField(enable: $1) } + .disposed(by: disposeBag) + + reactor.pulse(\.$text) + .bind(with: self) { $0.textFieldView.assignText($1) } + .disposed(by: disposeBag) + + reactor.pulse(\.$makeTextFieldFirstResponder) + .bind(with: self) { owner, _ in owner.textFieldView.makeTextFieldFirstResponder() } + .disposed(by: disposeBag) + + reactor.pulse(\.$scrollTableToLast) + .filter { $0 } + .bind(with: self) { owner, _ in owner.scrollCommentTableToLast() } + .disposed(by: disposeBag) + + let keyboardWillShow = NotificationCenter.default.rx + .notification(UIResponder.keyboardWillShowNotification) + .flatMap { + guard + let rect = $0.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect + else { return Observable.just(.zero) } + return Observable.just(rect.height) + } + + let keyboardWillHide = NotificationCenter.default.rx + .notification(UIResponder.keyboardWillHideNotification) + .flatMap { _ in Observable.just(.zero) } + + keyboardWillShow + .bind(with: self) { owner, height in + let bottomInset = owner.view.safeAreaInsets.bottom + let keyboardHeight = height - bottomInset + + // TODO: - 애니메이션 메서드로 빼기 + UIView.animate(withDuration: 1.0) { + owner.textFieldView.snp.updateConstraints { + $0.bottom.equalTo(owner.view.safeAreaLayoutGuide.snp.bottom).offset(-keyboardHeight) + } + owner.textFieldView.layoutIfNeeded() + } + } + .disposed(by: disposeBag) + + keyboardWillHide + .bind(with: self) { owner, height in + // TODO: - 애니메이션 메서드로 빼기 + UIView.animate(withDuration: 1.0) { + owner.textFieldView.snp.updateConstraints { + $0.bottom.equalTo(owner.view.safeAreaLayoutGuide.snp.bottom).offset(0) + } + owner.textFieldView.layoutIfNeeded() + } + } + .disposed(by: disposeBag) + } + + public override func setupUI() { + super.setupUI() + view.addSubviews(topBarView, commentTableView, textFieldView) + } + + public override func setupAutoLayout() { + super.setupAutoLayout() + + topBarView.snp.makeConstraints { + $0.height.equalTo(60) + $0.top.equalToSuperview() + $0.horizontalEdges.equalToSuperview() + } + + commentTableView.snp.makeConstraints { + $0.top.equalTo(topBarView.snp.bottom) + $0.horizontalEdges.equalToSuperview() + } + + textFieldView.snp.makeConstraints { + $0.height.equalTo(46) + $0.top.equalTo(commentTableView.snp.bottom) + $0.horizontalEdges.equalToSuperview() + $0.bottom.equalTo(view.safeAreaLayoutGuide.snp.bottom).offset(0) + } + } + + public override func setupAttributes() { + super.setupAttributes() + // TODO: - App.Repository 제거하기 + dataSource.canEditRowAtIndexPath = { + let myMemberId = App.Repository.member.memberID.value + let commentMemberId = $0[$1].currentState.comment.memberId + return myMemberId == commentMemberId + } + } +} + + +// MARK: - Extensions + +extension CommentViewController { + + private func makeCommentTableView() -> CommentTableView { + CommentTableView( + reactor: CommentTableReactor() + ) + } + + private func makeCommentTextFieldView() -> CommentTextFieldView { + CommentTextFieldView( + reactor: CommentTextFieldReactor() + ) + } + + private func prepareDatasource() -> RxDataSource { + return RxDataSource { dataSource, tableView, indexPath, reactor in + let cell = tableView.dequeueReusableCell( + withIdentifier: CommentCell.id + ) as! CommentCell + cell.reactor = reactor + return cell + } + } + +} + + +extension CommentViewController { + + private func scrollCommentTableToLast() { + guard + let count = reactor?.currentState.commentDatasource.first?.items.count + else { return } + let indexPath = IndexPath(item: count - 1, section: 0) + commentTableView.scrollToRow(at: indexPath, at: .bottom, animated: true) + } + +} 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 dd2f3db4a..18aa5a98d 100644 --- a/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Reactor/InputFamilyLinkReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Reactor/InputFamilyLinkReactor.swift @@ -60,8 +60,8 @@ extension InputFamilyLinkReactor { let body = JoinFamilyRequest(inviteCode: String(code)) let commonLogic: (JoinFamilyEntity) -> Observable = { joinFamilyData in -// App.Repository.member.familyId.accept(joinFamilyData.familyId) -// App.Repository.member.familyCreatedAt.accept(joinFamilyData.createdAt) + App.Repository.member.familyId.accept(joinFamilyData.familyId) + App.Repository.member.familyCreatedAt.accept(joinFamilyData.createdAt) return Observable.just(Mutation.setShowHome(true)) } diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainFamilyViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainFamilyViewReactor.swift index f814520a6..5264a8b67 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainFamilyViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainFamilyViewReactor.swift @@ -19,8 +19,8 @@ final class MainFamilyViewReactor: Reactor { } enum Mutation { - case setSharePanel(String) - case showShareAcitivityView(URL?) +// case setSharePanel(String) +// case showShareAcitivityView(URL?) case updateFamilyDataSource([FamilySection.Item]) case setFetchFailureToastMessageView @@ -29,7 +29,7 @@ final class MainFamilyViewReactor: Reactor { } struct State { - @Pulse var familyInvitationLink: URL? +// @Pulse var familyInvitationLink: URL? @Pulse var shouldPresentFetchFailureToastMessageView: Bool = false @Pulse var familySection: FamilySection.Model = FamilySection.Model(model: 0, items: []) @@ -39,6 +39,10 @@ final class MainFamilyViewReactor: Reactor { let initialState: State = State() @Injected var provider: ServiceProviderProtocol @Injected var familyUseCase: FamilyUseCaseProtocol + + @Injected var fetchSharinUrlUseCase: FetchInvitationLinkUseCaseProtocol + + @Navigator var navigator: HomeNavigatorProtocol } extension MainFamilyViewReactor { @@ -46,12 +50,14 @@ extension MainFamilyViewReactor { switch action { case .tapInviteFamily: MPEvent.Home.shareLink.track(with: nil) - return familyUseCase.executeFetchInvitationUrl() - .map { - guard let invitationLink = $0?.url else { - return .setFetchFailureToastMessageView + return fetchSharinUrlUseCase.execute() + .withUnretained(self) + .flatMap { + guard let invitationLink = $0.1?.url else { + return Observable.just(.setFetchFailureToastMessageView) } - return .setSharePanel(invitationLink) + $0.0.navigator.presentSharingSheet(url: URL(string: invitationLink)) + return Observable.empty() } case .updateFamilySection(let items): if items.count < 2 { @@ -70,12 +76,12 @@ extension MainFamilyViewReactor { newState.familySection.items = familySectionItem case .setInviteFamilyView(let isShow): newState.isShowingInviteFamilyView = isShow - case let .showShareAcitivityView(url): - newState.familyInvitationLink = url +// case let .showShareAcitivityView(url): +// newState.familyInvitationLink = url case .setFetchFailureToastMessageView: newState.shouldPresentFetchFailureToastMessageView = true - case let .setSharePanel(urlString): - newState.familyInvitationLink = URL(string: urlString) +// case let .setSharePanel(urlString): +// newState.familyInvitationLink = URL(string: urlString) } return newState diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainFamilyViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainFamilyViewController.swift index fafeb37c0..eb11c95f6 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainFamilyViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainFamilyViewController.swift @@ -121,16 +121,16 @@ extension MainFamilyViewController { .bind(to: familyCollectionView.rx.isHidden) .disposed(by: disposeBag) - reactor.pulse(\.$familyInvitationLink) - .observe(on: RxSchedulers.main) - .withUnretained(self) - .bind(onNext: { - $0.0.makeInvitationUrlSharePanel( - $0.1, - provider: reactor.provider - ) - }) - .disposed(by: disposeBag) +// reactor.pulse(\.$familyInvitationLink) +// .observe(on: RxSchedulers.main) +// .withUnretained(self) +// .bind(onNext: { +// $0.0.makeInvitationUrlSharePanel( +// $0.1, +// provider: reactor.provider +// ) +// }) +// .disposed(by: disposeBag) reactor.pulse(\.$shouldPresentFetchFailureToastMessageView) .filter { $0 } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/Activity/CopyInvitationUrlActivity.swift b/14th-team5-iOS/App/Sources/Presentation/Management/Activity/CopyInvitationUrlActivity.swift similarity index 90% rename from 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/Activity/CopyInvitationUrlActivity.swift rename to 14th-team5-iOS/App/Sources/Presentation/Management/Activity/CopyInvitationUrlActivity.swift index 43e83064b..a35ade7f8 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/Activity/CopyInvitationUrlActivity.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Management/Activity/CopyInvitationUrlActivity.swift @@ -5,8 +5,11 @@ // Created by 김건우 on 12/13/23. // +import Core import UIKit +// TODO: - 코드 리팩토링하기 + public class CopyInvitationUrlActivity: UIActivity { private enum Activity { static let activityTitle: String = "초대 링크 복사" @@ -46,7 +49,7 @@ public class CopyInvitationUrlActivity: UIActivity { public override func perform() { UIPasteboard.general.string = url.description - provider.managementService.didTapCopUrlAction() + provider.managementService.didTapCopUrlAction() // TODO: - 오타 수정 } } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/Activity/UrlActivityItemSource.swift b/14th-team5-iOS/App/Sources/Presentation/Management/Activity/UrlActivityItemSource.swift similarity index 97% rename from 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/Activity/UrlActivityItemSource.swift rename to 14th-team5-iOS/App/Sources/Presentation/Management/Activity/UrlActivityItemSource.swift index 9a2bb8af1..19e2b987a 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/Activity/UrlActivityItemSource.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Management/Activity/UrlActivityItemSource.swift @@ -7,6 +7,8 @@ import LinkPresentation +// TODO: - 코드 리팩토링하기 + public class UrlActivityItemSource: NSObject, UIActivityItemSource { var title: String var url: URL 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 134c8b3f5..c0715aef9 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Management/Reactor/ManagementReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Management/Reactor/ManagementReactor.swift @@ -18,7 +18,7 @@ public final class ManagementReactor: Reactor { // MARK: - Action public enum Action { - case fetchPaginationFamilyMemebers + case fetchPaginationFamilyMemeber(refresh: Bool) case didTapSharingContainer case didTapSettingBarButton case didTapFamilyNameEditButton @@ -30,6 +30,13 @@ public final class ManagementReactor: Reactor { public enum Mutation { case setMemberDatasource([FamilyMemberCellReactor]) + + case setHiddenSharingProgressHud(Bool) + case setHiddenTableProgressHud(Bool) + case setHiddenMemberFetchFailureView(Bool) + case setEndRefreshing(Bool) + + case setTableHeaderInfo((String, Int)?) } @@ -37,6 +44,13 @@ public final class ManagementReactor: Reactor { public struct State { @Pulse var memberDatasource: [FamilyMemberSectionModel] = [.init(model: (), items: [])] + + var hiddenSharingProgressHud: Bool = true + var hiddenTableProgressHud: Bool = false + var hiddenMemberFetchFailureView: Bool = true + @Pulse var isRefreshing: Bool = false + + var tableHeaderInfo: (String, Int)? = nil } @@ -91,23 +105,20 @@ public final class ManagementReactor: Reactor { // Mixpanel MPEvent.Family.shareLink.track(with: nil) - managementService.hiddenSharingProgressHud(hidden: false) return Observable.concat( + Observable.just(.setHiddenSharingProgressHud(false)), fetchSharingUrlUseCase.execute() .withUnretained(self) .concatMap { - guard let sharingUrl = $0.1?.url else { + guard let url = $0.1?.url else { Haptic.notification(type: .error) - managementService.hiddenSharingProgressHud(hidden: true) $0.0.navigator.showErrorToast() - - return Observable.empty() + return Observable.just(.setHiddenSharingProgressHud(true)) } - managementService.hiddenSharingProgressHud(hidden: true) - $0.0.navigator.presentSharingSheet(url: URL(string: sharingUrl)) - return Observable.empty() + $0.0.navigator.presentSharingSheet(url: URL(string: url)) + return Observable.just(.setHiddenSharingProgressHud(true)) } ) @@ -121,35 +132,40 @@ public final class ManagementReactor: Reactor { navigator.toFamilyNameSetting() return Observable.empty() - case .fetchPaginationFamilyMemebers: + case let .fetchPaginationFamilyMemeber(refresh): let query = FamilyPaginationQuery() return Observable.concat( + Observable.just(.setHiddenTableProgressHud(refresh)), + Observable.just(.setHiddenMemberFetchFailureView(true)), + fetchFamilyMemberUseCase.execute(query: query) .withUnretained(self) .concatMap { guard let results = $0.1?.results else { Haptic.notification(type: .error) - managementService.hiddenTableProgressHud(hidden: true) - managementService.hiddenMemberFetchFailureView(hidden: false) $0.0.navigator.showErrorToast() - managementService.endTableRefreshing() - - return Observable.just(.setMemberDatasource([])) + return Observable.concat( + Observable.just(.setMemberDatasource([])), + Observable.just(.setHiddenTableProgressHud(true)), + Observable.just(.setHiddenMemberFetchFailureView(false)), + Observable.just(.setEndRefreshing(true)) + ) } - managementService.hiddenTableProgressHud(hidden: true) - managementService.hiddenMemberFetchFailureView(hidden: true) - // TODO: - 새로운 가족 이름 반영하기 - managementService.setTableHeaderInfo(familyName: "나의 가족", memberCount: results.count) - managementService.endTableRefreshing() - let items = results.sorted { [unowned self] in self.checkIsMeUseCase.execute(memberId: $0.memberId) && !self.checkIsMeUseCase.execute(memberId: $1.memberId) }.map { FamilyMemberCellReactor(.management, member: $0) } - return Observable.just(.setMemberDatasource(items)) + return Observable.concat( + Observable.just(.setMemberDatasource(items)), + Observable.just(.setHiddenTableProgressHud(true)), + Observable.just(.setHiddenMemberFetchFailureView(true)), + Observable.just(.setEndRefreshing(true)), + Observable.just(.setTableHeaderInfo(("나의 가족", results.count))) + ) + // TODO: - 가족 정보 반영 로직 구현하기 } ) @@ -168,12 +184,29 @@ public final class ManagementReactor: Reactor { public func reduce(state: State, mutation: Mutation) -> State { var newState = state + switch mutation { - case let .setMemberDatasource(items): let dataSource = FamilyMemberSectionModel(model: (), items: items) newState.memberDatasource = [dataSource] + + case let .setHiddenSharingProgressHud(hidden): + newState.hiddenSharingProgressHud = hidden + + case let .setHiddenTableProgressHud(hidden): + newState.hiddenTableProgressHud = hidden + + case let .setHiddenMemberFetchFailureView(hidden): + newState.hiddenMemberFetchFailureView = hidden + + case let .setEndRefreshing(isRefreshing): + newState.isRefreshing = isRefreshing + + case let .setTableHeaderInfo(headerInfo): + newState.tableHeaderInfo = headerInfo } + return newState } + } diff --git a/14th-team5-iOS/App/Sources/Presentation/Management/Reactor/ManagementTableHeaderReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Management/Reactor/ManagementTableHeaderReactor.swift index 500c1f408..33810e539 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Management/Reactor/ManagementTableHeaderReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Management/Reactor/ManagementTableHeaderReactor.swift @@ -20,22 +20,15 @@ public final class ManagementTableHeaderReactor { // MARK: - Mutation - public enum Mutation { - case setFamilyName(String) - case setMemberCount(Int) - } + public enum Mutation { } // MARK: - State - public struct State { - var familyName: String = "나의 가족" - var memberCount: String = "0" - } + public struct State { } - // MARK: - Properties - @Injected var provider: ServiceProviderProtocol + // MARK: - Properties public var initialState: State @@ -46,49 +39,4 @@ public final class ManagementTableHeaderReactor { self.initialState = State() } - - // MARK: - Transform - - public func transform(mutation: Observable) -> Observable { - let eventMutation = provider.managementService.event - .flatMap { event in - switch event { - case let .setTableHeaderInfo(familyName, memberCount): - return Observable.concat( - Observable.just(.setFamilyName(familyName)), - Observable.just(.setMemberCount(memberCount)) - ) - - default: - return Observable.empty() - } - } - - return Observable.merge(eventMutation, mutation) - } - - // MARK: - Mutate - - public func mutate(action: Action) -> Observable { - return .empty() - } - - - // MARK: - Reduce - - public func reduce(state: State, mutation: Mutation) -> State { - var newState = state - - switch mutation { - case let .setFamilyName(name): - newState.familyName = name - - case let .setMemberCount(count): - newState.memberCount = String(count) - } - - return newState - } - - } diff --git a/14th-team5-iOS/App/Sources/Presentation/Management/Reactor/ManagementTableReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Management/Reactor/ManagementTableReactor.swift index 619fdfbe6..3e7f04067 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Management/Reactor/ManagementTableReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Management/Reactor/ManagementTableReactor.swift @@ -20,20 +20,12 @@ final public class ManagementTableReactor { // MARK: - Mutation - public enum Mutation { - case setHiddenTableProgresssHud(Bool) - case setHiddenFetchFailureView(Bool) - case setEndRefreshing(Bool) - } + public enum Mutation { } // MARK: - State - public struct State { - var hiddenTableProgressHud: Bool = false - var hiddenFetchFailureView: Bool = true - @Pulse var isRefreshing: Bool = true - } + public struct State { } // MARK: - Prperties @@ -49,48 +41,4 @@ final public class ManagementTableReactor { self.initialState = State() } - - // MARK: - Transform - - public func transform(mutation: Observable) -> Observable { - let eventMutation = provider.managementService.event - .flatMap { event in - switch event { - case let .hiddenTableProgressHud(hidden): - return Observable.just(.setHiddenTableProgresssHud(hidden)) - - case let .hiddenMemberFetchFailureView(hidden): - return Observable.just(.setHiddenFetchFailureView(hidden)) - - case .endTableRefreshing: - return Observable.just(.setEndRefreshing(false)) - - default: - return Observable.empty() - } - } - - return Observable.merge(eventMutation, mutation) - } - - - // MARK: - Reduce - - public func reduce(state: State, mutation: Mutation) -> State { - var newState = state - - switch mutation { - case let .setHiddenTableProgresssHud(hidden): - newState.hiddenTableProgressHud = hidden - - case let .setHiddenFetchFailureView(hidden): - newState.hiddenFetchFailureView = hidden - - case let .setEndRefreshing(isRefreshing): - newState.isRefreshing = isRefreshing - } - - return newState - } - } diff --git a/14th-team5-iOS/App/Sources/Presentation/Management/Reactor/SharingContainerReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Management/Reactor/SharingContainerReactor.swift index 6da9a3fa9..b83089f34 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Management/Reactor/SharingContainerReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Management/Reactor/SharingContainerReactor.swift @@ -20,24 +20,18 @@ final public class SharingContainerReactor: Reactor { // MARK: - Mutation - public enum Mutation { - case setProgressHud(Bool) - } + public enum Mutation { } // MARK: - State - public struct State { - var hiddenProgresHud: Bool = true - } + public struct State { } // MARK: - Properties public var initialState: State - @Injected var provider: ServiceProviderProtocol - // MARK: - Intializer @@ -45,32 +39,4 @@ final public class SharingContainerReactor: Reactor { self.initialState = State() } - - // MARK: - Transform - - public func transform(mutation: Observable) -> Observable { - let eventMutation = provider.managementService.event - .flatMap { event -> Observable in - switch event { - case let .hiddenSharingProgressHud(hidden): - return Observable.just(.setProgressHud(hidden)) - default: - return Observable.empty() - } - } - - return Observable.merge(mutation, eventMutation) - } - - - // MARK: - Reduce - - public func reduce(state: State, mutation: Mutation) -> State { - var newState = state - switch mutation { - case let .setProgressHud(hidden): - newState.hiddenProgresHud = hidden - } - return newState - } } diff --git a/14th-team5-iOS/App/Sources/Presentation/Management/View/ManagementTableHeaderView.swift b/14th-team5-iOS/App/Sources/Presentation/Management/View/ManagementTableHeaderView.swift index ae42dcc36..9d500be19 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Management/View/ManagementTableHeaderView.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Management/View/ManagementTableHeaderView.swift @@ -29,28 +29,6 @@ public final class ManagementTableHeaderView: BaseView { // MARK: - Helpers - public override func bind(reactor: ManagementTableReactor) { - super.bind(reactor: reactor) - - bindOutput(reactor: reactor) - } - - private func bindOutput(reactor: ManagementTableReactor) { - - reactor.state.map { $0.hiddenTableProgressHud } - .distinctUntilChanged() - .bind(with: self) { owner, hidden in - hidden ? owner.progressHud.close() : owner.progressHud.show() - } - .disposed(by: disposeBag) - - reactor.state.map { $0.hiddenFetchFailureView } - .distinctUntilChanged() - .bind(to: fetchFailureView.rx.isHidden) - .disposed(by: disposeBag) - - reactor.pulse(\.$isRefreshing) - .delay(RxInterval._700milliseconds, scheduler: RxScheduler.main) - .bind(to: basicRefreshControl.rx.isRefreshing) - .disposed(by: disposeBag) - - - } - - public override func setupUI() { super.setupUI() @@ -134,7 +105,14 @@ public final class ManagementTableView: BaseTableView { extension ManagementTableView { - // TODO: - 임시 코드 삭제하기 + func hiddenTableProgressHud(hidden: Bool) { + hidden ? progressHud.close() : progressHud.show() + } + + func hiddenFetchFailureView(hidden: Bool) { + fetchFailureView.isHidden = hidden + } + func endRefreshing() { basicRefreshControl.endRefreshing() } diff --git a/14th-team5-iOS/App/Sources/Presentation/Management/View/SharingContainerView.swift b/14th-team5-iOS/App/Sources/Presentation/Management/View/SharingContainerView.swift index 34bc97011..cc743c7ea 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Management/View/SharingContainerView.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Management/View/SharingContainerView.swift @@ -41,39 +41,7 @@ public final class SharingContainerView: BaseView { // MARK: - Helpers - - public override func bind(reactor: SharingContainerReactor) { - super.bind(reactor: reactor) - - bindInput(reactor: reactor) - bindOutput(reactor: reactor) - } - - private func bindInput(reactor: SharingContainerReactor) { } - - private func bindOutput(reactor: SharingContainerReactor) { - let hiddenProgressHud = reactor.state - .map { $0.hiddenProgresHud } - .asDriver(onErrorJustReturn: true) - - hiddenProgressHud - .distinctUntilChanged() - .drive(basicProgressHud.rx.isHidden) - .disposed(by: disposeBag) - - hiddenProgressHud - .map { !$0 } - .distinctUntilChanged() - .drive(basicProgressHud.rx.isAnimating) - .disposed(by: disposeBag) - - hiddenProgressHud - .map { !$0 } - .distinctUntilChanged() - .drive(sharingImageView.rx.isHidden) - .disposed(by: disposeBag) - } - + public override func setupUI() { super.setupUI() @@ -155,3 +123,20 @@ public final class SharingContainerView: BaseView { } } } + + +// MARK: - Extensions + +extension SharingContainerView { + + func hiddenSharingProgressHud(hidden: Bool) { + if hidden { + basicProgressHud.stopAnimating() + sharingImageView.isHidden = false + } else { + basicProgressHud.startAnimating() + sharingImageView.isHidden = true + } + } + +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Management/ViewController/ManagementViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Management/ViewController/ManagementViewController.swift index b49dae592..320d36d8f 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Management/ViewController/ManagementViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Management/ViewController/ManagementViewController.swift @@ -49,7 +49,7 @@ public final class ManagementViewController: BBNavigationViewController.just(()) - .map { Reactor.Action.fetchPaginationFamilyMemebers } + .map { Reactor.Action.fetchPaginationFamilyMemeber(refresh: false) } .bind(to: reactor.action) .disposed(by: disposeBag) @@ -71,7 +71,7 @@ public final class ManagementViewController: BBNavigationViewController Observable { - switch action { - case .fetchUserName: - let userName = memberUseCase.executeFetchUserName(memberId: initialState.memberId) - return Observable.just(.injectUserName(userName)) - - case .fetchProfileImageUrlString: - let urlString = memberUseCase.executeProfileImageUrlString(memberId: initialState.memberId) - return Observable.just(.injectProfileImageUrlString(urlString)) - - case .didTapProfileButton: - let memberId = initialState.memberId - if memberUseCase.executeCheckIsValidMember(memberId: memberId) { - provider.postGlobalState.pushProfileViewController(memberId) - } - return Observable.empty() - } - } - - // MARK: - Reduce - public func reduce(state: State, mutation: Mutation) -> State { - var newState = state - switch mutation { - case let .injectUserName(userName): - newState.userName = userName - - case let .injectProfileImageUrlString(urlString): - newState.profileImageUrlString = urlString - } - return newState - } -} - -// MARK: - Extensions -extension CommentCellReactor: IdentifiableType, Equatable { - // MARK: - IdentifiableType - public typealias Identity = String - public var identity: Identity { - return initialState.commentId - } - - // MARK: - Equatable - public static func == (lhs: CommentCellReactor, rhs: CommentCellReactor) -> Bool { - return lhs.initialState.commentId == rhs.initialState.commentId - } -} diff --git a/14th-team5-iOS/App/Sources/Presentation/PostComment/Reactor/CommentViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/PostComment/Reactor/CommentViewReactor.swift deleted file mode 100644 index 488dfc10c..000000000 --- a/14th-team5-iOS/App/Sources/Presentation/PostComment/Reactor/CommentViewReactor.swift +++ /dev/null @@ -1,344 +0,0 @@ -// -// PostCommentReactor.swift -// App -// -// Created by 김건우 on 1/18/24. -// - -import Core -import Domain -import Foundation - -import ReactorKit -import RxSwift - -final public class CommentViewReactor: Reactor { - // MARK: - Action - public enum Action { - case inputComment(String) - case fetchPostComment - case createPostComment(String?) - case deletePostComment(String) - case keyboardWillShow(CGFloat) - case keyboardWillHide - } - - // MARK: - Mutation - public enum Mutation { - case injectInputComment(String) - case injectPostComment([CommentCellReactor]) - case appendPostComment(CommentCellReactor) - case removePostComment(String) - case setTableViewOffset(CGFloat) - case setUploadCommentFamilureTaostMessageView - case setDeleteCommentCompleteToastMessageView - case setDeleteCommentFamilureToastMessageView - case setCommentFetchFailureToastMessageView - case setHiddenNoCommentView(Bool) - case setHiddenPaperAirplaneLottieView(Bool) - case generateErrorHapticNotification - case scrollToLast - case becomeFirstResponseder - case clearCommentTextField - case enableCommentTextField(Bool) - - case dismiss - } - - // MARK: - State - public struct State { - var postId: String - @Pulse var commentCount: Int - - var inputComment: String - @Pulse var displayComment: [CommentSectionModel] - @Pulse var shouldScrollToLast: Int - @Pulse var shouldClearCommentTextField: Bool - @Pulse var shouldPresentUploadCommentFailureTaostMessageView: Bool - @Pulse var shouldPresentDeleteCommentCompleteToastMessageView: Bool - @Pulse var shouldPresentDeleteCommentFailureToastMessageView: Bool - @Pulse var shouldPresentCommentFetchFailureTaostMessageView: Bool - @Pulse var shouldPresentEmptyCommentView: Bool - @Pulse var shouldPresentPaperAirplaneLottieView: Bool - @Pulse var shouldGenerateErrorHapticNotification: Bool - @Pulse var shouldDismiss: Bool - @Pulse var becomeFirstResponder: Bool - var enableCommentTextField: Bool - var tableViewBottomOffset: CGFloat - } - - // MARK: - Properties - public var initialState: State - - @Injected var memberUseCase: MemberUseCaseProtocol - @Injected var postCommentUseCase: PostCommentUseCaseProtocol - @Injected var provider: ServiceProviderProtocol - - private var postComentCount: Int = 0 - private var hasReceivedInputEvent: Bool = false - - // MARK: - Intializer - public init(postId: String) { - self.initialState = State( - postId: postId, - commentCount: 0, - inputComment: "", - displayComment: [.init(model: .none, items: [])], - shouldScrollToLast: 0, - shouldClearCommentTextField: false, - shouldPresentUploadCommentFailureTaostMessageView: false, - shouldPresentDeleteCommentCompleteToastMessageView: false, - shouldPresentDeleteCommentFailureToastMessageView: false, - shouldPresentCommentFetchFailureTaostMessageView: false, - shouldPresentEmptyCommentView: false, - shouldPresentPaperAirplaneLottieView: false, - shouldGenerateErrorHapticNotification: false, - shouldDismiss: false, - becomeFirstResponder: false, - enableCommentTextField: false, - tableViewBottomOffset: 0 - ) - } - - // MARK: - Transform - public func transform(mutation: Observable) -> Observable { - let inputMutation = provider.postGlobalState.input - .withUnretained(self) - .flatMap { - let postId = $0.0.currentState.postId - // 처음 시트가 열리고 - if !$0.0.hasReceivedInputEvent { - // Post Id가 동일하고 텍스트가 있으면 - if $0.1.0 == postId && !$0.1.1.isEmpty { - $0.0.hasReceivedInputEvent = true // 이후 불필요한 스트림 막기 - return Observable.just(.injectInputComment($0.1.1)) - } - } - return Observable.empty() - } - - let postMutation = provider.postGlobalState.event - .flatMap { event in - switch event { - case let .pushProfileViewController(_): - return Observable.just(.dismiss) - default: - return Observable.empty() - } - } - - return Observable.merge(mutation, inputMutation, postMutation) - } - - // MARK: - Mutate - public func mutate(action: Action) -> Observable { - switch action { - case let .inputComment(text): - let postId = currentState.postId - provider.postGlobalState.storeCommentText(postId, text: text) - return Observable.just(.injectInputComment(text)) - - case .fetchPostComment: - let postId = currentState.postId - let query = PostCommentPaginationQuery() - - return Observable.concat( - Observable.just(.setHiddenPaperAirplaneLottieView(false)), - Observable.just(.enableCommentTextField(false)), - - postCommentUseCase.executeFetchPostComment(postId: postId, query: query) - .concatMap { - // 통신에 실패한다면 - guard let commentResponseArray = $0 else { - return Observable.concat( - Observable.just(.setHiddenPaperAirplaneLottieView(true)), - Observable.just(.setCommentFetchFailureToastMessageView), - Observable.just(.generateErrorHapticNotification), - Observable.just(.injectPostComment([])), - Observable.just(.setHiddenNoCommentView(true)) - ) - } - - // 댓글이 없다면 - guard !commentResponseArray.results.isEmpty else { - return Observable.concat( - Observable.just(.setHiddenPaperAirplaneLottieView(true)), - Observable.just(.enableCommentTextField(true)), - Observable.just(.becomeFirstResponseder), - Observable.just(.injectPostComment([])) - ) - } - - let reactors = commentResponseArray.results.map { CommentCellReactor( - $0, - memberUseCase: self.memberUseCase, - postCommentUseCase: self.postCommentUseCase, - provider: self.provider - ) - } - - return Observable.concat( - Observable.just(.setHiddenPaperAirplaneLottieView(true)), - Observable.just(.injectPostComment(reactors)), - Observable.just(.scrollToLast), - Observable.just(.enableCommentTextField(true)), - Observable.just(.becomeFirstResponseder) - ) - } - ) - - case let .createPostComment(comment): - guard let safeComment = comment, - !safeComment.trimmingCharacters(in: .whitespaces).isEmpty else { - return Observable.just(.clearCommentTextField) - } - - let postId = initialState.postId - let body = CreatePostCommentRequest(content: safeComment) - - return Observable.concat( - Observable.just(.enableCommentTextField(false)), - - postCommentUseCase.executeCreatePostComment(postId: postId, body: body) - .withUnretained(self) - .concatMap { - guard let commentResponse = $0.1 else { - return Observable.concat( - Observable.just(.enableCommentTextField(true)), - Observable.just(.generateErrorHapticNotification), - Observable.just(.setUploadCommentFamilureTaostMessageView) - - ) - } - - let reactor = CommentCellReactor( - commentResponse, - memberUseCase: self.memberUseCase, - postCommentUseCase: self.postCommentUseCase, - provider: self.provider - ) - - let count = $0.0.currentState.commentCount - $0.0.provider.postGlobalState.clearCommentText() - $0.0.provider.postGlobalState.renewalPostCommentCount(count + 1) - return Observable.concat( - Observable.just(.enableCommentTextField(true)), - Observable.just(.clearCommentTextField), - Observable.just(.appendPostComment(reactor)), - Observable.just(.scrollToLast) - ) - } - ) - - case let .deletePostComment(commentId): - let postId = initialState.postId - - return postCommentUseCase.executeDeletePostComment(postId: postId, commentId: commentId) - .withUnretained(self) - .flatMap { - guard let deleteSuccessResponse = $0.1, - deleteSuccessResponse.success else { - return Observable.concat( - Observable.just(.generateErrorHapticNotification), - Observable.just(.setUploadCommentFamilureTaostMessageView) - ) - } - - let count = $0.0.currentState.commentCount - $0.0.provider.postGlobalState.renewalPostCommentCount(count - 1) - return Observable.concat( - Observable.just(.removePostComment(commentId)), - Observable.just(.setDeleteCommentCompleteToastMessageView) - ) - - } - - case let .keyboardWillShow(height): - return Observable.concat( - Observable.just(.setTableViewOffset(height)), - Observable.just(.scrollToLast) - ) - - case .keyboardWillHide: - return Observable.concat( - Observable.just(.setTableViewOffset(.zero)), - Observable.just(.scrollToLast) - ) - } - } - - // MARK: - Reduce - public func reduce(state: State, mutation: Mutation) -> State { - var newState = state - switch mutation { - case let .injectInputComment(text): - newState.inputComment = text - - case let .injectPostComment(reactor): - newState.displayComment = [.init(model: .none, items: reactor)] - newState.commentCount = reactor.count - - case let .appendPostComment(reactor): - guard var dataSource = newState.displayComment.first else { - break - } - dataSource.items.append(reactor) - newState.displayComment = [dataSource] - newState.commentCount = dataSource.items.count - - case let .removePostComment(commentId): - guard var dataSource = newState.displayComment.first else { - break - } - dataSource.items.removeAll(where: { - $0.currentState.commentId == commentId - }) - newState.displayComment = [dataSource] - newState.commentCount = dataSource.items.count - - case .setDeleteCommentCompleteToastMessageView: - newState.shouldPresentDeleteCommentCompleteToastMessageView = true - - case .setDeleteCommentFamilureToastMessageView: - newState.shouldPresentDeleteCommentFailureToastMessageView = true - - case .setUploadCommentFamilureTaostMessageView: - newState.shouldPresentUploadCommentFailureTaostMessageView = true - - case .setCommentFetchFailureToastMessageView: - newState.shouldPresentCommentFetchFailureTaostMessageView = true - - case let .setHiddenNoCommentView(hidden): - newState.shouldPresentEmptyCommentView = hidden - - case let .setHiddenPaperAirplaneLottieView(hidden): - newState.shouldPresentPaperAirplaneLottieView = hidden - - case .generateErrorHapticNotification: - newState.shouldGenerateErrorHapticNotification = true - - case .scrollToLast: - guard let dataSource = newState.displayComment.first else { - break - } - newState.shouldScrollToLast = dataSource.items.count - 1 - - case .becomeFirstResponseder: - newState.becomeFirstResponder = true - - case let .enableCommentTextField(enabled): - newState.enableCommentTextField = enabled - - case .clearCommentTextField: - newState.shouldClearCommentTextField = true - - case let .setTableViewOffset(height): - newState.tableViewBottomOffset = height - - case .dismiss: - newState.shouldDismiss = true - } - - return newState - } -} diff --git a/14th-team5-iOS/App/Sources/Presentation/PostComment/Strings/PostCommentStrings.swift b/14th-team5-iOS/App/Sources/Presentation/PostComment/Strings/PostCommentStrings.swift deleted file mode 100644 index f4eaf2add..000000000 --- a/14th-team5-iOS/App/Sources/Presentation/PostComment/Strings/PostCommentStrings.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// PostCommentStrings.swift -// App -// -// Created by 김건우 on 1/31/24. -// - -import Foundation - -typealias PostCommentStrings = String.PostComment -extension String { - enum PostComment { } -} - -extension PostCommentStrings { - static let commentDeleteText = "댓글이 삭제되었습니다" - static let commentFetchFailureText = "댓글을 불러오는데 실패했어요" -} diff --git a/14th-team5-iOS/App/Sources/Presentation/PostComment/ViewController/PostCommentViewController.swift b/14th-team5-iOS/App/Sources/Presentation/PostComment/ViewController/PostCommentViewController.swift deleted file mode 100644 index 80b3f933f..000000000 --- a/14th-team5-iOS/App/Sources/Presentation/PostComment/ViewController/PostCommentViewController.swift +++ /dev/null @@ -1,384 +0,0 @@ -// -// PostCommentViewController.swift -// App -// -// Created by 김건우 on 1/18/24. -// - -import Core -import DesignSystem -import UIKit - -import RxCocoa -import RxDataSources -import RxSwift -import Then -import SnapKit - -fileprivate typealias _Str = PostCommentStrings -final public class PostCommentViewController: BaseViewController { - // MARK: - Views - private let commentNavigationBarView: PostCommentTopBarView = PostCommentTopBarView() - - private let noCommentLabel: NoCommentLabel = NoCommentLabel() - private let commentTableView: UITableView = UITableView() - - private let commentTextField: UITextField = UITextField() - private let textFieldContainerView: UIView = UIView() - private let createCommentButton: UIButton = UIButton(type: .system) - - private let bibbiLottieView: AirplaneLottieView = AirplaneLottieView() - private let fetchFailureView: BibbiFetchFailureView = BibbiFetchFailureView(type: .comment) - - // MARK: - Properties - private lazy var dataSource: RxTableViewSectionedAnimatedDataSource = prepareDatasource() - - // MARK: - LifeCycles - public override func viewDidLoad() { - super.viewDidLoad() - } - - public override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - navigationController?.navigationBar.isHidden = true - } - - // MARK: - Helpers - public override func bind(reactor: CommentViewReactor) { - super.bind(reactor: reactor) - bindInput(reactor: reactor) - bindOutput(reactor: reactor) - bindDatasource(reactor: reactor) - } - - private func bindInput(reactor: CommentViewReactor) { - Observable.just(()) - .map { Reactor.Action.fetchPostComment } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - Observable.merge( - commentTableView.rx.tap.asObservable(), - noCommentLabel.rx.tap.asObservable() - ) - .throttle(RxConst.milliseconds300Interval, scheduler: RxSchedulers.main) - .withUnretained(self) - .subscribe { $0.0.commentTextField.resignFirstResponder() } - .disposed(by: disposeBag) - - commentTextField.rx.text.orEmpty - .skip(while: { $0.isEmpty }) - .map { Reactor.Action.inputComment($0) } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - createCommentButton.rx.tap - .throttle(RxConst.milliseconds300Interval, scheduler: RxSchedulers.main) - .withUnretained(self) - .do(onNext: { _ in Haptic.impact(style: .rigid) }) - .map { Reactor.Action.createPostComment($0.0.commentTextField.text) } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - commentTextField.rx.controlEvent(.editingDidEndOnExit) - .throttle(RxConst.milliseconds300Interval, scheduler: RxSchedulers.main) - .withUnretained(self) - .map { Reactor.Action.createPostComment($0.0.commentTextField.text) } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - let keyboardWillShow = NotificationCenter.default.rx.notification(UIResponder.keyboardWillShowNotification) - .flatMap { notification in - guard let value = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else { - return Observable.just(0) - } - return Observable.just(value.cgRectValue.height) - } - - keyboardWillShow - .map { Reactor.Action.keyboardWillShow($0) } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - let keyboardWillHide = NotificationCenter.default.rx.notification(UIResponder.keyboardWillHideNotification) - .flatMap { notification in - return Observable.just(0) - } - - keyboardWillHide - .map { _ in Reactor.Action.keyboardWillHide } - .bind(to: reactor.action) - .disposed(by: disposeBag) - } - - private func bindOutput(reactor: CommentViewReactor) { - reactor.pulse(\.$commentCount) - .map { $0 != 0 } - .bind(to: noCommentLabel.rx.isHidden) - .disposed(by: disposeBag) - - reactor.state.map { $0.inputComment } - .distinctUntilChanged() - .withUnretained(self) - .subscribe { - $0.0.commentTextField.text = $0.1 - if let button = $0.0.commentTextField.rightView as? UIButton { - button.isEnabled = !$0.1.isEmpty - } - } - .disposed(by: disposeBag) - - reactor.pulse(\.$shouldPresentUploadCommentFailureTaostMessageView) - .filter { $0 } - .withUnretained(self) - .subscribe { - $0.0.makeErrorBibbiToastView( - duration: 0.8, - offset: 70, - direction: .down - ) - } - .disposed(by: disposeBag) - - reactor.pulse(\.$shouldPresentDeleteCommentCompleteToastMessageView) - .filter { $0 } - .withUnretained(self) - .subscribe { - $0.0.makeBibbiToastView( - text: _Str.commentDeleteText, - image: DesignSystemAsset.warning.image, - offset: 70, - direction: .down - ) - } - .disposed(by: disposeBag) - - reactor.pulse(\.$shouldPresentDeleteCommentFailureToastMessageView) - .filter { $0 } - .withUnretained(self) - .subscribe { - $0.0.makeErrorBibbiToastView( - duration: 0.8, - offset: 70, - direction: .down - ) - } - .disposed(by: disposeBag) - - reactor.pulse(\.$shouldPresentCommentFetchFailureTaostMessageView) - .filter { $0 } - .delay(RxConst.milliseconds100Interval, scheduler: RxSchedulers.main) - .withUnretained(self) - .subscribe { - $0.0.makeBibbiToastView( - text: _Str.commentFetchFailureText, - image: DesignSystemAsset.warning.image, - offset: 70, - direction: .down - ) - $0.0.fetchFailureView.isHidden = false - $0.0.commentTextField.isUserInteractionEnabled = false - $0.0.commentTextField.rightView?.isUserInteractionEnabled = false - } - .disposed(by: disposeBag) - - reactor.pulse(\.$shouldPresentEmptyCommentView) - .bind(to: noCommentLabel.rx.isHidden) - .disposed(by: disposeBag) - - reactor.pulse(\.$shouldPresentPaperAirplaneLottieView) - .bind(to: bibbiLottieView.rx.isHidden) - .disposed(by: disposeBag) - - reactor.pulse(\.$shouldDismiss) - .filter { $0 } - .bind(with: self, onNext: { owner, _ in - owner.dismiss(animated: true) - }) - .disposed(by: disposeBag) - - reactor.pulse(\.$shouldGenerateErrorHapticNotification) - .filter { $0 } - .subscribe(onNext: { _ in Haptic.notification(type: .error) }) - .disposed(by: disposeBag) - - reactor.state.map { $0.enableCommentTextField } - .distinctUntilChanged() - .withUnretained(self) - .subscribe { - if let button = $0.0.commentTextField.rightView as? UIButton { - button.isEnabled = $0.1 - } - } - .disposed(by: disposeBag) - - reactor.pulse(\.$becomeFirstResponder) - .filter { $0 } - .withUnretained(self) - .subscribe { $0.0.commentTextField.becomeFirstResponder() } - .disposed(by: disposeBag) - - reactor.pulse(\.$shouldClearCommentTextField) - .filter { $0 } - .withUnretained(self) - .subscribe { $0.0.commentTextField.text = String.none } - .disposed(by: disposeBag) - - reactor.pulse(\.$shouldScrollToLast) - .filter { $0 > 0 } - .withUnretained(self) - .subscribe { - let indexPath = IndexPath(row: $0.1, section: 0) - $0.0.commentTableView.scrollToRow( - at: indexPath, - at: .bottom, - animated: true - ) - } - .disposed(by: disposeBag) - - reactor.state.map { $0.tableViewBottomOffset } - .distinctUntilChanged() - .withUnretained(self) - .subscribe { `self`, height in - let safeAreaHeight = `self`.view.safeAreaInsets.bottom - let keyboardHeight = height == .zero ? 0 : (-height + safeAreaHeight) - UIView.animate(withDuration: 1.0) { - `self`.textFieldContainerView.snp.updateConstraints { - $0.bottom.equalTo(self.view.safeAreaLayoutGuide.snp.bottom).offset(keyboardHeight) - } - `self`.view.layoutIfNeeded() - } - } - .disposed(by: disposeBag) - } - - public override func setupUI() { - super.setupUI() - - view.addSubviews(commentNavigationBarView, commentTableView, textFieldContainerView) - commentTableView.addSubviews(bibbiLottieView, noCommentLabel, fetchFailureView) - textFieldContainerView.addSubviews(commentTextField) - } - - public override func setupAutoLayout() { - super.setupAutoLayout() - - commentNavigationBarView.snp.makeConstraints { - $0.height.equalTo(60) - $0.top.equalToSuperview() - $0.horizontalEdges.equalToSuperview() - } - - noCommentLabel.snp.makeConstraints { - $0.top.equalTo(commentNavigationBarView.snp.bottom).offset(74) - $0.bottom.equalTo(textFieldContainerView.snp.top).offset(-5) - $0.horizontalEdges.equalToSuperview() - } - - commentTableView.snp.makeConstraints { - $0.top.equalTo(commentNavigationBarView.snp.bottom) - $0.horizontalEdges.equalToSuperview() - } - - textFieldContainerView.snp.makeConstraints { - $0.height.equalTo(46) - $0.top.equalTo(commentTableView.snp.bottom) - $0.horizontalEdges.equalToSuperview() - $0.bottom.equalTo(view.safeAreaLayoutGuide.snp.bottom).offset(0) - } - - bibbiLottieView.snp.makeConstraints { - $0.centerX.equalToSuperview() - $0.horizontalEdges.equalToSuperview() - $0.top.equalToSuperview().offset(UIScreen.isPhoneSE ? 100 : 140) - } - - fetchFailureView.snp.makeConstraints { - $0.top.equalToSuperview().offset(70) - $0.centerX.equalToSuperview() - } - - commentTextField.snp.makeConstraints { - $0.verticalEdges.equalToSuperview() - $0.horizontalEdges.equalToSuperview().inset(15) - } - } - - public override func setupAttributes() { - super.setupAttributes() - - textFieldContainerView.do { - $0.backgroundColor = UIColor.gray900 - } - - commentTableView.do { - $0.estimatedRowHeight = 250 - $0.rowHeight = UITableView.automaticDimension - $0.allowsSelection = false - $0.separatorStyle = .none - $0.backgroundColor = UIColor.bibbiBlack - $0.contentInset = UIEdgeInsets(top: 10, left: 0, bottom: 0, right: 0) - - $0.register(CommentCell.self, forCellReuseIdentifier: CommentCell.id) - } - - commentTextField.do { - $0.textColor = UIColor.bibbiWhite - $0.attributedPlaceholder = NSAttributedString( - string: "댓글 달기...", - attributes: [.foregroundColor: UIColor.gray300] - ) - $0.backgroundColor = UIColor.clear - $0.rightView = createCommentButton - $0.rightViewMode = .always - $0.returnKeyType = .done - } - - createCommentButton.do { - $0.isEnabled = false - $0.setTitle("등록", for: .normal) - $0.tintColor = UIColor.mainYellow - } - - noCommentLabel.do { - $0.isHidden = true - } - - fetchFailureView.do { - $0.isHidden = true - } - } -} - -extension PostCommentViewController { - private func prepareDatasource() -> RxTableViewSectionedAnimatedDataSource { - return RxTableViewSectionedAnimatedDataSource { dataSource, tableView, indexPath, reactor in - let cell = tableView.dequeueReusableCell(withIdentifier: CommentCell.id) as! CommentCell - cell.reactor = reactor - return cell - } - } - - private func bindDatasource(reactor: CommentViewReactor) { - dataSource.canEditRowAtIndexPath = { dataSource, indexPath in - let myMemberId = App.Repository.member.memberID.value - let cellMemberId = dataSource[indexPath].currentState.memberId - return myMemberId == cellMemberId - } - - reactor.state.map { $0.displayComment } - .distinctUntilChanged() - .bind(to: commentTableView.rx.items(dataSource: dataSource)) - .disposed(by: disposeBag) - - commentTableView.rx.itemDeleted - .withUnretained(self) - .map { - let commentId = $0.0.dataSource[$0.1].currentState.commentId - return Reactor.Action.deletePostComment(commentId) - } - .bind(to: reactor.action) - .disposed(by: disposeBag) - } -} diff --git a/14th-team5-iOS/App/Sources/Presentation/PostComment/Dependency/PostCommentDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/Trash/Dependencygg/PostCommentDIContainer.swift similarity index 91% rename from 14th-team5-iOS/App/Sources/Presentation/PostComment/Dependency/PostCommentDIContainer.swift rename to 14th-team5-iOS/App/Sources/Presentation/Trash/Dependencygg/PostCommentDIContainer.swift index 329dfc2c9..dfeab6790 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostComment/Dependency/PostCommentDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Trash/Dependencygg/PostCommentDIContainer.swift @@ -28,8 +28,8 @@ public final class PostCommentDIContainer { } // MARK: - Make - public func makeViewController() -> PostCommentViewController { - return PostCommentViewController(reactor: makeReactor()) + public func makeViewController() -> CommentViewController { + return CommentViewController(reactor: makeReactor()) } public func makeMemberRepository() -> MemberRepositoryProtocol { diff --git a/14th-team5-iOS/Core/Sources/Base/BaseTableView.swift b/14th-team5-iOS/Core/Sources/Base/BaseTableView.swift index 2300d853a..62a9abd54 100644 --- a/14th-team5-iOS/Core/Sources/Base/BaseTableView.swift +++ b/14th-team5-iOS/Core/Sources/Base/BaseTableView.swift @@ -11,17 +11,20 @@ import ReactorKit import RxSwift open class BaseTableView: UITableView, ReactorKit.View where R: Reactor { + public typealias Reactor = R // MARK: - Properties + private var initialReactor: Reactor? + public var disposeBag: RxSwift.DisposeBag = DisposeBag() // MARK: - Intializer public convenience init(reactor: Reactor? = nil) { - self.init(frame: .zero) - self.reactor = reactor + self.init(frame: .zero, style: .plain) + self.initialReactor = reactor } public override init(frame: CGRect, style: UITableView.Style) { @@ -29,6 +32,7 @@ open class BaseTableView: UITableView, ReactorKit.View where R: Reactor { setupUI() setupAutoLayout() setupAttributes() + setupReactor() } public required init?(coder: NSCoder) { @@ -48,4 +52,8 @@ open class BaseTableView: UITableView, ReactorKit.View where R: Reactor { // 뷰의 속성 설정을 위한 메서드 open func setupAttributes() { } + + open func setupReactor() { + self.reactor = initialReactor + } } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastConfiguration.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastConfiguration.swift index 331309e5b..8f44f55e6 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastConfiguration.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastConfiguration.swift @@ -53,16 +53,16 @@ private extension BBToastConfiguration { private static func defaultEnteringAnimation(with direction: BBToast.Direction) -> BBToast.Animation { switch direction { - case .top: + case let .top(yOffset): return .custom( - transformation: CGAffineTransform(scaleX: 0.9, y: 0.9).translatedBy(x: 0, y: -100) + transformation: CGAffineTransform(scaleX: 0.9, y: 0.9).translatedBy(x: 0, y: -yOffset - 100) ) - case .bottom: + case let .bottom(yOffset): return .custom( - transformation: CGAffineTransform(scaleX: 0.9, y: 0.9).translatedBy(x: 0, y: 100) + transformation: CGAffineTransform(scaleX: 0.9, y: 0.9).translatedBy(x: 0, y: +yOffset + 100) ) - + case .center: return .custom( transformation: CGAffineTransform(scaleX: 0.5, y: 0.5) diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/DefaultToastView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/DefaultToastView.swift index e31b47c79..1e7299edf 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/DefaultToastView.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/DefaultToastView.swift @@ -69,6 +69,7 @@ public class DefaultToastView: UIView, BBToastView { case let .top(yOffset): topAnchor.constraint(equalTo: superview.layoutMarginsGuide.topAnchor, constant: yOffset).isActive = true case let .center(xOffset, yOffset): + centerXAnchor.constraint(equalTo: superview.layoutMarginsGuide.centerXAnchor, constant: xOffset).isActive = true centerYAnchor.constraint(equalTo: superview.layoutMarginsGuide.centerYAnchor, constant: yOffset).isActive = true } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ManagementService.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ManagementService.swift index 9d4d33ace..e36e9daab 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ManagementService.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ManagementService.swift @@ -11,11 +11,6 @@ import RxSwift public enum ManagementEvent { case didTapCopyUrlAction - case hiddenSharingProgressHud(hidden: Bool) - case hiddenTableProgressHud(hidden: Bool) - case hiddenMemberFetchFailureView(hidden: Bool) - case setTableHeaderInfo(familyName: String, memberCount: Int) - case endTableRefreshing } public protocol ManagementServiceType { @@ -23,16 +18,6 @@ public protocol ManagementServiceType { @discardableResult func didTapCopUrlAction() -> Observable - @discardableResult - func hiddenSharingProgressHud(hidden: Bool) -> Observable - @discardableResult - func hiddenTableProgressHud(hidden: Bool) -> Observable - @discardableResult - func hiddenMemberFetchFailureView(hidden: Bool) -> Observable - @discardableResult - func setTableHeaderInfo(familyName: String, memberCount: Int) -> Observable<(String, Int)> - @discardableResult - func endTableRefreshing() -> Observable } final public class ManagementService: BaseService, ManagementServiceType { @@ -44,30 +29,5 @@ final public class ManagementService: BaseService, ManagementServiceType { return Observable.just(()) } - public func hiddenSharingProgressHud(hidden: Bool) -> Observable { - event.onNext(.hiddenSharingProgressHud(hidden: hidden)) - return Observable.just(hidden) - } - - public func hiddenTableProgressHud(hidden: Bool) -> Observable { - event.onNext(.hiddenTableProgressHud(hidden: hidden)) - return Observable.just(hidden) - } - - public func hiddenMemberFetchFailureView(hidden: Bool) -> Observable { - event.onNext(.hiddenMemberFetchFailureView(hidden: hidden)) - return Observable.just(hidden) - } - - public func setTableHeaderInfo(familyName: String, memberCount: Int) -> Observable<(String, Int)> { - event.onNext(.setTableHeaderInfo(familyName: familyName, memberCount: memberCount)) - return Observable<(String, Int)>.just((familyName, memberCount)) - } - - public func endTableRefreshing() -> Observable { - event.onNext(.endTableRefreshing) - return Observable.just(()) - } - } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ServiceProvider.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ServiceProvider.swift index 7616bb9a9..a578cf896 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ServiceProvider.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ServiceProvider.swift @@ -8,6 +8,7 @@ import Foundation public protocol ServiceProviderProtocol: AnyObject { + var mainService: MainServiceType { get } var managementService: ManagementServiceType { get } var postGlobalState: PostGlobalStateType { get } @@ -17,14 +18,13 @@ public protocol ServiceProviderProtocol: AnyObject { var timerGlobalState: TimerGlobalStateType { get } var realEmojiGlobalState: RealEmojiGlobalStateType { get } var profilePageGlobalState: ProfileFeedGlobalStateType { get } - var mainService: MainServiceType { get } } final public class ServiceProvider: ServiceProviderProtocol { + public lazy var mainService: MainServiceType = MainService(provider: self) public lazy var managementService: any ManagementServiceType = ManagementService(provider: self) - public lazy var postGlobalState: PostGlobalStateType = PostGlobalState(provider: self) public lazy var calendarGlabalState: CalendarGlobalStateType = CalendarGlobalState(provider: self) public lazy var toastGlobalState: ToastMessageGlobalStateType = ToastMessageGlobalState(provider: self) @@ -34,7 +34,6 @@ final public class ServiceProvider: ServiceProviderProtocol { public lazy var realEmojiGlobalState: RealEmojiGlobalStateType = RealEmojiGlobalState(provider: self) public lazy var profilePageGlobalState: ProfileFeedGlobalStateType = ProfileFeedGlobalState(provider: self) - public lazy var mainService: MainServiceType = MainService(provider: self) public init() { } } diff --git a/14th-team5-iOS/Core/Sources/Extensions/UIViewController+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/UIViewController+Ext.swift index b09eef584..c49dea07d 100644 --- a/14th-team5-iOS/Core/Sources/Extensions/UIViewController+Ext.swift +++ b/14th-team5-iOS/Core/Sources/Extensions/UIViewController+Ext.swift @@ -12,6 +12,8 @@ import SnapKit import Then extension UIViewController { + + @available(*, deprecated, message: "BBToast를 사용하세요.") public enum ToastDirection { case up case down @@ -147,41 +149,6 @@ extension UIViewController { } } -extension UIViewController { - - @available(*, deprecated, message: "Navigator 안에서 호출하세요.") - public func makeSharePanel( - _ activityItemSources: [UIActivityItemSource], - activities: [UIActivity], - excludedActivityTypes: [UIActivity.ActivityType] = [.addToReadingList, .copyToPasteboard] - ) { - let items: [Any] = activityItemSources - let activityVC = UIActivityViewController( - activityItems: items, - applicationActivities: activities - ) - activityVC.excludedActivityTypes = excludedActivityTypes - present(activityVC, animated: true) - } - - /// 친구 초대 공유 시트를 보여줍니다. - /// - Parameters: - /// - url: 공유할 URL - /// - globalState: GlobalState (선택) - /// - @available(*, deprecated, message: "Navigator 안에서 호출하세요.") - public func makeInvitationUrlSharePanel(_ url: URL?, provider globalState: ServiceProviderProtocol? = nil) { - guard let url = url else { return } - let itemSource = UrlActivityItemSource( - title: "삐삐! 가족에게 보내는 하루 한 번 생존 신고", - url: url - ) - let copyToPastboard = CopyInvitationUrlActivity(url) - - makeSharePanel([itemSource], activities: [copyToPastboard]) - } -} - extension UIViewController { /// 전달한 뷰 컨트롤러를 팝오버로 보여줍니다. /// diff --git a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIWorker.swift index 84fa2d44a..e8f84bf64 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIWorker.swift @@ -34,7 +34,7 @@ extension FamilyAPIWorker { // MARK: - Join Family public func joinFamily(body: JoinFamilyRequestDTO) -> Single { - let spec = MeAPIs.joinFamily.spec + let spec = FamilyAPIs.joinFamily.spec return request(spec: spec, jsonEncodable: body) .subscribe(on: Self.queue) From 806dd491b7b7ebada59d79de2933a7c89c2c1a40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=84?= Date: Mon, 16 Sep 2024 12:30:27 +0900 Subject: [PATCH 207/263] =?UTF-8?q?feat:=20=EC=98=A8=20=EB=B3=B4=EB=94=A9?= =?UTF-8?q?=20=ED=99=94=EB=A9=B4=20Skip=20=EC=B2=98=EB=A6=AC=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84=20(#630)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../App/Sources/Application/AppDelegate.swift | 5 +- .../DIContainer/AppDIContainer.swift | 3 + .../DIContainer/MyDIContainer.swift | 20 ++++++ .../DIContainer/NavigatorDIContainer.swift | 34 ++++++++- .../DIContainer/PrivacyDIContainer.swift | 38 ++++++++++ .../DIContainer/ResignDIContainer.swift | 32 +++++++++ .../DIContainer/SignOutDIContainer.swift | 32 +++++++++ .../Navigator/AccountProfileNavigator.swift | 31 ++++++++ .../Navigator/AccountResignNavigator.swift | 29 ++++++++ .../Navigator/AccountSignInNavigator.swift | 34 +++++++++ .../Navigator/JoinFamilyNavigator.swift | 29 ++++++++ .../Navigator/ManagementNavigator.swift | 3 +- .../Navigator/OnboardingNavigator.swift | 37 ++++++++++ .../Navigator/PrivacyNavigator.swift | 37 ++++++++++ .../PrivacyViewControllerWrapper.swift | 38 ++++++++++ .../AccountResignViewControllerWrapper.swift | 32 +++++++++ .../AccountSignIn/AccountSignInReactor.swift | 32 ++++++--- .../AccountSignInViewController.swift | 36 +++++----- .../Reactor/AccountSignUpReactor.swift | 11 ++- .../AccountProfileViewController.swift | 7 +- .../AccountSignUpViewController.swift | 6 -- .../JoinFamilyViewController.swift | 6 +- .../OnBoarding/OnBoardingReactor.swift | 3 +- .../OnBoarding/OnBoardingViewController.swift | 11 ++- .../Dependency/PrivacyDIContainer.swift | 47 ------------ .../Privacy/PrivacyViewController.swift | 24 +++---- .../Privacy/Reactor/PrivacyViewReactor.swift | 46 ++++++------ .../Profile/ProfileViewController.swift | 2 +- .../Resign/AccountResignViewCotroller.swift | 21 ++---- .../Dependency/AccountResignDIContainer.swift | 33 --------- .../Reactor/AccountResignViewReactor.swift | 17 +++-- .../Splash/SplashViewController.swift | 4 -- .../Sources/Extensions/Notification+Ext.swift | 1 - .../Sources/APIs/App/AppAPI/AppAPIs.swift | 2 +- .../MembersProfileResponseDTO.swift | 0 .../Members/MemberAPI/MembersAPIWorker.swift | 10 +++ .../Members/MemberAPI/MembersAPIs.swift | 3 + .../Repositories/MembersRepository.swift | 1 - .../APIs/My/Repository/MyRepository.swift | 11 ++- .../Repository/PrivacyRepository.swift | 38 ++++++++++ .../AccountResignResponseDTO.swift | 19 +++++ .../Repository/AccountResignRepository.swift | 28 ++++++++ .../Resign/ResignAPI/ResignAPIWorker.swift | 6 +- .../Resign/ResignAPI/ResignAPIs.swift | 0 .../Trash/Account/MeAPI/MeAPIWorker.swift | 19 ----- .../Privacy/DataMapping/BibbiAppInfoDTO.swift | 28 -------- .../Privacy/PrivacyAPI/PrivacyAPIWorker.swift | 61 ---------------- .../Privacy/PrivacyAPI/PrivacyAPIs.swift | 27 ------- .../Repositories/PrivacyViewRepository.swift | 72 ------------------- .../DataMapping/AccountFamilyResignDTO.swift | 20 ------ .../DataMapping/AccountFcmResignDTO.swift | 19 ----- .../Resign/DataMapping/AccountResignDTO.swift | 19 ----- .../AccountResignViewRepository.swift | 30 -------- .../Members}/MembersProfileEntity.swift | 0 .../Members}/ProfileImageEditParameter.swift | 1 + .../Resign/AccountResignEntity.swift} | 6 +- .../AccountResignRepository.swift | 14 ++++ .../MembersRepository.swift} | 6 +- .../Sources/Repositories/MyRepository.swift | 2 + .../Repositories/PrivacyRepository.swift | 17 +++++ .../Repositories/JoinFamilyRepository.swift | 1 - .../Privacy/Entity/BibbiAppInfoResponse.swift | 21 ------ .../Interfaces/PrivacyViewInterface.swift | 21 ------ .../Parameters/BibbiAppInfoParameter.swift | 17 ----- .../Privacy/UseCases/PrivacyViewUseCase.swift | 49 ------------- .../Entity/AccountFamilyResignResponse.swift | 16 ----- .../Entity/AccountFcmResignResponse.swift | 17 ----- .../Interfaces/AccountResignInterface.swift | 16 ----- .../UseCases/AccountResignUseCase.swift | 27 ------- .../SignOut/UseCases/SignOutUseCase.swift | 2 + .../Members/DeleteMembersProfileUseCase.swift | 0 .../Members/FetchMembersPostListUseCase.swift | 0 .../Members/FetchMembersProfileUseCase.swift | 0 .../Members/UpdateMembersProfileUseCase.swift | 0 .../My/FetchIsFirstOnboardingUseCase.swift | 27 +++++++ .../My/UpdateIsFirstOnboardingUseCase.swift | 26 +++++++ .../FetchAuthorizationItemsUseCase.swift | 29 ++++++++ .../Privacy/FetchPrivacyItemsUseCase.swift | 27 +++++++ .../Resign/DeleteAccountResignUseCase.swift | 27 +++++++ 79 files changed, 830 insertions(+), 693 deletions(-) create mode 100644 14th-team5-iOS/App/Sources/Application/DIContainer/PrivacyDIContainer.swift create mode 100644 14th-team5-iOS/App/Sources/Application/DIContainer/ResignDIContainer.swift create mode 100644 14th-team5-iOS/App/Sources/Application/DIContainer/SignOutDIContainer.swift create mode 100644 14th-team5-iOS/App/Sources/Application/Navigator/AccountProfileNavigator.swift create mode 100644 14th-team5-iOS/App/Sources/Application/Navigator/AccountResignNavigator.swift create mode 100644 14th-team5-iOS/App/Sources/Application/Navigator/AccountSignInNavigator.swift create mode 100644 14th-team5-iOS/App/Sources/Application/Navigator/JoinFamilyNavigator.swift create mode 100644 14th-team5-iOS/App/Sources/Application/Navigator/OnboardingNavigator.swift create mode 100644 14th-team5-iOS/App/Sources/Application/Navigator/PrivacyNavigator.swift create mode 100644 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Privacy/PrivacyViewControllerWrapper.swift create mode 100644 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Resign/AccountResignViewControllerWrapper.swift delete mode 100644 14th-team5-iOS/App/Sources/Presentation/Privacy/Dependency/PrivacyDIContainer.swift delete mode 100644 14th-team5-iOS/App/Sources/Presentation/Resign/Dependency/AccountResignDIContainer.swift rename 14th-team5-iOS/Data/Sources/{Trash => APIs}/Members/DataMapping/MembersProfileResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/{Trash => APIs}/Members/MemberAPI/MembersAPIWorker.swift (89%) rename 14th-team5-iOS/Data/Sources/{Trash => APIs}/Members/MemberAPI/MembersAPIs.swift (87%) rename 14th-team5-iOS/Data/Sources/{Trash => APIs}/Members/Repositories/MembersRepository.swift (99%) create mode 100644 14th-team5-iOS/Data/Sources/APIs/Privacy/Repository/PrivacyRepository.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/Resign/DataMapping/AccountResignResponseDTO.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/Resign/Repository/AccountResignRepository.swift rename 14th-team5-iOS/Data/Sources/{Trash => APIs}/Resign/ResignAPI/ResignAPIWorker.swift (72%) rename 14th-team5-iOS/Data/Sources/{Trash => APIs}/Resign/ResignAPI/ResignAPIs.swift (100%) delete mode 100644 14th-team5-iOS/Data/Sources/Trash/Privacy/DataMapping/BibbiAppInfoDTO.swift delete mode 100644 14th-team5-iOS/Data/Sources/Trash/Privacy/PrivacyAPI/PrivacyAPIWorker.swift delete mode 100644 14th-team5-iOS/Data/Sources/Trash/Privacy/PrivacyAPI/PrivacyAPIs.swift delete mode 100644 14th-team5-iOS/Data/Sources/Trash/Privacy/Repositories/PrivacyViewRepository.swift delete mode 100644 14th-team5-iOS/Data/Sources/Trash/Resign/DataMapping/AccountFamilyResignDTO.swift delete mode 100644 14th-team5-iOS/Data/Sources/Trash/Resign/DataMapping/AccountFcmResignDTO.swift delete mode 100644 14th-team5-iOS/Data/Sources/Trash/Resign/DataMapping/AccountResignDTO.swift delete mode 100644 14th-team5-iOS/Data/Sources/Trash/Resign/Repositories/AccountResignViewRepository.swift rename 14th-team5-iOS/Domain/Sources/{Trash/Members/Entities => Entities/Members}/MembersProfileEntity.swift (100%) rename 14th-team5-iOS/Domain/Sources/{Trash/Members/Parameters => Entities/Members}/ProfileImageEditParameter.swift (89%) rename 14th-team5-iOS/Domain/Sources/{Trash/Resign/Entity/AccountResignResponse.swift => Entities/Resign/AccountResignEntity.swift} (59%) create mode 100644 14th-team5-iOS/Domain/Sources/Repositories/AccountResignRepository.swift rename 14th-team5-iOS/Domain/Sources/{Trash/Members/Interfaces/MembersRepositoryProtocol.swift => Repositories/MembersRepository.swift} (86%) create mode 100644 14th-team5-iOS/Domain/Sources/Repositories/PrivacyRepository.swift delete mode 100644 14th-team5-iOS/Domain/Sources/Trash/Privacy/Entity/BibbiAppInfoResponse.swift delete mode 100644 14th-team5-iOS/Domain/Sources/Trash/Privacy/Interfaces/PrivacyViewInterface.swift delete mode 100644 14th-team5-iOS/Domain/Sources/Trash/Privacy/Parameters/BibbiAppInfoParameter.swift delete mode 100644 14th-team5-iOS/Domain/Sources/Trash/Privacy/UseCases/PrivacyViewUseCase.swift delete mode 100644 14th-team5-iOS/Domain/Sources/Trash/Resign/Entity/AccountFamilyResignResponse.swift delete mode 100644 14th-team5-iOS/Domain/Sources/Trash/Resign/Entity/AccountFcmResignResponse.swift delete mode 100644 14th-team5-iOS/Domain/Sources/Trash/Resign/Interfaces/AccountResignInterface.swift delete mode 100644 14th-team5-iOS/Domain/Sources/Trash/Resign/UseCases/AccountResignUseCase.swift rename 14th-team5-iOS/Domain/Sources/{Trash/Members => }/UseCases/Members/DeleteMembersProfileUseCase.swift (100%) rename 14th-team5-iOS/Domain/Sources/{Trash/Members => }/UseCases/Members/FetchMembersPostListUseCase.swift (100%) rename 14th-team5-iOS/Domain/Sources/{Trash/Members => }/UseCases/Members/FetchMembersProfileUseCase.swift (100%) rename 14th-team5-iOS/Domain/Sources/{Trash/Members => }/UseCases/Members/UpdateMembersProfileUseCase.swift (100%) create mode 100644 14th-team5-iOS/Domain/Sources/UseCases/My/FetchIsFirstOnboardingUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/UseCases/My/UpdateIsFirstOnboardingUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/UseCases/Privacy/FetchAuthorizationItemsUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/UseCases/Privacy/FetchPrivacyItemsUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/UseCases/Resign/DeleteAccountResignUseCase.swift diff --git a/14th-team5-iOS/App/Sources/Application/AppDelegate.swift b/14th-team5-iOS/App/Sources/Application/AppDelegate.swift index 69421c73b..b9a606ad1 100644 --- a/14th-team5-iOS/App/Sources/Application/AppDelegate.swift +++ b/14th-team5-iOS/App/Sources/Application/AppDelegate.swift @@ -69,7 +69,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate { PickDIContainer(), MissionDIContainer(), MemberDIContainer(), - MyDIContainer() + MyDIContainer(), + SignOutDIContainer(), + PrivacyDIContainer(), + ResignDIContainer() ] containers.forEach { $0.registerDependencies() diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/AppDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/AppDIContainer.swift index 582387cc1..525a31e12 100644 --- a/14th-team5-iOS/App/Sources/Application/DIContainer/AppDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/AppDIContainer.swift @@ -40,6 +40,9 @@ final class AppDIContainer: BaseContainer { return ServiceProvider() } + container.register(type: AppUserDefaultsType.self) { _ in + return AppUserDefaults() + } } } diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/MyDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/MyDIContainer.swift index 35ba2c326..e40337e24 100644 --- a/14th-team5-iOS/App/Sources/Application/DIContainer/MyDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/MyDIContainer.swift @@ -49,6 +49,18 @@ final class MyDIContainer: BaseContainer { ) } + private func makeFetchIsFirstOnboardingUseCase() -> FetchIsFirstOnboardingUseCaseProtocol { + return FetchIsFirstOnboardingUseCase( + myRepository: makeMyRepository() + ) + } + + private func makeUpdateIsFirstOnboardingUseCase() -> UpdateIsFirstOnboardingUseCaseProtocol { + return UpdateIsFirstOnboardingUseCase( + myRepository: makeMyRepository() + ) + } + // MARK: - Make Repository @@ -84,6 +96,14 @@ final class MyDIContainer: BaseContainer { self.makeCheckIsVaildMemberUseCase() } + container.register(type: FetchIsFirstOnboardingUseCaseProtocol.self) { _ in + return self.makeFetchIsFirstOnboardingUseCase() + } + + container.register(type: UpdateIsFirstOnboardingUseCaseProtocol.self) { _ in + return self.makeUpdateIsFirstOnboardingUseCase() + } + } } diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/NavigatorDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/NavigatorDIContainer.swift index c3b18560d..fc3414d73 100644 --- a/14th-team5-iOS/App/Sources/Application/DIContainer/NavigatorDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/NavigatorDIContainer.swift @@ -37,8 +37,14 @@ final class NavigatorDIContainer: BaseContainer { ) } - container.register(type: HomeNavigatorProtocol.self) { _ in - HomeNavigator( + container.register(type: AccountSignInNavigatorProtocol.self) { _ in + AccountSignInNavigator( + navigationController: makeUINavigationController() + ) + } + + container.register(type: AccountProfileNavigatorProtocol.self) { _ in + AccountProfileNavigator( navigationController: makeUINavigationController() ) } @@ -70,6 +76,30 @@ final class NavigatorDIContainer: BaseContainer { container.register(type: FamilyEntranceNavigatorProtocol.self) { _ in FamilyEntranceNavigator(navigationController: makeUINavigationController()) } + + container.register(type: JoinFamilyNavigatorProtocol.self) { _ in + JoinFamilyNavigator( + navigationController: makeUINavigationController() + ) + } + + container.register(type: OnboardingNavigatorProtocol.self) { _ in + OnboardingNavigator( + navigationController: makeUINavigationController() + ) + } + + container.register(type: PrivacyNavigatorProtocol.self) { _ in + PrivacyNavigator( + navigationController: makeUINavigationController() + ) + } + + container.register(type: AccountResignNavigatorProtocol.self) { _ in + AccountResignNavigator( + navigationController: makeUINavigationController() + ) + } } } diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/PrivacyDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/PrivacyDIContainer.swift new file mode 100644 index 000000000..731820ee6 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/PrivacyDIContainer.swift @@ -0,0 +1,38 @@ +// +// PrivacyDIContainer.swift +// App +// +// Created by Kim dohyun on 9/11/24. +// + +import Core +import Data +import Domain +import MacrosInterface + + + +final class PrivacyDIContainer: BaseContainer { + + + private func makeRepository() -> PrivacyRepositoryProtocol { + return PrivacyRepository() + } + + private func makeFetchAuthorizationItemUseCase() -> FetchAuthorizationItemsUseCaseProtocol { + return FetchAuthorizationItemsUseCase(privacyRepository: makeRepository()) + } + + private func makeFetchPrivacyItemsUseCase() -> FetchPrivacyItemsUseCaseProtocol { + return FetchPrivacyItemsUseCase(privacyRepository: makeRepository()) + } + func registerDependencies() { + container.register(type: FetchAuthorizationItemsUseCaseProtocol.self) { _ in + makeFetchAuthorizationItemUseCase() + } + + container.register(type: FetchPrivacyItemsUseCaseProtocol.self) { _ in + makeFetchPrivacyItemsUseCase() + } + } +} diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/ResignDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/ResignDIContainer.swift new file mode 100644 index 000000000..f79cf9807 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/ResignDIContainer.swift @@ -0,0 +1,32 @@ +// +// ResignDIContainer.swift +// App +// +// Created by Kim dohyun on 9/12/24. +// + +import Core +import Data +import Domain + + +final class ResignDIContainer: BaseContainer { + + private func makeResignRepository() -> AccountResignRepositoryProtocol { + return AccountResignRepository() + } + + private func makeDeleteAccountResignUseCaseProtocol() -> DeleteAccountResignUseCaseProtocol { + return DeleteAccountResignUseCase( + accountResignRepository: makeResignRepository() + ) + } + + func registerDependencies() { + container.register(type: DeleteAccountResignUseCaseProtocol.self) { _ in + makeDeleteAccountResignUseCaseProtocol() + } + + } + +} diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/SignOutDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/SignOutDIContainer.swift new file mode 100644 index 000000000..f870039a5 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/SignOutDIContainer.swift @@ -0,0 +1,32 @@ +// +// SignOutDIContainer.swift +// App +// +// Created by Kim dohyun on 9/11/24. +// + +import Core +import Data +import Domain + + +final class SignOutDIContainer: BaseContainer { + private func makeSignOutUseCase() -> SignOutUseCaseProtocol { + return SignOutUseCase( + keychainRepository: KeychainRepository.shared, + userDefaultsRepository: UserDefaultsRepository.shared, + fcmRepository: makeFCMRepository() + ) + } + + private func makeFCMRepository() -> MeAPIs.Worker { + return MeAPIs.Worker() + } + + func registerDependencies() { + container.register(type: SignOutUseCaseProtocol.self) { _ in + makeSignOutUseCase() + } + } + +} diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/AccountProfileNavigator.swift b/14th-team5-iOS/App/Sources/Application/Navigator/AccountProfileNavigator.swift new file mode 100644 index 000000000..a32f1f67d --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/Navigator/AccountProfileNavigator.swift @@ -0,0 +1,31 @@ +// +// AccountProfileNavigator.swift +// App +// +// Created by Kim dohyun on 9/12/24. +// + +import Core +import UIKit + + +protocol AccountProfileNavigatorProtocol: BaseNavigator { + func toOnboarding() +} + + + +final class AccountProfileNavigator: AccountProfileNavigatorProtocol { + + var navigationController: UINavigationController + + + init(navigationController: UINavigationController) { + self.navigationController = navigationController + } + + func toOnboarding() { + let vc = OnboardingViewControllerWrapper().viewController + navigationController.setViewControllers([vc], animated: false) + } +} diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/AccountResignNavigator.swift b/14th-team5-iOS/App/Sources/Application/Navigator/AccountResignNavigator.swift new file mode 100644 index 000000000..ae3bc62d6 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/Navigator/AccountResignNavigator.swift @@ -0,0 +1,29 @@ +// +// AccountResignNavigator.swift +// App +// +// Created by Kim dohyun on 9/12/24. +// + +import Core +import UIKit + +protocol AccountResignNavigatorProtocol: BaseNavigator { + func toSignIn() +} + + +final class AccountResignNavigator: AccountResignNavigatorProtocol { + + var navigationController: UINavigationController + + + init(navigationController: UINavigationController) { + self.navigationController = navigationController + } + + func toSignIn() { + let vc = SignInViewControllerWrapper().viewController + navigationController.setViewControllers([vc], animated: false) + } +} diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/AccountSignInNavigator.swift b/14th-team5-iOS/App/Sources/Application/Navigator/AccountSignInNavigator.swift new file mode 100644 index 000000000..3b18bdda2 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/Navigator/AccountSignInNavigator.swift @@ -0,0 +1,34 @@ +// +// AccountSignInNavigator.swift +// App +// +// Created by Kim dohyun on 9/12/24. +// + +import Core +import UIKit + + +protocol AccountSignInNavigatorProtocol: BaseNavigator { + func toMain() + func toSignUp() +} + +final class AccountSignInNavigator: AccountSignInNavigatorProtocol { + + var navigationController: UINavigationController + + init(navigationController: UINavigationController) { + self.navigationController = navigationController + } + + func toMain() { + let vc = MainViewControllerWrapper().viewController + navigationController.setViewControllers([vc], animated: false) + } + + func toSignUp() { + let vc = AccountSignUpDIContainer().makeViewController() + navigationController.setViewControllers([vc], animated: false) + } +} diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/JoinFamilyNavigator.swift b/14th-team5-iOS/App/Sources/Application/Navigator/JoinFamilyNavigator.swift new file mode 100644 index 000000000..bd0f3eab2 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/Navigator/JoinFamilyNavigator.swift @@ -0,0 +1,29 @@ +// +// JoinFamilyNavigator.swift +// App +// +// Created by Kim dohyun on 9/12/24. +// + +import Core +import UIKit + + +protocol JoinFamilyNavigatorProtocol: BaseNavigator { + func toMain() +} + + +final class JoinFamilyNavigator: JoinFamilyNavigatorProtocol { + + var navigationController: UINavigationController + + init(navigationController: UINavigationController) { + self.navigationController = navigationController + } + + func toMain() { + let vc = MainViewControllerWrapper().viewController + navigationController.setViewControllers([vc], animated: false) + } +} diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/ManagementNavigator.swift b/14th-team5-iOS/App/Sources/Application/Navigator/ManagementNavigator.swift index d0aa79c1e..9f404b53d 100644 --- a/14th-team5-iOS/App/Sources/Application/Navigator/ManagementNavigator.swift +++ b/14th-team5-iOS/App/Sources/Application/Navigator/ManagementNavigator.swift @@ -41,8 +41,7 @@ final class ManagementNavigator: ManagementNavigatorProtocol { } func toSetting(memberId: String) { - // TODO: - Wrapper로 바꾸기 - let vc = PrivacyDIContainer(memberId: memberId).makeViewController() + let vc = PrivacyViewControllerWrapper(memberId: memberId).makeViewController() navigationController.pushViewController(vc, animated: true) } diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/OnboardingNavigator.swift b/14th-team5-iOS/App/Sources/Application/Navigator/OnboardingNavigator.swift new file mode 100644 index 000000000..4a55803c7 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/Navigator/OnboardingNavigator.swift @@ -0,0 +1,37 @@ +// +// OnboardingNavigator.swift +// App +// +// Created by Kim dohyun on 9/11/24. +// + +import Core +import UIKit + + +protocol OnboardingNavigatorProtocol: BaseNavigator { + func toJoinFamily() + func toMain() +} + + +final class OnboardingNavigator: OnboardingNavigatorProtocol { + + var navigationController: UINavigationController + + init(navigationController: UINavigationController) { + self.navigationController = navigationController + } + + func toJoinFamily() { + let vc = JoinFamilyViewControllerWrapper().viewController + navigationController.setViewControllers([vc], animated: false) + + } + + func toMain() { + let vc = MainViewControllerWrapper().viewController + navigationController.setViewControllers([vc], animated: false) + } + +} diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/PrivacyNavigator.swift b/14th-team5-iOS/App/Sources/Application/Navigator/PrivacyNavigator.swift new file mode 100644 index 000000000..12b4ea8a6 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/Navigator/PrivacyNavigator.swift @@ -0,0 +1,37 @@ +// +// PrivacyNavigator.swift +// App +// +// Created by Kim dohyun on 9/11/24. +// + +import Core +import UIKit + + +protocol PrivacyNavigatorProtocol: BaseNavigator { + func toJoinFamily() + func toSignIn() +} + + +final class PrivacyNavigator: PrivacyNavigatorProtocol { + + var navigationController: UINavigationController + + init(navigationController: UINavigationController) { + self.navigationController = navigationController + } + + func toJoinFamily() { + let vc = JoinFamilyViewControllerWrapper().viewController + navigationController.setViewControllers([vc], animated: false) + } + + func toSignIn() { + let vc = SignInViewControllerWrapper().viewController + navigationController.setViewControllers([vc], animated: false) + } + +} + diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Privacy/PrivacyViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Privacy/PrivacyViewControllerWrapper.swift new file mode 100644 index 000000000..524447823 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Privacy/PrivacyViewControllerWrapper.swift @@ -0,0 +1,38 @@ +// +// PrivacyViewControllerWrapper.swift +// App +// +// Created by Kim dohyun on 9/11/24. +// + +import Core +import Foundation + + +final class PrivacyViewControllerWrapper: BaseWrapper { + typealias R = PrivacyViewReactor + typealias V = PrivacyViewController + + private let memberId: String + + public init(memberId: String) { + self.memberId = memberId + } + + var reactor: PrivacyViewReactor { + return makeReactor() + } + + var viewController: PrivacyViewController { + return makeViewController() + } + + func makeViewController() -> PrivacyViewController { + PrivacyViewController(reactor: makeReactor()) + } + + func makeReactor() -> PrivacyViewReactor { + return PrivacyViewReactor(memberId: memberId) + } +} + diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Resign/AccountResignViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Resign/AccountResignViewControllerWrapper.swift new file mode 100644 index 000000000..b88ce9030 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Resign/AccountResignViewControllerWrapper.swift @@ -0,0 +1,32 @@ +// +// AccountResignViewControllerWrapper.swift +// App +// +// Created by Kim dohyun on 9/12/24. +// + +import Core +import Foundation + +final class AccountResignViewControllerWrapper: BaseWrapper { + + typealias R = AccountResignViewReactor + typealias V = AccountResignViewCotroller + + + var reactor: AccountResignViewReactor { + return makeReactor() + } + + var viewController: AccountResignViewCotroller { + return makeViewController() + } + + func makeViewController() -> AccountResignViewCotroller { + return AccountResignViewCotroller(reactor: makeReactor()) + } + + func makeReactor() -> AccountResignViewReactor { + return AccountResignViewReactor() + } +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignIn/AccountSignInReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignIn/AccountSignInReactor.swift index 6415c501f..117e58e0b 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignIn/AccountSignInReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignIn/AccountSignInReactor.swift @@ -15,6 +15,7 @@ import FirebaseMessaging public final class AccountSignInReactor: Reactor { public var initialState: State + @Injected var fetchIsFirstOnboardingUseCase: any FetchIsFirstOnboardingUseCaseProtocol private var accountRepository: AccountImpl = AccountRepository() private let fcmUseCase: FCMUseCaseProtocol = FCMUseCase(FCMRepository: MeAPIs.Worker()) private let disposeBag = DisposeBag() @@ -27,44 +28,57 @@ public final class AccountSignInReactor: Reactor { public enum Mutation { case kakaoLogin(Bool) case appleLogin(Bool) + case setIsFirstOnboarding(Bool) + } public struct State { var pushAccountSingUpVC: Bool + @Pulse var isFirstOnboarding: Bool } init(/*accountRepository: AccountRepository, fcmUseCase: FCMUseCaseProtocol*/) { // self.accountRepository = accountRepository // self.fcmUseCase = fcmUseCase - self.initialState = State(pushAccountSingUpVC: false) + self.initialState = State( + pushAccountSingUpVC: false, + isFirstOnboarding: false + ) } } extension AccountSignInReactor { public func mutate(action: Action) -> Observable { + let isFirstOnboarding = self.fetchIsFirstOnboardingUseCase.execute() switch action { case .kakaoLoginTapped(let sns, let vc): - accountRepository.kakaoLogin(with: sns, vc: vc) - .flatMap { result in + return accountRepository.kakaoLogin(with: sns, vc: vc) + .flatMap { result -> Observable in switch result { case .success: self.saveFCM() - return Observable.just(Mutation.kakaoLogin(true)) + return .concat( + .just(.kakaoLogin(true)), + .just(.setIsFirstOnboarding(isFirstOnboarding)) + ) case .failed: - return Observable.just(Mutation.kakaoLogin(false)) + return .just(.kakaoLogin(false)) } } case .appleLoginTapped(let sns, let vc): - accountRepository.appleLogin(with: sns, vc: vc) + return accountRepository.appleLogin(with: sns, vc: vc) .flatMap { result -> Observable in switch result { case .success: self.saveFCM() - return Observable.just(Mutation.appleLogin(true)) + return .concat( + .just(.appleLogin(true)), + .just(.setIsFirstOnboarding(isFirstOnboarding)) + ) case .failed: - return Observable.just(Mutation.appleLogin(false)) + return .just(.appleLogin(false)) } } } @@ -77,6 +91,8 @@ extension AccountSignInReactor { newState.pushAccountSingUpVC = result case .appleLogin(let result): newState.pushAccountSingUpVC = result + case let .setIsFirstOnboarding(isFirstOnboarding): + newState.isFirstOnboarding = isFirstOnboarding } return newState } 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 8a38d93bf..43523d10f 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignIn/AccountSignInViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignIn/AccountSignInViewController.swift @@ -38,7 +38,6 @@ public final class AccountSignInViewController: BaseViewController Observable in + return accountRepository.signUp(name: currentState.nickname, date: date, photoURL: nil) + .withUnretained(self).flatMap { owner, tokenEntity -> Observable in + owner.updateIsFirstOnboardingUseCase.execute(true) return Observable.just(Mutation.didTapCompletehButton(tokenEntity)) } } else { - return accountRepository.signUp(name: currentState.nickname, date: date, photoURL: currentState.profilePresignedURL).flatMap { tokenEntity -> Observable in + return accountRepository.signUp(name: currentState.nickname, date: date, photoURL: currentState.profilePresignedURL) + .withUnretained(self).flatMap { owner, tokenEntity -> Observable in + owner.updateIsFirstOnboardingUseCase.execute(true) return Observable.just(Mutation.didTapCompletehButton(tokenEntity)) } } diff --git a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountProfileViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountProfileViewController.swift index 6d09dd963..3203ec6d1 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountProfileViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountProfileViewController.swift @@ -190,7 +190,7 @@ extension AccountProfileViewController { } private func showNextPage(accessToken: AccessTokenResponse?) { - + @Navigator var accountProfileNavigator: AccountProfileNavigatorProtocol guard let accessToken = accessToken else { return } let token = accessToken.accessToken @@ -199,10 +199,7 @@ extension AccountProfileViewController { let tk = AccessToken(accessToken: token, refreshToken: refreshToken, isTemporaryToken: isTemporaryToken) App.Repository.token.accessToken.accept(tk) - - let container = UINavigationController(rootViewController: OnBoardingDIContainer().makeViewController()) - container.modalPresentationStyle = .fullScreen - present(container, animated: false) + accountProfileNavigator.toOnboarding() } } diff --git a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountSignUpViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountSignUpViewController.swift index 2a3e25e9c..ac1ad3604 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountSignUpViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountSignUpViewController.swift @@ -86,12 +86,6 @@ extension AccountSignUpViewController { setViewControllers([prevPage], direction: .reverse, animated: true) } - - private func showOnboardingViewCotnroller() { - let onBoardingViewController = OnBoardingDIContainer().makeViewController() - onBoardingViewController.modalPresentationStyle = .fullScreen - present(onBoardingViewController, animated: true) - } } extension AccountSignUpViewController: UIPageViewControllerDataSource { diff --git a/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/ViewController/JoinFamilyViewController.swift b/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/ViewController/JoinFamilyViewController.swift index 3a8d9bfb8..864b8398a 100644 --- a/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/ViewController/JoinFamilyViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/ViewController/JoinFamilyViewController.swift @@ -116,12 +116,10 @@ final class JoinFamilyViewController: BaseViewController { extension JoinFamilyViewController { private func showHomeViewController(_ isShow: Bool) { guard isShow else { return } + @Navigator var joinFamilyNavigator: JoinFamilyNavigatorProtocol UserDefaults.standard.clearInviteCode() - - guard let sceneDelegate = UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate else { return } - sceneDelegate.window?.rootViewController = UINavigationController(rootViewController: MainViewControllerWrapper().makeViewController()) - sceneDelegate.window?.makeKeyAndVisible() + joinFamilyNavigator.toMain() } private func showInputLinkViewController(_ isShow: Bool) { diff --git a/14th-team5-iOS/App/Sources/Presentation/OnBoarding/OnBoardingReactor.swift b/14th-team5-iOS/App/Sources/Presentation/OnBoarding/OnBoardingReactor.swift index 784f11774..f6f5cb6c3 100644 --- a/14th-team5-iOS/App/Sources/Presentation/OnBoarding/OnBoardingReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/OnBoarding/OnBoardingReactor.swift @@ -57,14 +57,13 @@ extension OnBoardingReactor { }, familyUseCase.executeFetchPaginationFamilyMembers(query: .init()) ) - .flatMap { (granted: Bool, _) -> Observable in + .flatMap { [weak self] (granted: Bool, _) -> Observable in if granted { return Observable.just(.permissionTapped) } else { return Observable.empty() } } - } } diff --git a/14th-team5-iOS/App/Sources/Presentation/OnBoarding/OnBoardingViewController.swift b/14th-team5-iOS/App/Sources/Presentation/OnBoarding/OnBoardingViewController.swift index 0e55b3e86..132e741cd 100644 --- a/14th-team5-iOS/App/Sources/Presentation/OnBoarding/OnBoardingViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/OnBoarding/OnBoardingViewController.swift @@ -142,19 +142,16 @@ final public class OnBoardingViewController: BaseViewController PrivacyViewController { - return PrivacyViewController(reactor: makeReactor()) - } - - private func makeRepository() -> PrivacyViewInterface { - return PrivacyViewRepository() - } - - private func makeReactor() -> PrivacyViewReactor { - return PrivacyViewReactor(privacyUseCase: makeUseCase(), signOutUseCase: makeSignOutUseCase(), memberId: memberId) - } - - private func makeUseCase() -> PrivacyViewUseCase { - return PrivacyViewUseCase(privacyViewRepository: makeRepository()) - } -} - -extension PrivacyDIContainer { - private func makeFCMRepository() -> MeAPIs.Worker { - return MeAPIs.Worker() - } - - private func makeSignOutUseCase() -> SignOutUseCaseProtocol { - return SignOutUseCase(keychainRepository: KeychainRepository.shared, userDefaultsRepository: UserDefaultsRepository.shared, fcmRepository: makeFCMRepository()) - } -} diff --git a/14th-team5-iOS/App/Sources/Presentation/Privacy/PrivacyViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Privacy/PrivacyViewController.swift index ca8732ddf..92b46be8d 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Privacy/PrivacyViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Privacy/PrivacyViewController.swift @@ -118,9 +118,8 @@ public final class PrivacyViewController: BaseViewController .withUnretained(self) .bind { owner, isSuccess in guard isSuccess else { return } - guard let sceneDelegate = UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate else { return } - sceneDelegate.window?.rootViewController = AccountSignInDIContainer().makeViewController() - sceneDelegate.window?.makeKeyAndVisible() + @Navigator var privacyNavigator: PrivacyNavigatorProtocol + privacyNavigator.toSignIn() }.disposed(by: disposeBag) NotificationCenter.default @@ -140,8 +139,8 @@ public final class PrivacyViewController: BaseViewController .rx.tap .throttle(.milliseconds(300), scheduler: MainScheduler.instance) .bind(with: self, onNext: { owner, _ in - let webContentViewController = WebContentDIContainer(webURL: URLTypes.inquiry.originURL).makeViewController() - owner.navigationController?.pushViewController(webContentViewController, animated: true) + let inquiryViewController = WebContentViewControllerWrapper(url: URLTypes.inquiry.originURL).viewController + owner.navigationController?.pushViewController(inquiryViewController, animated: true) }).disposed(by: disposeBag) privacyTableView.rx @@ -155,11 +154,11 @@ public final class PrivacyViewController: BaseViewController } else if indexPath.item == 1 { UIApplication.shared.open(URLTypes.settings.originURL) } else if indexPath.item == 2{ - let webContentViewController = WebContentDIContainer(webURL: URLTypes.privacy.originURL).makeViewController() - owner.navigationController?.pushViewController(webContentViewController, animated: true) + let privacyWebViewController = WebContentViewControllerWrapper(url: URLTypes.privacy.originURL).viewController + owner.navigationController?.pushViewController(privacyWebViewController, animated: true) } else { - let webContentViewController = WebContentDIContainer(webURL: URLTypes.terms.originURL).makeViewController() - owner.navigationController?.pushViewController(webContentViewController, animated: true) + let termsWebViewController = WebContentViewControllerWrapper(url: URLTypes.terms.originURL).viewController + owner.navigationController?.pushViewController(termsWebViewController, animated: true) } case .userAuthorizationItem: if indexPath.item == 0 { @@ -167,7 +166,7 @@ public final class PrivacyViewController: BaseViewController } else if indexPath.item == 1 { owner.showFamilyResignAlertController() } else { - let resignViewController = AccountResignDIContainer().makeViewController() + let resignViewController = AccountResignViewControllerWrapper().viewController owner.navigationController?.pushViewController(resignViewController, animated: true) } } @@ -194,10 +193,9 @@ public final class PrivacyViewController: BaseViewController .map { $0.isFamilyResign } .filter { $0 } .subscribe { _ in + @Navigator var privacyNavigator: PrivacyNavigatorProtocol App.Repository.member.familyId.accept(nil) - guard let sceneDelegate = UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate else { return } - sceneDelegate.window?.rootViewController = UINavigationController(rootViewController: JoinFamilyDIContainer().makeViewController()) - sceneDelegate.window?.makeKeyAndVisible() + privacyNavigator.toJoinFamily() }.disposed(by: disposeBag) } diff --git a/14th-team5-iOS/App/Sources/Presentation/Privacy/Reactor/PrivacyViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Privacy/Reactor/PrivacyViewReactor.swift index 3a3bc49b8..957cf4459 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Privacy/Reactor/PrivacyViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Privacy/Reactor/PrivacyViewReactor.swift @@ -7,6 +7,7 @@ import Foundation +import Core import Domain import Data import ReactorKit @@ -15,9 +16,12 @@ import ReactorKit public final class PrivacyViewReactor: Reactor { public var initialState: State - private var privacyUseCase: PrivacyViewUseCaseProtocol + @Injected var fetchAppVersionUseCase: FetchAppVersionUseCaseProtocol + @Injected var signOutUseCase: SignOutUseCaseProtocol + @Injected var resignFamilyUseCase: ResignFamilyUseCaseProtocol + @Injected var fetchAuthorizationItemUseCase: FetchAuthorizationItemsUseCaseProtocol + @Injected var fetchPrivacyItemsUseCase: FetchPrivacyItemsUseCaseProtocol private let memberId: String - private let signOutUseCase: SignOutUseCaseProtocol public enum Action { case viewDidLoad @@ -27,7 +31,7 @@ public final class PrivacyViewReactor: Reactor { public enum Mutation { case setLoading(Bool) - case setBibbiAppInfo(BibbiAppInfoResponse) + case setBibbiAppInfo(AppVersionEntity) case setFamilyResign(Bool) case setPrivacyItemModel([PrivacyItemModel]) case setAuthorizationItemModel([PrivacyItemModel]) @@ -39,14 +43,12 @@ public final class PrivacyViewReactor: Reactor { var isFamilyResign: Bool var memberId: String var isSuccess: Bool - @Pulse var appInfo: BibbiAppInfoResponse? + @Pulse var appInfo: AppVersionEntity? @Pulse var section: [PrivacySectionModel] } - public init(privacyUseCase: PrivacyViewUseCaseProtocol, signOutUseCase: SignOutUseCaseProtocol, memberId: String) { - self.privacyUseCase = privacyUseCase - self.signOutUseCase = signOutUseCase + public init(memberId: String) { self.memberId = memberId self.initialState = State( isLoading: false, @@ -64,37 +66,36 @@ public final class PrivacyViewReactor: Reactor { public func mutate(action: Action) -> Observable { switch action { case .viewDidLoad: - let appKey: String = "7c5aaa36-570e-491f-b18a-26a1a0b72959" - let bibbiAppInfoParameter: BibbiAppInfoParameter = BibbiAppInfoParameter(appKey: appKey) return .concat( .just(.setLoading(true)), .merge( - privacyUseCase.executeBibbiAppInfo(parameter: bibbiAppInfoParameter) + fetchAppVersionUseCase.execute() .asObservable() .withUnretained(self) - .flatMap { owner, entity -> Observable in - owner.privacyUseCase.executePrivacyItems() + .flatMap { owner, appVersionEntity -> Observable in + owner.fetchPrivacyItemsUseCase.execute() .asObservable() - .flatMap { items -> Observable in - + .flatMap { privateInfo -> Observable in + guard let appVersionEntity = appVersionEntity else { return .empty() } var sectionItem: [PrivacyItemModel] = [] - items.forEach { - sectionItem.append(.privacyWithAuthItem(PrivacyCellReactor(descrption: $0, isCheck: entity.latest))) + privateInfo.forEach { + sectionItem.append(.privacyWithAuthItem(PrivacyCellReactor(descrption: $0, isCheck: appVersionEntity.latest))) } return .concat( .just(.setPrivacyItemModel(sectionItem)), - .just(.setBibbiAppInfo(entity)) + .just(.setBibbiAppInfo(appVersionEntity)) ) } }, - privacyUseCase.executeAuthorizationItem() + + fetchAuthorizationItemUseCase.execute() .asObservable() - .flatMap { items -> Observable in + .flatMap { authorizationInfo -> Observable in var sectionItems: [PrivacyItemModel] = [] - items.forEach { + authorizationInfo.forEach { sectionItems.append(.userAuthorizationItem(PrivacyCellReactor(descrption: $0, isCheck: false))) } @@ -121,11 +122,12 @@ public final class PrivacyViewReactor: Reactor { case .didTapFamilyUserResign: return .concat( .just(.setLoading(true)), - privacyUseCase.executeAccountFamilyResign() + resignFamilyUseCase.execute() .asObservable() + .compactMap { $0 } .flatMap { entity -> Observable in return .concat( - .just(.setFamilyResign(entity.isSuccess)), + .just(.setFamilyResign(entity.success)), .just(.setLoading(false)) ) } diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileViewController.swift index cdaacb201..c61f4aa70 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileViewController.swift @@ -258,7 +258,7 @@ public final class ProfileViewController: BaseViewController .withLatestFrom(reactor.state.map { $0.memberId }) .withUnretained(self) .bind { owner, memberId in - let privacyViewController = PrivacyDIContainer(memberId: memberId).makeViewController() + let privacyViewController = PrivacyViewControllerWrapper(memberId: memberId).viewController owner.navigationController?.pushViewController(privacyViewController, animated: true) }.disposed(by: disposeBag) } diff --git a/14th-team5-iOS/App/Sources/Presentation/Resign/AccountResignViewCotroller.swift b/14th-team5-iOS/App/Sources/Presentation/Resign/AccountResignViewCotroller.swift index 008215378..a5ef68302 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Resign/AccountResignViewCotroller.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Resign/AccountResignViewCotroller.swift @@ -155,19 +155,14 @@ final class AccountResignViewCotroller: BaseViewController ViewController { - return AccountResignViewCotroller(reactor: makeReactor()) - } - public func makeReactor() -> Reactor { - return AccountResignViewReactor(resignUseCase: makeUseCase()) - } - - public func makeRepository() -> Repository { - return AccountResignViewRepository() - } - - public func makeUseCase() -> UseCase { - return AccountResignUseCase(accountResignViewRepository: makeRepository()) - } -} diff --git a/14th-team5-iOS/App/Sources/Presentation/Resign/Reactor/AccountResignViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Resign/Reactor/AccountResignViewReactor.swift index ecb625c06..0ec3620bb 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Resign/Reactor/AccountResignViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Resign/Reactor/AccountResignViewReactor.swift @@ -13,7 +13,8 @@ import ReactorKit import RxSwift final class AccountResignViewReactor: Reactor { - private let resignUseCase: AccountResignUseCaseProtocol + @Injected var deleteAccountResignUseCase: DeleteAccountResignUseCaseProtocol + @Injected var updateIsFirstOnboardingUseCase: UpdateIsFirstOnboardingUseCaseProtocol var initialState: State enum Action { @@ -34,8 +35,7 @@ final class AccountResignViewReactor: Reactor { var isSuccess: Bool } - init(resignUseCase: AccountResignUseCaseProtocol) { - self.resignUseCase = resignUseCase + init() { self.initialState = State( isLoading: false, isSeleced: false, @@ -54,22 +54,21 @@ final class AccountResignViewReactor: Reactor { case let .didTapCheckButton(isSelected): return .just(.setSelect(isSelected)) case .didTapResignButton: - //TODO: MemberID는 유저 디폴트 저장한거 사용 하자 MPEvent.Account.withdrawl.track(with: nil) - return resignUseCase.executeAccountResign() + return deleteAccountResignUseCase.execute() .asObservable() + .compactMap { $0 } .withUnretained(self) - .flatMap { owner, entity -> Observable in + .flatMap { owner, entity -> Observable in if entity.isSuccess { - + owner.updateIsFirstOnboardingUseCase.execute(false) return .concat( .just(.setLoading(true)), .just(.setResignEntity(entity.isSuccess)), .just(.setLoading(false)) ) - } else { - return .empty() } + return .empty() } } } diff --git a/14th-team5-iOS/App/Sources/Presentation/Splash/SplashViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Splash/SplashViewController.swift index b0da7977b..4702040e5 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Splash/SplashViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Splash/SplashViewController.swift @@ -92,10 +92,6 @@ public final class SplashViewController: BaseViewController { } private func showNextPage(with member: MemberInfo?) { - guard let sceneDelegate = UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate else { return } - var container: UINavigationController - - // TODO: - Reactor로 집어넣기 @Navigator var splashNavigator: SplashNavigatorProtocol guard let member = member else { diff --git a/14th-team5-iOS/Core/Sources/Extensions/Notification+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/Notification+Ext.swift index 1fb028418..1962bb9f8 100644 --- a/14th-team5-iOS/Core/Sources/Extensions/Notification+Ext.swift +++ b/14th-team5-iOS/Core/Sources/Extensions/Notification+Ext.swift @@ -14,7 +14,6 @@ extension Notification.Name { public static let AccountViewPresignURLDismissNotification = Notification.Name("AccountViewPresignURLDismissNotification") public static let AppVersionsCheckWithRedirectStore = Notification.Name("AppVersionsCheckWithRedirectStore") public static let ProfileImageInitializationUpdate = Notification.Name("ProfileImageInitializationUpdate") - public static let UserAccountDeleted = Notification.Name("UserAccountDeleted") public static let UserAccountLogout = Notification.Name("UserAccountLogout") public static let UserFamilyResign = Notification.Name("UserFamilyResign") public static let DidFinishProfileImageUpdate = Notification.Name("DidFinishProfileImageUpdate") diff --git a/14th-team5-iOS/Data/Sources/APIs/App/AppAPI/AppAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/App/AppAPI/AppAPIs.swift index 6e188a5d0..34a7a714f 100644 --- a/14th-team5-iOS/Data/Sources/APIs/App/AppAPI/AppAPIs.swift +++ b/14th-team5-iOS/Data/Sources/APIs/App/AppAPI/AppAPIs.swift @@ -15,7 +15,7 @@ public enum AppAPIs: API { public var spec: APISpec { switch self { case let .appVersion(appKey): - return APISpec(method: .get, url: "\(BibbiAPI.hostApi)/me/app-version&appKey=\(appKey)") + return APISpec(method: .get, url: "\(BibbiAPI.hostApi)/me/app-version?appKey=\(appKey)") } } diff --git a/14th-team5-iOS/Data/Sources/Trash/Members/DataMapping/MembersProfileResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Members/DataMapping/MembersProfileResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Trash/Members/DataMapping/MembersProfileResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Members/DataMapping/MembersProfileResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/Trash/Members/MemberAPI/MembersAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/MembersAPIWorker.swift similarity index 89% rename from 14th-team5-iOS/Data/Sources/Trash/Members/MemberAPI/MembersAPIWorker.swift rename to 14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/MembersAPIWorker.swift index 3ec32d8bc..530c7ad93 100644 --- a/14th-team5-iOS/Data/Sources/Trash/Members/MemberAPI/MembersAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/MembersAPIWorker.swift @@ -83,4 +83,14 @@ extension MembersAPIWorker { .asSingle() } + public func deleteAccountUser(memberId: String, body: Encodable) -> Single { + let spec = ResignAPIs.accountResign(memberId).spec + + return request(spec: spec, jsonEncodable: body) + .subscribe(on: Self.queue) + .map(AccountResignResponseDTO.self) + .catchAndReturn(nil) + .asSingle() + } + } diff --git a/14th-team5-iOS/Data/Sources/Trash/Members/MemberAPI/MembersAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/MembersAPIs.swift similarity index 87% rename from 14th-team5-iOS/Data/Sources/Trash/Members/MemberAPI/MembersAPIs.swift rename to 14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/MembersAPIs.swift index 7e90bf073..31754f3ba 100644 --- a/14th-team5-iOS/Data/Sources/Trash/Members/MemberAPI/MembersAPIs.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/MembersAPIs.swift @@ -16,6 +16,7 @@ enum MembersAPIs: API { case profileUploadToPreSignedURL(String) case profileEditImage(String) case profileDeleteImage(String) + case accountResign(String) var spec: APISpec { switch self { @@ -31,6 +32,8 @@ enum MembersAPIs: API { return APISpec(method: .put, url: "\(BibbiAPI.hostApi)/members/profile-image-url/\(memberId)") case let .profileDeleteImage(memberId): return APISpec(method: .delete, url: "\(BibbiAPI.hostApi)/members/profile-image-url/\(memberId)") + case let .accountResign(memberId): + return APISpec(method: .delete, url: "\(BibbiAPI.hostApi)/members/\(memberId)") } } diff --git a/14th-team5-iOS/Data/Sources/Trash/Members/Repositories/MembersRepository.swift b/14th-team5-iOS/Data/Sources/APIs/Members/Repositories/MembersRepository.swift similarity index 99% rename from 14th-team5-iOS/Data/Sources/Trash/Members/Repositories/MembersRepository.swift rename to 14th-team5-iOS/Data/Sources/APIs/Members/Repositories/MembersRepository.swift index 4ae2a612c..400f1ca47 100644 --- a/14th-team5-iOS/Data/Sources/Trash/Members/Repositories/MembersRepository.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Members/Repositories/MembersRepository.swift @@ -50,5 +50,4 @@ extension MembersRepository: MembersRepositoryProtocol { .map { $0?.toDomain() } .catchAndReturn(nil) } - } diff --git a/14th-team5-iOS/Data/Sources/APIs/My/Repository/MyRepository.swift b/14th-team5-iOS/Data/Sources/APIs/My/Repository/MyRepository.swift index 50ee5055f..e2b2f7867 100644 --- a/14th-team5-iOS/Data/Sources/APIs/My/Repository/MyRepository.swift +++ b/14th-team5-iOS/Data/Sources/APIs/My/Repository/MyRepository.swift @@ -11,7 +11,7 @@ import Foundation public final class MyRepository: MyRepositoryProtocol { // MARK: - Properties - + private let appUserDefaults: AppUserDefaultsType = AppUserDefaults() // private let familyUserDefaults = FamilyUserDefaults() // MARK: - Intializer @@ -22,6 +22,7 @@ public final class MyRepository: MyRepositoryProtocol { extension MyRepository { + public func fetchMyMemberId() -> String? { // TODO: - 리팩토링된 FamilyUserDefaults로 바꾸기 FamilyUserDefaults.returnMyMemberId() @@ -43,4 +44,12 @@ extension MyRepository { FamilyUserDefaults.load(memberId: memberId)?.profileImageURL } + public func fetchIsFirstOnboarding() -> Bool { + return appUserDefaults.loadIsFirstOnboarding() + } + + public func updateIsFirstOnboarding(_ isFirstOnboarding: Bool) { + appUserDefaults.saveIsFirstOnboarding(isFirstOnboarding) + } + } diff --git a/14th-team5-iOS/Data/Sources/APIs/Privacy/Repository/PrivacyRepository.swift b/14th-team5-iOS/Data/Sources/APIs/Privacy/Repository/PrivacyRepository.swift new file mode 100644 index 000000000..7705afdc7 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/Privacy/Repository/PrivacyRepository.swift @@ -0,0 +1,38 @@ +// +// PrivacyRepository.swift +// Data +// +// Created by Kim dohyun on 9/11/24. +// + +import Domain +import Foundation + +import RxSwift + +public final class PrivacyRepository: PrivacyRepositoryProtocol { + + public init() { } + + public func fetchPrivacyItems() -> Observable> { + let privacyItems: Array = Privacy.allCases.map { $0.descrption } + + + return Observable.create { observer in + observer.onNext(privacyItems) + + return Disposables.create() + } + } + + public func fetchAuthorizationItems() -> Observable> { + let authorizationItems: Array = UserAuthorization.allCases.map { $0.descrption } + + return Observable.create { observer in + observer.onNext(authorizationItems) + + return Disposables.create() + } + } + +} diff --git a/14th-team5-iOS/Data/Sources/APIs/Resign/DataMapping/AccountResignResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Resign/DataMapping/AccountResignResponseDTO.swift new file mode 100644 index 000000000..e3e66936d --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/Resign/DataMapping/AccountResignResponseDTO.swift @@ -0,0 +1,19 @@ +// +// AccountResignResponseDTO.swift +// Domain +// +// Created by Kim dohyun on 1/2/24. +// + +import Foundation +import Domain + +public struct AccountResignResponseDTO: Decodable { + public var success: Bool +} + +extension AccountResignResponseDTO { + public func toDomain() -> AccountResignEntity { + return .init(isSuccess: success) + } +} diff --git a/14th-team5-iOS/Data/Sources/APIs/Resign/Repository/AccountResignRepository.swift b/14th-team5-iOS/Data/Sources/APIs/Resign/Repository/AccountResignRepository.swift new file mode 100644 index 000000000..ce82eac75 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/Resign/Repository/AccountResignRepository.swift @@ -0,0 +1,28 @@ +// +// AccountResignRepository.swift +// Data +// +// Created by Kim dohyun on 9/12/24. +// + +import Domain +import Foundation + +import RxSwift + +public final class AccountResignRepository: AccountResignRepositoryProtocol { + + public let disposeBag: DisposeBag = DisposeBag() + private let resignApiWorker: ResignAPIWorker = ResignAPIWorker() + public init() { } +} + +extension AccountResignRepository { + + public func deleteAccountResignItem() -> Observable { + return resignApiWorker.resignUser(memberId: FamilyUserDefaults.getMyMemberId()) + .compactMap { $0?.toDomain() } + .asObservable() + + } +} diff --git a/14th-team5-iOS/Data/Sources/Trash/Resign/ResignAPI/ResignAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/Resign/ResignAPI/ResignAPIWorker.swift similarity index 72% rename from 14th-team5-iOS/Data/Sources/Trash/Resign/ResignAPI/ResignAPIWorker.swift rename to 14th-team5-iOS/Data/Sources/APIs/Resign/ResignAPI/ResignAPIWorker.swift index 41c4b26c7..de5345b67 100644 --- a/14th-team5-iOS/Data/Sources/Trash/Resign/ResignAPI/ResignAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Resign/ResignAPI/ResignAPIWorker.swift @@ -31,12 +31,12 @@ extension ResignAPIs { extension ResignAPIWorker { - public func resignUser(accessToken: String, memberId: String) -> Single { + public func resignUser(memberId: String) -> Single { let spec = ResignAPIs.accountResign(memberId).spec - return request(spec: spec, headers: [BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.xAuthToken(accessToken)]) + return request(spec: spec) .subscribe(on: Self.queue) - .map(AccountResignDTO.self) + .map(AccountResignResponseDTO.self) .catchAndReturn(nil) .asSingle() } diff --git a/14th-team5-iOS/Data/Sources/Trash/Resign/ResignAPI/ResignAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/Resign/ResignAPI/ResignAPIs.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Trash/Resign/ResignAPI/ResignAPIs.swift rename to 14th-team5-iOS/Data/Sources/APIs/Resign/ResignAPI/ResignAPIs.swift diff --git a/14th-team5-iOS/Data/Sources/Trash/Account/MeAPI/MeAPIWorker.swift b/14th-team5-iOS/Data/Sources/Trash/Account/MeAPI/MeAPIWorker.swift index 69aadfc8f..cb6c17dcf 100644 --- a/14th-team5-iOS/Data/Sources/Trash/Account/MeAPI/MeAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Trash/Account/MeAPI/MeAPIWorker.swift @@ -80,15 +80,6 @@ extension MeAPIWorker: MeRepositoryProtocol, JoinFamilyRepository, FCMRepository .asSingle() } - @available(*, deprecated, renamed: "resignFamily") - private func resignFamily(spec: APISpec, headers: [APIHeader]?) -> Single { - return request(spec: spec, headers: headers) - .subscribe(on: Self.queue) - .map(AccountFamilyResignResponse.self) - .catchAndReturn(nil) - .asSingle() - } - private func fetchAppVersion(spec: APISpec) -> Single { return request(spec: spec, headers: [BibbiAPI.Header.xAppKey]) .subscribe(on: Self.queue) @@ -157,16 +148,6 @@ extension MeAPIWorker { .asSingle() } - @available(*, deprecated, renamed: "resignFamily") - public func resignFamily() -> Single { - let spec = PrivacyAPIs.accountFamilyResign.spec - return Observable.just(()) - .withLatestFrom(self._headers) - .withUnretained(self) - .flatMap { $0.0.resignFamily(spec: spec, headers: $0.1) } - .asSingle() - } - public func fetchAppVersion() -> Single { let spec = MeAPIs.appVersion.spec return Observable.just(()) diff --git a/14th-team5-iOS/Data/Sources/Trash/Privacy/DataMapping/BibbiAppInfoDTO.swift b/14th-team5-iOS/Data/Sources/Trash/Privacy/DataMapping/BibbiAppInfoDTO.swift deleted file mode 100644 index a9077f38c..000000000 --- a/14th-team5-iOS/Data/Sources/Trash/Privacy/DataMapping/BibbiAppInfoDTO.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// BibbiAppInfoDTO.swift -// Domain -// -// Created by Kim dohyun on 1/17/24. -// - -import Foundation -import Domain - - -public struct BibbiAppInfoDTO: Decodable { - public var appKey: String - public var appVersion: String - public var latest: Bool - public var inReview: Bool - public var inService: Bool -} - - -extension BibbiAppInfoDTO { - public func toDomain() -> BibbiAppInfoResponse { - return .init( - appKey: appKey, - appVersion: appVersion, - latest: latest) - } -} diff --git a/14th-team5-iOS/Data/Sources/Trash/Privacy/PrivacyAPI/PrivacyAPIWorker.swift b/14th-team5-iOS/Data/Sources/Trash/Privacy/PrivacyAPI/PrivacyAPIWorker.swift deleted file mode 100644 index 739fac2c7..000000000 --- a/14th-team5-iOS/Data/Sources/Trash/Privacy/PrivacyAPI/PrivacyAPIWorker.swift +++ /dev/null @@ -1,61 +0,0 @@ -// -// PrivacyAPIWorker.swift -// Data -// -// Created by Kim dohyun on 1/1/24. -// - -import Core -import Foundation - -import Alamofire -import Domain -import RxSwift - - -typealias PrivacyAPIWorker = PrivacyAPIs.Worker - - - -extension PrivacyAPIs { - final class Worker: APIWorker { - - static let queue = { - ConcurrentDispatchQueueScheduler(queue: DispatchQueue(label: "PrivacyAPIQueue", qos: .utility)) - }() - - override init() { - super.init() - self.id = "PrivacyAPIWorker" - } - - } - -} - - -extension PrivacyAPIWorker { - - public func requestBibbiAppInfo(accessToken: String, parameter: Encodable) -> Single { - let spec = PrivacyAPIs.bibbiAppInfo.spec - - return request(spec: spec, headers: [BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.xAuthToken(accessToken)], parameters: parameter) - .subscribe(on: Self.queue) - .map(BibbiAppInfoDTO.self) - .catchAndReturn(nil) - .asSingle() - - } - - public func resignFamily(accessToken: String) -> Single { - let spec = PrivacyAPIs.accountFamilyResign.spec - - return request(spec: spec, headers: [BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.xAuthToken(accessToken)]) - .subscribe(on: Self.queue) - .map(AccountFamilyResignDTO.self) - .catchAndReturn(nil) - .asSingle() - } - -} - diff --git a/14th-team5-iOS/Data/Sources/Trash/Privacy/PrivacyAPI/PrivacyAPIs.swift b/14th-team5-iOS/Data/Sources/Trash/Privacy/PrivacyAPI/PrivacyAPIs.swift deleted file mode 100644 index 92fb5906c..000000000 --- a/14th-team5-iOS/Data/Sources/Trash/Privacy/PrivacyAPI/PrivacyAPIs.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// PrivacyAPIs.swift -// Data -// -// Created by Kim dohyun on 1/1/24. -// - -import Core -import Foundation - - - -enum PrivacyAPIs: API { - case bibbiAppInfo - case accountFamilyResign - - var spec: APISpec { - switch self { - case .bibbiAppInfo: - return APISpec(method: .get, url: "\(BibbiAPI.hostApi)/me/app-version") - case .accountFamilyResign: - return APISpec(method: .post, url: "\(BibbiAPI.hostApi)/me/quit-family") - } - } -} - - diff --git a/14th-team5-iOS/Data/Sources/Trash/Privacy/Repositories/PrivacyViewRepository.swift b/14th-team5-iOS/Data/Sources/Trash/Privacy/Repositories/PrivacyViewRepository.swift deleted file mode 100644 index 3618bd475..000000000 --- a/14th-team5-iOS/Data/Sources/Trash/Privacy/Repositories/PrivacyViewRepository.swift +++ /dev/null @@ -1,72 +0,0 @@ -// -// PrivacyViewRepository.swift -// Data -// -// Created by Kim dohyun on 12/16/23. -// - -import Foundation - -import Core -import Domain -import RxSwift -import RxCocoa - - - - -public final class PrivacyViewRepository { - - public init() { } - - private let privacyAPIWorker: PrivacyAPIWorker = PrivacyAPIWorker() - private let signInHelper: AccountSignInHelper = AccountSignInHelper() - private let accessToken: String = App.Repository.token.accessToken.value?.accessToken ?? "" - public var disposeBag: DisposeBag = DisposeBag() - -} - - -extension PrivacyViewRepository: PrivacyViewInterface { - - public func fetchPrivacyItem() -> Observable> { - let privacyItems: Array = Privacy.allCases.map { $0.descrption } - - - return Observable.create { observer in - observer.onNext(privacyItems) - - return Disposables.create() - } - } - - public func fetchAuthorizationItem() -> Observable> { - let authorizationItems: Array = UserAuthorization.allCases.map { $0.descrption } - - return Observable.create { observer in - observer.onNext(authorizationItems) - - return Disposables.create() - } - } - - public func fetchBibbiAppInfo(parameter: Encodable) -> Observable { - return privacyAPIWorker.requestBibbiAppInfo(accessToken: accessToken, parameter: parameter) - .compactMap { $0?.toDomain() } - .asObservable() - } - - public func fetchAccountLogout() -> Observable { - guard let accountSnsType = UserDefaults.standard.snsType else { return .empty() } - return signInHelper.signOut(sns: accountSnsType) - .asObservable() - } - - - public func fetchAccountFamilyResign() -> Observable { - return privacyAPIWorker.resignFamily(accessToken: accessToken) - .compactMap { $0?.toDomain() } - .asObservable() - } - -} diff --git a/14th-team5-iOS/Data/Sources/Trash/Resign/DataMapping/AccountFamilyResignDTO.swift b/14th-team5-iOS/Data/Sources/Trash/Resign/DataMapping/AccountFamilyResignDTO.swift deleted file mode 100644 index 2bb48bc2e..000000000 --- a/14th-team5-iOS/Data/Sources/Trash/Resign/DataMapping/AccountFamilyResignDTO.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// AccountFamilyResignDTO.swift -// Domain -// -// Created by Kim dohyun on 1/17/24. -// - -import Foundation -import Domain - -public struct AccountFamilyResignDTO: Decodable { - public var success: Bool - -} - -extension AccountFamilyResignDTO { - public func toDomain() -> AccountFamilyResignResponse { - return .init(isSuccess: success) - } -} diff --git a/14th-team5-iOS/Data/Sources/Trash/Resign/DataMapping/AccountFcmResignDTO.swift b/14th-team5-iOS/Data/Sources/Trash/Resign/DataMapping/AccountFcmResignDTO.swift deleted file mode 100644 index 4485bbade..000000000 --- a/14th-team5-iOS/Data/Sources/Trash/Resign/DataMapping/AccountFcmResignDTO.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// AccountFcmResignDTO.swift -// Domain -// -// Created by Kim dohyun on 1/5/24. -// - -import Foundation -import Domain - -public struct AccountFcmResignDTO: Decodable { - public var success: Bool -} - -extension AccountFcmResignDTO { - public func toDomain() -> AccountFcmResignResponse { - return .init(isSuccess: success) - } -} diff --git a/14th-team5-iOS/Data/Sources/Trash/Resign/DataMapping/AccountResignDTO.swift b/14th-team5-iOS/Data/Sources/Trash/Resign/DataMapping/AccountResignDTO.swift deleted file mode 100644 index 5b5e2a71e..000000000 --- a/14th-team5-iOS/Data/Sources/Trash/Resign/DataMapping/AccountResignDTO.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// AccountResignDTO.swift -// Domain -// -// Created by Kim dohyun on 1/2/24. -// - -import Foundation -import Domain - -public struct AccountResignDTO: Decodable { - public var success: Bool -} - -extension AccountResignDTO { - public func toDomain() -> AccountResignResponse { - return .init(isSuccess: success) - } -} diff --git a/14th-team5-iOS/Data/Sources/Trash/Resign/Repositories/AccountResignViewRepository.swift b/14th-team5-iOS/Data/Sources/Trash/Resign/Repositories/AccountResignViewRepository.swift deleted file mode 100644 index 24b18625e..000000000 --- a/14th-team5-iOS/Data/Sources/Trash/Resign/Repositories/AccountResignViewRepository.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// AccountResignViewRepository.swift -// Data -// -// Created by Kim dohyun on 1/2/24. -// - -import Foundation - -import Core -import Domain -import RxSwift - -public final class AccountResignViewRepository { - - public init() { } - - private let resignAPIWorker: ResignAPIWorker = ResignAPIWorker() - private let accessToken = App.Repository.token.accessToken.value?.accessToken ?? "" - public var disposeBag: DisposeBag = DisposeBag() -} - -extension AccountResignViewRepository: AccountResignInterface { - - public func fetchAccountResign() -> Observable { - return resignAPIWorker.resignUser(accessToken: accessToken, memberId: FamilyUserDefaults.getMyMemberId()) - .compactMap { $0?.toDomain() } - .asObservable() - } -} diff --git a/14th-team5-iOS/Domain/Sources/Trash/Members/Entities/MembersProfileEntity.swift b/14th-team5-iOS/Domain/Sources/Entities/Members/MembersProfileEntity.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Trash/Members/Entities/MembersProfileEntity.swift rename to 14th-team5-iOS/Domain/Sources/Entities/Members/MembersProfileEntity.swift diff --git a/14th-team5-iOS/Domain/Sources/Trash/Members/Parameters/ProfileImageEditParameter.swift b/14th-team5-iOS/Domain/Sources/Entities/Members/ProfileImageEditParameter.swift similarity index 89% rename from 14th-team5-iOS/Domain/Sources/Trash/Members/Parameters/ProfileImageEditParameter.swift rename to 14th-team5-iOS/Domain/Sources/Entities/Members/ProfileImageEditParameter.swift index 46ec42e9c..d60d2ff09 100644 --- a/14th-team5-iOS/Domain/Sources/Trash/Members/Parameters/ProfileImageEditParameter.swift +++ b/14th-team5-iOS/Domain/Sources/Entities/Members/ProfileImageEditParameter.swift @@ -8,6 +8,7 @@ import Foundation +//TODO: Request로 네이밍 수정 public struct ProfileImageEditParameter: Encodable { public var profileImageUrl: String diff --git a/14th-team5-iOS/Domain/Sources/Trash/Resign/Entity/AccountResignResponse.swift b/14th-team5-iOS/Domain/Sources/Entities/Resign/AccountResignEntity.swift similarity index 59% rename from 14th-team5-iOS/Domain/Sources/Trash/Resign/Entity/AccountResignResponse.swift rename to 14th-team5-iOS/Domain/Sources/Entities/Resign/AccountResignEntity.swift index 666890773..659e3611f 100644 --- a/14th-team5-iOS/Domain/Sources/Trash/Resign/Entity/AccountResignResponse.swift +++ b/14th-team5-iOS/Domain/Sources/Entities/Resign/AccountResignEntity.swift @@ -1,13 +1,13 @@ // -// AccountResignResponse.swift +// AccountResignEntity.swift // Domain // -// Created by Kim dohyun on 1/2/24. +// Created by Kim dohyun on 9/12/24. // import Foundation -public struct AccountResignResponse { +public struct AccountResignEntity { public var isSuccess: Bool public init(isSuccess: Bool) { diff --git a/14th-team5-iOS/Domain/Sources/Repositories/AccountResignRepository.swift b/14th-team5-iOS/Domain/Sources/Repositories/AccountResignRepository.swift new file mode 100644 index 000000000..8cd5080ed --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Repositories/AccountResignRepository.swift @@ -0,0 +1,14 @@ +// +// AccountResignRepository.swift +// Domain +// +// Created by Kim dohyun on 9/12/24. +// + +import Foundation + +import RxSwift + +public protocol AccountResignRepositoryProtocol { + func deleteAccountResignItem() -> Observable +} diff --git a/14th-team5-iOS/Domain/Sources/Trash/Members/Interfaces/MembersRepositoryProtocol.swift b/14th-team5-iOS/Domain/Sources/Repositories/MembersRepository.swift similarity index 86% rename from 14th-team5-iOS/Domain/Sources/Trash/Members/Interfaces/MembersRepositoryProtocol.swift rename to 14th-team5-iOS/Domain/Sources/Repositories/MembersRepository.swift index a6b85ba63..ddaa3877c 100644 --- a/14th-team5-iOS/Domain/Sources/Trash/Members/Interfaces/MembersRepositoryProtocol.swift +++ b/14th-team5-iOS/Domain/Sources/Repositories/MembersRepository.swift @@ -1,8 +1,8 @@ // -// MembersRepositoryProtocol.swift +// MembersRepository.swift // Domain // -// Created by Kim dohyun on 6/5/24. +// Created by Kim dohyun on 9/11/24. // import Foundation @@ -18,3 +18,5 @@ public protocol MembersRepositoryProtocol { func updataProfileImageToS3(memberId: String, parameter: ProfileImageEditParameter) -> Single func deleteProfileImageToS3(memberId: String) -> Single } + + diff --git a/14th-team5-iOS/Domain/Sources/Repositories/MyRepository.swift b/14th-team5-iOS/Domain/Sources/Repositories/MyRepository.swift index 670424add..28c219fb6 100644 --- a/14th-team5-iOS/Domain/Sources/Repositories/MyRepository.swift +++ b/14th-team5-iOS/Domain/Sources/Repositories/MyRepository.swift @@ -12,4 +12,6 @@ public protocol MyRepositoryProtocol { func fetchMyUserName() -> String? func fetchUserName(memberId: String) -> String? func fetchProfileImageUrl(memberId: String) -> String? + func fetchIsFirstOnboarding() -> Bool + func updateIsFirstOnboarding(_ isFirstOnboarding: Bool) } diff --git a/14th-team5-iOS/Domain/Sources/Repositories/PrivacyRepository.swift b/14th-team5-iOS/Domain/Sources/Repositories/PrivacyRepository.swift new file mode 100644 index 000000000..15620ae99 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Repositories/PrivacyRepository.swift @@ -0,0 +1,17 @@ +// +// PrivacyRepository.swift +// Domain +// +// Created by Kim dohyun on 9/11/24. +// + +import Foundation + +import RxSwift +import RxCocoa + + +public protocol PrivacyRepositoryProtocol { + func fetchPrivacyItems() -> Observable> + func fetchAuthorizationItems() -> Observable> +} diff --git a/14th-team5-iOS/Domain/Sources/Trash/Family/Interfaces/Repositories/JoinFamilyRepository.swift b/14th-team5-iOS/Domain/Sources/Trash/Family/Interfaces/Repositories/JoinFamilyRepository.swift index 5ea04bed0..006efe3de 100644 --- a/14th-team5-iOS/Domain/Sources/Trash/Family/Interfaces/Repositories/JoinFamilyRepository.swift +++ b/14th-team5-iOS/Domain/Sources/Trash/Family/Interfaces/Repositories/JoinFamilyRepository.swift @@ -11,5 +11,4 @@ import RxSwift @available(*, deprecated, renamed: "FamilyRepository") public protocol JoinFamilyRepository { func joinFamily(body: JoinFamilyRequest) -> Single - func resignFamily() -> Single } diff --git a/14th-team5-iOS/Domain/Sources/Trash/Privacy/Entity/BibbiAppInfoResponse.swift b/14th-team5-iOS/Domain/Sources/Trash/Privacy/Entity/BibbiAppInfoResponse.swift deleted file mode 100644 index af006d563..000000000 --- a/14th-team5-iOS/Domain/Sources/Trash/Privacy/Entity/BibbiAppInfoResponse.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// BibbiAppInfoResponse.swift -// Domain -// -// Created by Kim dohyun on 1/17/24. -// - -import Foundation - - -public struct BibbiAppInfoResponse { - public var appKey: String - public var appVersion: String - public var latest: Bool - - public init(appKey: String, appVersion: String, latest: Bool) { - self.appKey = appKey - self.appVersion = appVersion - self.latest = latest - } -} diff --git a/14th-team5-iOS/Domain/Sources/Trash/Privacy/Interfaces/PrivacyViewInterface.swift b/14th-team5-iOS/Domain/Sources/Trash/Privacy/Interfaces/PrivacyViewInterface.swift deleted file mode 100644 index 49bc73b3f..000000000 --- a/14th-team5-iOS/Domain/Sources/Trash/Privacy/Interfaces/PrivacyViewInterface.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// PrivacyViewInterface.swift -// Domain -// -// Created by Kim dohyun on 1/1/24. -// - -import Foundation - -import RxSwift - - -public protocol PrivacyViewInterface: AnyObject { - var disposeBag: DisposeBag { get } - - func fetchPrivacyItem() -> Observable> - func fetchAuthorizationItem() -> Observable> - func fetchBibbiAppInfo(parameter: Encodable) -> Observable - func fetchAccountLogout() -> Observable - func fetchAccountFamilyResign() -> Observable -} diff --git a/14th-team5-iOS/Domain/Sources/Trash/Privacy/Parameters/BibbiAppInfoParameter.swift b/14th-team5-iOS/Domain/Sources/Trash/Privacy/Parameters/BibbiAppInfoParameter.swift deleted file mode 100644 index b5cfa09b3..000000000 --- a/14th-team5-iOS/Domain/Sources/Trash/Privacy/Parameters/BibbiAppInfoParameter.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// BibbiAppInfoParameter.swift -// Domain -// -// Created by Kim dohyun on 1/17/24. -// - -import Foundation - - -public struct BibbiAppInfoParameter: Encodable { - public var appKey: String - - public init(appKey: String) { - self.appKey = appKey - } -} diff --git a/14th-team5-iOS/Domain/Sources/Trash/Privacy/UseCases/PrivacyViewUseCase.swift b/14th-team5-iOS/Domain/Sources/Trash/Privacy/UseCases/PrivacyViewUseCase.swift deleted file mode 100644 index 9c042a67b..000000000 --- a/14th-team5-iOS/Domain/Sources/Trash/Privacy/UseCases/PrivacyViewUseCase.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// PrivacyViewUseCase.swift -// Domain -// -// Created by Kim dohyun on 1/1/24. -// - -import Foundation - -import RxSwift - - -public protocol PrivacyViewUseCaseProtocol { - func executePrivacyItems() -> Observable> - func executeAuthorizationItem() -> Observable> - func executeBibbiAppInfo(parameter: Encodable) -> Observable - func executeLogout() -> Observable - func executeAccountFamilyResign() -> Observable -} - - -public final class PrivacyViewUseCase: PrivacyViewUseCaseProtocol { - private let privacyViewRepository: PrivacyViewInterface - - public init(privacyViewRepository: PrivacyViewInterface) { - self.privacyViewRepository = privacyViewRepository - } - - public func executePrivacyItems() -> Observable> { - return privacyViewRepository.fetchPrivacyItem() - } - - public func executeAuthorizationItem() -> Observable> { - return privacyViewRepository.fetchAuthorizationItem() - } - - public func executeBibbiAppInfo(parameter: Encodable) -> Observable { - return privacyViewRepository.fetchBibbiAppInfo(parameter: parameter) - } - - public func executeLogout() -> Observable { - return privacyViewRepository.fetchAccountLogout() - - } - - public func executeAccountFamilyResign() -> Observable { - return privacyViewRepository.fetchAccountFamilyResign() - } -} diff --git a/14th-team5-iOS/Domain/Sources/Trash/Resign/Entity/AccountFamilyResignResponse.swift b/14th-team5-iOS/Domain/Sources/Trash/Resign/Entity/AccountFamilyResignResponse.swift deleted file mode 100644 index fab49928c..000000000 --- a/14th-team5-iOS/Domain/Sources/Trash/Resign/Entity/AccountFamilyResignResponse.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// AccountFamilyResignResponse.swift -// Domain -// -// Created by Kim dohyun on 1/17/24. -// - -import Foundation - -public struct AccountFamilyResignResponse: Codable { - public var isSuccess: Bool - - public init(isSuccess: Bool) { - self.isSuccess = isSuccess - } -} diff --git a/14th-team5-iOS/Domain/Sources/Trash/Resign/Entity/AccountFcmResignResponse.swift b/14th-team5-iOS/Domain/Sources/Trash/Resign/Entity/AccountFcmResignResponse.swift deleted file mode 100644 index 7c6145078..000000000 --- a/14th-team5-iOS/Domain/Sources/Trash/Resign/Entity/AccountFcmResignResponse.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// AccountFcmResignResponse.swift -// Domain -// -// Created by Kim dohyun on 1/5/24. -// - -import Foundation - - -public struct AccountFcmResignResponse { - public var isSuccess: Bool - - public init(isSuccess: Bool) { - self.isSuccess = isSuccess - } -} diff --git a/14th-team5-iOS/Domain/Sources/Trash/Resign/Interfaces/AccountResignInterface.swift b/14th-team5-iOS/Domain/Sources/Trash/Resign/Interfaces/AccountResignInterface.swift deleted file mode 100644 index e0be08f62..000000000 --- a/14th-team5-iOS/Domain/Sources/Trash/Resign/Interfaces/AccountResignInterface.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// AccountResignInterface.swift -// Domain -// -// Created by Kim dohyun on 1/2/24. -// - -import Foundation - -import RxSwift - -public protocol AccountResignInterface: AnyObject { - var disposeBag: DisposeBag { get } - - func fetchAccountResign() -> Observable -} diff --git a/14th-team5-iOS/Domain/Sources/Trash/Resign/UseCases/AccountResignUseCase.swift b/14th-team5-iOS/Domain/Sources/Trash/Resign/UseCases/AccountResignUseCase.swift deleted file mode 100644 index a5bf7286a..000000000 --- a/14th-team5-iOS/Domain/Sources/Trash/Resign/UseCases/AccountResignUseCase.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// AccountResignUseCase.swift -// Domain -// -// Created by Kim dohyun on 1/2/24. -// - -import Foundation - -import RxSwift - -public protocol AccountResignUseCaseProtocol { - func executeAccountResign() -> Observable -} - - -public final class AccountResignUseCase: AccountResignUseCaseProtocol { - private let accountResignViewRepository: AccountResignInterface - - public init(accountResignViewRepository: AccountResignInterface) { - self.accountResignViewRepository = accountResignViewRepository - } - - public func executeAccountResign() -> Observable { - return accountResignViewRepository.fetchAccountResign() - } -} diff --git a/14th-team5-iOS/Domain/Sources/Trash/SignOut/UseCases/SignOutUseCase.swift b/14th-team5-iOS/Domain/Sources/Trash/SignOut/UseCases/SignOutUseCase.swift index b29e2a059..709865757 100644 --- a/14th-team5-iOS/Domain/Sources/Trash/SignOut/UseCases/SignOutUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/Trash/SignOut/UseCases/SignOutUseCase.swift @@ -15,6 +15,8 @@ public protocol SignOutUseCaseProtocol { public class SignOutUseCase: SignOutUseCaseProtocol { + //FIXME: - Repository 분리 + //FIXME: - SocialSignOutUse로 로직 수정 private let keychainRepository: KeychainRepositoryProtocol private let userDefaultsRepository: UserDefaultsRepositoryProtocol private let fcmRepository: FCMRepositoryProtocol diff --git a/14th-team5-iOS/Domain/Sources/Trash/Members/UseCases/Members/DeleteMembersProfileUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Members/DeleteMembersProfileUseCase.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Trash/Members/UseCases/Members/DeleteMembersProfileUseCase.swift rename to 14th-team5-iOS/Domain/Sources/UseCases/Members/DeleteMembersProfileUseCase.swift diff --git a/14th-team5-iOS/Domain/Sources/Trash/Members/UseCases/Members/FetchMembersPostListUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Members/FetchMembersPostListUseCase.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Trash/Members/UseCases/Members/FetchMembersPostListUseCase.swift rename to 14th-team5-iOS/Domain/Sources/UseCases/Members/FetchMembersPostListUseCase.swift diff --git a/14th-team5-iOS/Domain/Sources/Trash/Members/UseCases/Members/FetchMembersProfileUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Members/FetchMembersProfileUseCase.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Trash/Members/UseCases/Members/FetchMembersProfileUseCase.swift rename to 14th-team5-iOS/Domain/Sources/UseCases/Members/FetchMembersProfileUseCase.swift diff --git a/14th-team5-iOS/Domain/Sources/Trash/Members/UseCases/Members/UpdateMembersProfileUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Members/UpdateMembersProfileUseCase.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Trash/Members/UseCases/Members/UpdateMembersProfileUseCase.swift rename to 14th-team5-iOS/Domain/Sources/UseCases/Members/UpdateMembersProfileUseCase.swift diff --git a/14th-team5-iOS/Domain/Sources/UseCases/My/FetchIsFirstOnboardingUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/My/FetchIsFirstOnboardingUseCase.swift new file mode 100644 index 000000000..cebe4292b --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/UseCases/My/FetchIsFirstOnboardingUseCase.swift @@ -0,0 +1,27 @@ +// +// FetchIsFirstOnboardingUseCase.swift +// Domain +// +// Created by Kim dohyun on 9/9/24. +// + +import Foundation + + +public protocol FetchIsFirstOnboardingUseCaseProtocol { + func execute() -> Bool +} + + +public final class FetchIsFirstOnboardingUseCase: FetchIsFirstOnboardingUseCaseProtocol { + + private let myRepository: any MyRepositoryProtocol + + public init(myRepository: any MyRepositoryProtocol) { + self.myRepository = myRepository + } + + public func execute() -> Bool { + return myRepository.fetchIsFirstOnboarding() + } +} diff --git a/14th-team5-iOS/Domain/Sources/UseCases/My/UpdateIsFirstOnboardingUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/My/UpdateIsFirstOnboardingUseCase.swift new file mode 100644 index 000000000..b6ad5b234 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/UseCases/My/UpdateIsFirstOnboardingUseCase.swift @@ -0,0 +1,26 @@ +// +// UpdateIsFirstOnboardingUseCase.swift +// Domain +// +// Created by Kim dohyun on 9/10/24. +// + +import Foundation + + +public protocol UpdateIsFirstOnboardingUseCaseProtocol { + func execute(_ isFirstOnboarding: Bool) +} + +public final class UpdateIsFirstOnboardingUseCase: UpdateIsFirstOnboardingUseCaseProtocol { + + private let myRepository: any MyRepositoryProtocol + + public init(myRepository: any MyRepositoryProtocol) { + self.myRepository = myRepository + } + + public func execute(_ isFirstOnboarding: Bool) { + myRepository.updateIsFirstOnboarding(isFirstOnboarding) + } +} diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Privacy/FetchAuthorizationItemsUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Privacy/FetchAuthorizationItemsUseCase.swift new file mode 100644 index 000000000..f1990e213 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/UseCases/Privacy/FetchAuthorizationItemsUseCase.swift @@ -0,0 +1,29 @@ +// +// FetchAuthorizationItemsUseCase.swift +// Domain +// +// Created by Kim dohyun on 9/11/24. +// + +import Foundation + +import RxSwift + + +public protocol FetchAuthorizationItemsUseCaseProtocol { + func execute() -> Observable> +} + + +public final class FetchAuthorizationItemsUseCase: FetchAuthorizationItemsUseCaseProtocol { + + private let privacyRepository: any PrivacyRepositoryProtocol + + public init(privacyRepository: any PrivacyRepositoryProtocol) { + self.privacyRepository = privacyRepository + } + + public func execute() -> Observable> { + return privacyRepository.fetchAuthorizationItems() + } +} diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Privacy/FetchPrivacyItemsUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Privacy/FetchPrivacyItemsUseCase.swift new file mode 100644 index 000000000..a30254068 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/UseCases/Privacy/FetchPrivacyItemsUseCase.swift @@ -0,0 +1,27 @@ +// +// FetchPrivacyItemsUseCase.swift +// Domain +// +// Created by Kim dohyun on 9/11/24. +// + +import Foundation + +import RxSwift + +public protocol FetchPrivacyItemsUseCaseProtocol { + func execute() -> Observable> +} + +public final class FetchPrivacyItemsUseCase: FetchPrivacyItemsUseCaseProtocol { + + private let privacyRepository: any PrivacyRepositoryProtocol + + public init(privacyRepository: any PrivacyRepositoryProtocol) { + self.privacyRepository = privacyRepository + } + + public func execute() -> Observable> { + return privacyRepository.fetchPrivacyItems() + } +} diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Resign/DeleteAccountResignUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Resign/DeleteAccountResignUseCase.swift new file mode 100644 index 000000000..cfaa42e28 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/UseCases/Resign/DeleteAccountResignUseCase.swift @@ -0,0 +1,27 @@ +// +// DeleteAccountResignUseCase.swift +// Domain +// +// Created by Kim dohyun on 9/12/24. +// + +import Foundation + +import RxSwift + +public protocol DeleteAccountResignUseCaseProtocol { + func execute() -> Observable +} + +public final class DeleteAccountResignUseCase: DeleteAccountResignUseCaseProtocol { + + private let accountResignRepository: any AccountResignRepositoryProtocol + + public init(accountResignRepository: any AccountResignRepositoryProtocol) { + self.accountResignRepository = accountResignRepository + } + + public func execute() -> Observable { + return accountResignRepository.deleteAccountResignItem() + } +} From 4659830f957d832220ef2075fc78cb9135bb8c37 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Mon, 16 Sep 2024 15:23:08 +0900 Subject: [PATCH 208/263] =?UTF-8?q?fix:=20=EC=BA=98=EB=A6=B0=EB=8D=94=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EC=A7=84=EC=9E=85=20=EC=8B=9C=20=ED=81=AC?= =?UTF-8?q?=EB=9E=98=EC=8B=9C,=20=EA=B0=80=EC=A1=B1=20=EA=B7=B8=EB=A3=B9?= =?UTF-8?q?=20=EC=9E=85=EC=9E=A5=ED=95=98=EA=B8=B0=20=EC=8B=A4=ED=8C=A8=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EA=B0=80?= =?UTF-8?q?=EC=A1=B1=20=EA=B7=B8=EB=A3=B9=20=EC=9D=B4=EB=A6=84=20=EB=B0=98?= =?UTF-8?q?=EC=98=81=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84=20(#633)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 캘린더화면 진입 시 크래시가 일어나는 버그 수정 (#632) * fix: 가족 그룹 입장하기가 안되는 문제 수정 (#532) * fix: 댓글없음UI가 표시되는 않는 문제 수정 (#632) * feat: 가족관리화면에 가족그룹명 반영 로직 구현 (#629) * delete: FamilyName관련 UseCase 코드 삭제 (#632) --- .../Calendar/View/BannerView.swift | 1 + .../DailyCalendarViewController.swift | 2 +- .../MonthlyCalendarViewController.swift | 16 +- .../Comment/Reactor/CommentViewReactor.swift | 12 +- .../Reactor/InputFamilyLinkReactor.swift | 5 +- .../Reactor/JoinFamilyReactor.swift | 5 +- .../FamilyNameSettingViewReactor.swift | 7 +- .../Reactor/ManagementReactor.swift | 60 ++++++-- .../View/ManagementTableHeaderView.swift | 7 +- .../ManagementViewController.swift | 16 +- .../Profile/Reactor/ProfileViewReactor.swift | 2 +- .../Presentation/Splash/SplashReactor.swift | 14 +- .../Sources/Base/ReactorViewController.swift | 4 +- .../Bibbi/BBServices/ManagementService.swift | 8 + .../KeychainWrapper/KeychainType.swift | 4 + .../KeychainWrapper/KeychainWrapperKey.swift | 8 +- .../UserDefaultsWrapper/UserDefaultsKey.swift | 1 + .../Trash/TempNavigationViewController.swift | 141 ++++++++++++++++++ .../Sources/Trash/TempViewController.swift | 61 ++++++++ .../Data/Sources/APIs/APIWorker.swift | 1 - .../Family/FamilyAPI/FamilyAPIWorker.swift | 5 +- .../Family/Repository/FamilyRepository.swift | 106 +++++++++---- .../TokenKeychain/TokenKeychain.swift | 73 ++++++--- .../FamilyUserDefaults.swift | 21 ++- .../AccountAPI/AccountSignInHelper.swift | 1 + .../Trash/Account/MeAPI/MeAPIWorker.swift | 15 ++ .../UserDefaults/FamilyUserDefautls.swift | 2 +- .../Family/FamilyMemberProfileEntity.swift | 2 + .../Repositories/FamilyRepository.swift | 1 + 29 files changed, 508 insertions(+), 93 deletions(-) create mode 100644 14th-team5-iOS/Core/Sources/Trash/TempNavigationViewController.swift create mode 100644 14th-team5-iOS/Core/Sources/Trash/TempViewController.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/BannerView.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/BannerView.swift index b4cb823a9..8a0634b82 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/BannerView.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/BannerView.swift @@ -74,6 +74,7 @@ extension BannerView { intimacyDescText .offset(y: 6) } + .foregroundStyle(Color(uiColor: UIColor.bibbiBlack)) } var familyTopPercentageText: some View { diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/DailyCalendarViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/DailyCalendarViewController.swift index a40b26a12..f10922acc 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/DailyCalendarViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/DailyCalendarViewController.swift @@ -20,7 +20,7 @@ import SnapKit import Then fileprivate typealias _Str = CalendarStrings -public final class DailyCalendarViewController: BBNavigationViewController { +public final class DailyCalendarViewController: TempNavigationViewController { // MARK: - Views private let imageView: UIImageView = UIImageView() private let calendarView: FSCalendar = FSCalendar() diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/MonthlyCalendarViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/MonthlyCalendarViewController.swift index 65308f6ce..d96c76bd1 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/MonthlyCalendarViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/MonthlyCalendarViewController.swift @@ -24,7 +24,7 @@ struct MemberDTO { } fileprivate typealias _Str = CalendarStrings -public final class MonthlyCalendarViewController: BaseViewController { +public final class MonthlyCalendarViewController: TempNavigationViewController { // MARK: - Views private lazy var calendarCollectionView: UICollectionView = UICollectionView( frame: .zero, @@ -74,7 +74,7 @@ public final class MonthlyCalendarViewController: BaseViewController Observable { -// let commentService = provider.commentService - switch action { case .fetchComment: let query = PostCommentPaginationQuery() @@ -192,6 +190,7 @@ final public class CommentViewReactor: Reactor { Observable.just(.appendComment(reactor)), Observable.just(.setEnableConfirmButton(true)), Observable.just(.setEnableCommentTextField(true)), + Observable.just(.setHiddenNoneCommentView(true)), Observable.just(.scrollTableToLast(true)), Observable.just(.setText(nil)) ) @@ -216,7 +215,14 @@ final public class CommentViewReactor: Reactor { } $0.0.navigator.showCommentDeleteToast() - return Observable.just(.deleteComment(commentId)) + if $0.0.commentCount == 0 + 1 { + return Observable.concat( + Observable.just(.setHiddenNoneCommentView(false)), + Observable.just(.deleteComment(commentId)) + ) + } else { + return Observable.just(.deleteComment(commentId)) + } } } } 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 18aa5a98d..926b638ee 100644 --- a/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Reactor/InputFamilyLinkReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Reactor/InputFamilyLinkReactor.swift @@ -40,6 +40,8 @@ public final class InputFamilyLinkReactor: Reactor { public let initialState: State @Injected var familyUseCase: FamilyUseCaseProtocol + @Injected var joinFamilyUseCase: JoinFamilyUseCaseProtocol + init() { self.initialState = State() } @@ -60,6 +62,7 @@ extension InputFamilyLinkReactor { let body = JoinFamilyRequest(inviteCode: String(code)) let commonLogic: (JoinFamilyEntity) -> Observable = { joinFamilyData in + // Repository에서 이미 UserDefaults와 App.Repository에 저장하고 있음 App.Repository.member.familyId.accept(joinFamilyData.familyId) App.Repository.member.familyCreatedAt.accept(joinFamilyData.createdAt) return Observable.just(Mutation.setShowHome(true)) @@ -80,7 +83,7 @@ extension InputFamilyLinkReactor { } } } else { - return familyUseCase.executeJoinFamily(body: body) + return joinFamilyUseCase.execute(body: body) .asObservable() .flatMap { joinFamilyData in guard let joinFamilyData = joinFamilyData else { diff --git a/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Reactor/JoinFamilyReactor.swift b/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Reactor/JoinFamilyReactor.swift index dc5e79654..8a8c8f412 100644 --- a/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Reactor/JoinFamilyReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Reactor/JoinFamilyReactor.swift @@ -51,8 +51,9 @@ extension JoinFamilyReactor { guard let familyResponse: CreateFamilyEntity = $0 else { return Observable.just(Mutation.setShowHome(false)) } -// App.Repository.member.familyCreatedAt.accept(familyResponse.createdAt) -// App.Repository.member.familyId.accept(familyResponse.familyId) + // Repository에서 이미 UserDefaults에 데이터를 저장하고 있음 + App.Repository.member.familyCreatedAt.accept(familyResponse.createdAt) + App.Repository.member.familyId.accept(familyResponse.familyId) return Observable.just(Mutation.setShowHome(true)) } case .joinFamily: 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 9b29d3b86..c76ed8e32 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Management/Reactor/FamilyNameSettingViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Management/Reactor/FamilyNameSettingViewReactor.swift @@ -17,6 +17,8 @@ public final class FamilyNameSettingViewReactor: Reactor { @Injected private var updateFamilyNameUseCase: any UpdateFamilyNameUseCaseProtocol @Injected private var fetchFamilyEditerUseCase: any FetchMembersProfileUseCaseProtocol + @Injected private var provider: any ServiceProviderProtocol + public enum FamilyNameUpdateType { case initial case update @@ -98,10 +100,13 @@ public final class FamilyNameSettingViewReactor: Reactor { case let .didTapUpdateFamilyGroupNickname(type): let familyName = type == .initial ? nil : currentState.familyGroupNickName let updateFamilyBody = UpdateFamilyNameRequest(familyName: familyName) + return updateFamilyNameUseCase.execute(body: updateFamilyBody) .asObservable() .compactMap { $0 } - .flatMap { familyGroupNameEntity -> Observable in + .withUnretained(self) + .do(onNext: { $0.0.provider.managementService.didUpdateFamilyInfo() }) + .flatMap { owner, familyGroupNameEntity -> Observable in return .just(.setUpdateFamilyNameItem(familyGroupNameEntity)) } } 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 c0715aef9..c3f5f0583 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Management/Reactor/ManagementReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Management/Reactor/ManagementReactor.swift @@ -18,6 +18,7 @@ public final class ManagementReactor: Reactor { // MARK: - Action public enum Action { + case fetchFamilyGroupInfo case fetchPaginationFamilyMemeber(refresh: Bool) case didTapSharingContainer case didTapSettingBarButton @@ -36,7 +37,8 @@ public final class ManagementReactor: Reactor { case setHiddenMemberFetchFailureView(Bool) case setEndRefreshing(Bool) - case setTableHeaderInfo((String, Int)?) + case setFamilyName(String) + case setMemberCount(Int) } @@ -50,7 +52,8 @@ public final class ManagementReactor: Reactor { var hiddenMemberFetchFailureView: Bool = true @Pulse var isRefreshing: Bool = false - var tableHeaderInfo: (String, Int)? = nil + var familyName: String? = nil + var memberCount: Int? = nil } @@ -61,6 +64,7 @@ public final class ManagementReactor: Reactor { @Navigator var navigator: ManagementNavigatorProtocol @Injected var fetchMyMemberIdIUseCase: FetchMyMemberIdUseCaseProtocol + @Injected var fetchFamilyGroupInfoUseCase: FetchFamilyGroupInfoUseCaseProtocol @Injected var fetchFamilyMemberUseCase: FetchFamilyMembersUseCaseProtocol @Injected var fetchSharingUrlUseCase: FetchInvitationLinkUseCaseProtocol @Injected var checkIsMeUseCase: CheckIsMeUseCaseProtocol @@ -77,16 +81,36 @@ public final class ManagementReactor: Reactor { // MARK: - Transform + public func transform(action: Observable) -> Observable { + let eventAction = provider.managementService.event + .withUnretained(self) + .flatMap { + switch $0.1 { + case .didUpdateFamilyInfo: + return Observable.merge( + Observable.just(.fetchFamilyGroupInfo), + Observable.just(.fetchPaginationFamilyMemeber(refresh: true)) + ) + + @unknown default: + return Observable.empty() + } + } + + return Observable.merge(action, eventAction) + } + public func transform(mutation: Observable) -> Observable { let eventMutation = provider.managementService.event .withUnretained(self) - .flatMap { owner, event -> Observable in - switch event { + .flatMap { + switch $0.1 { case .didTapCopyUrlAction: - owner.navigator.showSuccessToast() + $0.0.navigator.showSuccessToast() return Observable.empty() - default: + + @unknown default: return Observable.empty() } } @@ -98,8 +122,6 @@ public final class ManagementReactor: Reactor { // MARK: - Mutate public func mutate(action: Action) -> Observable { - let managementService = provider.managementService - switch action { case .didTapSharingContainer: // Mixpanel @@ -132,6 +154,16 @@ public final class ManagementReactor: Reactor { navigator.toFamilyNameSetting() return Observable.empty() + case .fetchFamilyGroupInfo: + return fetchFamilyGroupInfoUseCase.execute() + .withUnretained(self) + .flatMap { + guard let familyInfo = $0.1 else { + return Observable.just(.setFamilyName("나의 가족")) + } + return Observable.just(.setFamilyName(familyInfo.familyName)) + } + case let .fetchPaginationFamilyMemeber(refresh): let query = FamilyPaginationQuery() @@ -158,14 +190,15 @@ public final class ManagementReactor: Reactor { !self.checkIsMeUseCase.execute(memberId: $1.memberId) }.map { FamilyMemberCellReactor(.management, member: $0) } + let memberCount = results.count + return Observable.concat( Observable.just(.setMemberDatasource(items)), Observable.just(.setHiddenTableProgressHud(true)), Observable.just(.setHiddenMemberFetchFailureView(true)), Observable.just(.setEndRefreshing(true)), - Observable.just(.setTableHeaderInfo(("나의 가족", results.count))) + Observable.just(.setMemberCount(memberCount)) ) - // TODO: - 가족 정보 반영 로직 구현하기 } ) @@ -202,8 +235,11 @@ public final class ManagementReactor: Reactor { case let .setEndRefreshing(isRefreshing): newState.isRefreshing = isRefreshing - case let .setTableHeaderInfo(headerInfo): - newState.tableHeaderInfo = headerInfo + case let .setFamilyName(name): + newState.familyName = name + + case let .setMemberCount(count): + newState.memberCount = count } return newState diff --git a/14th-team5-iOS/App/Sources/Presentation/Management/View/ManagementTableHeaderView.swift b/14th-team5-iOS/App/Sources/Presentation/Management/View/ManagementTableHeaderView.swift index 9d500be19..e1c9fd2d9 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Management/View/ManagementTableHeaderView.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Management/View/ManagementTableHeaderView.swift @@ -82,9 +82,12 @@ public final class ManagementTableHeaderView: BaseView.just(()) + .map { Reactor.Action.fetchFamilyGroupInfo } + .bind(to: reactor.action) + .disposed(by: disposeBag) + Observable.just(()) .map { Reactor.Action.fetchPaginationFamilyMemeber(refresh: false) } .bind(to: reactor.action) @@ -107,9 +112,14 @@ public final class ManagementViewController: BBNavigationViewController.just(.setMemberInfo(nil)) } + return Observable.just(.setMemberInfo(memberInfo)) + } } } ]) @@ -91,6 +98,7 @@ public final class SplashReactor: Reactor { switch mutation { case .setMemberInfo(let memberInfo): if let memberInfo = memberInfo { + // MeAPIWorker에서 UserDefaults와 App에 저장하고 있습니다~ (삭제해도 무방) App.Repository.member.memberID.accept(memberInfo.memberId) App.Repository.member.familyId.accept(memberInfo.familyId) } diff --git a/14th-team5-iOS/Core/Sources/Base/ReactorViewController.swift b/14th-team5-iOS/Core/Sources/Base/ReactorViewController.swift index 15f8f0135..90425375f 100644 --- a/14th-team5-iOS/Core/Sources/Base/ReactorViewController.swift +++ b/14th-team5-iOS/Core/Sources/Base/ReactorViewController.swift @@ -61,8 +61,6 @@ open class ReactorViewController: UIViewController, ReactorKit.View where R: } open func setupReactor() { - if let reactor = initialReactor { - self.reactor = reactor - } + self.reactor = initialReactor } } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ManagementService.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ManagementService.swift index e36e9daab..53888d14a 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ManagementService.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ManagementService.swift @@ -11,6 +11,7 @@ import RxSwift public enum ManagementEvent { case didTapCopyUrlAction + case didUpdateFamilyInfo } public protocol ManagementServiceType { @@ -18,6 +19,8 @@ public protocol ManagementServiceType { @discardableResult func didTapCopUrlAction() -> Observable + @discardableResult + func didUpdateFamilyInfo() -> Observable } final public class ManagementService: BaseService, ManagementServiceType { @@ -29,5 +32,10 @@ final public class ManagementService: BaseService, ManagementServiceType { return Observable.just(()) } + public func didUpdateFamilyInfo() -> Observable { + event.onNext(.didUpdateFamilyInfo) + return Observable.just(()) + } + } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/KeychainWrapper/KeychainType.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/KeychainWrapper/KeychainType.swift index ccd1a1967..431371dff 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/KeychainWrapper/KeychainType.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/KeychainWrapper/KeychainType.swift @@ -15,4 +15,8 @@ extension KeychainType { public var keychain: KeychainWrapper { KeychainWrapper.standard } + + public func remove(forKey key: KeychainWrapper.Key) { + keychain.remove(forKey: key) + } } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/KeychainWrapper/KeychainWrapperKey.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/KeychainWrapper/KeychainWrapperKey.swift index b80b8d940..015e0b764 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/KeychainWrapper/KeychainWrapperKey.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/KeychainWrapper/KeychainWrapperKey.swift @@ -10,16 +10,22 @@ import Foundation public extension KeychainWrapper.Key { // MARK: - SignIn + static let signInType: Self = "signInType" static let idToken: Self = "idToken" // 소셜 로그인의 AccessToken + // MARK: - OAuth + static let newAccessToken: Self = "newAccessToken" static let newRefreshToken: Self = "newRefreshToken" + static let newIsTemporaryToken: Self = "newIsTemporaryToken" static let newFcmToken: Self = "newFcmToken" - // MARK: - Old OAuth Key + + + // MARK: - Old OAuth Key (Deprecated) static let accessToken: Self = "accessToken" static let fcmToken: Self = "fcmToken" diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/UserDefaultsWrapper/UserDefaultsKey.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/UserDefaultsWrapper/UserDefaultsKey.swift index d089a08a3..4bfb738fb 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/UserDefaultsWrapper/UserDefaultsKey.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/UserDefaultsWrapper/UserDefaultsKey.swift @@ -25,6 +25,7 @@ public extension UserDefaultsWrapper.Key { static let familyName: Self = "familyName" static let familyMembers: Self = "familyMembers" static let familyMemberIds: Self = "familyMemberIds" + static let familyNameEditorId: Self = "familyEditorId" // MARK: - My UserDefaults diff --git a/14th-team5-iOS/Core/Sources/Trash/TempNavigationViewController.swift b/14th-team5-iOS/Core/Sources/Trash/TempNavigationViewController.swift new file mode 100644 index 000000000..d19ab80c1 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Trash/TempNavigationViewController.swift @@ -0,0 +1,141 @@ +// +// TempNavigationViewController.swift +// Core +// +// Created by 김건우 on 9/15/24. +// + + +import UIKit + +import SnapKit +import ReactorKit + +/// 캘린더 화면 리팩토링 전까지 쓸 임시 뷰컨 +open class TempNavigationViewController: TempViewController where R: Reactor { + + // MARK: - Typealias + typealias Reactor = R + + // MARK: - Views + + public let navigationBar = BBNavigationBar() + public let contentView = UIView() + + // MARK: - Properties + + /// 왼쪽 버튼이 특정 타입시, popViewController 기본 구현을 제공할 지 여부를 결정합니다. + /// 기본값은 true입니다. + public var enableAutoPopViewController = true + + // MARK: - Intitalizer + public override init() { + super.init() + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Lifecycles + open override func viewDidLoad() { + super.viewDidLoad() + + } + + open override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + navigationController?.navigationBar.isHidden = true + } + + // MARK: - Helpers + open override func bind(reactor: R) { + super.bind(reactor: reactor) + + // 왼쪽 버튼이 특정 타입시, popViewController 기본 구현 제공 + navigationBar.rx.didTapLeftBarButton + .bind(with: self) { owner, _ in + let item = owner.navigationBar.leftBarButtonItem + + owner.popViewController(item) + } + .disposed(by: disposeBag) + } + + open override func setupUI() { + super.setupUI() + + view.addSubviews(navigationBar, contentView) + } + + open override func setupAutoLayout() { + super.setupAutoLayout() + + navigationBar.snp.makeConstraints { + $0.top.equalTo(view.safeAreaLayoutGuide) + $0.horizontalEdges.equalToSuperview() + $0.height.equalTo(42) // 내비게이션 바 기본 높이 42 + } + + contentView.snp.makeConstraints { + $0.top.equalTo(navigationBar.snp.bottom) + $0.horizontalEdges.equalToSuperview() + $0.bottom.equalToSuperview() + } + + } + + open override func setupAttributes() { + super.setupAttributes() + + navigationBar.layer.zPosition = 888 + } + +} + + +// MARK: - Extensions + +extension TempNavigationViewController { + + /// NavigationBar의 높이를 바꿉니다. + public func setNavigationBarHeight(_ height: CGFloat) { + navigationBar.snp.updateConstraints { + $0.top.equalTo(view.safeAreaLayoutGuide) + $0.horizontalEdges.equalToSuperview() + $0.height.equalTo(height) + } + + contentView.snp.makeConstraints { + $0.top.equalTo(navigationBar.snp.bottom) + $0.horizontalEdges.equalToSuperview() + $0.bottom.equalToSuperview() + } + } + + + /// NavigationBar를 View의 맨 앞으로 가져옵니다. + /// contentView가 아닌 view에 새로운 UI를 배치할 때, 꼭 호출해주어야 합니다. + @available(*, deprecated) + public func bringNavigationBarViewToFront() { + view.bringSubviewToFront(navigationBar) + } + +} + + +extension TempNavigationViewController { + + private func popViewController(_ ifTypeIsXMark: BBNavigationButtonStyle?) { + if enableAutoPopViewController { + switch ifTypeIsXMark { + case .arrowLeft, .xmark: + self.navigationController?.popViewController(animated: true) + @unknown default: + return + } + } + } + +} + diff --git a/14th-team5-iOS/Core/Sources/Trash/TempViewController.swift b/14th-team5-iOS/Core/Sources/Trash/TempViewController.swift new file mode 100644 index 000000000..5668cc415 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Trash/TempViewController.swift @@ -0,0 +1,61 @@ +// +// TempViewController.swift +// Core +// +// Created by 김건우 on 9/15/24. +// + + +import DesignSystem +import UIKit + +import ReactorKit +import RxSwift + +open class TempViewController: UIViewController, ReactorKit.View where R: Reactor { + + // MARK: - Typealias + + public typealias Reactor = R + + // MARK: - Properties + + public var disposeBag: RxSwift.DisposeBag = DisposeBag() + + // MARK: - Intializer + + public init() { + super.init(nibName: nil, bundle: nil) + } + + public convenience init(reactor: Reactor? = nil) { + self.init() + self.reactor = reactor + } + + required public init?(coder: NSCoder) { + super.init(coder: coder) + } + + // MARK: - Lifecycles + + override open func viewDidLoad() { + super.viewDidLoad() + setupUI() + setupAutoLayout() + setupAttributes() + } + + // MARK: - Helpers + + open func bind(reactor: R) { } + + open func setupUI() { } + + open func setupAutoLayout() { } + + open func setupAttributes() { + view.backgroundColor = .bibbiBlack + } +} + diff --git a/14th-team5-iOS/Data/Sources/APIs/APIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/APIWorker.swift index 192d13728..ddf34b3c2 100644 --- a/14th-team5-iOS/Data/Sources/APIs/APIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/APIWorker.swift @@ -140,7 +140,6 @@ public class APIWorker: NSObject { } - // MARK: - Upload func upload( diff --git a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIWorker.swift index e8f84bf64..f44415ded 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIWorker.swift @@ -35,8 +35,11 @@ extension FamilyAPIWorker { public func joinFamily(body: JoinFamilyRequestDTO) -> Single { let spec = FamilyAPIs.joinFamily.spec + // TODO: - 자동으로 헤더 집어넣게 코드 리팩토링하기 + let accessToken = App.Repository.token.accessToken.value + let headers = BibbiHeader.commonHeaders(accessToken: accessToken?.accessToken) - return request(spec: spec, jsonEncodable: body) + return request(spec: spec, headers: headers, jsonEncodable: body) .subscribe(on: Self.queue) .map(JoinFamilyResponseDTO.self) .catchAndReturn(nil) diff --git a/14th-team5-iOS/Data/Sources/APIs/Family/Repository/FamilyRepository.swift b/14th-team5-iOS/Data/Sources/APIs/Family/Repository/FamilyRepository.swift index 4a510af0e..645866a19 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Family/Repository/FamilyRepository.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Family/Repository/FamilyRepository.swift @@ -19,6 +19,8 @@ public final class FamilyRepository: FamilyRepositoryProtocol { private let familyApiWorker: FamilyAPIWorker = FamilyAPIWorker() + private let familyUserDefaults: FamilyInfoUserDefaultsType = FamilyInfoUserDefaults() + // MARK: - Intializer public init() { } @@ -33,11 +35,16 @@ extension FamilyRepository { return familyApiWorker.joinFamily(body: body) .map { $0?.toDomain() } - .do(onSuccess: { [weak self] response in + .do(onSuccess: { [weak self] in guard let self else { return } - App.Repository.member.familyId.accept(response?.familyId) // TODO: - 리팩토링된 FamilyUserDefaults로 바꾸기 - App.Repository.member.familyCreatedAt.accept(response?.createdAt) - fetchPaginationFamilyMembers(query: .init()) // TODO: - 로직 분리하기 + self.familyUserDefaults.saveFamilyId($0?.familyId) + self.familyUserDefaults.saveFamilyCreatedAt($0?.createdAt) + + // TODO: - 리팩토링된 FamilyUserDefaults로 바꾸기 + App.Repository.member.familyId.accept($0?.familyId) + App.Repository.member.familyCreatedAt.accept($0?.createdAt) + // TODO: - 로직 분리하기 + fetchPaginationFamilyMembers(query: .init()) }) .asObservable() } @@ -47,6 +54,14 @@ extension FamilyRepository { public func resignFamily() -> Observable { return familyApiWorker.resignFamily() .map { $0?.toDomain() } + .do(onSuccess: { [weak self] in + guard let self else { return } + if let sucess = $0?.success, sucess { + self.familyUserDefaults.remove(forKey: .familyId) + self.familyUserDefaults.remove(forKey: .familyName) + self.familyUserDefaults.remove(forKey: .familyCreatedAt) + } + }) .asObservable() } @@ -55,9 +70,14 @@ extension FamilyRepository { public func createFamily() -> Observable { return familyApiWorker.createFamily() .map { $0?.toDomain() } - .do(onSuccess: { - App.Repository.member.familyId.accept($0?.familyId) // TODO: - 리팩토링된 FamilyUserDefaults로 바꾸기 - App.Repository.member.familyCreatedAt.accept($0?.createdAt) // TODO: - 리팩토링된 FamilyUserDefaults로 바꾸기 + .do(onSuccess: { [weak self] in + guard let self else { return } + self.familyUserDefaults.saveFamilyId($0?.familyId) + self.familyUserDefaults.saveFamilyCreatedAt($0?.createdAt) + + // TODO: - 리팩토링된 FamilyUserDefaults로 바꾸기 + App.Repository.member.familyId.accept($0?.familyId) + App.Repository.member.familyCreatedAt.accept($0?.createdAt) }) .asObservable() } @@ -65,21 +85,25 @@ extension FamilyRepository { // MARK: - Fetch Family ID public func fetchFamilyId() -> String? { - // TODO: - 리팩토링된 FamilyUserDefaults로 바꾸기 - App.Repository.member.familyId.value + familyUserDefaults.loadFamilyId() } // MARK: - Fetch Family CreatedAt public func fetchFamilyCreatedAt() -> Observable { - // TODO: - 리팩토링된 FamilyUserDefaults로 바꾸기 - var familyId: String = App.Repository.member.familyId.value ?? "" + guard + let familyId = familyUserDefaults.loadFamilyId() + else { return .error(NSError()) } // TODO: - Error 타입 정의하기 return familyApiWorker.fetchFamilyCreatedAt(familyId: familyId) .map { $0?.toDomain() } - .do(onSuccess: { - App.Repository.member.familyCreatedAt.accept($0?.createdAt) // TODO: - 리팩토링된 FamilyUserDefaults로 바꾸기 + .do(onSuccess: { [weak self] in + guard let self else { return } + self.familyUserDefaults.saveFamilyCreatedAt($0?.createdAt) + + // TODO: - 리팩토링된 FamilyUserDefaults로 바꾸기 + App.Repository.member.familyCreatedAt.accept($0?.createdAt) }) .asObservable() } @@ -87,8 +111,9 @@ extension FamilyRepository { // MARK: - Fetch Invitation Url public func fetchInvitationLink() -> Observable { - // TODO: - 리팩토링된 FamilyUserDefaults로 바꾸기 - var familyId: String = App.Repository.member.familyId.value ?? "" + guard + let familyId = familyUserDefaults.loadFamilyId() + else { return .error(NSError()) } // TODO: - Error 타입 정의하기 return familyApiWorker.fetchInvitationLink(familyId: familyId) .map { $0?.toDomain() } @@ -98,33 +123,58 @@ extension FamilyRepository { // MARK: - Fetch Family Members public func fetchPaginationFamilyMembers(query: FamilyPaginationQuery) -> Observable { - // TODO: - 리팩토링된 FamilyUserDefaults로 바꾸기 - var familyId: String = App.Repository.member.familyId.value ?? "" + guard + let familyId = familyUserDefaults.loadFamilyId() + else { return .error(NSError()) } // TODO: - Error 타입 정의하기 return familyApiWorker.fetchPaginationFamilyMember(familyId: familyId, query: query) .map { $0?.toDomain() } - .do(onSuccess: { - FamilyUserDefaults.saveFamilyMembers($0?.results ?? []) // TODO: - UserDefaults로 바꾸기 + .do(onSuccess: { [weak self] in + guard let self else { return } + + if let profiles = $0?.results { + self.familyUserDefaults.saveFamilyMembers(profiles) + } }) .asObservable() } public func fetchPaginationFamilyMembers(memberIds: [String]) -> [FamilyMemberProfileEntity] { - // TODO: - 리팩토링된 FamilyUserDefaults로 바꾸기 - return FamilyUserDefaults.loadMembersFromUserDefaults(memberIds: memberIds) + var results: [FamilyMemberProfileEntity] = [] + for memberId in memberIds { + guard + let member = familyUserDefaults.loadFamilyMember(memberId) + else { continue } + results.append(member) + } + return results } + + // MARK: - Fetch Family Name + + public func fetchFamilyName() -> String? { + familyUserDefaults.loadFamilyName() + } + + // MARK: - Update Family Name public func updateFamilyName(body: UpdateFamilyNameRequest) -> Observable { - // TODO: - 리팩토링된 FamilyUserDefaults로 바꾸기 - var familyId: String = App.Repository.member.familyId.value ?? "" let body = UpdateFamilyNameRequestDTO(familyName: body.familyName) + guard + let familyId = familyUserDefaults.loadFamilyId() + else { return .error(NSError()) } // TODO: - Error 타입 정의하기 + return familyApiWorker.updateFamilyName(familyId: familyId, body: body) .map { $0?.toDomain() } - .do(onSuccess: { - FamilyUserDefaults.saveFamilyEditorId(familyEditorId: $0?.familyNameEditorId ?? "") + .do(onSuccess: { [weak self] in + guard let self else { return } + self.familyUserDefaults.saveFamilyId($0?.familyId) + self.familyUserDefaults.saveFamilyName($0?.familyName) + self.familyUserDefaults.saveFamilyCreatedAt($0?.createdAt) + self.familyUserDefaults.saveFamilyNameEditorId($0?.familyNameEditorId) }) .asObservable() } @@ -132,6 +182,12 @@ extension FamilyRepository { public func fetchFamilyGroupInfo() -> Observable { return familyApiWorker.fetchFamilyGroupInfo() .map { $0?.toDomain() } + .do(onSuccess: { [weak self] in + guard let self else { return } + self.familyUserDefaults.saveFamilyId($0?.familyId) + self.familyUserDefaults.saveFamilyName($0?.familyName) + self.familyUserDefaults.saveFamilyNameEditorId($0?.familyNameEditorId) + }) .asObservable() } } 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 a6329a4aa..d2a46b3b4 100644 --- a/14th-team5-iOS/Data/Sources/Storages/Keychain/TokenKeychain/TokenKeychain.swift +++ b/14th-team5-iOS/Data/Sources/Storages/Keychain/TokenKeychain/TokenKeychain.swift @@ -19,6 +19,9 @@ public protocol TokenKeychainType: KeychainType { func saveAccessToken(_ accessToken: String?) func loadAccessToken() -> String? + func saveIsTemporaryToken(_ isTemporary: Bool?) + func loadIsTemporaryToken() -> Bool? + func saveOldAccessToken(_ tokenResult: AccessToken?) func loadOldAccessToken() -> AccessToken? @@ -36,20 +39,26 @@ final public class TokenKeychain: TokenKeychainType { // MARK: - IdToken + + /// 로그인 서버(카카오, 애플)로부터 발급받은 접근 토큰을 저장합니다. public func saveIdToken(_ idToken: String?) { keychain[.idToken] = idToken } + /// 로그인 서버(카카오, 애플)로부터 발급받은 접근 토큰을 불러옵니다. public func loadIdToken() -> String? { keychain[.idToken] } // MARK: - SignInType + + /// 로그인 타입(애플, 카카오)를 저장합니다. public func saveSignInType(_ type: SignInType?) { keychain[.signInType] = type?.rawValue } + /// 로그인 타입(애플, 카카오)를 불러옵니다. public func loadSignInType() -> SignInType? { guard let sign: String = keychain[.signInType], @@ -60,52 +69,76 @@ final public class TokenKeychain: TokenKeychainType { // MARK: - AccessToken + + /// 삐삐 서버로부터 발급받은 접근 토큰을 저장합니다. public func saveAccessToken(_ accessToken: String?) { keychain[.newAccessToken] = accessToken } + /// 삐삐 서버로부터 발급받은 접근 토큰을 불러옵니다. public func loadAccessToken() -> String? { keychain[.newAccessToken] } - // MARK: - Old AccessToken - public func saveOldAccessToken(_ tokenResult: AccessToken?) { - // AccessToken은 Core 모듈의 TokenRepository에 정의되어 있음 - guard - let data = try? JSONEncoder().encode(tokenResult), - let str = String(data: data, encoding: .utf8) - else { return } - keychain[.accessToken] = str - } - - 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 - } - - // 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 토큰을 저장합니다. public func saveFCMToken(_ fcmToken: String?) { keychain[.newFcmToken] = fcmToken } + /// FCM 서버로부터 발급받은 FCM 토큰을 저장합니다. public func loadFCMToken() -> String? { keychain[.newFcmToken] } + + + // 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) + 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 + } + } diff --git a/14th-team5-iOS/Data/Sources/Storages/UserDefaults/FamilyUserDefaults/FamilyUserDefaults.swift b/14th-team5-iOS/Data/Sources/Storages/UserDefaults/FamilyUserDefaults/FamilyUserDefaults.swift index 58bcc5233..e3e4a650f 100644 --- a/14th-team5-iOS/Data/Sources/Storages/UserDefaults/FamilyUserDefaults/FamilyUserDefaults.swift +++ b/14th-team5-iOS/Data/Sources/Storages/UserDefaults/FamilyUserDefaults/FamilyUserDefaults.swift @@ -11,9 +11,7 @@ import Core import Domain -public protocol FamilyInfoUserDefaultsType: UserDefaultsType { - typealias Profile = FamilyMemberProfileEntity - +public protocol FamilyInfoUserDefaultsType: UserDefaultsType { func loadFamilyMember(_ memberId: String) -> Profile? func saveFamilyMembers(_ members: [Profile]) @@ -28,6 +26,9 @@ public protocol FamilyInfoUserDefaultsType: UserDefaultsType { func saveFamilyName(_ familyName: String?) func loadFamilyName() -> String? + + func saveFamilyNameEditorId(_ editorId: String?) + func loadFamilyNameEditorId() -> String? } @@ -108,4 +109,18 @@ final public class FamilyInfoUserDefaults: FamilyInfoUserDefaultsType { return familyName } + + // MARK: - Family Editor Id + + public func saveFamilyNameEditorId(_ editorId: String?) { + userDefaults[.familyNameEditorId] = editorId + } + + public func loadFamilyNameEditorId() -> String? { + guard + let familyNameEditorId: String = userDefaults[.familyNameEditorId] + else { return nil } + return familyNameEditorId + } + } diff --git a/14th-team5-iOS/Data/Sources/Trash/Account/AccountAPI/AccountSignInHelper.swift b/14th-team5-iOS/Data/Sources/Trash/Account/AccountAPI/AccountSignInHelper.swift index 952a29869..6b6a8b068 100644 --- a/14th-team5-iOS/Data/Sources/Trash/Account/AccountAPI/AccountSignInHelper.swift +++ b/14th-team5-iOS/Data/Sources/Trash/Account/AccountAPI/AccountSignInHelper.swift @@ -17,6 +17,7 @@ import RxSwift // TODO: - SignInAPIWorker로 코드 이동, Deprecated 처리 +@available(*, deprecated, renamed: "SignInHelper") final class AccountSignInHelper: NSObject { // MARK: - Properties diff --git a/14th-team5-iOS/Data/Sources/Trash/Account/MeAPI/MeAPIWorker.swift b/14th-team5-iOS/Data/Sources/Trash/Account/MeAPI/MeAPIWorker.swift index cb6c17dcf..5afa38b23 100644 --- a/14th-team5-iOS/Data/Sources/Trash/Account/MeAPI/MeAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Trash/Account/MeAPI/MeAPIWorker.swift @@ -14,6 +14,8 @@ import Alamofire fileprivate typealias _PayLoad = MeAPIs.PayLoad typealias MeAPIWorker = MeAPIs.Worker + +@available(*, deprecated, renamed: "MeAPIWorker", message: "MeAPIWorker, MeRespository가 만들어지기 전 사용할 임시 코드입니다") extension MeAPIs { public final class Worker: APIWorker { @@ -49,11 +51,24 @@ extension MeAPIWorker: MeRepositoryProtocol, JoinFamilyRepository, FCMRepository .asSingle() } + /// MeRepository가 만들어지면 없어질 코드 private func getMemberInfo(spec: APISpec) -> Single { + let myUserDefaults = MyUserDefaults() + let familyUserDefaults = FamilyInfoUserDefaults() + return request(spec: spec) .subscribe(on: Self.queue) .map(MemberInfo.self) .catchAndReturn(nil) + .do(onNext: { + myUserDefaults.saveUserName($0?.name) + myUserDefaults.saveMemberId($0?.memberId) + familyUserDefaults.saveFamilyId($0?.familyId) + + App.Repository.member.memberID.accept($0?.memberId) + App.Repository.member.familyId.accept($0?.familyId) + App.Repository.member.nickname.accept($0?.name) + }) .asSingle() } diff --git a/14th-team5-iOS/Data/Sources/Trash/UserDefaults/FamilyUserDefautls.swift b/14th-team5-iOS/Data/Sources/Trash/UserDefaults/FamilyUserDefautls.swift index 688416657..a45086be4 100644 --- a/14th-team5-iOS/Data/Sources/Trash/UserDefaults/FamilyUserDefautls.swift +++ b/14th-team5-iOS/Data/Sources/Trash/UserDefaults/FamilyUserDefautls.swift @@ -11,7 +11,7 @@ import Domain import RxSwift import RxCocoa -@available(*, deprecated, renamed: "FamilyInfoUserDefaults") +@available(*, deprecated, renamed: "FamilyInfoUserDefaults", message: "FamilyInfoUserDefaults!!!!!!!!!!!! 쓰세요") public class FamilyUserDefaults { /// familyIdKey - familyId 저장 /// familyId - memberId를 배열로 저장 diff --git a/14th-team5-iOS/Domain/Sources/Entities/Family/FamilyMemberProfileEntity.swift b/14th-team5-iOS/Domain/Sources/Entities/Family/FamilyMemberProfileEntity.swift index 4daa69bb2..636a122aa 100644 --- a/14th-team5-iOS/Domain/Sources/Entities/Family/FamilyMemberProfileEntity.swift +++ b/14th-team5-iOS/Domain/Sources/Entities/Family/FamilyMemberProfileEntity.swift @@ -7,6 +7,8 @@ import Foundation +public typealias Profile = FamilyMemberProfileEntity + public struct PaginationResponseFamilyMemberProfileEntity { public var results: [FamilyMemberProfileEntity] diff --git a/14th-team5-iOS/Domain/Sources/Repositories/FamilyRepository.swift b/14th-team5-iOS/Domain/Sources/Repositories/FamilyRepository.swift index 7dcfb5ec9..e6777400f 100644 --- a/14th-team5-iOS/Domain/Sources/Repositories/FamilyRepository.swift +++ b/14th-team5-iOS/Domain/Sources/Repositories/FamilyRepository.swift @@ -19,5 +19,6 @@ public protocol FamilyRepositoryProtocol { func fetchPaginationFamilyMembers(query: FamilyPaginationQuery) -> Observable func fetchPaginationFamilyMembers(memberIds: [String]) -> [FamilyMemberProfileEntity] func fetchFamilyGroupInfo() -> Observable + func fetchFamilyName() -> String? func updateFamilyName(body: UpdateFamilyNameRequest) -> Observable } From c825a7acac713b61af30c3c3965af2c9c4b41668 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Wed, 18 Sep 2024 09:22:49 +0900 Subject: [PATCH 209/263] =?UTF-8?q?fix:=20=EB=8C=93=EA=B8=80=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=EC=97=90=EC=84=9C=20=EC=95=8C=20=EC=88=98=20=EC=97=86?= =?UTF-8?q?=EB=8A=94=20=EC=82=AC=EC=9A=A9=EC=9E=90=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=ED=95=84=EB=A1=9C=20=EC=9D=B4=EB=8F=99=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95=20(#634)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Reactor/Cell/CommentCellReactor.swift | 2 +- .../CommentViewController.swift | 1 - .../APIs/My/Repository/MyRepository.swift | 19 ++++++++----------- .../My/CheckIsVaildMemberUseCase.swift | 2 +- 4 files changed, 10 insertions(+), 14 deletions(-) diff --git a/14th-team5-iOS/App/Sources/Presentation/Comment/Reactor/Cell/CommentCellReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Comment/Reactor/Cell/CommentCellReactor.swift index b8a39ea38..87b674e03 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Comment/Reactor/Cell/CommentCellReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Comment/Reactor/Cell/CommentCellReactor.swift @@ -82,8 +82,8 @@ final public class CommentCellReactor: Reactor { navigator.dismiss { [weak self] in self?.navigator.toProfile(memberId: memberId) } -// provider.postGlobalState.pushProfileViewController(memberId) } + return Observable.empty() } } 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 1e1489f4d..ca79708cf 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Comment/ViewController/CommentViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Comment/ViewController/CommentViewController.swift @@ -52,7 +52,6 @@ final public class CommentViewController: ReactorViewController.just(()) - .delay(RxInterval._900milliseconds, scheduler: RxScheduler.main) .map { Reactor.Action.fetchComment } .bind(to: reactor.action) .disposed(by: disposeBag) diff --git a/14th-team5-iOS/Data/Sources/APIs/My/Repository/MyRepository.swift b/14th-team5-iOS/Data/Sources/APIs/My/Repository/MyRepository.swift index e2b2f7867..bcae3e273 100644 --- a/14th-team5-iOS/Data/Sources/APIs/My/Repository/MyRepository.swift +++ b/14th-team5-iOS/Data/Sources/APIs/My/Repository/MyRepository.swift @@ -11,8 +11,11 @@ import Foundation public final class MyRepository: MyRepositoryProtocol { // MARK: - Properties + private let appUserDefaults: AppUserDefaultsType = AppUserDefaults() - // private let familyUserDefaults = FamilyUserDefaults() + private let myUserDefaults: MyUserDefaultsType = MyUserDefaults() + private let familyUserDefaults: FamilyInfoUserDefaultsType = FamilyInfoUserDefaults() + // MARK: - Intializer @@ -22,26 +25,20 @@ public final class MyRepository: MyRepositoryProtocol { extension MyRepository { - public func fetchMyMemberId() -> String? { - // TODO: - 리팩토링된 FamilyUserDefaults로 바꾸기 - FamilyUserDefaults.returnMyMemberId() + return myUserDefaults.loadMemberId() } public func fetchMyUserName() -> String? { - // TODO: - 리팩토링된 FamilyUserDefaults로 바꾸기 - let myMemberId = FamilyUserDefaults.returnMyMemberId() - return FamilyUserDefaults.load(memberId: myMemberId)?.name + return myUserDefaults.loadUserName() } public func fetchUserName(memberId: String) -> String? { - // TODO: - 리팩토링된 FamilyUserDefaults로 바꾸기 - FamilyUserDefaults.load(memberId: memberId)?.name + return familyUserDefaults.loadFamilyMember(memberId)?.name } public func fetchProfileImageUrl(memberId: String) -> String? { - // TODO: - 리팩토링된 FamilyUserDefaults로 바꾸기 - FamilyUserDefaults.load(memberId: memberId)?.profileImageURL + return familyUserDefaults.loadFamilyMember(memberId)?.profileImageURL } public func fetchIsFirstOnboarding() -> Bool { diff --git a/14th-team5-iOS/Domain/Sources/UseCases/My/CheckIsVaildMemberUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/My/CheckIsVaildMemberUseCase.swift index 7e025e1e1..63f06b995 100644 --- a/14th-team5-iOS/Domain/Sources/UseCases/My/CheckIsVaildMemberUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/My/CheckIsVaildMemberUseCase.swift @@ -23,7 +23,7 @@ public class CheckIsVaildMemberUseCase: CheckIsVaildMemberUseCaseProtocol { // MARK: - Execute public func execute(memberId: String) -> Bool { - myRepository.fetchUserName(memberId: memberId) != "" + return myRepository.fetchUserName(memberId: memberId) != nil } } From d1b644e1cccbb16844823a62dc004782348a1ac7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=84?= Date: Fri, 20 Sep 2024 17:31:20 +0900 Subject: [PATCH 210/263] =?UTF-8?q?feat:=20BBToolTip=20View=20UI=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=ED=95=B4=EC=9A=94=20(#639)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: BBToolTipView 추가 * fix: AccountSignInViewController MainViewController 화면 전환 분기 처리 로직 수정 - AccountSignUpReactor UpdateIsFirstOnboardingUseCase 제거 - MainViewReactor UpdateIsFirstOnboardingUseCase 추가 * feat: BBToolTipView BBToolTipPosition Type 정의 - BBToolTipXPosition로 Tip X 값 위치 정의 - BBToolTipConfig Struct 추가 - BBToolTipType 추가 - BBToolTipView 내부 UIConfigure * feat: BBToolTipView Type에 따라 Layout Configure 분리 로직 추가 - BBToolTipXPosition 외부 타입으로 처리 및 Property 추가 * fix: AppUserDefaults saveIsFirstOnboarding, loadIsFirstOnboarding Optional Type 으로 수정 - MyRepository FetchIsFirstOnboarding, updateIsOnboarding Optional Type 으로 수정 - BBToolTipView Layout Update Method 추가 및 Auto Layout remakeConstraints 로 수정 * feat: BBAnimatable 애니메이션 관련 Protocol 메서드 정의 및 내부 메서드 구현 - BBDrawable draw 관련 Protocol 메서드 정의 및 내부 메서드 구현 - BBToolTipConfig, BBToolTipPosition, BBToolTipType 파일 추가 - BBToolTipView BBDrawable, BBAnimatable 프로토콜 채택 코드 추가 * comment: BBAnimatable, BBDrawable, BBTooTipConfig, BBTooTipPosition, BBToolTipType, BBTooTipView 주석 추가 * feat: BBToolTipView final 키워드 추가 * fix: BBToolTipConfiguration 네이밍 수정 * fix: BBAnimatable 네이밍 수정 및 각 메서드 책임 분리 - BBToolTipView 오타 수정 --- .../AccountSignIn/AccountSignInReactor.swift | 2 +- .../AccountSignInViewController.swift | 2 +- .../Reactor/AccountSignUpReactor.swift | 3 - .../Home/Reactor/MainViewReactor.swift | 3 +- .../Profile/ProfileViewController.swift | 13 +- .../Reactor/AccountResignViewReactor.swift | 2 +- .../BBCommons/BBToolTip/BBAnimatable.swift | 57 ++++++++ .../BBCommons/BBToolTip/BBDrawable.swift | 86 ++++++++++++ .../BBToolTip/BBToolTipConfiguration.swift | 50 +++++++ .../BBToolTip/BBToolTipPosition.swift | 35 +++++ .../BBCommons/BBToolTip/BBToolTipType.swift | 123 +++++++++++++++++ .../BBCommons/BBToolTip/BBToolTipView.swift | 127 ++++++++++++++++++ .../APIs/My/Repository/MyRepository.swift | 4 +- .../AppUserDefaults/AppUserDefaults.swift | 10 +- .../Sources/Repositories/MyRepository.swift | 4 +- .../My/FetchIsFirstOnboardingUseCase.swift | 4 +- .../My/UpdateIsFirstOnboardingUseCase.swift | 4 +- 17 files changed, 506 insertions(+), 23 deletions(-) create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToolTip/BBAnimatable.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToolTip/BBDrawable.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToolTip/BBToolTipConfiguration.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToolTip/BBToolTipPosition.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToolTip/BBToolTipType.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToolTip/BBToolTipView.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignIn/AccountSignInReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignIn/AccountSignInReactor.swift index 117e58e0b..5fba51f43 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignIn/AccountSignInReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignIn/AccountSignInReactor.swift @@ -49,7 +49,7 @@ public final class AccountSignInReactor: Reactor { extension AccountSignInReactor { public func mutate(action: Action) -> Observable { - let isFirstOnboarding = self.fetchIsFirstOnboardingUseCase.execute() + let isFirstOnboarding = self.fetchIsFirstOnboardingUseCase.execute() == nil ? false : true switch action { case .kakaoLoginTapped(let sns, let vc): return accountRepository.kakaoLogin(with: sns, vc: vc) 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 43523d10f..d2cfed3b4 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignIn/AccountSignInViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignIn/AccountSignInViewController.swift @@ -145,7 +145,7 @@ extension AccountSignInViewController { return } - if isFirstOnboarding { + if isFirstOnboarding || isTemporaryToken == false { signInNavigator.toMain() return } diff --git a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/Reactor/AccountSignUpReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/Reactor/AccountSignUpReactor.swift index 7a1958ef3..c95243ec3 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/Reactor/AccountSignUpReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/Reactor/AccountSignUpReactor.swift @@ -15,7 +15,6 @@ import ReactorKit fileprivate typealias _Str = AccountSignUpStrings public final class AccountSignUpReactor: Reactor { public var initialState: State - @Injected var updateIsFirstOnboardingUseCase: any UpdateIsFirstOnboardingUseCaseProtocol private var accountRepository: AccountImpl private let memberId: String private let profileType: AccountLoaction @@ -127,13 +126,11 @@ extension AccountSignUpReactor { if self.currentState.profilePresignedURL.isEmpty { return accountRepository.signUp(name: currentState.nickname, date: date, photoURL: nil) .withUnretained(self).flatMap { owner, tokenEntity -> Observable in - owner.updateIsFirstOnboardingUseCase.execute(true) return Observable.just(Mutation.didTapCompletehButton(tokenEntity)) } } else { return accountRepository.signUp(name: currentState.nickname, date: date, photoURL: currentState.profilePresignedURL) .withUnretained(self).flatMap { owner, tokenEntity -> Observable in - owner.updateIsFirstOnboardingUseCase.execute(true) return Observable.just(Mutation.didTapCompletehButton(tokenEntity)) } } diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift index 58cf0ed96..1e4083871 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift @@ -94,6 +94,7 @@ final class MainViewReactor: Reactor { @Injected var fetchMainUseCase: FetchMainUseCaseProtocol @Injected var fetchMainNightUseCase: FetchNightMainViewUseCaseProtocol @Injected var pickUseCase: PickUseCaseProtocol + @Injected var updateIsFirstOnboardingUseCase: any UpdateIsFirstOnboardingUseCaseProtocol @Injected var checkMissionAlertShowUseCase: CheckMissionAlertShowUseCaseProtocol } @@ -172,7 +173,7 @@ extension MainViewReactor { } case .calculateTime: let (isInTime, time) = self.calculateRemainingTime() - + self.updateIsFirstOnboardingUseCase.execute(true) if isInTime { return Observable.concat([ Observable.just(Mutation.setInTime(true)), diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileViewController.swift index c61f4aa70..d29591239 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileViewController.swift @@ -37,7 +37,8 @@ public final class ProfileViewController: BaseViewController private lazy var profilePickerController: PHPickerViewController = PHPickerViewController(configuration: pickerConfiguration) private lazy var profileFeedViewController: ProfileFeedPageViewController = ProfileFeedPageViewControllerWrapper(memberId: reactor?.currentState.memberId ?? "").viewController - + //TODO: - Test Code 추후 제거 + private let toolTipView: BBToolTipView = BBToolTipView(toolTipType: .activeCameraTime) public override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) @@ -57,7 +58,8 @@ public final class ProfileViewController: BaseViewController super.setupUI() addChild(profileFeedViewController) - view.addSubviews(profileView, profileLineView, profileFeedViewController.view, profileSegementControl, profileIndicatorView) + //TODO: - Test Code 추후 제거 + view.addSubviews(profileView, profileLineView, profileFeedViewController.view, profileSegementControl, profileIndicatorView, toolTipView) profileFeedViewController.didMove(toParent: self) } @@ -85,7 +87,12 @@ public final class ProfileViewController: BaseViewController $0.left.right.equalToSuperview() $0.bottom.equalTo(profileLineView.snp.top) } - + //TODO: - Test Code 추후 제거 + toolTipView.snp.makeConstraints { + $0.width.equalTo(187) + $0.height.equalTo(52) + $0.center.equalToSuperview() + } profileLineView.snp.makeConstraints { $0.height.equalTo(1) diff --git a/14th-team5-iOS/App/Sources/Presentation/Resign/Reactor/AccountResignViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Resign/Reactor/AccountResignViewReactor.swift index 0ec3620bb..da69d6037 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Resign/Reactor/AccountResignViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Resign/Reactor/AccountResignViewReactor.swift @@ -61,7 +61,7 @@ final class AccountResignViewReactor: Reactor { .withUnretained(self) .flatMap { owner, entity -> Observable in if entity.isSuccess { - owner.updateIsFirstOnboardingUseCase.execute(false) + owner.updateIsFirstOnboardingUseCase.execute(nil) return .concat( .just(.setLoading(true)), .just(.setResignEntity(entity.isSuccess)), diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToolTip/BBAnimatable.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToolTip/BBAnimatable.swift new file mode 100644 index 000000000..9cd47602b --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToolTip/BBAnimatable.swift @@ -0,0 +1,57 @@ +// +// BBAnimatable.swift +// Core +// +// Created by Kim dohyun on 9/19/24. +// + +import UIKit + + +//MARK: - Typealias + +public typealias BBComponentPresentable = BBComponentShowable & BBComponentClosable + + +/// **Animate**, **CGAffineTransform**, **CABasicAnimation** 을 활용한 Animation 메서드를 정의하는 Protocol 입니다. +/// 해당 **BBComponentShowable** 프로토콜은 Component 객체를 보여주는 애니메이션을 정의하는 프로토콜입니다. +public protocol BBComponentShowable { + func showPopover(duration: TimeInterval, options: UIView.AnimationOptions, transform: CGAffineTransform, alpha: CGFloat) +} + +/// **Animate**, **CGAffineTransform**, **CABasicAnimation** 을 활용한 Animation 메서드를 정의하는 Protocol 입니다. +/// 해당 **BBComponentClosable** 프로토콜은 Component 객체를 숨기는 애니메이션을 정의하는 프로토콜입니다. +public protocol BBComponentClosable { + func hidePopover(duration: TimeInterval, options: UIView.AnimationOptions, transform: CGAffineTransform, alpha: CGFloat) +} + + +//MARK: - Extensions + +public extension BBComponentShowable where Self: UIView { + /// showPopover 메서드 호출 시 Popover 애니메이션 효과를 실행합니다. + func showPopover(duration: TimeInterval = 0.3, options: UIView.AnimationOptions = [.curveEaseInOut], transform: CGAffineTransform = CGAffineTransform(scaleX: 0.1, y: 0.1), alpha: CGFloat = 1) { + self.transform = transform + self.alpha = alpha + + UIView.animate(withDuration: duration, delay: 0, options: options) { [weak self] in + guard let self else { return } + self.transform = CGAffineTransform.identity + self.alpha = 1 + } + } +} + +public extension BBComponentClosable where Self: UIView { + /// hidePopover 메서드 호출 시 Popover 애니메이션 효과를 제거합니다. + func hidePopover(duration: TimeInterval = 0.3, options: UIView.AnimationOptions = [.curveEaseInOut], transform: CGAffineTransform = CGAffineTransform(scaleX: 0.1, y: 0.1), alpha: CGFloat = 0) { + + UIView.animate(withDuration: duration, delay: 0, options: options) { [weak self] in + guard let self else { return } + self.transform = transform + self.alpha = alpha + } completion: { _ in + self.transform = CGAffineTransform.identity + } + } +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToolTip/BBDrawable.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToolTip/BBDrawable.swift new file mode 100644 index 000000000..348387470 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToolTip/BBDrawable.swift @@ -0,0 +1,86 @@ +// +// BBDrawable.swift +// Core +// +// Created by Kim dohyun on 9/19/24. +// + +import UIKit + +/// **UIBezierPath**, ** CALayer**, **CGMutablePath**을 활용한 draw 메서드를 정의하는 Protocol입니다. +protocol BBDrawable { + func drawToolTip(_ frame: CGRect, type: BBToolTipType, context: CGContext) + func drawToolTipArrowShape(_ frame: CGRect, type: BBToolTipType, path: CGMutablePath) + func drawToolTipBottomShape(_ frame: CGRect, toolTipType: BBToolTipType, cornerRadius: CGFloat, path: CGMutablePath) + func drawToolTipTopShape(_ frame: CGRect, toolTipType: BBToolTipType, cornerRadius: CGFloat, path: CGMutablePath) +} + + +extension BBDrawable { + + + /// drawToolTip 메서드 호출 시 **BBToolTipType** 에 해당하는 ToolTip Layout을 **CGContext** 내에서 드로잉 하는 메서드입니다. + /// + /// drawToolTip에 frame은 UIView의 **draw(_: )** 메서드에서 호출되고 있습니다. + /// ToolTip Layout을 변경할 경우 **setNeedsDisplay** 메서드를 호출하시면 됩니다. + func drawToolTip(_ frame: CGRect, type: BBToolTipType, context: CGContext) { + let toolTipPath = CGMutablePath() + + switch type { + case .contributor, .monthlyCalendar: + drawToolTipArrowShape(frame, type: type, path: toolTipPath) + drawToolTipTopShape(frame, toolTipType: type, cornerRadius: type.configure.cornerRadius, path: toolTipPath) + default: + drawToolTipArrowShape(frame, type: type, path: toolTipPath) + drawToolTipBottomShape(frame, toolTipType: type, cornerRadius: type.configure.cornerRadius, path: toolTipPath) + } + + toolTipPath.closeSubpath() + context.addPath(toolTipPath) + context.setFillColor(type.configure.backgroundColor.cgColor) + context.fillPath() + } + + /// drawToolTipArrowShape 메서드 호출 시 ToolTip에 Arrow 모양을 드로잉 하도록 실행합니다. + /// + /// **BBToolTipType** 에 따라 Arrow의 위치가 배치됩니다. + func drawToolTipArrowShape(_ frame: CGRect, type: BBToolTipType, path: CGMutablePath) { + let margin: CGFloat = 16 + let arrowTipXPosition = type.xPosition.rawValue * frame.width + let adjustedArrowTipXPosition = min(max(arrowTipXPosition, margin + type.configure.arrowWidth / 2), frame.width - margin - type.configure.arrowWidth / 2) + let arrowLeft = adjustedArrowTipXPosition - type.configure.arrowWidth / 2 + let arrowRight = adjustedArrowTipXPosition + type.configure.arrowWidth / 2 + + switch type { + case .contributor, .monthlyCalendar: + path.move(to: CGPoint(x: arrowLeft, y: type.configure.arrowHeight)) + path.addLine(to: CGPoint(x: adjustedArrowTipXPosition, y: 0)) + path.addLine(to: CGPoint(x: arrowRight, y: type.configure.arrowHeight)) + default: + path.move(to: CGPoint(x: arrowLeft, y: frame.height - type.configure.arrowHeight)) + path.addLine(to: CGPoint(x: adjustedArrowTipXPosition, y: frame.height)) + path.addLine(to: CGPoint(x: arrowRight, y: frame.height - type.configure.arrowHeight)) + } + } + + /// drawToolTipTopShape 메서드 실행 시 ToolTip의 **ContentShape** 영역들을 드로잉 하도록 실행합니다. + /// + /// Note: - 해당 메서드는 **BBToolTipVerticalPosition** 이 Top일 경우 실행합니다. + func drawToolTipTopShape(_ frame: CGRect, toolTipType: BBToolTipType, cornerRadius: CGFloat, path: CGMutablePath) { + path.addArc(tangent1End: CGPoint(x: frame.maxX, y: toolTipType.configure.arrowHeight), tangent2End: CGPoint(x: frame.maxX, y: frame.maxY + frame.height), radius: cornerRadius) + path.addArc(tangent1End: CGPoint(x: frame.maxX, y: frame.maxY), tangent2End: CGPoint(x: frame.minX, y: frame.maxY), radius: cornerRadius) + + path.addArc(tangent1End: CGPoint(x: frame.minX, y: frame.maxY), tangent2End: CGPoint(x: frame.minX, y: toolTipType.configure.arrowHeight), radius: cornerRadius) + path.addArc(tangent1End: CGPoint(x: frame.minX, y: toolTipType.configure.arrowHeight), tangent2End: CGPoint(x: frame.maxX, y: toolTipType.configure.arrowHeight), radius: cornerRadius) + } + + /// drawToolTipBottomShape 메서드 실행 시 ToolTip의 **ContentShape** 영역들을 드로잉 하도록 실행합니다. + /// + /// Note: - 해당 메서드는 **BBToolTipVerticalPosition** 이 Bottom일 경우 실행합니다. + func drawToolTipBottomShape(_ frame: CGRect, toolTipType: BBToolTipType, cornerRadius: CGFloat, path: CGMutablePath) { + path.addArc(tangent1End: CGPoint(x: frame.maxX, y: frame.height - toolTipType.configure.arrowHeight), tangent2End: CGPoint(x: frame.maxX, y: 0), radius: cornerRadius) + path.addArc(tangent1End: CGPoint(x: frame.maxX, y: 0), tangent2End: CGPoint(x: frame.minX, y: 0), radius: cornerRadius) + path.addArc(tangent1End: CGPoint(x: frame.minX, y: 0), tangent2End: CGPoint(x: frame.minX, y: frame.height - toolTipType.configure.arrowHeight), radius: cornerRadius) + path.addArc(tangent1End: CGPoint(x: frame.minX, y: frame.height - toolTipType.configure.arrowHeight), tangent2End: CGPoint(x: frame.maxX, y: frame.height - toolTipType.configure.arrowHeight), radius: cornerRadius) + } +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToolTip/BBToolTipConfiguration.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToolTip/BBToolTipConfiguration.swift new file mode 100644 index 000000000..f46711b80 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToolTip/BBToolTipConfiguration.swift @@ -0,0 +1,50 @@ +// +// BBToolTipConfiguration.swift +// Core +// +// Created by Kim dohyun on 9/19/24. +// + +import UIKit + +import DesignSystem + +/// BBToolTip에 (UI, Width, Height) Properties를 설정하기 위한 구조체입니다. +public struct BBToolTipConfiguration { + /// ToolTip Corner Radius + public var cornerRadius: CGFloat + /// TooTip TextFont Foreground Color + public var foregroundColor: UIColor + /// TooTip Background Color + public var backgroundColor: UIColor + /// ToolTip Arrow Position + public var position: BBToolTipVerticalPosition + /// ToolTip Text Font + public var font: BBFontStyle + /// ToolTip Content Text + public var contentText: String + /// ToolTip Arrow Width + public var arrowWidth: CGFloat + /// ToolTip Arrow Height + public var arrowHeight: CGFloat + + public init( + cornerRadius: CGFloat = 12, + foregroundColor: UIColor = .bibbiBlack, + backgroundColor: UIColor = .mainYellow, + position: BBToolTipVerticalPosition = .top, + font: BBFontStyle = .body2Regular, + contentText: String = "", + arrowWidth: CGFloat = 15, + arrowHeight: CGFloat = 12 + ) { + self.cornerRadius = cornerRadius + self.foregroundColor = foregroundColor + self.backgroundColor = backgroundColor + self.position = position + self.font = font + self.contentText = contentText + self.arrowWidth = arrowWidth + self.arrowHeight = arrowHeight + } +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToolTip/BBToolTipPosition.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToolTip/BBToolTipPosition.swift new file mode 100644 index 000000000..78dbf975b --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToolTip/BBToolTipPosition.swift @@ -0,0 +1,35 @@ +// +// BBToolTipPosition.swift +// Core +// +// Created by Kim dohyun on 9/19/24. +// + +import Foundation + +/// BBToolTip에 Arrow Horizontal 위치를 설정하기 위한 Nested types입니다. +public enum BBToolTipHorizontalPosition: CGFloat { + /// 왼쪽 가장자리에 위치하는 포지션입니다. + case left = 0.0 + + /// 왼쪽과 중앙 사이에 위치하는 포지션입니다. + case midLeft = 0.25 + + /// 중앙에 위치하는 포지션입니다. + case center = 0.5 + + /// 오른쪽과 중앙 사이에 위치하는 포지션입니다. + case midRight = 0.75 + + /// 오른쪽 가장자리에 위치하는 포지션입니다. + case right = 1.0 +} + +/// BBToolTip에 Arrow Vertical 위지를 설정하기 위한 Nested types입니다. +public enum BBToolTipVerticalPosition { + /// 상단에 배치하는 포지션입니다. + case top + + /// 하단에 배치하는 포지션입니다. + case bottom +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToolTip/BBToolTipType.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToolTip/BBToolTipType.swift new file mode 100644 index 000000000..21b428435 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToolTip/BBToolTipType.swift @@ -0,0 +1,123 @@ +// +// BBToolTipType.swift +// Core +// +// Created by Kim dohyun on 9/19/24. +// + +import UIKit + +import DesignSystem + +/// BBToolTip의 Style을 설정하기 위한 Nested types입니다. +/// 해당 **BBToolTipType** 을 통해 BBToolTip의 Layout을 구성합니다. +public enum BBToolTipType { + /// 홈 화면 inactive Camera Button State ToolTip Type + case inactiveCameraTime + /// 홈 화면 active Camera Button State ToolTip Type + case activeCameraTime + /// 가족 방 화면 Family Name Setting ToolTip Type + case familyNameEdit + /// 홈 화면 생존 신고 이전 Mission Camera Button inactive State Tool Tip + case inactiveSurvivalCameraNoUpload + /// 홈 화면 미션 이미지 업로드 이후 Camera Button inactive State Tool Tip + case inactiveMissionCameraPostUpload + /// 홈 화면 inactive Mission Camera Button State Tool Tip + case inactiveMissionCamera + /// 홈 화면 active Mission Camera Button State Tool Tip + case activeMissionCamera + /// 컨트리뷰터 화면 Description Button Touch Tool Tip + case contributor + /// 추억 캘린더 화면 Description Button Touch Tool Tip + case monthlyCalendar + /// 홈 화면 생존 신고 알림 Tool Tip + case waitingSurvivalImage(contentText: String, imageURL:[URL]) + + + var xPosition: BBToolTipHorizontalPosition { + switch self { + case .monthlyCalendar, .contributor: + return .midLeft + case .familyNameEdit: + return .right + default: + return .center + } + } + + + var configure: BBToolTipConfiguration { + switch self { + case .inactiveCameraTime: + return .init( + foregroundColor: .bibbiWhite, + backgroundColor: .gray700, + position: .bottom, + contentText: "오늘의 생존신고는 완료되었어요" + ) + case .activeCameraTime: + return .init( + foregroundColor: .bibbiWhite, + backgroundColor: .gray700, + position: .bottom, + contentText: "하루에 한 번 사진을 올릴 수 있어요" + ) + case .familyNameEdit: + return .init( + foregroundColor: .bibbiBlack, + backgroundColor: .mainYellow, + position: .bottom, + contentText: "가족 방 이름을 변경해보세요!" + ) + case .inactiveSurvivalCameraNoUpload: + return .init( + foregroundColor: .bibbiWhite, + backgroundColor: .gray700, + position: .bottom, + contentText: "생존신고 후 미션 사진을 올릴 수 있어요" + ) + case .inactiveMissionCameraPostUpload: + return .init( + foregroundColor: .bibbiWhite, + backgroundColor: .gray700, + position: .bottom, + contentText: "오늘의 미션은 완료되었어요" + ) + case .inactiveMissionCamera: + return .init( + foregroundColor: .bibbiWhite, + backgroundColor: .gray700, + position: .bottom, + contentText: "아직 미션 사진을 찍을 수 없어요" + ) + case .activeMissionCamera: + return .init( + foregroundColor: .bibbiWhite, + backgroundColor: .gray700, + position: .bottom, + contentText: "미션 사진을 찍으러 가볼까요?" + ) + case .contributor: + return .init( + foregroundColor: .bibbiWhite, + backgroundColor: .gray700, + position: .top, + contentText: "생존신고 횟수가 동일한 경우\n이모지, 댓글 수를 합산해서 등수를 정해요" + ) + case .monthlyCalendar: + return .init( + foregroundColor: .bibbiWhite, + backgroundColor: .gray700, + position: .top, + contentText: "모두가 참여한 날과 업로드한 사진 수로\n이 달의 친밀도를 측정합니다" + ) + case let .waitingSurvivalImage(contentText, profile): + return .init( + foregroundColor: .bibbiBlack, + backgroundColor: .mainYellow, + position: .bottom, + contentText: "\(contentText)님 외 \(profile.count - 1)명이 기다리고 있어요" + ) + } + } +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToolTip/BBToolTipView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToolTip/BBToolTipView.swift new file mode 100644 index 000000000..22612f967 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToolTip/BBToolTipView.swift @@ -0,0 +1,127 @@ +// +// BBToolTipView.swift +// Core +// +// Created by Kim dohyun on 9/13/24. +// + +import UIKit + +import DesignSystem +import Kingfisher +import SnapKit +import Then + + +public final class BBToolTipView: UIView, BBDrawable, BBComponentPresentable { + + //MARK: Properties + private let contentLabel: BBLabel = BBLabel() + private let profileStackView: UIStackView = UIStackView() + public var toolTipType: BBToolTipType = .activeCameraTime { + didSet { + setupToolTipContent() + setupAutoLayout(toolTipType) + setNeedsDisplay() + } + } + + public init() { + super.init(frame: .zero) + setupToolTipUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func draw(_ rect: CGRect) { + super.draw(rect) + guard let context = UIGraphicsGetCurrentContext() else { return } + context.saveGState() + drawToolTip(rect, type: toolTipType, context: context) + context.restoreGState() + } + + //MARK: Configure + private func setupToolTipContent() { + + profileStackView.do { + $0.spacing = -4 + $0.distribution = .fillEqually + } + + contentLabel.do { + $0.text = toolTipType.configure.contentText + $0.fontStyle = toolTipType.configure.font + $0.textAlignment = .center + $0.numberOfLines = 0 + $0.textColor = toolTipType.configure.foregroundColor + $0.sizeToFit() + } + + self.do { + $0.backgroundColor = .clear + } + } + + private func setupToolTipUI() { + addSubviews(contentLabel, profileStackView) + } + + + private func setupWaitingToolTipUI(imageURL: [URL]) { + imageURL.forEach { + createProfileImageView(imageURL: $0) + } + } + + private func createProfileImageView(imageURL: URL) { + let imageView = UIImageView(frame: .init(x: 0, y: 0, width: 20, height: 20)) + imageView.contentMode = .scaleAspectFill + imageView.layer.borderColor = UIColor.mainYellow.cgColor + imageView.layer.borderWidth = 2 + imageView.clipsToBounds = true + imageView.layer.cornerRadius = 10 + imageView.kf.setImage(with: imageURL) + profileStackView.addArrangedSubview(imageView) + } + + private func setupAutoLayout(_ type: BBToolTipType) { + let arrowHeight = toolTipType.configure.arrowHeight + let textPadding: CGFloat = 10 + + switch type { + case .monthlyCalendar, .contributor: + contentLabel.snp.remakeConstraints { + $0.left.equalToSuperview().inset(16) + $0.right.equalToSuperview().inset(16) + $0.top.equalToSuperview().inset((arrowHeight + textPadding)) + $0.bottom.equalToSuperview().inset(textPadding) + } + case let .waitingSurvivalImage(_ ,imageURL): + setupWaitingToolTipUI(imageURL: imageURL) + + profileStackView.snp.remakeConstraints { + $0.width.equalTo(24 * imageURL.count) + $0.left.equalToSuperview().offset(16) + $0.height.equalTo(24) + $0.centerY.equalTo(contentLabel) + } + + contentLabel.snp.remakeConstraints { + $0.left.equalTo(profileStackView.snp.right).offset(2) + $0.right.equalToSuperview().inset(16) + $0.bottom.equalToSuperview().inset((arrowHeight + textPadding)) + $0.top.equalToSuperview().inset(textPadding) + } + default: + contentLabel.snp.remakeConstraints { + $0.left.equalToSuperview().inset(16) + $0.right.equalToSuperview().inset(16) + $0.bottom.equalToSuperview().inset((arrowHeight + textPadding)) + $0.top.equalToSuperview().inset(textPadding) + } + } + } +} diff --git a/14th-team5-iOS/Data/Sources/APIs/My/Repository/MyRepository.swift b/14th-team5-iOS/Data/Sources/APIs/My/Repository/MyRepository.swift index bcae3e273..abec96461 100644 --- a/14th-team5-iOS/Data/Sources/APIs/My/Repository/MyRepository.swift +++ b/14th-team5-iOS/Data/Sources/APIs/My/Repository/MyRepository.swift @@ -41,11 +41,11 @@ extension MyRepository { return familyUserDefaults.loadFamilyMember(memberId)?.profileImageURL } - public func fetchIsFirstOnboarding() -> Bool { + public func fetchIsFirstOnboarding() -> Bool? { return appUserDefaults.loadIsFirstOnboarding() } - public func updateIsFirstOnboarding(_ isFirstOnboarding: Bool) { + public func updateIsFirstOnboarding(_ isFirstOnboarding: Bool?) { appUserDefaults.saveIsFirstOnboarding(isFirstOnboarding) } 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 6fb7dc26b..00f59b933 100644 --- a/14th-team5-iOS/Data/Sources/Storages/UserDefaults/AppUserDefaults/AppUserDefaults.swift +++ b/14th-team5-iOS/Data/Sources/Storages/UserDefaults/AppUserDefaults/AppUserDefaults.swift @@ -19,8 +19,8 @@ public protocol AppUserDefaultsType: UserDefaultsType { func saveIsFirstShowWidgetAlert(_ value: Bool?) func loadIsFirstShowWidgetAlert() -> Bool? - func saveIsFirstOnboarding(_ value: Bool) - func loadIsFirstOnboarding() -> Bool + func saveIsFirstOnboarding(_ value: Bool?) + func loadIsFirstOnboarding() -> Bool? func saveIsFirstFamilyManagement(_ value: Bool) func loadIsFirstFamilyManagement() -> Bool @@ -94,13 +94,13 @@ final public class AppUserDefaults: AppUserDefaultsType { // MARK: - Onboarding - public func saveIsFirstOnboarding(_ value: Bool) { + public func saveIsFirstOnboarding(_ value: Bool?) { userDefaults[.isFirstOnboarding] = value } - public func loadIsFirstOnboarding() -> Bool { + public func loadIsFirstOnboarding() -> Bool? { guard let isFirstOnboarding: Bool = userDefaults[.isFirstOnboarding] else { - return false + return nil } return isFirstOnboarding } diff --git a/14th-team5-iOS/Domain/Sources/Repositories/MyRepository.swift b/14th-team5-iOS/Domain/Sources/Repositories/MyRepository.swift index 28c219fb6..0b7a73c4f 100644 --- a/14th-team5-iOS/Domain/Sources/Repositories/MyRepository.swift +++ b/14th-team5-iOS/Domain/Sources/Repositories/MyRepository.swift @@ -12,6 +12,6 @@ public protocol MyRepositoryProtocol { func fetchMyUserName() -> String? func fetchUserName(memberId: String) -> String? func fetchProfileImageUrl(memberId: String) -> String? - func fetchIsFirstOnboarding() -> Bool - func updateIsFirstOnboarding(_ isFirstOnboarding: Bool) + func fetchIsFirstOnboarding() -> Bool? + func updateIsFirstOnboarding(_ isFirstOnboarding: Bool?) } diff --git a/14th-team5-iOS/Domain/Sources/UseCases/My/FetchIsFirstOnboardingUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/My/FetchIsFirstOnboardingUseCase.swift index cebe4292b..e32a5d7a9 100644 --- a/14th-team5-iOS/Domain/Sources/UseCases/My/FetchIsFirstOnboardingUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/My/FetchIsFirstOnboardingUseCase.swift @@ -9,7 +9,7 @@ import Foundation public protocol FetchIsFirstOnboardingUseCaseProtocol { - func execute() -> Bool + func execute() -> Bool? } @@ -21,7 +21,7 @@ public final class FetchIsFirstOnboardingUseCase: FetchIsFirstOnboardingUseCaseP self.myRepository = myRepository } - public func execute() -> Bool { + public func execute() -> Bool? { return myRepository.fetchIsFirstOnboarding() } } diff --git a/14th-team5-iOS/Domain/Sources/UseCases/My/UpdateIsFirstOnboardingUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/My/UpdateIsFirstOnboardingUseCase.swift index b6ad5b234..6ab821e7c 100644 --- a/14th-team5-iOS/Domain/Sources/UseCases/My/UpdateIsFirstOnboardingUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/My/UpdateIsFirstOnboardingUseCase.swift @@ -9,7 +9,7 @@ import Foundation public protocol UpdateIsFirstOnboardingUseCaseProtocol { - func execute(_ isFirstOnboarding: Bool) + func execute(_ isFirstOnboarding: Bool?) } public final class UpdateIsFirstOnboardingUseCase: UpdateIsFirstOnboardingUseCaseProtocol { @@ -20,7 +20,7 @@ public final class UpdateIsFirstOnboardingUseCase: UpdateIsFirstOnboardingUseCas self.myRepository = myRepository } - public func execute(_ isFirstOnboarding: Bool) { + public func execute(_ isFirstOnboarding: Bool?) { myRepository.updateIsFirstOnboarding(isFirstOnboarding) } } From ce11370633df997e7f7188a81b6fc26c44d3ec75 Mon Sep 17 00:00:00 2001 From: g_mi <62610032+akrudal@users.noreply.github.com> Date: Fri, 20 Sep 2024 18:21:39 +0900 Subject: [PATCH 211/263] =?UTF-8?q?=ED=99=88=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EB=84=A4=EB=B9=84=EA=B2=8C=EC=9D=B4=EC=85=98=20=EB=B0=94=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=ED=95=A9=EB=8B=88?= =?UTF-8?q?=EB=8B=A4=20(#635)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add navigator (#618) * feat: add family management usecase(#618) * refactor: reactor and viewcontroller(#618) * feat: family name to title(#618) * feat: new icon to family management(#618) * add code review (#618) --- .../DIContainer/AppDIContainer.swift | 15 ++ .../DIContainer/NavigatorDIContainer.swift | 4 + .../Application/Navigator/MainNavigator.swift | 81 ++++++++ .../Home/Reactor/MainViewReactor.swift | 192 +++++++++++------- .../ViewControllers/MainViewController.swift | 112 +++------- .../Profile/ProfileViewController.swift | 10 +- .../BBNavigationBar/BBNavigationBar.swift | 4 +- .../APIs/App/Repository/AppRepository.swift | 13 +- .../DataMapping/MainNightResponseDTO.swift | 6 +- .../DataMapping/MainResponseDTO.swift | 2 + .../AppUserDefaults/AppUserDefaults.swift | 11 +- .../Entities/MainView/MainViewEntity.swift | 6 +- .../MainView/NightMainViewEntity.swift | 8 +- .../Sources/Repositories/AppRepository.swift | 3 + .../App/FetchFamilyManagementUseCase.swift | 34 ++++ .../App/UpdateFamilyManagementUseCase.swift | 27 +++ 16 files changed, 355 insertions(+), 173 deletions(-) create mode 100644 14th-team5-iOS/App/Sources/Application/Navigator/MainNavigator.swift create mode 100644 14th-team5-iOS/Domain/Sources/UseCases/App/FetchFamilyManagementUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/UseCases/App/UpdateFamilyManagementUseCase.swift diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/AppDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/AppDIContainer.swift index 525a31e12..8718a1fa1 100644 --- a/14th-team5-iOS/App/Sources/Application/DIContainer/AppDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/AppDIContainer.swift @@ -19,6 +19,14 @@ final class AppDIContainer: BaseContainer { ) } + private func makeFetchFamilyManagementUseCase() -> FetchIsFirstFamilyManagementUseCaseProtocol { + FetchFamilyManagementUseCase(repository: makeAppRepository()) + } + + private func makeSaveFamilyManagementUseCase() -> UpdateFamilyManagementUseCaseProtocol { + UpdateFamilyManagementUseCase(repository: makeAppRepository()) + } + // MARK: - Make Repository @@ -30,6 +38,13 @@ final class AppDIContainer: BaseContainer { // MARK: - Register func registerDependencies() { + container.register(type: FetchIsFirstFamilyManagementUseCaseProtocol.self) { _ in + self.makeFetchFamilyManagementUseCase() + } + + container.register(type: UpdateFamilyManagementUseCaseProtocol.self) { _ in + self.makeSaveFamilyManagementUseCase() + } container.register(type: FetchAppVersionUseCaseProtocol.self) { _ in self.makeFetchAppVersionUseCase() diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/NavigatorDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/NavigatorDIContainer.swift index fc3414d73..f2d2420bf 100644 --- a/14th-team5-iOS/App/Sources/Application/DIContainer/NavigatorDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/NavigatorDIContainer.swift @@ -31,6 +31,10 @@ final class NavigatorDIContainer: BaseContainer { ) } + container.register(type: MainNavigatorProtocol.self) { _ in + MainNavigator(navigationController: makeUINavigationController()) + } + container.register(type: SplashNavigatorProtocol.self) { _ in SplashNavigator( navigationController: makeUINavigationController() diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/MainNavigator.swift b/14th-team5-iOS/App/Sources/Application/Navigator/MainNavigator.swift new file mode 100644 index 000000000..d045efa81 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/Navigator/MainNavigator.swift @@ -0,0 +1,81 @@ +// +// MainNavigator.swift +// App +// +// Created by 마경미 on 02.09.24. +// + +import UIKit + +import Core +import Domain + + +protocol MainNavigatorProtocol: BaseNavigator { + // alert + func showSurvivalAlert() + func pickAlert(_ name: String) + func missionUnlockedAlert() + + + // toast + func showToast(_ image: UIImage?, _ message: String) + + // viewcontroller + func toCamera(_ type: UploadLocation) + func toDailyCalendar(_ date: String) + func toFamilyManagement() + func toMonthlyCalendar() +} + +final class MainNavigator: MainNavigatorProtocol { + // MARK: - Properties + + var navigationController: UINavigationController + + // MARK: - Intializer + + init( + navigationController: UINavigationController + ) { + self.navigationController = navigationController + } + + // MARK: - To + func showSurvivalAlert() { + BBAlert.style(.takePhoto).show() + } + + func pickAlert(_ name: String) { + BBAlert.style(.picking(name: name)).show() + } + + func missionUnlockedAlert() { + BBAlert.style(.mission).show() + } + + func showToast(_ image: UIImage?, _ message: String) { + BBToast.default(image: image ?? UIImage(), title: message).show() + } + + func toCamera(_ type: UploadLocation) { + let vc = CameraViewControllerWrapper(cameraType: type).viewController + navigationController.pushViewController(vc, animated: true) + } + + func toDailyCalendar(_ date: String) { + let vc = DailyCalendarViewControllerWrapper(selection: date.toDate()).viewController + navigationController.pushViewController(vc, animated: true) + } + + func toFamilyManagement() { + let vc = ManagementViewControllerWrapper().viewController + navigationController.pushViewController(vc, animated: true) + } + + func toMonthlyCalendar() { + let vc = MonthlyCalendarViewControllerWrapper().viewController + navigationController.pushViewController(vc, animated: true) + } + +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift index 1e4083871..77ee7f411 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift @@ -31,18 +31,23 @@ final class MainViewReactor: Reactor { case weeklycalendarViewController(String) case familyManagementViewController case monthlyCalendarViewController + case showToastMessage(UIImage?, String) + case showErrorToast } enum Action { case calculateTime case setTimer(Bool, Int) + case fetchMainUseCase case fetchMainNightUseCase + case checkFamilyManagement case checkMissionAlert(Bool, Bool) + case openNextViewController(TapAction) case didTapSegmentControl(PostType) - case pickConfirmButtonTapped(String, String) + case pickConfirmButtonTapped } enum Mutation { @@ -54,12 +59,9 @@ final class MainViewReactor: Reactor { case setCamerEnabled case setBalloonText case setDescriptionText + case setFamilyManagement(Bool) - case setPickSuccessToastMessage(String) - case setCopySuccessToastMessage - case setFailureToastMessage - - case showNextView(OpenType) + case setPickMember(String, String) } struct State { @@ -67,48 +69,53 @@ final class MainViewReactor: Reactor { var pageIndex: Int = 0 var leftCount: Int = 0 + var familyname: String? var missionText: String = "" var balloonText: BalloonText = .survivalStandard var description: Description = .survivalNone + var isFirstFamilyManagement: Bool = false var isFamilySurvivalUploadedToday: Bool = false var isFamilyMissionUploadedToday: Bool = false var isMeSurvivalUploadedToday: Bool = false var isMeMissionUploadedToday: Bool = false var isMissionUnlocked: Bool = false - @Pulse var openNextView: OpenType? = nil + @Pulse var pickedMember: (memberId: String, name: String)? = nil + @Pulse var cameraEnabled: Bool = false @Pulse var pickers: [Picker] = [] @Pulse var contributor: FamilyRankData = FamilyRankData.empty @Pulse var familySection: [FamilySection.Item] = [] - - @Pulse var shouldPresentPickSuccessToastMessage: String? - @Pulse var shouldPresentCopySuccessToastMessage: Bool = false - @Pulse var shouldPresentFailureToastMessage: Bool = false } let initialState: State = State() + + @Navigator var navigator: MainNavigatorProtocol + @Injected var provider: ServiceProviderProtocol @Injected var fetchMainUseCase: FetchMainUseCaseProtocol @Injected var fetchMainNightUseCase: FetchNightMainViewUseCaseProtocol @Injected var pickUseCase: PickUseCaseProtocol @Injected var updateIsFirstOnboardingUseCase: any UpdateIsFirstOnboardingUseCaseProtocol @Injected var checkMissionAlertShowUseCase: CheckMissionAlertShowUseCaseProtocol + @Injected var checkFamilyManagementUseCase: FetchIsFirstFamilyManagementUseCaseProtocol + @Injected var saveFamilyManagementUseCase: UpdateFamilyManagementUseCaseProtocol } extension MainViewReactor { func transform(mutation: Observable) -> Observable { let homeMutation = provider.mainService.event - .flatMap { event in + .flatMap { event -> Observable in switch event { case let .presentPickAlert(name, id): - return Observable.just(.showNextView(.pickAlert(name, id))) + self.pushViewController(type: .pickAlert(name, id)) + return .just(.setPickMember(id, name)) case .refreshMain: return self.mutate(action: .calculateTime) default: - return Observable.empty() + return .empty() } } @@ -116,9 +123,10 @@ extension MainViewReactor { .flatMap { event -> Observable in switch event { case .didTapCopyUrlAction: - return Observable.just(.setCopySuccessToastMessage) + self.pushViewController(type: .showToastMessage(DesignSystemAsset.link.image, "링크가 복사되었어요")) + return .empty() default: - return Observable.empty() + return .empty() } } @@ -130,25 +138,25 @@ extension MainViewReactor { case .fetchMainUseCase: return fetchMainUseCase.execute() .asObservable() - .flatMap { result -> Observable in + .flatMap { result -> Observable in guard let data = result else { return Observable.empty() } return Observable.concat( - Observable.just(.updateMainData(data)), - Observable.just(.setBalloonText), - Observable.just(.setCamerEnabled), + .just(.updateMainData(data)), + .just(.setBalloonText), + .just(.setCamerEnabled), self.mutate(action: .checkMissionAlert(data.isMissionUnlocked, data.isMeSurvivalUploadedToday)) ) } case .fetchMainNightUseCase: return fetchMainNightUseCase.execute() .asObservable() - .flatMap { result -> Observable in + .flatMap { result -> Observable in guard let data = result else { - return Observable.empty() + return .empty() } - return Observable.just(.updateMainNight(data)) + return .just(.updateMainNight(data)) } case .setTimer(let isInTime, let time): if isInTime { @@ -156,9 +164,9 @@ extension MainViewReactor { .timer(.seconds(time), scheduler: MainScheduler.instance) .flatMap {_ in return Observable.concat([ - Observable.just(Mutation.setInTime(false)), + .just(Mutation.setInTime(false)), self.mutate(action: .fetchMainNightUseCase), - Observable.just(Mutation.setDescriptionText) + .just(Mutation.setDescriptionText) ]) } } else { @@ -166,7 +174,7 @@ extension MainViewReactor { .timer(.seconds(time), scheduler: MainScheduler.instance) .flatMap {_ in return Observable.concat([ - Observable.just(Mutation.setInTime(true)), + .just(Mutation.setInTime(true)), self.mutate(action: .fetchMainUseCase) ]) } @@ -176,13 +184,13 @@ extension MainViewReactor { self.updateIsFirstOnboardingUseCase.execute(true) if isInTime { return Observable.concat([ - Observable.just(Mutation.setInTime(true)), + .just(.setInTime(true)), self.mutate(action: .fetchMainUseCase), self.mutate(action: .setTimer(isInTime, time)) ]) } else { return Observable.concat([ - Observable.just(Mutation.setInTime(false)), + .just(.setInTime(false)), self.mutate(action: .fetchMainNightUseCase), self.mutate(action: .setTimer(isInTime, time)) ]) @@ -190,60 +198,73 @@ extension MainViewReactor { } case .didTapSegmentControl(let type): return Observable.concat( - Observable.just(.setPageIndex(type.getIndex())), - Observable.just(.setBalloonText), - Observable.just(.setDescriptionText), - Observable.just(.setCamerEnabled) + .just(.setPageIndex(type.getIndex())), + .just(.setBalloonText), + .just(.setDescriptionText), + .just(.setCamerEnabled) ) - case let .pickConfirmButtonTapped(name, id): - return pickUseCase.executePickMember(memberId: id) - .flatMap { response in - guard let response = response, - response.success else { - return Observable.just(.setFailureToastMessage) + case let .pickConfirmButtonTapped: + guard let pickedMember = currentState.pickedMember else { + return .empty() + } + return pickUseCase.executePickMember(memberId: pickedMember.memberId) + .compactMap { $0 } + .flatMap { response -> Observable in + if !response.success { + self.pushViewController(type: .showErrorToast) + } else { + self.pushViewController(type: + .showToastMessage(DesignSystemAsset.yellowPaperPlane.image, + "\(pickedMember.name)님에게 생존신고 알림을 보냈어요") + ) } - self.provider.mainService.showPickButton(false, memberId: id) - return Observable.concat( - Observable.just(.setPickSuccessToastMessage(name)), - self.mutate(action: .calculateTime) - ) + return .empty() } case .openNextViewController(let type): switch type { case .cameraButtonTap: if currentState.pageIndex == 0 { - return Observable.just(.showNextView(.cameraViewController(.survival))) + self.pushViewController(type: .cameraViewController(.survival)) } else { if currentState.isMeSurvivalUploadedToday { - return Observable.just(.showNextView(.cameraViewController(.mission))) + self.pushViewController(type: .cameraViewController(.mission)) } else { - return Observable.just(.showNextView(.survivalAlert)) + self.pushViewController(type: .survivalAlert) } } case .navigationRightButtonTap: - return Observable.just(.showNextView(.monthlyCalendarViewController)) + self.pushViewController(type: .monthlyCalendarViewController) case .navigationLeftButtonTap: - return Observable.just(.showNextView(.familyManagementViewController)) + self.pushViewController(type: .familyManagementViewController) + + if currentState.isFirstFamilyManagement { + saveFamilyManagementUseCase.execute(false) + return Observable.just(.setFamilyManagement(false)) + } case .contributorNextButtonTap: guard let date = currentState.contributor.recentPostDate else { return .empty() } - return Observable.just(.showNextView(.weeklycalendarViewController(date))) + self.pushViewController(type: .weeklycalendarViewController(date)) } + return .empty() case .checkMissionAlert(let isUnlocked, let isMeSurvivalUploadedToday): - if isUnlocked && isMeSurvivalUploadedToday { - return checkMissionAlertShowUseCase.execute() - .flatMap { isAlreadyShown in - if !isAlreadyShown { - return Observable.just(.showNextView(.missionUnlockedAlert)) - } else { - return Observable.empty() - } - } - } else { - return Observable.empty() + if !(isUnlocked && isMeSurvivalUploadedToday) { + return .empty() } + + return checkMissionAlertShowUseCase.execute() + .filter { !$0 } + .flatMap { isAlreadyShown -> Observable in + self.pushViewController(type: .missionUnlockedAlert) + return .empty() + } + case .checkFamilyManagement: + return checkFamilyManagementUseCase.execute() + .flatMap { + return Observable.just(.setFamilyManagement($0)) + } } } @@ -253,17 +274,12 @@ extension MainViewReactor { switch mutation { case .setInTime(let isInTime): newState.isInTime = isInTime - case .setCopySuccessToastMessage: - newState.shouldPresentCopySuccessToastMessage = true - case let .setPickSuccessToastMessage(name): - newState.shouldPresentPickSuccessToastMessage = name - case .setFailureToastMessage: - newState.shouldPresentFailureToastMessage = true case .setPageIndex(let index): newState.pageIndex = index case .updateMainData(let data): newState = updateMainData(newState, data) case .updateMainNight(let data): + newState.familyname = data.familyName newState.familySection = FamilySection.Model(model: 0, items: data.mainFamilyProfileDatas.map { .main(MainFamilyCellReactor($0, service: provider)) }).items @@ -274,17 +290,55 @@ extension MainViewReactor { newState = setBalloonText(newState) case .setDescriptionText: newState = setDescriptionText(newState) - case .showNextView(let type): - newState.openNextView = type + case .setFamilyManagement(let isFirst): + newState.isFirstFamilyManagement = isFirst + case .setPickMember(let id, let name): + newState.pickedMember = (id, name) } return newState } } +extension MainViewReactor { + private func pushViewController(type: MainViewReactor.OpenType) { + switch type { + case .monthlyCalendarViewController: + navigator.toMonthlyCalendar() + case .familyManagementViewController: + navigator.toFamilyManagement() + case .weeklycalendarViewController(let date): + navigator.toDailyCalendar(date) + case .cameraViewController(let type): + MPEvent.Home.cameraTapped.track(with: nil) + navigator.toCamera(type) + case .survivalAlert: + navigator.showSurvivalAlert() + case .pickAlert(let name, _): + navigator.pickAlert(name) + case .missionUnlockedAlert: + navigator.missionUnlockedAlert() + case .showToastMessage(let image, let message): + navigator.showToast(image, message) + case .showErrorToast: + navigator.showToast(DesignSystemAsset.warning.image, "에러가 발생했습니다") + } + } +} + +extension MainViewReactor: BBAlertDelegate { + func didTapAlertButton(_ alert: BBAlert?, index: Int?, button: BBButton) { + if index == 0 { + alert?.close() + self.action.onNext(.pickConfirmButtonTapped) + } + } +} + extension MainViewReactor { private func updateMainData(_ state: State, _ data: MainViewEntity) -> State { var newState = state + newState.familyname = data.familyName newState.isMissionUnlocked = data.isMissionUnlocked newState.isMeSurvivalUploadedToday = data.isMeSurvivalUploadedToday newState.isMeMissionUploadedToday = data.isMeMissionUploadedToday @@ -355,7 +409,7 @@ extension MainViewReactor { var newState = state guard let inTime = currentState.isInTime, - inTime else { + inTime else { newState.description = .survivalNone return newState } 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 64b385d13..a87cece7e 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift @@ -15,7 +15,7 @@ import RxDataSources import RxCocoa import RxSwift -final class MainViewController: BaseViewController, UICollectionViewDelegateFlowLayout { +final class MainViewController: BBNavigationViewController, UICollectionViewDelegateFlowLayout { private let familyViewController: MainFamilyViewController = MainFamilyViewControllerWrapper().makeViewController() private let timerView: TimerView = TimerView(reactor: TimerReactor()) @@ -27,7 +27,6 @@ final class MainViewController: BaseViewController, UICollectio private let pageViewController: SegmentPageViewController = SegmentPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal) private let cameraButton: MainCameraButtonView = MainCameraButtonView(reactor: MainCameraReactor()) - private let alertConfirmRelay: PublishRelay<(String, String)> = PublishRelay<(String, String)>() override func viewDidLoad() { super.viewDidLoad() @@ -36,7 +35,6 @@ final class MainViewController: BaseViewController, UICollectio } override func bind(reactor: MainViewReactor) { - print("bind main reactor") super.bind(reactor: reactor) bindInput(reactor: reactor) bindOutput(reactor: reactor) @@ -48,9 +46,9 @@ final class MainViewController: BaseViewController, UICollectio addChild(familyViewController) addChild(pageViewController) - view.addSubviews(familyViewController.view, timerView, descriptionLabel, - imageView, segmentControl, pageViewController.view, - cameraButton, contributorView) + contentView.addSubviews(familyViewController.view, timerView, descriptionLabel, + imageView, segmentControl, pageViewController.view, + cameraButton, contributorView) familyViewController.didMove(toParent: self) pageViewController.didMove(toParent: self) @@ -60,7 +58,7 @@ final class MainViewController: BaseViewController, UICollectio super.setupAutoLayout() familyViewController.view.snp.makeConstraints { - $0.top.equalTo(navigationBarView.snp.bottom) + $0.top.equalToSuperview() $0.horizontalEdges.equalToSuperview() $0.height.equalTo(138) } @@ -110,8 +108,9 @@ final class MainViewController: BaseViewController, UICollectio override func setupAttributes() { super.setupAttributes() - navigationBarView.do { - $0.setNavigationView(leftItem: .family, centerItem: .logo, rightItem: .calendar) + navigationBar.do { + $0.leftBarButtonItem = .person(new: false) + $0.rightBarButtonItem = .calendar } contributorView.do { @@ -123,7 +122,7 @@ final class MainViewController: BaseViewController, UICollectio extension MainViewController { private func bindInput(reactor: MainViewReactor) { Observable.merge( - Observable.just(()) + rx.viewWillAppear .map { _ in Reactor.Action.calculateTime }, NotificationCenter.default.rx.notification(UIScene.willEnterForegroundNotification) .map { _ in Reactor.Action.calculateTime } @@ -131,6 +130,11 @@ extension MainViewController { .bind(to: reactor.action) .disposed(by: disposeBag) + Observable.just(()) + .map { Reactor.Action.checkFamilyManagement } + .bind(to: reactor.action) + .disposed(by: disposeBag) + Observable.merge( segmentControl.survivalButton.rx.tap.map { Reactor.Action.didTapSegmentControl(.survival) }, segmentControl.missionButton.rx.tap.map { Reactor.Action.didTapSegmentControl(.mission) }, @@ -144,17 +148,14 @@ extension MainViewController { Observable.merge( contributorView.nextButtonTapEvent.map { Reactor.Action.openNextViewController(.contributorNextButtonTap)}, cameraButton.camerTapEvent.map { Reactor.Action.openNextViewController(.cameraButtonTap )}, - navigationBarView.rx.rightButtonTap.map { Reactor.Action.openNextViewController(.navigationRightButtonTap)}, - navigationBarView.rx.leftButtonTap.map { _ in Reactor.Action.openNextViewController(.navigationLeftButtonTap)} + navigationBar.rx.didTapRightBarButton.map { _ in Reactor.Action.openNextViewController(.navigationRightButtonTap)}, + navigationBar.rx.didTapLeftBarButton.map { _ in Reactor.Action.openNextViewController(.navigationLeftButtonTap)} ) .observe(on: MainScheduler.instance) - .throttle(RxConst.milliseconds300Interval, scheduler: MainScheduler.instance) + .throttle(RxInterval._300milliseconds, scheduler: MainScheduler.instance) .bind(to: reactor.action) .disposed(by: disposeBag) - alertConfirmRelay.map { Reactor.Action.pickConfirmButtonTapped($0.0, $0.1) } - .bind(to: reactor.action) - .disposed(by: disposeBag) contributorView.infoButton.rx.tap .throttle(RxConst.milliseconds300Interval, scheduler: MainScheduler.instance) @@ -172,6 +173,7 @@ extension MainViewController { } private func bindOutput(reactor: MainViewReactor) { + reactor.state.map { $0.isInTime }.compactMap { $0 } .distinctUntilChanged() .withUnretained(self) @@ -222,37 +224,15 @@ extension MainViewController { .bind(to: contributorView.contributorRelay) .disposed(by: disposeBag) - reactor.pulse(\.$openNextView).compactMap { $0 } + Observable.combineLatest( + reactor.state.map { $0.familyname }.distinctUntilChanged(), + reactor.state.map { $0.isFirstFamilyManagement }.distinctUntilChanged() + ) .withUnretained(self) .observe(on: MainScheduler.instance) - .bind(onNext: { $0.0.pushViewController(type: $0.1) }) - .disposed(by: disposeBag) - - reactor.pulse(\.$shouldPresentPickSuccessToastMessage) - .compactMap { $0 } - .bind(with: self) { owner, name in - owner.makeBibbiToastView( - text: "\(name)님에게 생존신고 알림을 보냈어요", - image: DesignSystemAsset.yellowPaperPlane.image - ) - } - .disposed(by: disposeBag) - - reactor.pulse(\.$shouldPresentCopySuccessToastMessage) - .filter { $0 } - .bind(with: self) { owner, _ in - owner.makeBibbiToastView( - text: "링크가 복사되었어요", - image: DesignSystemAsset.link.image - ) - } - .disposed(by: disposeBag) - - reactor.pulse(\.$shouldPresentFailureToastMessage) - .filter { $0 } - .bind(with: self) { owner, _ in - owner.makeErrorBibbiToastView() - } + .bind(onNext: { + $0.0.setNavigation(title: $0.1.0, isFirstFamilyManagement: $0.1.1) + }) .disposed(by: disposeBag) } } @@ -282,42 +262,14 @@ extension MainViewController { imageView.image = description.image } - private func pushViewController(type: MainViewReactor.OpenType) { - switch type { - case .monthlyCalendarViewController: - navigationController?.pushViewController(MonthlyCalendarDIConatainer().makeViewController(), animated: true) - case .familyManagementViewController: - navigationController?.pushViewController(FamilyManagementDIContainer().makeViewController(), animated: true) - case .weeklycalendarViewController(let date): - navigationController?.pushViewController(WeeklyCalendarDIConatainer(date: date.toDate()).makeViewController(), animated: true) - case .cameraViewController(let type): - MPEvent.Home.cameraTapped.track(with: nil) - navigationController?.pushViewController(CameraViewControllerWrapper(cameraType: type).viewController, animated: true) - case .survivalAlert: - BibbiAlertBuilder(self) - .alertStyle(.takeSurvival) - .setConfirmAction { [weak self] in - guard let self else { return } - self.navigationController?.pushViewController(CameraViewControllerWrapper(cameraType: .survival).viewController, animated: true) - } - .present() - case .pickAlert(let name, let id): - BibbiAlertBuilder(self) - .alertStyle(.pickMember(name)) - .setConfirmAction { [weak self] in - guard let self else { return } - self.alertConfirmRelay.accept((name, id)) } - .present() - case .missionUnlockedAlert: - BibbiAlertBuilder(self) - .alertStyle(.missionKey) - .setConfirmAction { [weak self] in - guard let self else { return } - self.navigationController?.pushViewController(CameraViewControllerWrapper(cameraType: .mission).viewController, animated: true) - } - .present() + private func setNavigation(title: String?, isFirstFamilyManagement: Bool) { + navigationBar.leftBarButtonItem = .person(new: isFirstFamilyManagement) + if let title { + navigationBar.navigationTitleFontStyle = .homeTitle + navigationBar.navigationTitle = title + } else { + navigationBar.navigationImage = .bibbi } - } } diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileViewController.swift index d29591239..e769ac8f4 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileViewController.swift @@ -37,8 +37,6 @@ public final class ProfileViewController: BaseViewController private lazy var profilePickerController: PHPickerViewController = PHPickerViewController(configuration: pickerConfiguration) private lazy var profileFeedViewController: ProfileFeedPageViewController = ProfileFeedPageViewControllerWrapper(memberId: reactor?.currentState.memberId ?? "").viewController - //TODO: - Test Code 추후 제거 - private let toolTipView: BBToolTipView = BBToolTipView(toolTipType: .activeCameraTime) public override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) @@ -59,7 +57,7 @@ public final class ProfileViewController: BaseViewController addChild(profileFeedViewController) //TODO: - Test Code 추후 제거 - view.addSubviews(profileView, profileLineView, profileFeedViewController.view, profileSegementControl, profileIndicatorView, toolTipView) + view.addSubviews(profileView, profileLineView, profileFeedViewController.view, profileSegementControl, profileIndicatorView) profileFeedViewController.didMove(toParent: self) } @@ -87,12 +85,6 @@ public final class ProfileViewController: BaseViewController $0.left.right.equalToSuperview() $0.bottom.equalTo(profileLineView.snp.top) } - //TODO: - Test Code 추후 제거 - toolTipView.snp.makeConstraints { - $0.width.equalTo(187) - $0.height.equalTo(52) - $0.center.equalToSuperview() - } profileLineView.snp.makeConstraints { $0.height.equalTo(1) diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBar/BBNavigationBar.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBar/BBNavigationBar.swift index 8ef50cf4e..d935b5748 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBar/BBNavigationBar.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBNavigationBar/BBNavigationBar.swift @@ -54,7 +54,7 @@ public class BBNavigationBar: UIView { /// NavigationBar의 Image를 바꿉니다. /// Image를 적용하면 Title이 사라집니다. - public var navigationImage: BBNavigationButtonStyle? { + public var navigationImage: BBNavigationTitleStyle? { didSet { navigationImageView.isHidden = false navigationTitleLabel.isHidden = true @@ -323,7 +323,7 @@ extension BBNavigationBar { /// - rightBarButtonItemScale: 네비게이션 바의 오른쪽 버튼 크기 /// - rightBarButtonYOffset: 네비게이션 바의 오른쪽 버튼 Y 위치 public func set( - _ image: BBNavigationButtonStyle? = nil, + _ image: BBNavigationTitleStyle? = nil, imageScale: CGFloat = 1.0, leftBarButtonItem: BBNavigationButtonStyle? = nil, leftBarButtonTint: UIColor = .gray300, 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 958df3f0e..0c1d8a415 100644 --- a/14th-team5-iOS/Data/Sources/APIs/App/Repository/AppRepository.swift +++ b/14th-team5-iOS/Data/Sources/APIs/App/Repository/AppRepository.swift @@ -11,12 +11,15 @@ import Foundation import RxSwift public final class AppRepository: AppRepositoryProtocol { - + // MARK: - Properties public let disposeBag = DisposeBag() // MARK: - APIWorker private let appApiWorker = AppAPIWorker() + + // MARK: - UserDefaults + private let appUserDefaults = AppUserDefaults() // MARK: - Intializer public init() { } @@ -37,4 +40,12 @@ extension AppRepository { .asObservable() } + public func fetchIsFirstFamilyManagement() -> RxSwift.Observable { + let isFirstFamily = appUserDefaults.loadIsFirstFamilyManagement() + return .just(isFirstFamily) + } + + public func saveIsFirstFamilyManagement(isFirst: Bool) { + appUserDefaults.saveIsFirstFamilyManagement(isFirst) + } } diff --git a/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/DataMapping/MainNightResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/DataMapping/MainNightResponseDTO.swift index 94634cd54..1f5648667 100644 --- a/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/DataMapping/MainNightResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/DataMapping/MainNightResponseDTO.swift @@ -41,7 +41,9 @@ struct MainNightResponseDTO: Codable { let familyMemberMonthlyRanking: FamilyMemberMonthlyRanking func toDomain() -> NightMainViewEntity { - return .init(mainFamilyProfileDatas: topBarElements.map { $0.toDomain() }, - familyRankData: familyMemberMonthlyRanking.toDomain()) + return .init( + familyName: topBarElements.first?.familyName, + mainFamilyProfileDatas: topBarElements.map { $0.toDomain() }, + familyRankData: familyMemberMonthlyRanking.toDomain()) } } diff --git a/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/DataMapping/MainResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/DataMapping/MainResponseDTO.swift index 616271083..3e1badf1c 100644 --- a/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/DataMapping/MainResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/DataMapping/MainResponseDTO.swift @@ -10,6 +10,7 @@ import Foundation import Domain struct TopBarElement: Codable { + let familyName: String? let memberId: String let imageUrl: String? let noImageLetter: String @@ -68,6 +69,7 @@ struct MainResponseDTO: Codable { func toDomain() -> MainViewEntity { return .init( + familyName: topBarElements.first?.familyName, mainFamilyProfileDatas: topBarElements.map { $0.toDomain() }, leftUploadCountUntilMissionUnlock: leftUploadCountUntilMissionUnlock, isMissionUnlocked: isMissionUnlocked, 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 00f59b933..0435d7d5f 100644 --- a/14th-team5-iOS/Data/Sources/Storages/UserDefaults/AppUserDefaults/AppUserDefaults.swift +++ b/14th-team5-iOS/Data/Sources/Storages/UserDefaults/AppUserDefaults/AppUserDefaults.swift @@ -8,7 +8,6 @@ import Core import Foundation - public protocol AppUserDefaultsType: UserDefaultsType { func saveIsFirstLaunchApp(_ value: Bool?) func loadIsFirstLaunchApp() -> Bool? @@ -23,7 +22,7 @@ public protocol AppUserDefaultsType: UserDefaultsType { func loadIsFirstOnboarding() -> Bool? func saveIsFirstFamilyManagement(_ value: Bool) - func loadIsFirstFamilyManagement() -> Bool + func loadIsFirstFamilyManagement() -> Bool? func saveInviteCode(_ inviteCode: String) func loadInviteCode() -> String? @@ -111,12 +110,8 @@ final public class AppUserDefaults: AppUserDefaultsType { userDefaults[.isFirstFamilyManagement] = value } - public func loadIsFirstFamilyManagement() -> Bool { - guard let isFirstFamilyManagement: Bool = userDefaults[.isFirstFamilyManagement] else { - return false - } - - return isFirstFamilyManagement + public func loadIsFirstFamilyManagement() -> Bool? { + return userDefaults[.isFirstFamilyManagement] } } diff --git a/14th-team5-iOS/Domain/Sources/Entities/MainView/MainViewEntity.swift b/14th-team5-iOS/Domain/Sources/Entities/MainView/MainViewEntity.swift index cf54dac6d..45791e546 100644 --- a/14th-team5-iOS/Domain/Sources/Entities/MainView/MainViewEntity.swift +++ b/14th-team5-iOS/Domain/Sources/Entities/MainView/MainViewEntity.swift @@ -18,6 +18,7 @@ public struct Picker { } public struct MainViewEntity { + public let familyName: String? public let mainFamilyProfileDatas: [FamilyMemberProfileEntity] public let leftUploadCountUntilMissionUnlock: Int public let isFamilySurvivalUploadedToday: Bool @@ -29,6 +30,7 @@ public struct MainViewEntity { public let pickers: [Picker] public init( + familyName: String?, mainFamilyProfileDatas: [FamilyMemberProfileEntity], leftUploadCountUntilMissionUnlock: Int, isMissionUnlocked: Bool, @@ -37,7 +39,9 @@ public struct MainViewEntity { pickers: [Picker], survivalUploadCount: Int, missionUploadCount: Int, - dailyMissionContent: String) { + dailyMissionContent: String + ) { + self.familyName = familyName self.mainFamilyProfileDatas = mainFamilyProfileDatas self.leftUploadCountUntilMissionUnlock = leftUploadCountUntilMissionUnlock self.isMissionUnlocked = isMissionUnlocked diff --git a/14th-team5-iOS/Domain/Sources/Entities/MainView/NightMainViewEntity.swift b/14th-team5-iOS/Domain/Sources/Entities/MainView/NightMainViewEntity.swift index c91dd491e..594c73e31 100644 --- a/14th-team5-iOS/Domain/Sources/Entities/MainView/NightMainViewEntity.swift +++ b/14th-team5-iOS/Domain/Sources/Entities/MainView/NightMainViewEntity.swift @@ -78,10 +78,16 @@ extension FamilyRankData { } public struct NightMainViewEntity { + public let familyName: String? public let mainFamilyProfileDatas: [FamilyMemberProfileEntity] public let familyRankData: FamilyRankData - public init(mainFamilyProfileDatas: [FamilyMemberProfileEntity], familyRankData: FamilyRankData) { + public init( + familyName: String?, + mainFamilyProfileDatas: [FamilyMemberProfileEntity], + familyRankData: FamilyRankData + ) { + self.familyName = familyName self.mainFamilyProfileDatas = mainFamilyProfileDatas self.familyRankData = familyRankData } diff --git a/14th-team5-iOS/Domain/Sources/Repositories/AppRepository.swift b/14th-team5-iOS/Domain/Sources/Repositories/AppRepository.swift index d6910c75d..5dcdb49cb 100644 --- a/14th-team5-iOS/Domain/Sources/Repositories/AppRepository.swift +++ b/14th-team5-iOS/Domain/Sources/Repositories/AppRepository.swift @@ -11,4 +11,7 @@ import RxSwift public protocol AppRepositoryProtocol { func fetchAppVersion() -> Observable + func fetchIsFirstFamilyManagement() -> Observable + + func saveIsFirstFamilyManagement(isFirst: Bool) -> Void } diff --git a/14th-team5-iOS/Domain/Sources/UseCases/App/FetchFamilyManagementUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/App/FetchFamilyManagementUseCase.swift new file mode 100644 index 000000000..8c226dab1 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/UseCases/App/FetchFamilyManagementUseCase.swift @@ -0,0 +1,34 @@ +// +// FetchIsFirstFamilyManagementUseCaseProtocol.swift +// Domain +// +// Created by 마경미 on 01.09.24. +// + +import Foundation + +import RxSwift + +public protocol FetchIsFirstFamilyManagementUseCaseProtocol { + func execute() -> Observable +} + +public class FetchFamilyManagementUseCase: FetchIsFirstFamilyManagementUseCaseProtocol { + + private let repository: AppRepositoryProtocol + + public init(repository: AppRepositoryProtocol) { + self.repository = repository + } + + public func execute() -> Observable { + repository.fetchIsFirstFamilyManagement() + .map { isFirst in + guard let isFirst else { + return false + } + return isFirst + } + .asObservable() + } +} diff --git a/14th-team5-iOS/Domain/Sources/UseCases/App/UpdateFamilyManagementUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/App/UpdateFamilyManagementUseCase.swift new file mode 100644 index 000000000..cf0334ed0 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/UseCases/App/UpdateFamilyManagementUseCase.swift @@ -0,0 +1,27 @@ +// +// SaveFamilyManagementUseCase.swift +// Domain +// +// Created by 마경미 on 16.09.24. +// + +import Foundation + +import RxSwift + +public protocol UpdateFamilyManagementUseCaseProtocol { + func execute(_ isFirst: Bool) +} + +public class UpdateFamilyManagementUseCase: UpdateFamilyManagementUseCaseProtocol { + + private let repository: AppRepositoryProtocol + + public init(repository: AppRepositoryProtocol) { + self.repository = repository + } + + public func execute(_ isFirst: Bool) { + repository.saveIsFirstFamilyManagement(isFirst: isFirst) + } +} From fa1b3ec5ce2032a6b2c3f464892a88fad4021868 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Fri, 20 Sep 2024 21:28:04 +0900 Subject: [PATCH 212/263] =?UTF-8?q?feat:=20BBAlertAction=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84,=20BBAlert=20=EB=AC=B8=EC=84=9C=20=EC=A3=BC=EC=84=9D?= =?UTF-8?q?=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BBAlert/AlertViews/BBAlertView.swift | 2 +- .../DefaultToastView/DefaultAlertView.swift | 53 +++-- .../Bibbi/BBCommons/BBAlert/BBAlert.swift | 225 +++++++++++++----- .../BBCommons/BBAlert/BBAlertAction.swift | 49 ++++ .../BBAlert/BBAlertActionStyle.swift | 86 +++++++ .../BBCommons/BBAlert/BBAlertAnimation.swift | 16 ++ .../BBCommons/BBAlert/BBAlertBackground.swift | 7 + .../BBCommons/BBAlert/BBAlertButton.swift | 27 --- .../BBAlert/BBAlertButtonsAxis.swift | 17 -- .../BBAlert/BBAlertConfiguration.swift | 20 ++ .../BBCommons/BBAlert/BBAlertDelegate.swift | 27 +++ .../BBCommons/BBAlert/BBAlertStyle.swift | 3 + .../BBAlert/BBAlertViewConfiguration.swift | 45 +++- .../BBCommons/BBAlert/ButtonLayout.swift | 44 ---- .../Popover/BibbiPopOverView.swift | 0 .../DescriptionPopoverViewController.swift | 0 16 files changed, 449 insertions(+), 172 deletions(-) create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertAction.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertActionStyle.swift delete mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertButton.swift delete mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertButtonsAxis.swift delete mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/ButtonLayout.swift rename 14th-team5-iOS/Core/Sources/{Bibbi/BBCommons => Trash}/Popover/BibbiPopOverView.swift (100%) rename 14th-team5-iOS/Core/Sources/{Bibbi/BBCommons => Trash}/Popover/DescriptionPopoverViewController.swift (100%) diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/AlertViews/BBAlertView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/AlertViews/BBAlertView.swift index dc006c54d..9d4d89461 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/AlertViews/BBAlertView.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/AlertViews/BBAlertView.swift @@ -8,5 +8,5 @@ import UIKit public protocol BBAlertView: UIView { - func createView(for alert: BBAlert) + func createView(for alert: BBAlert, actions: [BBAlertAction]) } 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 2e8b292d1..692d3a3d8 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 @@ -9,6 +9,7 @@ import UIKit public class DefaultAlertView: UIView, BBAlertView { + // MARK: - Views private let child: BBAlertStackView @@ -21,6 +22,8 @@ public class DefaultAlertView: UIView, BBAlertView { public var id: Int = -1 private var alert: BBAlert? + + private var actions: [BBAlertAction] = [] private let viewConfig: BBAlertViewConfiguration private let buttonSpacing: CGFloat = 8 @@ -47,8 +50,11 @@ public class DefaultAlertView: UIView, BBAlertView { // MARK: - Create - public func createView(for alert: BBAlert) { + public func createView(for alert: BBAlert, actions: [BBAlertAction]) { + precondition(!actions.isEmpty, "🟡 BBAlert에 적어도 하나 이상의 BBAlertAction이 필요합니다.") + self.alert = alert + self.actions = actions self.child.alert = alert setupUI() @@ -61,8 +67,8 @@ public class DefaultAlertView: UIView, BBAlertView { private func setupUI() { - for type in viewConfig.buttonLayout.buttons { - let button = createAlertButton(for: type) + for action in actions { + let button = createAlertButton(with: action) buttonStack.addArrangedSubview(button) } @@ -94,11 +100,11 @@ public class DefaultAlertView: UIView, BBAlertView { buttonStack.trailingAnchor.constraint(equalTo: self.layoutMarginsGuide.trailingAnchor), ]) - if case .vertical = viewConfig.buttonLayout.axis { + if case .vertical = viewConfig.buttonAxis { let count = CGFloat(buttonStack.arrangedSubviews.count) - buttonStack.heightAnchor.constraint(equalToConstant: count * viewConfig.buttonLayout.height + (buttonSpacing * count)).isActive = true + buttonStack.heightAnchor.constraint(equalToConstant: count * viewConfig.buttonHieght + (buttonSpacing * count)).isActive = true } else { - buttonStack.heightAnchor.constraint(equalToConstant: viewConfig.buttonLayout.height).isActive = true + buttonStack.heightAnchor.constraint(equalToConstant: viewConfig.buttonHieght).isActive = true } child.translatesAutoresizingMaskIntoConstraints = false @@ -113,7 +119,7 @@ public class DefaultAlertView: UIView, BBAlertView { private func setupButtonConstraints() { for button in buttonStack.arrangedSubviews { NSLayoutConstraint.activate([ - button.heightAnchor.constraint(equalToConstant: viewConfig.buttonLayout.height) + button.heightAnchor.constraint(equalToConstant: viewConfig.buttonHieght) ]) } } @@ -125,7 +131,7 @@ public class DefaultAlertView: UIView, BBAlertView { layer.cornerRadius = viewConfig.cornerRadius ?? 16 backgroundColor = viewConfig.backgroundColor - buttonStack.axis = viewConfig.buttonLayout.axis + buttonStack.axis = viewConfig.buttonAxis buttonStack.spacing = buttonSpacing buttonStack.alignment = .fill buttonStack.distribution = .fillEqually @@ -133,32 +139,27 @@ public class DefaultAlertView: UIView, BBAlertView { addShadow() } - private func createAlertButton(for type: BBAlert.Button) -> BBButton { + private func createAlertButton(with action: BBAlertAction) -> BBButton { let button = BBButton(type: .system) - switch type { - case let .normal(title, titleFontStyle, titleColor, backgroundColor, action): + switch action.style { + case let .custom(titleFontStyle, titleColor, backgroundColor): setupAlertButtotAttribute( button, - title: title, + title: action.title, titleFontStlye: titleFontStyle, titleColor: titleColor, backgroundColor: backgroundColor, - action: action - ) - - case let .confirm(title, action): - setupAlertButtotAttribute( - button, - title: title, - action: action + action: action.handler ) - case let .cancel(title): + default: setupAlertButtotAttribute( button, - title: title, - titleColor: .gray400, - backgroundColor: .gray700 + title: action.title, + titleFontStlye: action.style.titleFontStyle, + titleColor: action.style.titleColor, + backgroundColor: action.style.backgroundColor, + action: action.handler ) } @@ -171,7 +172,7 @@ public class DefaultAlertView: UIView, BBAlertView { titleFontStlye: BBFontStyle? = nil, titleColor: UIColor? = nil, backgroundColor: UIColor? = nil, - action: BBAlertAction = nil + action: BBAlertActionHandler = nil ) { self.id += 1 button.setId(id) @@ -185,7 +186,7 @@ public class DefaultAlertView: UIView, BBAlertView { button.layer.masksToBounds = false let action = UIAction { [weak self] _ in - guard let self = self else { return } + guard let self else { return } // 델리게이트 실행 BBAlert.multicast.invoke { $0.didTapAlertButton( diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlert.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlert.swift index 1b6aed376..4156c5d27 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlert.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlert.swift @@ -10,8 +10,49 @@ import UIKit // MARK: - Typelias -public typealias BBAlertAction = ((BBAlert?) -> Void)? +/// BBAlert 버튼의 동작을 정의하는 핸들러입니다. +public typealias BBAlertActionHandler = ((BBAlert?) -> Void)? + +// MARK: - BBAlert + +/// BBAlert는 BBAlert을 화면에 띄우게 도와줍니다. +/// +/// BBAlertAction 클래스로 BBAlert 버튼의 액션과 스타일을 설정할 수 있습니다. **show(after:)** 메서드로 BBAlert를 띄울 수 있으며, **close(animated:, completion:)** 메서드로 BBAlert를 사라지게 할 수 있습니다. +/// +/// 아래는 BBAlert를 띄우는 가장 기본적인 방법을 보여줍니다. +/// +/// ```swift +/// let cancelAction = BBAlertAction("취소", style: .cancel) +/// let okAction = BBAlertAction("확인") { [weak self] in +/// self?.didTapOkAction() +/// } +/// let alert = BBAlert.text( +/// "Hello, BBAlert!", +/// actions: [cancelAction, okAction] +/// ) +/// alert.show() +/// ``` +/// +/// - Important: BBAlert에 아무런 BBAlertAction을 추가하지 않고 **show(after:)** 메서드를 호출하면 크래시가 발생합니다. 적어도 하나 이상의 BBAlertAction이 필요합니다. 이는 BBAlert의 안전한 사용을 위해 의도되었습니다. +/// +/// BBAlert는 델리게이트 패턴을 지원합니다. BBAlert의 생명 주기에 맞게 필요한 동작을 구현할 수 있습니다. BBAlert는 멀티캐스트 델리게이트 패턴으로 구현되어 있습니다. +/// +/// 아래는 BBAlert에 델리게이트 패턴을 구현하는 방법을 보여줍니다. +/// +/// ```swift +/// // ViewController.swift +/// let alert = BBAlert.text("Hello, BBAlert!") +/// alert.addDelegate(self) +/// +/// extension ViewController: BBAlertDelgate { ... } +/// ```` +/// +/// - Note: 지원하는 델리게이트 메서드에 대한 자세한 정보는 ``BBAlertDelegate``를 참조하세요. +/// +/// ``BBAlertConfiguration``과 ``BBAlertViewConfiguration`` 구조체를 활용하여 BBAlert의 애니메이션, 배경 색상 및 BBAlert 뷰의 크기, 둥글기 반경, 버튼 배치 방향을 설정할 수 있습니다. +/// +/// - Authors: 김소월 public class BBAlert { // MARK: - Properties @@ -20,6 +61,8 @@ public class BBAlert { public let view: BBAlertView private var backgroundView: UIView? + + public private(set) var actions: [BBAlertAction] = [] public static var defaultImageTint: UIColor = .bibbiBlack @@ -36,6 +79,7 @@ public class BBAlert { /// - titleFontStyle: 타이틀의 폰트 스타일 /// - subtitle: 서브 타이틀 텍스트 /// - subtitleFontStyle: 서브 타이틀의 폰트 스타일 + /// - actions: 버튼의 액션과 스타일 /// - viewConfig: AlertView 설정값 /// - config: Alert 설정값 /// - Returns: BBAlert @@ -44,6 +88,7 @@ public class BBAlert { titleFontStyle: BBFontStyle? = nil, subtitle: String? = nil, subtitleFontStyle: BBFontStyle? = nil, + actions: [BBAlertAction] = [], viewConfig: BBAlertViewConfiguration = BBAlertViewConfiguration(), config: BBAlertConfiguration = BBAlertConfiguration() ) -> BBAlert { @@ -58,7 +103,7 @@ public class BBAlert { viewConfig: viewConfig ) - return BBAlert(view: view, config: config) + return BBAlert(view: view, actions: actions, config: config) } @@ -70,6 +115,7 @@ public class BBAlert { /// - titleFontStyle: 타이틀의 폰트 스타일 /// - subtitle: 서브 타이틀 텍스트 /// - subtitleFontStyle: 서브 타이틀의 폰트 스타일 + /// - actions: 버튼의 액션과 스타일 /// - viewConfig: AlertView 설정값 /// - config: Alert 설정값 /// - Returns: BBAlert @@ -80,6 +126,7 @@ public class BBAlert { titleFontStyle: BBFontStyle? = nil, subtitle: String? = nil, subtitleFontStyle: BBFontStyle? = nil, + actions: [BBAlertAction] = [], viewConfig: BBAlertViewConfiguration = BBAlertViewConfiguration(), config: BBAlertConfiguration = BBAlertConfiguration() ) -> BBAlert { @@ -96,7 +143,7 @@ public class BBAlert { viewConfig: viewConfig ) - return BBAlert(view: view, config: config) + return BBAlert(view: view, actions: actions, config: config) } /// 정해진 Style의 Alert를 생성합니다. @@ -107,18 +154,18 @@ public class BBAlert { /// - Returns: BBAlert public static func style( _ style: BBAlertStyle, - primaryAction action: BBAlertAction = nil, + primaryAction action: BBAlertActionHandler = nil, config: BBAlertConfiguration = BBAlertConfiguration() ) -> BBAlert { switch style { case .logout: - let layout = BBAlertButtonLayout( - buttons: [.cancel(), .confirm(action: action)], - axis: .horizontal - ) + let actions = [ + BBAlertAction(title: "취소", style: .cancel), + BBAlertAction(title: "확인", handler: action) + ] let viewConfig = BBAlertViewConfiguration( minHeight: 145, - buttonLayout: layout + buttonAxis: .horizontal ) let view = DefaultAlertView( child: TextAlertView( @@ -128,16 +175,16 @@ public class BBAlert { ), viewConfig: viewConfig ) - return BBAlert(view: view, config: config) + return BBAlert(view: view, actions: actions, config: config) case .makeNewFamily: - let layout = BBAlertButtonLayout( - buttons: [.cancel(), .confirm(action: action)], - axis: .horizontal - ) + let actions = [ + BBAlertAction(title: "취소", style: .cancel), + BBAlertAction(title: "확인", handler: action) + ] let viewConfig = BBAlertViewConfiguration( minHeight: 181, - buttonLayout: layout + buttonAxis: .horizontal ) let view = DefaultAlertView( child: TextAlertView( @@ -147,16 +194,16 @@ public class BBAlert { ), viewConfig: viewConfig ) - return BBAlert(view: view, config: config) + return BBAlert(view: view, actions: actions, config: config) case .resetFamilyName: - let layout = BBAlertButtonLayout( - buttons: [.cancel(), .confirm(action: action)], - axis: .horizontal - ) + let actions = [ + BBAlertAction(title: "취소", style: .cancel), + BBAlertAction(title: "확인", handler: action) + ] let viewConfig = BBAlertViewConfiguration( minHeight: 181, - buttonLayout: layout + buttonAxis: .horizontal ) let view = DefaultAlertView( child: TextAlertView( @@ -166,16 +213,16 @@ public class BBAlert { ), viewConfig: viewConfig ) - return BBAlert(view: view, config: config) + return BBAlert(view: view, actions: actions, config: config) case .widget: - let layout = BBAlertButtonLayout( - buttons: [.confirm(title: "확인하기", action: action), .cancel(title: "닫기")], - axis: .vertical - ) + let actions = [ + BBAlertAction(title: "확인하기", handler: action), + BBAlertAction(title: "닫기", style: .cancel) + ] let viewConfig = BBAlertViewConfiguration( minHeight: 384, - buttonLayout: layout + buttonAxis: .vertical ) let view = DefaultAlertView( child: ImageAlertView( @@ -186,16 +233,16 @@ public class BBAlert { ), viewConfig: viewConfig ) - return BBAlert(view: view, config: config) + return BBAlert(view: view, actions: actions, config: config) case .mission: - let layout = BBAlertButtonLayout( - buttons: [.confirm(title: "미션 사진 찍기", action: action), .cancel(title: "닫기")], - axis: .vertical - ) + let actions = [ + BBAlertAction(title: "미션 사진 찍기", handler: action), + BBAlertAction(title: "닫기", style: .cancel) + ] let viewConfig = BBAlertViewConfiguration( minHeight: 384, - buttonLayout: layout + buttonAxis: .vertical ) let view = DefaultAlertView( child: ImageAlertView( @@ -206,16 +253,16 @@ public class BBAlert { ), viewConfig: viewConfig ) - return BBAlert(view: view, config: config) + return BBAlert(view: view, actions: actions, config: config) case let .picking(name): - let layout = BBAlertButtonLayout( - buttons: [.confirm(title: "지금 하기", action: action), .cancel(title: "다음에 하기")], - axis: .vertical - ) + let actions = [ + BBAlertAction(title: "지금 하기", handler: action), + BBAlertAction(title: "다음에 하기", style: .cancel) + ] let viewConfig = BBAlertViewConfiguration( minHeight: 384, - buttonLayout: layout + buttonAxis: .vertical ) let view = DefaultAlertView( child: ImageAlertView( @@ -226,16 +273,16 @@ public class BBAlert { ), viewConfig: viewConfig ) - return BBAlert(view: view, config: config) + return BBAlert(view: view, actions: actions, config: config) case .takePhoto: - let layout = BBAlertButtonLayout( - buttons: [.confirm(title: "생존 신고 먼저하기", action: action), .cancel(title: "다음에 하기")], - axis: .vertical - ) + let actions = [ + BBAlertAction(title: "셍존 신고 먼저하기", handler: action), + BBAlertAction(title: "다음에 하기", style: .cancel) + ] let viewConfig = BBAlertViewConfiguration( minHeight: 384, - buttonLayout: layout + buttonAxis: .vertical ) let view = DefaultAlertView( child: ImageAlertView( @@ -246,13 +293,21 @@ public class BBAlert { ), viewConfig: viewConfig ) - return BBAlert(view: view, config: config) + return BBAlert(view: view, actions: actions, config: config) } } + /// 직접 커스텀한 뷰로 BBAlert을 생성합니다. + /// - Parameters: + /// - child: BBAlertStackView 프로토콜을 준수하는 UIView + /// - actions: 버튼의 액션과 스타일 + /// - viewConfig: AlertView 설정값 + /// - config: Alert 설정값 + /// - Returns: BBAlert public static func custom( - _ child: BBAlertStackView, + _ child: any BBAlertStackView, + actions: [BBAlertAction] = [], viewConfig: BBAlertViewConfiguration = BBAlertViewConfiguration(), config: BBAlertConfiguration = BBAlertConfiguration() ) -> BBAlert { @@ -261,16 +316,24 @@ public class BBAlert { viewConfig: viewConfig ) - return BBAlert(view: view, config: config) + return BBAlert(view: view, actions: actions, config: config) } // MARK: - Show + + /// BBAlert를 화면에 보이게 합니다. + /// - Parameters: + /// - type: HapticFeedback의 타입 + /// - time: 지연 시간 public func show(haptic type: UINotificationFeedbackGenerator.FeedbackType, after time: TimeInterval = 0) { - UINotificationFeedbackGenerator().notificationOccurred(type) + Haptic.notification(type: type) show(after: time) } + + /// BBAlert를 화면에 보이게 합니다. + /// - Parameter delay: 지연 시간 public func show(after delay: TimeInterval = 0) { if let backgroundView = self.createBackgroundView() { self.backgroundView = backgroundView @@ -278,7 +341,7 @@ public class BBAlert { } config.view?.addSubview(view) ?? BBHelper.topController()?.view.addSubview(view) - view.createView(for: self) + view.createView(for: self, actions: self.actions) Self.multicast.invoke { $0.willShowAlert(self) } @@ -302,6 +365,13 @@ public class BBAlert { // MARK: - Close + + /// BBAlert를 화면에 사라지게 합니다. + /// + /// BBAlert이 화면에 사라지고 나면 완료 핸들러가 호출됩니다. 완료 핸들러는 델리게이트의 `didCloseAlert(_:)` 메서드가 호출되기 전에 실행됩니다. + /// - Parameters: + /// - animated: 애니메이션 유무 + /// - completion: 완료 핸들러 public func close( animated: Bool = true, completion: (() -> Void)? = nil @@ -329,9 +399,11 @@ public class BBAlert { public required init( view: BBAlertView, + actions: [BBAlertAction], config: BBAlertConfiguration ) { self.view = view + self.actions = actions self.config = config } @@ -342,10 +414,59 @@ public class BBAlert { extension BBAlert { + /// 델리게이트 패턴을 적용합니다. + /// - Parameter delegate: BBAlertDelgate 프로토콜을 준수하는 객체 public func addDelegate(_ delegate: BBAlertDelegate) { Self.multicast.add(delegate) } + /// 버튼을 추가합니다. + /// - Parameter action: 버튼의 액션과 스타일 + public func addAction(_ action: BBAlertAction) { + self.actions.append(action) + } + + /// 버튼을 추가합니다. + /// - Parameter action: 버튼의 액션과 스타일 + public func addAction(_ actions: BBAlertAction...) { + actions.forEach { + self.actions.append($0) + } + } + + /// 버튼을 추가합니다. + /// - Parameters: + /// - title: 버튼의 타이틀 + /// - style: 버튼의 스타일 + /// - handler: 버튼의 액션 + public func addAction( + title: String? = nil, + style: BBAlertActionStyle = .default, + handler: BBAlertActionHandler = nil + ) { + self.actions.append( + BBAlertAction(title: title, style: style, handler: handler) + ) + } + + + /// 특정 인덱스에 위치한 버튼을 삭제합니다. + /// - Parameter index: 인덱스 + /// - Returns: BBAlertAction + @discardableResult + public func removeAction(_ index: Int) -> BBAlertAction { + self.actions.remove(at: index) + } + + /// 모든 버튼을 삭제합니다. + public func removeAllAction() { + self.actions.removeAll() + } + +} + +extension BBAlert { + private func createBackgroundView() -> UIView? { switch config.background { case .none: @@ -359,10 +480,6 @@ extension BBAlert { } } -} - -extension BBAlert { - private func closeOverlappedAlerts() { BBAlert.activeAlerts.forEach { alert in alert.close(animated: false) diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertAction.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertAction.swift new file mode 100644 index 000000000..b0864a7be --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertAction.swift @@ -0,0 +1,49 @@ +// +// BBAlertAction.swift +// Core +// +// Created by 김건우 on 9/18/24. +// + +import UIKit + +/// 버튼의 타이틀, 스타일과 액션을 설정하는 클래스입니다. +/// +/// BBAlert 객체의 `actions` 프로퍼티에 할당하면 얼럿에 버튼과 액션이 적용됩니다. `handler`에 아무런 액션을 적용하지 않으면 Alert을 닫는 동작이 기본적으로 적용됩니다. +/// +/// - Authors: 김소월 +public class BBAlertAction { + + // MARK: - Properties + + /// 버튼의 타이틀을 설정합니다. + public var title: String? + + /// 버튼의 스타일을 설정합니다. + /// + /// - seealso: `BBAlertAction.Style` + public var style: BBAlertActionStyle + + /// 버튼의 액션을 설정합니다. + public var handler: BBAlertActionHandler + + + // MARK: - Intializer + + /// 버튼의 타이틀, 스타일과 액션을 설정하는 클래스입니다. + /// - Parameters: + /// - title: 버튼의 타이틀 + /// - style: 버튼의 스타일 + /// - handler: 버튼의 액션 + public init( + title: String? = nil, + style: BBAlertActionStyle = .default, + handler: BBAlertActionHandler = nil + ) { + self.title = title + self.style = style + self.handler = handler + } + +} + diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertActionStyle.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertActionStyle.swift new file mode 100644 index 000000000..2a9cebf93 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertActionStyle.swift @@ -0,0 +1,86 @@ +// +// BBAlertActionStyle.swift +// Core +// +// Created by 김건우 on 9/18/24. +// + +import UIKit + +// MARK: - Typealias + +/// 버튼의 스타일을 설정합니다. 모든 `BBAlertAction`의 기본 스타일은 `default`입니다. +/// +/// - Authors: 김소월 +public typealias BBAlertActionStyle = BBAlertAction.Style + +extension BBAlertAction { + + public enum Style { + + /// 가장 기본적인 버튼 스타일입니다. + case `default` + + /// 취소 액션에 어울리는 버튼 스타일입니다. + case cancel + + /// 주의를 요하는 액션에 어울리는 버튼 스타일입니다. + case destructive + + /// 직접 원하는 폰트 스타일, 글자 색상, 배경 색상을 정할 수 있는 버튼 스타일입니다. + case custom( + titleFontStyle: BBFontStyle? = nil, + titleColor: UIColor? = nil, + backgroundColor: UIColor? = nil + ) + } + +} + + +// MARK: - Extension + +extension BBAlertActionStyle { + + var titleFontStyle: BBFontStyle? { + switch self { + case .default, + .cancel, + .destructive: + return BBFontStyle.body1Bold + + default: + return nil + } + } + + var titleColor: UIColor? { + switch self { + case .default: + return UIColor.bibbiBlack + + case .cancel: + return UIColor.bibbiWhite + + case .destructive: + return UIColor.systemRed + + default: + return nil + } + } + + var backgroundColor: UIColor? { + switch self { + case .default: + return UIColor.mainYellow + + case .destructive, .cancel: + return UIColor.gray700 + + default: + return nil + } + } + +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertAnimation.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertAnimation.swift index 94ee53fa5..eb10c27b4 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertAnimation.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertAnimation.swift @@ -9,7 +9,23 @@ import UIKit extension BBAlert { + /// BBAlert 뷰가 나타나거나 사라질 때 수행되는 애니메이션을 정의한 열거형입니다. + /// + /// BBAlert 뷰가 나타날 때(enteringAnimation)는 뷰의 초기 상태를 전달해야 합니다. BBAlert 뷰가 사라질 때(exitingAnimation)은 뷰의 최종 상태를 전달해야 합니다. + /// + /// 예를 들어, BBAlert 뷰가 나타나거나 사라질 때 페이드 효과를 주기 원한다면 아래와 같이 ``BBAlertConfiguration``을 설정해야 합니다. + /// + /// ```swift + /// let config = BBConfiguration( + /// enteringAnimation: .fade(alpha: 0), + /// exitingAnimation: .fade(alpha: 0) + /// ) + /// ``` + /// + /// - Authors: 김소월 public enum Animation { + + /// BBAlert 뷰에 페이드 효과를 줍니다. case fade(alpha: CGFloat) case scaleAndFade(scaleX: CGFloat, scaleY: CGFloat, alpha: CGFloat) case custom(transformation: CGAffineTransform) diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertBackground.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertBackground.swift index 5bd28f5ff..e4f033727 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertBackground.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertBackground.swift @@ -9,8 +9,15 @@ import UIKit extension BBAlert { + /// Alert 뒤의 배경 색상을 설정합니다. + /// + /// - Authors: 김소월 public enum Background { + + /// 아무런 배경 색상을 적용하지 않습니다. case none + + // 특정 색상을 배경 색상으로 적용합니다. case color(color: UIColor = defaultImageTint.withAlphaComponent(0.25)) } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertButton.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertButton.swift deleted file mode 100644 index 5f934c1ce..000000000 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertButton.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// Button.swift -// BBAlert -// -// Created by 김건우 on 8/7/24. -// - -import UIKit - -extension BBAlert { - - public enum Button { - case normal( - title: String? = "확인", - titleFontStyle: BBFontStyle? = nil, - titleColor: UIColor? = nil, - backgroundColor: UIColor? = nil, - action: BBAlertAction = nil - ) - case confirm( - title: String? = "확인", - action: BBAlertAction = nil - ) - case cancel(title: String = "취소") - } - -} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertButtonsAxis.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertButtonsAxis.swift deleted file mode 100644 index 1dc26faef..000000000 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertButtonsAxis.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// ButtonAxis.swift -// BBAlert -// -// Created by 김건우 on 8/7/24. -// - -import Foundation - -extension BBAlert { - - public enum ButtonsAxis { - case vertical - case horizontal - } - -} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertConfiguration.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertConfiguration.swift index 415afa9e5..2f6de459d 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertConfiguration.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertConfiguration.swift @@ -7,20 +7,40 @@ import UIKit +/// BBAlert의 애니메이션, 오버랩 허용 유무를 설정할 수 있습니다. +/// +/// - Note: 높이, 너비, 배경 색상과 둥글기 반경, 버튼 축 방향과 높이 등 BBAlert 뷰에 대한 설정은 BBAlertViewConfiguration에서 하세요. +/// +/// - Authors: 김소월 public struct BBAlertConfiguration { // MARK: - Properties + /// Alert이 나타날 때 수행되는 애니메이션입니다. public let enteringAnimation: BBAlert.Animation + + /// Alert이 사라질 때 수행되는 애니메이션입니다. public let exitingAnimation: BBAlert.Animation + + /// Alert 뒤에 배경의 색상을 설정합니다. public let background: BBAlert.Background + + /// 여러 Alert의 겹치기 가능 여부를 설정합니다. public let allowOverlapAlert: Bool + /// Alert이 보여질 뷰를 설정합니다. public let view: UIView? // MARK: - Intializer + /// BBAlert의 애니메이션, 오버랩 허용 유무를 설정할 수 있습니다. + /// - Parameters: + /// - view: Alert이 보여질 뷰 + /// - enteringAnimation: Alert이 나타날 때 수행되는 애니메이션 + /// - exitingAnimation: Alert이 사라질 때 수행되는 애니메이션 + /// - background: Alert 뒤에 배경의 색상 + /// - allowOverlapAlert: Alert 겹치기 허용 유무 public init( attachedTo view: UIView? = nil, enteringAnimation: BBAlert.Animation = .default, diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertDelegate.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertDelegate.swift index 1ab9b0575..a2b769552 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertDelegate.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertDelegate.swift @@ -7,13 +7,40 @@ import Foundation +/// BBAlert의 생명 주기에 따라 필요한 동작을 정의하세요. +/// +/// - Authors: 김소월 public protocol BBAlertDelegate: AnyObject { + /// Alert이 화면에 나타나기 전 호출됩니다. + /// + /// - Note: Optional func willShowAlert(_ alert: BBAlert) + + /// Alert이 화면에 나타난 후 호출됩니다. + /// + /// - Note: Optional func didShowAlert(_ alert: BBAlert) + + /// Alert이 화면에 사라지기 전 호출됩니다. + /// + /// - Note: Optional func willCloseAlert(_ alert: BBAlert) + + /// Alert이 화면에 사라진 후 호출됩니다. + /// + /// - Note: Optional func didCloseAlert(_ alert: BBAlert) + /// Alert의 버튼을 클릭하면 호출됩니다. + /// + /// BBAlertAction으로 미리 액션을 전달했다면 해당 메서드를 구현할 필요가 없습니다. 런타임에 동적으로 버튼의 액션이 바뀌어야 한다면 구현하세요. + /// + /// BBAlert 버튼의 배치 방향이 수직(vertical)라면 제일 위 버튼의 인덱스가 0이 됩니다. 배치 방향이 수평(horizontal)이라면 제일 왼쪽 버튼의 인덱스가 0이 됩니다. + /// + /// Alert의 버튼을 클릭하면 이 메서드가 먼저 호출되고, BBAlertAction에 정의된 액션이 실행됩니다. + /// + /// - Note: Optional func didTapAlertButton(_ alert: BBAlert?, index: Int?, button: BBButton) } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertStyle.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertStyle.swift index 59076ad56..400e12484 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertStyle.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertStyle.swift @@ -9,6 +9,9 @@ import Foundation // MARK: - Typealias +/// BBAlert의 편리 사용을 위한 미리 정의되어 있는 Alert 스타일입니다. +/// +/// - Authors: 김소월 public typealias BBAlertStyle = BBAlert.Style extension BBAlert { diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertViewConfiguration.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertViewConfiguration.swift index 4324a9cd7..f428bf817 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertViewConfiguration.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertViewConfiguration.swift @@ -7,24 +7,60 @@ import UIKit +// MARK: - Typealias + +/// 버튼을 어느 방향으로 배치할 지 설정할 수 있습니다. +public typealias BBAlertButtonAxis = NSLayoutConstraint.Axis + + +/// BBAlert 뷰의 높이, 너비, 배경 색상과 둥글기 반경, 버튼 축 방향과 높이를 설정할 수 있습니다. +/// +/// - Note: 애니메이션, 오버랩 허용 유무 등 BBAlert에 대한 설정은 BBAlertConfiguration에서 하세요. +/// - Authors: 김소월 public struct BBAlertViewConfiguration { // MARK: - Properties + /// BBAlert 뷰의 최소 너비입니다. public let minWidth: CGFloat + + /// BBAlert 뷰의 최소 높이입니다. public let minHeight: CGFloat + /// BBAlert 뷰의 타이틀의 라인 수를 설정합니다. public let titleNumberOfLines: Int + + /// BBAlert 뷰의 서브 타이틀의 라인 수를 설정합니다. public let subtitleNumberOfLines: Int + /// BBAlert 뷰의 배경 색상을 설정합니다. + /// + /// - Note: ``BBAlertConfiguration``의 `background` 프로퍼티는 Alert 뒤의 배경 색상을 지정합니다. public let backgroundColor: UIColor? + + /// BBAlert 뷰의 둥글기 반경을 설정합니다. public let cornerRadius: CGFloat? - public let buttonLayout: BBAlertButtonLayout + /// BBAlert 뷰의 버튼 배치 방향을 설정합니다. + public let buttonAxis: BBAlertButtonAxis + + /// BBAlert 뷰의 버튼 높이를 설정합니다. 모든 버튼의 높이에 적용됩니다. + public let buttonHieght: CGFloat // MARK: - Intializer + + /// BBAlert 뷰의 높이, 너비, 배경 색상과 둥글기 반경, 버튼 축 방향과 높이를 설정할 수 있습니다. + /// - Parameters: + /// - minWidth: BBAlert 뷰의 최소 너비 + /// - minHeight: BBAlert 뷰의 최소 높이 + /// - titleNumberOfLines: BBAlert 뷰의 타이틀의 라인 수 + /// - subtitleNumberOfLines: BBAlert 뷰의 서브 타이틀의 라인 수 + /// - backgroundColor: BBAlert 뷰의 배경 색상 + /// - cornerRadius: BBAlert 뷰의 둥글기 반경 + /// - buttonAxis: BBAlert 뷰의 버튼 배치 방향 + /// - buttonHeight: BBAlert 뷰의 버튼 높이 public init( minWidth: CGFloat = 280, minHeight: CGFloat = 384, @@ -32,7 +68,8 @@ public struct BBAlertViewConfiguration { subtitleNumberOfLines: Int = 10, backgroundColor: UIColor? = .gray900, cornerRadius: CGFloat? = nil, - buttonLayout: BBAlertButtonLayout = BBAlertButtonLayout() + buttonAxis: BBAlertButtonAxis = .vertical, + buttonHeight: CGFloat = 44 ) { self.minWidth = minWidth self.minHeight = minHeight @@ -40,6 +77,8 @@ public struct BBAlertViewConfiguration { self.subtitleNumberOfLines = subtitleNumberOfLines self.backgroundColor = backgroundColor self.cornerRadius = cornerRadius - self.buttonLayout = buttonLayout + self.buttonAxis = buttonAxis + self.buttonHieght = buttonHeight } + } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/ButtonLayout.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/ButtonLayout.swift deleted file mode 100644 index 380df6125..000000000 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/ButtonLayout.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// BBAlertButtonConfiguration.swift -// Core -// -// Created by 김건우 on 8/9/24. -// - -import UIKit - -// MARK: - Typealias - -public typealias BBAlertButtonLayout = BBAlertViewConfiguration.ButtonLayout - -extension BBAlertViewConfiguration { - - public typealias BBAlertButtonAxis = NSLayoutConstraint.Axis - - public struct ButtonLayout { - - // MARK: - Properties - - public let buttons: [BBAlert.Button] - public let axis: BBAlertButtonAxis - public let height: CGFloat - - - // MARK: - Intializer - - public init( - buttons: [BBAlert.Button] = [ - .confirm(title: "확인"), - .cancel() - ], - axis: BBAlertButtonAxis = .vertical, - height: CGFloat = 44 - ) { - self.buttons = buttons - self.axis = axis - self.height = height - } - - } - -} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/Popover/BibbiPopOverView.swift b/14th-team5-iOS/Core/Sources/Trash/Popover/BibbiPopOverView.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/Popover/BibbiPopOverView.swift rename to 14th-team5-iOS/Core/Sources/Trash/Popover/BibbiPopOverView.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/Popover/DescriptionPopoverViewController.swift b/14th-team5-iOS/Core/Sources/Trash/Popover/DescriptionPopoverViewController.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/Popover/DescriptionPopoverViewController.swift rename to 14th-team5-iOS/Core/Sources/Trash/Popover/DescriptionPopoverViewController.swift From cfcf82ea6fcd4f8664a1691eb2ed96d59b3fbe47 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Sun, 22 Sep 2024 09:45:48 +0900 Subject: [PATCH 213/263] Update PULL_REQUEST_TEMPLATE.md (#642) --- .github/PULL_REQUEST_TEMPLATE.md | 61 +++++++++++++++++++++++--------- 1 file changed, 45 insertions(+), 16 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index fe4bfbaeb..93f52e264 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,29 +1,58 @@ -## 작업 내용 🧑‍💻 -- TODO +## 🔵PR을 올리기 전 아래 사항을 확인해주세요. +- 구현한 로직과 기능이 올바르게 작동되는지 충분히 테스트해주세요. +- 코드의 성능이나 메모리 효율성이 적절하게 고려되었는지, 불필요한 코드가 없는지 검토해주세요. +- 이번 PR에서 구현된 주요 기능이나 해결된 문제에 대해 자세히 서술해주세요. +(위 내용은 지워주세요) -- TODO -- TODO -## 변경 로직 ⚒️ +## 😽개요 -- TODO +* (작업한 코드에 대한 간략한 설명해주세요) -- TODO +## 🛠️작업 내용 -- TODO +### (소제목) -## 스크린샷 📷 +* (작업한 코드에 대한 자세한 설명해주세요. 필요하다면 이미지나 표를 첨부해주세요) -| 기능 | 스크린 샷 | -| :--: | :-------: | -| GIF | | +| 이미지① | +| :----: | +| | -## 테스트 케이스 ✅ -- [ ] todo -- [ ] todo -- [ ] todo +### (소제목) +* (작업한 코드에 대한 자세한 설명해주세요. 필요하다면 이미지나 표를 첨부해주세요) + +| 이미지① | +| :----: | +| | + + +## 🟡차후 계획 (Optional) + +* (작업한 코드가 추후 우리 프로젝트에 어떤 영향을 끼칠지, 앞으로 PR계획을 설명해주세요) + + +## 🗾이미지 (Optional) + +| 이미지① | +| :----: | +| | + + +## ✅테스트 케이스 + +* (구현한 로직 검증을 위해 테스트한 내용을 설명해주세요) + +--- + +#### 🙏🏻아래와 같이 PR을 리뷰해주세요. +- PR 내용이 부족하다면 보충 요청해주세요. +- 코드 스타일이 팀의 규칙에 맞게 작성되었는지, 일관성을 유지하고 있는지 확인해주세요. +- 코드에 대한 문서화나 주석이 필요한 부분에 적절하게 작성되어 있는지 확인해주세요. +- 구현된 로직이 효율적이고 올바르게 작성되었는지, 아키텍처를 잘 준수하고 있는지 검토해주세요. +- 네이밍, 포매팅, 주석 등 코드의 일관성이 유지되고 있는지 확인해주세요. From a8699ed00994b57d99ebd1e0616c8106035a31e7 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Sun, 22 Sep 2024 14:41:32 +0900 Subject: [PATCH 214/263] =?UTF-8?q?feat:=20BBAlertService=EA=B5=AC?= =?UTF-8?q?=ED=98=84,=20Pick=20=EB=A1=9C=EC=A7=81=EC=9D=B4=20=EC=A0=9C?= =?UTF-8?q?=EB=8C=80=EB=A1=9C=20=EC=9D=B4=EB=A4=84=EC=A7=80=EC=A7=80=20?= =?UTF-8?q?=EB=AA=BB=ED=95=98=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#640)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: BBAlertService구현, Pick로직문제수정 * feat: BBAlertService에 문서 주석 작성 --- .../Reactor/Cell/MainFamilyCellReactor.swift | 85 ++++- .../Home/Reactor/MainViewReactor.swift | 2 +- .../Views/MainFamilyCollectionViewCell.swift | 7 +- .../Bibbi/BBServices/BBAlertService.swift | 315 ++++++++++++++++++ .../Bibbi/BBServices/ServiceProvider.swift | 5 + 5 files changed, 395 insertions(+), 19 deletions(-) create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBServices/BBAlertService.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/Cell/MainFamilyCellReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/Cell/MainFamilyCellReactor.swift index 9cd5334cc..55b655fbf 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/Cell/MainFamilyCellReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/Cell/MainFamilyCellReactor.swift @@ -5,9 +5,10 @@ // Created by 마경미 on 21.04.24. // -import Foundation import Core +import Foundation +import DesignSystem import Domain import ReactorKit @@ -18,18 +19,47 @@ enum RankBadge: Int { case three = 3 } + +// MARK: - BBAlertActionType + +enum PickAlertAction: BBAlertActionType { + case pick + case cancel + + var title: String? { + switch self { + case .pick: return "지금 하기" + case .cancel: return "다음에 하기" + } + } + + var style: BBAlertActionStyle { + switch self { + case .pick: return .default + case .cancel: return .cancel + } + } +} + + + + +// MARK: - Reactor + final class MainFamilyCellReactor: Reactor { // MARK: - Action enum Action { case fetchData - case pickButtonTapped + + case didTapPickButton } // MARK: - Mutation enum Mutation { case setData - case setPickButtonAppearent(Bool) + + case setHiddenPickButton(Bool) } // MARK: - State @@ -38,12 +68,17 @@ final class MainFamilyCellReactor: Reactor { var profile: (imageUrl: String?, name: String) = (nil, .none) var rank: Int? = nil var isShowBirthdayBadge: Bool = false - var isShowPickButton: Bool = false + + var hiddenPickButton: Bool = false } // MARK: - Properties + let initialState: State - let provider: ServiceProviderProtocol + + @Injected var pickMemberUseCase: PickMemberUseCaseProtocol + + @Injected var provider: ServiceProviderProtocol // MARK: - Intializer init(_ profileData: FamilyMemberProfileEntity, service provider: ServiceProviderProtocol) { @@ -60,7 +95,7 @@ final class MainFamilyCellReactor: Reactor { guard id == self.currentState.profileData.memberId else { return Observable.empty() } - return Observable.just(.setPickButtonAppearent(show)) + return Observable.just(.setHiddenPickButton(show)) default: return Observable.empty() @@ -76,12 +111,34 @@ final class MainFamilyCellReactor: Reactor { case .fetchData: return Observable.just(.setData) - case .pickButtonTapped: - provider.mainService.pickButtonTapped( - name: currentState.profileData.name, - memberId: currentState.profileData.memberId + case .didTapPickButton: + let actions: [PickAlertAction] = [.pick, .cancel] + let memberId = initialState.profileData.memberId + let memberName = initialState.profileData.name + + return provider.bbAlertService.show( + image: DesignSystemAsset.exhaustedBibbiGraphic.image, + title: "생존 확인하기", + subtitle: "\(memberName)님의 생존 여부를 물어볼까요?\n지금 알림이 전송됩니다.", + actions: actions ) - return Observable.empty() + .withUnretained(self) + .flatMap { + switch $0.1 { + case .pick: + return $0.0.pickMemberUseCase.execute(memberId: memberId) + .flatMap { + guard + let entity = $0, entity.success + else { return Observable.empty() } + + return Observable.just(.setHiddenPickButton(true)) + } + + default: + return Observable.empty() + } + } } } @@ -94,10 +151,10 @@ final class MainFamilyCellReactor: Reactor { newState.profile = (currentState.profileData.profileImageURL, currentState.profileData.name) newState.rank = currentState.profileData.postRank newState.isShowBirthdayBadge = currentState.profileData.isShowBirthdayMark - newState.isShowPickButton = currentState.profileData.isShowPickIcon + newState.hiddenPickButton = !currentState.profileData.isShowPickIcon - case let .setPickButtonAppearent(show): - newState.isShowPickButton = show + case let .setHiddenPickButton(hidden): + newState.hiddenPickButton = hidden } return newState diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift index 77ee7f411..0deb119e3 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift @@ -281,7 +281,7 @@ extension MainViewReactor { case .updateMainNight(let data): newState.familyname = data.familyName newState.familySection = FamilySection.Model(model: 0, items: data.mainFamilyProfileDatas.map { - .main(MainFamilyCellReactor($0, service: provider)) + .main(MainFamilyCellReactor($0, service: provider)) }).items newState.contributor = data.familyRankData case .setCamerEnabled: diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Views/MainFamilyCollectionViewCell.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Views/MainFamilyCollectionViewCell.swift index 64312875f..dd92e3dc8 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Views/MainFamilyCollectionViewCell.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Views/MainFamilyCollectionViewCell.swift @@ -123,8 +123,8 @@ extension MainFamilyCollectionViewCell { .disposed(by: disposeBag) pickButton.rx.tap - .throttle(RxConst.milliseconds300Interval, scheduler: RxSchedulers.main) - .map { Reactor.Action.pickButtonTapped } + .throttle(RxInterval._300milliseconds, scheduler: RxScheduler.main) + .map { Reactor.Action.didTapPickButton } .bind(to: reactor.action) .disposed(by: disposeBag) } @@ -150,9 +150,8 @@ extension MainFamilyCollectionViewCell { .bind(to: birthdayBadge.rx.isHidden) .disposed(by: disposeBag) - reactor.state.map { $0.isShowPickButton } + reactor.state.map { $0.hiddenPickButton } .distinctUntilChanged() - .map { !$0 } .bind(to: pickButton.rx.isHidden) .disposed(by: disposeBag) } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBServices/BBAlertService.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBServices/BBAlertService.swift new file mode 100644 index 000000000..036c577d8 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBServices/BBAlertService.swift @@ -0,0 +1,315 @@ +// +// BBAlertService.swift +// Core +// +// Created by 김건우 on 9/20/24. +// + +import UIKit + +import RxSwift + + +// MARK: - BBAlertActionType + +/// BBAlertActionType은 BBAlert의 버튼 스타일과 액션을 정의하도록 도와주는 프로토콜입니다. +/// +/// 우리가 Reactor의 Action 열거형에 액션 케이스를 정의하듯이, 마찬가지로 BBAlertActionType 프로토콜의 준수하는 열거형의 케이스가 버튼 액션의 결과가 됩니다. +/// +/// - Authors: 김소월 +public protocol BBAlertActionType { + + /// title 프로퍼티는 필수 구현이며, 버튼의 타이틀을 의미합니다. + var title: String? { get } + + /// style 프로퍼티는 버튼의 스타일을 의미합니다. 선택 구현이며, 기본값은 default입니다. + var style: BBAlertActionStyle { get } +} + +public extension BBAlertActionType { + var style: BBAlertActionStyle { + return .default + } +} + + + +// MARK: - BBAlertServiceType + +public protocol BBAlertServiceType { + @discardableResult + func show( + title: String, + titleFontStyle: BBFontStyle?, + subtitle: String?, + subtitleFontStyle: BBFontStyle?, + actions: [Action], + viewConfig: BBAlertViewConfiguration, + config: BBAlertConfiguration + ) -> Observable where Action: BBAlertActionType + + @discardableResult + func show( + image: UIImage?, + imageTint: UIColor?, + title: String, + titleFontStyle: BBFontStyle?, + subtitle: String?, + subtitleFontStyle: BBFontStyle? , + actions: [Action], + viewConfig: BBAlertViewConfiguration, + config: BBAlertConfiguration + ) -> Observable where Action: BBAlertActionType + + @discardableResult + func show( + child: any BBAlertStackView, + actions: [Action], + viewConfig: BBAlertViewConfiguration, + config: BBAlertConfiguration + ) -> Observable where Action: BBAlertActionType + + @discardableResult + func show( + _ style: BBAlertStyle, + primaryAction: Action, + config: BBAlertConfiguration + ) -> Observable where Action: BBAlertActionType +} + +public extension BBAlertServiceType { + + /// 텍스트와 서브 텍스트가 포함된 Alert를 생성합니다. + /// - Parameters: + /// - title: 타이틀 텍스트 + /// - titleFontStyle: 타이틀의 폰트 스타일 + /// - subtitle: 서브 타이틀 텍스트 + /// - subtitleFontStyle: 서브 타이틀의 폰트 스타일 + /// - actions: BBAlertActionType을 준수하는 열거형 케이스 배열 + /// - viewConfig: AlertView 설정값 + /// - config: Alert 설정값 + /// - Returns: Observable + @discardableResult + func show( + title: String, + titleFontStyle: BBFontStyle? = nil, + subtitle: String? = nil, + subtitleFontStyle: BBFontStyle? = nil, + actions: [Action], + viewConfig: BBAlertViewConfiguration = BBAlertViewConfiguration(), + config: BBAlertConfiguration = BBAlertConfiguration() + ) -> Observable where Action: BBAlertActionType { + + return Observable.create { observer in + let alert = BBAlert.text( + title: title, + titleFontStyle: titleFontStyle, + subtitle: subtitle, + subtitleFontStyle: subtitleFontStyle, + viewConfig: viewConfig, + config: config + ) + + for action in actions { + let action = BBAlertAction(title: action.title, style: action.style) { alert in + observer.onNext(action) + alert?.close() + } + alert.addAction(action) + } + + alert.show() + + return Disposables.create { + alert.close() + } + } + + } + + /// 텍스트, 서브 텍스트와 이미지가 포함된 Alert를 생성합니다. + /// - Parameters: + /// - image: 이미지 + /// - imageTint: 이미지 강조 색상 + /// - title: 타이틀 텍스트 + /// - titleFontStyle: 타이틀의 폰트 스타일 + /// - subtitle: 서브 타이틀 텍스트 + /// - subtitleFontStyle: 서브 타이틀의 폰트 스타일 + /// - actions: BBAlertActionType을 준수하는 열거형 케이스 배열 + /// - viewConfig: AlertView 설정값 + /// - config: Alert 설정값 + /// - Returns: Observable + @discardableResult + func show( + image: UIImage? = nil, + imageTint: UIColor? = nil, + title: String, + titleFontStyle: BBFontStyle? = nil, + subtitle: String? = nil, + subtitleFontStyle: BBFontStyle? = nil, + actions: [Action], + viewConfig: BBAlertViewConfiguration = BBAlertViewConfiguration(), + config: BBAlertConfiguration = BBAlertConfiguration() + ) -> Observable where Action: BBAlertActionType { + + return Observable.create { observer in + let alert = BBAlert.image( + image: image, + imageTint: imageTint, + title: title, + titleFontStyle: titleFontStyle, + subtitle: subtitle, + subtitleFontStyle: subtitleFontStyle, + viewConfig: viewConfig, + config: config + ) + + for action in actions { + let action = BBAlertAction(title: action.title, style: action.style) { alert in + observer.onNext(action) + alert?.close() + } + alert.addAction(action) + } + + alert.show() + + return Disposables.create { + alert.close() + } + } + + } + + /// 직접 커스텀한 뷰로 BBAlert을 생성합니다. + /// - Parameters: + /// - child: BBAlertStackView 프로토콜을 준수하는 UIView + /// - actions: BBAlertActionType을 준수하는 열거형 케이스 배열 + /// - viewConfig: AlertView 설정값 + /// - config: Alert 설정값 + /// - Returns: Observable + @discardableResult + func show( + child: any BBAlertStackView, + actions: [Action], + viewConfig: BBAlertViewConfiguration = BBAlertViewConfiguration(), + config: BBAlertConfiguration = BBAlertConfiguration() + ) -> Observable where Action: BBAlertActionType { + + return Observable.create { observer in + let alert = BBAlert.custom( + child, + viewConfig: viewConfig, + config: config + ) + + for action in actions { + let action = BBAlertAction(title: action.title, style: action.style) { alert in + observer.onNext(action) + alert?.close() + } + alert.addAction(action) + } + + alert.show() + + return Disposables.create { + alert.close() + } + } + + } + + /// 정해진 Style의 Alert를 생성합니다. + /// - Parameters: + /// - style: 스타일 + /// - primaryAction: BBAlertActionType을 준수하는 열거형 케이스 + /// - config: Alert 설정값 + /// - Returns: Observable + @discardableResult + func show( + _ style: BBAlertStyle, + primaryAction action: Action, + config: BBAlertConfiguration = BBAlertConfiguration() + ) -> Observable { + + return Observable.create { observer in + let action: BBAlertActionHandler = { alert in + observer.onNext(action) + alert?.close() + } + + let alert = BBAlert.style( + style, + primaryAction: action, + config: config + ) + + alert.show() + + return Disposables.create { + alert.close() + } + } + + } + + +} + + + +// MARK: - BBAlertService + +/// BBAlert를 조금 더 Rx스럽게 사용하도록 도와주는 서비스입니다. +/// +/// 서비스의 **show(_:)** 메서드를 호출하기 전 버튼의 스타일과 액션이 정의된 **BBAlertActionType** 프로토콜을 준수하는 열거형을 선언해야 합니다. +/// +/// ```swift +/// +/// enum PickAlertAction: BBAlertActionType { +/// case pick +/// case cancel +/// +/// var title: String? { +/// switch self { +/// case .pick: return "지금 하기" +/// case .cancel: return "다음에 하기" +/// } +/// } +/// +/// var style: BBAlertActionStyle { +/// switch self { +/// case .pick: return .default +/// case .cancel: return .cancel +/// } +/// } +///} +/// ``` +/// +/// 이렇게 정의한 PickAlertAction 열거형의 케이스는 버튼 액션의 결과가 됩니다. 예를 들어, 지금 하기 버튼을 클릭하면 pick 케이스 항목이 방출되며, 방출된 아이템에 따라 flatMap 연산자에서 액션 분기 처리를 해줄 수 있습니다. 아래 코드는 버튼 액션을 받아 처리하는 방법을 보여줍니다. +/// +/// ```swift +/// let actions: [PickAlertAction] = [.pick, .cancel] +/// +///return provider.bbAlertService.show( +/// image: DesignSystemAsset.missionKeyGraphic.image, +/// title: "미션 열쇠 획득!", +/// subtitle: "열쇠를 획득해 잠금이 해제되었어요.", +/// actions: actions +/// ) +/// .withUnretained(self) +/// .flatMap { // 버튼을 클릭하면 곧바로 해당하는 액션 항목이 방출됨 +/// switch $0.1 { +/// case .pick: +/// return Observable.just(.setHiddenPickButton(true)) +/// +/// default: +/// return Observable.empty() +/// } +///} +/// ``` +/// +/// - Authors: 김소월 +/// +public final class BBAlertService: BaseService, BBAlertServiceType { } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ServiceProvider.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ServiceProvider.swift index a578cf896..2bc8695f1 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ServiceProvider.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ServiceProvider.swift @@ -8,6 +8,9 @@ import Foundation public protocol ServiceProviderProtocol: AnyObject { + + var bbAlertService: BBAlertServiceType { get } + var mainService: MainServiceType { get } var managementService: ManagementServiceType { get } @@ -22,6 +25,8 @@ public protocol ServiceProviderProtocol: AnyObject { final public class ServiceProvider: ServiceProviderProtocol { + public lazy var bbAlertService: any BBAlertServiceType = BBAlertService(provider: self) + public lazy var mainService: MainServiceType = MainService(provider: self) public lazy var managementService: any ManagementServiceType = ManagementService(provider: self) From 2b37e8d6a60d0b82a3b7519146918de481e71c10 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Mon, 23 Sep 2024 23:13:20 +0900 Subject: [PATCH 215/263] =?UTF-8?q?refactor:=20Wrapper=EC=97=90=20@Wrapper?= =?UTF-8?q?=20=EB=A7=A4=ED=81=AC=EB=A1=9C=20=EC=A0=81=EC=9A=A9=20(#643)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DailyCalendarViewControllerWrapper.swift | 22 +++---------- ...MonthlyCalendarViewControllerWrapper.swift | 25 +++----------- .../CameraDisplayViewControllerWrapper.swift | 22 ++----------- .../Camera/CameraViewControllerWrapper.swift | 19 ++--------- .../CommentViewControllerWrapper.swift | 23 +++---------- .../FamilyEntranceControllerWrapper.swift | 23 ++----------- ...milyNameSettingViewControllerWrapper.swift | 33 ------------------- ...InputFamilyLinkViewControllerWrapper.swift | 23 ++----------- .../JoinFamilyViewControllerWrapper.swift | 23 ++----------- .../MainFamilyViewControllerWrapper.swift | 24 ++++---------- .../Main/MainPostViewControllerWrapper.swift | 24 ++++---------- .../Main/MainViewControllerWrapper.swift | 22 +++---------- ...milyNameSettingViewControllerWrapper.swift | 19 +++++++++++ .../ManagementViewControllerWrapper.swift | 23 ++----------- .../OnboardingViewControllerWrapper.swift | 23 ++----------- .../PostDetailViewControllerWrapper.swift | 24 ++++---------- ...ReactionMembersViewControllerWrapper.swift | 20 +++-------- .../ReactionViewControllerWrapper.swift | 20 +++-------- ...SelectableEmojiViewControllerWrapper.swift | 2 +- .../PrivacyViewControllerWrapper.swift | 19 ++--------- .../ProfileDetailViewControllerWrapper.swift | 20 ++--------- ...ProfileFeedPageViewControllerWrapper.swift | 2 +- .../ProfileFeedViewControllerWrapper.swift | 22 +++---------- .../ProfileViewControllerWrapper.swift | 22 +++---------- .../AccountResignViewControllerWrapper.swift | 20 ++--------- .../SignIn/SignInViewControllerWrapper.swift | 24 +++----------- .../Splash/SplashViewControllerWrapper.swift | 24 +++----------- .../WebContentViewControllerWrapper.swift | 25 +++----------- .../Core/Sources/Base/BaseWrapper.swift | 23 ------------- 29 files changed, 115 insertions(+), 500 deletions(-) delete mode 100644 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/JoinFamily/FamilyNameSettingViewControllerWrapper.swift create mode 100644 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Management/FamilyNameSettingViewControllerWrapper.swift delete mode 100644 14th-team5-iOS/Core/Sources/Base/BaseWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Calendar/DailyCalendarViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Calendar/DailyCalendarViewControllerWrapper.swift index 22a7f9b45..ee634316e 100644 --- a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Calendar/DailyCalendarViewControllerWrapper.swift +++ b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Calendar/DailyCalendarViewControllerWrapper.swift @@ -7,26 +7,16 @@ import Core import Foundation +import MacrosInterface -final class DailyCalendarViewControllerWrapper: BaseWrapper { - - // MARK: - Typealias - - typealias R = DailyCalendarViewReactor - typealias V = DailyCalendarViewController +@Wrapper +final class DailyCalendarViewControllerWrapper { // MARK: - Properties let date: Date let link: NotificationDeepLink? // TODO: - link 지우기 - - var reactor: DailyCalendarViewReactor { - makeReactor() - } - - var viewController: DailyCalendarViewController { - makeViewController() - } + // MARK: - Intializer @@ -47,8 +37,4 @@ final class DailyCalendarViewControllerWrapper: BaseWrapper { ) } - func makeViewController() -> DailyCalendarViewController { - DailyCalendarViewController(reactor: makeReactor()) - } - } diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Calendar/MonthlyCalendarViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Calendar/MonthlyCalendarViewControllerWrapper.swift index ece594d4a..e9f618dda 100644 --- a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Calendar/MonthlyCalendarViewControllerWrapper.swift +++ b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Calendar/MonthlyCalendarViewControllerWrapper.swift @@ -7,30 +7,13 @@ import Core import Foundation +import MacrosInterface -final class MonthlyCalendarViewControllerWrapper: BaseWrapper { - - // MARK: - Typealias - - typealias R = MonthlyCalendarViewReactor - typealias V = MonthlyCalendarViewController - - // MARK: - Properties - - var viewController: V { - makeViewController() - } - - var reactor: R { - makeReactor() - } +@Wrapper +final class MonthlyCalendarViewControllerWrapper { // MARK: - Make - - func makeViewController() -> V { - return MonthlyCalendarViewController(reactor: makeReactor()) - } - + func makeReactor() -> R { return MonthlyCalendarViewReactor() } diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Camera/CameraDisplayViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Camera/CameraDisplayViewControllerWrapper.swift index cd66d23c6..5ccc11709 100644 --- a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Camera/CameraDisplayViewControllerWrapper.swift +++ b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Camera/CameraDisplayViewControllerWrapper.swift @@ -7,14 +7,11 @@ import Core import Domain - import Foundation +import MacrosInterface - -final class CameraDisplayViewControllerWrapper: BaseWrapper { - - typealias R = CameraDisplayViewReactor - typealias V = CameraDisplayViewController +@Wrapper +final class CameraDisplayViewControllerWrapper { private let displayData: Data private let missionTitle: String @@ -30,19 +27,6 @@ final class CameraDisplayViewControllerWrapper: BaseWrapper { self.cameraDisplayType = cameraDisplayType } - var reactor: CameraDisplayViewReactor { - return makeReactor() - } - - var viewController: CameraDisplayViewController { - return makeViewController() - } - - - func makeViewController() -> CameraDisplayViewController { - return CameraDisplayViewController(reactor: reactor) - } - func makeReactor() -> CameraDisplayViewReactor { return CameraDisplayViewReactor( displayData: displayData, diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Camera/CameraViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Camera/CameraViewControllerWrapper.swift index 0ac3435fc..54ccbe701 100644 --- a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Camera/CameraViewControllerWrapper.swift +++ b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Camera/CameraViewControllerWrapper.swift @@ -7,20 +7,11 @@ import Core import Domain - import Foundation +import MacrosInterface -final class CameraViewControllerWrapper: BaseWrapper { - - typealias R = CameraViewReactor - typealias V = CameraViewController - - var reactor: CameraViewReactor { - return makeReactor() - } - var viewController: CameraViewController { - return makeViewController() - } +@Wrapper +final class CameraViewControllerWrapper { private let cameraType: UploadLocation private let memberId: String @@ -45,8 +36,4 @@ final class CameraViewControllerWrapper: BaseWrapper { ) } - func makeViewController() -> CameraViewController { - return CameraViewController(reactor: reactor) - } - } diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Comment/CommentViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Comment/CommentViewControllerWrapper.swift index 8cea0349b..1922ec98d 100644 --- a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Comment/CommentViewControllerWrapper.swift +++ b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Comment/CommentViewControllerWrapper.swift @@ -7,26 +7,15 @@ import Core import Foundation +import MacrosInterface + +@Wrapper +final class CommentViewControllerWrapper { -final class CommentViewControllerWrapper: BaseWrapper { - - // MARK: - Typealias - - typealias R = CommentViewReactor - typealias V = CommentViewController - // MARK: - Properties let postId: String - var reactor: CommentViewReactor { - makeReactor() - } - - var viewController: CommentViewController { - makeViewController() - } - // MARK: - Intializer init(postId: String) { @@ -39,8 +28,4 @@ final class CommentViewControllerWrapper: BaseWrapper { CommentViewReactor(postId: postId) } - func makeViewController() -> CommentViewController { - CommentViewController(reactor: makeReactor()) - } - } diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/JoinFamily/FamilyEntranceControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/JoinFamily/FamilyEntranceControllerWrapper.swift index 4c91e560a..52d198c98 100644 --- a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/JoinFamily/FamilyEntranceControllerWrapper.swift +++ b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/JoinFamily/FamilyEntranceControllerWrapper.swift @@ -7,23 +7,10 @@ import Core import Foundation +import MacrosInterface -final class FamilyEntranceControllerWrapper: BaseWrapper { - - // MARK: - Typealias - - typealias R = FamilyEntranceReactor - typealias V = FamilyEntranceViewController - - // MARK: - Properties - - var reactor: FamilyEntranceReactor { - makeReactor() - } - - var viewController: FamilyEntranceViewController { - makeViewController() - } +@Wrapper +final class FamilyEntranceControllerWrapper { // MARK: - Make @@ -31,8 +18,4 @@ final class FamilyEntranceControllerWrapper: BaseWrapper { FamilyEntranceReactor() } - func makeViewController() -> FamilyEntranceViewController { - FamilyEntranceViewController(reactor: makeReactor()) - } - } diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/JoinFamily/FamilyNameSettingViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/JoinFamily/FamilyNameSettingViewControllerWrapper.swift deleted file mode 100644 index 186900cf2..000000000 --- a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/JoinFamily/FamilyNameSettingViewControllerWrapper.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// FamilyNameSettingViewControllerWrapper.swift -// App -// -// Created by Kim dohyun on 7/23/24. -// - -import Core -import Foundation - - -final class FamilyNameSettingViewControllerWrapper: BaseWrapper { - - typealias R = FamilyNameSettingViewReactor - typealias V = FamilyNameSettingViewController - - - func makeReactor() -> FamilyNameSettingViewReactor { - return FamilyNameSettingViewReactor() - } - - func makeViewController() -> FamilyNameSettingViewController { - return FamilyNameSettingViewController(reactor: reactor) - } - - var reactor: FamilyNameSettingViewReactor { - return makeReactor() - } - - var viewController: FamilyNameSettingViewController { - return makeViewController() - } -} diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/JoinFamily/InputFamilyLinkViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/JoinFamily/InputFamilyLinkViewControllerWrapper.swift index 467e45f14..80b4369bc 100644 --- a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/JoinFamily/InputFamilyLinkViewControllerWrapper.swift +++ b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/JoinFamily/InputFamilyLinkViewControllerWrapper.swift @@ -7,23 +7,10 @@ import Core import Foundation +import MacrosInterface -final class InputFamilyLinkViewControllerWrapper: BaseWrapper { - - // MARK: - Typealias - - typealias R = InputFamilyLinkReactor - typealias V = InputFamilyLinkViewController - - // MARK: - Properties - - var reactor: InputFamilyLinkReactor { - InputFamilyLinkReactor() - } - - var viewController: InputFamilyLinkViewController { - InputFamilyLinkViewController(reactor: makeReactor()) - } +@Wrapper +final class InputFamilyLinkViewControllerWrapper { // MARK: - Make @@ -31,8 +18,4 @@ final class InputFamilyLinkViewControllerWrapper: BaseWrapper { InputFamilyLinkReactor() } - func makeViewController() -> InputFamilyLinkViewController { - InputFamilyLinkViewController(reactor: makeReactor()) - } - } diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/JoinFamily/JoinFamilyViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/JoinFamily/JoinFamilyViewControllerWrapper.swift index 4cd4137bc..23ff69b43 100644 --- a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/JoinFamily/JoinFamilyViewControllerWrapper.swift +++ b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/JoinFamily/JoinFamilyViewControllerWrapper.swift @@ -7,23 +7,10 @@ import Core import Foundation +import MacrosInterface -final class JoinFamilyViewControllerWrapper: BaseWrapper { - - // MARK: - Typealias - - typealias R = JoinFamilyReactor - typealias V = JoinFamilyViewController - - // MARK: - Properties - - var reactor: JoinFamilyReactor { - makeReactor() - } - - var viewController: JoinFamilyViewController { - makeViewController() - } +@Wrapper +final class JoinFamilyViewControllerWrapper { // MARK: - Make @@ -31,8 +18,4 @@ final class JoinFamilyViewControllerWrapper: BaseWrapper { JoinFamilyReactor() } - func makeViewController() -> JoinFamilyViewController { - JoinFamilyViewController(reactor: makeReactor()) - } - } diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Main/MainFamilyViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Main/MainFamilyViewControllerWrapper.swift index 80a4f2398..492e8d096 100644 --- a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Main/MainFamilyViewControllerWrapper.swift +++ b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Main/MainFamilyViewControllerWrapper.swift @@ -5,27 +5,15 @@ // Created by 마경미 on 17.06.24. // -import Foundation - import Core +import Foundation +import MacrosInterface -final class MainFamilyViewControllerWrapper: BaseWrapper { - typealias R = MainFamilyViewReactor - typealias V = MainFamilyViewController - - func makeViewController() -> V { - return MainFamilyViewController(reactor: makeReactor()) - } - +@Wrapper +final class MainFamilyViewControllerWrapper { + func makeReactor() -> R { return MainFamilyViewReactor() } - - var viewController: V { - makeViewController() - } - - var reactor: R { - makeReactor() - } + } diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Main/MainPostViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Main/MainPostViewControllerWrapper.swift index 92dee3353..5c3dd6de1 100644 --- a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Main/MainPostViewControllerWrapper.swift +++ b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Main/MainPostViewControllerWrapper.swift @@ -5,34 +5,22 @@ // Created by 마경미 on 17.06.24. // -import Foundation - import Core import Domain +import Foundation +import MacrosInterface -final class MainPostViewControllerWrapper: BaseWrapper { - typealias R = MainPostViewReactor - typealias V = MainPostViewController - +@Wrapper +final class MainPostViewControllerWrapper { + private let type: PostType init(type: PostType) { self.type = type } - func makeViewController() -> V { - return MainPostViewController(reactor: makeReactor()) - } - func makeReactor() -> R { return MainPostViewReactor(initialState: .init(type: type)) } - - var viewController: V { - makeViewController() - } - - var reactor: R { - makeReactor() - } + } diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Main/MainViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Main/MainViewControllerWrapper.swift index e57770aca..03f366d23 100644 --- a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Main/MainViewControllerWrapper.swift +++ b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Main/MainViewControllerWrapper.swift @@ -5,27 +5,15 @@ // Created by 마경미 on 17.06.24. // -import Foundation - import Core +import Foundation +import MacrosInterface -final class MainViewControllerWrapper: BaseWrapper { - typealias R = MainViewReactor - typealias V = MainViewController - - func makeViewController() -> V { - return MainViewController(reactor: makeReactor()) - } - +@Wrapper +final class MainViewControllerWrapper { + func makeReactor() -> R { return MainViewReactor() } - var viewController: V { - makeViewController() - } - - var reactor: R { - makeReactor() - } } diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Management/FamilyNameSettingViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Management/FamilyNameSettingViewControllerWrapper.swift new file mode 100644 index 000000000..ae507d26c --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Management/FamilyNameSettingViewControllerWrapper.swift @@ -0,0 +1,19 @@ +// +// FamilyNameSettingViewControllerWrapper.swift +// App +// +// Created by Kim dohyun on 7/23/24. +// + +import Core +import Foundation +import MacrosInterface + +@Wrapper +final class FamilyNameSettingViewControllerWrapper { + + func makeReactor() -> FamilyNameSettingViewReactor { + return FamilyNameSettingViewReactor() + } + +} diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Management/ManagementViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Management/ManagementViewControllerWrapper.swift index 6187fedd4..f700b6df5 100644 --- a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Management/ManagementViewControllerWrapper.swift +++ b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Management/ManagementViewControllerWrapper.swift @@ -7,23 +7,10 @@ import Core import Foundation +import MacrosInterface -final class ManagementViewControllerWrapper: BaseWrapper { - - // MARK: - Typealias - - typealias R = ManagementReactor - typealias V = ManagementViewController - - // MARK: - Properties - - var reactor: ManagementReactor { - makeReactor() - } - - var viewController: ManagementViewController { - makeViewController() - } +@Wrapper +final class ManagementViewControllerWrapper { // MARK: - Make @@ -31,8 +18,4 @@ final class ManagementViewControllerWrapper: BaseWrapper { ManagementReactor() } - func makeViewController() -> ManagementViewController { - ManagementViewController(reactor: makeReactor()) - } - } diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Onboarding/OnboardingViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Onboarding/OnboardingViewControllerWrapper.swift index 03ce27274..fe9791497 100644 --- a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Onboarding/OnboardingViewControllerWrapper.swift +++ b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Onboarding/OnboardingViewControllerWrapper.swift @@ -7,23 +7,10 @@ import Core import Foundation +import MacrosInterface -final class OnboardingViewControllerWrapper: BaseWrapper { - - // MARK: - Typealias - - typealias R = OnBoardingReactor - typealias V = OnBoardingViewController - - // MARK: - Properties - - var reactor: OnBoardingReactor { - makeReactor() - } - - var viewController: OnBoardingViewController { - makeViewController() - } +@Wrapper +final class OnboardingViewControllerWrapper { // MARK: - Make @@ -31,8 +18,4 @@ final class OnboardingViewControllerWrapper: BaseWrapper { OnBoardingReactor() } - func makeViewController() -> OnBoardingViewController { - OnBoardingViewController(reactor: makeReactor()) - } - } diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/PostDetail/PostDetailViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/PostDetail/PostDetailViewControllerWrapper.swift index fdb39d305..fe350cea5 100644 --- a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/PostDetail/PostDetailViewControllerWrapper.swift +++ b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/PostDetail/PostDetailViewControllerWrapper.swift @@ -5,14 +5,13 @@ // Created by 마경미 on 17.06.24. // -import Foundation - import Core +import Foundation +import MacrosInterface -final class PostDetailViewControllerWrapper: BaseWrapper { - typealias R = PostReactor - typealias V = PostViewController - +@Wrapper +final class PostDetailViewControllerWrapper { + private let selectedIndex: Int private let originPostLists: PostSection.Model @@ -21,19 +20,8 @@ final class PostDetailViewControllerWrapper: BaseWrapper { self.originPostLists = originPostLists } - func makeViewController() -> V { - return PostViewController(reactor: makeReactor()) - } - func makeReactor() -> R { return PostReactor(initialState: .init(selectedIndex: selectedIndex, originPostLists: originPostLists)) } - - var viewController: V { - makeViewController() - } - - var reactor: R { - makeReactor() - } + } diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/PostDetail/ReactionMembersViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/PostDetail/ReactionMembersViewControllerWrapper.swift index e77fbbeac..97b7ef88d 100644 --- a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/PostDetail/ReactionMembersViewControllerWrapper.swift +++ b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/PostDetail/ReactionMembersViewControllerWrapper.swift @@ -5,14 +5,13 @@ // Created by 마경미 on 17.06.24. // -import Foundation - import Core import Domain +import Foundation +import MacrosInterface -final class ReactionMembersViewControllerWrapper: BaseWrapper { - typealias R = ReactionMemberViewReactor - typealias V = ReactionMembersViewController +@Wrapper +final class ReactionMembersViewControllerWrapper { private let emojiData: EmojiEntity @@ -20,19 +19,8 @@ final class ReactionMembersViewControllerWrapper: BaseWrapper { self.emojiData = emojiData } - func makeViewController() -> V { - return ReactionMembersViewController(reactor: makeReactor()) - } - func makeReactor() -> R { return ReactionMemberViewReactor(initialState: .init(emojiData: emojiData)) } - var viewController: V { - makeViewController() - } - - var reactor: R { - makeReactor() - } } diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/PostDetail/ReactionViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/PostDetail/ReactionViewControllerWrapper.swift index df0436c58..a99dde48b 100644 --- a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/PostDetail/ReactionViewControllerWrapper.swift +++ b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/PostDetail/ReactionViewControllerWrapper.swift @@ -5,14 +5,13 @@ // Created by 마경미 on 17.06.24. // -import Foundation - import Core import Domain +import Foundation +import MacrosInterface -final class ReactionViewControllerWrapper: BaseWrapper { - typealias R = ReactionViewReactor - typealias V = ReactionViewController +@Wrapper +final class ReactionViewControllerWrapper { private let type: ReactionType private let postListData: PostEntity @@ -22,19 +21,8 @@ final class ReactionViewControllerWrapper: BaseWrapper { self.postListData = postListData } - func makeViewController() -> V { - return ReactionViewController(reactor: makeReactor()) - } - func makeReactor() -> R { return ReactionViewReactor(initialState: .init(type: type, postListData: postListData)) } - var viewController: V { - makeViewController() - } - - var reactor: R { - makeReactor() - } } diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/PostDetail/SelectableEmojiViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/PostDetail/SelectableEmojiViewControllerWrapper.swift index 34345453d..cb12f4c7c 100644 --- a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/PostDetail/SelectableEmojiViewControllerWrapper.swift +++ b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/PostDetail/SelectableEmojiViewControllerWrapper.swift @@ -11,7 +11,7 @@ import Core import RxSwift -final class SelectableEmojiViewControllerWrapper: BaseWrapper { +final class SelectableEmojiViewControllerWrapper { typealias R = SelectableEmojiReactor typealias V = SelectableEmojiViewController diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Privacy/PrivacyViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Privacy/PrivacyViewControllerWrapper.swift index 524447823..779b7322e 100644 --- a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Privacy/PrivacyViewControllerWrapper.swift +++ b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Privacy/PrivacyViewControllerWrapper.swift @@ -7,11 +7,10 @@ import Core import Foundation +import MacrosInterface - -final class PrivacyViewControllerWrapper: BaseWrapper { - typealias R = PrivacyViewReactor - typealias V = PrivacyViewController +@Wrapper +final class PrivacyViewControllerWrapper { private let memberId: String @@ -19,18 +18,6 @@ final class PrivacyViewControllerWrapper: BaseWrapper { self.memberId = memberId } - var reactor: PrivacyViewReactor { - return makeReactor() - } - - var viewController: PrivacyViewController { - return makeViewController() - } - - func makeViewController() -> PrivacyViewController { - PrivacyViewController(reactor: makeReactor()) - } - func makeReactor() -> PrivacyViewReactor { return PrivacyViewReactor(memberId: memberId) } diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Profile/ProfileDetailViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Profile/ProfileDetailViewControllerWrapper.swift index b548641d2..b7a8d78cb 100644 --- a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Profile/ProfileDetailViewControllerWrapper.swift +++ b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Profile/ProfileDetailViewControllerWrapper.swift @@ -6,20 +6,11 @@ // import Core - import Foundation +import MacrosInterface -final class ProfileDetailViewControllerWrapper: BaseWrapper { - typealias R = ProfileDetailViewReactor - typealias V = ProfileDetailViewController - - var reactor: ProfileDetailViewReactor { - return makeReactor() - } - - var viewController: ProfileDetailViewController { - return makeViewController() - } +@Wrapper +final class ProfileDetailViewControllerWrapper { private let profileURL: URL private let userNickname: String @@ -33,9 +24,4 @@ final class ProfileDetailViewControllerWrapper: BaseWrapper { return ProfileDetailViewReactor(profileURL: profileURL, userNickname: userNickname) } - func makeViewController() -> ProfileDetailViewController { - return ProfileDetailViewController(reactor: reactor) - } - - } diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Profile/ProfileFeedPageViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Profile/ProfileFeedPageViewControllerWrapper.swift index 089a6ad34..ea60b2e2c 100644 --- a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Profile/ProfileFeedPageViewControllerWrapper.swift +++ b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Profile/ProfileFeedPageViewControllerWrapper.swift @@ -9,7 +9,7 @@ import Core import Foundation -final class ProfileFeedPageViewControllerWrapper: BaseWrapper { +final class ProfileFeedPageViewControllerWrapper { typealias R = ProfileFeedPageViewReactor typealias V = ProfileFeedPageViewController diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Profile/ProfileFeedViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Profile/ProfileFeedViewControllerWrapper.swift index f2500d827..9d690dd1b 100644 --- a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Profile/ProfileFeedViewControllerWrapper.swift +++ b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Profile/ProfileFeedViewControllerWrapper.swift @@ -7,22 +7,11 @@ import Core import Domain - import Foundation +import MacrosInterface - -final class ProfileFeedViewControllerWrapper: BaseWrapper { - - typealias R = ProfileFeedViewReactor - typealias V = ProfileFeedViewController - - var reactor: ProfileFeedViewReactor { - return makeReactor() - } - - var viewController: ProfileFeedViewController { - return makeViewController() - } +@Wrapper +final class ProfileFeedViewControllerWrapper { private let postType: PostType private let memberId: String @@ -36,8 +25,5 @@ final class ProfileFeedViewControllerWrapper: BaseWrapper { func makeReactor() -> ProfileFeedViewReactor { return ProfileFeedViewReactor(type: postType, memberId: memberId) } - - func makeViewController() -> ProfileFeedViewController { - return ProfileFeedViewController(reactor: reactor) - } + } diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Profile/ProfileViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Profile/ProfileViewControllerWrapper.swift index 614fdfa86..a2ef72766 100644 --- a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Profile/ProfileViewControllerWrapper.swift +++ b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Profile/ProfileViewControllerWrapper.swift @@ -6,12 +6,11 @@ // import Core - import Foundation +import MacrosInterface -final class ProfileViewControllerWrapper: BaseWrapper { - typealias R = ProfileViewReactor - typealias V = ProfileViewController +@Wrapper +final class ProfileViewControllerWrapper { private let memberId: String private let isUser: Bool @@ -22,20 +21,7 @@ final class ProfileViewControllerWrapper: BaseWrapper { self.memberId = memberId self.isUser = memberId == App.Repository.member.memberID.value ? true : false } - - var reactor: ProfileViewReactor { - return makeReactor() - } - - var viewController: ProfileViewController { - return makeViewController() - } - - - func makeViewController() -> ProfileViewController { - return ProfileViewController(reactor: reactor) - } - + func makeReactor() -> ProfileViewReactor { ProfileViewReactor(memberId: memberId, isUser: isUser) } diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Resign/AccountResignViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Resign/AccountResignViewControllerWrapper.swift index b88ce9030..0985e7537 100644 --- a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Resign/AccountResignViewControllerWrapper.swift +++ b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Resign/AccountResignViewControllerWrapper.swift @@ -7,24 +7,10 @@ import Core import Foundation +import MacrosInterface -final class AccountResignViewControllerWrapper: BaseWrapper { - - typealias R = AccountResignViewReactor - typealias V = AccountResignViewCotroller - - - var reactor: AccountResignViewReactor { - return makeReactor() - } - - var viewController: AccountResignViewCotroller { - return makeViewController() - } - - func makeViewController() -> AccountResignViewCotroller { - return AccountResignViewCotroller(reactor: makeReactor()) - } +@Wrapper +final class AccountResignViewControllerWrapper { func makeReactor() -> AccountResignViewReactor { return AccountResignViewReactor() diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/SignIn/SignInViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/SignIn/SignInViewControllerWrapper.swift index d9f16ae71..d2c3d89aa 100644 --- a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/SignIn/SignInViewControllerWrapper.swift +++ b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/SignIn/SignInViewControllerWrapper.swift @@ -7,25 +7,13 @@ import Core import Foundation +import MacrosInterface // TODO: - SignInViewReactor로 이름 수정하기 // TODO: - SignInViewController로 이름 수정하기 -final class SignInViewControllerWrapper: BaseWrapper { - - // MARK: - Typealias - - typealias R = AccountSignInReactor - typealias V = AccountSignInViewController - - // MARK: - Properties - - var reactor: AccountSignInReactor { - makeReactor() - } - - var viewController: AccountSignInViewController { - makeViewController() - } + +@Wrapper +final class SignInViewControllerWrapper { // MARK: - Make @@ -33,8 +21,4 @@ final class SignInViewControllerWrapper: BaseWrapper { AccountSignInReactor() } - func makeViewController() -> AccountSignInViewController { - AccountSignInViewController(reactor: makeReactor()) - } - } diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Splash/SplashViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Splash/SplashViewControllerWrapper.swift index 336ec8ebe..28808cb0f 100644 --- a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Splash/SplashViewControllerWrapper.swift +++ b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Splash/SplashViewControllerWrapper.swift @@ -7,31 +7,15 @@ import Core import Foundation +import MacrosInterface -final class SplashViewControllerWrapper: BaseWrapper { - - // MARK: - Typealias - - typealias R = SplashReactor - typealias V = SplashViewController - - // MARK: - Properties - - var reactor: SplashReactor { - makeReactor() - } - - var viewController: SplashViewController { - makeViewController() - } +@Wrapper +final class SplashViewControllerWrapper { // MARK: - Make func makeReactor() -> SplashReactor { SplashReactor() } - - func makeViewController() -> SplashViewController { - SplashViewController(reactor: makeReactor()) - } + } diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/WebContent/WebContentViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/WebContent/WebContentViewControllerWrapper.swift index 74fc5e9c5..672dd0f23 100644 --- a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/WebContent/WebContentViewControllerWrapper.swift +++ b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/WebContent/WebContentViewControllerWrapper.swift @@ -7,26 +7,15 @@ import Core import Foundation +import MacrosInterface -final class WebContentViewControllerWrapper: BaseWrapper { - - // MARK: - Typealias - - typealias R = WebContentReactor - typealias V = WebContentViewController - +@Wrapper +final class WebContentViewControllerWrapper { + // MARK: - Properties let url: URL? - var reactor: WebContentReactor { - makeReactor() - } - - var viewController: WebContentViewController { - makeViewController() - } - // MARK: - Intializer init(url: URL?) { @@ -38,9 +27,5 @@ final class WebContentViewControllerWrapper: BaseWrapper { func makeReactor() -> WebContentReactor { WebContentReactor(contentURL: url) } - - func makeViewController() -> WebContentViewController { - WebContentViewController(reactor: makeReactor()) - } - + } diff --git a/14th-team5-iOS/Core/Sources/Base/BaseWrapper.swift b/14th-team5-iOS/Core/Sources/Base/BaseWrapper.swift deleted file mode 100644 index 4fefbd592..000000000 --- a/14th-team5-iOS/Core/Sources/Base/BaseWrapper.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// BaseWrapper.swift -// Core -// -// Created by 김건우 on 6/14/24. -// - -import UIKit - -import ReactorKit - -public protocol BaseWrapper { - - associatedtype R: Reactor - associatedtype V: ReactorKit.View - - func makeReactor() -> R - func makeViewController() -> V - - var reactor: R { get } - var viewController: V { get } - -} From 440f2c9163dc69010651a44f05dc4bbd77370b76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=84?= Date: Wed, 25 Sep 2024 02:19:52 +0900 Subject: [PATCH 216/263] =?UTF-8?q?fix:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EC=8B=9C=20FamilyId=20=EC=A1=B0=ED=9A=8C=20=EB=AA=BB=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EC=9D=B4=EC=8A=88=20=EC=88=98=EC=A0=95=ED=95=B4?= =?UTF-8?q?=EC=9A=94=20(#648)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: Notification+Ext 회원 가입 및 회원 탈퇴 관련 NotificationCenter Extensions 코드 제거 - AccountSignInViewController FamilyId 예외 로직 추가 - AccountSignInNavigator toJoinFamily 화면 전환 로직 추가 * feat: SplashViewController JoinFamily 화면 전환 코드 추가 * fix: CameraAPIWorker memberId Parameters 제거 및 memberId 변수 추가 - AppKey 값 수정 - ProfileFeedViewReactor BaseViewReactor Reactor init 시점 변경으로 인해 로직 수정 - ProfileGlobalState fetchMemberId 함수 추가 - NavigationDIContainer, RealEmojiDIContainer 의존성 주입 코드 추가 --- .../DIContainer/NavigatorDIContainer.swift | 6 +++++ .../DIContainer/RealEmojiDIContainer.swift | 8 ++++++ .../Navigator/AccountSignInNavigator.swift | 6 +++++ .../Navigator/SplashNavigator.swift | 6 +++++ .../AccountSignInViewController.swift | 4 +++ .../Camera/Reactor/CameraViewReactor.swift | 4 +-- .../JoinFamilyViewController.swift | 11 +++----- .../Privacy/PrivacyViewController.swift | 25 ++++++------------- .../Profile/ProfileFeedViewController.swift | 6 ----- .../Reactor/ProfileFeedViewReactor.swift | 21 +++++++++++++--- .../Profile/Reactor/ProfileViewReactor.swift | 1 + .../Splash/SplashViewController.swift | 17 +++---------- .../Core/Sources/Bibbi/BBNetwork/API.swift | 2 +- .../Bibbi/BBServices/ProfileGlobalState.swift | 8 ++++++ .../Sources/Extensions/Notification+Ext.swift | 3 --- .../APIs/App/Repository/AppRepository.swift | 2 +- .../Camera/CameraAPI/CameraAPIWorker.swift | 16 +++++++++--- .../Camera/Repository/CameraRepository.swift | 10 ++++---- .../Entities/PostList/PostListQuery.swift | 2 +- .../Interfaces/CameraRepositoryProtocol.swift | 3 +-- .../FetchCameraRealEmojiListUseCase.swift | 6 ++--- 21 files changed, 97 insertions(+), 70 deletions(-) diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/NavigatorDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/NavigatorDIContainer.swift index f2d2420bf..71427a118 100644 --- a/14th-team5-iOS/App/Sources/Application/DIContainer/NavigatorDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/NavigatorDIContainer.swift @@ -31,6 +31,12 @@ final class NavigatorDIContainer: BaseContainer { ) } + container.register(type: HomeNavigatorProtocol.self) { _ in + HomeNavigator( + navigationController: makeUINavigationController() + ) + } + container.register(type: MainNavigatorProtocol.self) { _ in MainNavigator(navigationController: makeUINavigationController()) } diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/RealEmojiDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/RealEmojiDIContainer.swift index 92c448c33..4f2aad9fa 100644 --- a/14th-team5-iOS/App/Sources/Application/DIContainer/RealEmojiDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/RealEmojiDIContainer.swift @@ -25,6 +25,10 @@ final class RealEmojiDIContainer: BaseContainer { private func makeFetchRealEmojiListUseCase() -> FetchRealEmojiListUseCaseProtocol { return FetchRealEmojiListUseCase(realEmojiRepository: repository) } + + private func makeFetchMyRealEmojiUseCase() -> FetchMyRealEmojiUseCaseProtocol { + return FetchMyRealEmojiUseCase(realEmojiRepository: repository) + } } extension RealEmojiDIContainer { @@ -40,5 +44,9 @@ extension RealEmojiDIContainer { container.register(type: FetchRealEmojiListUseCaseProtocol.self) { _ in self.makeFetchRealEmojiListUseCase() } + + container.register(type: FetchMyRealEmojiUseCaseProtocol.self) { _ in + self.makeFetchMyRealEmojiUseCase() + } } } diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/AccountSignInNavigator.swift b/14th-team5-iOS/App/Sources/Application/Navigator/AccountSignInNavigator.swift index 3b18bdda2..7f0b145e7 100644 --- a/14th-team5-iOS/App/Sources/Application/Navigator/AccountSignInNavigator.swift +++ b/14th-team5-iOS/App/Sources/Application/Navigator/AccountSignInNavigator.swift @@ -12,6 +12,7 @@ import UIKit protocol AccountSignInNavigatorProtocol: BaseNavigator { func toMain() func toSignUp() + func toJoinFamily() } final class AccountSignInNavigator: AccountSignInNavigatorProtocol { @@ -31,4 +32,9 @@ final class AccountSignInNavigator: AccountSignInNavigatorProtocol { let vc = AccountSignUpDIContainer().makeViewController() navigationController.setViewControllers([vc], animated: false) } + + func toJoinFamily() { + let vc = JoinFamilyViewControllerWrapper().viewController + navigationController.setViewControllers([vc], animated: false) + } } diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/SplashNavigator.swift b/14th-team5-iOS/App/Sources/Application/Navigator/SplashNavigator.swift index 024940b7f..db1aa23a7 100644 --- a/14th-team5-iOS/App/Sources/Application/Navigator/SplashNavigator.swift +++ b/14th-team5-iOS/App/Sources/Application/Navigator/SplashNavigator.swift @@ -13,6 +13,7 @@ protocol SplashNavigatorProtocol: BaseNavigator { func toJoined() func toSignIn() func toOnboarding() + func toJoinFamily() } final class SplashNavigator: SplashNavigatorProtocol { @@ -48,4 +49,9 @@ final class SplashNavigator: SplashNavigatorProtocol { navigationController.setViewControllers([vc], animated: false) } + func toJoinFamily() { + let vc = JoinFamilyViewControllerWrapper().viewController + navigationController.setViewControllers([vc], animated: false) + } + } 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 d2cfed3b4..05269d5b0 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignIn/AccountSignInViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignIn/AccountSignInViewController.swift @@ -146,6 +146,10 @@ extension AccountSignInViewController { } if isFirstOnboarding || isTemporaryToken == false { + if App.Repository.member.familyId.value == nil { + signInNavigator.toJoinFamily() + return + } signInNavigator.toMain() return } diff --git a/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraViewReactor.swift index f4d5ec864..29ba262f4 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraViewReactor.swift @@ -221,7 +221,7 @@ extension CameraViewReactor { case .realEmoji: return .concat( - fetchRealEmojiListUseCase.execute(memberId: memberId) + fetchRealEmojiListUseCase.execute() .asObservable() .withUnretained(self) .flatMap { owner, entity -> Observable in @@ -373,7 +373,7 @@ extension CameraViewReactor { .flatMap { realEmojiEntity -> Observable in guard let createRealEmojiEntity = realEmojiEntity else { return .just(.setErrorAlert(true))} owner.provider.realEmojiGlobalState.createRealEmojiImage(indexPath: owner.currentState.emojiType.rawValue - 1, image: createRealEmojiEntity.realEmojiImageURL, emojiType: createRealEmojiEntity.realEmojiType) - return owner.fetchRealEmojiListUseCase.execute(memberId: owner.memberId) + return owner.fetchRealEmojiListUseCase.execute() .asObservable() .flatMap { reloadEntity -> Observable in return .concat( diff --git a/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/ViewController/JoinFamilyViewController.swift b/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/ViewController/JoinFamilyViewController.swift index 864b8398a..88376c15d 100644 --- a/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/ViewController/JoinFamilyViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/ViewController/JoinFamilyViewController.swift @@ -83,12 +83,6 @@ final class JoinFamilyViewController: BaseViewController { .bind(onNext: { $0.0.newGroupAlertController()}) .disposed(by: disposeBag) - NotificationCenter.default - .rx.notification(.didTapCreatFamilyGroupButton) - .map { _ in Reactor.Action.makeFamily } - .bind(to: reactor.action) - .disposed(by: disposeBag) - joinFamilyButton.rx.tap .map { Reactor.Action.joinFamily } .bind(to: reactor.action) @@ -137,9 +131,10 @@ extension JoinFamilyViewController { ) let cancelAction = UIAlertAction(title: "취소", style: .cancel, handler: nil) - let confirmAction = UIAlertAction(title: "확인", style: .default) { _ in + let confirmAction = UIAlertAction(title: "확인", style: .default) { [weak self] _ in + guard let self else { return } MPEvent.Account.creatGroupFinished.track(with: nil) - NotificationCenter.default.post(name: .didTapCreatFamilyGroupButton, object: nil, userInfo: nil) + self.reactor?.action.onNext(.makeFamily) } [cancelAction, confirmAction].forEach(resignAlertController.addAction(_:)) diff --git a/14th-team5-iOS/App/Sources/Presentation/Privacy/PrivacyViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Privacy/PrivacyViewController.swift index 92b46be8d..0fc776c1c 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Privacy/PrivacyViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Privacy/PrivacyViewController.swift @@ -121,20 +121,7 @@ public final class PrivacyViewController: BaseViewController @Navigator var privacyNavigator: PrivacyNavigatorProtocol privacyNavigator.toSignIn() }.disposed(by: disposeBag) - - NotificationCenter.default - .rx.notification(.UserAccountLogout) - .map { _ in Reactor.Action.didTapLogoutButton } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - NotificationCenter.default - .rx.notification(.UserFamilyResign) - .map { _ in Reactor.Action.didTapFamilyUserResign } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - + inquiryBannerView .rx.tap .throttle(.milliseconds(300), scheduler: MainScheduler.instance) @@ -233,8 +220,9 @@ extension PrivacyViewController { let cancelAction = UIAlertAction(title: "취소", style: .cancel, handler: nil) - let confirmAction = UIAlertAction(title: "확인", style: .default) { _ in - NotificationCenter.default.post(name: .UserAccountLogout, object: nil, userInfo: nil) + let confirmAction = UIAlertAction(title: "확인", style: .default) { [weak self]_ in + guard let self else { return } + self.reactor?.action.onNext(.didTapLogoutButton) } [cancelAction, confirmAction].forEach(logoutAlertController.addAction(_:)) @@ -251,8 +239,9 @@ extension PrivacyViewController { let cancelAction = UIAlertAction(title: "취소", style: .cancel, handler: nil) - let confirmAction = UIAlertAction(title: "확인", style: .default) { _ in - NotificationCenter.default.post(name: .UserFamilyResign, object: nil, userInfo: nil) + let confirmAction = UIAlertAction(title: "확인", style: .default) { [weak self ]_ in + guard let self else { return } + self.reactor?.action.onNext(.didTapFamilyUserResign) } [cancelAction, confirmAction].forEach(resignAlertController.addAction(_:)) diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileFeedViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileFeedViewController.swift index ae4f4b13d..01345af25 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileFeedViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileFeedViewController.swift @@ -68,12 +68,6 @@ final class ProfileFeedViewController: BaseViewController) -> Observable { + let fetchMemberIdMutation = provider.profileGlobalState.event + .flatMap { event -> Observable in + switch event { + case let .fetchMemberId(memberId): + return .just(.reloadFeedItems(memberId)) + default: + return .empty() + } + } + + return .merge(fetchMemberIdMutation, action) + } + func mutate(action: Action) -> Observable { var query: PostListQuery = PostListQuery(page: currentState.feedPage, size: 10, date: "", memberId: currentState.memberId, type: currentState.type, sort: .desc) switch action { - case .reloadFeedItems: + case let .reloadFeedItems(memberId): + query.memberId = memberId return feedUseCase.execute(query: query) .asObservable() .flatMap { entity -> Observable in diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift index ee7b64b6e..334ab7cd9 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift @@ -86,6 +86,7 @@ public final class ProfileViewReactor: Reactor { //TODO: Keychain, UserDefaults 추가 switch action { case .viewDidLoad: + provider.profileGlobalState.fetchMemberdId(memberId: currentState.memberId) return fetchMembersProfileUseCase.execute(memberId: currentState.memberId) .asObservable() .flatMap { entity -> Observable in diff --git a/14th-team5-iOS/App/Sources/Presentation/Splash/SplashViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Splash/SplashViewController.swift index 4702040e5..5c9f0b368 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Splash/SplashViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Splash/SplashViewController.swift @@ -93,31 +93,22 @@ public final class SplashViewController: BaseViewController { private func showNextPage(with member: MemberInfo?) { @Navigator var splashNavigator: SplashNavigatorProtocol - + print("memberId: \(member)") guard let member = member else { splashNavigator.toSignIn() -// container = UINavigationController(rootViewController: AccountSignInDIContainer().makeViewController()) -// sceneDelegate.window?.rootViewController = container -// sceneDelegate.window?.makeKeyAndVisible() return } - + print("member FamilYId: \(member.familyId)") if let _ = member.familyId { if UserDefaults.standard.inviteCode != nil { splashNavigator.toJoined() -// container = UINavigationController(rootViewController: JoinedFamilyDIContainer().makeViewController()) } else { splashNavigator.toHome() -// container = UINavigationController(rootViewController: MainViewControllerWrapper().makeViewController()) + return } -// sceneDelegate.window?.rootViewController = container -// sceneDelegate.window?.makeKeyAndVisible() return } else { - splashNavigator.toOnboarding() -// container = UINavigationController(rootViewController: OnBoardingDIContainer().makeViewController()) -// sceneDelegate.window?.rootViewController = container -// sceneDelegate.window?.makeKeyAndVisible() + splashNavigator.toJoinFamily() return } } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/API.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/API.swift index f7dc9e557..da83f68b9 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/API.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/API.swift @@ -51,7 +51,7 @@ public enum BibbiAPI { public var value: String { switch self { case let .auth(token): return "Bearer \(token)" - case .xAppKey: return "7c5aaa36-570e-491f-b18a-26a1a0b72959" // TODO: - 번들에서 가져오기 + case .xAppKey: return "7b159d28-b106-4b6d-a490-1fd654ce40c2" // 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/BBServices/ProfileGlobalState.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ProfileGlobalState.swift index 473b1bf4d..e0d204147 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ProfileGlobalState.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ProfileGlobalState.swift @@ -11,6 +11,7 @@ import RxSwift public enum ProfileEvent { case refreshFamilyMembers + case fetchMemberId(String) } public protocol ProfileGlobalStateType { @@ -18,6 +19,8 @@ public protocol ProfileGlobalStateType { @discardableResult func refreshFamilyMembers() -> Observable + @discardableResult + func fetchMemberdId(memberId: String) -> Observable } final public class ProfileGlobalState: BaseService, ProfileGlobalStateType { @@ -27,5 +30,10 @@ final public class ProfileGlobalState: BaseService, ProfileGlobalStateType { event.onNext(.refreshFamilyMembers) return Observable.just(()) } + + public func fetchMemberdId(memberId: String) -> Observable { + event.onNext(.fetchMemberId(memberId)) + return Observable.just(memberId) + } } diff --git a/14th-team5-iOS/Core/Sources/Extensions/Notification+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/Notification+Ext.swift index 1962bb9f8..20cfdfa13 100644 --- a/14th-team5-iOS/Core/Sources/Extensions/Notification+Ext.swift +++ b/14th-team5-iOS/Core/Sources/Extensions/Notification+Ext.swift @@ -14,11 +14,8 @@ extension Notification.Name { public static let AccountViewPresignURLDismissNotification = Notification.Name("AccountViewPresignURLDismissNotification") public static let AppVersionsCheckWithRedirectStore = Notification.Name("AppVersionsCheckWithRedirectStore") public static let ProfileImageInitializationUpdate = Notification.Name("ProfileImageInitializationUpdate") - public static let UserAccountLogout = Notification.Name("UserAccountLogout") - public static let UserFamilyResign = Notification.Name("UserFamilyResign") public static let DidFinishProfileImageUpdate = Notification.Name("DidFinishProfileImageUpdate") public static let didTapSelectableCameraButton = Notification.Name("didTapSelectableCameraButton") - public static let didTapCreatFamilyGroupButton = Notification.Name("didTapCreatFamilyGroupButton") public static let didTapBibbiToastTranstionButton = Notification.Name("didTapTranstionButton") public static let didTapUpdateButton = Notification.Name("didTapUpdateButton") } 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 0c1d8a415..0d374e76c 100644 --- a/14th-team5-iOS/Data/Sources/APIs/App/Repository/AppRepository.swift +++ b/14th-team5-iOS/Data/Sources/APIs/App/Repository/AppRepository.swift @@ -33,7 +33,7 @@ extension AppRepository { public func fetchAppVersion() -> Observable { // TODO: - xAppKey 불러오는 코드 다시 작성하기 - let appKey = "7c5aaa36-570e-491f-b18a-26a1a0b72959" + let appKey = "7b159d28-b106-4b6d-a490-1fd654ce40c2" return appApiWorker.fetchAppVersion(appKey: appKey) .map { $0?.toDomain() } diff --git a/14th-team5-iOS/Data/Sources/Trash/Camera/CameraAPI/CameraAPIWorker.swift b/14th-team5-iOS/Data/Sources/Trash/Camera/CameraAPI/CameraAPIWorker.swift index 8aabb169b..00999a868 100644 --- a/14th-team5-iOS/Data/Sources/Trash/Camera/CameraAPI/CameraAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Trash/Camera/CameraAPI/CameraAPIWorker.swift @@ -91,7 +91,9 @@ extension CameraAPIWorker { } - public func createRealEmojiPresignedURL(accessToken: String, memberId: String, parameters: Encodable) -> Single { + public func createRealEmojiPresignedURL(accessToken: String, parameters: Encodable) -> Single { + //TODO: Repository로 코드 원복 + let memberId = App.Repository.member.memberID.value ?? "" let spec = CameraAPIs.uploadRealEmojiURL(memberId).spec return request(spec: spec, headers: [BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.xAuthToken(accessToken)], jsonEncodable: parameters) @@ -102,7 +104,9 @@ extension CameraAPIWorker { } - public func uploadRealEmojiImageToS3(accessToken: String, memberId: String, parameters: Encodable) -> Single { + public func uploadRealEmojiImageToS3(accessToken: String, parameters: Encodable) -> Single { + //TODO: Repository로 코드 원복 + let memberId = App.Repository.member.memberID.value ?? "" let spec = CameraAPIs.updateRealEmojiImage(memberId).spec return request(spec: spec, headers: [BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.xAuthToken(accessToken)], jsonEncodable: parameters) @@ -112,7 +116,9 @@ extension CameraAPIWorker { .asSingle() } - public func loadRealEmojiImage(accessToken: String, memberId: String) -> Single { + public func loadRealEmojiImage(accessToken: String) -> Single { + //TODO: Repository로 코드 원복 + let memberId = App.Repository.member.memberID.value ?? "" let spec = CameraAPIs.reloadRealEmoji(memberId).spec return request(spec: spec, headers: [BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.xAuthToken(accessToken)]) @@ -122,7 +128,9 @@ extension CameraAPIWorker { .asSingle() } - public func updateRealEmojiImage(accessToken: String, memberId: String, realEmojiId: String, parameters: Encodable) -> Single { + public func updateRealEmojiImage(accessToken: String, realEmojiId: String, parameters: Encodable) -> Single { + //TODO: Repository로 코드 원복 + let memberId = App.Repository.member.memberID.value ?? "" let spec = CameraAPIs.modifyRealEmojiImage(memberId, realEmojiId).spec return request(spec: spec, headers: [BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.xAuthToken(accessToken)], jsonEncodable: parameters) .subscribe(on: Self.queue) diff --git a/14th-team5-iOS/Data/Sources/Trash/Camera/Repository/CameraRepository.swift b/14th-team5-iOS/Data/Sources/Trash/Camera/Repository/CameraRepository.swift index 74c28d799..11971766e 100644 --- a/14th-team5-iOS/Data/Sources/Trash/Camera/Repository/CameraRepository.swift +++ b/14th-team5-iOS/Data/Sources/Trash/Camera/Repository/CameraRepository.swift @@ -54,22 +54,22 @@ extension CameraRepository: CameraRepositoryProtocol { } public func fetchRealEmojiImageURL(memberId: String, parameters: CameraRealEmojiParameters) -> Single { - return cameraAPIWorker.createRealEmojiPresignedURL(accessToken: accessToken, memberId: memberId, parameters: parameters) + return cameraAPIWorker.createRealEmojiPresignedURL(accessToken: accessToken, parameters: parameters) .map { $0?.toDomain() } } public func uploadRealEmojiImageToS3(memberId: String, parameters: CameraCreateRealEmojiParameters) -> Single { - return cameraAPIWorker.uploadRealEmojiImageToS3(accessToken: accessToken, memberId: memberId, parameters: parameters) + return cameraAPIWorker.uploadRealEmojiImageToS3(accessToken: accessToken, parameters: parameters) .map { $0?.toDomain() } } - public func fetchRealEmojiItems(memberId: String) -> Single<[CameraRealEmojiImageItemEntity?]> { - return cameraAPIWorker.loadRealEmojiImage(accessToken: accessToken, memberId: memberId) + public func fetchRealEmojiItems() -> Single<[CameraRealEmojiImageItemEntity?]> { + return cameraAPIWorker.loadRealEmojiImage(accessToken: accessToken) .map { $0?.toDomain() ?? [] } } public func updateRealEmojiImage(memberId: String, realEmojiId: String, parameters: CameraUpdateRealEmojiParameters) -> Single { - return cameraAPIWorker.updateRealEmojiImage(accessToken: accessToken, memberId: memberId, realEmojiId: realEmojiId, parameters: parameters) + return cameraAPIWorker.updateRealEmojiImage(accessToken: accessToken, realEmojiId: realEmojiId, parameters: parameters) .map { $0?.toDomain() } } diff --git a/14th-team5-iOS/Domain/Sources/Entities/PostList/PostListQuery.swift b/14th-team5-iOS/Domain/Sources/Entities/PostList/PostListQuery.swift index 1196057e7..4cf46314e 100644 --- a/14th-team5-iOS/Domain/Sources/Entities/PostList/PostListQuery.swift +++ b/14th-team5-iOS/Domain/Sources/Entities/PostList/PostListQuery.swift @@ -41,7 +41,7 @@ public struct PostListQuery { public var page: Int public let size: Int public let date: String - public let memberId: String? + public var memberId: String? public let type: PostType public let sort: String diff --git a/14th-team5-iOS/Domain/Sources/Trash/Camera/Interfaces/CameraRepositoryProtocol.swift b/14th-team5-iOS/Domain/Sources/Trash/Camera/Interfaces/CameraRepositoryProtocol.swift index b2c0082a2..80ad9fa08 100644 --- a/14th-team5-iOS/Domain/Sources/Trash/Camera/Interfaces/CameraRepositoryProtocol.swift +++ b/14th-team5-iOS/Domain/Sources/Trash/Camera/Interfaces/CameraRepositoryProtocol.swift @@ -68,13 +68,12 @@ public protocol CameraRepositoryProtocol { var disposeBag: DisposeBag { get } var accessToken: String { get } - func addPresignedeImageURL(parameters: CameraDisplayImageParameters) -> Single func uploadImageToS3(to url: String, from image: Data) -> Single func editProfleImageToS3(memberId: String, parameter: ProfileImageEditParameter) -> Single func fetchRealEmojiImageURL(memberId: String, parameters: CameraRealEmojiParameters) -> Single func uploadRealEmojiImageToS3(memberId: String, parameters: CameraCreateRealEmojiParameters) -> Single - func fetchRealEmojiItems(memberId: String) -> Single<[CameraRealEmojiImageItemEntity?]> + func fetchRealEmojiItems() -> Single<[CameraRealEmojiImageItemEntity?]> func updateRealEmojiImage(memberId: String, realEmojiId: String, parameters: CameraUpdateRealEmojiParameters) -> Single func fetchTodayMissionItem() -> Single func combineWithTextImage(parameters: CameraDisplayPostParameters, query: CameraMissionFeedQuery) -> Single diff --git a/14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/RealEmoji/FetchCameraRealEmojiListUseCase.swift b/14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/RealEmoji/FetchCameraRealEmojiListUseCase.swift index 1795ea577..ec0d1dfde 100644 --- a/14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/RealEmoji/FetchCameraRealEmojiListUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/RealEmoji/FetchCameraRealEmojiListUseCase.swift @@ -12,7 +12,7 @@ import RxCocoa public protocol FetchCameraRealEmojiListUseCaseProtocol { - func execute(memberId: String) -> Single<[CameraRealEmojiImageItemEntity?]> + func execute() -> Single<[CameraRealEmojiImageItemEntity?]> } @@ -24,8 +24,8 @@ public final class FetchCameraRealEmojiListUseCase: FetchCameraRealEmojiListUseC self.cameraRepository = cameraRepository } - public func execute(memberId: String) -> Single<[CameraRealEmojiImageItemEntity?]> { - return cameraRepository.fetchRealEmojiItems(memberId: memberId) + public func execute() -> Single<[CameraRealEmojiImageItemEntity?]> { + return cameraRepository.fetchRealEmojiItems() } } From 32bdd37f6ec9b335ab9a4d1364b295512acca638 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=84?= Date: Thu, 26 Sep 2024 16:37:59 +0900 Subject: [PATCH 217/263] =?UTF-8?q?fix:=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EB=AF=B8=EC=85=98=20=ED=94=BC=EB=93=9C=20=EB=B6=88=EB=9F=AC?= =?UTF-8?q?=EC=98=A4=EA=B8=B0=20=EA=B8=B0=EB=8A=A5=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EB=B0=8F=20=EA=B8=B0=ED=83=80=20=EC=9D=B4=EC=8A=88=20=EB=B0=98?= =?UTF-8?q?=EC=98=81=20(#658)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: ProfileFeedViewReactor transform Method 제거, reloadFeedItems 매게변수 제거 - ProfileGlobalState fetchMemberId 함수 제거 - BaseViewController reactor init 시점 변경 및 setupReactor 메서드 제거 * feat: ProfileViewReactor DeleteMembersProfileUseCase 추가 및 프로필 이미지 삭제 API 호출 로직 추가 * fix: CameraViewReactor flashMode State Toggle 로직으로 변경 * fix: BaseTableView, ReactorViewController initialReactor Property 및 setupReactor 메서드 제거 --- .../Camera/Reactor/CameraViewReactor.swift | 2 +- .../Profile/ProfileFeedViewController.swift | 5 +++++ .../Reactor/ProfileFeedViewReactor.swift | 20 ++----------------- .../Profile/Reactor/ProfileViewReactor.swift | 9 +++------ .../Core/Sources/Base/BaseTableView.swift | 8 +------- .../Sources/Base/BaseViewController.swift | 10 +--------- .../Sources/Base/ReactorViewController.swift | 8 +------- .../Bibbi/BBServices/ProfileGlobalState.swift | 8 -------- 8 files changed, 14 insertions(+), 56 deletions(-) diff --git a/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraViewReactor.swift index 29ba262f4..037ca4857 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraViewReactor.swift @@ -126,7 +126,7 @@ public final class CameraViewReactor: Reactor { case .didTapToggleButton: return Observable.just(.setPosition(!self.currentState.isSwitchPosition)) case .didTapFlashButton: - return Observable.just(.setFlashMode(self.currentState.isFlashMode)) + return Observable.just(.setFlashMode(!self.currentState.isFlashMode)) case let .didTapZoomButton(scale): if self.currentState.zoomScale == 2.0 { return .just(.setZoomScale(self.currentState.zoomScale - scale)) diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileFeedViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileFeedViewController.swift index 01345af25..ba1e6fad2 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileFeedViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileFeedViewController.swift @@ -67,6 +67,11 @@ final class ProfileFeedViewController: BaseViewController) -> Observable { - let fetchMemberIdMutation = provider.profileGlobalState.event - .flatMap { event -> Observable in - switch event { - case let .fetchMemberId(memberId): - return .just(.reloadFeedItems(memberId)) - default: - return .empty() - } - } - - return .merge(fetchMemberIdMutation, action) - } - func mutate(action: Action) -> Observable { var query: PostListQuery = PostListQuery(page: currentState.feedPage, size: 10, date: "", memberId: currentState.memberId, type: currentState.type, sort: .desc) switch action { - case let .reloadFeedItems(memberId): - query.memberId = memberId + case .reloadFeedItems: return feedUseCase.execute(query: query) .asObservable() .flatMap { entity -> Observable in diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift index 334ab7cd9..fafd0a04d 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift @@ -19,6 +19,7 @@ public final class ProfileViewReactor: Reactor { @Injected private var createProfilePresignedUseCase: CreateCameraUseCaseProtocol @Injected private var uploadProfileImageUseCase: FetchCameraUploadImageUseCaseProtocol @Injected private var updateProfileUseCase: UpdateMembersProfileUseCaseProtocol + @Injected private var deleteProfileImageUseCase: DeleteMembersProfileUseCaseProtocol @@ -86,7 +87,6 @@ public final class ProfileViewReactor: Reactor { //TODO: Keychain, UserDefaults 추가 switch action { case .viewDidLoad: - provider.profileGlobalState.fetchMemberdId(memberId: currentState.memberId) return fetchMembersProfileUseCase.execute(memberId: currentState.memberId) .asObservable() .flatMap { entity -> Observable in @@ -177,19 +177,16 @@ public final class ProfileViewReactor: Reactor { } } ) - case .didTapInitProfile: - return fetchMembersProfileUseCase.execute(memberId: memberId) + return deleteProfileImageUseCase.execute(memberId: memberId) .asObservable() - .flatMap { entity -> Observable in + .flatMap { entity -> Observable in return .concat( .just(.setLoading(false)), .just(.setProfileMemberItems(entity)), .just(.setLoading(true)) ) - } - case let .didTapSegementControl(feedType): provider.profilePageGlobalState.didTapSegmentedPageType(type: feedType) return .just(.setProfileFeedType(feedType)) diff --git a/14th-team5-iOS/Core/Sources/Base/BaseTableView.swift b/14th-team5-iOS/Core/Sources/Base/BaseTableView.swift index 62a9abd54..4fd4726de 100644 --- a/14th-team5-iOS/Core/Sources/Base/BaseTableView.swift +++ b/14th-team5-iOS/Core/Sources/Base/BaseTableView.swift @@ -16,7 +16,6 @@ open class BaseTableView: UITableView, ReactorKit.View where R: Reactor { // MARK: - Properties - private var initialReactor: Reactor? public var disposeBag: RxSwift.DisposeBag = DisposeBag() @@ -24,7 +23,7 @@ open class BaseTableView: UITableView, ReactorKit.View where R: Reactor { public convenience init(reactor: Reactor? = nil) { self.init(frame: .zero, style: .plain) - self.initialReactor = reactor + self.reactor = reactor } public override init(frame: CGRect, style: UITableView.Style) { @@ -32,7 +31,6 @@ open class BaseTableView: UITableView, ReactorKit.View where R: Reactor { setupUI() setupAutoLayout() setupAttributes() - setupReactor() } public required init?(coder: NSCoder) { @@ -52,8 +50,4 @@ open class BaseTableView: UITableView, ReactorKit.View where R: Reactor { // 뷰의 속성 설정을 위한 메서드 open func setupAttributes() { } - - open func setupReactor() { - self.reactor = initialReactor - } } diff --git a/14th-team5-iOS/Core/Sources/Base/BaseViewController.swift b/14th-team5-iOS/Core/Sources/Base/BaseViewController.swift index a04876e5b..fdbd32303 100644 --- a/14th-team5-iOS/Core/Sources/Base/BaseViewController.swift +++ b/14th-team5-iOS/Core/Sources/Base/BaseViewController.swift @@ -16,7 +16,6 @@ open class BaseViewController: UIViewController, ReactorKit.View where R: Rea public typealias Reactor = R // MARK: - Properties - private var initialReactor: Reactor? public var disposeBag: RxSwift.DisposeBag = DisposeBag() public let navigationBarView: BibbiNavigationBarView = BibbiNavigationBarView() @@ -27,7 +26,7 @@ open class BaseViewController: UIViewController, ReactorKit.View where R: Rea public convenience init(reactor: Reactor? = nil) { self.init() - self.initialReactor = reactor + self.reactor = reactor } required public init?(coder: NSCoder) { @@ -40,7 +39,6 @@ open class BaseViewController: UIViewController, ReactorKit.View where R: Rea setupUI() setupAutoLayout() setupAttributes() - setupReactor() } // MARK: - Helpers @@ -62,12 +60,6 @@ open class BaseViewController: UIViewController, ReactorKit.View where R: Rea .disposed(by: disposeBag) } - open func setupReactor() { - if let reactor = initialReactor { - self.reactor = reactor - } - } - /// 서브 뷰 추가를 위한 메서드 open func setupUI() { view.addSubview(navigationBarView) diff --git a/14th-team5-iOS/Core/Sources/Base/ReactorViewController.swift b/14th-team5-iOS/Core/Sources/Base/ReactorViewController.swift index 90425375f..386c72cd4 100644 --- a/14th-team5-iOS/Core/Sources/Base/ReactorViewController.swift +++ b/14th-team5-iOS/Core/Sources/Base/ReactorViewController.swift @@ -19,8 +19,6 @@ open class ReactorViewController: UIViewController, ReactorKit.View where R: // MARK: - Properties - private var initialReactor: Reactor? - public var disposeBag: RxSwift.DisposeBag = DisposeBag() // MARK: - Intializer @@ -31,7 +29,7 @@ open class ReactorViewController: UIViewController, ReactorKit.View where R: public convenience init(reactor: Reactor? = nil) { self.init() - self.initialReactor = reactor + self.reactor = reactor } required public init?(coder: NSCoder) { @@ -45,7 +43,6 @@ open class ReactorViewController: UIViewController, ReactorKit.View where R: setupUI() setupAutoLayout() setupAttributes() - setupReactor() } // MARK: - Helpers @@ -60,7 +57,4 @@ open class ReactorViewController: UIViewController, ReactorKit.View where R: view.backgroundColor = .bibbiBlack } - open func setupReactor() { - self.reactor = initialReactor - } } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ProfileGlobalState.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ProfileGlobalState.swift index e0d204147..473b1bf4d 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ProfileGlobalState.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ProfileGlobalState.swift @@ -11,7 +11,6 @@ import RxSwift public enum ProfileEvent { case refreshFamilyMembers - case fetchMemberId(String) } public protocol ProfileGlobalStateType { @@ -19,8 +18,6 @@ public protocol ProfileGlobalStateType { @discardableResult func refreshFamilyMembers() -> Observable - @discardableResult - func fetchMemberdId(memberId: String) -> Observable } final public class ProfileGlobalState: BaseService, ProfileGlobalStateType { @@ -30,10 +27,5 @@ final public class ProfileGlobalState: BaseService, ProfileGlobalStateType { event.onNext(.refreshFamilyMembers) return Observable.just(()) } - - public func fetchMemberdId(memberId: String) -> Observable { - event.onNext(.fetchMemberId(memberId)) - return Observable.just(memberId) - } } From c1c593c6c071beafe2869aa582dbd7e3d15f641a Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Thu, 26 Sep 2024 16:45:00 +0900 Subject: [PATCH 218/263] =?UTF-8?q?feat:=20BBToastService=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84,=20BBToast=EC=9D=98=20=EB=B2=84=ED=8A=BC=20=EC=95=A1?= =?UTF-8?q?=EC=85=98=EC=9D=B4=20=EC=A0=84=EB=8B=AC=EB=90=98=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?(#646)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: BBToastService 구현 * feat: BBToast문서주석 작성 * fix: Toast의 Button위치가 올바르지 않은 문제 수정 * feat: 코드 리뷰 바녕 --- .../Navigator/ManagementNavigator.swift | 18 +- .../Home/Reactor/MainViewReactor.swift | 13 +- .../Activity/CopyInvitationUrlActivity.swift | 11 +- .../Reactor/ManagementReactor.swift | 29 +-- .../BBCommons/BBAlert/BBAlertAnimation.swift | 2 +- .../BBCommons/BBAlert/BBAlertBackground.swift | 2 +- .../Bibbi/BBCommons/BBButton/BBButton.swift | 7 +- .../Bibbi/BBCommons/BBToast/BBToast.swift | 73 ++++++- .../BBCommons/BBToast/BBToastAnimation.swift | 14 ++ .../BBCommons/BBToast/BBToastBackground.swift | 7 + .../BBToast/BBToastConfiguration.swift | 29 +++ .../BBCommons/BBToast/BBToastDelegate.swift | 25 +++ .../BBCommons/BBToast/BBToastDirection.swift | 2 + .../BBToast/BBToastDismissable.swift | 1 + .../BBToast/BBToastViewConfiguration.swift | 27 ++- .../DefaultToastView/ButtonToastView.swift | 68 ++++--- .../DefaultToastView/DefaultToastView.swift | 16 +- .../DefaultToastView/IconToastView.swift | 35 ++-- .../Bibbi/BBServices/BBToastService.swift | 184 ++++++++++++++++++ .../Bibbi/BBServices/ServiceProvider.swift | 2 + .../BBServices/ToastMessageGlobalState.swift | 2 + 21 files changed, 446 insertions(+), 121 deletions(-) create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBServices/BBToastService.swift diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/ManagementNavigator.swift b/14th-team5-iOS/App/Sources/Application/Navigator/ManagementNavigator.swift index 9f404b53d..4f6441567 100644 --- a/14th-team5-iOS/App/Sources/Application/Navigator/ManagementNavigator.swift +++ b/14th-team5-iOS/App/Sources/Application/Navigator/ManagementNavigator.swift @@ -16,9 +16,7 @@ protocol ManagementNavigatorProtocol: BaseNavigator { func toFamilyNameSetting() func presentSharingSheet(url: URL?) - - func showSuccessToast() - func showErrorToast() + } final class ManagementNavigator: ManagementNavigatorProtocol { @@ -72,18 +70,4 @@ final class ManagementNavigator: ManagementNavigatorProtocol { navigationController.present(activityVC, animated: true) } - - // MARK: - Show - - func showSuccessToast() { - BBToast.default( - image: DesignSystemAsset.link.image, - title: "링크가 복사되었어요" - ).show() - } - - func showErrorToast() { - BBToast.style(.error).show() - } - } diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift index 0deb119e3..0121f8f32 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift @@ -119,18 +119,7 @@ extension MainViewReactor { } } - let eventMutation = provider.managementService.event - .flatMap { event -> Observable in - switch event { - case .didTapCopyUrlAction: - self.pushViewController(type: .showToastMessage(DesignSystemAsset.link.image, "링크가 복사되었어요")) - return .empty() - default: - return .empty() - } - } - - return Observable.merge(mutation, eventMutation, homeMutation) + return Observable.merge(mutation, homeMutation) } func mutate(action: Action) -> Observable { diff --git a/14th-team5-iOS/App/Sources/Presentation/Management/Activity/CopyInvitationUrlActivity.swift b/14th-team5-iOS/App/Sources/Presentation/Management/Activity/CopyInvitationUrlActivity.swift index a35ade7f8..b2ec967d6 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Management/Activity/CopyInvitationUrlActivity.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Management/Activity/CopyInvitationUrlActivity.swift @@ -6,8 +6,11 @@ // import Core +import DesignSystem import UIKit +import RxSwift + // TODO: - 코드 리팩토링하기 public class CopyInvitationUrlActivity: UIActivity { @@ -48,8 +51,14 @@ public class CopyInvitationUrlActivity: UIActivity { } public override func perform() { + let viewConfig = BBToastViewConfiguration(minWidth: 100) + provider.bbToastService.show( + image: DesignSystemAsset.link.image, + title: "링크가 복사되었어요", + viewConfig: viewConfig + ) + UIPasteboard.general.string = url.description - provider.managementService.didTapCopUrlAction() // TODO: - 오타 수정 } } 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 c3f5f0583..79f4ac348 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Management/Reactor/ManagementReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Management/Reactor/ManagementReactor.swift @@ -5,13 +5,12 @@ // Created by 김건우 on 12/11/23. // -import UIKit - import Core +import DesignSystem import Domain -import Differentiator +import UIKit + import ReactorKit -import RxSwift public final class ManagementReactor: Reactor { @@ -101,24 +100,6 @@ public final class ManagementReactor: Reactor { } - public func transform(mutation: Observable) -> Observable { - let eventMutation = provider.managementService.event - .withUnretained(self) - .flatMap { - switch $0.1 { - case .didTapCopyUrlAction: - $0.0.navigator.showSuccessToast() - return Observable.empty() - - @unknown default: - return Observable.empty() - } - } - - return Observable.merge(mutation, eventMutation) - } - - // MARK: - Mutate public func mutate(action: Action) -> Observable { @@ -135,7 +116,7 @@ public final class ManagementReactor: Reactor { .concatMap { guard let url = $0.1?.url else { Haptic.notification(type: .error) - $0.0.navigator.showErrorToast() + $0.0.provider.bbToastService.show(.error) return Observable.just(.setHiddenSharingProgressHud(true)) } @@ -176,7 +157,7 @@ public final class ManagementReactor: Reactor { .concatMap { guard let results = $0.1?.results else { Haptic.notification(type: .error) - $0.0.navigator.showErrorToast() + $0.0.provider.bbToastService.show(.error) return Observable.concat( Observable.just(.setMemberDatasource([])), Observable.just(.setHiddenTableProgressHud(true)), diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertAnimation.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertAnimation.swift index eb10c27b4..33243ac97 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertAnimation.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertAnimation.swift @@ -16,7 +16,7 @@ extension BBAlert { /// 예를 들어, BBAlert 뷰가 나타나거나 사라질 때 페이드 효과를 주기 원한다면 아래와 같이 ``BBAlertConfiguration``을 설정해야 합니다. /// /// ```swift - /// let config = BBConfiguration( + /// let config = BBAlertConfiguration( /// enteringAnimation: .fade(alpha: 0), /// exitingAnimation: .fade(alpha: 0) /// ) diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertBackground.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertBackground.swift index e4f033727..13f8081b3 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertBackground.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertBackground.swift @@ -17,7 +17,7 @@ extension BBAlert { /// 아무런 배경 색상을 적용하지 않습니다. case none - // 특정 색상을 배경 색상으로 적용합니다. + /// 특정 색상을 배경 색상으로 적용합니다. case color(color: UIColor = defaultImageTint.withAlphaComponent(0.25)) } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBButton/BBButton.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBButton/BBButton.swift index a70cf66f8..21034c7c1 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBButton/BBButton.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBButton/BBButton.swift @@ -9,10 +9,8 @@ import UIKit import SnapKit -extension UIControl.State: Hashable { - -} +/// BBButton은 `titleLabel`이 BBFontStyle에 맞게 커스텀되어 있는 버튼입니다. public class BBButton: UIButton { // MARK: - Views @@ -120,3 +118,6 @@ public class BBButton: UIButton { } } + + +extension UIControl.State: Hashable { } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToast.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToast.swift index 3d8f097c6..f03292bb2 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToast.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToast.swift @@ -10,8 +10,38 @@ import UIKit // MARK: - Typealias -public typealias BBToastAction = ((BBToast?) -> Void)? +/// BBToast 버튼의 동작을 정의하는 핸들러입니다. +public typealias BBToastActionHandler = ((BBToast?) -> Void)? +/// BBToast는 BBToast를 화면에 띄우게 도와줍니다. +/// +/// **show(after:)** 메서드로 BBToastt를 띄울 수 있으며, **close(animated:, completion:)** 메서드로 BBToast를 사라지게 할 수 있습니다. +/// +/// 아래는 BBToast를 띄우는 가장 기본적인 방법을 보여줍니다. +/// +/// ```swift +/// let toast = BBTast.text("Hello, BBToast!") +/// toast.show() +/// ``` +/// +/// BBToast는 델리게이트 패턴을 지원합니다. BBToast의 생명 주기에 맞게 필요한 동작을 구현할 수 있습니다. BBToast는 멀티캐스트 델리게이트 패턴으로 구현되어 있습니다. +/// +/// /아래는 BBToast에 델리게이트 패턴을 구현하는 방법을 보여줍니다. +/// +/// ```swift +/// // ViewController.swift +/// let toast = BBToast.text("Hello, BBToast!") +/// toast.addDelegate(self) +/// +/// extension ViewController: BBToastDelegate { ... } +/// ```` +/// +/// - Note: 지원하는 델리게이트 메서드에 대한 자세한 정보는 ``BBToastDelegate``를 참조하세요. +/// +/// ``BBToastConfiguration``과 ``BBToastViewConfiguration`` 구조체를 활용하여 BBToast의 애니메이션, 배경 색상 및 BBToast 뷰의 크기, 둥글기 반경을 설정할 수 있습니다. +/// +/// - Authors: 김소월 +/// public class BBToast { // MARK: - Properties @@ -76,7 +106,7 @@ public class BBToast { /// - Returns: BBToast public static func `default`( image: UIImage, - imageTint: UIColor = defaultImageTint, + imageTint: UIColor? = defaultImageTint, title: String, titleColor: UIColor? = nil, titleFontStyle: BBFontStyle? = nil, @@ -111,14 +141,15 @@ public class BBToast { /// - config: Toast 설정값 /// - Returns: BBToast public static func button( - image: UIImage, - imageTint: UIColor = defaultImageTint, + image: UIImage? = nil, + imageTint: UIColor? = defaultImageTint, title: String, titleColor: UIColor? = nil, titleFontStyle: BBFontStyle? = nil, buttonTitle: String, buttonTitleFontStyle: BBFontStyle? = nil, buttonTint: UIColor? = nil, + action: BBToastActionHandler = nil, viewConfig: BBToastViewConfiguration = BBToastViewConfiguration(), config: BBToastConfiguration = BBToastConfiguration() ) -> BBToast { @@ -132,6 +163,7 @@ public class BBToast { buttonTitle: buttonTitle, buttonTitleFontStlye: buttonTitleFontStyle, buttonTint: buttonTint, + action: action, viewConfig: viewConfig ), viewConfig: viewConfig @@ -152,7 +184,7 @@ public class BBToast { switch style { case .error: let viewConfig = BBToastViewConfiguration( - minWidth: 250 + minWidth: 100 ) let view = DefaultToastView( child: IconToastView( @@ -166,6 +198,11 @@ public class BBToast { } } + /// 직접 커스텀한 뷰로 BBToast를 생성합니다. + /// - Parameters: + /// - view: BBToastStackView 프로토콜을 준수하는 UIView + /// - config: BBToast 설정값 + /// - Returns: BBToast public static func custom( view: BBToastView, config: BBToastConfiguration = BBToastConfiguration() @@ -176,14 +213,20 @@ public class BBToast { // MARK: - Show + /// BBToast를 화면에 보이게 합니다. + /// - Parameters: + /// - type: HapticFeedback의 타입 + /// - time: 지연 시간 public func show( haptic type: UINotificationFeedbackGenerator.FeedbackType, after time: TimeInterval = 0 ) { - UINotificationFeedbackGenerator().notificationOccurred(type) + Haptic.notification(type: type) show(after: time) } + /// BBToast를 화면에 보이게 합니다. + /// - Parameter delay: 지연 시간 public func show(after delay: TimeInterval = 0) { if let backgroundView = self.createBackgroundView() { self.backgroundView = backgroundView @@ -228,6 +271,12 @@ public class BBToast { // MARK: - Close + /// BBToast를 화면에 사라지게 합니다. + /// + /// BBToast가 화면에 사라지고 나면 완료 핸들러가 호출됩니다. 완료 핸들러는 델리게이트의 `didCloseToast(_:)` 메서드가 호출되기 전에 실행됩니다. + /// - Parameters: + /// - animated: 애니메이션 유무 + /// - completion: 완료 핸들러 public func close( animated: Bool = true, completion: (() -> Void)? = nil @@ -286,16 +335,22 @@ public class BBToast { extension BBToast { + /// 델리게이트 패턴을 적용합니다. + /// - Parameter delegate: BBToastDelegate 프로토콜을 준수하는 객체 public func addDelegate(_ delegate: BBToastDelegate) { Self.multicast.add(delegate) } - public func addButtonAction( - _ action: BBToastAction = nil + /// 버튼을 추가합니다. + /// + /// `BBToast.button` 메서드로 BBToast를 생성하는 경우, 해당 메서드로 버튼의 액션을 정의해주어야 합니다. + /// - Parameter action: 버튼의 액션 + public func setAction( + _ action: BBToastActionHandler = nil ) { if let view = view as? DefaultToastView, let subview = view.child as? ButtonToastView { - subview.buttonAction = action + subview.action = action } } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastAnimation.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastAnimation.swift index 2e12488a3..f9bf18d38 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastAnimation.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastAnimation.swift @@ -9,6 +9,20 @@ import UIKit extension BBToast { + /// BBToast 뷰가 나타나거나 사라질 때 수행되는 애니메이션을 정의한 열거형입니다. + /// + /// BBToast 뷰가 나타날 때(enteringAnimation)는 뷰의 초기 상태를 전달해야 합니다. BBToast 뷰가 사라질 때(exitingAnimation)은 뷰의 최종 상태를 전달해야 합니다. + /// + /// 예를 들어, BBToast 뷰가 나타나거나 사라질 때 페이드 효과를 주기 원한다면 아래와 같이 ``BBToastConfiguration``을 설정해야 합니다. + /// + /// ```swift + /// let config = BBToastConfiguration( + /// enteringAnimation: .fade(alpha: 0), + /// exitingAnimation: .fade(alpha: 0) + /// ) + /// ``` + /// + /// - Authors: 김소월 public enum Animation { case slide(x: CGFloat, y: CGFloat) case fade(alpha: CGFloat) diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastBackground.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastBackground.swift index a23c2a2b6..cbdea3486 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastBackground.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastBackground.swift @@ -9,8 +9,15 @@ import UIKit extension BBToast { + /// Alert 뒤의 배경 색상을 설정합니다. + /// + /// - Authors: 김소월 public enum Background { + + /// 아무런 배경 색상을 적용하지 않습니다. case none + + /// 특정 색상을 배경 색상으로 적용합니다. case color(color: UIColor = defaultImageTint.withAlphaComponent(0.25)) } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastConfiguration.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastConfiguration.swift index 8f44f55e6..d58310b72 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastConfiguration.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastConfiguration.swift @@ -7,23 +7,52 @@ import UIKit +/// BBToast의 애니메이션, 오버랩 허용 유무를 설정할 수 있습니다. +/// +/// - Note: 높이, 너비, 배경 색상과 둥글기 반경, 버튼 축 방향과 높이 등 BBToast 뷰에 대한 설정은 BBToastViewConfiguration에서 하세요. +/// +/// - Authors: 김소월 public struct BBToastConfiguration { // MARK: - Properties + /// Toast가 나타나는 방향입니다. public let direction: BBToast.Direction + + /// Toast가 사라지는 방법을 정의합니다. public let dismissables: [BBToast.Dismissable] + + /// Toast 애니메이션이 수행되는 시간입니다. public let animationTime: TimeInterval + + /// Toast가 나타날 때 수행되는 애니메이션입니다. public let enteringAnimation: BBToast.Animation + + /// Toast가 사라질 때 수행되는 애니메이션입니다. public let exitingAnimation: BBToast.Animation + + /// Toast 뒤에 배경의 색상을 설정합니다. public let background: BBToast.Background + + /// 여러 Toast의 겹치기 가능 여부를 설정합니다. public let allowToastOverlap: Bool + /// Toast가 보여질 뷰를 설정합니다. public let view: UIView? // MARK: - Intializer + /// BBToast의 애니메이션, 오버랩 허용 유무를 설정할 수 있습니다. + /// - Parameters: + /// - direction: Toast가 나타나는 방향 + /// - dismissables: Toast가 사라지는 방법 + /// - animationTime: Toast 애니메이션 수행 시간 + /// - enteringAnimation: Toast가 나타날 때 수행되는 애니메이션 + /// - exitingAnimation: Toast가 사라질 때 수행되는 애니메이션 + /// - view: Toast가 보여질 뷰 + /// - background: Toast 뒤의 배경의 색상 + /// - allowToastOverlap: Toast 겹치기 허용 유무 public init( direction: BBToast.Direction = .bottom(yOffset: 0), dismissables: [BBToast.Dismissable] = [.time(time: 2.5), .swipe(direction: .natural)], diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastDelegate.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastDelegate.swift index a0c7e1386..b4417b79b 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastDelegate.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastDelegate.swift @@ -7,13 +7,38 @@ import Foundation +/// BBToast의 생명 주기에 따라 필요한 동작을 정의하세요. +/// +/// - Authors: 김소월 public protocol BBToastDelegate: AnyObject { + /// Toast가 화면에 나타나기 전 호출됩니다. + /// + /// - Note: Optional func willShowToast(_ toast: BBToast) + + /// Toast가 화면에 나타난 후 호출됩니다. + /// + /// - Note: Optional func didShowToast(_ toast: BBToast) + + /// Toast가 화면에 사라진 전 호출됩니다. + /// + /// - Note: Optional func willCloseToast(_ toast: BBToast) + + /// Toast가 화면에 사라진 후 호출됩니다. + /// + /// - Note: Optional func didCloseToast(_ toast: BBToast) + /// Toast의 버튼을 클릭하면 호출됩니다. + /// + /// `setAction(_:)` 메서드로 미리 액션을 전달했다면 해당 메서드를 구현할 필요가 없습니다. 런타임에 동적으로 버튼의 액션이 바뀌어야 한다면 구현하세요. + /// + /// Toast의 버튼을 클릭하면 이 메서드가 먼저 호출되고, 전달한 액션이 실행됩니다. + /// + /// - Note: Optional func didTapToastButton(_ toast: BBToast?, index: Int?, button: BBButton) } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastDirection.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastDirection.swift index 69dc42c6a..9699aca62 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastDirection.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastDirection.swift @@ -9,12 +9,14 @@ import UIKit extension BBToast { + /// Toast가 나타나는 방향을 정의합니다. public enum Direction { case top(yOffset: CGFloat = 0) case bottom(yOffset: CGFloat = 0) case center(xOffset: CGFloat = 0, yOffset: CGFloat = 0) } + /// Toast가 어느 방향으로 스와이프하면 사라지는지 정의합니다. public enum DismissSwipeDirection: Equatable { case toTop case toBottom diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastDismissable.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastDismissable.swift index ae456c42f..901a1c7f9 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastDismissable.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastDismissable.swift @@ -9,6 +9,7 @@ import Foundation extension BBToast { + /// Toast가 사라지는 방법을 정의합니다. public enum Dismissable { case tap case longPress diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastViewConfiguration.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastViewConfiguration.swift index dc9a3284e..03f6e5323 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastViewConfiguration.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastViewConfiguration.swift @@ -11,27 +11,50 @@ public struct BBToastViewConfiguration { // MARK: - Properties + /// BBToast 뷰의 최소 너비입니다. public let minWidth: CGFloat + + /// BBToast 뷰의 최소 높이입니다. public let minHeight: CGFloat + /// BBToast 뷰의 버튼의 최소 너비입니다. public let minButtonWidth: CGFloat + + /// BBToast 뷰의 버턴의 최소 높이입니다. public let minButtonHeight: CGFloat + /// BBToast 뷰의 배경 색상을 설정합니다. + /// + /// - Note: ``BBToastConfiguration``의 `background` 프로퍼티는 Toast 뒤의 배경 색상을 지정합니다. public let backgroundColor: UIColor + /// BBToast 뷰의 타이틀의 라인 수를 설정합니다. public let titleNumberOfLines: Int + + /// BBToast 뷰의 서브 타이틀의 라인 수를 설정합니다. public let subtitleNumberOfLines: Int + /// BBToast 뷰의 둥글기 반경을 설정합니다. public let cornerRadius: CGFloat? // MARK: - Intializer + /// BBToast 뷰의 높이, 너비, 배경 색상과 둥글기 반경을 설정할 수 있습니다. + /// - Parameters: + /// - minWidth: BBToast 뷰의 최소 너비 + /// - minHeight: BBToast 뷰의 최소 높이 + /// - minButtonWidth: BBToast 뷰의 버튼의 최소 너비 + /// - minButtonHeight: BBToast 뷰의 버튼의 최소 높이 + /// - backgroundColor: BBToast 뷰의 배경 색상 + /// - titleNumberOfLines: BBToast 뷰의 타이틀의 라인 수 + /// - subtitleNumberOfLines: BBToast 뷰의 서브 타이틀의 라인 수 + /// - cornerRadius: BBToast 뷰의 둥글기 반경 public init( minWidth: CGFloat = 300, minHeight: CGFloat = 56, - minButtonWidth: CGFloat = 50, - minButtonHeight: CGFloat = 36, + minButtonWidth: CGFloat = 30, + minButtonHeight: CGFloat = 24, backgroundColor: UIColor = .gray900, titleNumberOfLines: Int = 1, subtitleNumberOfLines: Int = 1, diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/ButtonToastView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/ButtonToastView.swift index 5430a569d..201ba1ba8 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/ButtonToastView.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/ButtonToastView.swift @@ -12,18 +12,19 @@ final public class ButtonToastView: UIStackView, BBToastStackView { // MARK: - Views private let button: BBButton = BBButton() + private let container: UIView = UIView() // MARK: - Properties public var toast: BBToast? - public var buttonAction: BBToastAction + public var action: BBToastActionHandler = nil private let viewConfig: BBToastViewConfiguration // MARK: - Intializer public init( - image: UIImage, + image: UIImage? = nil, imageTint: UIColor? = nil, title: String, titleColor: UIColor? = nil, @@ -31,15 +32,16 @@ final public class ButtonToastView: UIStackView, BBToastStackView { buttonTitle: String, buttonTitleFontStlye: BBFontStyle? = nil, buttonTint: UIColor? = nil, + action: BBToastActionHandler = nil, viewConfig: BBToastViewConfiguration ) { - self.buttonAction = nil self.toast = nil + self.action = action self.viewConfig = viewConfig super.init(frame: .zero) commonInit() - + let iconView = IconToastView( image: image, imageTint: imageTint, @@ -53,25 +55,14 @@ final public class ButtonToastView: UIStackView, BBToastStackView { button.setTitle(buttonTitle, for: .normal) button.setTitleFontStyle(buttonTitleFontStlye ?? .body1Regular) button.setTitleColor(buttonTint ?? .gray100, for: .normal) + container.addSubview(button) + + addArrangedSubviews(iconView, container) - let action = UIAction { [weak self] _ in - guard let self = self else { return } - // 델리게이트 실행 - BBToast.multicast.invoke { - $0.didTapToastButton( - self.toast, - index: self.button.id, - button: self.button - ) - } - // 액션 클로저 실행 - buttonAction?(self.toast) - } - - button.addAction(action, for: .touchUpInside) + setupConstraints() + setupAttributes() - addArrangedSubview(iconView) - addArrangedSubview(button) + layoutIfNeeded() } required init(coder: NSCoder) { @@ -82,9 +73,42 @@ final public class ButtonToastView: UIStackView, BBToastStackView { private func commonInit() { axis = .horizontal - spacing = 6 + spacing = 0 alignment = .center distribution = .fillProportionally } + private func setupConstraints() { + button.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + button.leadingAnchor.constraint(equalTo: container.leadingAnchor), + button.trailingAnchor.constraint(equalTo: container.trailingAnchor), + button.topAnchor.constraint(equalTo: container.topAnchor), + button.bottomAnchor.constraint(equalTo: container.bottomAnchor) + ]) + + container.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + container.widthAnchor.constraint(equalToConstant: viewConfig.minButtonWidth), + container.heightAnchor.constraint(equalToConstant: viewConfig.minButtonHeight) + ]) + } + + private func setupAttributes() { + let action = UIAction { [weak self] _ in + guard let self else { return } + // 델리게이트 실행 + BBToast.multicast.invoke { + $0.didTapToastButton( + self.toast, + index: self.button.id, + button: self.button + ) + } + // 액션 클로저 실행 + self.action?(self.toast) + } + button.addAction(action, for: .touchUpInside) + } + } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/DefaultToastView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/DefaultToastView.swift index 1e7299edf..b2624be4d 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/DefaultToastView.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/DefaultToastView.swift @@ -73,7 +73,7 @@ public class DefaultToastView: UIView, BBToastView { centerYAnchor.constraint(equalTo: superview.layoutMarginsGuide.centerYAnchor, constant: yOffset).isActive = true } - setupSubviewConstraints() + setupSubviewConstraints(superview: superview) } private func setupAttributes() { @@ -89,11 +89,15 @@ public class DefaultToastView: UIView, BBToastView { // MARK: - Helpers - private func setupSubviewConstraints() { - child.snp.makeConstraints { - $0.verticalEdges.equalToSuperview().inset(10) - $0.horizontalEdges.equalToSuperview().inset(25) - } + private func setupSubviewConstraints(superview: UIView) { + child.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + child.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 15), + child.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -15), + child.topAnchor.constraint(equalTo: self.topAnchor, constant: 15), + child.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -15) + ]) } private func addShadow() { diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/IconToastView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/IconToastView.swift index 02c312c29..9dda1944c 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/IconToastView.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/IconToastView.swift @@ -13,7 +13,6 @@ public class IconToastView: UIStackView, BBToastStackView { // MARK: - Views - private let vStack = UIStackView() private let imageView = UIImageView() private let titleLabel = BBLabel(textAlignment: .center) @@ -25,7 +24,7 @@ public class IconToastView: UIStackView, BBToastStackView { // MARK: - Intializer public init( - image: UIImage, + image: UIImage? = nil, imageTint: UIColor? = nil, title: String, titleColor: UIColor? = nil, @@ -35,17 +34,20 @@ public class IconToastView: UIStackView, BBToastStackView { super.init(frame: .zero) commonInit() + if let image = image { + imageView.image = image + imageView.tintColor = imageTint + imageView.contentMode = .scaleAspectFit + addArrangedSubview(imageView) + } + titleLabel.text = title titleLabel.textColor = titleColor ?? .bibbiWhite titleLabel.fontStyle = titleFontStyle ?? .body1Regular titleLabel.numberOfLines = viewConfig.titleNumberOfLines - vStack.addArrangedSubview(titleLabel) - - imageView.image = image - imageView.tintColor = imageTint + titleLabel.textAlignment = .left + addArrangedSubview(titleLabel) - addArrangedSubview(imageView) - addArrangedSubview(vStack) } required init(coder: NSCoder) { @@ -54,22 +56,9 @@ public class IconToastView: UIStackView, BBToastStackView { private func commonInit() { axis = .horizontal - spacing = 15 + spacing = 10 alignment = .center - distribution = .fill - - setupAttributes() - } - - - // MARK: - Helpers - - private func setupAttributes() { - vStack.do { - $0.axis = .vertical - $0.spacing = 2 - $0.alignment = .leading - } + distribution = .fillProportionally } } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBServices/BBToastService.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBServices/BBToastService.swift new file mode 100644 index 000000000..7117280ee --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBServices/BBToastService.swift @@ -0,0 +1,184 @@ +// +// BBToastService.swift +// Core +// +// Created by 김건우 on 9/22/24. +// + +import UIKit + +import RxSwift + +// MARK: - ServiceType + +public protocol BBToastServiceType { + func show( + _ title: String, + titleColor: UIColor?, + titleFontStyle: BBFontStyle?, + viewConfig: BBToastViewConfiguration, + config: BBToastConfiguration + ) + + func show( + image: UIImage, + imageTint: UIColor?, + title: String, + titleColor: UIColor?, + titleFontStyle: BBFontStyle?, + viewConfig: BBToastViewConfiguration, + config: BBToastConfiguration + ) + + func show( + image: UIImage?, + imageTint: UIColor?, + title: String, + titleColor: UIColor?, + titleFontStyle: BBFontStyle?, + buttonTitle: String, + buttonTitleFontStyle: BBFontStyle?, + buttonTint: UIColor?, + viewConfig: BBToastViewConfiguration, + config: BBToastConfiguration + ) -> Observable + + func show( + _ style: BBToastStyle, + config: BBToastConfiguration + ) +} + +public extension BBToastServiceType { + + /// 텍스트가 포함된 Toast를 생성합니다. + /// - Parameters: + /// - title: 타이틀 텍스트 + /// - titleColor: 타이틀 색상 + /// - titleFontStyle: 타이틀의 폰트 스타일 + /// - viewConfig: ToastView 설정값 + /// - config: Toast 설정값 + func show( + _ title: String, + titleColor: UIColor? = nil, + titleFontStyle: BBFontStyle? = nil, + viewConfig: BBToastViewConfiguration = BBToastViewConfiguration(), + config: BBToastConfiguration = BBToastConfiguration() + ) { + BBToast.text( + title, + titleColor: titleColor, + titleFontStyle: titleFontStyle, + viewConfig: viewConfig, + config: config + ).show() + } + + /// 이미지와 텍스트가 포함된 Toast를 생성합니다, + /// - Parameters: + /// - image: 이미지 + /// - imageTint: 이미지 강조 색상 + /// - title: 타이틀 텍스트 + /// - titleColor: 타이틀 색상 + /// - titleFontStyle: 타이틀의 폰트 스타일 + /// - viewConfig: ToastView 설정값 + /// - config: Toast 설정값 + func show( + image: UIImage, + imageTint: UIColor? = nil, + title: String, + titleColor: UIColor? = nil, + titleFontStyle: BBFontStyle? = nil, + viewConfig: BBToastViewConfiguration = BBToastViewConfiguration(), + config: BBToastConfiguration = BBToastConfiguration() + ) { + BBToast.default( + image: image, + imageTint: imageTint, + title: title, + titleColor: titleColor, + titleFontStyle: titleFontStyle, + viewConfig: viewConfig, + config: config + ).show() + } + + /// 이미지, 텍스트와 버튼이 포함된 Toast를 생성합니다. + /// - Parameters: + /// - image: 이미지 + /// - imageTint: 이미지 강조 색상 + /// - title: 타이틀 텍스트 + /// - titleColor: 타이틀 색상 + /// - titleFontStyle: 타이틀의 폰트 스타일 + /// - buttonTitle: 버튼 타이틀 텍스트 + /// - buttonTitleFontStyle: 버튼 타이틀의 폰트 스타일 + /// - buttonTint: 버튼 강조 색상 + /// - viewConfig: ToastView 설정값 + /// - config: Toast 설정값 + /// - Returns: Observable + @discardableResult + func show( + image: UIImage? = nil, + imageTint: UIColor? = nil, + title: String, + titleColor: UIColor? = nil, + titleFontStyle: BBFontStyle? = nil, + buttonTitle: String, + buttonTitleFontStyle: BBFontStyle? = nil, + buttonTint: UIColor? = nil, + viewConfig: BBToastViewConfiguration = BBToastViewConfiguration(), + config: BBToastConfiguration = BBToastConfiguration() + ) -> Observable { + + return Observable.create { observer in + let action: BBToastActionHandler = { toast in + observer.onNext(()) + toast?.close() + } + + let toast = BBToast.button( + image: image, + imageTint: imageTint, + title: title, + titleColor: titleColor, + titleFontStyle: titleFontStyle, + buttonTitle: buttonTitle, + buttonTitleFontStyle: buttonTitleFontStyle, + buttonTint: buttonTint, + action: action, + viewConfig: viewConfig, + config: config + ) + + toast.show() + + return Disposables.create { + toast.close() + } + } + + } + + /// 정해진 Style의 Toast를 생성합니다. + /// - Parameters: + /// - style: 스타일 + /// - config: Toast 설정값 + func show( + _ style: BBToastStyle, + config: BBToastConfiguration = BBToastConfiguration() + ) { + BBToast.style( + style, + config: config + ).show() + } + +} + + +// MARK: - BBToastService + +/// BBToast를 조금 더 Rx스럽게 사용하도록 도와주는 서비스입니다. +public class BBToastService: BaseService, BBToastServiceType { } + + diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ServiceProvider.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ServiceProvider.swift index 2bc8695f1..e5f21266a 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ServiceProvider.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ServiceProvider.swift @@ -10,6 +10,7 @@ import Foundation public protocol ServiceProviderProtocol: AnyObject { var bbAlertService: BBAlertServiceType { get } + var bbToastService: BBToastServiceType { get } var mainService: MainServiceType { get } var managementService: ManagementServiceType { get } @@ -26,6 +27,7 @@ public protocol ServiceProviderProtocol: AnyObject { final public class ServiceProvider: ServiceProviderProtocol { public lazy var bbAlertService: any BBAlertServiceType = BBAlertService(provider: self) + public lazy var bbToastService: any BBToastServiceType = BBToastService(provider: self) public lazy var mainService: MainServiceType = MainService(provider: self) public lazy var managementService: any ManagementServiceType = ManagementService(provider: self) diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ToastMessageGlobalState.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ToastMessageGlobalState.swift index f5ff37497..d65034178 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ToastMessageGlobalState.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ToastMessageGlobalState.swift @@ -9,6 +9,7 @@ import Foundation import RxSwift +@available(*, deprecated, renamed: "BBToastService") public enum ToastMessageEvent { case showAllFamilyUploadedToastView(Bool) } @@ -24,6 +25,7 @@ public protocol ToastMessageGlobalStateType { func clearLastSelectedDate() } +@available(*, deprecated, renamed: "BBToastService") final public class ToastMessageGlobalState: BaseService, ToastMessageGlobalStateType { public var lastSelectedDate: Date = .distantFuture public var event: BehaviorSubject = BehaviorSubject(value: .showAllFamilyUploadedToastView(false)) From 0e94bcd53d6b143a3d9191cae042d4cc0459489e Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Thu, 26 Sep 2024 16:50:01 +0900 Subject: [PATCH 219/263] =?UTF-8?q?fix:=20=EB=8C=93=EA=B8=80=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=EB=91=A5=EA=B8=80=EA=B8=B0=20=EB=B0=98?= =?UTF-8?q?=EA=B2=BD=20=EC=88=98=EC=A0=95=20=EB=93=B1=20(#656)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 댓글이미지radius수정 * fix: 캘린더화면 스크롤 문제 수정 * fix: 작성자 이름이 짤리는 문제 수정 * fix: 작성자가 알수없음이라 뜨는 문제 수정 * fix: 스플래시에서 가족멤버 불러오기 구현 * delete: Splash에서 가족멤버 불러오는 코드 삭제 --- .../Reactor/CalendarPostCellReactor.swift | 6 ++-- .../Reactor/MonthlyCalendarViewReactor.swift | 16 ++++++++- .../Calendar/View/Cell/CalendarPostCell.swift | 2 +- .../MonthlyCalendarViewController.swift | 34 +++++++++---------- .../Comment/View/Cell/CommentCell.swift | 1 + 5 files changed, 38 insertions(+), 21 deletions(-) diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPostCellReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPostCellReactor.swift index 22e1be498..04c815277 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPostCellReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPostCellReactor.swift @@ -39,6 +39,8 @@ public final class CalendarPostCellReactor: Reactor { // MARK: - Properties public var initialState: State + @Injected var fetchUserNameUseCase: FetchUserNameUseCaseProtocol + @Injected var meUseCase: MemberUseCaseProtocol @Injected var provider: ServiceProviderProtocol @@ -72,9 +74,9 @@ public final class CalendarPostCellReactor: Reactor { case .requestAuthorName: let authorId = initialState.post.authorId - let authorName = meUseCase.executeFetchUserName(memberId: authorId) + let authorName = fetchUserNameUseCase.execute(memberId: authorId) ?? "알 수 없음" return Observable.just(.setAuthorName(authorName)) - + case .requestAuthorImageUrl: let authorId = initialState.post.authorId let authorImageUrl = meUseCase.executeProfileImageUrlString(memberId: authorId) diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/MonthlyCalendarViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/MonthlyCalendarViewReactor.swift index e27bfd078..43c893eb6 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/MonthlyCalendarViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/MonthlyCalendarViewReactor.swift @@ -26,6 +26,8 @@ public final class MonthlyCalendarViewReactor: Reactor { case pushDailyCalendarViewController(Date) case setInfoPopover(UIView) case setCalendarItems([String]) + // TODO: - 싹다 코드 리팩토링하기 + case setCalendarPageIndexPath(IndexPath) } // MARK: - State @@ -34,6 +36,8 @@ public final class MonthlyCalendarViewReactor: Reactor { @Pulse var shouldPushDailyCalendarViewController: Date? @Pulse var shouldPresnetInfoPopover: UIView? @Pulse var displayCalendar: [MonthlyCalendarSectionModel] + + var initalCalendarPageIndexPath: IndexPath? = nil } @@ -80,7 +84,12 @@ public final class MonthlyCalendarViewReactor: Reactor { return Observable.just(.popViewController) case let .addCalendarItems(items): - return Observable.just(.setCalendarItems(items)) + let indexPath = IndexPath(item: items.count-1, section: 0) + + return Observable.concat( + Observable.just(.setCalendarItems(items)), + Observable.just(.setCalendarPageIndexPath(indexPath)) + ) } } @@ -104,7 +113,12 @@ public final class MonthlyCalendarViewReactor: Reactor { items: items ) newState.displayCalendar = [newDatasource] + + case let .setCalendarPageIndexPath(indexPath): + newState.initalCalendarPageIndexPath = indexPath } + + return newState } } diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarPostCell.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarPostCell.swift index f6b22c684..d32c4d8b8 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarPostCell.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarPostCell.swift @@ -110,7 +110,7 @@ final class CalendarPostCell: BaseCollectionViewCell { reactor.state.compactMap { $0.authorName } .distinctUntilChanged() - .bind(with: self) { $0.authorNameLabel.text = $1[0] } + .bind(to: authorFirstNameLabel.rx.firstLetterText) .disposed(by: disposeBag) reactor.state.compactMap { $0.authorImageUrl } diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/MonthlyCalendarViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/MonthlyCalendarViewController.swift index d96c76bd1..bf426b69c 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/MonthlyCalendarViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/MonthlyCalendarViewController.swift @@ -16,12 +16,8 @@ import RxDataSources import SnapKit import Then -import MacrosInterface - -@Codable -struct MemberDTO { - var name: String -} +// 지금 당장 BBNavigationViewController로 바꿔도 안됨 +// 왜냐하면, PopoverViewController Delegate 문제가 발생하기 때문! fileprivate typealias _Str = CalendarStrings public final class MonthlyCalendarViewController: TempNavigationViewController { @@ -52,7 +48,7 @@ public final class MonthlyCalendarViewController: TempNavigationViewController RxCollectionViewSectionedReloadDataSource { return RxCollectionViewSectionedReloadDataSource { datasource, collectionView, indexPath, yearMonth in let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CalendarCell.id, for: indexPath) as! CalendarCell - cell.reactor = CalendarCellDIContainer( - yearMonth: yearMonth - ).makeReactor() + cell.reactor = CalendarCellReactor(yearMonth: yearMonth) return cell } } @@ -190,12 +191,11 @@ extension MonthlyCalendarViewController { ) } - private func scrollToLastIndexPath() { + private func scrollToLastIndexPath(_ indexPath: IndexPath) { calendarCollectionView.layoutIfNeeded() - let indexPath: IndexPath = IndexPath( - item: dataSource[0].items.count - 1, - section: 0 - ) + + print("======= \(dataSource[0].items.count - 1)") + calendarCollectionView.scrollToItem( at: indexPath, at: .centeredHorizontally, diff --git a/14th-team5-iOS/App/Sources/Presentation/Comment/View/Cell/CommentCell.swift b/14th-team5-iOS/App/Sources/Presentation/Comment/View/Cell/CommentCell.swift index cea16f36c..ebd4d01e4 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Comment/View/Cell/CommentCell.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Comment/View/Cell/CommentCell.swift @@ -160,6 +160,7 @@ final public class CommentCell: BaseTableViewCell { } profileImage.do { + $0.layer.masksToBounds = true $0.layer.cornerRadius = 44 / 2 $0.contentMode = .scaleAspectFill } From 84a5665056cd32040046a7a66dfe8fcab4c84b66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=84?= Date: Fri, 27 Sep 2024 18:15:00 +0900 Subject: [PATCH 220/263] =?UTF-8?q?feat:=20=EC=B9=B4=EB=A9=94=EB=9D=BC=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20BBToast=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#661)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DIContainer/NavigatorDIContainer.swift | 6 ++ .../Navigator/CameraDisplayNavigator.swift | 32 +++++++++ .../Camera/CameraDisplayViewController.swift | 69 +++++++------------ .../Reactor/CameraDisplayViewReactor.swift | 47 ++++++++++++- .../BBToast/BBToastConfiguration.swift | 4 +- 5 files changed, 109 insertions(+), 49 deletions(-) create mode 100644 14th-team5-iOS/App/Sources/Application/Navigator/CameraDisplayNavigator.swift diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/NavigatorDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/NavigatorDIContainer.swift index 71427a118..455e810bc 100644 --- a/14th-team5-iOS/App/Sources/Application/DIContainer/NavigatorDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/NavigatorDIContainer.swift @@ -77,6 +77,12 @@ final class NavigatorDIContainer: BaseContainer { ) } + container.register(type: CameraDisplayNavigatorProtocol.self) { _ in + CameraDisplayNavigator( + navigationController: makeUINavigationController() + ) + } + container.register(type: ManagementNavigatorProtocol.self) { _ in ManagementNavigator( navigationController: makeUINavigationController() diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/CameraDisplayNavigator.swift b/14th-team5-iOS/App/Sources/Application/Navigator/CameraDisplayNavigator.swift new file mode 100644 index 000000000..292603cbc --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/Navigator/CameraDisplayNavigator.swift @@ -0,0 +1,32 @@ +// +// CameraDisplayNavigator.swift +// App +// +// Created by Kim dohyun on 9/26/24. +// + +import Core +import DesignSystem +import UIKit + +protocol CameraDisplayNavigatorProtocol: BaseNavigator { + func toHome() +} + +final class CameraDisplayNavigator: CameraDisplayNavigatorProtocol { + + //MARK: - Properties + var navigationController: UINavigationController + + + //MARK: - Intializer + init(navigationController: UINavigationController) { + self.navigationController = navigationController + } + + //MARK: - Configure + func toHome() { + let vc = MainViewControllerWrapper().viewController + navigationController.setViewControllers([vc], animated: true) + } +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Camera/CameraDisplayViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Camera/CameraDisplayViewController.swift index 1f87b607c..9d2e26e59 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Camera/CameraDisplayViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Camera/CameraDisplayViewController.swift @@ -200,7 +200,7 @@ public final class CameraDisplayViewController: BaseViewController 8) } - .debounce(.milliseconds(300), scheduler: MainScheduler.instance) - .withUnretained(self) - .bind { owner, isShow in - guard isShow == true else { return } - owner.makeBibbiToastView(text: "8자까지 입력가능해요", image: DesignSystemAsset.warning.image, offset: 400) - }.disposed(by: disposeBag) + .filter{ $0.count > 8 } + .debounce(RxInterval._600milliseconds, scheduler: RxScheduler.main) + .map { _ in Reactor.Action.showInputTextError } + .bind(to: reactor.action) + .disposed(by: disposeBag) displayEditTextField.rx .text.orEmpty + .filter { !$0.contains(" ")} + .debounce(RxInterval._600milliseconds, scheduler: RxScheduler.main) .scan("") { previous, new -> String in if new.count > 8 { return previous @@ -290,7 +293,7 @@ public final class CameraDisplayViewController: BaseViewController Observable in + .withUnretained(self) + .flatMap { owner, entity -> Observable in if entity == nil { return .just(.setError(true)) } else { + owner.cameraDisplayNavigator.toHome() return .concat( .just(.setLoading(false)), .just(.setPostEntity(entity)), .just(.setLoading(true)), .just(.setError(false)), - self.provider.mainService.refreshMain() + owner.provider.mainService.refreshMain() .flatMap { _ in Observable.empty() } ) } @@ -176,6 +193,28 @@ public final class CameraDisplayViewReactor: Reactor { .just(.setDescription("")), .just(.setDisplayEditSection([])) ) + case .showInputTextError: + let config = BBToastConfiguration(direction: .bottom(yOffset: -360), animationTime: 1.0) + let viewConfig = BBToastViewConfiguration(minWidth: 207) + provider.bbToastService.show( + image: DesignSystemAsset.warning.image, + title: "8자까지 입력 가능해요", + viewConfig: viewConfig, + config: config + ) + return .empty() + + case let .showInputBlankTextError(displayText): + let config = BBToastConfiguration(direction: .bottom(yOffset: -360), animationTime: 1.0) + let viewConfig = BBToastViewConfiguration(minWidth: 207) + provider.bbToastService.show( + image: DesignSystemAsset.warning.image, + title: "띄어쓰기는 할 수 없어요", + viewConfig: viewConfig, + config: config + ) + let generateText = displayText.trimmingCharacters(in: .whitespaces) + return .just(.setTrimedText(generateText)) } } @@ -202,6 +241,8 @@ public final class CameraDisplayViewReactor: Reactor { newState.displayPostEntity = entity case let .setError(isError): newState.isError = isError + case let .setTrimedText(displayText): + newState.displayText = displayText } return newState } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastConfiguration.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastConfiguration.swift index d58310b72..0c33cc656 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastConfiguration.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastConfiguration.swift @@ -84,12 +84,12 @@ private extension BBToastConfiguration { switch direction { case let .top(yOffset): return .custom( - transformation: CGAffineTransform(scaleX: 0.9, y: 0.9).translatedBy(x: 0, y: -yOffset - 100) + transformation: CGAffineTransform(scaleX: 0.9, y: 0.9).translatedBy(x: 0, y: -yOffset - 300) ) case let .bottom(yOffset): return .custom( - transformation: CGAffineTransform(scaleX: 0.9, y: 0.9).translatedBy(x: 0, y: +yOffset + 100) + transformation: CGAffineTransform(scaleX: 0.9, y: 0.9).translatedBy(x: 0, y: -yOffset + 300) ) case .center: From d50a9072fb9ed7dfba762a4da8df88cc2fef2696 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=84?= Date: Fri, 27 Sep 2024 18:15:28 +0900 Subject: [PATCH 221/263] =?UTF-8?q?feat:=20Tuist=20CFBundleShortVersionStr?= =?UTF-8?q?ing=201.2.2=20=EB=B3=80=EA=B2=BD=20(#652)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 14th-team5-iOS/App/Project.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/14th-team5-iOS/App/Project.swift b/14th-team5-iOS/App/Project.swift index 8a32b42f4..32d643c8f 100644 --- a/14th-team5-iOS/App/Project.swift +++ b/14th-team5-iOS/App/Project.swift @@ -19,7 +19,7 @@ private let targets: [Target] = [ "CFBundleDisplayName": .string("Bibbi"), "CFBundleVersion": .string("1"), "CFBuildVersion": .string("0"), - "CFBundleShortVersionString": .string("1.2"), + "CFBundleShortVersionString": .string("1.2.2"), "UILaunchStoryboardName": .string("LaunchScreen.storyboard"), "UISupportedInterfaceOrientations": .array([.string("UIInterfaceOrientationPortrait")]), "UIUserInterfaceStyle": .string("Dark"), From 29cbd500276c47e236ce7844efba014a27a7b92b Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Fri, 27 Sep 2024 20:48:57 +0900 Subject: [PATCH 222/263] =?UTF-8?q?fix:=20Pick=20=EC=95=A1=EC=85=98=20?= =?UTF-8?q?=ED=9B=84=20Toast=EA=B0=80=20=EC=B6=9C=EB=A0=A5=EB=90=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#660)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Comment/Reactor/CommentViewReactor.swift | 4 ++-- .../CommentViewController.swift | 17 +++++++++------- .../Reactor/Cell/MainFamilyCellReactor.swift | 15 +++++++++++--- .../BBToast/BBToastConfiguration.swift | 2 +- .../BBCommons/BBToast/BBToastDirection.swift | 20 +++++++++++++++++++ .../DefaultToastView/ButtonToastView.swift | 2 +- .../DefaultToastView/DefaultToastView.swift | 4 ++-- .../DefaultToastView/IconToastView.swift | 2 +- 8 files changed, 49 insertions(+), 17 deletions(-) diff --git a/14th-team5-iOS/App/Sources/Presentation/Comment/Reactor/CommentViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Comment/Reactor/CommentViewReactor.swift index 95a1d39ce..221c7b27f 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Comment/Reactor/CommentViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Comment/Reactor/CommentViewReactor.swift @@ -47,7 +47,7 @@ final public class CommentViewReactor: Reactor { // MARK: - State public struct State { - @Pulse var commentDatasource: [CommentSectionModel] = [.init(model: "", items: [])] + @Pulse var commentDatasource: [CommentSectionModel] = [] var hiddenTableProgressHud: Bool = false var hiddenFetchFailureView: Bool = true @@ -233,7 +233,7 @@ final public class CommentViewReactor: Reactor { switch mutation { case let .setComments(comments): - let dataSource: CommentSectionModel = .init(model: "", items: comments) + let dataSource = CommentSectionModel(model: "", items: comments) newState.commentDatasource = [dataSource] case let .appendComment(comment): 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 ca79708cf..03852e043 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Comment/ViewController/CommentViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Comment/ViewController/CommentViewController.swift @@ -65,6 +65,7 @@ final public class CommentViewController: ReactorViewController RxDataSource { - return RxDataSource { dataSource, tableView, indexPath, reactor in + let dataSource = RxDataSource { dataSource, tableView, indexPath, reactor in let cell = tableView.dequeueReusableCell( withIdentifier: CommentCell.id ) as! CommentCell 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 } } diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/Cell/MainFamilyCellReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/Cell/MainFamilyCellReactor.swift index 55b655fbf..ddcc82a84 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/Cell/MainFamilyCellReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/Cell/MainFamilyCellReactor.swift @@ -7,7 +7,7 @@ import Core -import Foundation +import UIKit import DesignSystem import Domain @@ -127,11 +127,20 @@ final class MainFamilyCellReactor: Reactor { switch $0.1 { case .pick: return $0.0.pickMemberUseCase.execute(memberId: memberId) + .withUnretained(self) .flatMap { guard - let entity = $0, entity.success - else { return Observable.empty() } + let entity = $0.1, entity.success + else { + $0.0.provider.bbToastService.show(.error) + return Observable.empty() + } + $0.0.provider.bbToastService.show( + image: DesignSystemAsset.yellowPaperPlane.image, + imageTint: UIColor.mainYellow, + title: "\(memberName)님에게 생존신고 알림을 보냈어요" + ) return Observable.just(.setHiddenPickButton(true)) } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastConfiguration.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastConfiguration.swift index 0c33cc656..816aa3ac8 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastConfiguration.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastConfiguration.swift @@ -56,7 +56,7 @@ public struct BBToastConfiguration { public init( direction: BBToast.Direction = .bottom(yOffset: 0), dismissables: [BBToast.Dismissable] = [.time(time: 2.5), .swipe(direction: .natural)], - animationTime: TimeInterval = 0.6, + animationTime: TimeInterval = 0.8, enteringAnimation: BBToast.Animation = .default, exitingAnimation: BBToast.Animation = .default, attachTo view: UIView? = nil, diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastDirection.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastDirection.swift index 9699aca62..045616c10 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastDirection.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/BBToastDirection.swift @@ -11,15 +11,35 @@ extension BBToast { /// Toast가 나타나는 방향을 정의합니다. public enum Direction { + + /// Toast가 위에서 아래로 나타나게 합니다. + /// + /// yOffset 연관값은 Toast가 화면 상단으로부터 얼마나 멀리 떨어져 있는지 의미합니다. 예를 들어, 400값을 준다면 상단으로부터 400만큼 떨어져서 Toast가 내려옵니다. + /// + /// 애니메이션 속도가 너무 빠르다면 애니메이션 시간을 조정하세요. case top(yOffset: CGFloat = 0) + + /// Toast가 아래에서 위로 나타나게 합니다. + /// + /// yOffset 연관값은 Toast가 화면 하단으로부터 얼마나 멀리 떨어져 있는지 의미합니다. 예를 들어, -400값을 준다면 하단으로부터 -400만큼 떨어져서 Toast가 올라옵니다. + /// + /// 애니메이션 속도가 너무 빠르다면 애니메이션 시간을 조정하세요. case bottom(yOffset: CGFloat = 0) + + /// Toast가 중앙에 나타나게 합니다. case center(xOffset: CGFloat = 0, yOffset: CGFloat = 0) } /// Toast가 어느 방향으로 스와이프하면 사라지는지 정의합니다. public enum DismissSwipeDirection: Equatable { + + /// 위로 스와이프해야 Toast가 사라지게 합니다. case toTop + + /// 아래로 스와이프해야 Toast가 사라지게 합니다. case toBottom + + /// Toast가 나타나는 방향에 따라 스와이프 방향을 자동으로 설정하게 합니다. case natural func shouldApply(_ delta: CGFloat, direction: Direction) -> Bool { diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/ButtonToastView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/ButtonToastView.swift index 201ba1ba8..1343d2ba2 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/ButtonToastView.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/ButtonToastView.swift @@ -73,7 +73,7 @@ final public class ButtonToastView: UIStackView, BBToastStackView { private func commonInit() { axis = .horizontal - spacing = 0 + spacing = 5 alignment = .center distribution = .fillProportionally } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/DefaultToastView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/DefaultToastView.swift index b2624be4d..5814a2611 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/DefaultToastView.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/DefaultToastView.swift @@ -93,8 +93,8 @@ public class DefaultToastView: UIView, BBToastView { child.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - child.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 15), - child.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -15), + child.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 20), + child.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -20), child.topAnchor.constraint(equalTo: self.topAnchor, constant: 15), child.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -15) ]) diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/IconToastView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/IconToastView.swift index 9dda1944c..ae785a635 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/IconToastView.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToast/ToastViews/DefaultToastView/IconToastView.swift @@ -56,7 +56,7 @@ public class IconToastView: UIStackView, BBToastStackView { private func commonInit() { axis = .horizontal - spacing = 10 + spacing = 5 alignment = .center distribution = .fillProportionally } From 075d9f1adbeb8f3672efd935f1daf0a68a545087 Mon Sep 17 00:00:00 2001 From: g_mi <62610032+akrudal@users.noreply.github.com> Date: Sun, 29 Sep 2024 17:58:03 +0900 Subject: [PATCH 223/263] =?UTF-8?q?=EC=95=8C=20=EC=88=98=20=EC=97=86?= =?UTF-8?q?=EC=9D=8C=20=EC=98=A4=EB=A5=98=EC=99=80=20=EC=98=A8=EB=B3=B4?= =?UTF-8?q?=EB=94=A9=EC=97=90=EC=84=9C=20=ED=99=94=EB=A9=B4=EC=9D=B4=20?= =?UTF-8?q?=EB=84=98=EC=96=B4=EA=B0=80=EC=A7=80=20=EC=95=8A=EB=8A=94=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=EB=A5=BC=20=EC=88=98=EC=A0=95=ED=95=B4?= =?UTF-8?q?=EC=9A=94=20(#663)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DIContainer/PostDIContainer.swift | 7 +- .../Reactor/CalendarPostCellReactor.swift | 1 + .../Home/Reactor/MainPostViewReactor.swift | 7 +- .../ViewControllers/MainViewController.swift | 2 - .../Views/MainPostCollectionViewCell.swift | 2 +- .../OnBoarding/OnBoardingReactor.swift | 31 +- .../Reactor/PostDetailViewReactor.swift | 2 +- .../ReactionViewController.swift | 2 +- .../Views/PostDetailCollectionViewCell.swift | 19 +- .../PostDetail/Views/PostNavigationView.swift | 37 ++- .../Reactor/ProfileFeedViewReactor.swift | 6 +- .../Trash/Manager/DeepLinkManager.swift | 14 +- .../Sources/Extensions/Reactive+Ext.swift | 5 + .../Family/FamilyAPI/FamilyAPIWorker.swift | 2 +- .../Family/Repository/FamilyRepository.swift | 16 +- .../Repositories/MembersRepository.swift | 6 +- .../FetchReactionResponseDTO.swift | 6 +- .../FetchRealEmojiListResponseDTO.swift | 3 +- .../Repository/AccountResignRepository.swift | 5 +- .../FamilyUserDefaults.swift | 16 +- .../AccountRepository/AccountRepository.swift | 2 - .../Camera/Repository/CameraRepository.swift | 3 +- .../Member/Repository/MemberRepository.swift | 20 +- .../DataMapping/PostListResponseDTO.swift | 3 +- .../UserDefaults/FamilyUserDefautls.swift | 266 +++++++++--------- .../Family/FamilyMemberProfileEntity.swift | 4 +- .../Entities/PostList/PostEntity.swift | 18 +- .../Repositories/FamilyRepository.swift | 11 +- .../UseCases/Post/FetchPostListUseCase.swift | 74 ++++- 29 files changed, 353 insertions(+), 237 deletions(-) diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/PostDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/PostDIContainer.swift index 1198cc9c1..baa4bb71c 100644 --- a/14th-team5-iOS/App/Sources/Application/DIContainer/PostDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/PostDIContainer.swift @@ -11,14 +11,15 @@ import Domain final class PostDIContainer: BaseContainer { - private let repository: PostListRepositoryProtocol = PostRepository() + private let familyRepository: FamilyRepositoryProtocol = FamilyRepository() + private let postListRepository: PostListRepositoryProtocol = PostRepository() private func makePostUseCase() -> FetchPostListUseCaseProtocol { - return FetchPostListUseCase(postListRepository: repository) + return FetchPostListUseCase(postListRepository: postListRepository, familyRepository: familyRepository) } private func makeFetchMembersPostListUseCase() -> FetchMembersPostListUseCaseProtocol { - return FetchMembersPostListUseCase(postListRepository: repository) + return FetchMembersPostListUseCase(postListRepository: postListRepository) } } diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPostCellReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPostCellReactor.swift index 04c815277..bd03736d9 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPostCellReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPostCellReactor.swift @@ -74,6 +74,7 @@ public final class CalendarPostCellReactor: Reactor { case .requestAuthorName: let authorId = initialState.post.authorId + let authorName = fetchUserNameUseCase.execute(memberId: authorId) ?? "알 수 없음" return Observable.just(.setAuthorName(authorName)) diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainPostViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainPostViewReactor.swift index 8197ed100..eb9334abc 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainPostViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainPostViewReactor.swift @@ -57,14 +57,14 @@ extension MainPostViewReactor { .asObservable() .flatMap { (postList) -> Observable in guard let postList = postList, - !postList.postLists.isEmpty else { + !postList.isEmpty else { return Observable.from([ Mutation.setNoPostTodayView(true), Mutation.updateRefreshEnd(true), ]) } - - let postSectionItem = postList.postLists.map(PostSection.Item.main) + + let postSectionItem = postList.map(PostSection.Item.main) let mutations = [ Mutation.updatePostDataSource(postSectionItem), Mutation.setNoPostTodayView(false), @@ -83,7 +83,6 @@ extension MainPostViewReactor { case .updatePostDataSource(let postSectionItem): newState.isRefreshEnd = true newState.postSection.items = postSectionItem - App.Repository.member.postId.accept(UserDefaults.standard.postId) case .setNoPostTodayView(let isShow): newState.isShowingNoPostTodayView = isShow case .updateRefreshEnd(let status): 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 a87cece7e..1ee4c2b1d 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift @@ -30,8 +30,6 @@ final class MainViewController: BBNavigationViewController, UIC override func viewDidLoad() { super.viewDidLoad() - - UserDefaults.standard.inviteCode = nil } override func bind(reactor: MainViewReactor) { diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Views/MainPostCollectionViewCell.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Views/MainPostCollectionViewCell.swift index cf3f76fd3..a25ae7924 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Views/MainPostCollectionViewCell.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Views/MainPostCollectionViewCell.swift @@ -101,7 +101,7 @@ extension MainPostCollectionViewCell { } missionBadge.isHidden = data.missionId == nil ? true : false - nameLabel.text = data.author?.name ?? "알 수 없음" + nameLabel.text = data.author.name timeLabel.text = data.time.toDate(with: "yyyy-MM-dd'T'HH:mm:ssZ").relativeFormatter() } diff --git a/14th-team5-iOS/App/Sources/Presentation/OnBoarding/OnBoardingReactor.swift b/14th-team5-iOS/App/Sources/Presentation/OnBoarding/OnBoardingReactor.swift index f6f5cb6c3..05da4dc43 100644 --- a/14th-team5-iOS/App/Sources/Presentation/OnBoarding/OnBoardingReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/OnBoarding/OnBoardingReactor.swift @@ -40,24 +40,21 @@ extension OnBoardingReactor { public func mutate(action: Action) -> Observable { switch action { case .permissionTapped: - return Observable.zip( - Observable.create { observer in - MPEvent.Account.invitedGroupFinished.track(with: nil) - UNUserNotificationCenter.current().requestAuthorization( - options: [.alert, .badge, .sound], - completionHandler: { granted, error in - if granted { - MPEvent.Account.allowNotification.track(with: nil) - } - observer.onNext(granted) - observer.onCompleted() + Observable.create { observer in + MPEvent.Account.invitedGroupFinished.track(with: nil) + UNUserNotificationCenter.current().requestAuthorization( + options: [.alert, .badge, .sound], + completionHandler: { granted, error in + if granted { + MPEvent.Account.allowNotification.track(with: nil) } - ) - return Disposables.create() - }, - familyUseCase.executeFetchPaginationFamilyMembers(query: .init()) - ) - .flatMap { [weak self] (granted: Bool, _) -> Observable in + observer.onNext(granted) + observer.onCompleted() + } + ) + return Disposables.create() + } + .flatMap { [weak self] (granted: Bool) -> Observable in if granted { return Observable.just(.permissionTapped) } else { diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostDetailViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostDetailViewReactor.swift index 68f86d1ee..a362f37da 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostDetailViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostDetailViewReactor.swift @@ -65,7 +65,7 @@ extension PostDetailViewReactor { func mutate(action: Action) -> Observable { switch action { case .didTapProfileImageView: - let memberId = initialState.post.author?.memberId ?? .none + let memberId = initialState.post.author.memberId if memberUseCase.executeCheckIsValidMember(memberId: memberId) { provider.postGlobalState.pushProfileViewController(memberId) } diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/ReactionViewController.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/ReactionViewController.swift index c274236d8..4789bf875 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/ReactionViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/ReactionViewController.swift @@ -15,7 +15,7 @@ import RxCocoa import RxDataSources final class ReactionViewController: BaseViewController, UICollectionViewDelegateFlowLayout { - let postListData: BehaviorRelay = BehaviorRelay(value: .init(postId: "", author: nil, commentCount: 0, emojiCount: 0, imageURL: "", content: nil, time: "")) + let postListData: BehaviorRelay = BehaviorRelay(value: PostEntity.empty) private let selectedReactionSubject: PublishSubject = PublishSubject() diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/PostDetailCollectionViewCell.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/PostDetailCollectionViewCell.swift index 2e8d9c1b8..1fedabcb5 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/PostDetailCollectionViewCell.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/PostDetailCollectionViewCell.swift @@ -34,7 +34,7 @@ final class PostDetailCollectionViewCell: BaseCollectionViewCell { @@ -25,7 +27,7 @@ final class PostNavigationView: BaseView { self.init(frame: .zero) self.reactor = reactor } - + override init(frame: CGRect) { super.init(frame: frame) } @@ -42,9 +44,9 @@ final class PostNavigationView: BaseView { reactor.state .map { $0.selectedPost } - .debug("selectedPost") .asObservable() .withUnretained(self) + .observe(on: MainScheduler.instance) .bind(onNext: { $0.0.setData(data: $0.1) }) .disposed(by: disposeBag) } @@ -92,6 +94,7 @@ final class PostNavigationView: BaseView { } profileImageView.do { + $0.isHidden = true $0.clipsToBounds = true $0.contentMode = .scaleAspectFill $0.layer.cornerRadius = Layout.ProfileImageView.cornerRadius @@ -101,25 +104,21 @@ final class PostNavigationView: BaseView { extension PostNavigationView { private func setData(data: PostEntity) { - if let author = data.author, - let profileImageURL = author.profileImageURL, - let url = URL(string: profileImageURL), !profileImageURL.isEmpty { - profileImageView.kf.setImage(with: url) - defaultNameLabel.isHidden = true - } else { - if let author = data.author, - let first = author.name.first { - defaultNameLabel.text = "\(first)" + if let profileImageURL = data.author.profileImageURL, + let url = URL(string: profileImageURL), + !profileImageURL.isEmpty { + profileImageView.kf.setImage(with: url) + defaultNameLabel.isHidden = true } else { - defaultNameLabel.text = "알" + defaultNameLabel.text = "\(data.author.name.first ?? "알")" + profileImageView.kf.base.image = nil + defaultNameLabel.isHidden = false + profileImageView.backgroundColor = .gray800 } - - profileImageView.kf.base.image = nil - defaultNameLabel.isHidden = false - profileImageView.backgroundColor = .gray800 - } - - nameLabel.text = data.author?.name ?? "알 수 없음" + + nameLabel.text = data.author.name dateLabel.text = data.time.toDate(with: "yyyy-MM-dd'T'HH:mm:ssZ").relativeFormatter() + + profileImageView.isHidden = false } } diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileFeedViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileFeedViewReactor.swift index 3124e9c66..f16a6bba2 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileFeedViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileFeedViewReactor.swift @@ -15,7 +15,7 @@ import ReactorKit final class ProfileFeedViewReactor: Reactor { var initialState: State - @Injected private var feedUseCase: FetchMembersPostListUseCaseProtocol + @Injected private var feedUseCase: FetchPostListUseCaseProtocol @Injected private var provider: ServiceProviderProtocol enum Action { case reloadFeedItems @@ -146,8 +146,8 @@ final class ProfileFeedViewReactor: Reactor { missionType: $0.missionType, author: FamilyMemberProfileEntity( memberId: currentState.memberId, - profileImageURL: $0.author?.profileImageURL, - name: $0.author?.name ?? ""), + profileImageURL: $0.author.profileImageURL, + name: $0.author.name), commentCount: $0.commentCount, emojiCount: $0.emojiCount, imageURL: $0.imageURL, diff --git a/14th-team5-iOS/App/Sources/Presentation/Trash/Manager/DeepLinkManager.swift b/14th-team5-iOS/App/Sources/Presentation/Trash/Manager/DeepLinkManager.swift index 798daa9e6..f4251764e 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Trash/Manager/DeepLinkManager.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Trash/Manager/DeepLinkManager.swift @@ -32,14 +32,16 @@ final class DeepLinkManager { // 이번 3차 끝나고, postdetailviewcontroller에서 post 불러오는 형태로 바꿔보겠습니다. let disposeBag: DisposeBag = DisposeBag() let postRepository: PostListRepositoryProtocol = PostRepository() - lazy var postUseCase: FetchPostListUseCaseProtocol = FetchPostListUseCase(postListRepository: postRepository) + let familyRepository: FamilyRepositoryProtocol = FamilyRepository() + lazy var postUseCase: FetchPostListUseCaseProtocol = FetchPostListUseCase( + postListRepository: postRepository, familyRepository: familyRepository) private init() {} private func todayDeepLink(type: PostType, data: NotificationDeepLink) { fetchTodayPost(type: type) { result in guard let result = result else { return } - let items = result.postLists.map(PostSection.Item.main) + let items = result.map(PostSection.Item.main) items.enumerated().forEach { (index, item) in switch item { @@ -56,7 +58,7 @@ final class DeepLinkManager { private func todayCommentDeepLink(type: PostType, data: NotificationDeepLink) { fetchTodayPost(type: type) { result in guard let result = result else { return } - let items = result.postLists.map(PostSection.Item.main) + let items = result.map(PostSection.Item.main) items.enumerated().forEach { (index, item) in switch item { @@ -71,12 +73,12 @@ final class DeepLinkManager { } - private func fetchTodayPost(type: PostType, completion: @escaping (PostListPageEntity?) -> Void) { + private func fetchTodayPost(type: PostType, completion: @escaping ([PostEntity]?) -> Void) { let dateString = Date().toFormatString(with: "yyyy-MM-dd") let query = PostListQuery(date: dateString, type: type) postUseCase.execute(query: query) - .subscribe(onSuccess: { result in + .subscribe(onNext: { result in completion(result) }) .disposed(by: disposeBag) @@ -112,7 +114,7 @@ extension DeepLinkManager { func handleWidgetDeepLink(data: WidgetDeepLink) { fetchTodayPost(type: .survival) { result in guard let result = result else { return } - let items = result.postLists.map(PostSection.Item.main) + let items = result.map(PostSection.Item.main) items.enumerated().forEach { (index, item) in switch item { diff --git a/14th-team5-iOS/Core/Sources/Extensions/Reactive+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/Reactive+Ext.swift index 873c63e19..5b8747331 100644 --- a/14th-team5-iOS/Core/Sources/Extensions/Reactive+Ext.swift +++ b/14th-team5-iOS/Core/Sources/Extensions/Reactive+Ext.swift @@ -13,6 +13,11 @@ import RxCocoa import RxSwift extension Reactive where Base: UIViewController { + public var viewDidLoad: ControlEvent { + let event = self.methodInvoked(#selector(Base.viewDidLoad)).map { $0.first as? Bool ?? false } + return ControlEvent(events: event) + } + public var viewWillAppear: ControlEvent { let event = self.methodInvoked(#selector(Base.viewWillAppear)).map { $0.first as? Bool ?? false } return ControlEvent(events: event) diff --git a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIWorker.swift index f44415ded..f82ffe34c 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIWorker.swift @@ -102,7 +102,7 @@ extension FamilyAPIWorker { // MARK: - Fetch Family Member - public func fetchPaginationFamilyMember(familyId: String, query: FamilyPaginationQuery) -> Single { + public func fetchPaginationFamilyMember(query: FamilyPaginationQuery) -> Single { let page = query.page let size = query.size let spec = FamilyAPIs.fetchPaginationFamilyMembers(page, size).spec diff --git a/14th-team5-iOS/Data/Sources/APIs/Family/Repository/FamilyRepository.swift b/14th-team5-iOS/Data/Sources/APIs/Family/Repository/FamilyRepository.swift index 645866a19..413a9c481 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Family/Repository/FamilyRepository.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Family/Repository/FamilyRepository.swift @@ -12,11 +12,10 @@ import Foundation import RxSwift public final class FamilyRepository: FamilyRepositoryProtocol { - // MARK: - Properties public let disposeBag: DisposeBag = DisposeBag() - + private let familyApiWorker: FamilyAPIWorker = FamilyAPIWorker() private let familyUserDefaults: FamilyInfoUserDefaultsType = FamilyInfoUserDefaults() @@ -127,7 +126,7 @@ extension FamilyRepository { let familyId = familyUserDefaults.loadFamilyId() else { return .error(NSError()) } // TODO: - Error 타입 정의하기 - return familyApiWorker.fetchPaginationFamilyMember(familyId: familyId, query: query) + return familyApiWorker.fetchPaginationFamilyMember(query: query) .map { $0?.toDomain() } .do(onSuccess: { [weak self] in guard let self else { return } @@ -139,6 +138,14 @@ extension FamilyRepository { .asObservable() } + public func fetchAllFamilyMembers() -> Observable<[FamilyMemberProfileEntity]?> { + return familyApiWorker.fetchPaginationFamilyMember(query: .init()) + .map { response in + return response?.results.map { $0.toDomain() } + } + .asObservable() + } + public func fetchPaginationFamilyMembers(memberIds: [String]) -> [FamilyMemberProfileEntity] { var results: [FamilyMemberProfileEntity] = [] for memberId in memberIds { @@ -150,6 +157,9 @@ extension FamilyRepository { return results } + public func loadAllFamilyMembers() -> [FamilyMemberProfileEntity]? { + return familyUserDefaults.loadFamilyMembers() + } // MARK: - Fetch Family Name diff --git a/14th-team5-iOS/Data/Sources/APIs/Members/Repositories/MembersRepository.swift b/14th-team5-iOS/Data/Sources/APIs/Members/Repositories/MembersRepository.swift index 400f1ca47..4e1bd3f6d 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Members/Repositories/MembersRepository.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Members/Repositories/MembersRepository.swift @@ -16,6 +16,8 @@ import RxCocoa public final class MembersRepository { public var disposeBag: DisposeBag = DisposeBag() + + private let familyUserDefaults: FamilyInfoUserDefaults = FamilyInfoUserDefaults() private let membersAPIWorker: MembersAPIWorker = MembersAPIWorker() private let accessToken: String = App.Repository.token.accessToken.value?.accessToken ?? "" public init() { } @@ -35,7 +37,7 @@ extension MembersRepository: MembersRepositoryProtocol { return membersAPIWorker.updateProfileAlbumImageToS3(accessToken: accessToken, memberId: memberId, parameter: parameter) .do { guard let userEntity = $0?.toProfileEntity() else { return } - FamilyUserDefaults.saveMemberToUserDefaults(familyMember: userEntity) + self.familyUserDefaults.updateFamilyMember(userEntity) } .map { $0?.toDomain() } .catchAndReturn(nil) @@ -45,7 +47,7 @@ extension MembersRepository: MembersRepositoryProtocol { return membersAPIWorker.deleteProfileImageToS3(accessToken: accessToken, memberId: memberId) .do { guard let userEntity = $0?.toProfileEntity() else { return } - FamilyUserDefaults.saveMemberToUserDefaults(familyMember: userEntity) + self.familyUserDefaults.updateFamilyMember(userEntity) } .map { $0?.toDomain() } .catchAndReturn(nil) diff --git a/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/DataMapping/FetchReactionResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/DataMapping/FetchReactionResponseDTO.swift index 46d480f39..0525709ec 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/DataMapping/FetchReactionResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/DataMapping/FetchReactionResponseDTO.swift @@ -17,7 +17,8 @@ struct FetchReactionResult: Codable { let emojiType: String private func containsCurrentUser(memberIds: [String]) -> Bool { - let currentMemberId = FamilyUserDefaults.returnMyMemberId() + let myUserDefaults = MyUserDefaults() + let currentMemberId = myUserDefaults.loadMemberId() ?? "NONE" return memberIds.contains(currentMemberId) } } @@ -26,7 +27,8 @@ struct FetchReactionResponseDTO: Codable { let results: [FetchReactionResult] func toDomain() -> [EmojiEntity] { - let myMemberId = FamilyUserDefaults.returnMyMemberId() + let myUserDefaults = MyUserDefaults() + let myMemberId = myUserDefaults.loadMemberId() ?? "NONE" let groupedByEmojiType = Dictionary(grouping: results, by: { $0.emojiType }) let fetchedEmojiDataArray = groupedByEmojiType.map { (emojiType, responses) in diff --git a/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/DataMapping/FetchRealEmojiListResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/DataMapping/FetchRealEmojiListResponseDTO.swift index 0846e3f02..372fc3d96 100644 --- a/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/DataMapping/FetchRealEmojiListResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/DataMapping/FetchRealEmojiListResponseDTO.swift @@ -23,7 +23,8 @@ struct FetchRealEmojiListResponseDTO: Codable { let results: [RealEmojiListResult] func toDomain() -> [EmojiEntity]? { - let myMemberId = FamilyUserDefaults.returnMyMemberId() + let myUserDefaults = MyUserDefaults() + let myMemberId = myUserDefaults.loadMemberId() ?? "NONE" let groupedByEmojiType = Dictionary(grouping: results, by: { $0.realEmojiId }) let fetchedEmojiDataArray = groupedByEmojiType.map { (emojiType, responses) in diff --git a/14th-team5-iOS/Data/Sources/APIs/Resign/Repository/AccountResignRepository.swift b/14th-team5-iOS/Data/Sources/APIs/Resign/Repository/AccountResignRepository.swift index ce82eac75..c04a8fe57 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Resign/Repository/AccountResignRepository.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Resign/Repository/AccountResignRepository.swift @@ -20,7 +20,10 @@ public final class AccountResignRepository: AccountResignRepositoryProtocol { extension AccountResignRepository { public func deleteAccountResignItem() -> Observable { - return resignApiWorker.resignUser(memberId: FamilyUserDefaults.getMyMemberId()) + let myUserDefaults = MyUserDefaults() + let currentMemberId = myUserDefaults.loadMemberId() ?? "NONE" + + return resignApiWorker.resignUser(memberId: currentMemberId) .compactMap { $0?.toDomain() } .asObservable() diff --git a/14th-team5-iOS/Data/Sources/Storages/UserDefaults/FamilyUserDefaults/FamilyUserDefaults.swift b/14th-team5-iOS/Data/Sources/Storages/UserDefaults/FamilyUserDefaults/FamilyUserDefaults.swift index e3e4a650f..99e861c33 100644 --- a/14th-team5-iOS/Data/Sources/Storages/UserDefaults/FamilyUserDefaults/FamilyUserDefaults.swift +++ b/14th-team5-iOS/Data/Sources/Storages/UserDefaults/FamilyUserDefaults/FamilyUserDefaults.swift @@ -11,8 +11,9 @@ import Core import Domain -public protocol FamilyInfoUserDefaultsType: UserDefaultsType { +public protocol FamilyInfoUserDefaultsType: UserDefaultsType { func loadFamilyMember(_ memberId: String) -> Profile? + func updateFamilyMember(_ member: Profile) func saveFamilyMembers(_ members: [Profile]) func loadFamilyMembers() -> [Profile]? @@ -33,7 +34,6 @@ public protocol FamilyInfoUserDefaultsType: UserDefaultsType { final public class FamilyInfoUserDefaults: FamilyInfoUserDefaultsType { - // MARK: - Intializer @@ -50,6 +50,18 @@ final public class FamilyInfoUserDefaults: FamilyInfoUserDefaultsType { return member.first } + public func updateFamilyMember(_ member: Profile) { + guard var familyMembers = loadFamilyMembers() else { + return + } + + if let index = familyMembers.firstIndex(where: { $0.memberId == member.memberId }) { + familyMembers[index] = member + } + + saveFamilyMembers(familyMembers) + } + // MARK: - FamilyMembers public func saveFamilyMembers(_ members: [Profile]) { diff --git a/14th-team5-iOS/Data/Sources/Trash/Account/AccountRepository/AccountRepository.swift b/14th-team5-iOS/Data/Sources/Trash/Account/AccountRepository/AccountRepository.swift index 524a15cb2..01004616c 100644 --- a/14th-team5-iOS/Data/Sources/Trash/Account/AccountRepository/AccountRepository.swift +++ b/14th-team5-iOS/Data/Sources/Trash/Account/AccountRepository/AccountRepository.swift @@ -144,8 +144,6 @@ public final class AccountRepository: AccountImpl { App.Repository.member.nickname.accept(memberInfo.name) let member: FamilyMemberProfileEntity = FamilyMemberProfileEntity(memberId: memberInfo.memberId, profileImageURL: memberInfo.imageUrl, name: memberInfo.name) - FamilyUserDefaults.saveMyMemberId(memberId: memberInfo.memberId) - FamilyUserDefaults.saveMemberToUserDefaults(familyMember: member) } diff --git a/14th-team5-iOS/Data/Sources/Trash/Camera/Repository/CameraRepository.swift b/14th-team5-iOS/Data/Sources/Trash/Camera/Repository/CameraRepository.swift index 11971766e..163c709d9 100644 --- a/14th-team5-iOS/Data/Sources/Trash/Camera/Repository/CameraRepository.swift +++ b/14th-team5-iOS/Data/Sources/Trash/Camera/Repository/CameraRepository.swift @@ -48,7 +48,8 @@ extension CameraRepository: CameraRepositoryProtocol { return cameraAPIWorker.editProfileImageToS3(accessToken: accessToken, memberId: memberId, parameters: parameter) .do { guard let userEntity = $0?.toProfileEntity() else { return } - FamilyUserDefaults.saveMemberToUserDefaults(familyMember: userEntity) + let familyUserDefaults = FamilyInfoUserDefaults() + familyUserDefaults.updateFamilyMember(userEntity) } .map { $0?.toDomain() } } diff --git a/14th-team5-iOS/Data/Sources/Trash/Member/Repository/MemberRepository.swift b/14th-team5-iOS/Data/Sources/Trash/Member/Repository/MemberRepository.swift index bee456c54..a45350fe4 100644 --- a/14th-team5-iOS/Data/Sources/Trash/Member/Repository/MemberRepository.swift +++ b/14th-team5-iOS/Data/Sources/Trash/Member/Repository/MemberRepository.swift @@ -10,34 +10,38 @@ import Foundation public final class MemberRepository: MemberRepositoryProtocol { public init() { } + + let familyUserDefaults = FamilyInfoUserDefaults() + let myUserDefaults = MyUserDefaults() } extension MemberRepository { public func fetchFamilyNameEditorId() -> String { - return FamilyUserDefaults.loadFamilyNameEditorId() + return familyUserDefaults.loadFamilyNameEditorId() ?? "알 수 없음" } public func fetchUserName(memberId: String) -> String { - return FamilyUserDefaults.load(memberId: memberId)?.name ?? .unknown + return familyUserDefaults.loadFamilyMember(memberId)?.name ?? "알 수 없음" } public func fetchProfileImageUrlString(memberId: String) -> String { - return FamilyUserDefaults.load(memberId: memberId)?.profileImageURL ?? .unknown + return familyUserDefaults.loadFamilyMember(memberId)?.profileImageURL ?? .unknown } public func checkIsMe(memberId: String) -> Bool { - return FamilyUserDefaults.checkIsMyMemberId(memberId: memberId) + return myUserDefaults.loadMemberId() == memberId } public func checkIsValidMember(memberId: String) -> Bool { - let memberIds: [String] = FamilyUserDefaults.loadMemberIds() - for id in memberIds where id == memberId { - return true + if let familyMembers = familyUserDefaults.loadFamilyMembers() { + let ids = familyMembers.map { $0.memberId } + + return ids.contains(memberId) } return false } public func fetchMyMemberId() -> String { - return FamilyUserDefaults.getMyMemberId() + return myUserDefaults.loadMemberId() ?? .unknown } } diff --git a/14th-team5-iOS/Data/Sources/Trash/Post/PostList/DataMapping/PostListResponseDTO.swift b/14th-team5-iOS/Data/Sources/Trash/Post/PostList/DataMapping/PostListResponseDTO.swift index e97e127da..6a2d37ad3 100644 --- a/14th-team5-iOS/Data/Sources/Trash/Post/PostList/DataMapping/PostListResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/Trash/Post/PostList/DataMapping/PostListResponseDTO.swift @@ -23,12 +23,11 @@ struct PostListResultsDTO: Codable { extension PostListResultsDTO { func toDomain() -> PostEntity { - let author = FamilyUserDefaults.load(memberId: authorId) return .init( postId: postId, missionId: missionId, missionType: type, - author: author, + author: .init(memberId: authorId), commentCount: commentCount, emojiCount: emojiCount, imageURL: imageUrl, diff --git a/14th-team5-iOS/Data/Sources/Trash/UserDefaults/FamilyUserDefautls.swift b/14th-team5-iOS/Data/Sources/Trash/UserDefaults/FamilyUserDefautls.swift index a45086be4..59775fefc 100644 --- a/14th-team5-iOS/Data/Sources/Trash/UserDefaults/FamilyUserDefautls.swift +++ b/14th-team5-iOS/Data/Sources/Trash/UserDefaults/FamilyUserDefautls.swift @@ -1,136 +1,136 @@ +//// +//// FamilyDataRepository.swift +//// Core +//// +//// Created by 마경미 on 02.01.24. +//// // -// FamilyDataRepository.swift -// Core +//import Foundation +//import Domain // -// Created by 마경미 on 02.01.24. +//import RxSwift +//import RxCocoa // - -import Foundation -import Domain - -import RxSwift -import RxCocoa - -@available(*, deprecated, renamed: "FamilyInfoUserDefaults", message: "FamilyInfoUserDefaults!!!!!!!!!!!! 쓰세요") -public class FamilyUserDefaults { - /// familyIdKey - familyId 저장 - /// familyId - memberId를 배열로 저장 - /// 각 memberId - familymember 객체 저장 - - private static let familyIdKey = "familyId" - private static let familyEditorIdKey = "familyEditorId" - private static let myMemberIdKey = "memberId" - private static let memberIdsKey = "memberIds" - private static let dayOfBirths = "dayOfBirths" - - private static var userDefaults: UserDefaults { - UserDefaults.standard - } - - public static func checkIsMyMemberId(memberId: String) -> Bool { - return memberId == UserDefaults.standard.string(forKey: myMemberIdKey) - } - - public static func returnMyMemberId() -> String { - return UserDefaults.standard.string(forKey: myMemberIdKey) ?? "" - } - - public static func removeFamilyMembers() { - UserDefaults.standard.stringArray(forKey: memberIdsKey)?.forEach { - UserDefaults.standard.removeObject(forKey: $0) - print($0) - } - - UserDefaults.standard.removeObject(forKey: memberIdsKey) - } - - public static func saveMyMemberId(memberId: String) { - UserDefaults.standard.setValue(memberId, forKey: myMemberIdKey) - } - - public static func saveFamilyEditorId(familyEditorId: String) { - UserDefaults.standard.setValue(familyEditorId, forKey: familyEditorIdKey) - } - - public static func getMyMemberId() -> String { - return UserDefaults.standard.string(forKey: myMemberIdKey) ?? "" - } - - public static func getDateOfBirths() -> [Date] { - guard let dateOfBirths = userDefaults.array( - forKey: dayOfBirths - ) as? [Date] else { - return [] - } - return dateOfBirths - } - - public static func getMemberCount() -> Int { - return UserDefaults.standard.stringArray(forKey: myMemberIdKey)?.count ?? 0 - } - - public static func saveFamilyMembers(_ familyMembers: [FamilyMemberProfileEntity]) { - removeFamilyMembers() - saveMemberIdToUserDefaults(memberIds: familyMembers.map { $0.memberId }) - saveDayOfBirths(dateOfBirths: familyMembers.map { $0.dayOfBirth ?? Date() }) - familyMembers.forEach { saveMemberToUserDefaults(familyMember: $0) } - } - - public static func load(memberId: String) -> FamilyMemberProfileEntity? { - if let data = UserDefaults.standard.data(forKey: memberId) { - do { - let decoder = JSONDecoder() - let person = try decoder.decode(FamilyMemberProfileEntity.self, from: data) - return person - } catch { - print("Error decoding person: \(error.localizedDescription)") - } - } - return nil - } - - public static func loadMemberIds() -> [String] { - return userDefaults.array(forKey: memberIdsKey) as! [String] - } - - public static func loadFamilyNameEditorId() -> String { - return userDefaults.string(forKey: familyEditorIdKey) ?? "" - } -} - -extension FamilyUserDefaults { - public static func saveMemberToUserDefaults(familyMember: FamilyMemberProfileEntity) { - do { - let encoder = JSONEncoder() - let data = try encoder.encode(familyMember) - - UserDefaults.standard.set(data, forKey: familyMember.memberId) - } catch { - print("Error encoding person: \(error.localizedDescription)") - } - } - - private static func saveMemberIdToUserDefaults(memberIds: [String]) { - UserDefaults.standard.setValue(memberIds, forKey: memberIdsKey) - } - - private static func saveDayOfBirths(dateOfBirths: [Date]) { - userDefaults.setValue(dateOfBirths, forKey: self.dayOfBirths) - } - - static func loadMembersFromUserDefaults(memberIds: [String]) -> [FamilyMemberProfileEntity] { - var datas: [FamilyMemberProfileEntity] = [] - memberIds.forEach { - if let data = UserDefaults.standard.data(forKey: $0) { - do { - let decoder = JSONDecoder() - let data = try decoder.decode(FamilyMemberProfileEntity.self, from: data) - return datas.append(data) - } catch { - print("Error decoding person: \(error.localizedDescription)") - } - } - } - return datas - } -} +//@available(*, deprecated, renamed: "FamilyInfoUserDefaults", message: "FamilyInfoUserDefaults!!!!!!!!!!!! 쓰세요") +//public class FamilyUserDefaults { +// /// familyIdKey - familyId 저장 +// /// familyId - memberId를 배열로 저장 +// /// 각 memberId - familymember 객체 저장 +// +// private static let familyIdKey = "familyId" +// private static let familyEditorIdKey = "familyEditorId" +// private static let myMemberIdKey = "memberId" +// private static let memberIdsKey = "memberIds" +// private static let dayOfBirths = "dayOfBirths" +// +// private static var userDefaults: UserDefaults { +// UserDefaults.standard +// } +// +// public static func checkIsMyMemberId(memberId: String) -> Bool { +// return memberId == UserDefaults.standard.string(forKey: myMemberIdKey) +// } +// +// public static func returnMyMemberId() -> String { +// return UserDefaults.standard.string(forKey: myMemberIdKey) ?? "" +// } +// +// public static func removeFamilyMembers() { +// UserDefaults.standard.stringArray(forKey: memberIdsKey)?.forEach { +// UserDefaults.standard.removeObject(forKey: $0) +// print($0) +// } +// +// UserDefaults.standard.removeObject(forKey: memberIdsKey) +// } +// +// public static func saveMyMemberId(memberId: String) { +// UserDefaults.standard.setValue(memberId, forKey: myMemberIdKey) +// } +// +// public static func saveFamilyEditorId(familyEditorId: String) { +// UserDefaults.standard.setValue(familyEditorId, forKey: familyEditorIdKey) +// } +// +// public static func getMyMemberId() -> String { +// return UserDefaults.standard.string(forKey: myMemberIdKey) ?? "" +// } +// +// public static func getDateOfBirths() -> [Date] { +// guard let dateOfBirths = userDefaults.array( +// forKey: dayOfBirths +// ) as? [Date] else { +// return [] +// } +// return dateOfBirths +// } +// +// public static func getMemberCount() -> Int { +// return UserDefaults.standard.stringArray(forKey: myMemberIdKey)?.count ?? 0 +// } +// +// public static func saveFamilyMembers(_ familyMembers: [FamilyMemberProfileEntity]) { +// removeFamilyMembers() +// saveMemberIdToUserDefaults(memberIds: familyMembers.map { $0.memberId }) +// saveDayOfBirths(dateOfBirths: familyMembers.map { $0.dayOfBirth ?? Date() }) +// familyMembers.forEach { saveMemberToUserDefaults(familyMember: $0) } +// } +// +// public static func load(memberId: String) -> FamilyMemberProfileEntity? { +// if let data = UserDefaults.standard.data(forKey: memberId) { +// do { +// let decoder = JSONDecoder() +// let person = try decoder.decode(FamilyMemberProfileEntity.self, from: data) +// return person +// } catch { +// print("Error decoding person: \(error.localizedDescription)") +// } +// } +// return nil +// } +// +// public static func loadMemberIds() -> [String] { +// return userDefaults.array(forKey: memberIdsKey) as! [String] +// } +// +// public static func loadFamilyNameEditorId() -> String { +// return userDefaults.string(forKey: familyEditorIdKey) ?? "" +// } +//} +// +//extension FamilyUserDefaults { +// public static func saveMemberToUserDefaults(familyMember: FamilyMemberProfileEntity) { +// do { +// let encoder = JSONEncoder() +// let data = try encoder.encode(familyMember) +// +// UserDefaults.standard.set(data, forKey: familyMember.memberId) +// } catch { +// print("Error encoding person: \(error.localizedDescription)") +// } +// } +// +// private static func saveMemberIdToUserDefaults(memberIds: [String]) { +// UserDefaults.standard.setValue(memberIds, forKey: memberIdsKey) +// } +// +// private static func saveDayOfBirths(dateOfBirths: [Date]) { +// userDefaults.setValue(dateOfBirths, forKey: self.dayOfBirths) +// } +// +// static func loadMembersFromUserDefaults(memberIds: [String]) -> [FamilyMemberProfileEntity] { +// var datas: [FamilyMemberProfileEntity] = [] +// memberIds.forEach { +// if let data = UserDefaults.standard.data(forKey: $0) { +// do { +// let decoder = JSONDecoder() +// let data = try decoder.decode(FamilyMemberProfileEntity.self, from: data) +// return datas.append(data) +// } catch { +// print("Error decoding person: \(error.localizedDescription)") +// } +// } +// } +// return datas +// } +//} diff --git a/14th-team5-iOS/Domain/Sources/Entities/Family/FamilyMemberProfileEntity.swift b/14th-team5-iOS/Domain/Sources/Entities/Family/FamilyMemberProfileEntity.swift index 636a122aa..cc0d18b95 100644 --- a/14th-team5-iOS/Domain/Sources/Entities/Family/FamilyMemberProfileEntity.swift +++ b/14th-team5-iOS/Domain/Sources/Entities/Family/FamilyMemberProfileEntity.swift @@ -29,7 +29,7 @@ public struct FamilyMemberProfileEntity: Equatable, Hashable, Codable { public init( memberId: String, profileImageURL: String? = nil, - name: String, + name: String? = nil, dayOfBirth: Date? = .distantFuture, isShowBirthdayMark: Bool = false, isShowPickIcon: Bool = false, @@ -37,7 +37,7 @@ public struct FamilyMemberProfileEntity: Equatable, Hashable, Codable { ) { self.memberId = memberId self.profileImageURL = profileImageURL - self.name = name + self.name = name ?? "알 수 없음" self.dayOfBirth = dayOfBirth self.isShowBirthdayMark = isShowBirthdayMark self.isShowPickIcon = isShowPickIcon diff --git a/14th-team5-iOS/Domain/Sources/Entities/PostList/PostEntity.swift b/14th-team5-iOS/Domain/Sources/Entities/PostList/PostEntity.swift index 695fed8e7..3ecbdb83d 100644 --- a/14th-team5-iOS/Domain/Sources/Entities/PostList/PostEntity.swift +++ b/14th-team5-iOS/Domain/Sources/Entities/PostList/PostEntity.swift @@ -9,7 +9,7 @@ import Foundation public struct PostEntity: Equatable, Hashable { public let postId: String - public let author: FamilyMemberProfileEntity? + public var author: FamilyMemberProfileEntity public var commentCount: Int public let missionId: String? public let missionType: String? @@ -18,7 +18,7 @@ public struct PostEntity: Equatable, Hashable { public let content: String? public let time: String - public init(postId: String, missionId: String? = nil, missionType: String? = nil, author: FamilyMemberProfileEntity?, commentCount: Int, emojiCount: Int, imageURL: String, content: String?, time: String) { + public init(postId: String, missionId: String? = nil, missionType: String? = nil, author: FamilyMemberProfileEntity, commentCount: Int, emojiCount: Int, imageURL: String, content: String?, time: String) { self.postId = postId self.missionId = missionId self.missionType = missionType @@ -30,14 +30,22 @@ public struct PostEntity: Equatable, Hashable { self.time = time } - public static var empty: PostEntity { - return .init(postId: "", author: nil, commentCount: 0, emojiCount: 0, imageURL: "", content: nil, time: "") + static public var empty: PostEntity { + .init( + postId: "", + author: .init(memberId: ""), + commentCount: 0, + emojiCount: 0, + imageURL: "", + content: nil, + time: "" + ) } } public struct PostListPageEntity: Equatable { public let isLast: Bool - public let postLists: [PostEntity] + public var postLists: [PostEntity] public init(isLast: Bool, postLists: [PostEntity]) { self.isLast = isLast diff --git a/14th-team5-iOS/Domain/Sources/Repositories/FamilyRepository.swift b/14th-team5-iOS/Domain/Sources/Repositories/FamilyRepository.swift index e6777400f..7a1e8e277 100644 --- a/14th-team5-iOS/Domain/Sources/Repositories/FamilyRepository.swift +++ b/14th-team5-iOS/Domain/Sources/Repositories/FamilyRepository.swift @@ -13,12 +13,19 @@ public protocol FamilyRepositoryProtocol { func joinFamily(body: JoinFamilyRequest) -> Observable func resignFamily() -> Observable func createFamily() -> Observable + func fetchFamilyId() -> String? func fetchFamilyCreatedAt() -> Observable + func fetchInvitationLink() -> Observable - func fetchPaginationFamilyMembers(query: FamilyPaginationQuery) -> Observable - func fetchPaginationFamilyMembers(memberIds: [String]) -> [FamilyMemberProfileEntity] + func fetchFamilyGroupInfo() -> Observable + func fetchFamilyName() -> String? func updateFamilyName(body: UpdateFamilyNameRequest) -> Observable + + func loadAllFamilyMembers() -> [FamilyMemberProfileEntity]? + func fetchAllFamilyMembers() -> Observable<[FamilyMemberProfileEntity]?> + func fetchPaginationFamilyMembers(query: FamilyPaginationQuery) -> Observable + func fetchPaginationFamilyMembers(memberIds: [String]) -> [FamilyMemberProfileEntity] } diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Post/FetchPostListUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Post/FetchPostListUseCase.swift index d061a776a..c60ab6f08 100644 --- a/14th-team5-iOS/Domain/Sources/UseCases/Post/FetchPostListUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/Post/FetchPostListUseCase.swift @@ -9,17 +9,85 @@ import Foundation import RxSwift public protocol FetchPostListUseCaseProtocol { - func execute(query: PostListQuery) -> Single + func execute(query: PostListQuery) -> Observable + func execute(query: PostListQuery) -> Observable<[PostEntity]?> } public class FetchPostListUseCase: FetchPostListUseCaseProtocol { private let postListRepository: PostListRepositoryProtocol + private let familyRepository: FamilyRepositoryProtocol - public init(postListRepository: PostListRepositoryProtocol) { + public init( + postListRepository: PostListRepositoryProtocol, + familyRepository: FamilyRepositoryProtocol + ) { self.postListRepository = postListRepository + self.familyRepository = familyRepository + } + + public func execute(query: PostListQuery) -> Observable<[PostEntity]?> { + return postListRepository.fetchTodayPostList(query: query) + .flatMap { posts -> Single<[PostEntity]?> in + guard let posts = posts else { + return Single.just(nil) + } + + return self.loadFamilyMembersAndUpdatePosts(posts: posts.postLists) + } + .asObservable() + } + + private func loadFamilyMembersAndUpdatePosts(posts: [PostEntity]) -> Single<[PostEntity]?> { + let members = self.familyRepository.loadAllFamilyMembers() + + if let members = members { + return self.updatePostsWithMembers(posts: posts, members: members) + } else { + return self.familyRepository.fetchAllFamilyMembers() + .flatMap { membersFromApi -> Single<[PostEntity]?> in + return self.updatePostsWithMembers(posts: posts, members: membersFromApi) + } + .asSingle() + } + } + + private func updatePostsWithMembers(posts: [PostEntity], members: [Profile]?) -> Single<[PostEntity]?> { + guard let members = members else { + return Single.just(nil) + } + + let updatedPosts = posts.map { post in + var updatedPost = post + if let member = members.first(where: { $0.memberId == updatedPost.author.memberId }) { + updatedPost.author = member + } + return updatedPost + } + + return Single.just(updatedPosts) // updatedPosts를 Single로 반환 } - public func execute(query: PostListQuery) -> Single { + public func execute(query: PostListQuery) -> Observable { return postListRepository.fetchTodayPostList(query: query) + .flatMap { posts -> Single in + let members = self.familyRepository.loadAllFamilyMembers() + + guard let posts = posts, + let members = members else { + return Single.just(nil) + } + + var updatedPosts = posts + updatedPosts.postLists = posts.postLists.map { post in + var updatedPost = post + if let member = members.first(where: { $0.memberId == updatedPost.author.memberId }) { + updatedPost.author = member + } + return updatedPost + } + + return Single.just(updatedPosts) + } + .asObservable() } } From 3ef591073fd0278d7ff032428efcd1de377f6e22 Mon Sep 17 00:00:00 2001 From: g_mi <62610032+akrudal@users.noreply.github.com> Date: Mon, 30 Sep 2024 13:27:12 +0900 Subject: [PATCH 224/263] =?UTF-8?q?fix:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EB=B2=84=ED=8A=BC=20=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83?= =?UTF-8?q?=EC=9D=84=20=EC=88=98=EC=A0=95=ED=95=B4=EC=9A=94=20(#664)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: the layout(#623) * feat: add setImage method(#623) --- .../AccountSignInViewController.swift | 26 +++++++++++++------ .../Bibbi/BBCommons/BBButton/BBButton.swift | 19 +++++++++++--- .../AccountSignIn/Apple.imageset/Apple.svg | 3 +++ .../Contents.json | 2 +- .../appleLogin.imageset/appleLogin.svg | 12 --------- .../Contents.json | 2 +- .../AccountSignIn/kakao.imageset/kakao.svg | 3 +++ .../kakaoLogin.imageset/kakaoLogin.svg | 12 --------- 8 files changed, 41 insertions(+), 38 deletions(-) create mode 100644 14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/AccountSignIn/Apple.imageset/Apple.svg rename 14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/AccountSignIn/{appleLogin.imageset => Apple.imageset}/Contents.json (76%) delete mode 100644 14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/AccountSignIn/appleLogin.imageset/appleLogin.svg rename 14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/AccountSignIn/{kakaoLogin.imageset => kakao.imageset}/Contents.json (76%) create mode 100644 14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/AccountSignIn/kakao.imageset/kakao.svg delete mode 100644 14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/AccountSignIn/kakaoLogin.imageset/kakaoLogin.svg 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 05269d5b0..34e6c6dbd 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignIn/AccountSignInViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignIn/AccountSignInViewController.swift @@ -32,8 +32,8 @@ public final class AccountSignInViewController: BaseViewController + + diff --git a/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/AccountSignIn/appleLogin.imageset/Contents.json b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/AccountSignIn/Apple.imageset/Contents.json similarity index 76% rename from 14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/AccountSignIn/appleLogin.imageset/Contents.json rename to 14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/AccountSignIn/Apple.imageset/Contents.json index d122b9a54..c15804c57 100644 --- a/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/AccountSignIn/appleLogin.imageset/Contents.json +++ b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/AccountSignIn/Apple.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "appleLogin.svg", + "filename" : "Apple.svg", "idiom" : "universal" } ], diff --git a/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/AccountSignIn/appleLogin.imageset/appleLogin.svg b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/AccountSignIn/appleLogin.imageset/appleLogin.svg deleted file mode 100644 index c961874f3..000000000 --- a/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/AccountSignIn/appleLogin.imageset/appleLogin.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/AccountSignIn/kakaoLogin.imageset/Contents.json b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/AccountSignIn/kakao.imageset/Contents.json similarity index 76% rename from 14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/AccountSignIn/kakaoLogin.imageset/Contents.json rename to 14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/AccountSignIn/kakao.imageset/Contents.json index dadd5bc10..0bae14827 100644 --- a/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/AccountSignIn/kakaoLogin.imageset/Contents.json +++ b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/AccountSignIn/kakao.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "kakaoLogin.svg", + "filename" : "kakao.svg", "idiom" : "universal" } ], diff --git a/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/AccountSignIn/kakao.imageset/kakao.svg b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/AccountSignIn/kakao.imageset/kakao.svg new file mode 100644 index 000000000..37b5e14a8 --- /dev/null +++ b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/AccountSignIn/kakao.imageset/kakao.svg @@ -0,0 +1,3 @@ + + + diff --git a/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/AccountSignIn/kakaoLogin.imageset/kakaoLogin.svg b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/AccountSignIn/kakaoLogin.imageset/kakaoLogin.svg deleted file mode 100644 index 007312776..000000000 --- a/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/AccountSignIn/kakaoLogin.imageset/kakaoLogin.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - From 91ccdef9cfd4e65f99b35dd212bbc2fc823e7397 Mon Sep 17 00:00:00 2001 From: g_mi <62610032+akrudal@users.noreply.github.com> Date: Mon, 30 Sep 2024 13:58:32 +0900 Subject: [PATCH 225/263] =?UTF-8?q?feat:=20=EA=B0=80=EC=A1=B1=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=EB=A5=BC=20=EC=9C=A0=EC=A0=80=EB=94=94=ED=8E=84?= =?UTF-8?q?=ED=8A=B8=EC=97=90=20=EC=A0=80=EC=9E=A5=ED=95=B4=EC=9A=94=20(#6?= =?UTF-8?q?67)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add save family members(#666) * feat: add comment --- .../Bibbi/BBCommons/BBButton/BBButton.swift | 1 + .../Family/Repository/FamilyRepository.swift | 21 ++++--- .../MainViewAPI/MainViewAPIWorker.swift | 6 +- .../Repository/MainViewRepository.swift | 20 ++++++- .../UseCases/Post/FetchPostListUseCase.swift | 57 ++++++++++--------- 5 files changed, 67 insertions(+), 38 deletions(-) diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBButton/BBButton.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBButton/BBButton.swift index 814340015..63704aa80 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBButton/BBButton.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBButton/BBButton.swift @@ -107,6 +107,7 @@ public class BBButton: UIButton { self.id = id } + /// 버튼의 이미지를 변경합니다. public func setImage(_ image: UIImage?) { mainImageView.image = image } diff --git a/14th-team5-iOS/Data/Sources/APIs/Family/Repository/FamilyRepository.swift b/14th-team5-iOS/Data/Sources/APIs/Family/Repository/FamilyRepository.swift index 413a9c481..966e1c4db 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Family/Repository/FamilyRepository.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Family/Repository/FamilyRepository.swift @@ -129,20 +129,27 @@ extension FamilyRepository { return familyApiWorker.fetchPaginationFamilyMember(query: query) .map { $0?.toDomain() } .do(onSuccess: { [weak self] in - guard let self else { return } - - if let profiles = $0?.results { - self.familyUserDefaults.saveFamilyMembers(profiles) + guard let self, + let profiles = $0?.results else { + return } + + self.familyUserDefaults.saveFamilyMembers(profiles) }) .asObservable() } public func fetchAllFamilyMembers() -> Observable<[FamilyMemberProfileEntity]?> { return familyApiWorker.fetchPaginationFamilyMember(query: .init()) - .map { response in - return response?.results.map { $0.toDomain() } - } + .map { $0?.results.map{ $0.toDomain() }} + .do(onSuccess: { [weak self] in + guard let self, + let profiles = $0 else { + return + } + + self.familyUserDefaults.saveFamilyMembers(profiles) + }) .asObservable() } diff --git a/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/MainViewAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/MainViewAPIWorker.swift index b81f4f1a5..6313231a8 100644 --- a/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/MainViewAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/MainViewAPIWorker.swift @@ -39,23 +39,21 @@ extension MainViewAPIs { } extension MainAPIWorker { - func fetchMain() -> Single { + func fetchMain() -> Single { let spec = MainViewAPIs.fetchMain.spec return request(spec: spec, headers: headers) .subscribe(on: Self.queue) .map(MainResponseDTO.self) .catchAndReturn(nil) - .map { $0?.toDomain() } .asSingle() } - func fetchMainNight() -> Single { + func fetchMainNight() -> Single { let spec = MainViewAPIs.fetchMainNight.spec return request(spec: spec, headers: headers) .subscribe(on: Self.queue) .map(MainNightResponseDTO.self) .catchAndReturn(nil) - .map { $0?.toDomain() } .asSingle() } } diff --git a/14th-team5-iOS/Data/Sources/APIs/MainView/Repository/MainViewRepository.swift b/14th-team5-iOS/Data/Sources/APIs/MainView/Repository/MainViewRepository.swift index 9ef56c19a..c1b92c31f 100644 --- a/14th-team5-iOS/Data/Sources/APIs/MainView/Repository/MainViewRepository.swift +++ b/14th-team5-iOS/Data/Sources/APIs/MainView/Repository/MainViewRepository.swift @@ -14,6 +14,7 @@ import RxSwift public final class MainViewRepository: MainViewRepositoryProtocol { public let disposeBag: DisposeBag = DisposeBag() + private let familyUserDefaults: FamilyInfoUserDefaults = FamilyInfoUserDefaults() private let mainApiWorker: MainAPIWorker = MainAPIWorker() public init() { } @@ -22,12 +23,29 @@ public final class MainViewRepository: MainViewRepositoryProtocol { extension MainViewRepository { public func fetchMain() -> Observable { return mainApiWorker.fetchMain() + .map { $0?.toDomain() } + .do(onSuccess: { [weak self] in + guard + let self, + let profiles = $0?.mainFamilyProfileDatas else { + return + } + self.familyUserDefaults.saveFamilyMembers(profiles) + }) .asObservable() - } public func fetchMainNight() -> Observable { return mainApiWorker.fetchMainNight() + .map { $0?.toDomain() } + .do(onSuccess: { [weak self] in + guard + let self, + let profiles = $0?.mainFamilyProfileDatas else { + return + } + self.familyUserDefaults.saveFamilyMembers(profiles) + }) .asObservable() } } diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Post/FetchPostListUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Post/FetchPostListUseCase.swift index c60ab6f08..7f1916559 100644 --- a/14th-team5-iOS/Domain/Sources/UseCases/Post/FetchPostListUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/Post/FetchPostListUseCase.swift @@ -25,6 +25,8 @@ public class FetchPostListUseCase: FetchPostListUseCaseProtocol { self.familyRepository = familyRepository } + /// postList를 불러옵니다. - 메인 화면에서 사용 중 입니다. + /// 만약, userdefaults에 저장된 가족 멤버가 없다면, fetchFamily를 한 번 실행하여 post 정보를 가져옵니다. public func execute(query: PostListQuery) -> Observable<[PostEntity]?> { return postListRepository.fetchTodayPostList(query: query) .flatMap { posts -> Single<[PostEntity]?> in @@ -37,6 +39,33 @@ public class FetchPostListUseCase: FetchPostListUseCaseProtocol { .asObservable() } + /// PostList를 페이지네이션 형태로 불러옵니다 - 프로필 조회에서 사용 중 입니다. + public func execute(query: PostListQuery) -> Observable { + return postListRepository.fetchTodayPostList(query: query) + .flatMap { posts -> Single in + let members = self.familyRepository.loadAllFamilyMembers() + + guard let posts = posts, + let members = members else { + return Single.just(nil) + } + + var updatedPosts = posts + updatedPosts.postLists = posts.postLists.map { post in + var updatedPost = post + if let member = members.first(where: { $0.memberId == updatedPost.author.memberId }) { + updatedPost.author = member + } + return updatedPost + } + + return Single.just(updatedPosts) + } + .asObservable() + } +} + +extension FetchPostListUseCase { private func loadFamilyMembersAndUpdatePosts(posts: [PostEntity]) -> Single<[PostEntity]?> { let members = self.familyRepository.loadAllFamilyMembers() @@ -50,7 +79,7 @@ public class FetchPostListUseCase: FetchPostListUseCaseProtocol { .asSingle() } } - + private func updatePostsWithMembers(posts: [PostEntity], members: [Profile]?) -> Single<[PostEntity]?> { guard let members = members else { return Single.just(nil) @@ -64,30 +93,6 @@ public class FetchPostListUseCase: FetchPostListUseCaseProtocol { return updatedPost } - return Single.just(updatedPosts) // updatedPosts를 Single로 반환 - } - - public func execute(query: PostListQuery) -> Observable { - return postListRepository.fetchTodayPostList(query: query) - .flatMap { posts -> Single in - let members = self.familyRepository.loadAllFamilyMembers() - - guard let posts = posts, - let members = members else { - return Single.just(nil) - } - - var updatedPosts = posts - updatedPosts.postLists = posts.postLists.map { post in - var updatedPost = post - if let member = members.first(where: { $0.memberId == updatedPost.author.memberId }) { - updatedPost.author = member - } - return updatedPost - } - - return Single.just(updatedPosts) - } - .asObservable() + return Single.just(updatedPosts) } } From 29bdaa4d5d0c160d2363ea523b4e7c44bdef66c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=84?= Date: Mon, 30 Sep 2024 13:58:42 +0900 Subject: [PATCH 226/263] =?UTF-8?q?fix:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EC=8B=9C=20=EB=A9=94=EC=9D=B8=ED=99=94=EB=A9=B4=20=EC=A7=84?= =?UTF-8?q?=EC=9E=85=20=EB=AA=BB=ED=95=98=EB=8A=94=20=EC=9D=B4=EC=8A=88=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=ED=95=B4=EC=9A=94=20(#665)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: AccountSignInViewController 기존 ShowNextPage 코드 제거 - AccountSignInReactor meUseCase 및 SignInNavigator 추가 - OnboardingReactor updateIsFirstOnboardingUseCase 추가 * fix: AccountSignInReactor 화면 전환 로직 수정 - AccountResignViewReactor 회원 탈퇴시 isFirstOnboarding false 값으로 수정 - FetchIsFirstOnboardingUseCase 예외 처리 로직 추가 - AppUserDefaults 로직 수정 * fix: print구문 제거 --- .../Navigator/AccountSignInNavigator.swift | 6 ++ .../AccountSignIn/AccountSignInReactor.swift | 71 +++++++++++++------ .../AccountSignInViewController.swift | 33 --------- .../Home/Reactor/MainViewReactor.swift | 2 - .../OnBoarding/OnBoardingReactor.swift | 4 +- .../Reactor/AccountResignViewReactor.swift | 2 +- .../APIs/My/Repository/MyRepository.swift | 2 +- .../AppUserDefaults/AppUserDefaults.swift | 5 +- .../My/FetchIsFirstOnboardingUseCase.swift | 9 ++- .../My/UpdateIsFirstOnboardingUseCase.swift | 4 +- 10 files changed, 68 insertions(+), 70 deletions(-) diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/AccountSignInNavigator.swift b/14th-team5-iOS/App/Sources/Application/Navigator/AccountSignInNavigator.swift index 7f0b145e7..e412f89d8 100644 --- a/14th-team5-iOS/App/Sources/Application/Navigator/AccountSignInNavigator.swift +++ b/14th-team5-iOS/App/Sources/Application/Navigator/AccountSignInNavigator.swift @@ -13,6 +13,7 @@ protocol AccountSignInNavigatorProtocol: BaseNavigator { func toMain() func toSignUp() func toJoinFamily() + func toOnboarding() } final class AccountSignInNavigator: AccountSignInNavigatorProtocol { @@ -37,4 +38,9 @@ final class AccountSignInNavigator: AccountSignInNavigatorProtocol { let vc = JoinFamilyViewControllerWrapper().viewController navigationController.setViewControllers([vc], animated: false) } + + func toOnboarding() { + let vc = OnboardingViewControllerWrapper().viewController + navigationController.setViewControllers([vc], animated: false) + } } diff --git a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignIn/AccountSignInReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignIn/AccountSignInReactor.swift index 5fba51f43..f235027e5 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignIn/AccountSignInReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignIn/AccountSignInReactor.swift @@ -17,7 +17,9 @@ public final class AccountSignInReactor: Reactor { public var initialState: State @Injected var fetchIsFirstOnboardingUseCase: any FetchIsFirstOnboardingUseCaseProtocol private var accountRepository: AccountImpl = AccountRepository() + private let meUseCase: MeUseCaseProtocol = MeUseCase(meRepository: MeAPIs.Worker()) private let fcmUseCase: FCMUseCaseProtocol = FCMUseCase(FCMRepository: MeAPIs.Worker()) + @Navigator var signInNavigator: AccountSignInNavigatorProtocol private let disposeBag = DisposeBag() public enum Action { @@ -26,14 +28,10 @@ public final class AccountSignInReactor: Reactor { } public enum Mutation { - case kakaoLogin(Bool) - case appleLogin(Bool) case setIsFirstOnboarding(Bool) - } public struct State { - var pushAccountSingUpVC: Bool @Pulse var isFirstOnboarding: Bool } @@ -41,7 +39,6 @@ public final class AccountSignInReactor: Reactor { // self.accountRepository = accountRepository // self.fcmUseCase = fcmUseCase self.initialState = State( - pushAccountSingUpVC: false, isFirstOnboarding: false ) } @@ -49,36 +46,31 @@ public final class AccountSignInReactor: Reactor { extension AccountSignInReactor { public func mutate(action: Action) -> Observable { - let isFirstOnboarding = self.fetchIsFirstOnboardingUseCase.execute() == nil ? false : true switch action { case .kakaoLoginTapped(let sns, let vc): return accountRepository.kakaoLogin(with: sns, vc: vc) - .flatMap { result -> Observable in + .withUnretained(self) + .flatMap { owner, result -> Observable in switch result { case .success: - self.saveFCM() - return .concat( - .just(.kakaoLogin(true)), - .just(.setIsFirstOnboarding(isFirstOnboarding)) - ) + owner.saveFCM() + return owner.transitionViewController() case .failed: - return .just(.kakaoLogin(false)) + return .empty() } } case .appleLoginTapped(let sns, let vc): return accountRepository.appleLogin(with: sns, vc: vc) - .flatMap { result -> Observable in + .withUnretained(self) + .flatMap { owner, result -> Observable in switch result { case .success: self.saveFCM() - return .concat( - .just(.appleLogin(true)), - .just(.setIsFirstOnboarding(isFirstOnboarding)) - ) + return owner.transitionViewController() case .failed: - return .just(.appleLogin(false)) + return .empty() } } } @@ -87,10 +79,6 @@ extension AccountSignInReactor { public func reduce(state: State, mutation: Mutation) -> State { var newState = state switch mutation { - case .kakaoLogin(let result): - newState.pushAccountSingUpVC = result - case .appleLogin(let result): - newState.pushAccountSingUpVC = result case let .setIsFirstOnboarding(isFirstOnboarding): newState.isFirstOnboarding = isFirstOnboarding } @@ -112,3 +100,40 @@ extension AccountSignInReactor { } } } + + +extension AccountSignInReactor { + private func transitionViewController() -> Observable { + let isFirstOnboarding = self.fetchIsFirstOnboardingUseCase.execute() + return App.Repository.token.accessToken + .skip(1) + .withUnretained(self) + .flatMapLatest { owner, token -> Observable in + guard let token, + let isTemporaryToken = token.isTemporaryToken else { + return .empty() + } + + if isTemporaryToken { + owner.signInNavigator.toSignUp() + return .empty() + } + + return owner.meUseCase.getMemberInfo() + .asObservable() + .flatMap { memberInfo -> Observable in + if isFirstOnboarding { + if memberInfo?.familyId == nil { + owner.signInNavigator.toJoinFamily() + return .empty() + } + owner.signInNavigator.toMain() + return .empty() + } + owner.signInNavigator.toOnboarding() + return .empty() + } + } + } + +} 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 34e6c6dbd..e63cdf766 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignIn/AccountSignInViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignIn/AccountSignInViewController.swift @@ -129,39 +129,6 @@ public final class AccountSignInViewController: BaseViewController Observable { switch action { case .permissionTapped: - Observable.create { observer in + Observable.create { [weak self] observer in + self?.updateIsFirstOnboardingUseCase.execute(true) MPEvent.Account.invitedGroupFinished.track(with: nil) UNUserNotificationCenter.current().requestAuthorization( options: [.alert, .badge, .sound], diff --git a/14th-team5-iOS/App/Sources/Presentation/Resign/Reactor/AccountResignViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Resign/Reactor/AccountResignViewReactor.swift index da69d6037..0ec3620bb 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Resign/Reactor/AccountResignViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Resign/Reactor/AccountResignViewReactor.swift @@ -61,7 +61,7 @@ final class AccountResignViewReactor: Reactor { .withUnretained(self) .flatMap { owner, entity -> Observable in if entity.isSuccess { - owner.updateIsFirstOnboardingUseCase.execute(nil) + owner.updateIsFirstOnboardingUseCase.execute(false) return .concat( .just(.setLoading(true)), .just(.setResignEntity(entity.isSuccess)), diff --git a/14th-team5-iOS/Data/Sources/APIs/My/Repository/MyRepository.swift b/14th-team5-iOS/Data/Sources/APIs/My/Repository/MyRepository.swift index abec96461..930111c11 100644 --- a/14th-team5-iOS/Data/Sources/APIs/My/Repository/MyRepository.swift +++ b/14th-team5-iOS/Data/Sources/APIs/My/Repository/MyRepository.swift @@ -46,7 +46,7 @@ extension MyRepository { } public func updateIsFirstOnboarding(_ isFirstOnboarding: Bool?) { - appUserDefaults.saveIsFirstOnboarding(isFirstOnboarding) + return appUserDefaults.saveIsFirstOnboarding(isFirstOnboarding) } } 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 0435d7d5f..71f290718 100644 --- a/14th-team5-iOS/Data/Sources/Storages/UserDefaults/AppUserDefaults/AppUserDefaults.swift +++ b/14th-team5-iOS/Data/Sources/Storages/UserDefaults/AppUserDefaults/AppUserDefaults.swift @@ -98,10 +98,7 @@ final public class AppUserDefaults: AppUserDefaultsType { } public func loadIsFirstOnboarding() -> Bool? { - guard let isFirstOnboarding: Bool = userDefaults[.isFirstOnboarding] else { - return nil - } - return isFirstOnboarding + return userDefaults[.isFirstOnboarding] } // MARK: - FamilyManagement diff --git a/14th-team5-iOS/Domain/Sources/UseCases/My/FetchIsFirstOnboardingUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/My/FetchIsFirstOnboardingUseCase.swift index e32a5d7a9..8bb219dc4 100644 --- a/14th-team5-iOS/Domain/Sources/UseCases/My/FetchIsFirstOnboardingUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/My/FetchIsFirstOnboardingUseCase.swift @@ -9,7 +9,7 @@ import Foundation public protocol FetchIsFirstOnboardingUseCaseProtocol { - func execute() -> Bool? + func execute() -> Bool } @@ -21,7 +21,10 @@ public final class FetchIsFirstOnboardingUseCase: FetchIsFirstOnboardingUseCaseP self.myRepository = myRepository } - public func execute() -> Bool? { - return myRepository.fetchIsFirstOnboarding() + public func execute() -> Bool { + guard let isFirstOnboarding = myRepository.fetchIsFirstOnboarding() else { + return false + } + return isFirstOnboarding } } diff --git a/14th-team5-iOS/Domain/Sources/UseCases/My/UpdateIsFirstOnboardingUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/My/UpdateIsFirstOnboardingUseCase.swift index 6ab821e7c..b6ad5b234 100644 --- a/14th-team5-iOS/Domain/Sources/UseCases/My/UpdateIsFirstOnboardingUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/My/UpdateIsFirstOnboardingUseCase.swift @@ -9,7 +9,7 @@ import Foundation public protocol UpdateIsFirstOnboardingUseCaseProtocol { - func execute(_ isFirstOnboarding: Bool?) + func execute(_ isFirstOnboarding: Bool) } public final class UpdateIsFirstOnboardingUseCase: UpdateIsFirstOnboardingUseCaseProtocol { @@ -20,7 +20,7 @@ public final class UpdateIsFirstOnboardingUseCase: UpdateIsFirstOnboardingUseCas self.myRepository = myRepository } - public func execute(_ isFirstOnboarding: Bool?) { + public func execute(_ isFirstOnboarding: Bool) { myRepository.updateIsFirstOnboarding(isFirstOnboarding) } } From 82eb755b4f6a490266fac157ff9f971b6573b808 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=84?= Date: Tue, 1 Oct 2024 23:06:37 +0900 Subject: [PATCH 227/263] =?UTF-8?q?fix:=20=EA=B0=80=EC=A1=B1=EB=B0=A9=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=ED=99=94=EB=A9=B4=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=EC=9E=90=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EB=AA=BB=20=EA=B0=80=EC=A0=B8=EC=98=A4=EB=8A=94=20=EC=9D=B4?= =?UTF-8?q?=EC=8A=88=20=EC=88=98=EC=A0=95=ED=95=B4=EC=9A=94=20(#670)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: FamilyNameSettingViewController GroupEditerView isHidden 코드 제거 - CameraAPIWorker 메서드 내부 AccessToken 값 주입 코드 추가 - CameraRepository 각 메서드 Parameters AccessToken 제거 * fix: MembersAPIWorker AccessToken Parameter 제거 --- .../Camera/Reactor/CameraViewReactor.swift | 3 +- .../FamilyNameSettingViewController.swift | 8 ++--- .../Members/MemberAPI/MembersAPIWorker.swift | 34 ++++++++++--------- .../Repositories/MembersRepository.swift | 7 ++-- .../AccountRepository/AccountRepository.swift | 4 +-- .../Camera/CameraAPI/CameraAPIWorker.swift | 33 +++++++++++------- .../Camera/Repository/CameraRepository.swift | 19 +++++------ .../Interfaces/CameraRepositoryProtocol.swift | 1 - 8 files changed, 56 insertions(+), 53 deletions(-) diff --git a/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraViewReactor.swift index 037ca4857..c1c236259 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraViewReactor.swift @@ -284,12 +284,11 @@ extension CameraViewReactor { case .profile: //Profile 관련 이미지 업로드 Mutation - let profileImage = "\(imageData.hash).jpg" + let profileImage = "\(imageData.hashValue).jpg" let profileParameter = CameraDisplayImageParameters(imageName: profileImage) return .concat( .just(.setLoading(false)), - createProfileImageUseCase.execute(parameter: profileParameter) .asObservable() .withUnretained(self) diff --git a/14th-team5-iOS/App/Sources/Presentation/Management/ViewController/FamilyNameSettingViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Management/ViewController/FamilyNameSettingViewController.swift index a78699b66..9db1005bb 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Management/ViewController/FamilyNameSettingViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Management/ViewController/FamilyNameSettingViewController.swift @@ -112,10 +112,6 @@ final class FamilyNameSettingViewController: BBNavigationViewController Single { + public func fetchProfileMember(memberId: String) -> Single { let spec = MembersAPIs.profileMember(memberId).spec - - return request(spec: spec, headers: [BibbiAPI.Header.xAppVersion, BibbiAPI.Header.xUserPlatform, BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.xAuthToken(accessToken)]) + let accessToken: String = App.Repository.token.accessToken.value?.accessToken ?? "" + let headers = BibbiHeader.commonHeaders(accessToken: accessToken) + return request(spec: spec, headers: headers) .subscribe(on: Self.queue) .map(MembersProfileResponseDTO.self) .catchAndReturn(nil) @@ -44,39 +44,41 @@ extension MembersAPIWorker { } - public func createProfileImagePresingedURL(accessToken: String, parameters: Encodable) -> Single { + public func createProfileImagePresingedURL(parameters: Encodable) -> Single { let spec = MembersAPIs.profileAlbumUploadImageURL.spec - - return request(spec: spec, headers: [BibbiAPI.Header.xAppVersion, BibbiAPI.Header.xUserPlatform, BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.xAuthToken(accessToken)], jsonEncodable: parameters) + let accessToken: String = App.Repository.token.accessToken.value?.accessToken ?? "" + let headers = BibbiHeader.commonHeaders(accessToken: accessToken) + return request(spec: spec, headers: headers, jsonEncodable: parameters) .subscribe(on: Self.queue) .map(CameraDisplayImageResponseDTO.self) .catchAndReturn(nil) .asSingle() } - public func uploadToProfilePresingedURL(accessToken: String, toURL url: String, with imageData: Data) -> Single { + public func uploadToProfilePresingedURL(toURL url: String, with imageData: Data) -> Single { let spec = MembersAPIs.profileUploadToPreSignedURL(url).spec - - return upload(spec: spec, headers: [BibbiAPI.Header.xAppVersion, BibbiAPI.Header.xUserPlatform, BibbiAPI.Header.xAppKey, BibbiAPI.Header.xAuthToken(accessToken)], image: imageData) + let accessToken: String = App.Repository.token.accessToken.value?.accessToken ?? "" + let headers = BibbiHeader.commonHeaders(accessToken: accessToken) + return upload(spec: spec, headers: headers, image: imageData) .subscribe(on: Self.queue) .catchAndReturn(false) .map { _ in true } } - public func updateProfileAlbumImageToS3(accessToken: String, memberId: String, parameter: Encodable) -> Single { + public func updateProfileAlbumImageToS3(memberId: String, parameter: Encodable) -> Single { let spec = MembersAPIs.profileEditImage(memberId).spec - - return request(spec: spec, headers: [BibbiAPI.Header.xAppVersion, BibbiAPI.Header.xUserPlatform, BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.xAuthToken(accessToken)], jsonEncodable: parameter) + let accessToken: String = App.Repository.token.accessToken.value?.accessToken ?? "" + let headers = BibbiHeader.commonHeaders(accessToken: accessToken) + return request(spec: spec, headers: headers, jsonEncodable: parameter) .subscribe(on: Self.queue) .map(MembersProfileResponseDTO.self) .catchAndReturn(nil) .asSingle() } - public func deleteProfileImageToS3(accessToken: String, memberId: String) -> Single { + public func deleteProfileImageToS3(memberId: String) -> Single { let spec = MembersAPIs.profileDeleteImage(memberId).spec - - return request(spec: spec,headers: [BibbiAPI.Header.xAppVersion, BibbiAPI.Header.xUserPlatform, BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson]) + return request(spec: spec) .subscribe(on: Self.queue) .map(MembersProfileResponseDTO.self) .catchAndReturn(nil) diff --git a/14th-team5-iOS/Data/Sources/APIs/Members/Repositories/MembersRepository.swift b/14th-team5-iOS/Data/Sources/APIs/Members/Repositories/MembersRepository.swift index 4e1bd3f6d..f20744236 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Members/Repositories/MembersRepository.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Members/Repositories/MembersRepository.swift @@ -19,7 +19,6 @@ public final class MembersRepository { private let familyUserDefaults: FamilyInfoUserDefaults = FamilyInfoUserDefaults() private let membersAPIWorker: MembersAPIWorker = MembersAPIWorker() - private let accessToken: String = App.Repository.token.accessToken.value?.accessToken ?? "" public init() { } } @@ -28,13 +27,13 @@ public final class MembersRepository { extension MembersRepository: MembersRepositoryProtocol { public func fetchProfileMemberItems(memberId: String) -> Single { - return membersAPIWorker.fetchProfileMember(accessToken: accessToken, memberId: memberId) + return membersAPIWorker.fetchProfileMember(memberId: memberId) .map { $0?.toDomain() } .catchAndReturn(nil) } public func updataProfileImageToS3(memberId: String, parameter: ProfileImageEditParameter) -> Single { - return membersAPIWorker.updateProfileAlbumImageToS3(accessToken: accessToken, memberId: memberId, parameter: parameter) + return membersAPIWorker.updateProfileAlbumImageToS3(memberId: memberId, parameter: parameter) .do { guard let userEntity = $0?.toProfileEntity() else { return } self.familyUserDefaults.updateFamilyMember(userEntity) @@ -44,7 +43,7 @@ extension MembersRepository: MembersRepositoryProtocol { } public func deleteProfileImageToS3(memberId: String) -> Single { - return membersAPIWorker.deleteProfileImageToS3(accessToken: accessToken, memberId: memberId) + return membersAPIWorker.deleteProfileImageToS3(memberId: memberId) .do { guard let userEntity = $0?.toProfileEntity() else { return } self.familyUserDefaults.updateFamilyMember(userEntity) diff --git a/14th-team5-iOS/Data/Sources/Trash/Account/AccountRepository/AccountRepository.swift b/14th-team5-iOS/Data/Sources/Trash/Account/AccountRepository/AccountRepository.swift index 01004616c..edb2e95ee 100644 --- a/14th-team5-iOS/Data/Sources/Trash/Account/AccountRepository/AccountRepository.swift +++ b/14th-team5-iOS/Data/Sources/Trash/Account/AccountRepository/AccountRepository.swift @@ -125,13 +125,13 @@ public final class AccountRepository: AccountImpl { } public func executePresignedImageURLCreate(parameter: CameraDisplayImageParameters) -> Observable { - return profileWorker.createProfileImagePresingedURL(accessToken: accessToken, parameters: parameter) + return profileWorker.createProfileImagePresingedURL(parameters: parameter) .compactMap { $0?.toDomain() } .asObservable() } public func executeProfileImageUpload(to url: String, data: Data) -> Observable { - return profileWorker.uploadToProfilePresingedURL(accessToken: accessToken, toURL: url, with: data) + return profileWorker.uploadToProfilePresingedURL(toURL: url, with: data) .asObservable() } diff --git a/14th-team5-iOS/Data/Sources/Trash/Camera/CameraAPI/CameraAPIWorker.swift b/14th-team5-iOS/Data/Sources/Trash/Camera/CameraAPI/CameraAPIWorker.swift index 00999a868..2f10c6643 100644 --- a/14th-team5-iOS/Data/Sources/Trash/Camera/CameraAPI/CameraAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Trash/Camera/CameraAPI/CameraAPIWorker.swift @@ -32,9 +32,10 @@ extension CameraAPIs { extension CameraAPIWorker { - public func createProfilePresignedURL(accessToken: String, parameters: Encodable) -> Single { + public func createProfilePresignedURL(parameters: Encodable) -> Single { let spec = CameraAPIs.uploadProfileImageURL.spec + let accessToken = App.Repository.token.accessToken.value?.accessToken ?? "" return request(spec: spec, headers: [BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.xAuthToken(accessToken)], jsonEncodable: parameters) .subscribe(on: Self.queue) @@ -45,9 +46,9 @@ extension CameraAPIWorker { - public func createFeedPresignedURL(accessToken: String, parameters: Encodable) -> Single { + public func createFeedPresignedURL(parameters: Encodable) -> Single { let spec = CameraAPIs.uploadImageURL.spec - + let accessToken = App.Repository.token.accessToken.value?.accessToken ?? "" return request(spec: spec, headers: [BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.xAuthToken(accessToken)], jsonEncodable: parameters) .subscribe(on: Self.queue) @@ -56,8 +57,9 @@ extension CameraAPIWorker { .asSingle() } - public func editProfileImageToS3(accessToken: String, memberId: String, parameters: Encodable) -> Single { + public func editProfileImageToS3(memberId: String, parameters: Encodable) -> Single { let spec = CameraAPIs.editProfileImage(memberId).spec + let accessToken = App.Repository.token.accessToken.value?.accessToken ?? "" return request(spec: spec, headers: [BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.xAuthToken(accessToken)], jsonEncodable: parameters) .subscribe(on: Self.queue) .map(MembersProfileResponseDTO.self) @@ -65,7 +67,9 @@ extension CameraAPIWorker { .asSingle() } - public func uploadImageToPresignedURL(accessToken: String, toURL url: String, withImageData imageData: Data) -> Single { + public func uploadImageToPresignedURL(toURL url: String, withImageData imageData: Data) -> Single { + let accessToken = App.Repository.token.accessToken.value?.accessToken ?? "" + let spec = CameraAPIs.presignedURL(url).spec return upload(spec: spec, headers: [BibbiAPI.Header.xAppKey, BibbiAPI.Header.xAuthToken(accessToken)], image: imageData) .subscribe(on: Self.queue) @@ -74,9 +78,9 @@ extension CameraAPIWorker { .map { $0 } } - public func combineWithTextImageUpload(accessToken: String, parameters: Encodable, query: CameraMissionFeedQuery) -> Single { + public func combineWithTextImageUpload(parameters: Encodable, query: CameraMissionFeedQuery) -> Single { let spec = CameraAPIs.updateImage(query).spec - + let accessToken = App.Repository.token.accessToken.value?.accessToken ?? "" return request(spec: spec, headers: [BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.xAuthToken(accessToken)], jsonEncodable: parameters) .subscribe(on: Self.queue) .map(CameraDisplayPostResponseDTO.self) @@ -91,9 +95,10 @@ extension CameraAPIWorker { } - public func createRealEmojiPresignedURL(accessToken: String, parameters: Encodable) -> Single { + public func createRealEmojiPresignedURL(parameters: Encodable) -> Single { //TODO: Repository로 코드 원복 let memberId = App.Repository.member.memberID.value ?? "" + let accessToken = App.Repository.token.accessToken.value?.accessToken ?? "" let spec = CameraAPIs.uploadRealEmojiURL(memberId).spec return request(spec: spec, headers: [BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.xAuthToken(accessToken)], jsonEncodable: parameters) @@ -104,9 +109,10 @@ extension CameraAPIWorker { } - public func uploadRealEmojiImageToS3(accessToken: String, parameters: Encodable) -> Single { + public func uploadRealEmojiImageToS3(parameters: Encodable) -> Single { //TODO: Repository로 코드 원복 let memberId = App.Repository.member.memberID.value ?? "" + let accessToken = App.Repository.token.accessToken.value?.accessToken ?? "" let spec = CameraAPIs.updateRealEmojiImage(memberId).spec return request(spec: spec, headers: [BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.xAuthToken(accessToken)], jsonEncodable: parameters) @@ -116,9 +122,10 @@ extension CameraAPIWorker { .asSingle() } - public func loadRealEmojiImage(accessToken: String) -> Single { + public func loadRealEmojiImage() -> Single { //TODO: Repository로 코드 원복 let memberId = App.Repository.member.memberID.value ?? "" + let accessToken = App.Repository.token.accessToken.value?.accessToken ?? "" let spec = CameraAPIs.reloadRealEmoji(memberId).spec return request(spec: spec, headers: [BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.xAuthToken(accessToken)]) @@ -128,9 +135,10 @@ extension CameraAPIWorker { .asSingle() } - public func updateRealEmojiImage(accessToken: String, realEmojiId: String, parameters: Encodable) -> Single { + public func updateRealEmojiImage(realEmojiId: String, parameters: Encodable) -> Single { //TODO: Repository로 코드 원복 let memberId = App.Repository.member.memberID.value ?? "" + let accessToken = App.Repository.token.accessToken.value?.accessToken ?? "" let spec = CameraAPIs.modifyRealEmojiImage(memberId, realEmojiId).spec return request(spec: spec, headers: [BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.xAuthToken(accessToken)], jsonEncodable: parameters) .subscribe(on: Self.queue) @@ -139,8 +147,9 @@ extension CameraAPIWorker { .asSingle() } - public func fetchMissionItems(accessToken: String) -> Single { + public func fetchMissionItems() -> Single { let spec = CameraAPIs.fetchMissionToday.spec + let accessToken = App.Repository.token.accessToken.value?.accessToken ?? "" return request(spec: spec, headers: [BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.xAuthToken(accessToken)]) .subscribe(on: Self.queue) .map(CameraTodayMissionResponseDTO.self) diff --git a/14th-team5-iOS/Data/Sources/Trash/Camera/Repository/CameraRepository.swift b/14th-team5-iOS/Data/Sources/Trash/Camera/Repository/CameraRepository.swift index 163c709d9..f63e4dd79 100644 --- a/14th-team5-iOS/Data/Sources/Trash/Camera/Repository/CameraRepository.swift +++ b/14th-team5-iOS/Data/Sources/Trash/Camera/Repository/CameraRepository.swift @@ -20,7 +20,6 @@ public final class CameraRepository { public init() { } - public var accessToken: String = App.Repository.token.accessToken.value?.accessToken ?? "" } @@ -28,24 +27,24 @@ extension CameraRepository: CameraRepositoryProtocol { public func combineWithTextImage(parameters: CameraDisplayPostParameters, query:CameraMissionFeedQuery) -> Single { - return cameraAPIWorker.combineWithTextImageUpload(accessToken: accessToken, parameters: parameters, query: query) + return cameraAPIWorker.combineWithTextImageUpload(parameters: parameters, query: query) .map { $0?.toDomain() } .catchAndReturn(nil) } public func addPresignedeImageURL(parameters: CameraDisplayImageParameters) -> Single { - return cameraAPIWorker.createProfilePresignedURL(accessToken: accessToken, parameters: parameters) + return cameraAPIWorker.createProfilePresignedURL(parameters: parameters) .map { $0?.toDomain() } } public func uploadImageToS3(to url: String, from imageData: Data) -> Single { - return cameraAPIWorker.uploadImageToPresignedURL(accessToken: accessToken, toURL: url, withImageData: imageData) + return cameraAPIWorker.uploadImageToPresignedURL(toURL: url, withImageData: imageData) } public func editProfleImageToS3(memberId: String, parameter: ProfileImageEditParameter) -> Single { - return cameraAPIWorker.editProfileImageToS3(accessToken: accessToken, memberId: memberId, parameters: parameter) + return cameraAPIWorker.editProfileImageToS3(memberId: memberId, parameters: parameter) .do { guard let userEntity = $0?.toProfileEntity() else { return } let familyUserDefaults = FamilyInfoUserDefaults() @@ -55,27 +54,27 @@ extension CameraRepository: CameraRepositoryProtocol { } public func fetchRealEmojiImageURL(memberId: String, parameters: CameraRealEmojiParameters) -> Single { - return cameraAPIWorker.createRealEmojiPresignedURL(accessToken: accessToken, parameters: parameters) + return cameraAPIWorker.createRealEmojiPresignedURL(parameters: parameters) .map { $0?.toDomain() } } public func uploadRealEmojiImageToS3(memberId: String, parameters: CameraCreateRealEmojiParameters) -> Single { - return cameraAPIWorker.uploadRealEmojiImageToS3(accessToken: accessToken, parameters: parameters) + return cameraAPIWorker.uploadRealEmojiImageToS3(parameters: parameters) .map { $0?.toDomain() } } public func fetchRealEmojiItems() -> Single<[CameraRealEmojiImageItemEntity?]> { - return cameraAPIWorker.loadRealEmojiImage(accessToken: accessToken) + return cameraAPIWorker.loadRealEmojiImage() .map { $0?.toDomain() ?? [] } } public func updateRealEmojiImage(memberId: String, realEmojiId: String, parameters: CameraUpdateRealEmojiParameters) -> Single { - return cameraAPIWorker.updateRealEmojiImage(accessToken: accessToken, realEmojiId: realEmojiId, parameters: parameters) + return cameraAPIWorker.updateRealEmojiImage(realEmojiId: realEmojiId, parameters: parameters) .map { $0?.toDomain() } } public func fetchTodayMissionItem() -> Single { - return cameraAPIWorker.fetchMissionItems(accessToken: accessToken) + return cameraAPIWorker.fetchMissionItems() .map { $0?.toDomain() } } diff --git a/14th-team5-iOS/Domain/Sources/Trash/Camera/Interfaces/CameraRepositoryProtocol.swift b/14th-team5-iOS/Domain/Sources/Trash/Camera/Interfaces/CameraRepositoryProtocol.swift index 80ad9fa08..b54c96b41 100644 --- a/14th-team5-iOS/Domain/Sources/Trash/Camera/Interfaces/CameraRepositoryProtocol.swift +++ b/14th-team5-iOS/Domain/Sources/Trash/Camera/Interfaces/CameraRepositoryProtocol.swift @@ -67,7 +67,6 @@ public enum UploadLocation { public protocol CameraRepositoryProtocol { var disposeBag: DisposeBag { get } - var accessToken: String { get } func addPresignedeImageURL(parameters: CameraDisplayImageParameters) -> Single func uploadImageToS3(to url: String, from image: Data) -> Single func editProfleImageToS3(memberId: String, parameter: ProfileImageEditParameter) -> Single From e50bba378cbda3a01135db99dd71d89aaf14e101 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=84?= Date: Wed, 9 Oct 2024 01:15:12 +0900 Subject: [PATCH 228/263] =?UTF-8?q?fix:=20=EA=B0=80=EC=A1=B1=EB=B0=A9=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EC=84=A4=EC=A0=95=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20=EB=B0=8F=20=EC=B9=B4?= =?UTF-8?q?=EB=A9=94=EB=9D=BC=20=ED=85=8D=EC=8A=A4=ED=8A=B8=20=EC=A0=95?= =?UTF-8?q?=EB=A0=AC=20=EC=9D=B4=EC=8A=88=20=EC=88=98=EC=A0=95=ED=95=B4?= =?UTF-8?q?=EC=9A=94=20(#672)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: FamilyNameSettingViewController debounce Operator 제거 및 Layout 수정 - WebContentViewController NavigationBar Title 설정 구문 제거 - CameraDisplayViewReactor BBToast offsetY 값 수정 - CameraDisplayViewController 문구 입력시 텍스트 사라지는 이슈 수정 * fix: FamilyNameSettingViewController TextFiled AutoLayout 코드 수정 * fix: BibbMissionView, BibbInquireBannerView, BibbiProfileView, BibbiCheckBoxView 파일 위치 변겅 - BibbiFeedType 코드 위치 변경 --- .../Camera/CameraDisplayViewController.swift | 13 ++------- .../Reactor/CameraDisplayViewReactor.swift | 2 +- .../Camera/View}/BibbiMissionView.swift | 7 ++--- .../Content/WebContentViewController.swift | 7 ----- .../FamilyNameSettingViewController.swift | 6 ++--- .../View}/BibbiInquireBannerView.swift | 9 ++++--- .../Profile/View}/BibbiProfileView.swift | 27 +++++++++---------- .../Resign/View/BibbiCheckBoxView.swift} | 11 ++++---- .../BBServices/ProfileFeedGlobalState.swift | 5 ++++ 14th-team5-iOS/Core/Sources/URLTypes.swift | 7 ----- 10 files changed, 39 insertions(+), 55 deletions(-) rename 14th-team5-iOS/{Core/Sources/Bibbi/BBCommons/ShareView => App/Sources/Presentation/Camera/View}/BibbiMissionView.swift (93%) rename 14th-team5-iOS/{Core/Sources/Bibbi/BBCommons/ShareView => App/Sources/Presentation/Privacy/View}/BibbiInquireBannerView.swift (96%) rename 14th-team5-iOS/{Core/Sources/Bibbi/BBCommons/ShareView => App/Sources/Presentation/Profile/View}/BibbiProfileView.swift (87%) rename 14th-team5-iOS/{Core/Sources/Bibbi/BBCommons/ShareView/BibbiTermsView.swift => App/Sources/Presentation/Resign/View/BibbiCheckBoxView.swift} (93%) diff --git a/14th-team5-iOS/App/Sources/Presentation/Camera/CameraDisplayViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Camera/CameraDisplayViewController.swift index 9d2e26e59..86dbcdbfb 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Camera/CameraDisplayViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Camera/CameraDisplayViewController.swift @@ -256,9 +256,9 @@ public final class CameraDisplayViewController: BaseViewController 8 } .distinctUntilChanged() + .debounce(RxInterval._600milliseconds, scheduler: RxScheduler.main) .map { _ in Reactor.Action.showInputTextError } .bind(to: reactor.action) .disposed(by: disposeBag) @@ -269,18 +269,9 @@ public final class CameraDisplayViewController: BaseViewController 8 } - .debounce(RxInterval._600milliseconds, scheduler: RxScheduler.main) - .map { _ in Reactor.Action.showInputTextError } - .bind(to: reactor.action) - .disposed(by: disposeBag) - displayEditTextField.rx .text.orEmpty .filter { !$0.contains(" ")} - .debounce(RxInterval._600milliseconds, scheduler: RxScheduler.main) .scan("") { previous, new -> String in if new.count > 8 { return previous diff --git a/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraDisplayViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraDisplayViewReactor.swift index 739dbfa36..85eda3314 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraDisplayViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraDisplayViewReactor.swift @@ -141,7 +141,7 @@ public final class CameraDisplayViewReactor: Reactor { } ) case .didTapArchiveButton: - let config = BBToastConfiguration(direction: .bottom(yOffset: -360), animationTime: 1.0) + let config = BBToastConfiguration(direction: .bottom(yOffset: -20), animationTime: 1.0) let viewConfig = BBToastViewConfiguration(minWidth: 207) provider.bbToastService.show( image:DesignSystemAsset.camera.image.withTintColor(DesignSystemAsset.gray300.color), diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/ShareView/BibbiMissionView.swift b/14th-team5-iOS/App/Sources/Presentation/Camera/View/BibbiMissionView.swift similarity index 93% rename from 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/ShareView/BibbiMissionView.swift rename to 14th-team5-iOS/App/Sources/Presentation/Camera/View/BibbiMissionView.swift index 892f21503..01b12053b 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/ShareView/BibbiMissionView.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Camera/View/BibbiMissionView.swift @@ -1,17 +1,18 @@ // // BibbiMissionView.swift -// Core +// App // -// Created by Kim dohyun on 5/1/24. +// Created by Kim dohyun on 10/9/24. // import UIKit +import Core import DesignSystem import SnapKit -public final class BibbiMissionView: UIView { +final class BibbiMissionView: UIView { public let missionBadgeView: UIImageView = UIImageView() public let missionTitleView: BBLabel = BBLabel(.body2Bold, textAlignment: .center, textColor: .mainYellow) diff --git a/14th-team5-iOS/App/Sources/Presentation/Content/WebContentViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Content/WebContentViewController.swift index 59abea96c..05fc51cc1 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Content/WebContentViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Content/WebContentViewController.swift @@ -71,13 +71,6 @@ public class WebContentViewController: BaseViewController { .bind(to: webView.rx.loadURL) .disposed(by: disposeBag) - reactor.state - .compactMap { $0.url?.lastPathComponent == "privacy" ? "개인정보처리방침" : "이용 약관" } - .distinctUntilChanged() - .withUnretained(self) - .bind(onNext: { $0.0.navigationBarView.setNavigationTitle(title: $0.1) }) - .disposed(by: disposeBag) - reactor.state .map { $0.isLoading } .distinctUntilChanged() diff --git a/14th-team5-iOS/App/Sources/Presentation/Management/ViewController/FamilyNameSettingViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Management/ViewController/FamilyNameSettingViewController.swift index 9db1005bb..76211340f 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Management/ViewController/FamilyNameSettingViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Management/ViewController/FamilyNameSettingViewController.swift @@ -36,12 +36,12 @@ final class FamilyNameSettingViewController: BBNavigationViewController Date: Wed, 9 Oct 2024 19:12:39 +0900 Subject: [PATCH 229/263] =?UTF-8?q?feat:=20BBAPISpec,=20BBAPIWorker,=20BBN?= =?UTF-8?q?etworkService=20=EB=93=B1=20BBNetowrk=20=EB=AA=A8=EB=93=88=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#654)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: BBAPI, BBMethod 등 BBNetowrk 코드 구현 * feat: TokenKeychain에서 저장 타입 수정 * feat: CommentAPI 및 Worker 구현 * feat: 코드 정리 * feat: Spec에 paramters 프로퍼티 구현, BBParamter 구현 * feat: Network Error 처리 추가 구현 * feat: URLRequest 생성 로직 보강 * feat: Codable 객체를 Keychain에 저장 가능하도록 구현 * feat: Network모듈을 Network와 APIWorker 계층으로 분리 구현 * feat: SessionManager 코드 구현 및 문서 주석 작성 * delete: 필요없는 코드 삭제 * feat: Log 출력 코드 수정 * refactor: Session 관련 코드 개선 * feat: ErrorLogger 구현 * feat: 코드 리뷰 반영 --- .../App/Sources/Application/AppDelegate.swift | 1 + .../Comment/Reactor/CommentViewReactor.swift | 33 +-- .../CommentViewController.swift | 4 +- .../Core/Sources/Bibbi/BBErrorLogger.swift | 22 ++ .../Bibbi/BBNetwork/BBAPIErrorLogger.swift | 23 ++ .../Bibbi/BBNetwork/BBAPIErrorMapper.swift | 36 +++ .../Sources/Bibbi/BBNetwork/BBAPISpec.swift | 219 ++++++++++++++++++ .../Sources/Bibbi/BBNetwork/BBAPIWorker.swift | 195 ++++++++++++++++ .../Network/BBNetworkConfiguration.swift | 37 +++ .../BBNetwork/Network/BBNetworkError.swift | 112 +++++++++ .../Network/BBNetworkErrorLogger.swift | 16 ++ .../Network/BBNetworkEventMonitor.swift | 97 ++++++++ .../Network/BBNetworkInterceptor.swift | 85 +++++++ .../BBNetwork/Network/BBNetworkService.swift | 137 +++++++++++ .../Network/BBNetworkSessionManager.swift | 112 +++++++++ .../Network/Components/BBNetworkHeader.swift | 119 ++++++++++ .../Network/Components/BBNetworkMethod.swift | 41 ++++ .../Components/BBNetworkParameter.swift | 114 +++++++++ .../Bibbi/BBNetwork/Utils/BBBodyEncoder.swift | 24 ++ .../BBNetwork/Utils/BBResponseDecoder.swift | 40 ++++ .../KeychainWrapper/KeychainWrapper.swift | 45 +++- .../KeychainWrapperSubscript.swift | 16 +- .../Models/AuthToken.swift | 24 ++ .../UserDefaultsWrapper.swift | 24 ++ .../Core/Sources/Extensions/Bundle+Ext.swift | 4 + .../Core/Sources/Extensions/Data+Ext.swift | 24 ++ .../Sources/Extensions/Encodable+Ext.swift | 31 +++ .../Modifiers/Shimmer.swift | 0 .../Extensions/ObservableType+Ext.swift | 86 +++++++ .../{Bibbi => Trash}/BBNetwork/API.swift | 4 +- .../BBNetwork/APIConfig.swift | 1 + .../{Bibbi => Trash}/BBNetwork/APISpec.swift | 7 + .../Repositories/Token/TokenRepository.swift | 2 + .../Sources/{Utilities => Utils}/Haptic.swift | 0 .../Mixpanel/MixPanelService+Account.swift | 0 .../Mixpanel/MixPanelService+Camera.swift | 0 .../Mixpanel/MixPanelService+Family.swift | 0 .../Mixpanel/MixPanelService+Home.swift | 0 .../Mixpanel/MixpanelService.swift | 0 .../MulticastDelegate.swift | 0 .../Rx => Utils/Reactive}/RxInterval.swift | 0 .../Rx => Utils/Reactive}/RxObject.swift | 0 .../Rx => Utils/Reactive}/RxScheduler.swift | 0 .../Comment/CommentAPI/CommentAPIWorker.swift | 68 ++---- .../APIs/Comment/CommentAPI/CommentAPIs.swift | 43 +++- .../Repository/CommentRepository.swift | 14 +- .../OAuthAPI/Repository/OAuthRepository.swift | 16 +- .../TokenKeychain/Models/OldAccessToken.swift | 8 - .../TokenKeychain/TokenKeychain.swift | 87 ++----- .../Sources/{APIs => Trash}/APIWorker.swift | 3 +- .../{APIs => Trash}/BibbiNetworkMonitor.swift | 0 .../{APIs => Trash}/NetworkIntercepter.swift | 1 + .../Repositories/CommentRepository.swift | 2 +- .../UseCase/PostCommentUseCase.swift | 4 +- .../Comment/FetchCommentUseCase.swift | 4 +- 55 files changed, 1806 insertions(+), 179 deletions(-) create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBErrorLogger.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/BBAPIErrorLogger.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/BBAPIErrorMapper.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/BBAPISpec.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/BBAPIWorker.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/BBNetworkConfiguration.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/BBNetworkError.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/BBNetworkErrorLogger.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/BBNetworkEventMonitor.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/BBNetworkInterceptor.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/BBNetworkService.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/BBNetworkSessionManager.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/Components/BBNetworkHeader.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/Components/BBNetworkMethod.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/Components/BBNetworkParameter.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Utils/BBBodyEncoder.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Utils/BBResponseDecoder.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBStorages/UserDefaultsWrapper/Models/AuthToken.swift create mode 100644 14th-team5-iOS/Core/Sources/Extensions/Data+Ext.swift create mode 100644 14th-team5-iOS/Core/Sources/Extensions/Encodable+Ext.swift rename 14th-team5-iOS/Core/Sources/{Utilities => Extensions}/Modifiers/Shimmer.swift (100%) create mode 100644 14th-team5-iOS/Core/Sources/Extensions/ObservableType+Ext.swift rename 14th-team5-iOS/Core/Sources/{Bibbi => Trash}/BBNetwork/API.swift (97%) rename 14th-team5-iOS/Core/Sources/{Bibbi => Trash}/BBNetwork/APIConfig.swift (86%) rename 14th-team5-iOS/Core/Sources/{Bibbi => Trash}/BBNetwork/APISpec.swift (86%) rename 14th-team5-iOS/Core/Sources/{Utilities => Utils}/Haptic.swift (100%) rename 14th-team5-iOS/Core/Sources/{Utilities/Analytics => Utils}/Mixpanel/MixPanelService+Account.swift (100%) rename 14th-team5-iOS/Core/Sources/{Utilities/Analytics => Utils}/Mixpanel/MixPanelService+Camera.swift (100%) rename 14th-team5-iOS/Core/Sources/{Utilities/Analytics => Utils}/Mixpanel/MixPanelService+Family.swift (100%) rename 14th-team5-iOS/Core/Sources/{Utilities/Analytics => Utils}/Mixpanel/MixPanelService+Home.swift (100%) rename 14th-team5-iOS/Core/Sources/{Utilities/Analytics => Utils}/Mixpanel/MixpanelService.swift (100%) rename 14th-team5-iOS/Core/Sources/{Utilities => Utils}/MulticastDelegate.swift (100%) rename 14th-team5-iOS/Core/Sources/{Utilities/Rx => Utils/Reactive}/RxInterval.swift (100%) rename 14th-team5-iOS/Core/Sources/{Utilities/Rx => Utils/Reactive}/RxObject.swift (100%) rename 14th-team5-iOS/Core/Sources/{Utilities/Rx => Utils/Reactive}/RxScheduler.swift (100%) delete mode 100644 14th-team5-iOS/Data/Sources/Storages/Keychain/TokenKeychain/Models/OldAccessToken.swift rename 14th-team5-iOS/Data/Sources/{APIs => Trash}/APIWorker.swift (98%) rename 14th-team5-iOS/Data/Sources/{APIs => Trash}/BibbiNetworkMonitor.swift (100%) rename 14th-team5-iOS/Data/Sources/{APIs => Trash}/NetworkIntercepter.swift (98%) 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/Presentation/Comment/Reactor/CommentViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Comment/Reactor/CommentViewReactor.swift index 221c7b27f..164d85405 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Comment/Reactor/CommentViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Comment/Reactor/CommentViewReactor.swift @@ -6,6 +6,7 @@ // import Core +import Data import Domain import Foundation @@ -109,22 +110,9 @@ final public class CommentViewReactor: Reactor { Observable.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/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..aafcb5cc2 --- /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: 이 인스턴스가 사용하기를 원하는 `APIErrorLogger`입니다. 기본값은 `APIDefaultErrorLogger()`입니다. + 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..b1efebe16 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/BBNetworkEventMonitor.swift @@ -0,0 +1,97 @@ +// +// 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") + } + + // TODO: - Logger로 로그 출력하기 + print(httpLog + "\n--------------------------------------------------------\n") + } + + 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") + + // TODO: - Logger로 로그 출력하기 + print(httpLog + "\n--------------------------------------------------------\n") + } + + 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..21b658e7e 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 { + "7c5aaa36-570e-491f-b18a-26a1a0b72959" + } } 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 97% rename from 14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/API.swift rename to 14th-team5-iOS/Core/Sources/Trash/BBNetwork/API.swift index da83f68b9..2eff6e07e 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 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/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/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/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/APIs/APIWorker.swift b/14th-team5-iOS/Data/Sources/Trash/APIWorker.swift similarity index 98% rename from 14th-team5-iOS/Data/Sources/APIs/APIWorker.swift rename to 14th-team5-iOS/Data/Sources/Trash/APIWorker.swift index ddf34b3c2..6007004fe 100644 --- a/14th-team5-iOS/Data/Sources/APIs/APIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Trash/APIWorker.swift @@ -16,6 +16,7 @@ import RxSwift // MARK: - Base API Worker +@available(*, deprecated, renamed: "BBAPIWorker") public class APIWorker: NSObject { // MARK: - Identifier @@ -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/BibbiNetworkMonitor.swift b/14th-team5-iOS/Data/Sources/Trash/BibbiNetworkMonitor.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/APIs/BibbiNetworkMonitor.swift rename to 14th-team5-iOS/Data/Sources/Trash/BibbiNetworkMonitor.swift 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/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/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) } From 70edc2ea5e20fa543eccda87416d9e6ee3f1ccbf Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Fri, 11 Oct 2024 11:21:27 +0900 Subject: [PATCH 230/263] =?UTF-8?q?fix:=20=EA=B0=80=EC=A1=B1=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=EC=9D=B4=20=EA=B3=B5=EB=B0=B1=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=9C=A8=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?(#674)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Management/Reactor/FamilyNameSettingViewReactor.swift | 4 ++-- .../Management/Reactor/ManagementReactor.swift | 5 ++--- .../Core/Sources/Bibbi/BBNetwork/BBAPIWorker.swift | 2 +- .../DataMapping/FamilyGroupInfoResponseDTO.swift | 4 ++-- .../Sources/Entities/Family/FamilyGroupInfoEntity.swift | 8 ++++---- 5 files changed, 11 insertions(+), 12 deletions(-) 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/BBNetwork/BBAPIWorker.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/BBAPIWorker.swift index aafcb5cc2..9e83fbebd 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/BBAPIWorker.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/BBAPIWorker.swift @@ -96,7 +96,7 @@ open class BBRxAPIWorker { /// - Parameters: /// - service: 이 인스턴스가 사용하기를 원하는 `NetworkService`입니다. 기본값은 `BBNetworkDefaultService()`입니다. /// - errorResolver: 이 인스턴스가 사용하기를 원하는 `APIErrorMapper`입니다. 기본값은 `APIDefaultErrorMapper()`입니다. - /// - errorLogger: 이 인스턴스가 사용하기를 원하는 `APIErrorLogger`입니다. 기본값은 `APIDefaultErrorLogger()`입니다. + /// - errorLogger: 이 인스턴스가 사용하기를 원하는 `BBErrorLogger`입니다. 기본값은 `APIWorkerErrorLogger()`입니다. public init( with service: any BBNetworkService = BBNetworkDefaultService(), errorMapper: any APIErrorMapper = APIDefaultErrorMapper(), 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/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 From 1f53d8244849287d01a66291f8229d4995d7e158 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Tue, 15 Oct 2024 23:39:16 +0900 Subject: [PATCH 231/263] =?UTF-8?q?fix:=20BBAlert=20=EB=B2=84=ED=8A=BC=20?= =?UTF-8?q?=ED=8C=A8=EB=94=A9=20=EC=88=98=EC=A0=95=20(#676)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AlertViews/DefaultToastView/DefaultAlertView.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 { From 83ffe00549eb54dbcc2213bd0b8882f6f6113ff6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=84?= Date: Thu, 17 Oct 2024 00:49:58 +0900 Subject: [PATCH 232/263] =?UTF-8?q?feat:=20Tuist=20CFBundleShortVersionStr?= =?UTF-8?q?ing=201.2.3=EC=9C=BC=EB=A1=9C=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=20(#678)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Bundle+Ext xAppKey 값 수정 - BBNetwork Header xAppKey 값 수정 --- 14th-team5-iOS/App/Project.swift | 2 +- 14th-team5-iOS/Core/Sources/Extensions/Bundle+Ext.swift | 2 +- 14th-team5-iOS/Core/Sources/Trash/BBNetwork/API.swift | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/14th-team5-iOS/App/Project.swift b/14th-team5-iOS/App/Project.swift index 32d643c8f..91e141b03 100644 --- a/14th-team5-iOS/App/Project.swift +++ b/14th-team5-iOS/App/Project.swift @@ -19,7 +19,7 @@ private let targets: [Target] = [ "CFBundleDisplayName": .string("Bibbi"), "CFBundleVersion": .string("1"), "CFBuildVersion": .string("0"), - "CFBundleShortVersionString": .string("1.2.2"), + "CFBundleShortVersionString": .string("1.2.3"), "UILaunchStoryboardName": .string("LaunchScreen.storyboard"), "UISupportedInterfaceOrientations": .array([.string("UIInterfaceOrientationPortrait")]), "UIUserInterfaceStyle": .string("Dark"), diff --git a/14th-team5-iOS/Core/Sources/Extensions/Bundle+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/Bundle+Ext.swift index 21b658e7e..a814974a3 100644 --- a/14th-team5-iOS/Core/Sources/Extensions/Bundle+Ext.swift +++ b/14th-team5-iOS/Core/Sources/Extensions/Bundle+Ext.swift @@ -65,6 +65,6 @@ extension Bundle { } public var xAppKey: String { - "7c5aaa36-570e-491f-b18a-26a1a0b72959" + "db3ca026-0f9c-415a-a250-c97807f54add" } } diff --git a/14th-team5-iOS/Core/Sources/Trash/BBNetwork/API.swift b/14th-team5-iOS/Core/Sources/Trash/BBNetwork/API.swift index 2eff6e07e..26bc95141 100644 --- a/14th-team5-iOS/Core/Sources/Trash/BBNetwork/API.swift +++ b/14th-team5-iOS/Core/Sources/Trash/BBNetwork/API.swift @@ -53,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" From 51b167fdebb0bdfbf5cb1983607f9730e0f04b86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=84?= Date: Thu, 17 Oct 2024 21:56:38 +0900 Subject: [PATCH 233/263] =?UTF-8?q?feat:=20BibbiNetworkMonitor=20Legacy=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=A0=9C=EA=B1=B0=20(#681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: BibbiNetworkMonitor Legacy 코드 제거 - BBNetworkEventMonitor BBLogger 출력구문 추가 - BBLogger 내부 Logger Method 추가 - BBLogger log ObservableType 추가 * comment: BBLogger 주석 추가 * fix: github Action github_action_stg_upload_testflight, github_action_prd_upload_testflight 내부 app_store_build_number 메서드 추가 * fix: Tuist UILaunchStoryboardName 런치스크린 확장자 제거 * fix: Tuist destination iPhone으로 설정 - Marketing Version 1.2.3으로 수정 --- 14th-team5-iOS/App/Project.swift | 2 +- .../Network/BBNetworkEventMonitor.swift | 6 +- .../Core/Sources/Utils/Logger/BBLogger.swift | 187 ++++++++++++++++++ .../Data/Sources/Trash/APIWorker.swift | 2 +- .../Sources/Trash/BibbiNetworkMonitor.swift | 39 ---- .../Modular+Templates.swift | 2 +- .../Project+Templates.swift | 2 +- fastlane/Fastfile | 16 +- 8 files changed, 205 insertions(+), 51 deletions(-) create mode 100644 14th-team5-iOS/Core/Sources/Utils/Logger/BBLogger.swift delete mode 100644 14th-team5-iOS/Data/Sources/Trash/BibbiNetworkMonitor.swift diff --git a/14th-team5-iOS/App/Project.swift b/14th-team5-iOS/App/Project.swift index 91e141b03..cb83f420b 100644 --- a/14th-team5-iOS/App/Project.swift +++ b/14th-team5-iOS/App/Project.swift @@ -20,7 +20,7 @@ private let targets: [Target] = [ "CFBundleVersion": .string("1"), "CFBuildVersion": .string("0"), "CFBundleShortVersionString": .string("1.2.3"), - "UILaunchStoryboardName": .string("LaunchScreen.storyboard"), + "UILaunchStoryboardName": .string("LaunchScreen"), "UISupportedInterfaceOrientations": .array([.string("UIInterfaceOrientationPortrait")]), "UIUserInterfaceStyle": .string("Dark"), "NSPhotoLibraryAddUsageDescription" : .string("프로필 사진, 피드 업로드를 위한 사진 촬영을 위해 Bibbi가 앨범에 접근할 수 있도록 허용해 주세요"), diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/BBNetworkEventMonitor.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/BBNetworkEventMonitor.swift index b1efebe16..02a4b26cd 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/BBNetworkEventMonitor.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/BBNetworkEventMonitor.swift @@ -54,8 +54,7 @@ extension BBNetworkDefaultLogger: BBNetworkEventMonitor { httpLog.append("- HTTP BODY: \(httpBody)\n") } - // TODO: - Logger로 로그 출력하기 - print(httpLog + "\n--------------------------------------------------------\n") + BBLogger.logInfo(category: "Network", message: httpLog) } public func request( @@ -84,8 +83,7 @@ extension BBNetworkDefaultLogger: BBNetworkEventMonitor { httpLog.append("- STATUS CODE: \(statusCode)\n") httpLog.append("- RESONSE DATA: \(responseDataString)\n") - // TODO: - Logger로 로그 출력하기 - print(httpLog + "\n--------------------------------------------------------\n") + BBLogger.logInfo(category: "Network", message: httpLog) } public func request( 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/Data/Sources/Trash/APIWorker.swift b/14th-team5-iOS/Data/Sources/Trash/APIWorker.swift index 6007004fe..3fa32bcd8 100644 --- a/14th-team5-iOS/Data/Sources/Trash/APIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Trash/APIWorker.swift @@ -23,7 +23,7 @@ public class APIWorker: NSObject { 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( diff --git a/14th-team5-iOS/Data/Sources/Trash/BibbiNetworkMonitor.swift b/14th-team5-iOS/Data/Sources/Trash/BibbiNetworkMonitor.swift deleted file mode 100644 index f2e587b9a..000000000 --- a/14th-team5-iOS/Data/Sources/Trash/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/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..a44854c47 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,11 @@ 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 + ) + + new_build_number = build_num + 1 increment_build_number( @@ -125,7 +129,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 +148,11 @@ 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 + ) + + new_build_number = build_num + 1 increment_build_number( From 9ad25a11399cd533a51af2e14b317f739ff075fd Mon Sep 17 00:00:00 2001 From: g_mi <62610032+akrudal@users.noreply.github.com> Date: Thu, 17 Oct 2024 22:07:42 +0900 Subject: [PATCH 234/263] =?UTF-8?q?fix:=20Home=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EB=B0=9C=EC=83=9D=ED=95=9C=20=EC=9D=B4=EC=8A=88=EB=93=A4?= =?UTF-8?q?=EC=9D=84=20=EC=88=98=EC=A0=95=ED=95=B4=EC=9A=94.=20(#679)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: navigator input family link(#668) * fix: home issues(#668) * rename files(#668) * rename propertieis(#668) --- .../DIContainer/AppDIContainer.swift | 28 ++++++++++--- .../DIContainer/NavigatorDIContainer.swift | 8 +++- .../Navigator/InputFamilyLinkNavigator.swift | 38 +++++++++++++++++ .../Application/Navigator/MainNavigator.swift | 10 ++++- .../FamilyEntranceControllerWrapper.swift | 0 ...InputFamilyLinkViewControllerWrapper.swift | 0 .../JoinFamilyViewControllerWrapper.swift | 0 .../AccountSignInViewController.swift | 2 +- .../Reactor/InputFamilyLinkReactor.swift | 23 ++++------ .../InputFamilyLinkViewController.swift | 42 +------------------ .../Home/Reactor/MainViewReactor.swift | 30 +++++++++---- .../ViewControllers/MainViewController.swift | 7 +++- .../APIs/App/Repository/AppRepository.swift | 11 ++++- .../AppUserDefaults/AppUserDefaults.swift | 20 ++------- .../Sources/Repositories/AppRepository.swift | 5 ++- .../App/FetchFamilyManagementUseCase.swift | 8 ++-- .../App/IsFirstWidgetAlertUseCase.swift | 27 ++++++++++++ ... SaveIsFirstFamilyManagementUseCase.swift} | 4 +- .../App/SaveIsFirstWidgetAlertUseCase.swift | 27 ++++++++++++ .../MainView/CheckSurvivalTimeUseCase.swift | 35 ++++++++++++++++ 20 files changed, 226 insertions(+), 99 deletions(-) create mode 100644 14th-team5-iOS/App/Sources/Application/Navigator/InputFamilyLinkNavigator.swift rename 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/{JoinFamily => FamilyEntrance}/FamilyEntranceControllerWrapper.swift (100%) rename 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/{JoinFamily => FamilyEntrance}/InputFamilyLinkViewControllerWrapper.swift (100%) rename 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/{JoinFamily => FamilyEntrance}/JoinFamilyViewControllerWrapper.swift (100%) create mode 100644 14th-team5-iOS/Domain/Sources/UseCases/App/IsFirstWidgetAlertUseCase.swift rename 14th-team5-iOS/Domain/Sources/UseCases/App/{UpdateFamilyManagementUseCase.swift => SaveIsFirstFamilyManagementUseCase.swift} (74%) create mode 100644 14th-team5-iOS/Domain/Sources/UseCases/App/SaveIsFirstWidgetAlertUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/UseCases/MainView/CheckSurvivalTimeUseCase.swift 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(.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/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/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/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/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/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) + } +} From 5fb21d56cc5c3f03068900700e381de2fabb35d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=84?= Date: Fri, 18 Oct 2024 22:17:03 +0900 Subject: [PATCH 235/263] =?UTF-8?q?chore:=20github=20Action=20=EB=B8=8C?= =?UTF-8?q?=EB=9F=B0=EC=B9=98=20=EC=A0=9C=EC=95=BD=EA=B5=AC=EB=AC=B8=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=20=EB=B0=8F=20=EC=BD=94=EB=93=9C=20=EA=B0=84?= =?UTF-8?q?=EC=86=8C=ED=99=94=20(#685)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: github Action 브런치 제약구문 제거 및 코드 간소화 - branches event trigger branch 네이밍 release로 수정 - type event trigger 추가 및 closed 정의 * fix: fastlane upload prd testflight action 제약 구문 수정 - fastlane upload stg testflight action 코드 리펙토링 - branches event trigger release 추가 - types event trigger 제거 --- .github/workflows/swift.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index e0dd576c8..6696274ae 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -3,11 +3,10 @@ name: Bibbi on: push: branches: - - feat/* - - fix/* + - release pull_request: branches: - - release/** + - release - develop jobs: @@ -61,7 +60,7 @@ jobs: run: tuist generate - name: fastlane upload_prd_testflight - if: github.event.pull_request.base.ref == 'release' && github.head_ref == 'develop' + if: ${{ github.ref == 'refs/heads/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 +81,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 }} From 2c9999aad07b9af33930a7632e7e008c22f70384 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=84?= Date: Sat, 19 Oct 2024 00:11:45 +0900 Subject: [PATCH 236/263] =?UTF-8?q?fix:=20types=20event=20trigger=20closed?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=20(#687)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fastlane upload_prd_testflight if 구문 수정 --- .github/workflows/swift.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 6696274ae..53c94f7c6 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -8,6 +8,8 @@ on: branches: - release - develop + types: + - closed jobs: build: @@ -60,7 +62,7 @@ jobs: run: tuist generate - name: fastlane upload_prd_testflight - if: ${{ github.ref == 'refs/heads/release' && github.event_name == 'push' }} + 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 }} From 763efbf5b852a22708ce4bd975ce09aa2cd2b82f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=84?= Date: Sat, 19 Oct 2024 19:38:39 +0900 Subject: [PATCH 237/263] =?UTF-8?q?fix:=20app=5Fstore=5Fbuild=5Fnumber=20?= =?UTF-8?q?=EC=9D=B4=EB=AF=B8=20=EB=B0=B0=ED=8F=AC=EB=90=9C=20=EB=B9=8C?= =?UTF-8?q?=EB=93=9C=20=EB=84=98=EB=B2=84=20=EA=B8=B0=EC=A4=80=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EA=B0=80=EC=A0=B8=EC=98=A4=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EA=B2=8C=20=EC=88=98=EC=A0=95=20(#689)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastlane/Fastfile | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index a44854c47..48f224b90 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -63,7 +63,8 @@ platform :ios do ) build_num = app_store_build_number( - api_key: api_key + api_key: api_key, + live: false ) new_build_number = build_num + 1 @@ -149,7 +150,8 @@ platform :ios do ) build_num = app_store_build_number( - api_key: api_key + api_key: api_key, + live: false ) new_build_number = build_num + 1 From d9d21767be39e1b9d684e6aed2f1072515cf7cef Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Mon, 21 Oct 2024 12:42:41 +0900 Subject: [PATCH 238/263] =?UTF-8?q?refactor:=20Calendar=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EB=B7=B0=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC,?= =?UTF-8?q?=20=EB=A6=AC=EC=95=A1=ED=84=B0=20=EB=93=B1=20=EB=B6=80=EA=B0=80?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20?= =?UTF-8?q?(#682)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: CommentAPIs 코드 마이그레이션 * refacto: MemoriesCalendarCell 리팩토링 * refactor: MemoriesCalendarCell 코드 리팩토링 * refactor: DailyCalendarViewReactor 리팩토링 * refactor: MemoriesCalendarPostCell 리팩토링 * refactor: MemoriesCalendarPostCellReactor 리팩토링 * refactor: 일부 변수 및 메서드 이름 수정 --- .../DIContainer/CalendarDIContainer.swift | 15 - .../Navigator/DailyCalendarNavigator.swift | 8 +- .../DailyCalendarViewControllerWrapper.swift | 2 +- .../Config/CalendarImageCell+Type.swift | 13 + .../DailyCalendarSectionModel.swift | 0 .../MonthlyCalendarSectionModel.swift | 0 .../MemoriesCalendarPostHeaderDelegate.swift | 12 + .../RxFSCalendarDelegateProxy.swift | 23 +- .../RxMemoriesCalendarPostDelegateProxy.swift | 38 ++ .../Reactor/CalendarImageCellReactor.swift | 104 ----- .../Reactor/CalendarPageViewCellReactor.swift | 125 ------ .../Reactor/CalendarPostCellReactor.swift | 111 ----- .../Cell/MemoriesCalendarCellReactor.swift | 108 +++++ .../Cell/MemoriesCalendarPageReactor.swift | 129 ++++++ .../MemoriesCalendarPostCellReactor.swift | 126 ++++++ .../Reactor/DailyCalendarViewReactor.swift | 297 +++++++------ .../MemoriesCalendarPostHeaderReactor.swift | 51 +++ .../MemoriesCalendarPostImageReactor.swift | 29 ++ .../MemoriesCalendarTitleViewReactor.swift | 64 +++ .../Reactor/MonthlyCalendarViewReactor.swift | 137 +++--- .../Calendar/Strings/CalenderStrings.swift | 19 - .../Calendar/View/BannerView.swift | 15 +- .../Calendar/View/Cell/CalendarCell.swift | 263 ----------- .../Calendar/View/Cell/CalendarPostCell.swift | 268 ------------ ...eCell.swift => MemoriesCalendarCell.swift} | 141 +++--- .../Cell/MemoriesCalendarPageViewCell.swift | 226 ++++++++++ ... => MemoriesCalendarPlaceholderCell.swift} | 8 +- .../View/Cell/MemoriesCalendarPostCell.swift | 208 +++++++++ .../View/MemoriesCalendarPageTitleView.swift | 125 ++++++ .../View/MemoriesCalendarPostHeaderView.swift | 131 ++++++ .../View/MemoriesCalendarPostImageView.swift | 79 ++++ .../BannerHostingViewController.swift | 36 ++ .../DailyCalendarViewController.swift | 407 ++++++------------ .../MonthlyCalendarViewController.swift | 180 ++------ .../ViewModel/BannerViewModel.swift | 7 +- .../Reactor/Cell/CommentCellReactor.swift | 9 +- .../Comment/View/Cell/CommentCell.swift | 3 +- .../PostDetail/Views/MissionTextView.swift | 15 + .../DIContainer/CalendarCellDIContainer.swift | 4 +- .../CalendarImageCellDIContainer.swift | 16 +- .../CalendarPostCellDIContainer.swift | 6 +- .../DIContainer/CalendarPostDIContainer.swift | 2 +- .../MonthlyCalendarDIConatainer.swift | 0 .../BBServices/CalendarGlobalState.swift | 48 --- .../Bibbi/BBServices/CalendarService.swift | 52 +++ .../Bibbi/BBServices/PostGlobalState.swift | 4 +- .../Bibbi/BBServices/ServiceProvider.swift | 6 +- .../UserDefaultsWrapper.swift | 4 +- .../Extensions/ObservableType+Ext.swift | 10 + .../Sources/Extensions/Reactive+Ext.swift | 29 +- .../Core/Sources/Extensions/String+Ext.swift | 4 + .../Extensions/UICollectionView+Ext.swift | 27 ++ .../Core/Sources/Extensions/UIView+Ext.swift | 22 +- .../CalendarAPI/CalendarAPIWorker.swift | 68 +-- .../Calendar/CalendarAPI/CalendarAPIs.swift | 44 +- ...rrayResponseDailyCalendarResponseDTO.swift | 4 +- .../Repository/CalendarRepository.swift | 42 +- .../Family/Repository/FamilyRepository.swift | 28 +- .../ArrayResponseDailyCalendarEntity.swift | 8 +- .../Repositories/CalendarRepository.swift | 11 +- .../Calendar/UseCases/CalendarUseCase.swift | 20 +- .../Calendar/FetchCalendarBannerUseCase.swift | 6 +- .../Calendar/FetchDailyCalendarUseCase.swift | 4 +- .../FetchMonthlyCalendarUseCase.swift | 4 +- .../FetchStatisticsSummaryUseCase.swift | 4 +- .../My/FetchProfileImageUrlUseCase.swift | 13 +- .../UseCases/My/FetchUserNameUseCase.swift | 10 +- 67 files changed, 2146 insertions(+), 1886 deletions(-) create mode 100644 14th-team5-iOS/App/Sources/Presentation/Calendar/Config/CalendarImageCell+Type.swift rename 14th-team5-iOS/App/Sources/Presentation/Calendar/{ => Reactive}/DataSource/DailyCalendarSectionModel.swift (100%) rename 14th-team5-iOS/App/Sources/Presentation/Calendar/{ => Reactive}/DataSource/MonthlyCalendarSectionModel.swift (100%) create mode 100644 14th-team5-iOS/App/Sources/Presentation/Calendar/Reactive/DelegateProxy/Delegate/MemoriesCalendarPostHeaderDelegate.swift rename 14th-team5-iOS/App/Sources/Presentation/Calendar/{ => Reactive}/DelegateProxy/RxFSCalendarDelegateProxy.swift (57%) create mode 100644 14th-team5-iOS/App/Sources/Presentation/Calendar/Reactive/DelegateProxy/RxMemoriesCalendarPostDelegateProxy.swift delete mode 100644 14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarImageCellReactor.swift delete mode 100644 14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPageViewCellReactor.swift delete mode 100644 14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPostCellReactor.swift create mode 100644 14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/Cell/MemoriesCalendarCellReactor.swift create mode 100644 14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/Cell/MemoriesCalendarPageReactor.swift create mode 100644 14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/Cell/MemoriesCalendarPostCellReactor.swift create mode 100644 14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/MemoriesCalendarPostHeaderReactor.swift create mode 100644 14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/MemoriesCalendarPostImageReactor.swift create mode 100644 14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/MemoriesCalendarTitleViewReactor.swift delete mode 100644 14th-team5-iOS/App/Sources/Presentation/Calendar/Strings/CalenderStrings.swift delete mode 100644 14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarCell.swift delete mode 100644 14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarPostCell.swift rename 14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/{CalendarImageCell.swift => MemoriesCalendarCell.swift} (52%) create mode 100644 14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/MemoriesCalendarPageViewCell.swift rename 14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/{CalendarPlaceholderCell.swift => MemoriesCalendarPlaceholderCell.swift} (88%) create mode 100644 14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/MemoriesCalendarPostCell.swift create mode 100644 14th-team5-iOS/App/Sources/Presentation/Calendar/View/MemoriesCalendarPageTitleView.swift create mode 100644 14th-team5-iOS/App/Sources/Presentation/Calendar/View/MemoriesCalendarPostHeaderView.swift create mode 100644 14th-team5-iOS/App/Sources/Presentation/Calendar/View/MemoriesCalendarPostImageView.swift create mode 100644 14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/BannerHostingViewController.swift rename 14th-team5-iOS/App/Sources/Presentation/Calendar/{Reactor => }/ViewModel/BannerViewModel.swift (77%) rename 14th-team5-iOS/App/Sources/Presentation/{Calendar => Trash}/DIContainer/CalendarCellDIContainer.swift (86%) rename 14th-team5-iOS/App/Sources/Presentation/{Calendar => Trash}/DIContainer/CalendarImageCellDIContainer.swift (69%) rename 14th-team5-iOS/App/Sources/Presentation/{Calendar => Trash}/DIContainer/CalendarPostCellDIContainer.swift (81%) rename 14th-team5-iOS/App/Sources/Presentation/{Calendar => Trash}/DIContainer/CalendarPostDIContainer.swift (97%) rename 14th-team5-iOS/App/Sources/Presentation/{Calendar => Trash}/DIContainer/MonthlyCalendarDIConatainer.swift (100%) delete mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBServices/CalendarGlobalState.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBServices/CalendarService.swift create mode 100644 14th-team5-iOS/Core/Sources/Extensions/UICollectionView+Ext.swift diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/CalendarDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/CalendarDIContainer.swift index d7ed8b11a..cf20569d3 100644 --- a/14th-team5-iOS/App/Sources/Application/DIContainer/CalendarDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/CalendarDIContainer.swift @@ -37,14 +37,6 @@ final class CalendarDIContainer: BaseContainer { ) } - // Deprecated - private func makeOldCalendarUseCase() -> CalendarUseCaseProtocol { - CalendarUseCase( - calendarRepository: makeCalendarRepository() - ) - } - - // MARK: - Make Repository @@ -71,13 +63,6 @@ final class CalendarDIContainer: BaseContainer { container.register(type: FetchMonthlyCalendarUseCaseProtocol.self) { _ in self.makeFetchMonthlyCalendarUseCase() } - - - // Deprecated - container.register(type: CalendarUseCaseProtocol.self) { _ in - self.makeOldCalendarUseCase() - } - } diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/DailyCalendarNavigator.swift b/14th-team5-iOS/App/Sources/Application/Navigator/DailyCalendarNavigator.swift index ec6e8673d..68a06fb40 100644 --- a/14th-team5-iOS/App/Sources/Application/Navigator/DailyCalendarNavigator.swift +++ b/14th-team5-iOS/App/Sources/Application/Navigator/DailyCalendarNavigator.swift @@ -11,6 +11,7 @@ import UIKit protocol DailyCalendarNavigatorProtocol: BaseNavigator { func toProfile(memberId: String) func toComment(postId: String) + func backToMonthly() } final class DailyCalendarNavigator: DailyCalendarNavigatorProtocol { @@ -35,7 +36,12 @@ final class DailyCalendarNavigator: DailyCalendarNavigatorProtocol { func toComment(postId: String) { let vc = CommentViewControllerWrapper(postId: postId).viewController navigationController.presentPostCommentSheet(vc, from: .calendar) - // TODO: - present 메서드 수정하기 + } + + // MARK: - Back + + func backToMonthly() { + navigationController.popViewController(animated: true) } } diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Calendar/DailyCalendarViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Calendar/DailyCalendarViewControllerWrapper.swift index ee634316e..5b9a5ab37 100644 --- a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Calendar/DailyCalendarViewControllerWrapper.swift +++ b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Calendar/DailyCalendarViewControllerWrapper.swift @@ -32,7 +32,7 @@ final class DailyCalendarViewControllerWrapper { func makeReactor() -> DailyCalendarViewReactor { DailyCalendarViewReactor( - date: date, + initialSelection: date, notificationDeepLink: link ) } diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Config/CalendarImageCell+Type.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Config/CalendarImageCell+Type.swift new file mode 100644 index 000000000..e782a3b25 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/Config/CalendarImageCell+Type.swift @@ -0,0 +1,13 @@ +// +// CalendarType.swift +// App +// +// Created by 김건우 on 10/16/24. +// + +import Foundation + +public enum MomoriesCalendarType { + case daily + case month +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/DataSource/DailyCalendarSectionModel.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactive/DataSource/DailyCalendarSectionModel.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Calendar/DataSource/DailyCalendarSectionModel.swift rename to 14th-team5-iOS/App/Sources/Presentation/Calendar/Reactive/DataSource/DailyCalendarSectionModel.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/DataSource/MonthlyCalendarSectionModel.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactive/DataSource/MonthlyCalendarSectionModel.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Calendar/DataSource/MonthlyCalendarSectionModel.swift rename to 14th-team5-iOS/App/Sources/Presentation/Calendar/Reactive/DataSource/MonthlyCalendarSectionModel.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactive/DelegateProxy/Delegate/MemoriesCalendarPostHeaderDelegate.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactive/DelegateProxy/Delegate/MemoriesCalendarPostHeaderDelegate.swift new file mode 100644 index 000000000..9c786ddde --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactive/DelegateProxy/Delegate/MemoriesCalendarPostHeaderDelegate.swift @@ -0,0 +1,12 @@ +// +// File.swift +// App +// +// Created by 김건우 on 10/18/24. +// + +import UIKit + +@objc protocol MemoriesCalendarPostHeaderDelegate: AnyObject { + @objc optional func didTapProfileImageButton(_ button: UIButton, event: UIButton.Event) +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/DelegateProxy/RxFSCalendarDelegateProxy.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactive/DelegateProxy/RxFSCalendarDelegateProxy.swift similarity index 57% rename from 14th-team5-iOS/App/Sources/Presentation/Calendar/DelegateProxy/RxFSCalendarDelegateProxy.swift rename to 14th-team5-iOS/App/Sources/Presentation/Calendar/Reactive/DelegateProxy/RxFSCalendarDelegateProxy.swift index cccb1df8a..b8e8c68a6 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/DelegateProxy/RxFSCalendarDelegateProxy.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactive/DelegateProxy/RxFSCalendarDelegateProxy.swift @@ -5,9 +5,9 @@ // Created by 김건우 on 12/9/23. // +import Core import Foundation -import Core import FSCalendar import RxSwift import RxCocoa @@ -25,40 +25,27 @@ extension FSCalendar: HasDelegate { } extension Reactive where Base: FSCalendar { + var delegate: DelegateProxy { return RxFSCalendarDelegateProxy.proxy(for: self.base) } + /// 캘린더에서 셀을 선택하면 Date가 담긴 스트림이 흐릅니다. var didSelect: Observable { return delegate.methodInvoked(#selector(FSCalendarDelegate.calendar(_:didSelect:at:))) - .debug("calendar(_:didSelect:at:) 메서드 호출 성공") .map { $0[1] as! Date } } + /// 캘린더의 바운즈(bounds)가 변하면 CGRect이 담긴 스트림이 흐릅니다. var boundingRectWillChange: Observable { return delegate.methodInvoked(#selector(FSCalendarDelegate.calendar(_:boundingRectWillChange:animated:))) - .debug("calendar(_:boundingRectWillChange:animated:) 메서드 호출 성공") .map { $0[1] as! CGRect } } + /// 캘린더의 현재 보이는 페이지가 변하면 Date가 담긴 스트림이 흐릅니다. var calendarCurrentPageDidChange: Observable { return delegate.methodInvoked(#selector(FSCalendarDelegate.calendarCurrentPageDidChange(_:))) - .debug("calendarCurrentPageDidChange(_:) 메서드 호출 성공") .map { ($0[0] as! FSCalendar).currentPage } } - var fetchCalendarResponseDidChange: Observable<[String]> { - return delegate.methodInvoked(#selector(FSCalendarDelegate.calendarCurrentPageDidChange(_:))) - .debug("calendarCurrentPageDidChange(_:) 메서드 호출 성공") - .map { - let fsCalendar: FSCalendar = $0[0] as! FSCalendar - let currentPage: Date = fsCalendar.currentPage - - let previousMonth: String = (currentPage - 1.month).toFormatString() - let currentMonth: String = currentPage.toFormatString() - let nextMonth: String = (currentPage + 1.month).toFormatString() - - return [previousMonth, currentMonth, nextMonth] - } - } } diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactive/DelegateProxy/RxMemoriesCalendarPostDelegateProxy.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactive/DelegateProxy/RxMemoriesCalendarPostDelegateProxy.swift new file mode 100644 index 000000000..736530a28 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactive/DelegateProxy/RxMemoriesCalendarPostDelegateProxy.swift @@ -0,0 +1,38 @@ +// +// RxMemoriesCalendarPostDelegate.swift +// App +// +// Created by 김건우 on 10/18/24. +// + +import Foundation + +import RxCocoa +import RxSwift + +final class RxMemoriesCalendarPostDelegateProxy: DelegateProxy, DelegateProxyType, MemoriesCalendarPostHeaderDelegate { + + public static func registerKnownImplementations() { + self.register { RxMemoriesCalendarPostDelegateProxy(parentObject: $0, delegateProxy: self) } + } + +} + +extension MemoriesCalendarPostHeaderView: HasDelegate { + public typealias Delegate = MemoriesCalendarPostHeaderDelegate +} + +extension Reactive where Base: MemoriesCalendarPostHeaderView { + + var delegate: DelegateProxy { + return RxMemoriesCalendarPostDelegateProxy.proxy(for: self.base) + } + + /// 프로필 버튼을 클릭하면 빈 항목이 담긴 스트림이 흐릅니다. + var didTapProfileImageButton: ControlEvent { + let source = delegate.methodInvoked(#selector(MemoriesCalendarPostHeaderDelegate.didTapProfileImageButton(_:event:))) + .map { _ in () } + return ControlEvent(events: source) + } + +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarImageCellReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarImageCellReactor.swift deleted file mode 100644 index 0a70b1001..000000000 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarImageCellReactor.swift +++ /dev/null @@ -1,104 +0,0 @@ -// -// ImageCalendarCellReactor.swift -// App -// -// Created by 김건우 on 12/9/23. -// - -import Foundation - -import Core -import Domain -import ReactorKit -import RxSwift - -final public class CalendarImageCellReactor: Reactor { - // MARK: - Type - public enum CalendarType { - case week - case month - } - - // MARK: - Action - public enum Action { } - - // MARK: - Mutate - public enum Mutation { - case selectDate - case deselectDate - } - - // MARK: - State - public struct State { - var date: Date - var representativePostId: String - var representativeThumbnailUrl: String - var allFamilyMemebersUploaded: Bool - var isSelected: Bool - } - - // MARK: - Properties - public var initialState: State - - @Injected var calendarUseCase: CalendarUseCaseProtocol - @Injected var provider: ServiceProviderProtocol - - public let type: CalendarType - - // MARK: - Intializer - init( - type: CalendarType, - monthlyEntity: CalendarEntity, - isSelected: Bool - ) { - self.initialState = State( - date: monthlyEntity.date, - representativePostId: monthlyEntity.representativePostId, - representativeThumbnailUrl: monthlyEntity.representativeThumbnailUrl, - allFamilyMemebersUploaded: monthlyEntity.allFamilyMemebersUploaded, - isSelected: isSelected - ) - - self.type = type - } - - // MARK: - Transform - public func transform(mutation: Observable) -> Observable { - let eventMutation = provider.calendarGlabalState.event - .withUnretained(self) - .flatMap { - switch $0.1 { - case let .didSelectDate(date): - if $0.0.initialState.date.isEqual(with: date) { - let lastSelectedDate: Date = $0.0.provider.toastGlobalState.lastSelectedDate - // 이전에 선택된 날짜와 같지 않다면 (셀이 재사용되더라도 ToastView가 다시 뜨게 하지 않기 위함) - if !lastSelectedDate.isEqual(with: date) && $0.0.initialState.allFamilyMemebersUploaded { - // 전체 가족 업로드 유무에 따른 토스트 뷰 출력 이벤트 방출함 - $0.0.provider.toastGlobalState.showAllFamilyUploadedToastMessageView(selection: date) - } - return Observable.just(.selectDate) - } else { - return Observable.just(.deselectDate) - } - - default: - return Observable.empty() - } - } - - return Observable.merge(mutation, eventMutation) - } - - // MARK: - Reduce { - public func reduce(state: State, mutation: Mutation) -> State { - var newState = state - switch mutation { - case .selectDate: - newState.isSelected = true - - case .deselectDate: - newState.isSelected = false - } - return newState - } -} diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPageViewCellReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPageViewCellReactor.swift deleted file mode 100644 index a4f3c84e1..000000000 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPageViewCellReactor.swift +++ /dev/null @@ -1,125 +0,0 @@ -// -// CalendarPageViewCellReactor.swift -// App -// -// Created by 김건우 on 12/6/23. -// - -import UIKit - -import Core -import Data -import Domain -import ReactorKit -import RxSwift - -public final class CalendarCellReactor: Reactor { - // MARK: - Action - public enum Action { - case dateSelected(Date) - case requestBanner - case requestStatistics - case requestMonthlyCalendar - case infoButtonTapped(UIView) - } - - // MARK: - Mutation - public enum Mutation { - case setBanner(BannerEntity) - case setStatistics(FamilyMonthlyStatisticsEntity) - case setMonthlyCalendar(ArrayResponseCalendarEntity) - } - - // MARK: - State - public struct State { - var yearMonth: String - var displayBanner: BannerViewModel.State? - var displayMemoryCount: Int - var displayMonthlyCalendar: ArrayResponseCalendarEntity? - } - - // MARK: - Properties - public var initialState: State - - @Injected var provider: ServiceProviderProtocol - @Injected var calendarUseCase: CalendarUseCaseProtocol - - @Navigator var navigator: MonthlyCalendarNavigatorProtocol - - // MARK: - Intializer - init(yearMonth: String) { - self.initialState = State( - yearMonth: yearMonth, - displayMemoryCount: 0 - ) - } - - // MARK: - Mutate - public func mutate(action: Action) -> Observable { - switch action { - case let .dateSelected(date): - // navigator.toDailyCalendar(selection: date) - return provider.calendarGlabalState.pushCalendarPostVC(date) - .flatMap { _ in Observable.empty() } - - case .requestStatistics: - let yearMonth = currentState.yearMonth - - return calendarUseCase.executeFetchStatisticsSummary(yearMonth: yearMonth) - .flatMap { - guard let statistics = $0 else { - return Observable.empty() - } - return Observable.just(.setStatistics(statistics)) - } - - case .requestBanner: - let yearMonth = currentState.yearMonth - - return calendarUseCase.executeFetchCalendarBenner(yearMonth: yearMonth) - .flatMap { - guard let banner = $0 else { - return Observable.empty() - } - return Observable.just(.setBanner(banner)) - } - - case .requestMonthlyCalendar: - let yearMonth = currentState.yearMonth - - return calendarUseCase.executeFetchCalednarResponse(yearMonth: yearMonth) - .map { - guard let arrayCalendarResponse = $0 else { - return .setMonthlyCalendar(.init(results: [])) - } - return .setMonthlyCalendar(arrayCalendarResponse) - } - - case let .infoButtonTapped(sourceView): provider.calendarGlabalState.didTapCalendarInfoButton(sourceView) - return Observable.empty() - } - } - - // MARK: - Reduce - public func reduce(state: State, mutation: Mutation) -> State { - var newState = state - switch mutation { - case let .setStatistics(statistics): - newState.displayMemoryCount = statistics.totalImageCnt - - case let .setBanner(banner): - let bannerState = BannerViewModel.State( - familyTopPercentage: banner.familyTopPercentage, - allFamilyMemberUploadedDays: banner.allFammilyMembersUploadedDays, - bannerImage: banner.bannerImage, - bannerString: banner.bannerString, - bannerColor: banner.bannerColor - ) - newState.displayBanner = bannerState - - case let .setMonthlyCalendar(arrayCalendarResponse): - newState.displayMonthlyCalendar = arrayCalendarResponse - } - return newState - } -} diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPostCellReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPostCellReactor.swift deleted file mode 100644 index bd03736d9..000000000 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/CalendarPostCellReactor.swift +++ /dev/null @@ -1,111 +0,0 @@ -// -// CalendarPostCellReactor.swift -// App -// -// Created by 김건우 on 5/7/24. -// - -import Core -import Domain -import Foundation - -import ReactorKit - -public final class CalendarPostCellReactor: Reactor { - - // MARK: - Action - public enum Action { - case requestDisplayContent - case requestAuthorName - case requestAuthorImageUrl - case authorImageButtonTapped - } - - // MARK: - Mutation - public enum Mutation { - case setAuthorName(String) - case setAuthorImageUrl(String) - case setContent([DisplayEditItemModel]) - } - - // MARK: - State - public struct State { - var post: DailyCalendarEntity - var authorName: String? - var authorImageUrl: String? - var content: [DisplayEditSectionModel]? - } - - // MARK: - Properties - public var initialState: State - - @Injected var fetchUserNameUseCase: FetchUserNameUseCaseProtocol - - @Injected var meUseCase: MemberUseCaseProtocol - @Injected var provider: ServiceProviderProtocol - - // MARK: - Intializer - public init( - post: DailyCalendarEntity - ) { - self.initialState = State( - post: post - ) - } - - // MARK: - Mutate - public func mutate(action: Action) -> Observable { - switch action { - case .requestDisplayContent: - let content: String = currentState.post.postContent - var sectionItem: [DisplayEditItemModel] = [] - content.forEach { - sectionItem.append( - .fetchDisplayItem( - DisplayEditCellReactor( - title: String($0), - radius: 10, - font: .head2Bold - ) - ) - ) - } - return Observable.just(.setContent(sectionItem)) - - case .requestAuthorName: - let authorId = initialState.post.authorId - - let authorName = fetchUserNameUseCase.execute(memberId: authorId) ?? "알 수 없음" - return Observable.just(.setAuthorName(authorName)) - - case .requestAuthorImageUrl: - let authorId = initialState.post.authorId - let authorImageUrl = meUseCase.executeProfileImageUrlString(memberId: authorId) - return Observable.just(.setAuthorImageUrl(authorImageUrl)) - - case .authorImageButtonTapped: - let authorId = initialState.post.authorId - provider.postGlobalState.pushProfileViewController(authorId) - return Observable.empty() - } - } - - // MARK: - Reduce - public func reduce(state: State, mutation: Mutation) -> State { - var newState = state - - switch mutation { - case let .setAuthorName(name): - newState.authorName = name - - case let .setAuthorImageUrl(url): - newState.authorImageUrl = url - - case let .setContent(section): - newState.content = [.displayKeyword(section)] - } - - return newState - } - -} diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/Cell/MemoriesCalendarCellReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/Cell/MemoriesCalendarCellReactor.swift new file mode 100644 index 000000000..739c37d02 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/Cell/MemoriesCalendarCellReactor.swift @@ -0,0 +1,108 @@ +// +// ImageCalendarCellReactor.swift +// App +// +// Created by 김건우 on 12/9/23. +// + +import Core +import DesignSystem +import Domain +import Foundation + +import ReactorKit +import MacrosInterface + +@Reactor +final public class MemoriesCalendarCellReactor { + + // MARK: - Typealias + + public typealias Action = NoAction + + // MARK: - Mutate + + public enum Mutation { + case didSelect(Bool) + } + + // MARK: - State + + public struct State { + var date: Date + var thumbnailImageUrl: String + var allMemebersUploaded: Bool + var isSelected: Bool + } + + + // MARK: - Properties + + public let type: MomoriesCalendarType + public var initialState: State + + @Injected var provider: ServiceProviderProtocol + + + // MARK: - Intializer + + init( + of type: MomoriesCalendarType, + with entity: MonthlyCalendarEntity, + isSelected selection: Bool = false + ) { + self.type = type + self.initialState = State( + date: entity.date, + thumbnailImageUrl: entity.representativeThumbnailUrl, + allMemebersUploaded: entity.allFamilyMemebersUploaded, + isSelected: selection + ) + } + + // MARK: - Transform + + public func transform(mutation: Observable) -> Observable { + let eventMutation = provider.calendarService.event + .flatMap(with: self) { + switch $1 { + case let .didSelect(current): + let cellDate = $0.initialState.date + // 셀 내 날짜와 선택한 날짜가 동일하면 + if cellDate.isEqual(with: current) { + // 이전에 선택된 날짜 불러오기 + let previous = $0.provider.calendarService.getPreviousSelection() + // 모든 가족 구성원이 게시물을 업로드하고, + // 셀 내 날짜와 이전에 선택된 날짜가 동일하지 않다면 (캘린더를 스크롤하더라도 토스트가 다시 뜨지 않게) + if !cellDate.isEqual(with: previous) && $0.initialState.allMemebersUploaded { + // TODO: - 로직 간소화하기 + let viewConfig = BBToastViewConfiguration(minWidth: 100) + $0.provider.bbToastService.show( + image: DesignSystemAsset.fire.image, + title: "우리 가족 모두가 사진을 올린 날", + viewConfig: viewConfig + ) + } + return Observable.just(.didSelect(true)) + } else { + return Observable.just(.didSelect(false)) + } + } + } + + return Observable.merge(mutation, eventMutation) + } + + + // MARK: - Reduce + + public func reduce(state: State, mutation: Mutation) -> State { + var newState = state + switch mutation { + case let .didSelect(bool): + newState.isSelected = bool + } + return newState + } + +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/Cell/MemoriesCalendarPageReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/Cell/MemoriesCalendarPageReactor.swift new file mode 100644 index 000000000..81e5bb482 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/Cell/MemoriesCalendarPageReactor.swift @@ -0,0 +1,129 @@ +// +// CalendarPageViewCellReactor.swift +// App +// +// Created by 김건우 on 12/6/23. +// + +import Core +import Data +import Domain +import Foundation +import MacrosInterface + +import ReactorKit + +@Reactor +public final class MemoriesCalendarPageReactor { + + // MARK: - Action + + public enum Action { + case didSelect(Date) + case viewDidLoad + } + + + // MARK: - Mutation + + public enum Mutation { + case setBannerInfo(BannerEntity) + case setStatisticsSummary(FamilyMonthlyStatisticsEntity) + case setMonthlyCalendar(ArrayResponseMonthlyCalendarEntity) + } + + + // MARK: - State + + public struct State { + var yearMonth: String + var bannerInfo: BannerViewModel.State? + var imageCount: Int? + var calendarEntity: ArrayResponseMonthlyCalendarEntity? + } + + + // MARK: - Properties + + public var initialState: State + + @Injected var provider: ServiceProviderProtocol + @Injected var fetchCalendarBannerUseCase: FetchCalendarBannerUseCaseProtocol + @Injected var fetchStatisticsSummaryUseCase: FetchStatisticsSummaryUseCaseProtocol + @Injected var fetchMonthlyCalendarUseCase: FetchMonthlyCalendarUseCaseProtocol + + @Navigator var navigator: MonthlyCalendarNavigatorProtocol + + // MARK: - Intializer + + init(yearMonth: String) { + self.initialState = State(yearMonth: yearMonth) + } + + + // MARK: - Mutate + + public func mutate(action: Action) -> Observable { + switch action { + case .viewDidLoad: + let yearMonth = initialState.yearMonth + return Observable.merge( + setCalendarBannrInfo(yearMonth: yearMonth), + setStatisticsSummary(yearMonth: yearMonth), + setMonthlyCalendar(yearMonth: yearMonth) + ) + + case let .didSelect(date): + navigator.toDailyCalendar(selection: date) + return Observable.empty() + } + } + + + // MARK: - Reduce + + public func reduce(state: State, mutation: Mutation) -> State { + var newState = state + switch mutation { + case let .setBannerInfo(banner): + let bannerState = BannerViewModel.State( + familyTopPercentage: banner.familyTopPercentage, + allFamilyMemberUploadedDays: banner.allFammilyMembersUploadedDays, + bannerImage: banner.bannerImage, + bannerString: banner.bannerString, + bannerColor: banner.bannerColor + ) + newState.bannerInfo = bannerState + + case let .setStatisticsSummary(statistics): + newState.imageCount = statistics.totalImageCnt + + case let .setMonthlyCalendar(arrayCalendarResponse): + newState.calendarEntity = arrayCalendarResponse + } + return newState + } + +} + + +// MARK: - Extensions + +private extension MemoriesCalendarPageReactor { + + func setCalendarBannrInfo(yearMonth: String) -> Observable { + return fetchCalendarBannerUseCase.execute(yearMonth: yearMonth) + .flatMap { Observable.just(.setBannerInfo($0)) } + } + + func setStatisticsSummary(yearMonth: String) -> Observable { + return fetchStatisticsSummaryUseCase.execute(yearMonth: yearMonth) + .flatMap { Observable.just(.setStatisticsSummary($0)) } + } + + func setMonthlyCalendar(yearMonth: String) -> Observable { + return fetchMonthlyCalendarUseCase.execute(yearMonth: yearMonth) + .flatMap { Observable.just(.setMonthlyCalendar($0)) } + } + +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/Cell/MemoriesCalendarPostCellReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/Cell/MemoriesCalendarPostCellReactor.swift new file mode 100644 index 000000000..a631bb4e9 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/Cell/MemoriesCalendarPostCellReactor.swift @@ -0,0 +1,126 @@ +// +// CalendarPostCellReactor.swift +// App +// +// Created by 김건우 on 5/7/24. +// + +import Core +import Domain +import Foundation +import MacrosInterface + +import ReactorKit + +@Reactor +public final class MemoriesCalendarPostCellReactor { + + // MARK: - Action + + public enum Action { + case viewDidLoad + case didTapProfileImageButton + } + + // MARK: - Mutation + + public enum Mutation { + case setMemberName(String) + case setProfileImageUrl(URL) + case setContentDatasource([DisplayEditItemModel]) + } + + // MARK: - State + + public struct State { + var dailyPost: DailyCalendarEntity + var memberName: String? + var profileImageUrl: URL? + var contentDatasource: [DisplayEditSectionModel]? + } + + // MARK: - Properties + + public var initialState: State + + @Injected var fetchUserNameUseCase: FetchUserNameUseCaseProtocol + @Injected var fetchProfileImageUrlUseCase: FetchProfileImageUrlUseCaseProtocol + @Injected var checkIsVaildMemberUseCase: CheckIsVaildMemberUseCaseProtocol + @Injected var provider: ServiceProviderProtocol + + @Navigator var navigator: DailyCalendarNavigatorProtocol + + + // MARK: - Intializer + + public init(postEntity entity: DailyCalendarEntity) { + self.initialState = State(dailyPost: entity) + } + + // MARK: - Mutate + + public func mutate(action: Action) -> Observable { + switch action { + case .viewDidLoad: + let memberId = initialState.dailyPost.authorId + return Observable.concat( + setMemberName(memberId: memberId), + setProfileImageUrl(memberId: memberId), + setContentDatasource(post: initialState.dailyPost) + ) + + case .didTapProfileImageButton: + let memberId = initialState.dailyPost.authorId + if checkIsVaildMemberUseCase.execute(memberId: memberId) { + navigator.toProfile(memberId: memberId) + } + return Observable.empty() + } + } + + // MARK: - Reduce + + public func reduce(state: State, mutation: Mutation) -> State { + var newState = state + switch mutation { + case let .setMemberName(name): + newState.memberName = name + + case let .setProfileImageUrl(url): + newState.profileImageUrl = url + + case let .setContentDatasource(section): + newState.contentDatasource = [.displayKeyword(section)] + } + return newState + } + +} + + +// MARK: - Extensions + +private extension MemoriesCalendarPostCellReactor { + + func setMemberName(memberId: String) -> Observable { + let memberName = fetchUserNameUseCase.execute(memberId: memberId) + return Observable.just(.setMemberName(memberName)) + } + + func setProfileImageUrl(memberId: String) -> Observable { + let imageUrl = fetchProfileImageUrlUseCase.execute(memberId: memberId) + if let url = imageUrl { + return Observable.just(.setProfileImageUrl(url)) + } + return Observable.empty() + } + + func setContentDatasource(post: DailyCalendarEntity) -> Observable { + var sectionItem: [DisplayEditItemModel] = [] + post.postContent?.forEach { + sectionItem.append(.fetchDisplayItem(.init(title: String($0), radius: 10, font: .head2Bold))) + } + return Observable.just(.setContentDatasource(sectionItem)) + } + +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/DailyCalendarViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/DailyCalendarViewReactor.swift index 9ab7e18d8..5f52442ab 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/DailyCalendarViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/DailyCalendarViewReactor.swift @@ -16,223 +16,214 @@ import ReactorKit import RxSwift public final class DailyCalendarViewReactor: Reactor { + // MARK: - Action + public enum Action { - case dateSelected(Date) - case requestDailyCalendar(Date) - case requestMonthlyCalendar(String) - case imageIndex(Int) - case renewEmoji(Int) - case popViewController + case viewDidLoad + case didSelect(date: Date) + case fetchMonthlyCalendar(date: Date) + case updateVisiblePost(index: Int) + case backToMonthly } + // MARK: - Mutation + public enum Mutation { - case setAllUploadedToastMessageView(Bool) - case setDailyCalendar([DailyCalendarEntity]) - case setMonthlyCalendar(String, ArrayResponseCalendarEntity) - case setImageIndex(Int) - case setVisiblePost(DailyCalendarEntity) - case setSelectionHaptic + case setDailyPosts([DailyCalendarEntity]) + case setMonthlyCalendar(String, [MonthlyCalendarEntity]) + case setVisiblePost(Int) case renewCommentCount(Int) - case pushProfileViewController(String) - case popViewController - case clearNotificationDeepLink + case clearNotificationDeepLink // 삭제하기 } + // MARK: - State + public struct State { - var date: Date - - var imageUrl: String? + var initialSelection: Date + @Pulse var dailyPostsDataSource: [DailyCalendarSectionModel] + @Pulse var monthlyCalendars: [String: [MonthlyCalendarEntity]] var visiblePost: DailyCalendarEntity? - @Pulse var displayDailyCalendar: [DailyCalendarSectionModel] - @Pulse var displayMonthlyCalendar: [String: [CalendarEntity]] - @Pulse var shouldPresentAllUploadedToastMessageView: Bool - @Pulse var shouldGenerateSelectionHaptic: Bool - @Pulse var shouldPushProfileViewController: String? - @Pulse var shouldPopViewController: Bool - - var notificationDeepLink: NotificationDeepLink? // 댓글 푸시 알림 체크 변수 + var notificationDeepLink: NotificationDeepLink? // 삭제하기 } + // MARK: - Properties - @Injected var provider: ServiceProviderProtocol - @Injected var calendarUseCase: CalendarUseCaseProtocol public var initialState: State - private var hasReceivedPostEvent: Bool = false - private var hasReceivedSelectionEvent: Bool = false - private var hasFetchedCalendarResponse: [String] = [] - private var hasThumbnailImages: [Date] = [] + @Injected var fetchDailyPostsUseCase: FetchDailyCalendarUseCaseProtocol + @Injected var fetchMonthlyCalendarUseCase: FetchMonthlyCalendarUseCaseProtocol + @Injected var provider: ServiceProviderProtocol + @Navigator var navigator: DailyCalendarNavigatorProtocol + + private var hasFetchedDailyPosts: [Date] = [] + private var hasFetchedMonthlyCalendars: [Date] = [] + + private var dailyPostDataSource: DailyCalendarSectionModel? { + guard let datasource = currentState.dailyPostsDataSource.first else { return nil } + return datasource + } + // MARK: - Intializer + init( - date: Date, - notificationDeepLink deepLink: NotificationDeepLink? + initialSelection date: Date, + notificationDeepLink deepLink: NotificationDeepLink? // 삭제하기 ) { self.initialState = State( - date: date, - displayDailyCalendar: [], - displayMonthlyCalendar: [:], - shouldPresentAllUploadedToastMessageView: false, - shouldGenerateSelectionHaptic: false, - shouldPushProfileViewController: nil, - shouldPopViewController: false, - notificationDeepLink: deepLink + initialSelection: date, + dailyPostsDataSource: [], + monthlyCalendars: [:], + + notificationDeepLink: deepLink // 삭제하기 ) } + // MARK: - Transfor + public func transform(mutation: Observable) -> Observable { - let toastMutation = provider.toastGlobalState.event + let eventMutation = provider.postGlobalState.event .flatMap { event -> Observable in switch event { - case let .showAllFamilyUploadedToastView(uploaded): - return Observable.just(.setAllUploadedToastMessageView(uploaded)) - } - } - - let postMutation = provider.postGlobalState.event - .flatMap { event -> Observable in - switch event { - case let .pushProfileViewController(memberId): - return Observable.just(.pushProfileViewController(memberId)) - case let .renewalPostCommentCount(count): + case let .renewalCommentCount(count): return Observable.just(.renewCommentCount(count)) default: return .empty() } } - - return Observable.merge(mutation, toastMutation, postMutation) + return Observable.merge(mutation, eventMutation) } + // MARK: - Mutate + public func mutate(action: Action) -> Observable { switch action { - case .popViewController: - provider.toastGlobalState.clearToastMessageEvent() - return Observable.just(.popViewController) - case let .dateSelected(date): - // 처음 이벤트를 받거나 썸네일 이미지가 존재하는 셀에 한하여 - if !hasReceivedSelectionEvent || hasThumbnailImages.contains(date) { - hasReceivedSelectionEvent = true - // 셀 클릭 이벤트 방출 - provider.calendarGlabalState.didSelectDate(date) - return Observable.just(.setSelectionHaptic) - } - return Observable.empty() + case .viewDidLoad: + let yearMonth = currentState.initialSelection + let yearMonthDay = currentState.initialSelection.toFormatString(with: .dashYyyyMMdd) + + provider.calendarService.didSelect(date: currentState.initialSelection) + return Observable.merge(fetchDailyPost(with: yearMonthDay), fetchMonthlyCalendars(with: yearMonth)) + + case let .didSelect(date): + let yearMonthDay = date.toFormatString(with: .dashYyyyMMdd) - case let .requestDailyCalendar(date): - // 처음 이벤트를 받거나 썸네일 이미지가 존재하는 셀에 한하여 - if !hasReceivedPostEvent || hasThumbnailImages.contains(date) { - hasReceivedPostEvent = true - // 가족이 게시한 포스트 가져오기 - let yearMonthDay: String = date.toFormatString(with: .dashYyyyMMdd) - return calendarUseCase.executeFetchDailyCalendarResponse(yearMonthDay: yearMonthDay) - .flatMap { entity in - guard let posts: [DailyCalendarEntity] = entity?.results else { - return Observable.empty() - } - - return Observable.concat( - Observable.just(.setDailyCalendar(posts)), - Observable.just(.setImageIndex(0)), - Observable.just(.clearNotificationDeepLink) - ) - } + if hasFetchedDailyPosts.contains(date) { + provider.calendarService.didSelect(date: date) + return fetchDailyPost(with: yearMonthDay) } return Observable.empty() - case let .requestMonthlyCalendar(yearMonth): - // 이전에 불러온 적이 없다면 - if !hasFetchedCalendarResponse.contains(yearMonth) { - return calendarUseCase.executeFetchCalednarResponse(yearMonth: yearMonth) - .withUnretained(self) - .map { - guard let arrayCalendarResponse = $0.1 else { - return .setMonthlyCalendar(yearMonth, .init(results: [])) - } - $0.0.hasFetchedCalendarResponse.append(yearMonth) - $0.0.hasThumbnailImages.append( - contentsOf: arrayCalendarResponse.results.map { $0.date } - ) - // NOTE: - 썸네일 이미지가 존재하는 일(日)자에 한하여 데이터를 불러옴 - return .setMonthlyCalendar(yearMonth, arrayCalendarResponse) - } - // 이전에 불러온 적이 있다면 - } else { - return Observable.empty() - } + case let .fetchMonthlyCalendar(date): + return fetchMonthlyCalendars(with: date) - case let .imageIndex(index): - return Observable.just(.setImageIndex(index)) + case let .updateVisiblePost(index): + return Observable.just(.setVisiblePost(index)) - case let .renewEmoji(index): - guard let dataSource = currentState.displayDailyCalendar.first else { - return Observable.empty() - } - let post = dataSource.items[index] - return Observable.just(.setVisiblePost(post)) + case .backToMonthly: + provider.calendarService.removePreviousSelection() + navigator.backToMonthly() + return Observable.empty() } + + + } + // MARK: - Reduce + public func reduce(state: State, mutation: Mutation) -> State { var newState = state switch mutation { - case let .setImageIndex(index): - guard let items = newState.displayDailyCalendar.first?.items else { - return newState + case let .setDailyPosts(posts): + newState.dailyPostsDataSource = [DailyCalendarSectionModel(model: (), items: posts)] + + case let .setMonthlyCalendar(yearMonth, arrayCalendarResponse): + newState.monthlyCalendars[yearMonth] = arrayCalendarResponse + + case let .setVisiblePost(index): + if let datasource = dailyPostDataSource { + newState.visiblePost = datasource.items[index] } - newState.imageUrl = items[index].postImageUrl case let .renewCommentCount(count): - guard var posts = currentState.displayDailyCalendar.first?.items, - let index = posts.firstIndex(where: { post in - post.postId == currentState.visiblePost?.postId - }) else { - return newState + if let datasource = dailyPostDataSource, + let index = datasource.items.firstIndex(where: { $0.postId == currentState.visiblePost?.postId }) { + guard var newPost = currentState.visiblePost else { return state } + newPost.commentCount = count + var newDailyPosts = datasource.items + newDailyPosts[index] = newPost + newState.visiblePost = newPost + newState.dailyPostsDataSource = [.init(model: (), items: newDailyPosts)] } - guard var renewedPost = currentState.visiblePost else { - return newState - } - renewedPost.commentCount = count - posts[index] = renewedPost - newState.visiblePost = posts[index] - newState.displayDailyCalendar = [.init(model: (), items: posts)] - - case let .setAllUploadedToastMessageView(uploaded): - newState.shouldPresentAllUploadedToastMessageView = uploaded - - case let .setMonthlyCalendar(yearMonth, arrayCalendarResponse): - newState.displayMonthlyCalendar[yearMonth] = arrayCalendarResponse.results - case let .setDailyCalendar(postResponse): - newState.displayDailyCalendar = [DailyCalendarSectionModel(model: (), items: postResponse)] - - case let .setVisiblePost(post): - newState.visiblePost = post - - case let .pushProfileViewController(memberId): - newState.shouldPushProfileViewController = memberId - - case .popViewController: - newState.shouldPopViewController = true - - case .clearNotificationDeepLink: + case .clearNotificationDeepLink: // 삭제하기 newState.notificationDeepLink = nil - - case .setSelectionHaptic: - newState.shouldGenerateSelectionHaptic = true } - return newState } } + + +// MARK: - Extensions + +private extension DailyCalendarViewReactor { + + func fetchDailyPost(with yearMonthDay: String) -> Observable { + fetchDailyPostsUseCase.execute(yearMonthDay: yearMonthDay) + .flatMap(with: self) { + return Observable.concat( + Observable.just(.setDailyPosts($1.results)), + Observable.just(.setVisiblePost(0)), + Observable.just(.clearNotificationDeepLink) // 삭제하기 + ) + } + } + + func fetchMonthlyCalendars(with date: Date) -> Observable { + let (prev, curr, next) = makePrevCurrNextYearMonth(date) + + let monthlyCalendars: Observable = Observable.merge( + !hasFetchedMonthlyCalendars.contains(prev) + ? fetchMonthlyCalendar(with: prev.toFormatString(with: .dashYyyyMM)) + : Observable.empty(), + !hasFetchedMonthlyCalendars.contains(curr) + ? fetchMonthlyCalendar(with: curr.toFormatString(with: .dashYyyyMM)) + : Observable.empty(), + !hasFetchedMonthlyCalendars.contains(next) + ? fetchMonthlyCalendar(with: next.toFormatString(with: .dashYyyyMM)) + : Observable.empty() + ) + return monthlyCalendars + } + + func fetchMonthlyCalendar(with yearMonth: String) -> Observable { + fetchMonthlyCalendarUseCase.execute(yearMonth: yearMonth) + .flatMap(with: self) { + $0.hasFetchedDailyPosts.append(contentsOf: $1.results.map(\.date)) + $0.hasFetchedMonthlyCalendars.append(yearMonth.toDate(with: .dashYyyyMM)) + return Observable.just(.setMonthlyCalendar(yearMonth, $1.results)) + } + + } + +} + +private extension DailyCalendarViewReactor { + + func makePrevCurrNextYearMonth(_ date: Date) -> (prev: Date, curr: Date, next: Date) { + return (date - 1.month, date, date + 1.month) + } + +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/MemoriesCalendarPostHeaderReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/MemoriesCalendarPostHeaderReactor.swift new file mode 100644 index 000000000..559c13e6a --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/MemoriesCalendarPostHeaderReactor.swift @@ -0,0 +1,51 @@ +// +// MemoriesCalendarPostHeaderReactor.swift +// App +// +// Created by 김건우 on 10/17/24. +// + +import Core +import Domain +import Foundation + +import ReactorKit + +final public class MemoriesCalendarPostHeaderReactor: Reactor { + + public typealias Action = NoAction + + // MARK: - Mutate + + public enum Mutation { } + + // MARK: - State + + public struct State { + var memberName: String? + var profileImageUrl: URL? + } + + // MARK: - Properties + + public let initialState: State + + // MARK: - Intializer + + public init() { self.initialState = State() } + + + // MARK: - Mutate + + public func mutate(action: Action) -> Observable { + return .empty() + } + + + // MARK: - Reduce + + public func reduce(state: State, mutation: Mutation) -> State { + return state + } + +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/MemoriesCalendarPostImageReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/MemoriesCalendarPostImageReactor.swift new file mode 100644 index 000000000..cfaee652b --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/MemoriesCalendarPostImageReactor.swift @@ -0,0 +1,29 @@ +// +// MemoriesCalendarPostImageReactor.swift +// App +// +// Created by 김건우 on 10/17/24. +// + +import Foundation + +import ReactorKit + +final public class MemoriesCalendarPostImageReactor: Reactor { + + public typealias Action = NoAction + + // MARK: - State + + public struct State { } + + // MARK: - Properties + + public let initialState: State + + // MARK: - Intializer + + public init() { self.initialState = State() } + +} + diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/MemoriesCalendarTitleViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/MemoriesCalendarTitleViewReactor.swift new file mode 100644 index 000000000..8e580bb4f --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/MemoriesCalendarTitleViewReactor.swift @@ -0,0 +1,64 @@ +// +// MemoriesCalendarTitleViewReactor.swift +// App +// +// Created by 김건우 on 10/16/24. +// + +import ReactorKit +import MacrosInterface + +@Reactor +final public class MemoriesCalendarTitleViewReactor { + + // MARK: - Action + + public enum Action { + case didTapTipButton + } + + // MARK: - Mutation + + public enum Mutation { + case setTooltipHidden(hidden: Bool) + } + + // MARK: - State + + public struct State { + @Pulse var hiddenTooltipView: Bool = true + } + + // MARK: - Properties + + public var initialState: State = State() + + // MARK: - Intializer + + public init() { + self.initialState = State() + } + + + // MARK: - Mutate + + public func mutate(action: Action) -> Observable { + switch action { + case .didTapTipButton: + return Observable.just(.setTooltipHidden(hidden: !currentState.hiddenTooltipView)) + } + } + + + // MARK: - Reduce + + public func reduce(state: State, mutation: Mutation) -> State { + var newState = state + switch mutation { + case let .setTooltipHidden(hidden): + newState.hiddenTooltipView = hidden + } + return newState + } + +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/MonthlyCalendarViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/MonthlyCalendarViewReactor.swift index 43c893eb6..be1a0a985 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/MonthlyCalendarViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/MonthlyCalendarViewReactor.swift @@ -5,120 +5,111 @@ // Created by 김건우 on 12/6/23. // -import UIKit - import Core import Data import Domain +import Foundation + import ReactorKit -import RxSwift public final class MonthlyCalendarViewReactor: Reactor { + // MARK: - Action + public enum Action { - case popViewController - case addCalendarItems([String]) + case viewDidLoad } // MARK: - Mutation + public enum Mutation { - case popViewController - case pushDailyCalendarViewController(Date) - case setInfoPopover(UIView) - case setCalendarItems([String]) - // TODO: - 싹다 코드 리팩토링하기 - case setCalendarPageIndexPath(IndexPath) + case setCalendarPage([String]) } // MARK: - State + public struct State { - @Pulse var shouldPopViewController: Bool - @Pulse var shouldPushDailyCalendarViewController: Date? - @Pulse var shouldPresnetInfoPopover: UIView? - @Pulse var displayCalendar: [MonthlyCalendarSectionModel] - - var initalCalendarPageIndexPath: IndexPath? = nil + @Pulse var pageDatasource: [MonthlyCalendarSectionModel] } // MARK: - Properties + public var initialState: State + @Injected var fetchFamilyCreatedAtUseCase: FetchFamilyCreatedAtUseCaseProtocol @Injected var provider: ServiceProviderProtocol - @Injected var calendarUseCase: CalendarUseCaseProtocol - + @Navigator var navigator: MonthlyCalendarNavigatorProtocol // MARK: - Intializer - init() { - self.initialState = State( - shouldPopViewController: false, - displayCalendar: [.init(model: (), items: [])] - ) - } - // MARK: - Transform - public func transform(mutation: Observable) -> Observable { - let eventMutation = provider.calendarGlabalState.event - .flatMap { event -> Observable in - switch event { - case let .pushCalendarPostVC(date): - return Observable.just(.pushDailyCalendarViewController(date)) - - case let .didTapInfoButton(sourceView): - return Observable.just(.setInfoPopover(sourceView)) - - default: - return Observable.empty() - } - } - - return Observable.merge(mutation, eventMutation) + init() { + self.initialState = State(pageDatasource: [.init(model: (), items: [])]) } // MARK: - Mutate + public func mutate(action: Action) -> Observable { switch action { - case .popViewController: - provider.toastGlobalState.clearLastSelectedDate() - return Observable.just(.popViewController) - - case let .addCalendarItems(items): - let indexPath = IndexPath(item: items.count-1, section: 0) - - return Observable.concat( - Observable.just(.setCalendarItems(items)), - Observable.just(.setCalendarPageIndexPath(indexPath)) - ) - + case .viewDidLoad: + return fetchFamilyCreatedAtUseCase.execute() + .flatMap(with: self) { + guard let createdAt = $1?.createdAt + else { + return Observable.just(.setCalendarPage($0.createCalendarPageItems(from: ._20240101))) + } + return Observable.just(.setCalendarPage($0.createCalendarPageItems(from: createdAt))) + } } } + // MARK: - Reduce + public func reduce(state: State, mutation: Mutation) -> State { var newState = state switch mutation { - case .popViewController: - newState.shouldPopViewController = true - - case let .pushDailyCalendarViewController(date): - newState.shouldPushDailyCalendarViewController = date - - case let .setInfoPopover(sourceView): - newState.shouldPresnetInfoPopover = sourceView - - case let .setCalendarItems(items): - let newDatasource = MonthlyCalendarSectionModel( - model: (), - items: items - ) - newState.displayCalendar = [newDatasource] - - case let .setCalendarPageIndexPath(indexPath): - newState.initalCalendarPageIndexPath = indexPath + case let .setCalendarPage(items): + newState.pageDatasource = [.init(model: (), items: items)] } + return newState + } +} + + +// MARK: - Extensions + +private extension MonthlyCalendarViewReactor { + + func createCalendarPageItems(from startDate: Date, to endDate: Date = Date()) -> [String] { + var items: [String] = [] + let calendar: Calendar = Calendar.current - return newState + let monthInterval: Int = calculateMonthInterval(from: startDate, to: endDate) + + for value in 0...monthInterval { + if let date = calendar.date(byAdding: .month, value: value, to: startDate) { + let yyyyMM = date.toFormatString(with: .dashYyyyMM) + items.append(yyyyMM) + } + } + + return items } + + func calculateMonthInterval(from startDate: Date, to endDate: Date = .now) -> Int { + let calendar: Calendar = Calendar.current + + let startComponents = calendar.dateComponents([.year, .month], from: startDate) + let endComponents = calendar.dateComponents([.year, .month], from: endDate) + + let yearDiff = endComponents.year! - startComponents.year! + let monthDiff = endComponents.month! - startComponents.month! + + let monthInterval = yearDiff * 12 + monthDiff + return monthInterval + } + } diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Strings/CalenderStrings.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/Strings/CalenderStrings.swift deleted file mode 100644 index ade6abfae..000000000 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/Strings/CalenderStrings.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// CalenderCell.swift -// App -// -// Created by 김건우 on 12/9/23. -// - -import Foundation - -typealias CalendarStrings = String.Calendar -extension String { - enum Calendar {} -} - -extension String.Calendar { - static let mainTitle: String = "추억 캘린더" - static let infoText: String = "모두가 참여한 날과 업로드한 사진 수로\n이 달의 친밀도를 측정합니다" - static let allFamilyUploadedText: String = "우리 가족 모두가 사진을 올린 날" -} diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/BannerView.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/BannerView.swift index 8a0634b82..bdc9d13ed 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/BannerView.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/BannerView.swift @@ -5,14 +5,14 @@ // Created by 김건우 on 1/26/24. // - -import UIKit import DesignSystem import SwiftUI struct BannerView: View { + // MARK: - Mertic - private enum Metric { + + private enum Metric { // `isPhoneSE` 프로퍼티 개선하기 static var topPadding: CGFloat = UIScreen.isPhoneSE ? 12 : 18 static var vSpacing: CGFloat = UIScreen.isPhoneSE ? 1 : 6 static var scaleWidth: CGFloat = UIScreen.isPhoneSE ? 0.5 : 1 @@ -21,17 +21,22 @@ struct BannerView: View { } // MARK: - Properties + @ObservedObject var viewModel: BannerViewModel private let bold = DesignSystemFontFamily.Pretendard.bold private let regular = DesignSystemFontFamily.Pretendard.regular + // MARK: - Intializer + init(viewModel: BannerViewModel) { self.viewModel = viewModel } + // MARK: - Body + var body: some View { banner .frame(maxWidth: .infinity, maxHeight: .infinity) @@ -50,7 +55,9 @@ struct BannerView: View { } } + // MARK: - Extensions + extension BannerView { var banner: some View { VStack { @@ -112,7 +119,9 @@ extension BannerView { } } + // MARK: - Preview + struct BannerView_Previews: PreviewProvider { static let viewModel = BannerViewModel(state: .init( diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarCell.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarCell.swift deleted file mode 100644 index 69d35c20e..000000000 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarCell.swift +++ /dev/null @@ -1,263 +0,0 @@ -// -// CalendarPageViewCell.swift -// App -// -// Created by 김건우 on 12/6/23. -// - -import Core -import DesignSystem -import Domain -import SwiftUI -import UIKit - -import FSCalendar -import ReactorKit -import RxCocoa -import RxSwift -import SnapKit -import Then - -final class CalendarCell: BaseCollectionViewCell { - // MARK: - Id - static var id: String = "CalendarCell" - - // MARK: - Views - private lazy var labelStack: UIStackView = UIStackView() - private let titleLabel: BBLabel = BBLabel(.head2Bold, textAlignment: .center, textColor: .gray200) - private let countLabel: BBLabel = BBLabel(.body1Regular, textColor: .gray200) - private let infoButton: UIButton = UIButton(type: .system) - - private lazy var bannerView: BannerView = BannerView(viewModel: bannerViewModel) - private lazy var bannerController: UIHostingController = UIHostingController(rootView: bannerView) - - private let calendarView: FSCalendar = FSCalendar() - - // MARK: - Properties - private let infoImage: UIImage = DesignSystemAsset.infoCircleFill.image - .withRenderingMode(.alwaysTemplate) - private lazy var bannerViewModel: BannerViewModel = BannerViewModel(reactor: reactor, state: .init()) - - // MARK: - Helpers - override func bind(reactor: CalendarCellReactor) { - super.bind(reactor: reactor) - bindInput(reactor: reactor) - bindOutput(reactor: reactor) - } - - private func bindInput(reactor: CalendarCellReactor) { - Observable.just(()) - .map { Reactor.Action.requestBanner } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - Observable.just(()) - .map { Reactor.Action.requestStatistics } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - Observable.just(()) - .map { Reactor.Action.requestMonthlyCalendar } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - infoButton.rx.tap - .throttle(RxConst.milliseconds300Interval, scheduler: MainScheduler.instance) - .withUnretained(self) - .map { Reactor.Action.infoButtonTapped($0.0.infoButton) } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - calendarView.rx.didSelect - .map { Reactor.Action.dateSelected($0) } - .bind(to: reactor.action) - .disposed(by: disposeBag) - } - - private func bindOutput(reactor: CalendarCellReactor) { - reactor.state.compactMap { $0.displayBanner } - .distinctUntilChanged(\.familyTopPercentage) - .withUnretained(self) - .subscribe { - $0.0.bannerViewModel.updateState(state: $0.1) - } - .disposed(by: disposeBag) - - reactor.state.map { $0.displayMemoryCount } - .distinctUntilChanged() - .bind(to: countLabel.rx.memoryCountText) - .disposed(by: disposeBag) - - reactor.state.map { $0.displayMonthlyCalendar } - .withUnretained(self) - .subscribe { $0.0.calendarView.reloadData() } - .disposed(by: disposeBag) - - let currentDate = reactor.state.map { $0.yearMonth } - .map { $0.toDate(with: .dashYyyyMM) } - .asDriver(onErrorJustReturn: .now) - - currentDate - .distinctUntilChanged() - .drive(calendarView.rx.currentPage) - .disposed(by: disposeBag) - - currentDate - .distinctUntilChanged() - .drive(titleLabel.rx.calendarTitleText) - .disposed(by: disposeBag) - } - - override func setupUI() { - super.setupUI() - contentView.addSubviews( - labelStack, countLabel, bannerController.view, calendarView - ) - labelStack.addArrangedSubviews( - titleLabel, infoButton - ) - } - - override func setupAutoLayout() { - super.setupAutoLayout() - labelStack.snp.makeConstraints { - $0.top.equalToSuperview().offset(24) - $0.leading.equalToSuperview().offset(24) - } - - countLabel.snp.makeConstraints { - $0.top.equalToSuperview().offset(24) - $0.trailing.equalToSuperview().offset(-24) - } - - bannerController.view.snp.makeConstraints { - $0.top.equalTo(labelStack.snp.bottom).offset(22) - $0.horizontalEdges.equalToSuperview().inset(20) - $0.bottom.equalTo(calendarView.snp.top).offset(-28) - } - - calendarView.snp.makeConstraints { - $0.bottom.equalToSuperview().offset(UIScreen.isPhoneSE ? -8 : -30) - $0.horizontalEdges.equalToSuperview().inset(0.5) - $0.height.equalTo(contentView.snp.width).multipliedBy(0.98) - } - - infoButton.snp.makeConstraints { - $0.size.equalTo(20) - } - } - - override func setupAttributes() { - super.setupAttributes() - infoButton.do { - $0.setImage( - infoImage, - for: .normal - ) - $0.tintColor = .gray300 - } - - labelStack.do { - $0.axis = .horizontal - $0.spacing = 10.0 - $0.alignment = .fill - $0.distribution = .fill - } - - calendarView.do { - $0.headerHeight = 0.0 - $0.weekdayHeight = 40.0 - - $0.today = nil - $0.scrollEnabled = false - $0.placeholderType = .fillSixRows - $0.adjustsBoundingRectWhenChangingMonths = true - - $0.appearance.selectionColor = UIColor.clear - - $0.appearance.titleFont = UIFont.style(.body1Regular) - $0.appearance.titleDefaultColor = UIColor.bibbiWhite - $0.appearance.titleSelectionColor = UIColor.bibbiWhite - - $0.appearance.weekdayFont = UIFont.style(.caption) - $0.appearance.weekdayTextColor = UIColor.gray300 - $0.appearance.caseOptions = .weekdayUsesSingleUpperCase - - $0.appearance.titlePlaceholderColor = UIColor.gray700 - - $0.backgroundColor = UIColor.clear - - $0.locale = Locale(identifier: "ko_kr") - $0.register(CalendarImageCell.self, forCellReuseIdentifier: CalendarImageCell.id) - $0.register(CalendarPlaceholderCell.self, forCellReuseIdentifier: CalendarPlaceholderCell.id) - - $0.delegate = self - $0.dataSource = self - } - - bannerController.view.do { - $0.backgroundColor = UIColor.clear - } - } -} - -// MARK: - Extensions -extension CalendarCell: FSCalendarDelegate { - func calendar(_ calendar: FSCalendar, shouldSelect date: Date, at monthPosition: FSCalendarMonthPosition) -> Bool { - let month = date.month - let currentMonth = calendar.currentPage.month - - if let calendarCell = calendar.cell(for: date, at: monthPosition) as? CalendarImageCell { - // 셀의 날짜가 현재 월(月)과 동일하고, 썸네일 이미지가 있다면 - if month == currentMonth && calendarCell.hasThumbnailImage { - return true - } - } - - return false - } -} - -extension CalendarCell: FSCalendarDataSource { - func calendar(_ calendar: FSCalendar, cellFor date: Date, at position: FSCalendarMonthPosition) -> FSCalendarCell { - let calendarMonth = calendar.currentPage.month - let positionMonth = date.month - // 셀의 날짜가 현재 월(月)과 동일하다면 - if calendarMonth == positionMonth { - let cell = calendar.dequeueReusableCell( - withIdentifier: CalendarImageCell.id, - for: date, - at: position - ) as! CalendarImageCell - - // 해당 일자에 데이터가 존재하지 않는다면 - guard let dayResponse = reactor?.currentState.displayMonthlyCalendar?.results.filter({ $0.date == date }).first else { - let emptyResponse = CalendarEntity( - date: date, - representativePostId: .none, - representativeThumbnailUrl: .none, - allFamilyMemebersUploaded: false - ) - cell.reactor = CalendarImageCellDIContainer( - type: .month, - monthlyEntity: emptyResponse - ).makeReactor() - return cell - } - - cell.reactor = CalendarImageCellDIContainer( - type: .month, - monthlyEntity: dayResponse - ).makeReactor() - return cell - // 셀의 날짜가 현재 월(月)과 동일하지 않다면 - } else { - let cell = calendar.dequeueReusableCell( - withIdentifier: CalendarPlaceholderCell.id, - for: date, - at: position - ) as! CalendarPlaceholderCell - return cell - } - } -} diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarPostCell.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarPostCell.swift deleted file mode 100644 index d32c4d8b8..000000000 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarPostCell.swift +++ /dev/null @@ -1,268 +0,0 @@ -// -// CalendarPostCell.swift -// App -// -// Created by 김건우 on 5/7/24. -// - -import Core -import Domain -import UIKit - -import SnapKit -import Then -import RxSwift -import RxCocoa -import RxDataSources -import Kingfisher - -final class CalendarPostCell: BaseCollectionViewCell { - - // MARK: - Id - static let id = "CalendarPostCell" - - // MARK: - Views - private let authorStackView: UIStackView = UIStackView() - private let authorImageContainerView: UIView = UIView() - private let authorImageView: UIImageView = UIImageView() - private let authorNameLabel: BBLabel = BBLabel(.caption, textColor: .gray200) - private let authorFirstNameLabel: BBLabel = BBLabel(.caption, textColor: .bibbiWhite) - private let postImageView: UIImageView = UIImageView() - private let missionTextView: MissionTextView = MissionTextView() - private let contentCollectionView: UICollectionView = UICollectionView( - frame: .zero, - collectionViewLayout: UICollectionViewFlowLayout() - ) - - // MARK: - Properties - private lazy var datasource = prepareContentDatasource() - private let flowLayout: UICollectionViewFlowLayout = UICollectionViewFlowLayout() - - // MARK: - Intializer - - - // MARK: - LifeCycles - override func prepareForReuse() { - super.prepareForReuse() - authorNameLabel.text = .unknown - authorFirstNameLabel.text = "알" - authorImageView.image = nil - postImageView.image = nil - } - - // MARK: - Helpers - override func bind(reactor: CalendarPostCellReactor) { - super.bind(reactor: reactor) - bindInput(reactor: reactor) - bindOutput(reactor: reactor) - } - - private func bindInput(reactor: CalendarPostCellReactor) { - Observable.just(()) - .flatMap { _ in - Observable.concat( - Observable.just(.requestDisplayContent), - Observable.just(.requestAuthorName), - Observable.just(.requestAuthorImageUrl) - ) - } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - authorImageContainerView.rx.tap - .throttle(RxConst.milliseconds300Interval, scheduler: RxSchedulers.main) - .map { Reactor.Action.authorImageButtonTapped } - .bind(to: reactor.action) - .disposed(by: disposeBag) - } - - private func bindOutput(reactor: CalendarPostCellReactor) { - - let post = reactor.state.map { $0.post } - .asDriver(onErrorDriveWith: .empty()) - - post - .distinctUntilChanged() - .drive(with: self) { owner, post in - owner.postImageView.kf.setImage( - with: URL(string: post.postImageUrl), - options: [.transition(.fade(0.15))] - ) - } - .disposed(by: disposeBag) - - post - .map { $0.missionContent} - .distinctUntilChanged() - .drive(missionTextView.missionLabel.rx.text) - .disposed(by: disposeBag) - - post - .map { $0.missionContent.isEmpty } - .distinctUntilChanged() - .drive(missionTextView.rx.isHidden) - .disposed(by: disposeBag) - - reactor.state.map { $0.authorName } - .distinctUntilChanged() - .bind(to: authorNameLabel.rx.text) - .disposed(by: disposeBag) - - reactor.state.compactMap { $0.authorName } - .distinctUntilChanged() - .bind(to: authorFirstNameLabel.rx.firstLetterText) - .disposed(by: disposeBag) - - reactor.state.compactMap { $0.authorImageUrl } - .distinctUntilChanged() - .bind(with: self) { owner, url in - owner.authorImageView.kf.setImage( - with: URL(string: url) - ) - } - .disposed(by: disposeBag) - - reactor.state.compactMap { $0.content } - .bind(to: contentCollectionView.rx.items(dataSource: datasource)) - .disposed(by: disposeBag) - } - - override func setupUI() { - super.setupUI() - - contentView.addSubviews(authorStackView, postImageView) - authorImageContainerView.addSubviews(authorFirstNameLabel, authorImageView) - authorStackView.addArrangedSubviews(authorImageContainerView, authorNameLabel) - postImageView.addSubviews(contentCollectionView, missionTextView) - } - - override func setupAutoLayout() { - super.setupAutoLayout() - - authorStackView.snp.makeConstraints { - $0.horizontalEdges.equalToSuperview().inset(16) - $0.height.equalTo(34) - } - - authorImageContainerView.snp.makeConstraints { - $0.size.equalTo(34) - } - - authorFirstNameLabel.snp.makeConstraints { - $0.center.equalToSuperview() - } - - authorImageView.snp.makeConstraints { - $0.size.equalTo(34) - } - - contentCollectionView.snp.makeConstraints { - $0.height.equalTo(41) - $0.bottom.equalTo(postImageView.snp.bottom).offset(-20) - $0.horizontalEdges.equalToSuperview() - } - - missionTextView.snp.makeConstraints { - $0.top.equalToSuperview().offset(16) - $0.horizontalEdges.equalToSuperview().inset(32) - $0.height.equalTo(41) - } - - postImageView.snp.makeConstraints { - $0.horizontalEdges.equalToSuperview() - $0.height.equalTo(postImageView.snp.width) - $0.top.equalTo(authorStackView.snp.bottom).offset(8) - } - } - - override func setupAttributes() { - super.setupAttributes() - - authorStackView.do { - $0.axis = .horizontal - $0.spacing = 12 - } - - authorImageContainerView.do { - $0.layer.masksToBounds = true - $0.layer.cornerRadius = 34 / 2 - $0.backgroundColor = UIColor.gray800 - $0.isUserInteractionEnabled = true - } - - authorImageView.do { - $0.contentMode = .scaleAspectFill - $0.layer.masksToBounds = true - $0.layer.cornerRadius = 34 / 2 - } - - authorNameLabel.do { - $0.text = String.unknown - } - - authorFirstNameLabel.do { - $0.text = "알" - } - - postImageView.do { - $0.clipsToBounds = true - $0.backgroundColor = UIColor.gray100 - $0.contentMode = .scaleAspectFill - $0.layer.cornerRadius = 48 - } - - contentCollectionView.do { - $0.backgroundColor = .clear - $0.isScrollEnabled = false - $0.showsVerticalScrollIndicator = false - $0.showsHorizontalScrollIndicator = false - $0.collectionViewLayout = flowLayout - $0.register( - DisplayEditCollectionViewCell.self, - forCellWithReuseIdentifier: DisplayEditCollectionViewCell.id - ) - $0.delegate = self - } - - flowLayout.do { - $0.itemSize = CGSize(width: 28, height: 41) - $0.minimumInteritemSpacing = 2 - } - } - -} - -// MARK: - Extensions -extension CalendarPostCell { - private func prepareContentDatasource() -> RxCollectionViewSectionedReloadDataSource { - return RxCollectionViewSectionedReloadDataSource { datasources, collectionView, indexPath, sectionItem in - switch sectionItem { - case let .fetchDisplayItem(reactor): - guard let cell = collectionView.dequeueReusableCell( - withReuseIdentifier: DisplayEditCollectionViewCell.id, - for: indexPath - ) as? DisplayEditCollectionViewCell else { - return UICollectionViewCell() - } - cell.reactor = reactor - return cell - } - } - } -} - -extension CalendarPostCell: UICollectionViewDelegateFlowLayout { - public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { - guard let count = reactor?.currentState.post.postContent.count else { - return .zero - } - - let totalCellWidth = 28 * count - let totalSpacingWidth = 2 * (count - 1) - - let leftInset = (collectionView.frame.width - CGFloat(totalCellWidth + totalSpacingWidth)) / 2 - let rightInset = leftInset - - return UIEdgeInsets(top: 0, left: leftInset, bottom: 0, right: rightInset) - } -} diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarImageCell.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/MemoriesCalendarCell.swift similarity index 52% rename from 14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarImageCell.swift rename to 14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/MemoriesCalendarCell.swift index d2b6db359..4d8716e5f 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarImageCell.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/MemoriesCalendarCell.swift @@ -17,21 +17,29 @@ import RxSwift import SnapKit import Then -final public class CalendarImageCell: FSCalendarCell, ReactorKit.View { +final public class MemoriesCalendarCell: FSCalendarCell, ReactorKit.View { + // MARK: - Id + static let id: String = "ImageCalendarCell" // MARK: - Views - private let dayLabel: BBLabel = BBLabel(.body1Regular, textAlignment: .center) - private let containerView: UIView = UIView() - private let thumbnailView: UIImageView = UIImageView() + + private let backgroundGray: UIView = UIView() + private let thumbnailImage: UIImageView = UIImageView() private let todayStrokeView: UIView = UIView() - private let allFamilyUploadedBadge: UIImageView = UIImageView() + private let dayLabel: BBLabel = BBLabel(.body1Regular, textAlignment: .center) + + private let allMembersUploadedBadge: UIImageView = UIImageView() + // MARK: - Properties + public var disposeBag: RxSwift.DisposeBag = DisposeBag() + // MARK: - Intializer + public override init!(frame: CGRect) { super.init(frame: .zero) setupUI() @@ -43,83 +51,62 @@ final public class CalendarImageCell: FSCalendarCell, ReactorKit.View { fatalError("init(coder:) has not been implemented") } + // MARK: - LifeCycles + public override func prepareForReuse() { - dayLabel.textColor = UIColor.bibbiWhite - thumbnailView.image = nil - thumbnailView.layer.borderWidth = .zero - thumbnailView.layer.borderColor = UIColor.bibbiWhite.cgColor + super.prepareForReuse() + todayStrokeView.isHidden = true - allFamilyUploadedBadge.isHidden = true + thumbnailImage.image = nil + thumbnailImage.layer.borderWidth = .zero + thumbnailImage.layer.borderColor = UIColor.bibbiWhite.cgColor + dayLabel.textColor = UIColor.bibbiWhite + allMembersUploadedBadge.isHidden = true } + // MARK: - Helpers - public func bind(reactor: CalendarImageCellReactor) { - bindInput(reactor: reactor) + + public func bind(reactor: MemoriesCalendarCellReactor) { bindOutput(reactor: reactor) } - private func bindInput(reactor: CalendarImageCellReactor) { } - - private func bindOutput(reactor: CalendarImageCellReactor) { - reactor.state.map { "\($0.date.day)" } + private func bindOutput(reactor: MemoriesCalendarCellReactor) { + let date = reactor.state.map { $0.date } + .asDriver(onErrorJustReturn: .now) + + date.map { $0.day.description } .distinctUntilChanged() - .bind(to: dayLabel.rx.text) + .drive(dayLabel.rx.text) .disposed(by: disposeBag) - reactor.state.map { $0.date.isToday } + date.map { $0.isToday } + .filter { $0 } .distinctUntilChanged() - .withUnretained(self) - .subscribe { - if $0.1 { - $0.0.todayStrokeView.isHidden = false - $0.0.dayLabel.textColor = UIColor.mainYellow - } - } + .drive(with: self, onNext: { owner, _ in owner.setTodayHighlight() }) .disposed(by: disposeBag) - - reactor.state.map { !$0.allFamilyMemebersUploaded } - .distinctUntilChanged() - .bind(to: allFamilyUploadedBadge.rx.isHidden) + + reactor.state.map { $0.allMemebersUploaded } + .map { !$0 } + .bind(to: allMembersUploadedBadge.rx.isHidden) .disposed(by: disposeBag) - reactor.state.compactMap { $0.representativeThumbnailUrl } + reactor.state.compactMap { $0.thumbnailImageUrl } + .compactMap { URL(string: $0) } .distinctUntilChanged() - .bind(to: thumbnailView.rx.kingfisherImage) + .bind(to: thumbnailImage.rx.kfImage) .disposed(by: disposeBag) - // 최초 셀 생성 시, 클릭 이벤트 발생 시 하이라이트를 위해 실행됨 reactor.state.map { $0.isSelected } + .filter { _ in reactor.type == .daily } .distinctUntilChanged() - .withUnretained(self) - .subscribe { - if reactor.type == .week { - if $0.1 { - $0.0.todayStrokeView.isHidden = true - - $0.0.thumbnailView.alpha = 1 - $0.0.containerView.alpha = 1 - $0.0.thumbnailView.layer.borderWidth = 1 - $0.0.thumbnailView.layer.borderColor = UIColor.bibbiWhite.cgColor - } else { - $0.0.thumbnailView.alpha = 0.3 - $0.0.containerView.alpha = 0.3 - $0.0.thumbnailView.layer.borderWidth = 0 - - if reactor.currentState.date.isToday { - $0.0.todayStrokeView.isHidden = false - $0.0.dayLabel.textColor = UIColor.mainYellow - } - } - } - } + .bind(with: self) { $0.setHighlight(with: $1) } .disposed(by: disposeBag) } private func setupUI() { - contentView.insertSubview(thumbnailView, at: 0) - contentView.insertSubview(containerView, at: 0) - contentView.addSubviews(dayLabel, todayStrokeView, allFamilyUploadedBadge) + contentView.addSubviews(backgroundGray, thumbnailImage, dayLabel, todayStrokeView, allMembersUploadedBadge) } private func setupAutoLayout() { @@ -127,12 +114,12 @@ final public class CalendarImageCell: FSCalendarCell, ReactorKit.View { $0.center.equalTo(contentView.snp.center) } - containerView.snp.makeConstraints { + backgroundGray.snp.makeConstraints { $0.center.equalTo(contentView.snp.center) $0.size.equalTo(contentView.snp.width).inset(2.25) } - thumbnailView.snp.makeConstraints { + thumbnailImage.snp.makeConstraints { $0.center.equalTo(contentView.snp.center) $0.size.equalTo(contentView.snp.width).inset(2.25) } @@ -142,7 +129,7 @@ final public class CalendarImageCell: FSCalendarCell, ReactorKit.View { $0.size.equalTo(contentView.snp.width).inset(2.25) } - allFamilyUploadedBadge.snp.makeConstraints { + allMembersUploadedBadge.snp.makeConstraints { $0.top.equalToSuperview().offset(5) $0.trailing.equalToSuperview().offset(-5) $0.size.equalTo(17) @@ -154,13 +141,13 @@ final public class CalendarImageCell: FSCalendarCell, ReactorKit.View { $0.isHidden = true } - containerView.do { + backgroundGray.do { $0.clipsToBounds = true $0.layer.cornerRadius = 13 $0.backgroundColor = .gray900 } - thumbnailView.do { + thumbnailImage.do { $0.clipsToBounds = true $0.contentMode = .scaleAspectFill $0.layer.cornerRadius = 13 @@ -176,7 +163,7 @@ final public class CalendarImageCell: FSCalendarCell, ReactorKit.View { $0.layer.borderColor = UIColor.mainYellow.cgColor } - allFamilyUploadedBadge.do { + allMembersUploadedBadge.do { $0.image = DesignSystemAsset.fire.image $0.isHidden = true $0.backgroundColor = UIColor.clear @@ -184,9 +171,33 @@ final public class CalendarImageCell: FSCalendarCell, ReactorKit.View { } } + // MARK: - Extensions -extension CalendarImageCell { + +extension MemoriesCalendarCell { + + func setHighlight(with selection: Bool) { + if selection { + backgroundGray.alpha = 1 + thumbnailImage.alpha = 1 + thumbnailImage.layer.borderWidth = 1 + thumbnailImage.layer.borderColor = UIColor.bibbiWhite.cgColor + todayStrokeView.isHidden = true + } else { + backgroundGray.alpha = 0.3 + thumbnailImage.alpha = 0.3 + thumbnailImage.layer.borderWidth = 0 + if reactor!.initialState.date.isToday { setTodayHighlight() } + } + } + + func setTodayHighlight() { + todayStrokeView.isHidden = false + dayLabel.textColor = UIColor.mainYellow + } + var hasThumbnailImage: Bool { - return thumbnailView.image != nil ? true : false + return thumbnailImage.image != nil ? true : false } + } diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/MemoriesCalendarPageViewCell.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/MemoriesCalendarPageViewCell.swift new file mode 100644 index 000000000..7a2d2eee4 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/MemoriesCalendarPageViewCell.swift @@ -0,0 +1,226 @@ +// +// CalendarPageViewCell.swift +// App +// +// Created by 김건우 on 12/6/23. +// + +import Core +import DesignSystem +import Domain +import SwiftUI +import UIKit + +import FSCalendar +import ReactorKit +import RxCocoa +import RxSwift +import SnapKit +import Then + +final class MemoriesCalendarPageViewCell: BaseCollectionViewCell { + + // MARK: - Id + + static var id: String = "CalendarCell" + + + // MARK: - Views + + private lazy var titleView = makeMemoriesCalendarTitleView() + private lazy var bannerViewController = BannerHostingViewController(reactor: reactor) + private let calendarView: FSCalendar = FSCalendar() + + + // MARK: - Properties + + private let infoImage: UIImage = DesignSystemAsset.infoCircleFill.image + .withRenderingMode(.alwaysTemplate) + + + // MARK: - Helpers + + override func bind(reactor: MemoriesCalendarPageReactor) { + super.bind(reactor: reactor) + + bindInput(reactor: reactor) + bindOutput(reactor: reactor) + } + + private func bindInput(reactor: MemoriesCalendarPageReactor) { + Observable.just(()) + .map { Reactor.Action.viewDidLoad } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + calendarView.rx.didSelect + .map { Reactor.Action.didSelect($0) } + .bind(to: reactor.action) + .disposed(by: disposeBag) + } + + private func bindOutput(reactor: MemoriesCalendarPageReactor) { + + let yearMonth = reactor.state.map { $0.yearMonth } + .map { $0.toDate(with: .dashYyyyMM) } + .asDriver(onErrorJustReturn: .distantPast) + + yearMonth + .drive(with: self, onNext: { $0.titleView.setTitle($1.toFormatString(with: "yyyy년 M월")) }) + .disposed(by: disposeBag) + + yearMonth + .drive(calendarView.rx.currentPage) + .disposed(by: disposeBag) + + reactor.state.compactMap { $0.imageCount } + .distinctUntilChanged() + .bind(with: self) { $0.titleView.setMemoryCount($1) } + .disposed(by: disposeBag) + + reactor.state.compactMap { $0.bannerInfo } + .distinctUntilChanged(\.familyTopPercentage) + .bind(with: self) { $0.bannerViewController.updateState($1) } + .disposed(by: disposeBag) + + reactor.state.map { $0.calendarEntity } + .withUnretained(self) + .subscribe { $0.0.calendarView.reloadData() } + .disposed(by: disposeBag) + } + + override func setupUI() { + super.setupUI() + contentView.addSubviews(bannerViewController.view, calendarView, titleView) + } + + override func setupAutoLayout() { + super.setupAutoLayout() + + titleView.snp.makeConstraints { + $0.top.equalToSuperview().offset(24) + $0.horizontalEdges.equalToSuperview().inset(24) + $0.height.equalTo(24) + } + + bannerViewController.view.snp.makeConstraints { + $0.top.equalTo(titleView.snp.bottom).offset(22) + $0.horizontalEdges.equalToSuperview().inset(20) + $0.bottom.equalTo(calendarView.snp.top).offset(-28) + } + + calendarView.snp.makeConstraints { + $0.bottom.equalToSuperview().offset(UIScreen.isPhoneSE ? -8 : -30) + $0.horizontalEdges.equalToSuperview().inset(0.5) + $0.height.equalTo(contentView.snp.width).multipliedBy(0.98) + } + } + + override func setupAttributes() { + super.setupAttributes() + + calendarView.do { + $0.headerHeight = 0 + $0.weekdayHeight = 40 + + $0.today = nil + $0.scrollEnabled = false + $0.placeholderType = .fillSixRows + $0.adjustsBoundingRectWhenChangingMonths = true + + $0.appearance.selectionColor = UIColor.clear + $0.appearance.titleFont = UIFont.style(.body1Regular) + $0.appearance.titleDefaultColor = UIColor.bibbiWhite + $0.appearance.titleSelectionColor = UIColor.bibbiWhite + $0.appearance.weekdayFont = UIFont.style(.caption) + $0.appearance.weekdayTextColor = UIColor.gray300 + $0.appearance.caseOptions = .weekdayUsesSingleUpperCase + $0.appearance.titlePlaceholderColor = UIColor.gray700 + + $0.backgroundColor = UIColor.clear + + $0.locale = Locale(identifier: "ko_kr") + $0.register(MemoriesCalendarCell.self, forCellReuseIdentifier: MemoriesCalendarCell.id) + $0.register(MemoriesCalendarPlaceholderCell.self, forCellReuseIdentifier: MemoriesCalendarPlaceholderCell.id) + + $0.delegate = self + $0.dataSource = self + } + + } +} + +// MARK: - Extensions + +extension MemoriesCalendarPageViewCell: FSCalendarDelegate { + + func calendar(_ calendar: FSCalendar, shouldSelect date: Date, at monthPosition: FSCalendarMonthPosition) -> Bool { + let currentMonth = date.month + let visibleMonth = calendar.currentPage.month + + if let cell = calendar.cell(for: date, at: monthPosition) as? MemoriesCalendarCell { + if visibleMonth == currentMonth && cell.hasThumbnailImage { + return true + } + } + return false + } + +} + +extension MemoriesCalendarPageViewCell: FSCalendarDataSource { + + func calendar(_ calendar: FSCalendar, cellFor date: Date, at position: FSCalendarMonthPosition) -> FSCalendarCell { + let currentMonth = date.month + let visibleMonth = calendar.currentPage.month + + if visibleMonth == currentMonth { + let cell = calendar.dequeueReusableCell( + withIdentifier: MemoriesCalendarCell.id, + for: date, + at: position + ) as! MemoriesCalendarCell + + guard + let entity = reactor?.currentState + .calendarEntity?.results + .first(where: { $0.date == date }) + else { + let entity = MonthlyCalendarEntity( + date: date, + representativePostId: "", + representativeThumbnailUrl: "", + allFamilyMemebersUploaded: false + ) + cell.reactor = MemoriesCalendarCellReactor( + of: .month, + with: entity + ) + return cell + } + + cell.reactor = MemoriesCalendarCellReactor( + of: .month, + with: entity + ) + return cell + + } else { + let cell = calendar.dequeueReusableCell( + withIdentifier: MemoriesCalendarPlaceholderCell.id, + for: date, + at: position + ) as! MemoriesCalendarPlaceholderCell + return cell + } + } + +} + +extension MemoriesCalendarPageViewCell { + + private func makeMemoriesCalendarTitleView() -> MemoriesCalendarPageTitleView { + MemoriesCalendarPageTitleView(reactor: .init()) + } + +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarPlaceholderCell.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/MemoriesCalendarPlaceholderCell.swift similarity index 88% rename from 14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarPlaceholderCell.swift rename to 14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/MemoriesCalendarPlaceholderCell.swift index 8180f0f59..780174f99 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/CalendarPlaceholderCell.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/MemoriesCalendarPlaceholderCell.swift @@ -16,11 +16,15 @@ import RxSwift import SnapKit import Then -final class CalendarPlaceholderCell: FSCalendarCell { +final class MemoriesCalendarPlaceholderCell: FSCalendarCell { + // MARK: - Properties + static let id: String = "CalendarPlaceholderCell" + // MARK: - Intializer + override init(frame: CGRect) { super.init(frame: .zero) setupAutoLayout() @@ -30,7 +34,9 @@ final class CalendarPlaceholderCell: FSCalendarCell { fatalError("init(coder:) has not been implemented") } + // MARK: - Helpers + func setupAutoLayout() { titleLabel.snp.makeConstraints { $0.center.equalTo(contentView.snp.center) diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/MemoriesCalendarPostCell.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/MemoriesCalendarPostCell.swift new file mode 100644 index 000000000..d66165cdb --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/MemoriesCalendarPostCell.swift @@ -0,0 +1,208 @@ +// +// CalendarPostCell.swift +// App +// +// Created by 김건우 on 5/7/24. +// + +import Core +import Domain +import UIKit + +import SnapKit +import Then +import RxSwift +import RxCocoa +import RxDataSources +import Kingfisher + +final class MemoriesCalendarPostCell: BaseCollectionViewCell { + + // MARK: - Typealias + + typealias RxDataSource = RxCollectionViewSectionedReloadDataSource + + + // MARK: - Id + + static let id = "CalendarPostCell" + + + // MARK: - Views + + private lazy var headerView = makeMemoriesCalendarPostHeaderView() + private lazy var postImageView = makeMemoriesCalendarPostImageView() + private lazy var collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) + + + // MARK: - Properties + + private lazy var datasource = prepareContentDatasource() + + + // MARK: - LifeCycles + + override func prepareForReuse() { + super.prepareForReuse() + headerView.prepareForReuse() + postImageView.prepareForReuse() + } + + + // MARK: - Helpers + + override func bind(reactor: MemoriesCalendarPostCellReactor) { + super.bind(reactor: reactor) + bindInput(reactor: reactor) + bindOutput(reactor: reactor) + } + + private func bindInput(reactor: MemoriesCalendarPostCellReactor) { + Observable.just(()) + .map { Reactor.Action.viewDidLoad } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + headerView.rx.didTapProfileImageButton + .throttle(RxInterval._300milliseconds, scheduler: RxScheduler.main) + .map { Reactor.Action.didTapProfileImageButton } + .bind(to: reactor.action) + .disposed(by: disposeBag) + } + + private func bindOutput(reactor: MemoriesCalendarPostCellReactor) { + let dailyPost = reactor.state.map { $0.dailyPost } + .asDriver(onErrorDriveWith: .empty()) + + dailyPost + .distinctUntilChanged() + .compactMap { $0.missionContent } + .drive(with: self, onNext: { $0.postImageView.setMissionText(text: $1) }) + .disposed(by: disposeBag) + + dailyPost + .distinctUntilChanged() + .drive(with: self, onNext: { $0.postImageView.setPostImage(imageUrl: $1.postImageUrl) }) + .disposed(by: disposeBag) + + reactor.state.map { $0.memberName } + .distinctUntilChanged() + .bind(with: self) { $0.headerView.setMemberName(text: $1) } + .disposed(by: disposeBag) + + reactor.state.map { $0.profileImageUrl } + .distinctUntilChanged() + .compactMap { $0 } + .bind(with: self) { $0.headerView.setProfileImage(imageUrl: $1) } + .disposed(by: disposeBag) + + reactor.state.compactMap { $0.contentDatasource } + .bind(to: collectionView.rx.items(dataSource: datasource)) + .disposed(by: disposeBag) + } + + override func setupUI() { + super.setupUI() + + contentView.addSubviews(headerView, postImageView, collectionView) + } + + override func setupAutoLayout() { + super.setupAutoLayout() + + headerView.snp.makeConstraints { + $0.top.equalTo(self.snp.top).offset(8) + $0.horizontalEdges.equalToSuperview().inset(16) + $0.height.equalTo(34) + } + + collectionView.snp.makeConstraints { + $0.height.equalTo(41) + $0.bottom.equalTo(postImageView.snp.bottom).offset(-20) + $0.horizontalEdges.equalToSuperview() + } + + postImageView.snp.makeConstraints { + $0.horizontalEdges.equalToSuperview() + $0.height.equalTo(postImageView.snp.width) + $0.horizontalEdges.equalToSuperview().inset(1) + $0.top.equalTo(headerView.snp.bottom).offset(8) + } + } + + override func setupAttributes() { + super.setupAttributes() + + collectionView.do { + $0.backgroundColor = .clear + $0.isScrollEnabled = false + $0.showsVerticalScrollIndicator = false + $0.showsHorizontalScrollIndicator = false + $0.collectionViewLayout = UICollectionViewFlowLayout() + $0.register(DisplayEditCollectionViewCell.self, forCellWithReuseIdentifier: DisplayEditCollectionViewCell.id) + $0.delegate = self + } + } + +} + + +// MARK: - Extensions + +extension MemoriesCalendarPostCell { + + private func prepareContentDatasource() -> RxDataSource { + return RxDataSource { datasources, collectionView, indexPath, sectionItem in + switch sectionItem { + case let .fetchDisplayItem(reactor): + guard let cell = collectionView.dequeueReusableCell( + withReuseIdentifier: DisplayEditCollectionViewCell.id, + for: indexPath + ) as? DisplayEditCollectionViewCell else { + return UICollectionViewCell() + } + cell.reactor = reactor + return cell + } + } + } + +} + +extension MemoriesCalendarPostCell { + + func makeMemoriesCalendarPostHeaderView() -> MemoriesCalendarPostHeaderView { + return MemoriesCalendarPostHeaderView(reactor: MemoriesCalendarPostHeaderReactor()) + } + + func makeMemoriesCalendarPostImageView() -> MemoriesCalendarPostImageView { + return MemoriesCalendarPostImageView(reactor: MemoriesCalendarPostImageReactor()) + } + +} + +extension MemoriesCalendarPostCell: UICollectionViewDelegateFlowLayout { + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + return CGSize(width: 28, height: 41) + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { + return 2 + } + + public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { + guard let count = reactor?.currentState.dailyPost.postContent?.count else { + return .zero + } + + let totalCellWidth = 28 * count + let totalSpacingWidth = 2 * (count - 1) + + let leftInset = (collectionView.frame.width - CGFloat(totalCellWidth + totalSpacingWidth)) / 2 + let rightInset = leftInset + + return UIEdgeInsets(top: 0, left: leftInset, bottom: 0, right: rightInset) + } + +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/MemoriesCalendarPageTitleView.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/MemoriesCalendarPageTitleView.swift new file mode 100644 index 000000000..4c550a018 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/MemoriesCalendarPageTitleView.swift @@ -0,0 +1,125 @@ +// +// MemoriesCalendarPageHeaderView.swift +// App +// +// Created by 김건우 on 10/16/24. +// + +import Core +import DesignSystem +import UIKit + +import SnapKit +import Then + +final public class MemoriesCalendarPageTitleView: BaseView { + + // MARK: - Views + + private let labelStack: UIStackView = UIStackView() + private let titleLabel: BBLabel = BBLabel(.head2Bold, textAlignment: .center, textColor: .gray200) + private let memoryCountLabel: BBLabel = BBLabel(.body1Regular, textColor: .gray200) + private let tipButton: UIButton = UIButton(type: .system) + + private let toolTipView: BBToolTipView = BBToolTipView() + + // MARK: - Properties + + private let tipImage: UIImage = DesignSystemAsset.infoCircleFill.image.withRenderingMode(.alwaysTemplate) + + + // MARK: - Helpers + + public override func bind(reactor: Reactor) { + super.bind(reactor: reactor) + + bindInput(reactor: reactor) + bindOutput(reactor: reactor) + } + + private func bindInput(reactor: Reactor) { + tipButton.rx.tap + .throttle(RxInterval._300milliseconds, scheduler: RxScheduler.main) + .map { Reactor.Action.didTapTipButton } + .bind(to: reactor.action) + .disposed(by: disposeBag) + } + + private func bindOutput(reactor: Reactor) { + reactor.pulse(\.$hiddenTooltipView) + .bind(with: self) { + $1 ? $0.toolTipView.hidePopover() + : $0.toolTipView.showPopover() + } + .disposed(by: disposeBag) + } + + + public override func setupUI() { + super.setupUI() + + self.addSubviews(labelStack, memoryCountLabel, toolTipView) + labelStack.addArrangedSubviews(titleLabel, tipButton) + } + + public override func setupAutoLayout() { + super.setupAutoLayout() + + labelStack.snp.makeConstraints { + $0.top.equalToSuperview().offset(0) + $0.leading.equalToSuperview().offset(0) + } + + memoryCountLabel.snp.makeConstraints { + $0.top.equalToSuperview().offset(0) + $0.trailing.equalToSuperview().offset(0) + } + + tipButton.snp.makeConstraints { + $0.size.equalTo(20) + } + + toolTipView.snp.makeConstraints { + $0.top.equalTo(tipButton.snp.bottom).offset(4) + $0.leading.equalToSuperview().offset(57.5) + } + } + + public override func setupAttributes() { + super.setupAttributes() + + self.clipsToBounds = false + + tipButton.do { + $0.setImage(tipImage, for: .normal) + $0.tintColor = .gray300 + } + + labelStack.do { + $0.axis = .horizontal + $0.spacing = 10 + $0.alignment = .fill + $0.distribution = .fill + } + + toolTipView.hidePopover() + toolTipView.toolTipType = .monthlyCalendar +// toolTipView.anchorPoint = CGPoint(x: 0.3, y: 0) + } + +} + + +// MARK: - Extensions + +public extension MemoriesCalendarPageTitleView { + + func setTitle(_ title: String) { + self.titleLabel.text = title + } + + func setMemoryCount(_ count: Int) { + self.memoryCountLabel.text = "\(count)개의 추억" + } + +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/MemoriesCalendarPostHeaderView.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/MemoriesCalendarPostHeaderView.swift new file mode 100644 index 000000000..c419e2134 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/MemoriesCalendarPostHeaderView.swift @@ -0,0 +1,131 @@ +// +// MemoriesCalendarPostHeaderView.swift +// App +// +// Created by 김건우 on 10/17/24. +// + +import Core +import UIKit + +import Then +import SnapKit +import Kingfisher + +final class MemoriesCalendarPostHeaderView: BaseView { + + // MARK: - Views + + private let profileStack: UIStackView = UIStackView() + private let profileBackgroundView: UIView = UIView() + private let profileImageButton: UIButton = UIButton(type: .custom) + private let firstNameLetter: BBLabel = BBLabel(.caption, textColor: .bibbiWhite) + private let memberNameLabel: BBLabel = BBLabel(.caption, textColor: .gray200) + + // MARK: - Properteis + + weak var delegate: (any MemoriesCalendarPostHeaderDelegate)? + + + // MARK: - Helpers + + public override func bind(reactor: Reactor) { + super.bind(reactor: reactor) + } + + public override func setupUI() { + super.setupUI() + + addSubview(profileStack) + profileBackgroundView.addSubviews(firstNameLetter, profileImageButton) + profileStack.addArrangedSubviews(profileBackgroundView, memberNameLabel) + } + + public override func setupAutoLayout() { + super.setupAutoLayout() + + profileStack.snp.makeConstraints { + $0.edges.equalToSuperview() + } + + profileBackgroundView.snp.makeConstraints { + $0.size.equalTo(34) + } + + profileImageButton.snp.makeConstraints { + $0.size.equalTo(34) + } + + firstNameLetter.snp.makeConstraints { + $0.center.equalToSuperview() + } + } + + public override func setupAttributes() { + super.setupAttributes() + + profileStack.do { + $0.spacing = 12 + $0.axis = .horizontal + } + + profileBackgroundView.do { + $0.layer.masksToBounds = true + $0.layer.cornerRadius = 34 / 2 + $0.backgroundColor = UIColor.gray800 + $0.isUserInteractionEnabled = true + } + + profileImageButton.do { + $0.contentMode = .scaleAspectFill + $0.layer.masksToBounds = true + $0.layer.cornerRadius = 34 / 2 + $0.adjustsImageWhenHighlighted = false + $0.addTarget(self, action: #selector(didTapProfileImageButton(_:event:)), for: .touchUpInside) + } + + memberNameLabel.do { + $0.text = "알 수 없음" + } + + firstNameLetter.do { + $0.text = "알" + } + } + +} + + +// MARK: - Extensions + +extension MemoriesCalendarPostHeaderView { + + @objc func didTapProfileImageButton(_ button: UIButton, event: UIControl.Event) { + delegate?.didTapProfileImageButton?(button, event: event) + } + +} + +extension MemoriesCalendarPostHeaderView { + + func prepareForReuse() { + profileImageButton.setImage(nil, for: .normal) + firstNameLetter.text = "알" + memberNameLabel.text = "알 수 없음" + } + + func setMemberName(text: String?) { + memberNameLabel.text = text + firstNameLetter.text = text?[0] + } + + func setProfileImage(imageUrl url: URL) { + KingfisherManager.shared.retrieveImage(with: url) { result in + if case let .success(imageResult) = result { + self.profileImageButton.setBackgroundImage(imageResult.image, for: .normal) // extension으로 빼기 + } + } + } + +} + diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/MemoriesCalendarPostImageView.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/MemoriesCalendarPostImageView.swift new file mode 100644 index 000000000..0e126fa0b --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/MemoriesCalendarPostImageView.swift @@ -0,0 +1,79 @@ +// +// MemoriesCalendarPostImageView.swift +// App +// +// Created by 김건우 on 10/17/24. +// + +import Core +import UIKit + +import Then +import SnapKit + +final class MemoriesCalendarPostImageView: BaseView { + + // MARK: - Views + + private let imageView: UIImageView = UIImageView() + private let missionText: MissionTextView = MissionTextView() + + // MARK: - Helpers + + public override func setupUI() { + super.setupUI() + + addSubviews(imageView, missionText) + } + + public override func setupAutoLayout() { + super.setupAutoLayout() + + imageView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + + missionText.snp.makeConstraints { + $0.top.equalToSuperview().offset(16) + $0.horizontalEdges.equalToSuperview().inset(32) + $0.height.equalTo(41) + } + } + + public override func setupAttributes() { + super.setupAttributes() + + imageView.do { + $0.clipsToBounds = true + $0.backgroundColor = UIColor.gray100 + $0.contentMode = .scaleAspectFill + $0.layer.cornerRadius = 48 + } + + missionText.do { + $0.isHidden = true + } + } + +} + + +// MARK: - Extensions + +extension MemoriesCalendarPostImageView { + + func prepareForReuse() { + imageView.image = nil + missionText.setHidden(hidden: true) + } + + func setPostImage(imageUrl url: String) { + imageView.kf.setImage(with: URL(string: url)!) + } + + func setMissionText(text: String?) { + missionText.setHidden(hidden: false) + missionText.setMissionText(text: text) + } + +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/BannerHostingViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/BannerHostingViewController.swift new file mode 100644 index 000000000..c160e0701 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/BannerHostingViewController.swift @@ -0,0 +1,36 @@ +// +// BannerViewController.swift +// App +// +// Created by 김건우 on 10/16/24. +// + +import SwiftUI + +import Then + +final class BannerHostingViewController: UIHostingController { + + private let _viewModel: BannerViewModel + + init(reactor: MemoriesCalendarPageReactor?) { + self._viewModel = BannerViewModel(reactor: reactor, state: .init()) + super.init(rootView: BannerView(viewModel: _viewModel)) + + self.view.backgroundColor = UIColor.clear + } + + @MainActor @preconcurrency required dynamic init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + var viewModel: BannerViewModel { + get { _viewModel } + set { } + } + + func updateState(_ state: BannerViewModel.State) { + viewModel.updateState(state: state) + } + +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/DailyCalendarViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/DailyCalendarViewController.swift index f10922acc..698286526 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/DailyCalendarViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/DailyCalendarViewController.swift @@ -19,32 +19,34 @@ import RxDataSources import SnapKit import Then -fileprivate typealias _Str = CalendarStrings -public final class DailyCalendarViewController: TempNavigationViewController { +public final class DailyCalendarViewController: BBNavigationViewController { + + // MARK: - Typealias + + typealias RxDataSource = RxCollectionViewSectionedReloadDataSource + + // MARK: - Views - private let imageView: UIImageView = UIImageView() + + private let backgroundImage: UIImageView = UIImageView() private let calendarView: FSCalendar = FSCalendar() - private lazy var postCollectionView: UICollectionView = UICollectionView( - frame: .zero, - collectionViewLayout: orthogonalCompositionalLayout - ) - private let reactionViewController: ReactionViewController = ReactionViewControllerWrapper(type: .calendar, postListData: .empty).makeViewController() - private let fireLottieView: LottieView = LottieView(with: .fire, contentMode: .scaleAspectFill) + private lazy var collectionView: UICollectionView = UICollectionView(frame: .zero,collectionViewLayout: compositionalLayout) + + private lazy var reactionViewController: ReactionViewController = makeReactionViewController() + // MARK: - Properties - private let visibleCellIndex: PublishRelay = PublishRelay() + private lazy var dataSource = prepareDatasource() - private let deepLinkRepo = DeepLinkRepository() + private let deepLinkRepo = DeepLinkRepository() // 삭제하기 + // MARK: - Lifecycles - public override func viewDidLoad() { - super.viewDidLoad() - } public override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) - App.Repository.deepLink.notification.accept(nil) + App.Repository.deepLink.notification.accept(nil) // 삭제하기 } // MARK: - Helpers @@ -55,228 +57,106 @@ public final class DailyCalendarViewController: TempNavigationViewController.just(reactor.initialState.date) - .flatMap { - Observable.merge( - Observable.just(Reactor.Action.dateSelected($0)), - Observable.just(Reactor.Action.requestDailyCalendar($0)) - ) - } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - let previousNextMonths: [String] = reactor.currentState.date.makePreviousNextMonth() - Observable.from(previousNextMonths) - .map { Reactor.Action.requestMonthlyCalendar($0) } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - navigationBar.rx.didTapLeftBarButton - .map { _ in Reactor.Action.popViewController } + Observable.just(()) + .map { Reactor.Action.viewDidLoad } .bind(to: reactor.action) .disposed(by: disposeBag) calendarView.rx.didSelect .distinctUntilChanged() - .flatMap { - Observable.merge( - Observable.just(Reactor.Action.dateSelected($0)), - Observable.just(Reactor.Action.requestDailyCalendar($0)) - ) - } + .throttle(RxInterval._300milliseconds, scheduler: RxScheduler.main) + .map { Reactor.Action.didSelect(date: $0) } .bind(to: reactor.action) .disposed(by: disposeBag) - calendarView.rx.calendarCurrentPageDidChange + let currentPageDidChange = calendarView.rx.calendarCurrentPageDidChange + .asDriver(onErrorJustReturn: .now) + + currentPageDidChange .distinctUntilChanged() - .withUnretained(self) - .subscribe { - $0.0.setupNavigationTitle($0.1) - } + .map { Reactor.Action.fetchMonthlyCalendar(date: $0) } + .drive(reactor.action) .disposed(by: disposeBag) - calendarView.rx.fetchCalendarResponseDidChange + currentPageDidChange .distinctUntilChanged() - .flatMap { - Observable.from($0) - .map { Reactor.Action.requestMonthlyCalendar($0) } - } - .bind(to: reactor.action) + .drive(with: self, onNext: { $0.setNavigationTitle($1) }) .disposed(by: disposeBag) calendarView.rx.boundingRectWillChange .distinctUntilChanged() - .withUnretained(self) - .subscribe { $0.0.updateCalendarViewConstraints($0.1) } + .bind(with: self) { $0.updateCalendarViewConstraints($1) } .disposed(by: disposeBag) - visibleCellIndex - .flatMap { - Observable.merge( - Observable.just(Reactor.Action.imageIndex($0)), - Observable.just(Reactor.Action.renewEmoji($0)) - ) - } + navigationBar.rx.didTapLeftBarButton + .map { _ in Reactor.Action.backToMonthly } .bind(to: reactor.action) .disposed(by: disposeBag) - } private func bindOutput(reactor: DailyCalendarViewReactor) { - reactor.state.map { $0.date } + reactor.state.map { $0.initialSelection } .distinctUntilChanged() - .withUnretained(self) - .subscribe { $0.0.calendarView.select($0.1, scrollToDate: true) } + .bind(with: self) { $0.calendarView.select($1, scrollToDate: true) } .disposed(by: disposeBag) - reactor.pulse(\.$displayMonthlyCalendar) - .withUnretained(self) - .subscribe { $0.0.calendarView.reloadData() } + reactor.pulse(\.$monthlyCalendars) + .bind(with: self) { owner, _ in owner.calendarView.reloadData() } .disposed(by: disposeBag) - let postResponse = reactor.pulse(\.$displayDailyCalendar).asDriver(onErrorJustReturn: []) + let dailyPosts = reactor.pulse(\.$dailyPostsDataSource) + .asDriver(onErrorJustReturn: []) - postResponse - .drive(postCollectionView.rx.items(dataSource: dataSource)) + dailyPosts + .drive(collectionView.rx.items(dataSource: dataSource)) .disposed(by: disposeBag) - postResponse - .drive(with: self) { - guard let items = $1.first?.items else { return } - - var indexPath = IndexPath(item: 0, section: 0) - // 알림으로 화면에 진입하면 - if let deepLink = reactor.currentState.notificationDeepLink { - let postId = deepLink.postId - guard let index = items.firstIndex(where: { post in - post.postId == postId - }) else { return } - indexPath = IndexPath(item: index, section: 0) - } - - // 일반 루트로 화면에 진입하면 - $0.postCollectionView.scrollToItem( - at: indexPath, - at: .centeredHorizontally, - animated: false - ) - } + dailyPosts + .drive(with: self, onNext: { owner, _ in owner.scrollCollectionView() }) .disposed(by: disposeBag) - reactor.state.compactMap { $0.imageUrl } + reactor.state.compactMap { $0.visiblePost } + .compactMap { URL(string: $0.postImageUrl) } .distinctUntilChanged() - .withUnretained(self) - .subscribe { - guard let url: URL = URL(string: $0.1) else { return } - KingfisherManager.shared.retrieveImage(with: url) { [unowned self] result in - switch result { - case let .success(value): - UIView.transition( - with: self.imageView, - duration: 0.15, - options: [.transitionCrossDissolve, .allowUserInteraction]) { - self.imageView.image = value.image - } - case .failure: - print("Kingfisher RetrieveImage Error") - } - } - } + .bind(to: backgroundImage.rx.kfImage) .disposed(by: disposeBag) reactor.state.compactMap { $0.visiblePost } .distinctUntilChanged() - .withUnretained(self) - .bind { owner, post in - let postListData = PostEntity( - postId: post.postId, - author: FamilyMemberProfileEntity(memberId: post.authorId, name: ""), - commentCount: post.commentCount, - emojiCount: post.emojiCount, - imageURL: post.postImageUrl, - content: post.postContent, - time: post.createdAt.toFormatString(with: .dashYyyyMMdd) + .bind(with: self) { + $0.reactionViewController.postListData.accept( + PostEntity( + postId: $1.postId, + author: .init(memberId: $1.authorId, name: ""), + commentCount: $1.commentCount, + emojiCount: $1.emojiCount, + imageURL: $1.postImageUrl, + content: $1.postContent, + time: $1.createdAt.toFormatString(with: .dashYyyyMMdd) + ) ) - owner.reactionViewController.postListData.accept(postListData) - } - .disposed(by: disposeBag) - - reactor.pulse(\.$shouldPushProfileViewController) - .delay(.milliseconds(500), scheduler: RxSchedulers.main) - .compactMap { $0 } - .bind(with: self) { owner, id in - owner.pushProfileViewController(memberId: id) } .disposed(by: disposeBag) - let allUploadedToastMessageView = reactor.pulse(\.$shouldPresentAllUploadedToastMessageView) - .asDriver(onErrorJustReturn: false) - - allUploadedToastMessageView - .filter { $0 } - .delay(RxConst.milliseconds100Interval) - .drive(with: self, onNext: { owner, _ in - owner.makeBibbiToastView( - text: _Str.allFamilyUploadedText, - image: DesignSystemAsset.fire.image - ) - }) - .disposed(by: disposeBag) - - allUploadedToastMessageView - .filter { $0 } - .delay(RxConst.milliseconds100Interval) - .drive(with: self, onNext: { owner, _ in - // 애니메이션 중이 아니라면 - if !owner.fireLottieView.isPlay { - owner.fireLottieView.play() - DispatchQueue.main.asyncAfter(deadline: .now() + 1.9) { - owner.fireLottieView.stop() - } - } - }) - .disposed(by: disposeBag) - - reactor.pulse(\.$shouldGenerateSelectionHaptic) - .filter { $0 } - .subscribe(onNext: { _ in Haptic.selection() }) - .disposed(by: disposeBag) - - // TODO: - 딥링크 코드 개선하기 - reactor.state.compactMap { $0.notificationDeepLink } - .distinctUntilChanged(at: \.postId) - .filter { $0.openComment } - .bind(with: self) { owner, deepLink in - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - let postCommentViewController = PostCommentDIContainer( - postId: deepLink.postId - ).makeViewController() - - owner.presentPostCommentSheet( - postCommentViewController, - from: .calendar - ) - } - } + NotificationCenter.default + .rx.notification(.didTapSelectableCameraButton) + .bind(with: self) { owner, _ in owner.pushCameraViewController(cameraType: .realEmoji)} .disposed(by: disposeBag) - - didTapCameraButtonNotifcationHandler() } public override func setupUI() { super.setupUI() - view.addSubviews(imageView) - imageView.addSubviews(calendarView, postCollectionView) - view.addSubview(fireLottieView) + view.addSubviews(backgroundImage) + backgroundImage.addSubviews(calendarView, collectionView) addChild(reactionViewController) - imageView.addSubview(reactionViewController.view) + backgroundImage.addSubview(reactionViewController.view) reactionViewController.didMove(toParent: self) } public override func setupAutoLayout() { super.setupAutoLayout() - imageView.snp.makeConstraints { + backgroundImage.snp.makeConstraints { $0.edges.equalToSuperview() } @@ -286,18 +166,14 @@ public final class DailyCalendarViewController: TempNavigationViewController RxCollectionViewSectionedReloadDataSource { - return RxCollectionViewSectionedReloadDataSource { datasource, collectionView, indexPath, post in - let cell = collectionView.dequeueReusableCell( - withReuseIdentifier: CalendarPostCell.id, - for: indexPath - ) as! CalendarPostCell - cell.reactor = CalendarPostCellDIContainer(post: post).makeReactor() - return cell - } - } - private func setupBlurEffect() { - let blurEffect = UIBlurEffect(style: .systemThinMaterialDark) - let visualEffectView = UIVisualEffectView(effect: blurEffect) - visualEffectView.frame = view.frame - imageView.insertSubview(visualEffectView, at: 0) - } - - private func setupNavigationTitle(_ date: Date) { + private func setNavigationTitle(_ date: Date) { navigationBar.navigationTitle = date.toFormatString(with: .yyyyM) } @@ -430,70 +277,80 @@ extension DailyCalendarViewController { view.layoutIfNeeded() } - private func pushCameraViewController(cameraType type: UploadLocation) { - let cameraViewController = CameraViewControllerWrapper(cameraType: type).viewController + // 다시 리팩토링하기 + private func scrollCollectionView() { + guard + let datasource = reactor?.currentState.dailyPostsDataSource.first, + let index = datasource.items.firstIndex(where: { + $0.postId == reactor?.currentState.visiblePost?.postId + }) else { return } + var indexPath = IndexPath(item: index, section: 0) + + // 삭제하기 + if let deepLink = reactor?.currentState.notificationDeepLink { + let postId = deepLink.postId + guard let index = datasource.items.firstIndex(where: { post in + post.postId == postId + }) else { return } + indexPath = IndexPath(item: index, section: 0) + } - navigationController?.pushViewController( - cameraViewController, - animated: true - ) + collectionView.scroll(to: indexPath) } - private func pushProfileViewController(memberId: String) { - let profileController = ProfileViewControllerWrapper( - memberId: memberId - ).viewController - - navigationController?.pushViewController( - profileController, - animated: true - ) + private func prepareDatasource() -> RxDataSource { + return RxDataSource { datasource, collectionView, indexPath, post in + let cell = collectionView.dequeueReusableCell( + withReuseIdentifier: MemoriesCalendarPostCell.id, + for: indexPath + ) as! MemoriesCalendarPostCell + cell.reactor = MemoriesCalendarPostCellReactor(postEntity: post) + return cell + } + } + + @available(*, deprecated, message: "삭제하기") + private func pushCameraViewController(cameraType type: UploadLocation) { + let vc = CameraViewControllerWrapper(cameraType: type).viewController + navigationController?.pushViewController(vc, animated: true) } + } extension DailyCalendarViewController { - private func didTapCameraButtonNotifcationHandler() { - NotificationCenter.default - .rx.notification(.didTapSelectableCameraButton) - .withUnretained(self) - .bind { owner, _ in - owner.pushCameraViewController(cameraType: .realEmoji) - } - .disposed(by: disposeBag) + + private func makeReactionViewController() -> ReactionViewController { + return ReactionViewControllerWrapper(type: .calendar, postListData: .empty).makeViewController() } + + } extension DailyCalendarViewController: FSCalendarDataSource { + public func calendar(_ calendar: FSCalendar, cellFor date: Date, at position: FSCalendarMonthPosition) -> FSCalendarCell { let cell = calendar.dequeueReusableCell( - withIdentifier: CalendarImageCell.id, + withIdentifier: MemoriesCalendarCell.id, for: date, at: position - ) as! CalendarImageCell + ) as! MemoriesCalendarCell - // 해당 일에 불러온 데이터가 없다면 - let yearMonth: String = date.toFormatString(with: .dashYyyyMM) + let yearMonth = date.toFormatString(with: .dashYyyyMM) guard let currentState = reactor?.currentState, - let monthlyEntity = currentState.displayMonthlyCalendar[yearMonth]?.filter({ $0.date.isEqual(with: date) }).first + let entity = currentState.monthlyCalendars[yearMonth]?.first(where: { $0.date.isEqual(with: date) }) else { - let emptyEntity = CalendarEntity( + let entity = MonthlyCalendarEntity( date: date, representativePostId: .none, representativeThumbnailUrl: .none, allFamilyMemebersUploaded: false ) - cell.reactor = CalendarImageCellDIContainer( - type: .week, - monthlyEntity: emptyEntity - ).makeReactor() + cell.reactor = MemoriesCalendarCellReactor(of: .daily, with: entity) return cell } - cell.reactor = CalendarImageCellDIContainer( - type: .week, - monthlyEntity: monthlyEntity, - isSelected: currentState.date.isEqual(with: date) - ).makeReactor() + cell.reactor = MemoriesCalendarCellReactor(of: .daily, with: entity, isSelected: currentState.initialSelection.isEqual(with: date)) return cell } + } diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/MonthlyCalendarViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/MonthlyCalendarViewController.swift index bf426b69c..f10c2a72b 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/MonthlyCalendarViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/MonthlyCalendarViewController.swift @@ -16,26 +16,25 @@ import RxDataSources import SnapKit import Then -// 지금 당장 BBNavigationViewController로 바꿔도 안됨 -// 왜냐하면, PopoverViewController Delegate 문제가 발생하기 때문! - -fileprivate typealias _Str = CalendarStrings -public final class MonthlyCalendarViewController: TempNavigationViewController { +public final class MonthlyCalendarViewController: BBNavigationViewController { + + // MARK: - Typealias + + typealias RxDataSource = RxCollectionViewSectionedReloadDataSource + + // MARK: - Views - private lazy var calendarCollectionView: UICollectionView = UICollectionView( - frame: .zero, - collectionViewLayout: orthogonalCompositionalLayout - ) + + private lazy var collectionView = UICollectionView(frame: .zero, collectionViewLayout: compositionalLayout) + // MARK: - Properties - private lazy var dataSource: RxCollectionViewSectionedReloadDataSource = prepareDatasource() - // MARK: - Lifecycles - public override func viewDidLoad() { - super.viewDidLoad() - } + private lazy var dataSource: RxDataSource = prepareDatasource() + // MARK: - Helpers + public override func bind(reactor: MonthlyCalendarViewReactor) { super.bind(reactor: reactor) bindInput(reactor: reactor) @@ -43,80 +42,30 @@ public final class MonthlyCalendarViewController: TempNavigationViewController.just(()) - .delay(RxConst.milliseconds100Interval, scheduler: RxSchedulers.main) - .bind(with: self) { owner, _ in - UIView.transition( - with: owner.calendarCollectionView, - duration: 0.25, - options: .transitionCrossDissolve - ) { [weak self] in - self?.calendarCollectionView.isHidden = false - } - } - .disposed(by: disposeBag) - - App.Repository.member.familyCreatedAt // 캘린더 페이지를 생성하는 코드 ex) 2024년 3월 ~ 9월 - .withUnretained(self) - .map { - guard let createdAt = $0.1 else { - let _20230101 = Date._20230101 - return $0.0.createCalendarItems(from: _20230101) - } - print("======= \(createdAt)") - print("======= \($0.0.createCalendarItems(from: createdAt))") - return $0.0.createCalendarItems(from: createdAt) - } - .map { Reactor.Action.addCalendarItems($0)} - .bind(to: reactor.action) - .disposed(by: disposeBag) - - - navigationBar.rx.didTapLeftBarButton - .map { _ in Reactor.Action.popViewController } + Observable.just(()) + .map { Reactor.Action.viewDidLoad } .bind(to: reactor.action) .disposed(by: disposeBag) } private func bindOutput(reactor: MonthlyCalendarViewReactor) { - reactor.pulse(\.$displayCalendar) - .bind(to: calendarCollectionView.rx.items(dataSource: dataSource)) - .disposed(by: disposeBag) - - reactor.state.compactMap { $0.initalCalendarPageIndexPath } - .bind(with: self) { owner, indexPath in - owner.scrollToLastIndexPath(indexPath) - } - .disposed(by: disposeBag) - - reactor.pulse(\.$shouldPushDailyCalendarViewController).compactMap { $0 } - .withUnretained(self) - .subscribe { $0.0.pushWeeklyCalendarViewController($0.1) } - .disposed(by: disposeBag) + let pageDatasource = reactor.pulse(\.$pageDatasource) + .asDriver(onErrorDriveWith: .empty()) - reactor.pulse(\.$shouldPresnetInfoPopover) - .withUnretained(self) - .subscribe { - $0.0.makeDescriptionPopoverView( - $0.0, - sourceView: $0.1, - text: _Str.infoText, - popoverSize: CGSize(width: 260, height: 62), - permittedArrowDrections: [.up] - ) - } + pageDatasource + .drive(collectionView.rx.items(dataSource: dataSource)) .disposed(by: disposeBag) } public override func setupUI() { super.setupUI() - view.addSubviews(calendarCollectionView) + view.addSubviews(collectionView) } public override func setupAutoLayout() { super.setupAutoLayout() - calendarCollectionView.snp.makeConstraints { + collectionView.snp.makeConstraints { $0.top.equalTo(navigationBar.snp.bottom) $0.horizontalEdges.equalToSuperview() $0.bottom.equalToSuperview() @@ -132,26 +81,30 @@ public final class MonthlyCalendarViewController: TempNavigationViewController RxCollectionViewSectionedReloadDataSource { - return RxCollectionViewSectionedReloadDataSource { datasource, collectionView, indexPath, yearMonth in - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CalendarCell.id, for: indexPath) as! CalendarCell - cell.reactor = CalendarCellReactor(yearMonth: yearMonth) - return cell - } - } - private func pushWeeklyCalendarViewController(_ date: Date) { - navigationController?.pushViewController( - WeeklyCalendarDIConatainer( - date: date - ).makeViewController(), - animated: true - ) - } - - private func scrollToLastIndexPath(_ indexPath: IndexPath) { - calendarCollectionView.layoutIfNeeded() - - print("======= \(dataSource[0].items.count - 1)") - - calendarCollectionView.scrollToItem( - at: indexPath, - at: .centeredHorizontally, - animated: false - ) - } } extension MonthlyCalendarViewController { - // TODO: - Item 생성 로직을 다른 곳으로 이동하기 - private func createCalendarItems(from startDate: Date, to endDate: Date = Date()) -> [String] { - var items: [String] = [] - let calendar: Calendar = Calendar.current - - let monthInterval: Int = getMonthInterval(from: startDate, to: endDate) - - for value in 0...monthInterval { - if let date = calendar.date(byAdding: .month, value: value, to: startDate) { - let yyyyMM = date.toFormatString(with: .dashYyyyMM) - items.append(yyyyMM) - } + + private func prepareDatasource() -> RxDataSource { + return RxDataSource { datasource, collectionView, indexPath, yearMonth in + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: MemoriesCalendarPageViewCell.id, for: indexPath) as! MemoriesCalendarPageViewCell + cell.reactor = MemoriesCalendarPageReactor(yearMonth: yearMonth) + return cell } - - return items } - private func getMonthInterval(from startDate: Date, to endDate: Date) -> Int { - let calendar: Calendar = Calendar.current - - let startComponents = calendar.dateComponents([.year, .month], from: startDate) - let endComponents = calendar.dateComponents([.year, .month], from: endDate) - - let yearDifference = endComponents.year! - startComponents.year! - let monthDifference = endComponents.month! - startComponents.month! - - let monthInterval = yearDifference * 12 + monthDifference - return monthInterval - } -} - -extension MonthlyCalendarViewController: UIPopoverPresentationControllerDelegate { - public func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle { - return .none - } } diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/ViewModel/BannerViewModel.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewModel/BannerViewModel.swift similarity index 77% rename from 14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/ViewModel/BannerViewModel.swift rename to 14th-team5-iOS/App/Sources/Presentation/Calendar/ViewModel/BannerViewModel.swift index d69a2b6b1..0e9d118de 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/ViewModel/BannerViewModel.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewModel/BannerViewModel.swift @@ -8,12 +8,14 @@ import Core import SwiftUI -public final class BannerViewModel: BaseViewModel { +public final class BannerViewModel: BaseViewModel { // MARK: - Properties + @Published var shimmeringActive: Bool = true // MARK: - State + public struct State: ViewModelState { var familyTopPercentage: Int = 0 var allFamilyMemberUploadedDays: Int = 0 @@ -23,7 +25,8 @@ public final class BannerViewModel: BaseViewModel.just(.setMemberName(memberName)) case .fetchProfileImage: diff --git a/14th-team5-iOS/App/Sources/Presentation/Comment/View/Cell/CommentCell.swift b/14th-team5-iOS/App/Sources/Presentation/Comment/View/Cell/CommentCell.swift index ebd4d01e4..c96b8f57c 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Comment/View/Cell/CommentCell.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Comment/View/Cell/CommentCell.swift @@ -85,7 +85,8 @@ final public class CommentCell: BaseTableViewCell { reactor.state.compactMap { $0.profileImageUrl } .distinctUntilChanged() - .bind(to: profileImage.rx.kingfisherImage) + .compactMap { $0 } + .bind(to: profileImage.rx.kfImage) .disposed(by: disposeBag) reactor.state.map { $0.comment.createdAt } diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/MissionTextView.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/MissionTextView.swift index a4c5bc1db..43ba79f59 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/MissionTextView.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/MissionTextView.swift @@ -89,3 +89,18 @@ final class MissionTextView: UIView { } } } + + +// MARK: - Extensions + +extension MissionTextView { + + func setHidden(hidden: Bool) { + self.isHidden = hidden + } + + func setMissionText(text: String?) { + missionLabel.text = text + } + +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/DIContainer/CalendarCellDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/Trash/DIContainer/CalendarCellDIContainer.swift similarity index 86% rename from 14th-team5-iOS/App/Sources/Presentation/Calendar/DIContainer/CalendarCellDIContainer.swift rename to 14th-team5-iOS/App/Sources/Presentation/Trash/DIContainer/CalendarCellDIContainer.swift index 0aaab6991..5d58f0e53 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/DIContainer/CalendarCellDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Trash/DIContainer/CalendarCellDIContainer.swift @@ -30,8 +30,8 @@ public final class CalendarCellDIContainer { return CalendarRepository() } - public func makeReactor() -> CalendarCellReactor { - return CalendarCellReactor( + public func makeReactor() -> MemoriesCalendarPageReactor { + return MemoriesCalendarPageReactor( yearMonth: yearMonth ) } diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/DIContainer/CalendarImageCellDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/Trash/DIContainer/CalendarImageCellDIContainer.swift similarity index 69% rename from 14th-team5-iOS/App/Sources/Presentation/Calendar/DIContainer/CalendarImageCellDIContainer.swift rename to 14th-team5-iOS/App/Sources/Presentation/Trash/DIContainer/CalendarImageCellDIContainer.swift index bb24b7dbf..9e96c473b 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/DIContainer/CalendarImageCellDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Trash/DIContainer/CalendarImageCellDIContainer.swift @@ -14,15 +14,15 @@ import Domain @available(*, deprecated) final public class CalendarImageCellDIContainer { // MARK: - Properties - public let type: CalendarImageCellReactor.CalendarType - public let monthlyEntity: CalendarEntity + public let type: MomoriesCalendarType + public let monthlyEntity: MonthlyCalendarEntity public let isSelected: Bool // MARK: - Intializer public init( - type: CalendarImageCellReactor.CalendarType, - monthlyEntity: CalendarEntity, + type: MomoriesCalendarType, + monthlyEntity: MonthlyCalendarEntity, isSelected: Bool = false ) { self.type = type @@ -39,10 +39,10 @@ final public class CalendarImageCellDIContainer { return CalendarRepository() } - public func makeReactor() -> CalendarImageCellReactor { - return CalendarImageCellReactor( - type: type, - monthlyEntity: monthlyEntity, + public func makeReactor() -> MemoriesCalendarCellReactor { + return MemoriesCalendarCellReactor( + of: type, + with: monthlyEntity, isSelected: isSelected ) } diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/DIContainer/CalendarPostCellDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/Trash/DIContainer/CalendarPostCellDIContainer.swift similarity index 81% rename from 14th-team5-iOS/App/Sources/Presentation/Calendar/DIContainer/CalendarPostCellDIContainer.swift rename to 14th-team5-iOS/App/Sources/Presentation/Trash/DIContainer/CalendarPostCellDIContainer.swift index d4637b9bd..744284a8d 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/DIContainer/CalendarPostCellDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Trash/DIContainer/CalendarPostCellDIContainer.swift @@ -28,9 +28,9 @@ public final class CalendarPostCellDIContainer { return MemberRepository() } - public func makeReactor() -> CalendarPostCellReactor { - return CalendarPostCellReactor( - post: post + public func makeReactor() -> MemoriesCalendarPostCellReactor { + return MemoriesCalendarPostCellReactor( + postEntity: post ) } } diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/DIContainer/CalendarPostDIContainer.swift b/14th-team5-iOS/App/Sources/Presentation/Trash/DIContainer/CalendarPostDIContainer.swift similarity index 97% rename from 14th-team5-iOS/App/Sources/Presentation/Calendar/DIContainer/CalendarPostDIContainer.swift rename to 14th-team5-iOS/App/Sources/Presentation/Trash/DIContainer/CalendarPostDIContainer.swift index a54919d66..f7613bca6 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/DIContainer/CalendarPostDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Trash/DIContainer/CalendarPostDIContainer.swift @@ -49,7 +49,7 @@ public final class WeeklyCalendarDIConatainer { public func makeReactor() -> DailyCalendarViewReactor { return DailyCalendarViewReactor( - date: date, + initialSelection: date, notificationDeepLink: deepLink // calendarUseCase: makeCalendarUseCase(), // provider: globalState diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/DIContainer/MonthlyCalendarDIConatainer.swift b/14th-team5-iOS/App/Sources/Presentation/Trash/DIContainer/MonthlyCalendarDIConatainer.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Calendar/DIContainer/MonthlyCalendarDIConatainer.swift rename to 14th-team5-iOS/App/Sources/Presentation/Trash/DIContainer/MonthlyCalendarDIConatainer.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBServices/CalendarGlobalState.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBServices/CalendarGlobalState.swift deleted file mode 100644 index c0e77f802..000000000 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBServices/CalendarGlobalState.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// CalendarGlobalState.swift -// Core -// -// Created by 김건우 on 12/9/23. -// - -import UIKit - -import RxSwift - -public enum CalendarEvent { - case pushCalendarPostVC(Date) - case didSelectDate(Date) - case didTapInfoButton(UIView) - case none -} - -public protocol CalendarGlobalStateType { - var event: BehaviorSubject { get } - - @discardableResult - func pushCalendarPostVC(_ date: Date) -> Observable - - @discardableResult - func didSelectDate(_ date: Date) -> Observable - - func didTapCalendarInfoButton(_ sourceView: UIView) -> Observable -} - -final public class CalendarGlobalState: BaseService, CalendarGlobalStateType { - public var event: BehaviorSubject = BehaviorSubject(value: .none) - - public func pushCalendarPostVC(_ date: Date) -> Observable { - event.onNext(.pushCalendarPostVC(date)) - return Observable.just(date) - } - - public func didSelectDate(_ date: Date) -> Observable { - event.onNext(.didSelectDate(date)) - return Observable.just(date) - } - - public func didTapCalendarInfoButton(_ sourceView: UIView) -> Observable { - event.onNext(.didTapInfoButton(sourceView)) - return Observable.just(()) - } -} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBServices/CalendarService.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBServices/CalendarService.swift new file mode 100644 index 000000000..75a4b843e --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBServices/CalendarService.swift @@ -0,0 +1,52 @@ +// +// CalendarGlobalState.swift +// Core +// +// Created by 김건우 on 12/9/23. +// + +import UIKit + +import RxSwift + +public enum CalendarEvent { + case didSelect(currentDate: Date) +} + +public protocol CalendarServiceType { + var event: BehaviorSubject { get } + + @discardableResult + func didSelect(date: Date) -> Observable + func getPreviousSelection() -> Date + func removePreviousSelection() +} + +final public class CalendarService: BaseService, CalendarServiceType { + + public var previousDate: Date = .distantPast + public var event = BehaviorSubject(value: .didSelect(currentDate: .distantPast)) + + /// 현재 선택한 날짜를 Reactor 전역에 방출합니다. + /// - Parameter date: 현재 선택한 날짜입니다. + /// - Returns: 현재 선택한 날짜를 담은 `Observable`을 반환합니다. + @discardableResult + public func didSelect(date: Date) -> Observable { + defer { self.previousDate = date } + event.onNext(.didSelect(currentDate: date)) + return Observable.just(date) + } + + /// 이전에 선택된 날짜를 반환합니다. + /// - Returns: 이전에 선택된 날짜입니다. + @available(*, deprecated) + public func getPreviousSelection() -> Date { + return previousDate + } + + @available(*, deprecated) + public func removePreviousSelection() { + self.previousDate = .distantPast + } + +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBServices/PostGlobalState.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBServices/PostGlobalState.swift index b8d5981d6..88b7ae966 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBServices/PostGlobalState.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBServices/PostGlobalState.swift @@ -11,7 +11,7 @@ import RxSwift public enum PostEvent { case pushProfileViewController(String) - case renewalPostCommentCount(Int) + case renewalCommentCount(Int) case receiveMissionContent(String) } @@ -43,7 +43,7 @@ final public class PostGlobalState: BaseService, PostGlobalStateType { } public func renewalPostCommentCount(_ count: Int) -> Observable { - event.onNext(.renewalPostCommentCount(count)) + event.onNext(.renewalCommentCount(count)) return Observable.just(count) } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ServiceProvider.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ServiceProvider.swift index e5f21266a..af03db146 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ServiceProvider.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ServiceProvider.swift @@ -12,11 +12,11 @@ public protocol ServiceProviderProtocol: AnyObject { var bbAlertService: BBAlertServiceType { get } var bbToastService: BBToastServiceType { get } + var calendarService: CalendarServiceType { get } var mainService: MainServiceType { get } var managementService: ManagementServiceType { get } var postGlobalState: PostGlobalStateType { get } - var calendarGlabalState: CalendarGlobalStateType { get } var toastGlobalState: ToastMessageGlobalStateType { get } var profileGlobalState: ProfileGlobalStateType { get } var timerGlobalState: TimerGlobalStateType { get } @@ -29,18 +29,16 @@ final public class ServiceProvider: ServiceProviderProtocol { public lazy var bbAlertService: any BBAlertServiceType = BBAlertService(provider: self) public lazy var bbToastService: any BBToastServiceType = BBToastService(provider: self) + public lazy var calendarService: CalendarServiceType = CalendarService(provider: self) public lazy var mainService: MainServiceType = MainService(provider: self) public lazy var managementService: any ManagementServiceType = ManagementService(provider: self) public lazy var postGlobalState: PostGlobalStateType = PostGlobalState(provider: self) - public lazy var calendarGlabalState: CalendarGlobalStateType = CalendarGlobalState(provider: self) public lazy var toastGlobalState: ToastMessageGlobalStateType = ToastMessageGlobalState(provider: self) public lazy var profileGlobalState: ProfileGlobalStateType = ProfileGlobalState(provider: self) - public lazy var timerGlobalState: TimerGlobalStateType = TimerGlobalState(provider: self) public lazy var realEmojiGlobalState: RealEmojiGlobalStateType = RealEmojiGlobalState(provider: self) public lazy var profilePageGlobalState: ProfileFeedGlobalStateType = ProfileFeedGlobalState(provider: self) - public init() { } } 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 30a91d43d..b5ef7375d 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/UserDefaultsWrapper/UserDefaultsWrapper.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBStorages/UserDefaultsWrapper/UserDefaultsWrapper.swift @@ -79,7 +79,7 @@ final public class UserDefaultsWrapper { _ value: T, forKey key: String ) where T: Encodable { - if let data = try? PropertyListEncoder().encode(value) { + if let data = try? JSONEncoder().encode(value) { set(data, forKey: key) } else { return @@ -144,7 +144,7 @@ final public class UserDefaultsWrapper { return nil } - if let value = try? PropertyListDecoder().decode(type, from: data) { + if let value = try? JSONDecoder().decode(type, from: data) { return value } else { return nil diff --git a/14th-team5-iOS/Core/Sources/Extensions/ObservableType+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/ObservableType+Ext.swift index 4b57268ca..6e1f93cd7 100644 --- a/14th-team5-iOS/Core/Sources/Extensions/ObservableType+Ext.swift +++ b/14th-team5-iOS/Core/Sources/Extensions/ObservableType+Ext.swift @@ -11,6 +11,16 @@ import RxSwift public extension ObservableType { + func map( + with object: O, + _ handler: @escaping (O, Element) -> E + ) -> Observable where O: AnyObject { + flatMap { [weak object] element in + guard let object else { return Observable.empty() } + return Observable.just(handler(object, element)) + } + } + /// 기존 `flatMap` 연산자에 with 매개변수를 붙인 새로운 연산자입니다. /// - Parameters: /// - object: 약한 참조하고자 하는 객체 diff --git a/14th-team5-iOS/Core/Sources/Extensions/Reactive+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/Reactive+Ext.swift index 5b8747331..74521f0ac 100644 --- a/14th-team5-iOS/Core/Sources/Extensions/Reactive+Ext.swift +++ b/14th-team5-iOS/Core/Sources/Extensions/Reactive+Ext.swift @@ -73,25 +73,6 @@ extension Reactive where Base: UILabel { } } - @available(*, deprecated, message: "삭제") - public var calendarTitleText: Binder { - Binder(self.base) { label, date in - var formatString: String = .none - if date.isEqual([.year], with: Date()) { - formatString = date.toFormatString(with: .m) - } else { - formatString = date.toFormatString(with: .yyyyM) - } - label.text = formatString - } - } - - @available(*, deprecated, message: "삭제") - public var memoryCountText: Binder { - Binder(self.base) { label, count in - label.text = "\(count)개의 추억" - } - } } extension Reactive where Base: WKWebView { @@ -105,6 +86,8 @@ extension Reactive where Base: WKWebView { } extension Reactive where Base: UIImageView { + + @available(*, deprecated, renamed: "kfImage") public var kingfisherImage: Binder { Binder(self.base) { imageView, urlString in imageView.kf.setImage( @@ -115,4 +98,12 @@ extension Reactive where Base: UIImageView { ) } } + + public var kfImage: Binder { + // TODO: - 이미지 캐시, 트랜지션 효과 추가 구현하기 + Binder(self.base) { imageView, url in + imageView.kf.setImage(with: url) + } + } + } diff --git a/14th-team5-iOS/Core/Sources/Extensions/String+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/String+Ext.swift index 9f9988bfd..8866b0a71 100644 --- a/14th-team5-iOS/Core/Sources/Extensions/String+Ext.swift +++ b/14th-team5-iOS/Core/Sources/Extensions/String+Ext.swift @@ -47,6 +47,9 @@ extension String { } extension String { + + /// 특정 `Index`에 위치한 문자열을 반환합니다. + /// - Returns: 해당 위치에 문자열이 있다면 `String?`을, 없다면 `nil`을 반환합니다. public subscript(_ index: Int) -> String? { guard index >= 0 && index < count else { return nil @@ -55,4 +58,5 @@ extension String { let index = self.index(self.startIndex, offsetBy: index) return String(self[index]) } + } diff --git a/14th-team5-iOS/Core/Sources/Extensions/UICollectionView+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/UICollectionView+Ext.swift new file mode 100644 index 000000000..505fbefc9 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Extensions/UICollectionView+Ext.swift @@ -0,0 +1,27 @@ +// +// UICollectionView+Ext.swift +// Core +// +// Created by 김건우 on 10/17/24. +// + +import UIKit + +public extension UICollectionView { + + /// 컬렉션 뷰를 특정 IndexPath로 스크롤합니다. + /// - Parameters: + /// - indexPath: 스크롤하고자 하는 IndexPath입니다. + /// - scrollPostion: 특정 셀을 스크롤할 위치입니다. 기본값은 `centeredHorizontally`입니다. + /// - animated: 스크롤 시 애니메이션 여부입니다. 기본값은 `false`입니다. + /// + /// - Authors: 김소월 + func scroll( + to indexPath: IndexPath, + at scrollPostion: ScrollPosition = .centeredHorizontally, + animated: Bool = false + ) { + self.scrollToItem(at: indexPath, at: scrollPostion, animated: animated) + } + +} diff --git a/14th-team5-iOS/Core/Sources/Extensions/UIView+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/UIView+Ext.swift index e13d697f1..4f29db37c 100644 --- a/14th-team5-iOS/Core/Sources/Extensions/UIView+Ext.swift +++ b/14th-team5-iOS/Core/Sources/Extensions/UIView+Ext.swift @@ -8,12 +8,17 @@ import UIKit extension UIView { + + /// 여러 `UIView`를 추가합니다. + /// - Parameter views: 가변 크기의 UIView입니다. public func addSubviews(_ views: UIView...) { views.forEach { self.addSubview($0) } } + /// 여러 `UIView`를 앞으로 다시 배치합니다. + /// - Parameter views: 가변 크기의 UIView입니다. public func bringSubviewToFronts(_ views: UIView...) { views.forEach { self.bringSubviewToFront($0) @@ -22,6 +27,8 @@ extension UIView { } extension UIView { + + @available(*, deprecated, message: "삭제") public func findSubview(of type: T.Type) -> T? { if let test = subviews.first(where: { $0 is T }) as? T { return test @@ -33,7 +40,7 @@ extension UIView { return nil } - + @available(*, deprecated, message: "삭제") public func asImage() -> UIImage { let renderer = UIGraphicsImageRenderer(bounds: bounds) return renderer.image { rendererContext in @@ -43,10 +50,23 @@ extension UIView { } extension UIView { + + @available(*, deprecated, renamed: "setBlurEffect") public func addBlurEffect(style: UIBlurEffect.Style) { let blurEffect = UIBlurEffect(style: style) let visualEffectView = UIVisualEffectView(effect: blurEffect) visualEffectView.frame = self.frame self.addSubview(visualEffectView) } + + /// 해당 `UIView`에 블러 효과를 적용합니다. + /// - Parameter style: `UIBlurEffect.Style` 타입의 스타일입니다. + public func setBlurEffect(style: UIBlurEffect.Style) { + let blurEffect = UIBlurEffect(style: style) + let effectView = UIVisualEffectView(effect: blurEffect) + effectView.frame = self.bounds + effectView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + self.insertSubview(effectView, at: 0) + } + } diff --git a/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/CalendarAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/CalendarAPIWorker.swift index c1afd82fe..0cf598adf 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/CalendarAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/CalendarAPIWorker.swift @@ -11,93 +11,49 @@ import Foundation import RxSwift -public typealias CalendarAPIWorker = CalendarAPIs.Worker +typealias CalendarAPIWorker = CalendarAPIs.Worker extension CalendarAPIs { - public final class Worker: APIWorker { - static let queue = { - ConcurrentDispatchQueueScheduler(queue: DispatchQueue(label: "CalendarAPIQueue", qos: .utility)) - }() - - public override init() { - super.init() - self.id = "CalendarAPIWorker" - } - } + final class Worker: BBRxAPIWorker { } } // MARK: - Extensions extension CalendarAPIWorker { + // MARK: - Fetch Banner Info - // MARK: - Fetch Calendar - - @available(*, deprecated) - public func fetchCalendarResponse(yearMonth: String) -> Single { - let spec = CalendarAPIs.calendarResponse(yearMonth).spec + public func fetchCalendarBanner(yearMonth: String) -> Observable { + let spec = CalendarAPIs.fetchBannerInfo(yearMonth).spec - return request(spec: spec) - .subscribe(on: Self.queue) - .map(ArrayResponseCalendarResponseDTO.self) - .catchAndReturn(nil) - .asSingle() + return request(spec) } - - // MARK: - Fetch Statistics Summary - public func fetchStatisticsSummary(yearMonth: String) -> Single { + public func fetchStatisticsSummary(yearMonth: String) -> Observable { let spec = CalendarAPIs.fetchStatisticsSummary(yearMonth).spec - return request(spec: spec) - .subscribe(on: Self.queue) - .map(FamilyMonthlyStatisticsResponseDTO.self) - .catchAndReturn(nil) - .asSingle() + return request(spec) } // MARK: - Fetch Monthly Calendar - public func fetchMonthlyCalendar(yearMonth: String) -> Single { + public func fetchMonthlyCalendar(yearMonth: String) -> Observable { let spec = CalendarAPIs.fetchMonthlyCalendar(yearMonth).spec - return request(spec: spec) - .subscribe(on: Self.queue) - .map(ArrayResponseMonthlyCalendarResponseDTO.self) - .catchAndReturn(nil) - .asSingle() + return request(spec) } // MARK: - Fetch Daily Calendar - public func fetchDailyCalendar(yearMonthDay: String) -> Single { + public func fetchDailyCalendar(yearMonthDay: String) -> Observable { let spec = CalendarAPIs.fetchDailyCalendar(yearMonthDay).spec - return request(spec: spec) - .subscribe(on: Self.queue) - .map(ArrayResponseDailyCalendarResponseDTO.self) - .catchAndReturn(nil) - .asSingle() - } - - - - - // MARK: - Fetch Banner - - public func fetchCalendarBanner(yearMonth: String) -> Single { - let spec = CalendarAPIs.fetchBanner(yearMonth).spec - - return request(spec: spec) - .subscribe(on: Self.queue) - .map(BannerResponseDTO.self) - .catchAndReturn(nil) - .asSingle() + return request(spec) } } diff --git a/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/CalendarAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/CalendarAPIs.swift index 9ee695640..d06960107 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/CalendarAPIs.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/CalendarAPIs.swift @@ -8,28 +8,42 @@ import Core import Foundation -public enum CalendarAPIs: API { - @available(*, deprecated) - case calendarResponse(String) - +enum CalendarAPIs: BBAPI { + case fetchBannerInfo(String) + case fetchStatisticsSummary(String) case fetchMonthlyCalendar(String) case fetchDailyCalendar(String) - case fetchStatisticsSummary(String) - case fetchBanner(String) - public var spec: APISpec { + var spec: Spec { switch self { - case let .calendarResponse(yearMonth): - return APISpec(method: .get, url: "\(BibbiAPI.hostApi)/calendar?type=MONTHLY&yearMonth=\(yearMonth)") - case let .fetchMonthlyCalendar(yearMonth): - return APISpec(method: .get, url: "\(BibbiAPI.hostApi)/calendar/monthly?yearMonth=\(yearMonth)") + return Spec( + method: .get, + path: "/calendar", + queryParameters: ["yearMonth": "\(yearMonth)", .type: "MONTHLY"] + ) + case let .fetchDailyCalendar(yearMonthDay): - return APISpec(method: .get, url: "\(BibbiAPI.hostApi)/calendar/daily?yearMonthDay=\(yearMonthDay)") + return Spec( + method: .get, + path: "/calendar/daily", + queryParameters: ["yearMonthDay": "\(yearMonthDay)"] + ) + case let .fetchStatisticsSummary(yearMonth): - return APISpec(method: .get, url: "\(BibbiAPI.hostApi)/calendar/summary?yearMonth=\(yearMonth)") - case let .fetchBanner(yearMonth): - return APISpec(method: .get, url: "\(BibbiAPI.hostApi)/calendar/banner?yearMonth=\(yearMonth)") + return Spec( + method: .get, + path: "/calendar/summary", + queryParameters: ["yearMonth": "\(yearMonth)"] + ) + + case let .fetchBannerInfo(yearMonth): + return Spec( + method: .get, + path: "/calendar/banner", + queryParameters: ["yearMonth": "\(yearMonth)"] + ) } } + } diff --git a/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/DataMapping/ArrayResponseDailyCalendarResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/DataMapping/ArrayResponseDailyCalendarResponseDTO.swift index a7da95b46..3b12821ca 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/DataMapping/ArrayResponseDailyCalendarResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/DataMapping/ArrayResponseDailyCalendarResponseDTO.swift @@ -59,8 +59,8 @@ extension ArrayResponseDailyCalendarResponseDTO.DailyCalendarResponseDTO { type: PostType(rawValue: type) ?? .survival, postId: postId, postImageUrl: postImageUrl, - postContent: postContent ?? .none, - missionContent: missionContent ?? .none, + postContent: postContent, + missionContent: missionContent, authorId: authorId, commentCount: commentCount, emojiCount: emojiCount, diff --git a/14th-team5-iOS/Data/Sources/APIs/Calendar/Repository/CalendarRepository.swift b/14th-team5-iOS/Data/Sources/APIs/Calendar/Repository/CalendarRepository.swift index 8e81011a6..f02a763f4 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Calendar/Repository/CalendarRepository.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Calendar/Repository/CalendarRepository.swift @@ -14,12 +14,12 @@ import RxSwift public final class CalendarRepository: CalendarRepositoryProtocol { // MARK: - Properties + public let disposeBag = DisposeBag() - // MARK: - API EndPoint - private let calendarApiWorker = CalendarAPIWorker() + // MARK: - APIWorker - // MARK: - Persistent Storage + private let calendarApiWorker = CalendarAPIWorker() // MARK: - Intializer public init() { } @@ -28,36 +28,24 @@ public final class CalendarRepository: CalendarRepositoryProtocol { // MARK: - Extensions extension CalendarRepository { - @available(*, deprecated) - public func fetchCalendarResponse(yearMonth: String) -> Observable { - return calendarApiWorker.fetchCalendarResponse(yearMonth: yearMonth) - .map { $0?.toDomain() } - .asObservable() + public func fetchCalendarBannerInfo(yearMonth: String) -> Observable { + return calendarApiWorker.fetchCalendarBanner(yearMonth: yearMonth) + .map { $0.toDomain() } } + public func fetchStatisticsSummary(yearMonth: String) -> Observable { + return calendarApiWorker.fetchStatisticsSummary(yearMonth: yearMonth) + .map { $0.toDomain() } + } - public func fetchMonthyCalendarResponse(yearMonth: String) -> Observable { + public func fetchMonthyCalendarResponse(yearMonth: String) -> Observable { return calendarApiWorker.fetchMonthlyCalendar(yearMonth: yearMonth) - .map { $0?.toDomain() } - .asObservable() + .map { $0.toDomain() } } - public func fetchDailyCalendarResponse(yearMonthDay: String) -> Observable { + public func fetchDailyCalendarResponse(yearMonthDay: String) -> Observable { return calendarApiWorker.fetchDailyCalendar(yearMonthDay: yearMonthDay) - .map { $0?.toDomain() } - .asObservable() - } - - public func fetchStatisticsSummary(yearMonth: String) -> Observable { - return calendarApiWorker.fetchStatisticsSummary(yearMonth: yearMonth) - .map { $0?.toDomain() } - .asObservable() + .map { $0.toDomain() } } - - public func fetchCalendarBanner(yearMonth: String) -> Observable { - return calendarApiWorker.fetchCalendarBanner(yearMonth: yearMonth) - .map { $0?.toDomain() } - .asObservable() - } - + } diff --git a/14th-team5-iOS/Data/Sources/APIs/Family/Repository/FamilyRepository.swift b/14th-team5-iOS/Data/Sources/APIs/Family/Repository/FamilyRepository.swift index 966e1c4db..f67d0f9eb 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Family/Repository/FamilyRepository.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Family/Repository/FamilyRepository.swift @@ -91,20 +91,20 @@ extension FamilyRepository { // MARK: - Fetch Family CreatedAt public func fetchFamilyCreatedAt() -> Observable { - guard - let familyId = familyUserDefaults.loadFamilyId() - else { return .error(NSError()) } // TODO: - Error 타입 정의하기 - - return familyApiWorker.fetchFamilyCreatedAt(familyId: familyId) - .map { $0?.toDomain() } - .do(onSuccess: { [weak self] in - guard let self else { return } - self.familyUserDefaults.saveFamilyCreatedAt($0?.createdAt) - - // TODO: - 리팩토링된 FamilyUserDefaults로 바꾸기 - App.Repository.member.familyCreatedAt.accept($0?.createdAt) - }) - .asObservable() + // 다시 리팩토링하기 + if let createdAt = familyUserDefaults.loadFamilyCreatedAt() { + return Observable.just(FamilyCreatedAtEntity(createdAt: createdAt)) + } else { + guard let familyId = familyUserDefaults.loadFamilyId() + else { return .error(NSError()) } // 에러 타입 다시 정의하기 + return familyApiWorker.fetchFamilyCreatedAt(familyId: familyId) + .map { $0?.toDomain() } + .do(onSuccess: { [weak self] in + guard let self else { return } + self.familyUserDefaults.saveFamilyCreatedAt($0?.createdAt) + }) + .asObservable() + } } // MARK: - Fetch Invitation Url diff --git a/14th-team5-iOS/Domain/Sources/Entities/Calendar/ArrayResponseDailyCalendarEntity.swift b/14th-team5-iOS/Domain/Sources/Entities/Calendar/ArrayResponseDailyCalendarEntity.swift index f7ecdc16c..d9d2ba4b1 100644 --- a/14th-team5-iOS/Domain/Sources/Entities/Calendar/ArrayResponseDailyCalendarEntity.swift +++ b/14th-team5-iOS/Domain/Sources/Entities/Calendar/ArrayResponseDailyCalendarEntity.swift @@ -20,8 +20,8 @@ public struct DailyCalendarEntity { public var type: PostType public var postId: String public var postImageUrl: String - public var postContent: String - public var missionContent: String + public var postContent: String? + public var missionContent: String? public var authorId: String public var commentCount: Int public var emojiCount: Int @@ -33,8 +33,8 @@ public struct DailyCalendarEntity { type: PostType, postId: String, postImageUrl: String, - postContent: String, - missionContent: String, + postContent: String?, + missionContent: String?, authorId: String, commentCount: Int, emojiCount: Int, diff --git a/14th-team5-iOS/Domain/Sources/Repositories/CalendarRepository.swift b/14th-team5-iOS/Domain/Sources/Repositories/CalendarRepository.swift index 19410247b..a124a7e80 100644 --- a/14th-team5-iOS/Domain/Sources/Repositories/CalendarRepository.swift +++ b/14th-team5-iOS/Domain/Sources/Repositories/CalendarRepository.swift @@ -10,11 +10,8 @@ import Foundation import RxSwift public protocol CalendarRepositoryProtocol { - @available(*, deprecated, renamed: "fetchMonthlyCalendarResponse") - func fetchCalendarResponse(yearMonth: String) -> Observable - - func fetchMonthyCalendarResponse(yearMonth: String) -> Observable - func fetchDailyCalendarResponse(yearMonthDay: String) -> Observable - func fetchStatisticsSummary(yearMonth: String) -> Observable - func fetchCalendarBanner(yearMonth: String) -> Observable + func fetchCalendarBannerInfo(yearMonth: String) -> Observable + func fetchStatisticsSummary(yearMonth: String) -> Observable + func fetchMonthyCalendarResponse(yearMonth: String) -> Observable + func fetchDailyCalendarResponse(yearMonthDay: String) -> Observable } diff --git a/14th-team5-iOS/Domain/Sources/Trash/Calendar/UseCases/CalendarUseCase.swift b/14th-team5-iOS/Domain/Sources/Trash/Calendar/UseCases/CalendarUseCase.swift index c1dc424a6..c39a0e280 100644 --- a/14th-team5-iOS/Domain/Sources/Trash/Calendar/UseCases/CalendarUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/Trash/Calendar/UseCases/CalendarUseCase.swift @@ -12,10 +12,10 @@ import RxSwift @available(*, deprecated) public protocol CalendarUseCaseProtocol { - func executeFetchCalednarResponse(yearMonth: String) -> Observable - func executeFetchDailyCalendarResponse(yearMonthDay: String) -> Observable - func executeFetchStatisticsSummary(yearMonth: String) -> Observable - func executeFetchCalendarBenner(yearMonth: String) -> Observable + func executeFetchCalednarResponse(yearMonth: String) -> Observable + func executeFetchDailyCalendarResponse(yearMonthDay: String) -> Observable + func executeFetchStatisticsSummary(yearMonth: String) -> Observable + func executeFetchCalendarBenner(yearMonth: String) -> Observable } @@ -27,19 +27,19 @@ public final class CalendarUseCase: CalendarUseCaseProtocol { self.calendarRepository = calendarRepository } - public func executeFetchCalednarResponse(yearMonth: String) -> Observable { - return calendarRepository.fetchCalendarResponse(yearMonth: yearMonth) + public func executeFetchCalednarResponse(yearMonth: String) -> Observable { + return .empty() } - public func executeFetchDailyCalendarResponse(yearMonthDay: String) -> Observable { + public func executeFetchDailyCalendarResponse(yearMonthDay: String) -> Observable { return calendarRepository.fetchDailyCalendarResponse(yearMonthDay: yearMonthDay) } - public func executeFetchStatisticsSummary(yearMonth: String) -> Observable { + public func executeFetchStatisticsSummary(yearMonth: String) -> Observable { return calendarRepository.fetchStatisticsSummary(yearMonth: yearMonth) } - public func executeFetchCalendarBenner(yearMonth: String) -> Observable { - return calendarRepository.fetchCalendarBanner(yearMonth: yearMonth) + public func executeFetchCalendarBenner(yearMonth: String) -> Observable { + return calendarRepository.fetchCalendarBannerInfo(yearMonth: yearMonth) } } diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Calendar/FetchCalendarBannerUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Calendar/FetchCalendarBannerUseCase.swift index 0b7edf197..93f1ff71c 100644 --- a/14th-team5-iOS/Domain/Sources/UseCases/Calendar/FetchCalendarBannerUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/Calendar/FetchCalendarBannerUseCase.swift @@ -10,7 +10,7 @@ import Foundation import RxSwift public protocol FetchCalendarBannerUseCaseProtocol { - func execute(yearMonth: String) -> Observable + func execute(yearMonth: String) -> Observable } public class FetchCalendarBannerUseCase: FetchCalendarBannerUseCaseProtocol { @@ -25,7 +25,7 @@ public class FetchCalendarBannerUseCase: FetchCalendarBannerUseCaseProtocol { // MARK: - Execute - public func execute(yearMonth: String) -> Observable { - calendarRepository.fetchCalendarBanner(yearMonth: yearMonth) + public func execute(yearMonth: String) -> Observable { + calendarRepository.fetchCalendarBannerInfo(yearMonth: yearMonth) } } diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Calendar/FetchDailyCalendarUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Calendar/FetchDailyCalendarUseCase.swift index 98080d055..8944a91dd 100644 --- a/14th-team5-iOS/Domain/Sources/UseCases/Calendar/FetchDailyCalendarUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/Calendar/FetchDailyCalendarUseCase.swift @@ -10,7 +10,7 @@ import Foundation import RxSwift public protocol FetchDailyCalendarUseCaseProtocol { - func execute(yearMonthDay: String) -> Observable + func execute(yearMonthDay: String) -> Observable } public class FetchDailyCalendarUseCase: FetchDailyCalendarUseCaseProtocol { @@ -24,7 +24,7 @@ public class FetchDailyCalendarUseCase: FetchDailyCalendarUseCaseProtocol { } // MARK: - Execute - public func execute(yearMonthDay: String) -> Observable { + public func execute(yearMonthDay: String) -> Observable { calendarRepository.fetchDailyCalendarResponse(yearMonthDay: yearMonthDay) } diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Calendar/FetchMonthlyCalendarUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Calendar/FetchMonthlyCalendarUseCase.swift index b55a1f1ee..9647dde71 100644 --- a/14th-team5-iOS/Domain/Sources/UseCases/Calendar/FetchMonthlyCalendarUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/Calendar/FetchMonthlyCalendarUseCase.swift @@ -10,7 +10,7 @@ import Foundation import RxSwift public protocol FetchMonthlyCalendarUseCaseProtocol { - func execute(yearMonth: String) -> Observable + func execute(yearMonth: String) -> Observable } public class FetchMonthlyCalendarUseCase: FetchMonthlyCalendarUseCaseProtocol { @@ -24,7 +24,7 @@ public class FetchMonthlyCalendarUseCase: FetchMonthlyCalendarUseCaseProtocol { } // MARK: - Execute - public func execute(yearMonth: String) -> Observable { + public func execute(yearMonth: String) -> Observable { calendarRepository.fetchMonthyCalendarResponse(yearMonth: yearMonth) } diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Calendar/FetchStatisticsSummaryUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Calendar/FetchStatisticsSummaryUseCase.swift index 62948f4a6..1dbc53078 100644 --- a/14th-team5-iOS/Domain/Sources/UseCases/Calendar/FetchStatisticsSummaryUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/Calendar/FetchStatisticsSummaryUseCase.swift @@ -10,7 +10,7 @@ import Foundation import RxSwift public protocol FetchStatisticsSummaryUseCaseProtocol { - func execute(yearMonth: String) -> Observable + func execute(yearMonth: String) -> Observable } @@ -25,7 +25,7 @@ public class FetchStatisticsSummaryUseCase: FetchStatisticsSummaryUseCaseProtoco } // MARK: - Execute - public func execute(yearMonth: String) -> Observable { + public func execute(yearMonth: String) -> Observable { calendarRepository.fetchStatisticsSummary(yearMonth: yearMonth) } } diff --git a/14th-team5-iOS/Domain/Sources/UseCases/My/FetchProfileImageUrlUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/My/FetchProfileImageUrlUseCase.swift index c22fbcb6e..54be9f1bc 100644 --- a/14th-team5-iOS/Domain/Sources/UseCases/My/FetchProfileImageUrlUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/My/FetchProfileImageUrlUseCase.swift @@ -8,7 +8,7 @@ import Foundation public protocol FetchProfileImageUrlUseCaseProtocol { - func execute(memberId: String) -> String? + func execute(memberId: String) -> URL? } public class FetchProfileImageUrlUseCase: FetchProfileImageUrlUseCaseProtocol { @@ -22,8 +22,15 @@ public class FetchProfileImageUrlUseCase: FetchProfileImageUrlUseCaseProtocol { } // MARK: - Execute - public func execute(memberId: String) -> String? { - myRepository.fetchProfileImageUrl(memberId: memberId) + + /// 주어진 멤버ID를 바탕으로 멤버 프로필 이미지 URL을 반환합니다. + /// - Parameter memberId: 멤버 ID입니다. + /// - Returns: 이미지 URL이 있다면 `URL?`을, 그렇지 않으면 `nil`을 반환합니다. + public func execute(memberId: String) -> URL? { + if let urlString = myRepository.fetchProfileImageUrl(memberId: memberId) { + return URL(string: urlString) + } + return nil } } diff --git a/14th-team5-iOS/Domain/Sources/UseCases/My/FetchUserNameUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/My/FetchUserNameUseCase.swift index 307763673..9de55d619 100644 --- a/14th-team5-iOS/Domain/Sources/UseCases/My/FetchUserNameUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/My/FetchUserNameUseCase.swift @@ -8,7 +8,7 @@ import Foundation public protocol FetchUserNameUseCaseProtocol { - func execute(memberId: String) -> String? + func execute(memberId: String) -> String } public class FetchUserNameUseCase: FetchUserNameUseCaseProtocol { @@ -22,8 +22,12 @@ public class FetchUserNameUseCase: FetchUserNameUseCaseProtocol { } // MARK: - Execute - public func execute(memberId: String) -> String? { - myRepository.fetchUserName(memberId: memberId) + + /// 매개변수로 주어진 멤버ID를 바탕으로 멤버 이름을 가져옵니다. + /// - Parameter memberId: 멤버 ID입니다. + /// - Returns: 멤버 이름 가져오기에 성공하면 멤버 이름을, 실패한다면 "알 수 없음" 문자열을 반환합니다. + public func execute(memberId: String) -> String { + myRepository.fetchUserName(memberId: memberId) ?? "알 수 없음" } } From 5234f456638a1b2f523d0f1152d843594aae4ed7 Mon Sep 17 00:00:00 2001 From: Zizi_Kim Date: Thu, 24 Oct 2024 19:53:23 +0900 Subject: [PATCH 239/263] =?UTF-8?q?feat:=20AlertService=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#692)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * delete: 필요없는 Service 삭제 * feat: AlertService 구현 * feat: 코드 리뷰 반영 --- .../Core/Sources/Bibbi/BBHelper.swift | 25 +++++- .../Bibbi/BBServices/AlertService.swift | 89 +++++++++++++++++++ .../Bibbi/BBServices/PostGlobalState.swift | 18 +--- .../Bibbi/BBServices/ProfileGlobalState.swift | 31 ------- .../Bibbi/BBServices/ServiceProvider.swift | 6 +- .../BBServices/ToastMessageGlobalState.swift | 46 ---------- 6 files changed, 118 insertions(+), 97 deletions(-) create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBServices/AlertService.swift delete mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBServices/ProfileGlobalState.swift delete mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBServices/ToastMessageGlobalState.swift diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBHelper.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBHelper.swift index 19734662e..cd97632a8 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBHelper.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBHelper.swift @@ -9,10 +9,14 @@ import UIKit final class BBHelper { + /// 가장 최상위 뷰 컨트롤러를 반환합니다. + /// UIWindow의 rootViewController에서부터 시작하여, 현재 계층 구조에서 최상위에 있는 뷰 컨트롤러를 탐색합니다. + /// - Returns: 현재 계층에서 최상위에 있는 뷰 컨트롤러입니다. 만약 윈도우나 뷰 컨트롤러가 없다면 `nil`을 반환합니다. + @available(*, deprecated, renamed: "topMostController") public static func topController() -> UIViewController? { if var topController = keyWindow()?.rootViewController { - while var presentedController = topController.presentedViewController { + while let presentedController = topController.presentedViewController { topController = presentedController } return topController @@ -22,6 +26,25 @@ final class BBHelper { return nil } + + /// 현재 최상위 뷰 컨트롤러를 재귀적으로 탐색하여 반환합니다. + /// UINavigationController과 같은 컨테이너 컨트롤러의 경우, 선택된 하위 뷰 컨트롤러를 기준으로 탐색을 진행합니다. + /// - Parameter base: 탐색의 시작점이 되는 UIViewController입니다. 기본값은 UIWindow의 rootViewController입니다. + /// - Returns: 탐색을 통해 얻어진 최상위 뷰 컨트롤러입니다. 기본값인 `keyWindow()?.rootViewController`에서 시작해 최상위 컨트롤러를 반환하며, 없다면 `nil`을 반환합니다. + public static func topMostController(base: UIViewController? = keyWindow()?.rootViewController) -> UIViewController? { + if let nav = base as? UINavigationController { + return topMostController(base: nav.visibleViewController) + } + + if let presented = base?.presentedViewController { + return topMostController(base: presented) + } + + return base + } + + /// 키 윈도우를 반환합니다. + /// - Returns: 키 원도우입니다. private static func keyWindow() -> UIWindow? { for scene in UIApplication.shared.connectedScenes { guard diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBServices/AlertService.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBServices/AlertService.swift new file mode 100644 index 000000000..8831467d0 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBServices/AlertService.swift @@ -0,0 +1,89 @@ +// +// AlertService.swift +// Core +// +// Created by 김건우 on 10/23/24. +// + +import UIKit + +import RxSwift + +// MARK: - AlertActionType + +public protocol AlertActionType { + + /// 버튼의 타이틀입니다. 필수 구현입니다. + var title: String? { get } + + /// 버튼의 스타일입니다. 선택 구현이며, 기본값은 `.default`입니다. + var style: UIAlertAction.Style { get } + +} + +public extension AlertActionType { + var style: UIAlertAction.Style { + return .default + } +} + + +// MARK: - AlertServiceType + +public protocol AlertServiceType { + @discardableResult + func show( + title: String?, + message: String?, + preferredStyle style: UIAlertController.Style, + actions: [Action] + ) -> Observable where Action: AlertActionType +} + +public extension AlertServiceType { + + /// Alert를 생성합니다. + /// - Parameters: + /// - title: Alert의 타이틀입니다. + /// - message: Alert의 메시지입니다. + /// - style: Alert의 스타일입니다. + /// - actions: `AlertActionType` 프로토콜을 준수하는 객체 배열입니다. + /// - Returns: `AlertActionType` 프로토콜을 준수하는 객체 타입을 방출하는 `Observable`을 반환합니다, + @discardableResult + func show( + title: String?, + message: String?, + preferredStyle style: UIAlertController.Style = .alert, + actions: [Action] + ) -> Observable where Action: AlertActionType { + + return Observable.create { observer in + let alert = UIAlertController( + title: title, + message: message, + preferredStyle: style + ) + + for action in actions { + alert.addAction( + UIAlertAction(title: action.title, style: action.style) { _ in + observer.onNext(action) + } + ) + } + + BBHelper.topMostController()?.present(alert, animated: true) + + return Disposables.create { + alert.dismiss(animated: true) + } + } + + } + +} + + +// MARK: - AlertService + +public final class AlertService: BaseService, AlertServiceType { } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBServices/PostGlobalState.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBServices/PostGlobalState.swift index 88b7ae966..b66b49841 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBServices/PostGlobalState.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBServices/PostGlobalState.swift @@ -16,16 +16,12 @@ public enum PostEvent { } public protocol PostGlobalStateType { - var input: BehaviorSubject<(String, String)> { get } + var event: PublishSubject { get } @discardableResult func pushProfileViewController(_ memberId: String) -> Observable - @discardableResult - func storeCommentText(_ postId: String, text: String) -> Observable<(String, String)> - func clearCommentText() - @discardableResult func renewalPostCommentCount(_ count: Int) -> Observable @@ -34,9 +30,10 @@ public protocol PostGlobalStateType { } final public class PostGlobalState: BaseService, PostGlobalStateType { - public var input: BehaviorSubject<(String, String)> = BehaviorSubject<(String, String)>(value: ("", "")) + public var event: PublishSubject = PublishSubject() + @available(*, deprecated, message: "Navigator를 사용하세요.") public func pushProfileViewController(_ memberId: String) -> Observable { event.onNext(.pushProfileViewController(memberId)) return Observable.just(memberId) @@ -47,15 +44,6 @@ final public class PostGlobalState: BaseService, PostGlobalStateType { return Observable.just(count) } - public func storeCommentText(_ postId: String, text: String) -> Observable<(String, String)> { - input.onNext((postId, text)) - return Observable<(String, String)>.just((postId, text)) - } - - public func clearCommentText() { - input.onNext((.none, .none)) - } - public func missionContentText(_ content: String) -> Observable { event.onNext(.receiveMissionContent(content)) return Observable.just(content) diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ProfileGlobalState.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ProfileGlobalState.swift deleted file mode 100644 index 473b1bf4d..000000000 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ProfileGlobalState.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// ProfileGlobalState.swift -// Core -// -// Created by 김건우 on 2/12/24. -// - -import UIKit - -import RxSwift - -public enum ProfileEvent { - case refreshFamilyMembers -} - -public protocol ProfileGlobalStateType { - var event: PublishSubject { get } - - @discardableResult - func refreshFamilyMembers() -> Observable -} - -final public class ProfileGlobalState: BaseService, ProfileGlobalStateType { - public var event: PublishSubject = PublishSubject() - - public func refreshFamilyMembers() -> Observable { - event.onNext(.refreshFamilyMembers) - return Observable.just(()) - } -} - diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ServiceProvider.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ServiceProvider.swift index af03db146..2a53263ee 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ServiceProvider.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ServiceProvider.swift @@ -9,6 +9,7 @@ import Foundation public protocol ServiceProviderProtocol: AnyObject { + var alertService: AlertServiceType { get } var bbAlertService: BBAlertServiceType { get } var bbToastService: BBToastServiceType { get } @@ -17,8 +18,6 @@ public protocol ServiceProviderProtocol: AnyObject { var managementService: ManagementServiceType { get } var postGlobalState: PostGlobalStateType { get } - var toastGlobalState: ToastMessageGlobalStateType { get } - var profileGlobalState: ProfileGlobalStateType { get } var timerGlobalState: TimerGlobalStateType { get } var realEmojiGlobalState: RealEmojiGlobalStateType { get } var profilePageGlobalState: ProfileFeedGlobalStateType { get } @@ -26,6 +25,7 @@ public protocol ServiceProviderProtocol: AnyObject { final public class ServiceProvider: ServiceProviderProtocol { + public lazy var alertService: any AlertServiceType = AlertService(provider: self) public lazy var bbAlertService: any BBAlertServiceType = BBAlertService(provider: self) public lazy var bbToastService: any BBToastServiceType = BBToastService(provider: self) @@ -34,8 +34,6 @@ final public class ServiceProvider: ServiceProviderProtocol { public lazy var managementService: any ManagementServiceType = ManagementService(provider: self) public lazy var postGlobalState: PostGlobalStateType = PostGlobalState(provider: self) - public lazy var toastGlobalState: ToastMessageGlobalStateType = ToastMessageGlobalState(provider: self) - public lazy var profileGlobalState: ProfileGlobalStateType = ProfileGlobalState(provider: self) public lazy var timerGlobalState: TimerGlobalStateType = TimerGlobalState(provider: self) public lazy var realEmojiGlobalState: RealEmojiGlobalStateType = RealEmojiGlobalState(provider: self) public lazy var profilePageGlobalState: ProfileFeedGlobalStateType = ProfileFeedGlobalState(provider: self) diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ToastMessageGlobalState.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ToastMessageGlobalState.swift deleted file mode 100644 index d65034178..000000000 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ToastMessageGlobalState.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// ToastGlobalState.swift -// Core -// -// Created by 김건우 on 1/3/24. -// - -import Foundation - -import RxSwift - -@available(*, deprecated, renamed: "BBToastService") -public enum ToastMessageEvent { - case showAllFamilyUploadedToastView(Bool) -} - -public protocol ToastMessageGlobalStateType { - var lastSelectedDate: Date { get set } - var event: BehaviorSubject { get } - - @discardableResult - func showAllFamilyUploadedToastMessageView(selection date: Date) -> Observable - - func clearToastMessageEvent() - func clearLastSelectedDate() -} - -@available(*, deprecated, renamed: "BBToastService") -final public class ToastMessageGlobalState: BaseService, ToastMessageGlobalStateType { - public var lastSelectedDate: Date = .distantFuture - public var event: BehaviorSubject = BehaviorSubject(value: .showAllFamilyUploadedToastView(false)) - - public func showAllFamilyUploadedToastMessageView(selection date: Date) -> Observable { - lastSelectedDate = date - event.onNext(.showAllFamilyUploadedToastView(true)) - return Observable.just(true) - } - - public func clearToastMessageEvent() { - event.onNext(.showAllFamilyUploadedToastView(false)) - } - - public func clearLastSelectedDate() { - lastSelectedDate = .distantFuture - } -} From aaa5872415197f76b04d1b5e11ccde2d55b61b98 Mon Sep 17 00:00:00 2001 From: g_mi <62610032+akrudal@users.noreply.github.com> Date: Wed, 6 Nov 2024 21:19:46 +0900 Subject: [PATCH 240/263] fix: qa issues(#696) (#697) --- .../Account/AccountSignUp/Reactor/AccountSignUpReactor.swift | 2 +- .../Core/Sources/Bibbi/BBCommons/BBAlert/BBAlert.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/Reactor/AccountSignUpReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/Reactor/AccountSignUpReactor.swift index c95243ec3..dc402b8db 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/Reactor/AccountSignUpReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/Reactor/AccountSignUpReactor.swift @@ -193,7 +193,7 @@ extension AccountSignUpReactor { case .setDayValue(let day): if let day = day { newState.day = day - newState.isValidDay = day < 31 + newState.isValidDay = day <= 31 } else { newState.isValidDay = false } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlert.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlert.swift index 4156c5d27..4991b3f2f 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlert.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlert.swift @@ -277,7 +277,7 @@ public class BBAlert { case .takePhoto: let actions = [ - BBAlertAction(title: "셍존 신고 먼저하기", handler: action), + BBAlertAction(title: "생존 신고 먼저하기", handler: action), BBAlertAction(title: "다음에 하기", style: .cancel) ] let viewConfig = BBAlertViewConfiguration( From 19493e7f8033df40605cd8bf58eaf96a3851d337 Mon Sep 17 00:00:00 2001 From: g_mi <62610032+akrudal@users.noreply.github.com> Date: Wed, 6 Nov 2024 21:20:38 +0900 Subject: [PATCH 241/263] =?UTF-8?q?fix:=20QA=20issue=EB=A5=BC=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=ED=95=A9=EB=8B=88=EB=8B=A4(#696)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Account/AccountSignUp/Reactor/AccountSignUpReactor.swift | 2 +- .../Core/Sources/Bibbi/BBCommons/BBAlert/BBAlert.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/Reactor/AccountSignUpReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/Reactor/AccountSignUpReactor.swift index c95243ec3..dc402b8db 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/Reactor/AccountSignUpReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/Reactor/AccountSignUpReactor.swift @@ -193,7 +193,7 @@ extension AccountSignUpReactor { case .setDayValue(let day): if let day = day { newState.day = day - newState.isValidDay = day < 31 + newState.isValidDay = day <= 31 } else { newState.isValidDay = false } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlert.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlert.swift index 4156c5d27..4991b3f2f 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlert.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlert.swift @@ -277,7 +277,7 @@ public class BBAlert { case .takePhoto: let actions = [ - BBAlertAction(title: "셍존 신고 먼저하기", handler: action), + BBAlertAction(title: "생존 신고 먼저하기", handler: action), BBAlertAction(title: "다음에 하기", style: .cancel) ] let viewConfig = BBAlertViewConfiguration( From b9add61f435f6f77a728b8010777886355c07964 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=84?= Date: Wed, 6 Nov 2024 21:32:14 +0900 Subject: [PATCH 242/263] =?UTF-8?q?fix:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EB=B2=84=ED=8A=BC=20=ED=81=B4=EB=A6=AD=EC=8B=9C=20touch=20even?= =?UTF-8?q?t=20cancel=20=ED=98=84=EC=83=81=20=EC=88=98=EC=A0=95=ED=95=B4?= =?UTF-8?q?=EC=9A=94=20(#695)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: SplashViewController print 구문 제거 - BBButton StackView isUserInteractionEnable fasle 처리 * fix: SplashViewController 화면 전환 로직 제거 - SplashReactor SplashNavigator 추가 및 화면 전환 로직 추가 * fix: SplashReactor Main화면 가는 함수 추가 - Bundle+Ext, API 파일 AppKey 수정 - FamilyInfoUserDefaults familyId, familyName String 타입 어노테이션 추가 * fix: Tuist CFBundleShortVersionString 1.2.4로 수정 - Marketing Version String 1.2.4로 수정 --- 14th-team5-iOS/App/Project.swift | 2 +- .../Presentation/Splash/SplashReactor.swift | 29 ++++++++++++++----- .../Splash/SplashViewController.swift | 29 ------------------- .../Bibbi/BBCommons/BBButton/BBButton.swift | 1 + .../Core/Sources/Extensions/Bundle+Ext.swift | 2 +- .../Core/Sources/Trash/BBNetwork/API.swift | 2 +- .../FamilyUserDefaults.swift | 8 ++--- .../Project+Templates.swift | 2 +- 8 files changed, 30 insertions(+), 45 deletions(-) diff --git a/14th-team5-iOS/App/Project.swift b/14th-team5-iOS/App/Project.swift index cb83f420b..3d188ab46 100644 --- a/14th-team5-iOS/App/Project.swift +++ b/14th-team5-iOS/App/Project.swift @@ -19,7 +19,7 @@ private let targets: [Target] = [ "CFBundleDisplayName": .string("Bibbi"), "CFBundleVersion": .string("1"), "CFBuildVersion": .string("0"), - "CFBundleShortVersionString": .string("1.2.3"), + "CFBundleShortVersionString": .string("1.2.4"), "UILaunchStoryboardName": .string("LaunchScreen"), "UISupportedInterfaceOrientations": .array([.string("UIInterfaceOrientationPortrait")]), "UIUserInterfaceStyle": .string("Dark"), diff --git a/14th-team5-iOS/App/Sources/Presentation/Splash/SplashReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Splash/SplashReactor.swift index fd1df3bdb..f15827500 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Splash/SplashReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Splash/SplashReactor.swift @@ -14,6 +14,8 @@ import RxSwift import Data public final class SplashReactor: Reactor { + @Navigator var splashNavigator: SplashNavigatorProtocol + // MARK: - Action public enum Action { case viewDidLoad @@ -49,8 +51,8 @@ public final class SplashReactor: Reactor { case .viewDidLoad: return meRepository.getAppVersion() .asObservable() - .flatMap { appVersionInfo in - + .withUnretained(self) + .flatMap { owner, appVersionInfo in guard let appVersionInfo = appVersionInfo else { return Observable.just(Mutation.setUpdateNeeded(nil)) } @@ -61,6 +63,7 @@ public final class SplashReactor: Reactor { App.Repository.token.accessToken .flatMap { token -> Observable in guard let _ = token else { + owner.splashNavigator.toSignIn() return Observable.just(Mutation.setMemberInfo(nil)) } @@ -69,17 +72,27 @@ public final class SplashReactor: Reactor { .withUnretained(self) .flatMap { owner, memberInfo in guard let memberInfo = memberInfo, - let familyId = memberInfo.familyId else { + let _ = memberInfo.familyId else { + owner.splashNavigator.toSignIn() return Observable.just(Mutation.setMemberInfo(nil)) } return owner.fetchFamilyCreatedAtUseCase.execute() .withUnretained(self) - .flatMap { - guard - let createdAt = $0.1 - else { return Observable.just(.setMemberInfo(nil)) } - return Observable.just(.setMemberInfo(memberInfo)) + .flatMap { owner, familyInfo -> Observable in + + if memberInfo.familyId != nil { + if UserDefaults.standard.inviteCode != nil { + owner.splashNavigator.toJoined() + return .just(.setMemberInfo(memberInfo)) + } else { + owner.splashNavigator.toHome() + return .just(.setMemberInfo(nil)) + } + } + + owner.splashNavigator.toJoinFamily() + return .just(.setMemberInfo(memberInfo)) } } } diff --git a/14th-team5-iOS/App/Sources/Presentation/Splash/SplashViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Splash/SplashViewController.swift index 5c9f0b368..334931ab9 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Splash/SplashViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Splash/SplashViewController.swift @@ -68,13 +68,6 @@ public final class SplashViewController: BaseViewController { .bind(onNext: { $0.0.openAppStore() }) .disposed(by: disposeBag) - reactor.pulse(\.$memberInfo) - .skip(1) - .withUnretained(self) - .observe(on: RxSchedulers.main) - .bind(onNext: { $0.0.showNextPage(with: $0.1)}) - .disposed(by: disposeBag) - reactor.pulse(\.$updatedNeeded) .skip(1) .filter { $0 == nil } @@ -91,28 +84,6 @@ public final class SplashViewController: BaseViewController { } } - private func showNextPage(with member: MemberInfo?) { - @Navigator var splashNavigator: SplashNavigatorProtocol - print("memberId: \(member)") - guard let member = member else { - splashNavigator.toSignIn() - return - } - print("member FamilYId: \(member.familyId)") - if let _ = member.familyId { - if UserDefaults.standard.inviteCode != nil { - splashNavigator.toJoined() - } else { - splashNavigator.toHome() - return - } - return - } else { - splashNavigator.toJoinFamily() - return - } - } - private func showUpdateAlert(_ appVersionInfo: AppVersionInfo?) { let updateAlertController = UIAlertController( title: "업데이트가 필요해요", diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBButton/BBButton.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBButton/BBButton.swift index 63704aa80..46faf7766 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBButton/BBButton.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBButton/BBButton.swift @@ -81,6 +81,7 @@ public class BBButton: UIButton { $0.spacing = 4 $0.distribution = .fillProportionally $0.axis = .horizontal + $0.isUserInteractionEnabled = false } mainTitleLabel.do { diff --git a/14th-team5-iOS/Core/Sources/Extensions/Bundle+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/Bundle+Ext.swift index a814974a3..ab0e4667f 100644 --- a/14th-team5-iOS/Core/Sources/Extensions/Bundle+Ext.swift +++ b/14th-team5-iOS/Core/Sources/Extensions/Bundle+Ext.swift @@ -65,6 +65,6 @@ extension Bundle { } public var xAppKey: String { - "db3ca026-0f9c-415a-a250-c97807f54add" + "da91623d-fe55-4115-8e14-aa7581761963" } } diff --git a/14th-team5-iOS/Core/Sources/Trash/BBNetwork/API.swift b/14th-team5-iOS/Core/Sources/Trash/BBNetwork/API.swift index 26bc95141..adc6f6706 100644 --- a/14th-team5-iOS/Core/Sources/Trash/BBNetwork/API.swift +++ b/14th-team5-iOS/Core/Sources/Trash/BBNetwork/API.swift @@ -53,7 +53,7 @@ public enum BibbiAPI { public var value: String { switch self { case let .auth(token): return "Bearer \(token)" - case .xAppKey: return "db3ca026-0f9c-415a-a250-c97807f54add" // TODO: - 번들에서 가져오기 + case .xAppKey: return "da91623d-fe55-4115-8e14-aa7581761963" // 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/Data/Sources/Storages/UserDefaults/FamilyUserDefaults/FamilyUserDefaults.swift b/14th-team5-iOS/Data/Sources/Storages/UserDefaults/FamilyUserDefaults/FamilyUserDefaults.swift index 99e861c33..f64c845fc 100644 --- a/14th-team5-iOS/Data/Sources/Storages/UserDefaults/FamilyUserDefaults/FamilyUserDefaults.swift +++ b/14th-team5-iOS/Data/Sources/Storages/UserDefaults/FamilyUserDefaults/FamilyUserDefaults.swift @@ -87,9 +87,9 @@ final public class FamilyInfoUserDefaults: FamilyInfoUserDefaultsType { } public func loadFamilyId() -> String? { - guard - let familyId: String? = userDefaults[.familyId] - else { return nil } + guard let familyId: String = userDefaults[.familyId] else { + return nil + } return familyId } @@ -116,7 +116,7 @@ final public class FamilyInfoUserDefaults: FamilyInfoUserDefaultsType { public func loadFamilyName() -> String? { guard - let familyName: String? = userDefaults[.familyName] + let familyName: String = userDefaults[.familyName] else { return nil } return familyName } diff --git a/Tuist/ProjectDescriptionHelpers/Project+Templates.swift b/Tuist/ProjectDescriptionHelpers/Project+Templates.swift index 1c68726be..98094bae9 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.2.3", + "MARKETING_VERSION": "1.2.4", "CURRENT_PROJECT_VERSION": "1", "VERSIONING_SYSTEM": "apple-generic" ], From 58785181699f5310b273ead73fb168bd0b217038 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=84?= Date: Thu, 7 Nov 2024 00:04:31 +0900 Subject: [PATCH 243/263] =?UTF-8?q?fix:=20MyUserDefaults=20memberId,=20use?= =?UTF-8?q?rName=20Type=20Annotation=20String=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#700)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Storages/UserDefaults/MyUserDefaults/MyUserDefaults.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/14th-team5-iOS/Data/Sources/Storages/UserDefaults/MyUserDefaults/MyUserDefaults.swift b/14th-team5-iOS/Data/Sources/Storages/UserDefaults/MyUserDefaults/MyUserDefaults.swift index c9ec3fb14..f59b99fe4 100644 --- a/14th-team5-iOS/Data/Sources/Storages/UserDefaults/MyUserDefaults/MyUserDefaults.swift +++ b/14th-team5-iOS/Data/Sources/Storages/UserDefaults/MyUserDefaults/MyUserDefaults.swift @@ -32,7 +32,7 @@ final public class MyUserDefaults: MyUserDefaultsType { public func loadMemberId() -> String? { guard - let memberId: String? = userDefaults[.memberId] + let memberId: String = userDefaults[.memberId] else { return nil } return memberId } @@ -46,7 +46,7 @@ final public class MyUserDefaults: MyUserDefaultsType { public func loadUserName() -> String? { guard - let userName: String? = userDefaults[.userName] + let userName: String = userDefaults[.userName] else { return nil } return userName } From 406dd0e68e5b71db544a8ec5f005f7eec3a38e03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=84?= Date: Fri, 8 Nov 2024 00:40:54 +0900 Subject: [PATCH 244/263] =?UTF-8?q?fix:=20SplashViewReactor=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=EC=A0=84=ED=99=98=20issue=20=EC=88=98=EC=A0=95=20(?= =?UTF-8?q?#702)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: App.Repository 중복 이벤트 방출 막기 위한 Take operator 추가 * fix: SplashReactor 최상위 Closure context unowned self 코드 명시 * fix: github Action release 조건절 수정 --- .github/workflows/swift.yml | 2 +- .../Presentation/Splash/SplashReactor.swift | 27 ++++++++++--------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 53c94f7c6..c857f7a39 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -62,7 +62,7 @@ jobs: run: tuist generate - name: fastlane upload_prd_testflight - if: ${{ github.base_ref == 'release' && github.event_name == 'push' }} + if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/heads/release') }} 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/Sources/Presentation/Splash/SplashReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Splash/SplashReactor.swift index f15827500..acb0bb94b 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Splash/SplashReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Splash/SplashReactor.swift @@ -35,7 +35,7 @@ public final class SplashReactor: Reactor { } // MARK: - Properties - private let meRepository = MeUseCase(meRepository: MeAPIs.Worker()) // TODO: - Injected로 수정하기 + private let meRepository: MeUseCaseProtocol = MeUseCase(meRepository: MeAPIs.Worker()) // TODO: - Injected로 수정하기 @Injected var familyUseCase: FamilyUseCaseProtocol @Injected var fetchFamilyCreatedAtUseCase: FetchFamilyCreatedAtUseCaseProtocol @@ -45,14 +45,17 @@ public final class SplashReactor: Reactor { // MARK: - Intializer init() { } + deinit { + print(#function) + } + // MARK: - Mutate public func mutate(action: Action) -> Observable { switch action { case .viewDidLoad: return meRepository.getAppVersion() .asObservable() - .withUnretained(self) - .flatMap { owner, appVersionInfo in + .flatMap { [unowned self] appVersionInfo -> Observable in guard let appVersionInfo = appVersionInfo else { return Observable.just(Mutation.setUpdateNeeded(nil)) } @@ -63,35 +66,33 @@ public final class SplashReactor: Reactor { App.Repository.token.accessToken .flatMap { token -> Observable in guard let _ = token else { - owner.splashNavigator.toSignIn() + self.splashNavigator.toSignIn() return Observable.just(Mutation.setMemberInfo(nil)) } return self.meRepository.getMemberInfo() .asObservable() - .withUnretained(self) - .flatMap { owner, memberInfo in + .flatMap { memberInfo -> Observable in guard let memberInfo = memberInfo, let _ = memberInfo.familyId else { - owner.splashNavigator.toSignIn() + self.splashNavigator.toSignIn() return Observable.just(Mutation.setMemberInfo(nil)) } - return owner.fetchFamilyCreatedAtUseCase.execute() - .withUnretained(self) - .flatMap { owner, familyInfo -> Observable in + return self.fetchFamilyCreatedAtUseCase.execute() + .flatMap { familyInfo -> Observable in if memberInfo.familyId != nil { if UserDefaults.standard.inviteCode != nil { - owner.splashNavigator.toJoined() + self.splashNavigator.toJoined() return .just(.setMemberInfo(memberInfo)) } else { - owner.splashNavigator.toHome() + self.splashNavigator.toHome() return .just(.setMemberInfo(nil)) } } - owner.splashNavigator.toJoinFamily() + self.splashNavigator.toJoinFamily() return .just(.setMemberInfo(memberInfo)) } } From 8e80d50293fe8a606c8b9eb69b3cebe8cc2e4233 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=84?= Date: Wed, 20 Nov 2024 21:04:40 +0900 Subject: [PATCH 245/263] =?UTF-8?q?refactor:=20PostAPIWorker=20=EA=B8=B0?= =?UTF-8?q?=EC=A1=B4=20Network=20=EB=AA=A8=EB=93=88=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81=20(#707)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 게시물 API 관련 APIs 코드 리팩토링 * refactor: PostAPIs 일부 대체된 API EndPoint 제거 - PostListAPIWorker Post 관련 api Method 추가 - PostRepository 내부 Post API 호출 관련 메서드 추가 - Post 관련 DTO, Entity 추가 - FetchPostListUseCase 네이밍 수정 * fix: PostListRequestDTO 네이밍 수정 --- .../DataMapping/CreatePostRequestDTO.swift | 16 ++++ .../CreatePresignedURLRequestDTO.swift | 13 ++++ .../DataMapping/PostDetailResponseDTO.swift | 39 ++++++++++ .../DataMapping/PostListQueryDTO.swift | 35 +++++++++ .../DataMapping/PostListRequestDTO.swift | 18 ----- .../PostList/DataMapping/PostRequestDTO.swift | 20 ----- .../DataMapping/PostResponseDTO.swift | 27 ------- .../Post/PostList/PostAPI/PostAPIs.swift | 44 ++++++++--- .../PostList/PostAPI/PostListAPIWorker.swift | 78 +++++++++++++------ .../PostList/Repository/PostRepository.swift | 34 +++++++- .../CreatePostPresignedURLRequest.swift | 17 ++++ .../Entities/PostList/CreatePostQuery.swift | 17 ++++ .../Entities/PostList/CreatePostRequest.swift | 25 ++++++ .../Entities/PostList/PostDetailEntity.swift | 45 +++++++++++ .../Repositories/PostListRepository.swift | 7 +- .../Members/FetchMembersPostListUseCase.swift | 6 +- .../UseCases/Post/FetchPostListUseCase.swift | 4 +- 17 files changed, 338 insertions(+), 107 deletions(-) create mode 100644 14th-team5-iOS/Data/Sources/Trash/Post/PostList/DataMapping/CreatePostRequestDTO.swift create mode 100644 14th-team5-iOS/Data/Sources/Trash/Post/PostList/DataMapping/CreatePresignedURLRequestDTO.swift create mode 100644 14th-team5-iOS/Data/Sources/Trash/Post/PostList/DataMapping/PostDetailResponseDTO.swift create mode 100644 14th-team5-iOS/Data/Sources/Trash/Post/PostList/DataMapping/PostListQueryDTO.swift delete mode 100644 14th-team5-iOS/Data/Sources/Trash/Post/PostList/DataMapping/PostListRequestDTO.swift delete mode 100644 14th-team5-iOS/Data/Sources/Trash/Post/PostList/DataMapping/PostRequestDTO.swift delete mode 100644 14th-team5-iOS/Data/Sources/Trash/Post/PostList/DataMapping/PostResponseDTO.swift create mode 100644 14th-team5-iOS/Domain/Sources/Entities/PostList/CreatePostPresignedURLRequest.swift create mode 100644 14th-team5-iOS/Domain/Sources/Entities/PostList/CreatePostQuery.swift create mode 100644 14th-team5-iOS/Domain/Sources/Entities/PostList/CreatePostRequest.swift create mode 100644 14th-team5-iOS/Domain/Sources/Entities/PostList/PostDetailEntity.swift diff --git a/14th-team5-iOS/Data/Sources/Trash/Post/PostList/DataMapping/CreatePostRequestDTO.swift b/14th-team5-iOS/Data/Sources/Trash/Post/PostList/DataMapping/CreatePostRequestDTO.swift new file mode 100644 index 000000000..a3c072048 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/Trash/Post/PostList/DataMapping/CreatePostRequestDTO.swift @@ -0,0 +1,16 @@ +// +// CreatePostRequestDTO.swift +// Data +// +// Created by 김도현 on 11/19/24. +// + +import Foundation + + +public struct CreatePostRequestDTO: Encodable { + public let imageUrl: String + public let content: String + public let uploadTime: String +} + diff --git a/14th-team5-iOS/Data/Sources/Trash/Post/PostList/DataMapping/CreatePresignedURLRequestDTO.swift b/14th-team5-iOS/Data/Sources/Trash/Post/PostList/DataMapping/CreatePresignedURLRequestDTO.swift new file mode 100644 index 000000000..1fd666c82 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/Trash/Post/PostList/DataMapping/CreatePresignedURLRequestDTO.swift @@ -0,0 +1,13 @@ +// +// CreatePresignedURLRequestDTO.swift +// Data +// +// Created by 김도현 on 11/19/24. +// + +import Foundation + + +public struct CreatePresignedURLRequestDTO: Encodable { + public let imageName: String +} diff --git a/14th-team5-iOS/Data/Sources/Trash/Post/PostList/DataMapping/PostDetailResponseDTO.swift b/14th-team5-iOS/Data/Sources/Trash/Post/PostList/DataMapping/PostDetailResponseDTO.swift new file mode 100644 index 000000000..4b600f0b3 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/Trash/Post/PostList/DataMapping/PostDetailResponseDTO.swift @@ -0,0 +1,39 @@ +// +// PostDetailResponseDTO.swift +// Data +// +// Created by 김도현 on 11/19/24. +// + +import Foundation + +import Domain + +struct PostDetailResponseDTO: Decodable { + let postId: String + let authorId: String + let type: String + let missionId: String? + let commentCount: Int + let emojiCount: Int + let imageUrl: String + let content: String + let createdAt: String +} + + +extension PostDetailResponseDTO { + func toDomain() -> PostDetailEntity { + return .init( + postId: postId, + authorId: authorId, + type: PostType(rawValue: type) ?? .survival, + missionId: missionId, + commentCount: commentCount, + emojiCount: emojiCount, + imageUrl: imageUrl, + content: content, + createdAt: createdAt + ) + } +} diff --git a/14th-team5-iOS/Data/Sources/Trash/Post/PostList/DataMapping/PostListQueryDTO.swift b/14th-team5-iOS/Data/Sources/Trash/Post/PostList/DataMapping/PostListQueryDTO.swift new file mode 100644 index 000000000..34152ec06 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/Trash/Post/PostList/DataMapping/PostListQueryDTO.swift @@ -0,0 +1,35 @@ +// +// PostListQueryDTO.swift +// Data +// +// Created by 김도현 on 11/20/24. +// + +import Foundation + +public struct PostListQueryDTO: Encodable { + public let page: Int + public let size: Int + public let date: String + public let memberId: String? + public let type: String + public let sort: String + + + public init( + page: Int, + size: Int, + date: String, + memberId: String? = nil, + type: String, + sort: String + ) { + self.page = page + self.size = size + self.date = date + self.memberId = memberId + self.type = type + self.sort = sort + } + +} diff --git a/14th-team5-iOS/Data/Sources/Trash/Post/PostList/DataMapping/PostListRequestDTO.swift b/14th-team5-iOS/Data/Sources/Trash/Post/PostList/DataMapping/PostListRequestDTO.swift deleted file mode 100644 index bf875b4ba..000000000 --- a/14th-team5-iOS/Data/Sources/Trash/Post/PostList/DataMapping/PostListRequestDTO.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// PostListRequestDTO.swift -// Data -// -// Created by Kim dohyun on 6/6/24. -// - -import Foundation - - -struct PostListRequestDTO: Codable { - let page: Int - let size: Int - let date: String - let memberId: String? - let sort: String - let type: String -} diff --git a/14th-team5-iOS/Data/Sources/Trash/Post/PostList/DataMapping/PostRequestDTO.swift b/14th-team5-iOS/Data/Sources/Trash/Post/PostList/DataMapping/PostRequestDTO.swift deleted file mode 100644 index 2bd9a0bf6..000000000 --- a/14th-team5-iOS/Data/Sources/Trash/Post/PostList/DataMapping/PostRequestDTO.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// PostRequestDTO.swift -// Data -// -// Created by Kim dohyun on 6/6/24. -// - -import Foundation - -import Domain - -public struct PostRequestDTO: Codable { - let postId: String -} - -//extension PostRequestDTO { -// func toDomain() -> BrieftPostQuery { -// return .init(postId: postId) -// } -//} diff --git a/14th-team5-iOS/Data/Sources/Trash/Post/PostList/DataMapping/PostResponseDTO.swift b/14th-team5-iOS/Data/Sources/Trash/Post/PostList/DataMapping/PostResponseDTO.swift deleted file mode 100644 index 5b59fe3cb..000000000 --- a/14th-team5-iOS/Data/Sources/Trash/Post/PostList/DataMapping/PostResponseDTO.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// PostResponseDTO.swift -// Data -// -// Created by Kim dohyun on 6/6/24. -// - -import Foundation -import Domain - -struct PostResponseDTO: Codable { - let postId: String - let authorId: String - let commentCount: Int - let emojiCount: Int - let imageUrl: String - let content: String - let createdAt: String -} - -//extension PostResponseDTO { -// func toDomain() -> BriefPostEntity { -// let writer = FamilyUserDefaults.load(memberId: authorId) -// -// return .init(writer: writer, time: createdAt, imageURL: imageUrl, imageText: content, emojis: []) -// } -//} diff --git a/14th-team5-iOS/Data/Sources/Trash/Post/PostList/PostAPI/PostAPIs.swift b/14th-team5-iOS/Data/Sources/Trash/Post/PostList/PostAPI/PostAPIs.swift index adfa8eb13..0567cccc4 100644 --- a/14th-team5-iOS/Data/Sources/Trash/Post/PostList/PostAPI/PostAPIs.swift +++ b/14th-team5-iOS/Data/Sources/Trash/Post/PostList/PostAPI/PostAPIs.swift @@ -8,18 +8,42 @@ import Core import Foundation -public enum PostAPIs: API { - case fetchPostList - case fetchPostDetail(PostRequestDTO) +/// 해당 Posts API는 Swagger에 있는 **게시물 API** 기준으로 사용되는 API 입니다. +enum PostsAPIs: BBAPI { + /// 게시물 조회 API + case fetchPostList(query: PostListQueryDTO) + /// 게시물 생성 API + case createPost(type: String, body: CreatePostRequestDTO) + /// 게시물 단일 조회 API + case fetchPostDetail(postId: String) + /// 게시물 사진 Presigned URL 요청 API + case createPostPresignedURL(body: CreatePresignedURLRequestDTO) - public var spec: APISpec { + var spec: Spec { switch self { - case .fetchPostList: - let urlString = "\(BibbiAPI.hostApi)/posts" - return APISpec(method: .get, url: urlString) - case let .fetchPostDetail(query): - let urlString = "\(BibbiAPI.hostApi)/posts/\(query.postId)" - return APISpec(method: .get, url: urlString) + case let .fetchPostList(query): + return Spec( + method: .get, + path: "/posts", + queryParametersEncodable: query + ) + case let .createPost(type, body): + return Spec( + method: .post, + path: "/posts", + queryParameters: [ + .type: "\(type)" + ], + bodyParametersEncodable: body + ) + case let .fetchPostDetail(postId): + return Spec(method: .get, path: "/posts/\(postId)") + case let .createPostPresignedURL(body): + return Spec(method: .post, path: "/posts/image-upload-request", bodyParametersEncodable: body) } } + + public final class Worker: BBRxAPIWorker { + public init() { } + } } diff --git a/14th-team5-iOS/Data/Sources/Trash/Post/PostList/PostAPI/PostListAPIWorker.swift b/14th-team5-iOS/Data/Sources/Trash/Post/PostList/PostAPI/PostListAPIWorker.swift index af7fdbab9..3f84eec16 100644 --- a/14th-team5-iOS/Data/Sources/Trash/Post/PostList/PostAPI/PostListAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Trash/Post/PostList/PostAPI/PostListAPIWorker.swift @@ -13,31 +13,61 @@ import Domain import Alamofire import RxSwift -public typealias PostAPIWorker = PostAPIs.Worker -extension PostAPIs { - public final class Worker: APIWorker { - static let queue = { - ConcurrentDispatchQueueScheduler(queue: DispatchQueue(label: "PostListAPIQueue", qos: .utility)) - }() - - public override init() { - super.init() - self.id = "PostListAPIWorker" - } - } -} + +typealias PostAPIWorker = PostsAPIs.Worker + + extension PostAPIWorker { - public func fetchTodayPostList(query: Domain.PostListQuery) -> RxSwift.Single { - let requestDTO = PostListRequestDTO(page: query.page, size: query.size, date: query.date, memberId: query.memberId, sort: query.sort, type: query.type.rawValue) - let spec = PostAPIs.fetchPostList.spec - return request(spec: spec, parameters: requestDTO) - .subscribe(on: Self.queue) - .map(PostListResponseDTO.self) - .map { - return $0?.toDomain() - } - .catchAndReturn(nil) - .asSingle() + + /// 게시물 전체 조회하기 위한 Method 입니다. + /// HTTP Method : GET + /// - Parameters : PostListQueryDTO + func fetchPostList(query: PostListQueryDTO) -> Observable + { + let spec = PostsAPIs.fetchPostList(query: query).spec + + return request(spec) + } + + /// 게시물 단일 조회 하기 위한 Method 입니다. + /// HTTP Method : GET + /// - Parameters : PostId(게시글 고유 ID) + /// - Returns : PostDetailResponseDTO + func fetchPostDetail(postId: String) -> Observable { + let spec = PostsAPIs.fetchPostDetail(postId: postId).spec + + return request(spec) + } + + /// 게시물 생성을 하기 위한 Method 입니다. + /// HTTP Method : POST + /// - Parameters : + /// - query : CreateFeedQuery + /// - body : CameraFeedRequestDTO + /// - type: String + /// - available : Bool + /// - Returns : CameraDisplayPostResponseDTO + func createPost(query: CreatePostQuery, body: CreatePostRequestDTO) -> Observable { + let spec = PostsAPIs.createPost(type: query.type, body: body).spec + + return request(spec) + } + + /// 게시믈 이미지 업로드를 하기 위한 Presigend-URL API 요청 Method 입니다 + /// HTTP Method : POST + /// - Returns : CameraDisplayImageResponseDTO + func createPostPresignedURL(body: CreatePresignedURLRequestDTO) -> Observable { + let spec = PostsAPIs.createPostPresignedURL(body: body).spec + + return request(spec) } + } + + + + + + + diff --git a/14th-team5-iOS/Data/Sources/Trash/Post/PostList/Repository/PostRepository.swift b/14th-team5-iOS/Data/Sources/Trash/Post/PostList/Repository/PostRepository.swift index da6a4aa15..7c27289b1 100644 --- a/14th-team5-iOS/Data/Sources/Trash/Post/PostList/Repository/PostRepository.swift +++ b/14th-team5-iOS/Data/Sources/Trash/Post/PostList/Repository/PostRepository.swift @@ -16,8 +16,38 @@ public final class PostRepository: PostListRepositoryProtocol { private let postAPIWorker: PostAPIWorker = PostAPIWorker() public init() { } +} + + +extension PostRepository { + public func fetchPostList(query: PostListQuery) -> Observable { + let query = PostListQueryDTO( + page: query.page, + size: query.size, + date: query.date, + type: query.type.rawValue, + sort: query.type.rawValue + ) + + return postAPIWorker.fetchPostList(query: query) + .map { $0?.toDomain() } + } + + public func fetchPostDetailItem(postId: String) -> Observable { + return postAPIWorker.fetchPostDetail(postId: postId) + .map { $0?.toDomain() } + } + + public func createPostItem(query: CreatePostQuery, body: CreatePostRequest) -> Observable { + let body = CreatePostRequestDTO(imageUrl: body.imageUrl, content: body.content, uploadTime: body.uploadTime) + + return postAPIWorker.createPost(query: query, body: body) + .map { $0.toDomain() } + } - public func fetchTodayPostList(query: Domain.PostListQuery) -> RxSwift.Single { - return postAPIWorker.fetchTodayPostList(query: query) + public func createPostPresignedURLItem(body: CreatePostPresignedURLRequest) -> Observable { + let body = CreatePresignedURLRequestDTO(imageName: body.imageName) + return postAPIWorker.createPostPresignedURL(body: body) + .map { $0.toDomain() } } } diff --git a/14th-team5-iOS/Domain/Sources/Entities/PostList/CreatePostPresignedURLRequest.swift b/14th-team5-iOS/Domain/Sources/Entities/PostList/CreatePostPresignedURLRequest.swift new file mode 100644 index 000000000..6e2fcb1ef --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Entities/PostList/CreatePostPresignedURLRequest.swift @@ -0,0 +1,17 @@ +// +// CreatePostPresignedURLRequest.swift +// Domain +// +// Created by 김도현 on 11/19/24. +// + +import Foundation + + +public struct CreatePostPresignedURLRequest { + public let imageName: String + + public init(imageName: String) { + self.imageName = imageName + } +} diff --git a/14th-team5-iOS/Domain/Sources/Entities/PostList/CreatePostQuery.swift b/14th-team5-iOS/Domain/Sources/Entities/PostList/CreatePostQuery.swift new file mode 100644 index 000000000..ebd76af6b --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Entities/PostList/CreatePostQuery.swift @@ -0,0 +1,17 @@ +// +// CreatePostQuery.swift +// Domain +// +// Created by 김도현 on 11/19/24. +// + +import Foundation + + +public struct CreatePostQuery { + public let type: String + + public init(type: String) { + self.type = type + } +} diff --git a/14th-team5-iOS/Domain/Sources/Entities/PostList/CreatePostRequest.swift b/14th-team5-iOS/Domain/Sources/Entities/PostList/CreatePostRequest.swift new file mode 100644 index 000000000..7ce0351c3 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Entities/PostList/CreatePostRequest.swift @@ -0,0 +1,25 @@ +// +// CreatePostRequest.swift +// Domain +// +// Created by 김도현 on 11/19/24. +// + +import Foundation + + +public struct CreatePostRequest { + public let imageUrl: String + public let content: String + public let uploadTime: String + + public init( + imageUrl: String, + content: String, + uploadTime: String + ) { + self.imageUrl = imageUrl + self.content = content + self.uploadTime = uploadTime + } +} diff --git a/14th-team5-iOS/Domain/Sources/Entities/PostList/PostDetailEntity.swift b/14th-team5-iOS/Domain/Sources/Entities/PostList/PostDetailEntity.swift new file mode 100644 index 000000000..333a3b5c4 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Entities/PostList/PostDetailEntity.swift @@ -0,0 +1,45 @@ +// +// PostDetailEntity.swift +// Domain +// +// Created by 김도현 on 11/19/24. +// + +import Foundation + + + +public struct PostDetailEntity { + public let postId: String + public let authorId: String + public let type: PostType + public let missionId: String? + public let commentCount: Int + public let emojiCount: Int + public let imageUrl: String + public let content: String + public let createdAt: String + + + public init( + postId: String, + authorId: String, + type: PostType, + missionId: String?, + commentCount: Int, + emojiCount: Int, + imageUrl: String, + content: String, + createdAt: String + ) { + self.postId = postId + self.authorId = authorId + self.type = type + self.missionId = missionId + self.commentCount = commentCount + self.emojiCount = emojiCount + self.imageUrl = imageUrl + self.content = content + self.createdAt = createdAt + } +} diff --git a/14th-team5-iOS/Domain/Sources/Repositories/PostListRepository.swift b/14th-team5-iOS/Domain/Sources/Repositories/PostListRepository.swift index e74ba50ac..13289dbea 100644 --- a/14th-team5-iOS/Domain/Sources/Repositories/PostListRepository.swift +++ b/14th-team5-iOS/Domain/Sources/Repositories/PostListRepository.swift @@ -9,5 +9,10 @@ import Foundation import RxSwift public protocol PostListRepositoryProtocol { - func fetchTodayPostList(query: PostListQuery) -> Single + /// FETCH + func fetchPostList(query: PostListQuery) -> Observable + func fetchPostDetailItem(postId: String) -> Observable + /// CREATE + func createPostItem(query: CreatePostQuery, body: CreatePostRequest) -> Observable + func createPostPresignedURLItem(body: CreatePostPresignedURLRequest) -> Observable } diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Members/FetchMembersPostListUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Members/FetchMembersPostListUseCase.swift index 0be3bc34d..0ffe5c227 100644 --- a/14th-team5-iOS/Domain/Sources/UseCases/Members/FetchMembersPostListUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/Members/FetchMembersPostListUseCase.swift @@ -11,7 +11,7 @@ import RxSwift import RxCocoa public protocol FetchMembersPostListUseCaseProtocol { - func execute(query: PostListQuery) -> Single + func execute(query: PostListQuery) -> Observable } @@ -24,8 +24,8 @@ public final class FetchMembersPostListUseCase: FetchMembersPostListUseCaseProto self.postListRepository = postListRepository } - public func execute(query: PostListQuery) -> Single { - return postListRepository.fetchTodayPostList(query: query) + public func execute(query: PostListQuery) -> Observable { + return postListRepository.fetchPostList(query: query) } diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Post/FetchPostListUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Post/FetchPostListUseCase.swift index 7f1916559..a80203800 100644 --- a/14th-team5-iOS/Domain/Sources/UseCases/Post/FetchPostListUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/Post/FetchPostListUseCase.swift @@ -28,7 +28,7 @@ public class FetchPostListUseCase: FetchPostListUseCaseProtocol { /// postList를 불러옵니다. - 메인 화면에서 사용 중 입니다. /// 만약, userdefaults에 저장된 가족 멤버가 없다면, fetchFamily를 한 번 실행하여 post 정보를 가져옵니다. public func execute(query: PostListQuery) -> Observable<[PostEntity]?> { - return postListRepository.fetchTodayPostList(query: query) + return postListRepository.fetchPostList(query: query) .flatMap { posts -> Single<[PostEntity]?> in guard let posts = posts else { return Single.just(nil) @@ -41,7 +41,7 @@ public class FetchPostListUseCase: FetchPostListUseCaseProtocol { /// PostList를 페이지네이션 형태로 불러옵니다 - 프로필 조회에서 사용 중 입니다. public func execute(query: PostListQuery) -> Observable { - return postListRepository.fetchTodayPostList(query: query) + return postListRepository.fetchPostList(query: query) .flatMap { posts -> Single in let members = self.familyRepository.loadAllFamilyMembers() From c712d4d0ce2faf51d323b7ee5ed2f769bff9be6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=84?= Date: Thu, 21 Nov 2024 18:19:58 +0900 Subject: [PATCH 246/263] =?UTF-8?q?refactor:=20CameraAPIWorker=20=EA=B8=B0?= =?UTF-8?q?=EC=A1=B4=20Network=20=EB=AA=A8=EB=93=88=20=EB=A6=AC=ED=8E=99?= =?UTF-8?q?=ED=86=A0=EB=A7=81=20(#705)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: CameraAPIs BBAPI 프로토콜 채택 및 네이밍 수정, 주석 추가 - CameraAPIWorker 메서드 네이밍 수정 및 주석 추가 - BBNetworkService, SessionManager, BBAPIWorker Upload Method 추가 - Upload 관련 Serializer 응답 핸들러 추가 - Camera 관련 Repository Method 네이밍 수정 및 RequestDTO, Entity 수정 - Camera 관련 Reactor 코드 리펙토랑 * fix: CameraAPIs fetchRealEmojiImage Path 오타 수정 - CameraViewReactor Member Id 조회 UseCase 추가 --- .../Reactor/CameraDisplayViewReactor.swift | 27 +-- .../Camera/Reactor/CameraViewReactor.swift | 68 ++++---- .../Profile/Reactor/ProfileViewReactor.swift | 33 ++-- .../Sources/Bibbi/BBNetwork/BBAPIWorker.swift | 52 ++++++ .../BBNetwork/Network/BBNetworkService.swift | 37 ++++ .../Network/BBNetworkSessionManager.swift | 20 +++ .../Utils/BBUploadResponseSerializer.swift | 27 +++ .../Sources/APIs/Camera/CameraAPIWorker.swift | 131 ++++++++++++++ .../Data/Sources/APIs/Camera/CameraAPIs.swift | 73 ++++++++ .../CameraCreateRealEmojiResponseDTO.swift | 0 .../CameraDisplayImageResponseDTO.swift | 0 .../CameraDisplayPostResponseDTO.swift | 0 .../CameraRealEmojiImageItemResponseDTO.swift | 0 .../CameraRealEmojiPreSignedResponseDTO.swift | 0 .../CameraTodayMissionResponseDTO.swift | 0 .../CameraUpdateRealEmojiResponseDTO.swift | 0 .../CreateEmojiImageReqeustDTO.swift | 18 ++ .../DataMapping/CreateFeedRequestDTO.swift | 15 ++ .../CreatePresignedURLReqeustDTO.swift | 13 ++ .../UpdateProfileImageRequestDTO.swift | 13 ++ .../UpdateRealEmojiImageRequestDTO.swift | 13 ++ .../Camera/Repository/CameraRepository.swift | 79 +++++++++ .../MembersProfileResponseDTO.swift | 2 +- .../Camera/CameraAPI/CameraAPIWorker.swift | 160 ------------------ .../Trash/Camera/CameraAPI/CameraAPIs.swift | 51 ------ .../Camera/Repository/CameraRepository.swift | 81 --------- .../Camera}/CameraCreateRealEmojiEntity.swift | 0 .../Camera}/CameraMissionFeedQuery.swift | 0 .../Camera}/CameraPostEntity.swift | 0 .../Camera}/CameraPreSignedEntity.swift | 0 .../CameraRealEmojiImageItemEntity.swift | 0 .../Camera}/CameraRealEmojiItems.swift | 0 .../CameraRealEmojiPreSignedEntity.swift | 0 .../Camera}/CameraTodayMssionEntity.swift | 0 .../Camera}/CameraUpdateRealEmojiEntity.swift | 0 .../Camera/CreateEmojiImageRequest.swift | 19 +++ .../Entities/Camera/CreateFeedQuery.swift | 17 ++ .../Camera/CreateFeedRequest.swift} | 14 +- .../Camera/CreatePresignedURLRequest.swift | 16 ++ .../Camera/UpdateProfileImageRequest.swift | 17 ++ .../Camera/UpdateRealEmojiImageRequest.swift | 17 ++ .../CameraRepository.swift} | 32 ++-- .../CameraDisplayImageParameters.swift | 0 .../CameraCreateRealEmojiParameters.swift | 20 --- .../CameraRealEmojiParameters.swift | 17 -- .../CameraUpdateRealEmojiParameters.swift | 18 -- .../FetchCameraTodayMissionUseCase.swift | 8 +- .../EditCameraProfileImageUseCase.swift | 9 +- .../FetchCameraRealEmojiListUseCase.swift | 6 +- .../FetchCameraRealEmojiUpdateUseCase.swift | 6 +- .../FetchCameraRealEmojiUploadUseCase.swift | 6 +- .../FetchCameraRealEmojiUseCase.swift | 11 +- .../Camera/CreateCameraImageUseCase.swift | 8 +- .../UseCases/Camera/CreateCameraUseCase.swift | 6 +- .../FetchCameraUploadImageUseCase.swift | 6 +- 55 files changed, 688 insertions(+), 478 deletions(-) create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Utils/BBUploadResponseSerializer.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/Camera/CameraAPIWorker.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/Camera/CameraAPIs.swift rename 14th-team5-iOS/Data/Sources/{Trash => APIs}/Camera/DataMapping/CameraCreateRealEmojiResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/{Trash => APIs}/Camera/DataMapping/CameraDisplayImageResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/{Trash => APIs}/Camera/DataMapping/CameraDisplayPostResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/{Trash => APIs}/Camera/DataMapping/CameraRealEmojiImageItemResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/{Trash => APIs}/Camera/DataMapping/CameraRealEmojiPreSignedResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/{Trash => APIs}/Camera/DataMapping/CameraTodayMissionResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/{Trash => APIs}/Camera/DataMapping/CameraUpdateRealEmojiResponseDTO.swift (100%) create mode 100644 14th-team5-iOS/Data/Sources/APIs/Camera/DataMapping/CreateEmojiImageReqeustDTO.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/Camera/DataMapping/CreateFeedRequestDTO.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/Camera/DataMapping/CreatePresignedURLReqeustDTO.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/Camera/DataMapping/UpdateProfileImageRequestDTO.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/Camera/DataMapping/UpdateRealEmojiImageRequestDTO.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/Camera/Repository/CameraRepository.swift delete mode 100644 14th-team5-iOS/Data/Sources/Trash/Camera/CameraAPI/CameraAPIWorker.swift delete mode 100644 14th-team5-iOS/Data/Sources/Trash/Camera/CameraAPI/CameraAPIs.swift delete mode 100644 14th-team5-iOS/Data/Sources/Trash/Camera/Repository/CameraRepository.swift rename 14th-team5-iOS/Domain/Sources/{Trash/Camera/Entity => Entities/Camera}/CameraCreateRealEmojiEntity.swift (100%) rename 14th-team5-iOS/Domain/Sources/{Trash/Camera/Entity => Entities/Camera}/CameraMissionFeedQuery.swift (100%) rename 14th-team5-iOS/Domain/Sources/{Trash/Camera/Entity => Entities/Camera}/CameraPostEntity.swift (100%) rename 14th-team5-iOS/Domain/Sources/{Trash/Camera/Entity => Entities/Camera}/CameraPreSignedEntity.swift (100%) rename 14th-team5-iOS/Domain/Sources/{Trash/Camera/Entity => Entities/Camera}/CameraRealEmojiImageItemEntity.swift (100%) rename 14th-team5-iOS/Domain/Sources/{Trash/Camera/Entity => Entities/Camera}/CameraRealEmojiItems.swift (100%) rename 14th-team5-iOS/Domain/Sources/{Trash/Camera/Entity => Entities/Camera}/CameraRealEmojiPreSignedEntity.swift (100%) rename 14th-team5-iOS/Domain/Sources/{Trash/Camera/Entity => Entities/Camera}/CameraTodayMssionEntity.swift (100%) rename 14th-team5-iOS/Domain/Sources/{Trash/Camera/Entity => Entities/Camera}/CameraUpdateRealEmojiEntity.swift (100%) create mode 100644 14th-team5-iOS/Domain/Sources/Entities/Camera/CreateEmojiImageRequest.swift create mode 100644 14th-team5-iOS/Domain/Sources/Entities/Camera/CreateFeedQuery.swift rename 14th-team5-iOS/Domain/Sources/{Trash/Camera/Parameters/CameraDisplayPostParameters.swift => Entities/Camera/CreateFeedRequest.swift} (54%) create mode 100644 14th-team5-iOS/Domain/Sources/Entities/Camera/CreatePresignedURLRequest.swift create mode 100644 14th-team5-iOS/Domain/Sources/Entities/Camera/UpdateProfileImageRequest.swift create mode 100644 14th-team5-iOS/Domain/Sources/Entities/Camera/UpdateRealEmojiImageRequest.swift rename 14th-team5-iOS/Domain/Sources/{Trash/Camera/Interfaces/CameraRepositoryProtocol.swift => Repositories/CameraRepository.swift} (50%) rename 14th-team5-iOS/Domain/Sources/Trash/{Camera => Account}/Parameters/CameraDisplayImageParameters.swift (100%) delete mode 100644 14th-team5-iOS/Domain/Sources/Trash/Camera/Parameters/CameraCreateRealEmojiParameters.swift delete mode 100644 14th-team5-iOS/Domain/Sources/Trash/Camera/Parameters/CameraRealEmojiParameters.swift delete mode 100644 14th-team5-iOS/Domain/Sources/Trash/Camera/Parameters/CameraUpdateRealEmojiParameters.swift rename 14th-team5-iOS/Domain/Sources/{Trash/Camera => }/UseCases/Camera/CreateCameraImageUseCase.swift (58%) rename 14th-team5-iOS/Domain/Sources/{Trash/Camera => }/UseCases/Camera/CreateCameraUseCase.swift (62%) rename 14th-team5-iOS/Domain/Sources/{Trash/Camera => }/UseCases/Camera/FetchCameraUploadImageUseCase.swift (67%) diff --git a/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraDisplayViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraDisplayViewReactor.swift index 85eda3314..378266d6c 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraDisplayViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraDisplayViewReactor.swift @@ -85,27 +85,23 @@ public final class CameraDisplayViewReactor: Reactor { switch action { case .viewDidLoad: let fileName = "\(currentState.displayData.hashValue).jpg" - let parameters: CameraDisplayImageParameters = CameraDisplayImageParameters(imageName: "\(fileName)") - + let body = CreatePresignedURLRequest(imageName: fileName) return .concat( .just(.setLoading(false)), .just(.setError(false)), .just(.setRenderImage(currentState.displayData)), - createPresignedCameraUseCase.execute(parameter: parameters) - .asObservable() + createPresignedCameraUseCase.execute(body: body) .withUnretained(self) .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) - .asObservable() .flatMap { owner, entity -> Observable in - guard let originalURL = entity?.imageURL else { + guard let remoteURL = entity?.imageURL else { return .concat( .just(.setLoading(true)), .just(.setError(true)) ) } - return owner.uploadImageUseCase.execute(to: originalURL, from: owner.currentState.displayData) + return owner.uploadImageUseCase.execute(remoteURL, image: owner.currentState.displayData) .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) - .asObservable() .flatMap { isSuccess -> Observable in if isSuccess { return .concat( @@ -160,17 +156,12 @@ public final class CameraDisplayViewReactor: Reactor { MPEvent.Camera.uploadPhoto.track(with: nil) guard let presingedURL = currentState.displayEntity?.imageURL else { return .just(.setError(true)) } - let originURL = configureOriginalS3URL(url: presingedURL) - let cameraQuery = CameraMissionFeedQuery(type: currentState.cameraType.rawValue, isUploded: true) - - let parameters: CameraDisplayPostParameters = CameraDisplayPostParameters( - imageUrl: originURL, - content: currentState.displayDescrption, - uploadTime: DateFormatter.yyyyMMddTHHmmssXXX.string(from: Date()) - ) + let remoteURL = configureOriginalS3URL(url: presingedURL) - return fetchCameraImageUseCase.execute(parameter: parameters, query: cameraQuery) - .asObservable() + let query = CreateFeedQuery(type: currentState.cameraType.rawValue) + let body = CreateFeedRequest(imageUrl: remoteURL, content: currentState.displayDescrption, uploadTime: DateFormatter.yyyyMMddTHHmmssXXX.string(from: .now)) + + return fetchCameraImageUseCase.execute(query: query, body: body) .catchAndReturn(nil) .withUnretained(self) .flatMap { owner, entity -> Observable in diff --git a/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraViewReactor.swift index c1c236259..e3ae14f22 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraViewReactor.swift @@ -27,6 +27,7 @@ public final class CameraViewReactor: Reactor { @Injected private var editProfileImageUseCase: EditCameraProfileImageUseCaseProtocol @Injected private var fetchRealEmojiListUseCase: FetchCameraRealEmojiListUseCaseProtocol @Injected private var fetchRealEmojiPreSignedUseCase: FetchCameraRealEmojiUseCaseProtocol + @Injected private var fetchMyMemberIdUseCase: FetchMyMemberIdUseCaseProtocol @Injected private var provider: ServiceProviderProtocol @@ -220,9 +221,12 @@ extension CameraViewReactor { switch cameraType { case .realEmoji: + guard let memberId = fetchMyMemberIdUseCase.execute() else { + return .just(.setErrorAlert(true)) + } + return .concat( - fetchRealEmojiListUseCase.execute() - .asObservable() + fetchRealEmojiListUseCase.execute(memberId: memberId) .withUnretained(self) .flatMap { owner, entity -> Observable in var sectionItem: [EmojiSectionItem] = [] @@ -283,28 +287,24 @@ extension CameraViewReactor { ) case .profile: - //Profile 관련 이미지 업로드 Mutation let profileImage = "\(imageData.hashValue).jpg" - let profileParameter = CameraDisplayImageParameters(imageName: profileImage) + let body = CreatePresignedURLRequest(imageName: profileImage) return .concat( .just(.setLoading(false)), - createProfileImageUseCase.execute(parameter: profileParameter) - .asObservable() + createProfileImageUseCase.execute(body: body) .withUnretained(self) .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) .flatMap { owner, entity -> Observable in - //TODO: 추후 오류 Alert 추가 - guard let presingedURL = entity?.imageURL else { + guard let remoteURL = entity?.imageURL else { return .concat( .just(.setLoading(true)), .just(.setErrorAlert(true)) ) } - return owner.uploadImageUseCase.execute(to: presingedURL, from: imageData) + return owner.uploadImageUseCase.execute(remoteURL, image: imageData) .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) - .asObservable() .flatMap { isSuccess -> Observable in if owner.memberId.isEmpty { @@ -316,12 +316,11 @@ extension CameraViewReactor { ) } - let originalURL = owner.configureProfileOriginalS3URL(url: presingedURL, with: .profile) - let profileImageEditParameter = ProfileImageEditParameter(profileImageUrl: originalURL) + let originalURL = owner.configureProfileOriginalS3URL(url: remoteURL, with: .profile) + let body = UpdateProfileImageRequest(profileImageUrl: originalURL) if isSuccess { - return owner.editProfileImageUseCase.execute(memberId: owner.memberId, parameter: profileImageEditParameter) - .asObservable() + return owner.editProfileImageUseCase.execute(memberId: owner.memberId, body: body) .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) .flatMap { editEntity -> Observable in @@ -346,33 +345,33 @@ extension CameraViewReactor { ) case .realEmoji: - let realEmojiImage = "\(imageData.hashValue).jpg" - let realEmojiParameter = CameraRealEmojiParameters(imageName: realEmojiImage) + guard let memberId = fetchMyMemberIdUseCase.execute() else { + return .just(.setErrorAlert(true)) + } + let realEmojiImage = "\(imageData.hashValue).jpg" + let body = CreatePresignedURLRequest(imageName: realEmojiImage) if currentState.realEmojiEntity[currentState.emojiType.rawValue - 1] == nil { return .concat( .just(.setLoading(false)), - fetchRealEmojiPreSignedUseCase.execute(memberId: memberId, parameter: realEmojiParameter) - .asObservable() + fetchRealEmojiPreSignedUseCase.execute(memberId: memberId, body: body) .withUnretained(self) .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) .flatMap { owner, entity -> Observable in - guard let presingedURL = entity?.imageURL else { return .just(.setErrorAlert(true))} + guard let remoteURL = entity?.imageURL else { return .just(.setErrorAlert(true))} - return owner.uploadImageUseCase.execute(to: presingedURL, from: imageData) + return owner.uploadImageUseCase.execute(remoteURL, image: imageData) .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) - .asObservable() .flatMap { isSuccess -> Observable in - let originalURL = owner.configureProfileOriginalS3URL(url: presingedURL, with: .realEmoji) - let realEmojiCreateParameter = CameraCreateRealEmojiParameters(type: owner.currentState.emojiType.emojiString, imageUrl: originalURL) + let originalURL = owner.configureProfileOriginalS3URL(url: remoteURL, with: .realEmoji) + let body = CreateEmojiImageRequest(type: owner.currentState.emojiType.emojiString, imageUrl: originalURL) if isSuccess { - return owner.fetchRealEmojiCreateUseCase.execute(memberId: owner.memberId, parameter: realEmojiCreateParameter) - .asObservable() + return owner.fetchRealEmojiCreateUseCase.execute(memberId: memberId, body: body) .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) .flatMap { realEmojiEntity -> Observable in guard let createRealEmojiEntity = realEmojiEntity else { return .just(.setErrorAlert(true))} owner.provider.realEmojiGlobalState.createRealEmojiImage(indexPath: owner.currentState.emojiType.rawValue - 1, image: createRealEmojiEntity.realEmojiImageURL, emojiType: createRealEmojiEntity.realEmojiType) - return owner.fetchRealEmojiListUseCase.execute() + return owner.fetchRealEmojiListUseCase.execute(memberId: memberId) .asObservable() .flatMap { reloadEntity -> Observable in return .concat( @@ -397,24 +396,23 @@ extension CameraViewReactor { ) } else { let realEmojiImage = "\(imageData.hashValue).jpg" - let realEmojiParameter = CameraRealEmojiParameters(imageName: realEmojiImage) + let body = CreatePresignedURLRequest(imageName: realEmojiImage) return .concat( .just(.setLoading(false)), - fetchRealEmojiPreSignedUseCase.execute(memberId: memberId, parameter: realEmojiParameter) - .asObservable() + fetchRealEmojiPreSignedUseCase.execute(memberId: memberId, body: body) .withUnretained(self) .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) .flatMap { owner, entity -> Observable in - guard let presingedURL = entity?.imageURL else { return .just(.setErrorAlert(true))} - let originalURL = owner.configureProfileOriginalS3URL(url: presingedURL, with: .realEmoji) - let updateRealEmojiParameter = CameraUpdateRealEmojiParameters(imageUrl: originalURL) - return owner.uploadImageUseCase.execute(to: presingedURL, from: imageData) + guard let remoteURL = entity?.imageURL else { return .just(.setErrorAlert(true))} + let originalURL = owner.configureProfileOriginalS3URL(url: remoteURL, with: .realEmoji) + + return owner.uploadImageUseCase.execute(remoteURL, image: imageData) .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) .asObservable() .flatMap { isSuccess -> Observable in if isSuccess { - return owner.fetchRealEmojiUpdateUseCase.execute(memberId: owner.memberId, emojiId: owner.currentState.realEmojiEntity[owner.currentState.emojiType.rawValue - 1]?.realEmojiId ?? "", parameter: updateRealEmojiParameter) - .asObservable() + let body = UpdateRealEmojiImageRequest(imageUrl: originalURL) + return owner.fetchRealEmojiUpdateUseCase.execute(memberId: memberId, realEmojiId: owner.currentState.realEmojiEntity[owner.currentState.emojiType.rawValue - 1]?.realEmojiId ?? "", body: body) .flatMap { updateRealEmojiEntity -> Observable in guard let updateEntity = updateRealEmojiEntity else { return .just(.setErrorAlert(true))} owner.provider.realEmojiGlobalState.updateRealEmojiImage(indexPath: owner.currentState.emojiType.rawValue - 1, image: updateEntity.realEmojiImageURL) diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift index fafd0a04d..1baccb1ab 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift @@ -84,7 +84,6 @@ public final class ProfileViewReactor: Reactor { public func mutate(action: Action) -> Observable { - //TODO: Keychain, UserDefaults 추가 switch action { case .viewDidLoad: return fetchMembersProfileUseCase.execute(memberId: currentState.memberId) @@ -92,22 +91,20 @@ public final class ProfileViewReactor: Reactor { .flatMap { entity -> Observable in .just(.setProfileMemberItems(entity)) } - case let .updateNickNameProfile(nickNameFileData): - let nickNameProfileImage: String = "\(nickNameFileData.hashValue).jpg" - let nickNameImageEditParameter: CameraDisplayImageParameters = CameraDisplayImageParameters(imageName: nickNameProfileImage) + case let .updateNickNameProfile(imageData): + let profileImage = "\(imageData.hashValue).jpg" + let createPresignedURL = CreatePresignedURLRequest(imageName: profileImage) return .concat( .just(.setLoading(false)), - createProfilePresignedUseCase.execute(parameter: nickNameImageEditParameter) - .asObservable() + createProfilePresignedUseCase.execute(body: createPresignedURL) .withUnretained(self) .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) .flatMap { owner, entity -> Observable in - guard let profilePresingedURL = entity?.imageURL else { return .empty() } - return owner.uploadProfileImageUseCase.execute(to: profilePresingedURL, from: nickNameFileData) + guard let remoteURL = entity?.imageURL else { return .empty() } + return owner.uploadProfileImageUseCase.execute(remoteURL, image: imageData) .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) - .asObservable() .flatMap { isSuccess -> Observable in - let originalPath = owner.configureProfileOriginalS3URL(url: profilePresingedURL) + let originalPath = owner.configureProfileOriginalS3URL(url: remoteURL) let profileEditParameter: ProfileImageEditParameter = ProfileImageEditParameter(profileImageUrl: originalPath) if isSuccess { return owner.updateProfileUseCase.execute(memberId: owner.currentState.memberId, parameter: profileEditParameter) @@ -140,22 +137,20 @@ public final class ProfileViewReactor: Reactor { ) } - case let .didSelectPHAssetsImage(fileData): - let profileImage: String = "\(fileData.hashValue).jpg" - let profileImageEditParameter: CameraDisplayImageParameters = CameraDisplayImageParameters(imageName: profileImage) + case let .didSelectPHAssetsImage(assetImage): + let imageName: String = "\(assetImage.hashValue).jpg" + let createPresignedURL = CreatePresignedURLRequest(imageName: imageName) return .concat( .just(.setLoading(false)), - createProfilePresignedUseCase.execute(parameter: profileImageEditParameter) - .asObservable() + createProfilePresignedUseCase.execute(body: createPresignedURL) .withUnretained(self) .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) .flatMap { owner, entity -> Observable in - guard let profilePresingedURL = entity?.imageURL else { return .empty() } - return owner.uploadProfileImageUseCase.execute(to: profilePresingedURL, from: fileData) + guard let remoteURL = entity?.imageURL else { return .empty() } + return owner.uploadProfileImageUseCase.execute(remoteURL, image: assetImage) .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) - .asObservable() .flatMap { isSuccess -> Observable in - let originalPath = owner.configureProfileOriginalS3URL(url: profilePresingedURL) + let originalPath = owner.configureProfileOriginalS3URL(url: remoteURL) let profileEditParameter: ProfileImageEditParameter = ProfileImageEditParameter(profileImageUrl: originalPath) if isSuccess { return owner.updateProfileUseCase.execute(memberId: owner.currentState.memberId, parameter: profileEditParameter) diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/BBAPIWorker.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/BBAPIWorker.swift index 9e83fbebd..2bcbcf560 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/BBAPIWorker.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/BBAPIWorker.swift @@ -75,6 +75,14 @@ public protocol Workable: AnyObject { func request( _ spec: any ResponseRequestable ) -> Observable where D: Decodable + + @discardableResult + func upload( + _ presignedURL: String, + with binaryData: Data, + serializer: BBUploadResponseSerializer, + on queue: any SchedulerType + ) -> Observable } // MARK: - Default API Worker @@ -154,6 +162,50 @@ extension BBRxAPIWorker: Workable { } + /// presignedURL을 바탕으로 URLRequest로 Convert 하여 S3 Bucket으로 요청하는 메서드 입니다. + /// + /// HTTP 통신을에 성공을 하게 되면 `BBUploadResponseSerializer`을 통해 True 값을 반환하며, 실패한다면 `APIWorkerError`를 방출 합니다. + /// - Parameters: + /// - presignedURL: 서버에서 발급 받은 `Presigned URL` 입니다. + /// - binaryData : 이미지를 `데이터` 타입으로 변환한 값 입니다. + /// - serializer : `DataResponseSerializerProtocol`을 채택한 응답 핸들러입니다. + /// - Returns: Observable + public func upload( + _ presignedURL: String, + with binaryData: Data, + serializer: BBUploadResponseSerializer = BBUploadResponseSerializer(), + on queue: any SchedulerType = RxScheduler.main + ) -> Observable { + + guard let remoteURL = URL(string: presignedURL) else { + return .just(false) + } + + var request = URLRequest(url: remoteURL) + request.method = .put + request.httpBody = binaryData + request.headers = BBNetworkHeaders.default.asHTTPHeaders + + return Observable.create { [unowned self] observer in + let uploadRequest = self.service.upload(request, with: binaryData, serializer: serializer) { result in + switch result { + case let .success(isUpload): + observer.onNext(isUpload ?? false) + observer.onCompleted() + case let .failure(error): + let mappedError = self.errorMapper.map(networkError: error) + self.errorLogger.log(localizedError: mappedError) + observer.onError(mappedError) + } + } + + return Disposables.create { + let _ = uploadRequest?.cancel() + } + } + .observe(on: queue) + } + /// 매개변수로 주어진 스펙(spec) 정보를 바탕으로 HTTP 통신을 수행합니다. /// /// HTTP 통신에 성공하면 디코딩된 값을 next 항목으로 방출하고, 실패한다면 `APIWorkerError` 타입의 에러가 담긴 error 항목을 방출합니다. diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/BBNetworkService.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/BBNetworkService.swift index f2988d02e..819a1b839 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/BBNetworkService.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/BBNetworkService.swift @@ -8,6 +8,7 @@ import Foundation import Alamofire +import RxSwift // MARK: - Cancellable @@ -22,11 +23,19 @@ extension Alamofire.Request: BBNetworkCancellable { } public protocol BBNetworkService { typealias CompletionHandler = (Result) -> Void + typealias UploadHandler = (Result) -> Void func request( with spec: any Requestable, completion: @escaping CompletionHandler ) -> (any BBNetworkCancellable)? + + func upload( + _ spec: URLRequest, + with binaryData: Data, + serializer: BBUploadResponseSerializer, + completion: @escaping UploadHandler + ) -> (any BBNetworkCancellable)? } @@ -76,6 +85,25 @@ public final class BBNetworkDefaultService { } + public func upload( + with spec: URLRequest, + data: Data, + serializer: BBUploadResponseSerializer, + completion: @escaping UploadHandler) -> any BBNetworkCancellable + { + let uploadRequest = sessionManager.upload(with: spec, data: data, serializer: serializer) { uploadResponse in + if let statusCode = uploadResponse.response?.statusCode { + guard (200..<300) ~= statusCode else { + let networkError = self.map(statusCode: statusCode) + completion(.failure(networkError)) + return + } + completion(.success(true)) + } + } + return uploadRequest + } + private func map(statusCode code: Int) -> BBNetworkError { switch code { case 400: @@ -134,4 +162,13 @@ extension BBNetworkDefaultService: BBNetworkService { } } + public func upload( + _ spec: URLRequest, + with binaryData: Data, + serializer: BBUploadResponseSerializer, + completion: @escaping UploadHandler + ) -> (any BBNetworkCancellable)? { + return upload(with: spec, data: binaryData, serializer: serializer, completion: completion) + } + } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/BBNetworkSessionManager.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/BBNetworkSessionManager.swift index e7014afc6..76e53ee30 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/BBNetworkSessionManager.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/BBNetworkSessionManager.swift @@ -13,12 +13,19 @@ import Alamofire public protocol BBNetworkSessionManager { typealias CompletionHandler = (AFDataResponse) -> Void + typealias UploadHandler = (AFDataResponse) -> Void func request( with request: URLRequest, completion: @escaping CompletionHandler ) -> BBNetworkCancellable + func upload( + with request: URLRequest, + data: Data, + serializer: BBUploadResponseSerializer, + completion: @escaping UploadHandler + ) -> BBNetworkCancellable } @@ -109,4 +116,17 @@ extension BBNetworkSession: BBNetworkSessionManager { return dataRequest } + public func upload( + with request: URLRequest, + data: Data, + serializer: BBUploadResponseSerializer, + completion: @escaping UploadHandler + ) -> any BBNetworkCancellable { + let uploadRequest = session.upload(data, with: request) + + uploadRequest.response(responseSerializer: serializer) { response in + completion(response) + } + return uploadRequest + } } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Utils/BBUploadResponseSerializer.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Utils/BBUploadResponseSerializer.swift new file mode 100644 index 000000000..4f2b98258 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Utils/BBUploadResponseSerializer.swift @@ -0,0 +1,27 @@ +// +// BBUploadResponseSerializer.swift +// Core +// +// Created by 김도현 on 11/18/24. +// + +import Foundation + +import Alamofire + +// MARK: - Upload Response Serializer +public struct BBUploadResponseSerializer: DataResponseSerializerProtocol { + public typealias SerializedObject = Bool + + public init() { } + + + public func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: (any Error)?) throws -> Bool { + guard let statusCode = response?.statusCode, + (200..<300) ~= statusCode else { + throw BBNetworkError.generic(error!) + } + + return true + } +} diff --git a/14th-team5-iOS/Data/Sources/APIs/Camera/CameraAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/Camera/CameraAPIWorker.swift new file mode 100644 index 000000000..10501112e --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/Camera/CameraAPIWorker.swift @@ -0,0 +1,131 @@ +// +// CameraAPIWorker.swift +// Data +// +// Created by Kim dohyun on 12/21/23. +// + +import Foundation +import Core + +import Alamofire +import Domain +import RxSwift + + +typealias CameraAPIWorker = CameraAPIs.Worker + +extension CameraAPIWorker { + + /// 회읜 프로필 이미지 업로드를 하기 위한 Presigned-URL API 요청 Method + /// HTTP Method: POST + /// - Parameters : body (업로드 image Name) + /// - Returns : CameraDisplayImageResponseDTO + public func createProfilePresignedURL(body: CreatePresignedURLReqeustDTO) -> Observable { + let spec = CameraAPIs.createProfilePresignedURL(body: body).spec + + return request(spec) + } + + /// 게시믈 이미지 업로드를 하기 위한 Presigend-URL API 요청 Method 입니다 + /// HTTP Method : POST + /// - Returns : CameraDisplayImageResponseDTO + public func createFeedPresignedURL(body: CreatePresignedURLReqeustDTO) -> Observable { + let spec = CameraAPIs.createFeedPresignedURL(body: body).spec + + return request(spec) + } + + /// 사용자 프로필 이미지를 업데이트 하기 위한 Method 입니다. + /// HTTP Method : PUT + /// - Parameters : MemberId (사용자 멤버 ID) + /// - Returns : MembersProfileResponseDTO + public func updateProfileImage(memberId: String, body: UpdateProfileImageRequestDTO) -> Observable { + let spec = CameraAPIs.updateProfileImage(memberId: memberId, body: body).spec + + return request(spec) + } + + /// 게시물 생성을 하기 위한 Method 입니다. + /// HTTP Method : POST + /// - Parameters : + /// - query : CreateFeedQuery + /// - body : CameraFeedRequestDTO + /// - type: String + /// - available : Bool + /// - Returns : CameraDisplayPostResponseDTO + public func createFeed(query: CreateFeedQuery, body: CreateFeedRequestDTO) -> Observable { + let spec = CameraAPIs.createFeed(type: query.type, body: body).spec + + return request(spec) + } + + /// 리얼 이모지를 업로드 하기 위한 Presigend-URL API 요청 Method 입니다 + /// HTTP Method : POST + /// - Parameters : + /// - MemberId (사용자 멤버 ID) + /// - body: CreatePresignedURLReqeustDTO (업로드 Image Name) + /// - Returns : CameraRealEmojiPreSignedResponseDTO + public func createRealEmojiPresignedURL(memberId: String, body: CreatePresignedURLReqeustDTO) -> Observable { + let spec = CameraAPIs.createRealEmojiPresignedURL(memberID: memberId, body: body).spec + + return request(spec) + } + + /// 리얼 이모지를 조회 하기 위한 API Method 입니다. + /// HTTP Method : GET + /// - Parameters : MemberId (사용자 멤버 ID) + /// - Returns : CameraRealEmojiImageItemResponseDTO + public func fetchRealEmoji(memberId: String) -> Observable { + let spec = CameraAPIs.fetchRealEmojiImage(memberId: memberId).spec + + return request(spec) + } + + /// 리얼 이모지를 수정하기 위한 API Method 입니다. + /// HTTP Method : PUT + /// - Parameters + /// - MemberId (사용자 멤버 ID) + /// - realEmojiId (리얼 이모지 ID) + /// - Returns : CameraUpdateRealEmojiResponseDTO + public func updateRealEmoji(memberId: String, realEmojiId: String, body: UpdateRealEmojiImageRequestDTO) -> Observable { + let spec = CameraAPIs.updateRealEmojiImage(memberId: memberId, realEmojiId: realEmojiId, body: body).spec + + return request(spec) + } + + /// 리얼 이모지를 추가하기 위한 API Method 입니다. + /// HTTP Method : POST + ///- Parameters : + /// - MemberId (사용자 멤버 ID) + /// - body : CreatePresignedURLReqeustDTO + /// - type : 리얼 이모지 타입 + /// - imageUrl : 리얼 이모지 URL + ///- Returns : CameraCreateRealEmojiResponseDTO + public func createRealEmoji(memberId: String, body: CreateEmojiImageReqeustDTO) -> Observable { + let spec = CameraAPIs.createRealEmojiImage(memberId: memberId, body: body).spec + + return request(spec) + } + + /// 일일 미션 조회를 하기 위한 API Method입니다. + /// HTTP Method : GET + /// - Returns : CameraTodayMissionResponseDTO + public func fetchDailyMisson() -> Observable { + let spec = CameraAPIs.fetchTodayMission.spec + + return request(spec) + } + + /// 피드, 프로필 이미지를 S3 Bucket에 업로드 하기 위한 Method 입니다. + /// HTTP Method : PUT + /// - Parameters + /// - presignedURL : 서버에서 발급 받은 Presigned-URL + /// - image : Image Data Type + /// - Returns : 업로드 성공 여부 확인 (Bool) Type + public func uploadImageToPresignedURL(_ presignedURL: String, image: Data) -> Observable { + return upload(presignedURL, with: image) + } + +} + diff --git a/14th-team5-iOS/Data/Sources/APIs/Camera/CameraAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/Camera/CameraAPIs.swift new file mode 100644 index 000000000..ccc516fde --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/Camera/CameraAPIs.swift @@ -0,0 +1,73 @@ +// +// CameraAPIs.swift +// Data +// +// Created by Kim dohyun on 12/21/23. +// + +import Core +import Domain +import Foundation + + +enum CameraAPIs: BBAPI { + /// 회원 프로필 Presigend URL 요청 API + case createProfilePresignedURL(body: CreatePresignedURLReqeustDTO) + /// 회원 프로필 이미지 수정 API + case updateProfileImage(memberId: String, body: UpdateProfileImageRequestDTO) + /// 게시물 사진 Presigned URL 요청 API + case createFeedPresignedURL(body: CreatePresignedURLReqeustDTO) + /// S3 Bucket 업로드 API + case uploadTos3Bucket(presignedURL: String) + /// 게시물 생성 API + case createFeed(type: String, body: CreateFeedRequestDTO) + /// 리얼 이모지 Presigned URL 요청 API + case createRealEmojiPresignedURL(memberID: String, body: CreatePresignedURLReqeustDTO) + /// 리얼 이미지 추가 API + case createRealEmojiImage(memberId: String, body: CreateEmojiImageReqeustDTO) + /// 리얼 이미지 조회 API + case fetchRealEmojiImage(memberId: String) + /// 리얼 이모지 수정 API + case updateRealEmojiImage(memberId: String, realEmojiId: String, body: UpdateRealEmojiImageRequestDTO) + /// 금일의 일일 미션 조회 API + case fetchTodayMission + + var spec: Spec { + switch self { + case let .createProfilePresignedURL(body): + return Spec(method: .post, path: "/members/image-upload-request", bodyParametersEncodable: body) + case let .updateProfileImage(memberId, body): + return Spec(method: .put, path: "/members/profile-image-url/\(memberId)", bodyParametersEncodable: body) + case let .createFeedPresignedURL(body): + return Spec(method: .post, path: "/posts/image-upload-request", bodyParametersEncodable: body) + case let .uploadTos3Bucket(presignedURL): + return Spec(method: .put, path: presignedURL) + case let .createFeed(type, body): + return Spec( + method: .post, + path: "/posts", + queryParameters: [ + .type: "\(type)" + ], + bodyParametersEncodable: body + ) + case let .createRealEmojiPresignedURL(memberId, body): + return Spec(method: .post, path: "/members/\(memberId)/real-emoji/image-upload-request", bodyParametersEncodable: body) + case let .createRealEmojiImage(memberId, body): + return Spec(method: .post, path: "/members/\(memberId)/real-emoji", bodyParametersEncodable: body) + case let .fetchRealEmojiImage(memberId): + return Spec(method: .get, path: "/members/\(memberId)/real-emoji") + case let .updateRealEmojiImage(memberId, realEmojiId, body): + return Spec(method: .put, path: "/members/\(memberId)/real-emoji/\(realEmojiId)", bodyParametersEncodable: body) + case .fetchTodayMission: + return Spec(method: .get, path: "/missions/today") + + } + + } + + public final class Worker: BBRxAPIWorker { + public init() { super.init() } + } + +} diff --git a/14th-team5-iOS/Data/Sources/Trash/Camera/DataMapping/CameraCreateRealEmojiResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Camera/DataMapping/CameraCreateRealEmojiResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Trash/Camera/DataMapping/CameraCreateRealEmojiResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Camera/DataMapping/CameraCreateRealEmojiResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/Trash/Camera/DataMapping/CameraDisplayImageResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Camera/DataMapping/CameraDisplayImageResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Trash/Camera/DataMapping/CameraDisplayImageResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Camera/DataMapping/CameraDisplayImageResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/Trash/Camera/DataMapping/CameraDisplayPostResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Camera/DataMapping/CameraDisplayPostResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Trash/Camera/DataMapping/CameraDisplayPostResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Camera/DataMapping/CameraDisplayPostResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/Trash/Camera/DataMapping/CameraRealEmojiImageItemResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Camera/DataMapping/CameraRealEmojiImageItemResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Trash/Camera/DataMapping/CameraRealEmojiImageItemResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Camera/DataMapping/CameraRealEmojiImageItemResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/Trash/Camera/DataMapping/CameraRealEmojiPreSignedResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Camera/DataMapping/CameraRealEmojiPreSignedResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Trash/Camera/DataMapping/CameraRealEmojiPreSignedResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Camera/DataMapping/CameraRealEmojiPreSignedResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/Trash/Camera/DataMapping/CameraTodayMissionResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Camera/DataMapping/CameraTodayMissionResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Trash/Camera/DataMapping/CameraTodayMissionResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Camera/DataMapping/CameraTodayMissionResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/Trash/Camera/DataMapping/CameraUpdateRealEmojiResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Camera/DataMapping/CameraUpdateRealEmojiResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Trash/Camera/DataMapping/CameraUpdateRealEmojiResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Camera/DataMapping/CameraUpdateRealEmojiResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/Camera/DataMapping/CreateEmojiImageReqeustDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Camera/DataMapping/CreateEmojiImageReqeustDTO.swift new file mode 100644 index 000000000..7d1b988e0 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/Camera/DataMapping/CreateEmojiImageReqeustDTO.swift @@ -0,0 +1,18 @@ +// +// CreateEmojiImageReqeustDTO.swift +// Data +// +// Created by 김도현 on 11/17/24. +// + +import Foundation + +public struct CreateEmojiImageReqeustDTO: Encodable { + public let type: String + public let imageUrl: String + + public init(type: String, imageUrl: String) { + self.type = type + self.imageUrl = imageUrl + } +} diff --git a/14th-team5-iOS/Data/Sources/APIs/Camera/DataMapping/CreateFeedRequestDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Camera/DataMapping/CreateFeedRequestDTO.swift new file mode 100644 index 000000000..0d01f2b23 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/Camera/DataMapping/CreateFeedRequestDTO.swift @@ -0,0 +1,15 @@ +// +// CreateFeedRequestDTO.swift +// Data +// +// Created by 김도현 on 11/17/24. +// + +import Foundation + +public struct CreateFeedRequestDTO: Encodable { + public let imageUrl: String + public let content: String + public let uploadTime: String +} + diff --git a/14th-team5-iOS/Data/Sources/APIs/Camera/DataMapping/CreatePresignedURLReqeustDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Camera/DataMapping/CreatePresignedURLReqeustDTO.swift new file mode 100644 index 000000000..4eaba126b --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/Camera/DataMapping/CreatePresignedURLReqeustDTO.swift @@ -0,0 +1,13 @@ +// +// CreatePresignedURLReqeustDTO.swift +// Data +// +// Created by 김도현 on 11/17/24. +// + +import Foundation + + +public struct CreatePresignedURLReqeustDTO: Encodable { + public let imageName: String +} diff --git a/14th-team5-iOS/Data/Sources/APIs/Camera/DataMapping/UpdateProfileImageRequestDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Camera/DataMapping/UpdateProfileImageRequestDTO.swift new file mode 100644 index 000000000..3d5911e3a --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/Camera/DataMapping/UpdateProfileImageRequestDTO.swift @@ -0,0 +1,13 @@ +// +// UpdateProfileImageRequestDTO.swift +// Data +// +// Created by 김도현 on 11/17/24. +// + +import Foundation + + +public struct UpdateProfileImageRequestDTO: Encodable { + public let profileImageUrl: String +} diff --git a/14th-team5-iOS/Data/Sources/APIs/Camera/DataMapping/UpdateRealEmojiImageRequestDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Camera/DataMapping/UpdateRealEmojiImageRequestDTO.swift new file mode 100644 index 000000000..c5f5bd0eb --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/Camera/DataMapping/UpdateRealEmojiImageRequestDTO.swift @@ -0,0 +1,13 @@ +// +// UpdateRealEmojiImageRequestDTO.swift +// Data +// +// Created by 김도현 on 11/18/24. +// + +import Foundation + + +public struct UpdateRealEmojiImageRequestDTO: Encodable { + public let imageUrl: String +} diff --git a/14th-team5-iOS/Data/Sources/APIs/Camera/Repository/CameraRepository.swift b/14th-team5-iOS/Data/Sources/APIs/Camera/Repository/CameraRepository.swift new file mode 100644 index 000000000..f883280ec --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/Camera/Repository/CameraRepository.swift @@ -0,0 +1,79 @@ +// +// CameraRepository.swift +// Data +// +// Created by Kim dohyun on 6/7/24. +// + +import Foundation + +import Core +import Domain +import RxCocoa +import RxSwift + + +public final class CameraRepository { + + public var disposeBag: DisposeBag = DisposeBag() + private let cameraAPIWorker: CameraAPIWorker = CameraAPIWorker() + + public init() { } + + +} + +extension CameraRepository: CameraRepositoryProtocol { + + public func createFeedImage(query: CreateFeedQuery, body: CreateFeedRequest) -> Observable { + let body = CreateFeedRequestDTO(imageUrl: body.imageUrl, content: body.content, uploadTime: body.uploadTime) + + return cameraAPIWorker.createFeed(query: query, body: body) + .map { $0?.toDomain() } + } + + public func createEmojiImagePresignedURL(memberID: String, body: CreatePresignedURLRequest) -> Observable { + let body = CreatePresignedURLReqeustDTO(imageName: body.imageName) + return cameraAPIWorker.createRealEmojiPresignedURL(memberId: memberID, body: body) + .map { $0?.toDomain() } + } + + public func createProfileImagePresignedURL(body: CreatePresignedURLRequest) -> Observable { + let body = CreatePresignedURLReqeustDTO(imageName: body.imageName) + return cameraAPIWorker.createProfilePresignedURL(body: body) + .map { $0?.toDomain() } + } + + public func createEmojiImage(memberId: String, body: CreateEmojiImageRequest) -> Observable { + let body = CreateEmojiImageReqeustDTO(type: body.type, imageUrl: body.imageUrl) + return cameraAPIWorker.createRealEmoji(memberId: memberId, body: body) + .map { $0.toDomain() } + } + + public func fetchDailyMissonItem() -> Observable { + return cameraAPIWorker.fetchDailyMisson() + .map { $0?.toDomain() } + } + + public func fetchEmojiList(memberId: String) -> Observable<[CameraRealEmojiImageItemEntity?]> { + return cameraAPIWorker.fetchRealEmoji(memberId: memberId) + .map { $0?.toDomain() ?? [] } + } + + public func updateUserProfileImage(memberId: String, body: UpdateProfileImageRequest) -> Observable { + let body = UpdateProfileImageRequestDTO(profileImageUrl: body.profileImageUrl) + return cameraAPIWorker.updateProfileImage(memberId: memberId, body: body) + .map { $0?.toDomain() } + } + + public func updateEmojiImage(memberId: String, realEmojiId: String, body: UpdateRealEmojiImageRequest) -> Observable { + let body = UpdateRealEmojiImageRequestDTO(imageUrl: body.imageUrl) + return cameraAPIWorker.updateRealEmoji(memberId: memberId, realEmojiId: realEmojiId, body: body) + .map { $0?.toDomain() } + } + + public func uploadImageToS3Bucket(_ presignedURL: String, image: Data) -> Observable { + return cameraAPIWorker.uploadImageToPresignedURL(presignedURL, image: image) + } + +} diff --git a/14th-team5-iOS/Data/Sources/APIs/Members/DataMapping/MembersProfileResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Members/DataMapping/MembersProfileResponseDTO.swift index a14d45914..d1f250ea7 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Members/DataMapping/MembersProfileResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Members/DataMapping/MembersProfileResponseDTO.swift @@ -10,7 +10,7 @@ import Foundation import Domain -struct MembersProfileResponseDTO: Decodable { +public struct MembersProfileResponseDTO: Decodable { let memberId: String let name: String let imageUrl: String? diff --git a/14th-team5-iOS/Data/Sources/Trash/Camera/CameraAPI/CameraAPIWorker.swift b/14th-team5-iOS/Data/Sources/Trash/Camera/CameraAPI/CameraAPIWorker.swift deleted file mode 100644 index 2f10c6643..000000000 --- a/14th-team5-iOS/Data/Sources/Trash/Camera/CameraAPI/CameraAPIWorker.swift +++ /dev/null @@ -1,160 +0,0 @@ -// -// CameraAPIWorker.swift -// Data -// -// Created by Kim dohyun on 12/21/23. -// - -import Foundation -import Core - -import Alamofire -import Domain -import RxSwift - - -typealias CameraAPIWorker = CameraAPIs.Worker - - -extension CameraAPIs { - final class Worker: APIWorker { - - static let queue = { - ConcurrentDispatchQueueScheduler(queue: DispatchQueue(label: "CameraAPIQueue", qos: .utility)) - }() - - override init() { - super.init() - self.id = "CameraAPIWorker" - } - } -} - - -extension CameraAPIWorker { - public func createProfilePresignedURL(parameters: Encodable) -> Single { - - let spec = CameraAPIs.uploadProfileImageURL.spec - let accessToken = App.Repository.token.accessToken.value?.accessToken ?? "" - - return request(spec: spec, headers: [BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.xAuthToken(accessToken)], jsonEncodable: parameters) - .subscribe(on: Self.queue) - .map(CameraDisplayImageResponseDTO.self) - .catchAndReturn(nil) - .asSingle() - } - - - - public func createFeedPresignedURL(parameters: Encodable) -> Single { - let spec = CameraAPIs.uploadImageURL.spec - let accessToken = App.Repository.token.accessToken.value?.accessToken ?? "" - - return request(spec: spec, headers: [BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.xAuthToken(accessToken)], jsonEncodable: parameters) - .subscribe(on: Self.queue) - .map(CameraDisplayImageResponseDTO.self) - .catchAndReturn(nil) - .asSingle() - } - - public func editProfileImageToS3(memberId: String, parameters: Encodable) -> Single { - let spec = CameraAPIs.editProfileImage(memberId).spec - let accessToken = App.Repository.token.accessToken.value?.accessToken ?? "" - return request(spec: spec, headers: [BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.xAuthToken(accessToken)], jsonEncodable: parameters) - .subscribe(on: Self.queue) - .map(MembersProfileResponseDTO.self) - .catchAndReturn(nil) - .asSingle() - } - - public func uploadImageToPresignedURL(toURL url: String, withImageData imageData: Data) -> Single { - let accessToken = App.Repository.token.accessToken.value?.accessToken ?? "" - - let spec = CameraAPIs.presignedURL(url).spec - return upload(spec: spec, headers: [BibbiAPI.Header.xAppKey, BibbiAPI.Header.xAuthToken(accessToken)], image: imageData) - .subscribe(on: Self.queue) - .catchAndReturn(false) - .debug() - .map { $0 } - } - - public func combineWithTextImageUpload(parameters: Encodable, query: CameraMissionFeedQuery) -> Single { - let spec = CameraAPIs.updateImage(query).spec - let accessToken = App.Repository.token.accessToken.value?.accessToken ?? "" - return request(spec: spec, headers: [BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.xAuthToken(accessToken)], jsonEncodable: parameters) - .subscribe(on: Self.queue) - .map(CameraDisplayPostResponseDTO.self) - .map { dto in - guard let dto = dto else { return nil } - let repository = PostUserDefaultsRepository() - repository.savePostUploadDate(createdAt: dto.createdAt) - return dto - } - .catchAndReturn(nil) - .asSingle() - - } - - public func createRealEmojiPresignedURL(parameters: Encodable) -> Single { - //TODO: Repository로 코드 원복 - let memberId = App.Repository.member.memberID.value ?? "" - let accessToken = App.Repository.token.accessToken.value?.accessToken ?? "" - let spec = CameraAPIs.uploadRealEmojiURL(memberId).spec - - return request(spec: spec, headers: [BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.xAuthToken(accessToken)], jsonEncodable: parameters) - .subscribe(on: Self.queue) - .map(CameraRealEmojiPreSignedResponseDTO.self) - .catchAndReturn(nil) - .asSingle() - - } - - public func uploadRealEmojiImageToS3(parameters: Encodable) -> Single { - //TODO: Repository로 코드 원복 - let memberId = App.Repository.member.memberID.value ?? "" - let accessToken = App.Repository.token.accessToken.value?.accessToken ?? "" - let spec = CameraAPIs.updateRealEmojiImage(memberId).spec - - return request(spec: spec, headers: [BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.xAuthToken(accessToken)], jsonEncodable: parameters) - .subscribe(on: Self.queue) - .map(CameraCreateRealEmojiResponseDTO.self) - .catchAndReturn(nil) - .asSingle() - } - - public func loadRealEmojiImage() -> Single { - //TODO: Repository로 코드 원복 - let memberId = App.Repository.member.memberID.value ?? "" - let accessToken = App.Repository.token.accessToken.value?.accessToken ?? "" - let spec = CameraAPIs.reloadRealEmoji(memberId).spec - - return request(spec: spec, headers: [BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.xAuthToken(accessToken)]) - .subscribe(on: Self.queue) - .map(CameraRealEmojiImageItemResponseDTO.self) - .catchAndReturn(nil) - .asSingle() - } - - public func updateRealEmojiImage(realEmojiId: String, parameters: Encodable) -> Single { - //TODO: Repository로 코드 원복 - let memberId = App.Repository.member.memberID.value ?? "" - let accessToken = App.Repository.token.accessToken.value?.accessToken ?? "" - let spec = CameraAPIs.modifyRealEmojiImage(memberId, realEmojiId).spec - return request(spec: spec, headers: [BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.xAuthToken(accessToken)], jsonEncodable: parameters) - .subscribe(on: Self.queue) - .map(CameraUpdateRealEmojiResponseDTO.self) - .catchAndReturn(nil) - .asSingle() - } - - public func fetchMissionItems() -> Single { - let spec = CameraAPIs.fetchMissionToday.spec - let accessToken = App.Repository.token.accessToken.value?.accessToken ?? "" - return request(spec: spec, headers: [BibbiAPI.Header.xAppKey, BibbiAPI.Header.acceptJson, BibbiAPI.Header.xAuthToken(accessToken)]) - .subscribe(on: Self.queue) - .map(CameraTodayMissionResponseDTO.self) - .catchAndReturn(nil) - .asSingle() - } -} - diff --git a/14th-team5-iOS/Data/Sources/Trash/Camera/CameraAPI/CameraAPIs.swift b/14th-team5-iOS/Data/Sources/Trash/Camera/CameraAPI/CameraAPIs.swift deleted file mode 100644 index 484af9d77..000000000 --- a/14th-team5-iOS/Data/Sources/Trash/Camera/CameraAPI/CameraAPIs.swift +++ /dev/null @@ -1,51 +0,0 @@ -// -// CameraAPIs.swift -// Data -// -// Created by Kim dohyun on 12/21/23. -// - -import Core -import Domain -import Foundation - - -enum CameraAPIs: API { - case uploadImageURL - case presignedURL(String) - case updateImage(CameraMissionFeedQuery) - case uploadProfileImageURL - case editProfileImage(String) - case uploadRealEmojiURL(String) - case updateRealEmojiImage(String) - case reloadRealEmoji(String) - case modifyRealEmojiImage(String, String) - case fetchMissionToday - - var spec: APISpec { - switch self { - case .uploadProfileImageURL: - return APISpec(method: .post, url: "\(BibbiAPI.hostApi)/members/image-upload-request") - case let .editProfileImage(memberId): - return APISpec(method: .put, url: "\(BibbiAPI.hostApi)/members/profile-image-url/\(memberId)") - case .uploadImageURL: - return APISpec(method: .post, url: "\(BibbiAPI.hostApi)/posts/image-upload-request") - case let .presignedURL(url): - return APISpec(method: .put, url: url) - case let .updateImage(query): - return APISpec(method: .post, url: "\(BibbiAPI.hostApi)/posts?type=\(query.type)&available=\(query.isUploded)") - case let .uploadRealEmojiURL(memberId): - return APISpec(method: .post, url: "\(BibbiAPI.hostApi)/members/\(memberId)/real-emoji/image-upload-request") - case let .updateRealEmojiImage(memberId): - return APISpec(method: .post, url: "\(BibbiAPI.hostApi)/members/\(memberId)/real-emoji") - case let .reloadRealEmoji(memberId): - return APISpec(method: .get, url: "\(BibbiAPI.hostApi)/members/\(memberId)/real-emoji") - case let .modifyRealEmojiImage(memberId, realEmojiId): - return APISpec(method: .put, url: "\(BibbiAPI.hostApi)/members/\(memberId)/real-emoji/\(realEmojiId)") - case .fetchMissionToday: - return APISpec(method: .get, url: "\(BibbiAPI.hostApi)/missions/today") - } - } - - -} diff --git a/14th-team5-iOS/Data/Sources/Trash/Camera/Repository/CameraRepository.swift b/14th-team5-iOS/Data/Sources/Trash/Camera/Repository/CameraRepository.swift deleted file mode 100644 index f63e4dd79..000000000 --- a/14th-team5-iOS/Data/Sources/Trash/Camera/Repository/CameraRepository.swift +++ /dev/null @@ -1,81 +0,0 @@ -// -// CameraRepository.swift -// Data -// -// Created by Kim dohyun on 6/7/24. -// - -import Foundation - -import Core -import Domain -import RxCocoa -import RxSwift - - -public final class CameraRepository { - - public var disposeBag: DisposeBag = DisposeBag() - private let cameraAPIWorker: CameraAPIWorker = CameraAPIWorker() - - public init() { } - - -} - -extension CameraRepository: CameraRepositoryProtocol { - - - public func combineWithTextImage(parameters: CameraDisplayPostParameters, query:CameraMissionFeedQuery) -> Single { - return cameraAPIWorker.combineWithTextImageUpload(parameters: parameters, query: query) - .map { $0?.toDomain() } - .catchAndReturn(nil) - } - - public func addPresignedeImageURL(parameters: CameraDisplayImageParameters) -> Single { - return cameraAPIWorker.createProfilePresignedURL(parameters: parameters) - .map { $0?.toDomain() } - } - - - - public func uploadImageToS3(to url: String, from imageData: Data) -> Single { - return cameraAPIWorker.uploadImageToPresignedURL(toURL: url, withImageData: imageData) - } - - public func editProfleImageToS3(memberId: String, parameter: ProfileImageEditParameter) -> Single { - return cameraAPIWorker.editProfileImageToS3(memberId: memberId, parameters: parameter) - .do { - guard let userEntity = $0?.toProfileEntity() else { return } - let familyUserDefaults = FamilyInfoUserDefaults() - familyUserDefaults.updateFamilyMember(userEntity) - } - .map { $0?.toDomain() } - } - - public func fetchRealEmojiImageURL(memberId: String, parameters: CameraRealEmojiParameters) -> Single { - return cameraAPIWorker.createRealEmojiPresignedURL(parameters: parameters) - .map { $0?.toDomain() } - } - - public func uploadRealEmojiImageToS3(memberId: String, parameters: CameraCreateRealEmojiParameters) -> Single { - return cameraAPIWorker.uploadRealEmojiImageToS3(parameters: parameters) - .map { $0?.toDomain() } - } - - public func fetchRealEmojiItems() -> Single<[CameraRealEmojiImageItemEntity?]> { - return cameraAPIWorker.loadRealEmojiImage() - .map { $0?.toDomain() ?? [] } - } - - public func updateRealEmojiImage(memberId: String, realEmojiId: String, parameters: CameraUpdateRealEmojiParameters) -> Single { - return cameraAPIWorker.updateRealEmojiImage(realEmojiId: realEmojiId, parameters: parameters) - .map { $0?.toDomain() } - } - - public func fetchTodayMissionItem() -> Single { - return cameraAPIWorker.fetchMissionItems() - .map { $0?.toDomain() } - } - -} diff --git a/14th-team5-iOS/Domain/Sources/Trash/Camera/Entity/CameraCreateRealEmojiEntity.swift b/14th-team5-iOS/Domain/Sources/Entities/Camera/CameraCreateRealEmojiEntity.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Trash/Camera/Entity/CameraCreateRealEmojiEntity.swift rename to 14th-team5-iOS/Domain/Sources/Entities/Camera/CameraCreateRealEmojiEntity.swift diff --git a/14th-team5-iOS/Domain/Sources/Trash/Camera/Entity/CameraMissionFeedQuery.swift b/14th-team5-iOS/Domain/Sources/Entities/Camera/CameraMissionFeedQuery.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Trash/Camera/Entity/CameraMissionFeedQuery.swift rename to 14th-team5-iOS/Domain/Sources/Entities/Camera/CameraMissionFeedQuery.swift diff --git a/14th-team5-iOS/Domain/Sources/Trash/Camera/Entity/CameraPostEntity.swift b/14th-team5-iOS/Domain/Sources/Entities/Camera/CameraPostEntity.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Trash/Camera/Entity/CameraPostEntity.swift rename to 14th-team5-iOS/Domain/Sources/Entities/Camera/CameraPostEntity.swift diff --git a/14th-team5-iOS/Domain/Sources/Trash/Camera/Entity/CameraPreSignedEntity.swift b/14th-team5-iOS/Domain/Sources/Entities/Camera/CameraPreSignedEntity.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Trash/Camera/Entity/CameraPreSignedEntity.swift rename to 14th-team5-iOS/Domain/Sources/Entities/Camera/CameraPreSignedEntity.swift diff --git a/14th-team5-iOS/Domain/Sources/Trash/Camera/Entity/CameraRealEmojiImageItemEntity.swift b/14th-team5-iOS/Domain/Sources/Entities/Camera/CameraRealEmojiImageItemEntity.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Trash/Camera/Entity/CameraRealEmojiImageItemEntity.swift rename to 14th-team5-iOS/Domain/Sources/Entities/Camera/CameraRealEmojiImageItemEntity.swift diff --git a/14th-team5-iOS/Domain/Sources/Trash/Camera/Entity/CameraRealEmojiItems.swift b/14th-team5-iOS/Domain/Sources/Entities/Camera/CameraRealEmojiItems.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Trash/Camera/Entity/CameraRealEmojiItems.swift rename to 14th-team5-iOS/Domain/Sources/Entities/Camera/CameraRealEmojiItems.swift diff --git a/14th-team5-iOS/Domain/Sources/Trash/Camera/Entity/CameraRealEmojiPreSignedEntity.swift b/14th-team5-iOS/Domain/Sources/Entities/Camera/CameraRealEmojiPreSignedEntity.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Trash/Camera/Entity/CameraRealEmojiPreSignedEntity.swift rename to 14th-team5-iOS/Domain/Sources/Entities/Camera/CameraRealEmojiPreSignedEntity.swift diff --git a/14th-team5-iOS/Domain/Sources/Trash/Camera/Entity/CameraTodayMssionEntity.swift b/14th-team5-iOS/Domain/Sources/Entities/Camera/CameraTodayMssionEntity.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Trash/Camera/Entity/CameraTodayMssionEntity.swift rename to 14th-team5-iOS/Domain/Sources/Entities/Camera/CameraTodayMssionEntity.swift diff --git a/14th-team5-iOS/Domain/Sources/Trash/Camera/Entity/CameraUpdateRealEmojiEntity.swift b/14th-team5-iOS/Domain/Sources/Entities/Camera/CameraUpdateRealEmojiEntity.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Trash/Camera/Entity/CameraUpdateRealEmojiEntity.swift rename to 14th-team5-iOS/Domain/Sources/Entities/Camera/CameraUpdateRealEmojiEntity.swift diff --git a/14th-team5-iOS/Domain/Sources/Entities/Camera/CreateEmojiImageRequest.swift b/14th-team5-iOS/Domain/Sources/Entities/Camera/CreateEmojiImageRequest.swift new file mode 100644 index 000000000..9128bb142 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Entities/Camera/CreateEmojiImageRequest.swift @@ -0,0 +1,19 @@ +// +// CreateEmojiImageRequest.swift +// Domain +// +// Created by 김도현 on 11/17/24. +// + +import Foundation + + +public struct CreateEmojiImageRequest { + public let type: String + public let imageUrl: String + + public init(type: String, imageUrl: String) { + self.type = type + self.imageUrl = imageUrl + } +} diff --git a/14th-team5-iOS/Domain/Sources/Entities/Camera/CreateFeedQuery.swift b/14th-team5-iOS/Domain/Sources/Entities/Camera/CreateFeedQuery.swift new file mode 100644 index 000000000..96a434a04 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Entities/Camera/CreateFeedQuery.swift @@ -0,0 +1,17 @@ +// +// CreateFeedQuery.swift +// Domain +// +// Created by 김도현 on 11/18/24. +// + +import Foundation + + +public struct CreateFeedQuery { + public let type: String + + public init(type: String) { + self.type = type + } +} diff --git a/14th-team5-iOS/Domain/Sources/Trash/Camera/Parameters/CameraDisplayPostParameters.swift b/14th-team5-iOS/Domain/Sources/Entities/Camera/CreateFeedRequest.swift similarity index 54% rename from 14th-team5-iOS/Domain/Sources/Trash/Camera/Parameters/CameraDisplayPostParameters.swift rename to 14th-team5-iOS/Domain/Sources/Entities/Camera/CreateFeedRequest.swift index 88d5fea35..400beea0b 100644 --- a/14th-team5-iOS/Domain/Sources/Trash/Camera/Parameters/CameraDisplayPostParameters.swift +++ b/14th-team5-iOS/Domain/Sources/Entities/Camera/CreateFeedRequest.swift @@ -1,23 +1,25 @@ // -// CameraDisplayPostParameters.swift +// CreateFeedRequest.swift // Domain // -// Created by Kim dohyun on 12/22/23. +// Created by 김도현 on 11/17/24. // import Foundation - -public struct CameraDisplayPostParameters: Encodable { +public struct CreateFeedRequest { public let imageUrl: String public let content: String public let uploadTime: String - public init(imageUrl: String, content: String, uploadTime: String) { + public init( + imageUrl: String, + content: String, + uploadTime: String + ) { self.imageUrl = imageUrl self.content = content self.uploadTime = uploadTime } - } diff --git a/14th-team5-iOS/Domain/Sources/Entities/Camera/CreatePresignedURLRequest.swift b/14th-team5-iOS/Domain/Sources/Entities/Camera/CreatePresignedURLRequest.swift new file mode 100644 index 000000000..f72c5b71b --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Entities/Camera/CreatePresignedURLRequest.swift @@ -0,0 +1,16 @@ +// +// CreatePresignedURLRequest.swift +// Domain +// +// Created by 김도현 on 11/17/24. +// + +import Foundation + +public struct CreatePresignedURLRequest { + public let imageName: String + + public init(imageName: String) { + self.imageName = imageName + } +} diff --git a/14th-team5-iOS/Domain/Sources/Entities/Camera/UpdateProfileImageRequest.swift b/14th-team5-iOS/Domain/Sources/Entities/Camera/UpdateProfileImageRequest.swift new file mode 100644 index 000000000..686153969 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Entities/Camera/UpdateProfileImageRequest.swift @@ -0,0 +1,17 @@ +// +// UpdateProfileImageRequest.swift +// Domain +// +// Created by 김도현 on 11/17/24. +// + +import Foundation + + +public struct UpdateProfileImageRequest { + public let profileImageUrl: String + + public init(profileImageUrl: String) { + self.profileImageUrl = profileImageUrl + } +} diff --git a/14th-team5-iOS/Domain/Sources/Entities/Camera/UpdateRealEmojiImageRequest.swift b/14th-team5-iOS/Domain/Sources/Entities/Camera/UpdateRealEmojiImageRequest.swift new file mode 100644 index 000000000..47eaecc9d --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Entities/Camera/UpdateRealEmojiImageRequest.swift @@ -0,0 +1,17 @@ +// +// UpdateRealEmojiImageRequest.swift +// Domain +// +// Created by 김도현 on 11/18/24. +// + +import Foundation + + +public struct UpdateRealEmojiImageRequest { + public let imageUrl: String + + public init(imageUrl: String) { + self.imageUrl = imageUrl + } +} diff --git a/14th-team5-iOS/Domain/Sources/Trash/Camera/Interfaces/CameraRepositoryProtocol.swift b/14th-team5-iOS/Domain/Sources/Repositories/CameraRepository.swift similarity index 50% rename from 14th-team5-iOS/Domain/Sources/Trash/Camera/Interfaces/CameraRepositoryProtocol.swift rename to 14th-team5-iOS/Domain/Sources/Repositories/CameraRepository.swift index b54c96b41..f88501873 100644 --- a/14th-team5-iOS/Domain/Sources/Trash/Camera/Interfaces/CameraRepositoryProtocol.swift +++ b/14th-team5-iOS/Domain/Sources/Repositories/CameraRepository.swift @@ -1,14 +1,13 @@ // -// CameraRepositoryProtocol.swift +// CameraRepository.swift // Domain // -// Created by Kim dohyun on 6/7/24. +// Created by 김도현 on 11/18/24. // import Foundation import RxSwift -import RxCocoa public enum UploadLocation { case survival @@ -62,18 +61,23 @@ public enum UploadLocation { } } - - public protocol CameraRepositoryProtocol { var disposeBag: DisposeBag { get } - func addPresignedeImageURL(parameters: CameraDisplayImageParameters) -> Single - func uploadImageToS3(to url: String, from image: Data) -> Single - func editProfleImageToS3(memberId: String, parameter: ProfileImageEditParameter) -> Single - func fetchRealEmojiImageURL(memberId: String, parameters: CameraRealEmojiParameters) -> Single - func uploadRealEmojiImageToS3(memberId: String, parameters: CameraCreateRealEmojiParameters) -> Single - func fetchRealEmojiItems() -> Single<[CameraRealEmojiImageItemEntity?]> - func updateRealEmojiImage(memberId: String, realEmojiId: String, parameters: CameraUpdateRealEmojiParameters) -> Single - func fetchTodayMissionItem() -> Single - func combineWithTextImage(parameters: CameraDisplayPostParameters, query: CameraMissionFeedQuery) -> Single + /// CREATE 메서드 + func createFeedImage(query: CreateFeedQuery, body: CreateFeedRequest) -> Observable + func createProfileImagePresignedURL(body: CreatePresignedURLRequest) -> Observable + func createEmojiImagePresignedURL(memberID: String, body: CreatePresignedURLRequest) -> Observable + func createEmojiImage(memberId: String, body: CreateEmojiImageRequest) -> Observable + + /// FETCH 메서드 + func fetchEmojiList(memberId: String) -> Observable<[CameraRealEmojiImageItemEntity?]> + func fetchDailyMissonItem() -> Observable + + /// UPDATE 메서드 + func updateUserProfileImage(memberId: String, body: UpdateProfileImageRequest) -> Observable + func updateEmojiImage(memberId: String, realEmojiId: String, body: UpdateRealEmojiImageRequest) -> Observable + + /// UPLOAD 메서드 + func uploadImageToS3Bucket(_ presignedURL: String, image: Data) -> Observable } diff --git a/14th-team5-iOS/Domain/Sources/Trash/Camera/Parameters/CameraDisplayImageParameters.swift b/14th-team5-iOS/Domain/Sources/Trash/Account/Parameters/CameraDisplayImageParameters.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Trash/Camera/Parameters/CameraDisplayImageParameters.swift rename to 14th-team5-iOS/Domain/Sources/Trash/Account/Parameters/CameraDisplayImageParameters.swift diff --git a/14th-team5-iOS/Domain/Sources/Trash/Camera/Parameters/CameraCreateRealEmojiParameters.swift b/14th-team5-iOS/Domain/Sources/Trash/Camera/Parameters/CameraCreateRealEmojiParameters.swift deleted file mode 100644 index 72d0eadcf..000000000 --- a/14th-team5-iOS/Domain/Sources/Trash/Camera/Parameters/CameraCreateRealEmojiParameters.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// CameraCreateRealEmojiParameters.swift -// Domain -// -// Created by Kim dohyun on 1/22/24. -// - -import Foundation - - -public struct CameraCreateRealEmojiParameters: Encodable { - public var type: String - public var imageUrl: String - - public init(type: String, imageUrl: String) { - self.type = type - self.imageUrl = imageUrl - } - -} diff --git a/14th-team5-iOS/Domain/Sources/Trash/Camera/Parameters/CameraRealEmojiParameters.swift b/14th-team5-iOS/Domain/Sources/Trash/Camera/Parameters/CameraRealEmojiParameters.swift deleted file mode 100644 index 83bdbeb4d..000000000 --- a/14th-team5-iOS/Domain/Sources/Trash/Camera/Parameters/CameraRealEmojiParameters.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// CameraRealEmojiParameters.swift -// Domain -// -// Created by Kim dohyun on 1/22/24. -// - -import Foundation - - -public struct CameraRealEmojiParameters: Encodable { - public var imageName: String - - public init(imageName: String) { - self.imageName = imageName - } -} diff --git a/14th-team5-iOS/Domain/Sources/Trash/Camera/Parameters/CameraUpdateRealEmojiParameters.swift b/14th-team5-iOS/Domain/Sources/Trash/Camera/Parameters/CameraUpdateRealEmojiParameters.swift deleted file mode 100644 index 73eb477f2..000000000 --- a/14th-team5-iOS/Domain/Sources/Trash/Camera/Parameters/CameraUpdateRealEmojiParameters.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// CameraUpdateRealEmojiParameters.swift -// Domain -// -// Created by Kim dohyun on 1/24/24. -// - -import Foundation - - -public struct CameraUpdateRealEmojiParameters: Encodable { - - public var imageUrl: String - - public init(imageUrl: String) { - self.imageUrl = imageUrl - } -} diff --git a/14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/Mission/FetchCameraTodayMissionUseCase.swift b/14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/Mission/FetchCameraTodayMissionUseCase.swift index 8eb066361..799963063 100644 --- a/14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/Mission/FetchCameraTodayMissionUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/Mission/FetchCameraTodayMissionUseCase.swift @@ -12,7 +12,7 @@ import RxCocoa public protocol FetchCameraTodayMissionUseCaseProtocol { - func execute() -> Single + func execute() -> Observable } @@ -24,9 +24,7 @@ public final class FetchCameraTodayMissionUseCase: FetchCameraTodayMissionUseCas self.cameraRepository = cameraRepository } - public func execute() -> Single { - return cameraRepository.fetchTodayMissionItem() + public func execute() -> Observable { + return cameraRepository.fetchDailyMissonItem() } - - } diff --git a/14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/Profile/EditCameraProfileImageUseCase.swift b/14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/Profile/EditCameraProfileImageUseCase.swift index 84589cf10..e01c9d942 100644 --- a/14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/Profile/EditCameraProfileImageUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/Profile/EditCameraProfileImageUseCase.swift @@ -11,7 +11,7 @@ import RxSwift import RxCocoa public protocol EditCameraProfileImageUseCaseProtocol { - func execute(memberId: String, parameter: ProfileImageEditParameter) -> Single + func execute(memberId: String, body: UpdateProfileImageRequest) -> Observable } @@ -23,13 +23,10 @@ public final class EditCameraProfileImageUseCase: EditCameraProfileImageUseCaseP self.cameraRepository = cameraRepository } - - public func execute(memberId: String, parameter: ProfileImageEditParameter) -> Single { - return cameraRepository.editProfleImageToS3(memberId: memberId, parameter: parameter) + public func execute(memberId: String, body: UpdateProfileImageRequest) -> Observable { + return cameraRepository.updateUserProfileImage(memberId: memberId, body: body) } - - } diff --git a/14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/RealEmoji/FetchCameraRealEmojiListUseCase.swift b/14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/RealEmoji/FetchCameraRealEmojiListUseCase.swift index ec0d1dfde..4c6764bc0 100644 --- a/14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/RealEmoji/FetchCameraRealEmojiListUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/RealEmoji/FetchCameraRealEmojiListUseCase.swift @@ -12,7 +12,7 @@ import RxCocoa public protocol FetchCameraRealEmojiListUseCaseProtocol { - func execute() -> Single<[CameraRealEmojiImageItemEntity?]> + func execute(memberId: String) -> Observable<[CameraRealEmojiImageItemEntity?]> } @@ -24,8 +24,8 @@ public final class FetchCameraRealEmojiListUseCase: FetchCameraRealEmojiListUseC self.cameraRepository = cameraRepository } - public func execute() -> Single<[CameraRealEmojiImageItemEntity?]> { - return cameraRepository.fetchRealEmojiItems() + public func execute(memberId: String) -> Observable<[CameraRealEmojiImageItemEntity?]> { + return cameraRepository.fetchEmojiList(memberId: memberId) } } diff --git a/14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/RealEmoji/FetchCameraRealEmojiUpdateUseCase.swift b/14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/RealEmoji/FetchCameraRealEmojiUpdateUseCase.swift index d0ad19af5..eb2787aa9 100644 --- a/14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/RealEmoji/FetchCameraRealEmojiUpdateUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/RealEmoji/FetchCameraRealEmojiUpdateUseCase.swift @@ -12,7 +12,7 @@ import RxCocoa public protocol FetchCameraRealEmojiUpdateUseCaseProtocol { - func execute(memberId: String, emojiId: String, parameter: CameraUpdateRealEmojiParameters) -> Single + func execute(memberId: String, realEmojiId: String, body: UpdateRealEmojiImageRequest) -> Observable } @@ -23,7 +23,7 @@ public final class FetchCameraRealEmojiUpdateUseCase: FetchCameraRealEmojiUpdate self.cameraRepostiroy = cameraRepostiroy } - public func execute(memberId: String, emojiId: String, parameter: CameraUpdateRealEmojiParameters) -> Single { - return cameraRepostiroy.updateRealEmojiImage(memberId: memberId, realEmojiId: emojiId, parameters: parameter) + public func execute(memberId: String, realEmojiId: String, body: UpdateRealEmojiImageRequest) -> Observable { + return cameraRepostiroy.updateEmojiImage(memberId: memberId, realEmojiId: realEmojiId, body: body) } } diff --git a/14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/RealEmoji/FetchCameraRealEmojiUploadUseCase.swift b/14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/RealEmoji/FetchCameraRealEmojiUploadUseCase.swift index c24738ee4..5c9c684b4 100644 --- a/14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/RealEmoji/FetchCameraRealEmojiUploadUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/RealEmoji/FetchCameraRealEmojiUploadUseCase.swift @@ -11,7 +11,7 @@ import RxSwift import RxCocoa public protocol FetchCameraRealEmojiUploadUseCaseProtocol { - func execute(memberId: String, parameter: CameraCreateRealEmojiParameters) -> Single + func execute(memberId: String, body: CreateEmojiImageRequest) -> Observable } public final class FetchCameraRealEmojiUploadUseCase: FetchCameraRealEmojiUploadUseCaseProtocol { @@ -22,8 +22,8 @@ public final class FetchCameraRealEmojiUploadUseCase: FetchCameraRealEmojiUpload self.cameraRepository = cameraRepository } - public func execute(memberId: String, parameter: CameraCreateRealEmojiParameters) -> Single { - return cameraRepository.uploadRealEmojiImageToS3(memberId: memberId, parameters: parameter) + public func execute(memberId: String, body: CreateEmojiImageRequest) -> Observable { + return cameraRepository.createEmojiImage(memberId: memberId, body: body) } } diff --git a/14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/RealEmoji/FetchCameraRealEmojiUseCase.swift b/14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/RealEmoji/FetchCameraRealEmojiUseCase.swift index b5bc3a91f..ede13a632 100644 --- a/14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/RealEmoji/FetchCameraRealEmojiUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/RealEmoji/FetchCameraRealEmojiUseCase.swift @@ -12,12 +12,9 @@ import RxCocoa public protocol FetchCameraRealEmojiUseCaseProtocol { - func execute(memberId: String, parameter: CameraRealEmojiParameters) -> Single - + func execute(memberId: String, body: CreatePresignedURLRequest) -> Observable } - - public final class FetchCameraRealEmojiUseCase: FetchCameraRealEmojiUseCaseProtocol { private let cameraRepository: any CameraRepositoryProtocol @@ -26,9 +23,7 @@ public final class FetchCameraRealEmojiUseCase: FetchCameraRealEmojiUseCaseProto self.cameraRepository = cameraRepository } - - public func execute(memberId: String, parameter: CameraRealEmojiParameters) -> Single { - return cameraRepository.fetchRealEmojiImageURL(memberId: memberId, parameters: parameter) + public func execute(memberId: String, body: CreatePresignedURLRequest) -> Observable { + return cameraRepository.createEmojiImagePresignedURL(memberID: memberId, body: body) } - } diff --git a/14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/Camera/CreateCameraImageUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Camera/CreateCameraImageUseCase.swift similarity index 58% rename from 14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/Camera/CreateCameraImageUseCase.swift rename to 14th-team5-iOS/Domain/Sources/UseCases/Camera/CreateCameraImageUseCase.swift index 345f244df..139d07cf2 100644 --- a/14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/Camera/CreateCameraImageUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/Camera/CreateCameraImageUseCase.swift @@ -12,18 +12,18 @@ import RxCocoa public protocol CreateCameraImageUseCaseProtocol { - func execute(parameter: CameraDisplayPostParameters, query: CameraMissionFeedQuery) -> Single + func execute(query: CreateFeedQuery, body: CreateFeedRequest) -> Observable } public final class CreateCameraImageUseCase: CreateCameraImageUseCaseProtocol { - + private let cameraRepository: any CameraRepositoryProtocol public init(cameraRepository: any CameraRepositoryProtocol) { self.cameraRepository = cameraRepository } - public func execute(parameter: CameraDisplayPostParameters, query: CameraMissionFeedQuery) -> Single { - return cameraRepository.combineWithTextImage(parameters: parameter, query: query) + public func execute(query: CreateFeedQuery, body: CreateFeedRequest) -> Observable { + return cameraRepository.createFeedImage(query: query, body: body) } } diff --git a/14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/Camera/CreateCameraUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Camera/CreateCameraUseCase.swift similarity index 62% rename from 14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/Camera/CreateCameraUseCase.swift rename to 14th-team5-iOS/Domain/Sources/UseCases/Camera/CreateCameraUseCase.swift index 588f4af73..c466fca92 100644 --- a/14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/Camera/CreateCameraUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/Camera/CreateCameraUseCase.swift @@ -11,7 +11,7 @@ import RxSwift import RxCocoa public protocol CreateCameraUseCaseProtocol { - func execute(parameter: CameraDisplayImageParameters) -> Single + func execute(body: CreatePresignedURLRequest) -> Observable } public final class CreateCameraUseCase: CreateCameraUseCaseProtocol { @@ -22,8 +22,8 @@ public final class CreateCameraUseCase: CreateCameraUseCaseProtocol { self.cameraRepository = cameraRepository } - public func execute(parameter: CameraDisplayImageParameters) -> RxSwift.Single { - return cameraRepository.addPresignedeImageURL(parameters: parameter) + public func execute(body: CreatePresignedURLRequest) -> Observable { + return cameraRepository.createProfileImagePresignedURL(body: body) } } diff --git a/14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/Camera/FetchCameraUploadImageUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Camera/FetchCameraUploadImageUseCase.swift similarity index 67% rename from 14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/Camera/FetchCameraUploadImageUseCase.swift rename to 14th-team5-iOS/Domain/Sources/UseCases/Camera/FetchCameraUploadImageUseCase.swift index 913ece64b..cad667673 100644 --- a/14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/Camera/FetchCameraUploadImageUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/Camera/FetchCameraUploadImageUseCase.swift @@ -12,7 +12,7 @@ import RxCocoa public protocol FetchCameraUploadImageUseCaseProtocol { - func execute(to url: String, from image: Data) -> Single + func execute(_ presignedURL: String, image: Data) -> Observable } @@ -24,7 +24,7 @@ public final class FetchCameraUploadImageUseCase: FetchCameraUploadImageUseCaseP self.cameraRepository = cameraRepository } - public func execute(to url: String, from image: Data) -> Single { - return cameraRepository.uploadImageToS3(to: url, from: image) + public func execute(_ presignedURL: String, image: Data) -> Observable { + return cameraRepository.uploadImageToS3Bucket(presignedURL, image: image) } } From 8b432cee3f17ac64f9ed1039cf45d339e6545926 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=84?= Date: Mon, 25 Nov 2024 22:40:09 +0900 Subject: [PATCH 247/263] =?UTF-8?q?refactor:=20MissonAPIs,=20MissonAPIWork?= =?UTF-8?q?er=20=EA=B8=B0=EC=A1=B4=20APISpec=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=20(#709)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MissonAPIWorker 주석 추가 및 네이밍 수정 - Trash 폴더에 있던 Misson 관련 파일 이전 --- .../Mission/MissionAPI/MissionAPIWorker.swift | 43 +++++++------------ .../Mission/MissionAPI/MissionAPIs.swift | 24 +++++++---- .../Repository/MissionRepository.swift | 19 +++++--- .../Mission}/MissionContentEntity.swift | 0 .../Repositories/MissonRepository.swift | 22 ++++++++++ .../MissionRepositoryProtocol.swift | 15 ------- .../CheckMissionAlertShowUseCase.swift | 0 .../Misson}/FetchMissionContentUseCase.swift | 6 +-- 8 files changed, 68 insertions(+), 61 deletions(-) rename 14th-team5-iOS/Domain/Sources/{Trash/Mission/Entities => Entities/Mission}/MissionContentEntity.swift (100%) create mode 100644 14th-team5-iOS/Domain/Sources/Repositories/MissonRepository.swift delete mode 100644 14th-team5-iOS/Domain/Sources/Trash/Mission/Interfaces/MissionRepositoryProtocol.swift rename 14th-team5-iOS/Domain/Sources/{Trash/Mission/UseCases/Mission => UseCases/Misson}/CheckMissionAlertShowUseCase.swift (100%) rename 14th-team5-iOS/Domain/Sources/{Trash/Mission/UseCases/Mission => UseCases/Misson}/FetchMissionContentUseCase.swift (68%) diff --git a/14th-team5-iOS/Data/Sources/Trash/Mission/MissionAPI/MissionAPIWorker.swift b/14th-team5-iOS/Data/Sources/Trash/Mission/MissionAPI/MissionAPIWorker.swift index 2932965a1..add212220 100644 --- a/14th-team5-iOS/Data/Sources/Trash/Mission/MissionAPI/MissionAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Trash/Mission/MissionAPI/MissionAPIWorker.swift @@ -13,38 +13,25 @@ import Domain import RxSwift typealias MissionAPIWorker = MissionAPIs.Worker -extension MissionAPIs { - public final class Worker: APIWorker { - static let queue = { - ConcurrentDispatchQueueScheduler(queue: DispatchQueue(label: "MssionAPIQueue", qos: .utility)) - }() - - public override init() { - super.init() - self.id = "MissionAPIWorker" - } - } -} extension MissionAPIWorker { - private func getMissionContent(spec: APISpec, headers: [APIHeader]?) -> Single { - return request(spec: spec, headers: headers) - .subscribe(on: Self.queue) - .map(MissionContentResponseDTO.self) - .catchAndReturn(nil) - .map { $0?.toDomain() } - .asSingle() - } + /// 미션 정보를 조회하기 위한 API 입니다. + /// HTTP Method : GET + /// - Parameters : missonid(미션 ID) + /// - Returns : MissionContentResponseDTO + func fetchMissonContent(missonId: String) -> Observable { + let spec = MissionAPIs.fetchMissonContent(missonId: missonId).spec + + return request(spec) + } - func getMissionContent(missionId: String) -> Single { - let spec = MissionAPIs.getMissionContent(missionId).spec + /// 미션 일일 정보를 조회하기 위한 API 입니다. + /// HTTP Method : GET + /// - Returns : CameraTodayMissionResponseDTO + func fetchDailyMisson() -> Observable { + let spec = MissionAPIs.fetchDailyMisson.spec - return Observable.just(()) - .withLatestFrom(self._headers) - .observe(on: Self.queue) - .withUnretained(self) - .flatMap { $0.0.getMissionContent(spec: spec, headers: $0.1)} - .asSingle() + return request(spec) } } diff --git a/14th-team5-iOS/Data/Sources/Trash/Mission/MissionAPI/MissionAPIs.swift b/14th-team5-iOS/Data/Sources/Trash/Mission/MissionAPI/MissionAPIs.swift index c290b7ba0..fcf7cb654 100644 --- a/14th-team5-iOS/Data/Sources/Trash/Mission/MissionAPI/MissionAPIs.swift +++ b/14th-team5-iOS/Data/Sources/Trash/Mission/MissionAPI/MissionAPIs.swift @@ -10,18 +10,24 @@ import Foundation import Core import Domain -enum MissionAPIs: API { - case getTodayMission - case getMissionContent(String) - - var spec: APISpec { +enum MissionAPIs: BBAPI { + /// 미션 정보 조회 API + case fetchMissonContent(missonId: String) + /// 미션 일일 정보 조회 API + case fetchDailyMisson + + var spec: Spec { switch self { - case .getTodayMission: - return APISpec(method: .get, url: "\(BibbiAPI.hostApi)/missions/today") - case let .getMissionContent(missionId): - return APISpec(method: .get, url: "\(BibbiAPI.hostApi)/missions/\(missionId)") + case let .fetchMissonContent(missionId): + return Spec(method: .get, path: "/missions/\(missionId)") + case .fetchDailyMisson: + return Spec(method: .get, path: "/missions/today") } } + + final class Worker: BBRxAPIWorker { + init() { } + } } diff --git a/14th-team5-iOS/Data/Sources/Trash/Mission/Repository/MissionRepository.swift b/14th-team5-iOS/Data/Sources/Trash/Mission/Repository/MissionRepository.swift index 90d24b30e..e0e60f3c9 100644 --- a/14th-team5-iOS/Data/Sources/Trash/Mission/Repository/MissionRepository.swift +++ b/14th-team5-iOS/Data/Sources/Trash/Mission/Repository/MissionRepository.swift @@ -13,6 +13,7 @@ import Domain import RxSwift public final class MissionRepository: MissionRepositoryProtocol { + public let disposeBag: DisposeBag = DisposeBag() private let lastMissionUploadDateId = "lastMissionUploadDateId" @@ -22,26 +23,32 @@ public final class MissionRepository: MissionRepositoryProtocol { } extension MissionRepository { - - public func getMissionContent(missionId: String) -> Single { - return missionAPIWorker.getMissionContent(missionId: missionId) + + public func fetchMissonContentItem(missonId: String) -> Observable { + return missionAPIWorker.fetchMissonContent(missonId: missonId) + .map { $0?.toDomain() } + } + + public func fetchDailyMissonItem() -> Observable { + return missionAPIWorker.fetchDailyMisson() + .map { $0?.toDomain() } } public func isAlreadyShowMissionAlert() -> Observable { guard let lastDate = UserDefaults.standard.string(forKey: lastMissionUploadDateId) else { - saveMissionUploadDate() + updateMissonUploadDate() return .just(false) } if lastDate == Date().toFormatString(with: "yyyy-MM-dd") { return .just(true) } else { - saveMissionUploadDate() + updateMissonUploadDate() return .just(false) } } - private func saveMissionUploadDate() { + public func updateMissonUploadDate() { let isoDateFormatter = ISO8601DateFormatter() isoDateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] diff --git a/14th-team5-iOS/Domain/Sources/Trash/Mission/Entities/MissionContentEntity.swift b/14th-team5-iOS/Domain/Sources/Entities/Mission/MissionContentEntity.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Trash/Mission/Entities/MissionContentEntity.swift rename to 14th-team5-iOS/Domain/Sources/Entities/Mission/MissionContentEntity.swift diff --git a/14th-team5-iOS/Domain/Sources/Repositories/MissonRepository.swift b/14th-team5-iOS/Domain/Sources/Repositories/MissonRepository.swift new file mode 100644 index 000000000..20b631139 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Repositories/MissonRepository.swift @@ -0,0 +1,22 @@ +// +// MissonRepository.swift +// Domain +// +// Created by 김도현 on 11/20/24. +// + +import Foundation + +import RxSwift + + +public protocol MissionRepositoryProtocol { + + /// FETCH + func fetchMissonContentItem(missonId: String) -> Observable + func fetchDailyMissonItem() -> Observable + + /// LOCAL DB + func isAlreadyShowMissionAlert() -> Observable + func updateMissonUploadDate() +} diff --git a/14th-team5-iOS/Domain/Sources/Trash/Mission/Interfaces/MissionRepositoryProtocol.swift b/14th-team5-iOS/Domain/Sources/Trash/Mission/Interfaces/MissionRepositoryProtocol.swift deleted file mode 100644 index af158700d..000000000 --- a/14th-team5-iOS/Domain/Sources/Trash/Mission/Interfaces/MissionRepositoryProtocol.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// MissionRepositoryProtocol.swift -// Domain -// -// Created by 마경미 on 21.04.24. -// - -import Foundation - -import RxSwift - -public protocol MissionRepositoryProtocol { - func getMissionContent(missionId: String) -> Single - func isAlreadyShowMissionAlert() -> Observable -} diff --git a/14th-team5-iOS/Domain/Sources/Trash/Mission/UseCases/Mission/CheckMissionAlertShowUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Misson/CheckMissionAlertShowUseCase.swift similarity index 100% rename from 14th-team5-iOS/Domain/Sources/Trash/Mission/UseCases/Mission/CheckMissionAlertShowUseCase.swift rename to 14th-team5-iOS/Domain/Sources/UseCases/Misson/CheckMissionAlertShowUseCase.swift diff --git a/14th-team5-iOS/Domain/Sources/Trash/Mission/UseCases/Mission/FetchMissionContentUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Misson/FetchMissionContentUseCase.swift similarity index 68% rename from 14th-team5-iOS/Domain/Sources/Trash/Mission/UseCases/Mission/FetchMissionContentUseCase.swift rename to 14th-team5-iOS/Domain/Sources/UseCases/Misson/FetchMissionContentUseCase.swift index a11803642..670a33f9e 100644 --- a/14th-team5-iOS/Domain/Sources/Trash/Mission/UseCases/Mission/FetchMissionContentUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/Misson/FetchMissionContentUseCase.swift @@ -12,7 +12,7 @@ import RxCocoa public protocol FetchMissionContentUseCaseProtocol { - func execute(missionId: String) -> Single + func execute(missionId: String) -> Observable } public final class FetchMissionContentUseCase: FetchMissionContentUseCaseProtocol { @@ -22,8 +22,8 @@ public final class FetchMissionContentUseCase: FetchMissionContentUseCaseProtoco self.missionRepository = missionRepository } - public func execute(missionId: String) -> Single { - return missionRepository.getMissionContent(missionId: missionId) + public func execute(missionId: String) -> Observable { + return missionRepository.fetchMissonContentItem(missonId: missionId) } } From 30f55817c77feb2c58e5049c82e06aa2dfcb41fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=84?= Date: Tue, 26 Nov 2024 23:01:20 +0900 Subject: [PATCH 248/263] =?UTF-8?q?=EA=B8=B0=EC=A1=B4=20MembersAPIWorker?= =?UTF-8?q?=20Network=20Module=20=EC=BD=94=EB=93=9C=EB=A5=BC=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A7=81=ED=95=B4=EC=9A=94=20(#711)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: AccountResignViewController 기존 UI refactoring - PickDIContainer, ResignDIContainer 파일 제거 - MembersAPI, APIWorker 코드 리펙토링 - Members 관련 UseCase 생성 - 사용하지 않는 PickUseCase 제거 - Members 관련 DTO, Entity 코드 리팩토링 및 코드 컨벤션에 맞게 파일 생성 * refactor: Members 관련 DTO, Entity 코드 추가 - Camera 업로드 관련 비즈니스 로직 코드 수정 - Members, Camera 관련 Navigator 코드 추가 * fix: UpdateMembersProfileUseCase 내부 비즈니스 로직 코드 제거 - CreateMembersPreSignedURLUseCase 비즈니스 로직 코드 추가 - MembersRepository, UseCase 회원 탈퇴 API Body 값 제거, createMemberImagePreSignedURL memberId Parameters 제거 - ProfileViewReactor 회원 프로필 이미지 변경시 API 예외 처리 로직 추가 - CameraViewReactor 비즈니스 로직 코드 리팩토링 및 예외 처리 로직 추가 - ProfileDIContainer UseCase 의존성 주입 코드 추가 * fix: AccountProfileViewReactor 카메라를 이용한 프로필 변경 비즈니스 로직 수정 * fix: CameraViewController NotificationCenter userInfo presigedURL 추가 * fix: ProfileViewController NotificationCenter distinctUntilChanged 연산자 추가 * fix: Trash 폴더 내부 Camera 관련 UseCase 제거 - FetchDailyMissionContentUseCase 추가 - MissionContentEntity, MissionTodayContentResponseDTO 추가 - CameraViewReactor Mission관련 비즈니스 로직 수정 - MissonDIContainer FetchMissionContentUseCase 의존성 추가 --- .../App/Sources/Application/AppDelegate.swift | 4 +- .../DIContainer/CameraDIContainer.swift | 30 ---- .../DIContainer/MissionDIContainer.swift | 8 + .../DIContainer/NavigatorDIContainer.swift | 12 ++ .../DIContainer/PickDIContainer.swift | 69 -------- .../DIContainer/PostDIContainer.swift | 16 ++ .../DIContainer/ProfileDIContainer.swift | 36 ++++ .../DIContainer/ResignDIContainer.swift | 32 ---- .../Navigator/AccountResignNavigator.swift | 5 + .../Navigator/CameraNavigator.swift | 38 +++++ .../Navigator/ProfileNavigator.swift | 60 +++++++ .../AccountResignViewControllerWrapper.swift | 2 +- .../Reactor/AccountSignUpReactor.swift | 76 ++++----- .../AccountProfileViewController.swift | 9 +- .../Camera/CameraViewController.swift | 20 +-- .../Reactor/CameraDisplayViewReactor.swift | 75 +++------ .../Camera/Reactor/CameraViewReactor.swift | 159 +++++++----------- .../Reactor/Cell/MainFamilyCellReactor.swift | 2 +- .../Home/Reactor/MainViewReactor.swift | 4 +- .../Profile/ProfileViewController.swift | 31 ++-- .../Profile/Reactor/ProfileViewReactor.swift | 117 +++++-------- ...wift => AccountResignViewController.swift} | 72 ++++---- .../Cell/ResignReaonsTableViewCell.swift | 70 ++++++++ .../Resign/Config/ReasonType.swift | 35 ++++ .../AccountResignSectionModel.swift | 13 ++ .../Reactor/AccountResignViewReactor.swift | 63 +++---- .../ResignReasonTableViewCellReactor.swift | 23 +++ .../Resign/View/BibbiCheckBoxView.swift | 88 ---------- .../BBNetwork/Network/BBNetworkError.swift | 30 ++++ .../Sources/APIs/Camera/CameraAPIWorker.swift | 54 +----- .../Data/Sources/APIs/Camera/CameraAPIs.swift | 27 --- .../CameraDisplayImageResponseDTO.swift | 28 --- .../CameraDisplayPostResponseDTO.swift | 42 ----- .../CameraTodayMissionResponseDTO.swift | 37 ---- .../DataMapping/CreateFeedRequestDTO.swift | 15 -- .../Camera/Repository/CameraRepository.swift | 27 --- .../CreateMemberPickResponseDTO.swift | 22 +++ .../CreateMemberPresignedURLRequestDTO.swift | 12 ++ .../CreateMemberPresignedURLResponseDTO.swift | 25 +++ .../DataMapping/DeleteMemberResponseDTO.swift | 21 +++ .../MembersProfileResponseDTO.swift | 2 +- .../UpdateMemberImageRequestDTO.swift} | 4 +- .../UpdateMemberNameRequestDTO.swift | 13 ++ .../UpdateMemberNameResponseDTO.swift | 33 ++++ .../Members/MemberAPI/MembersAPIWorker.swift | 132 ++++++++------- .../APIs/Members/MemberAPI/MembersAPIs.swift | 60 ++++--- .../Repositories/MembersRepository.swift | 46 ++++- .../MissionContentResponseDTO.swift | 0 .../MissonTodayContentResponseDTO.swift | 34 ++++ .../Mission/MissionAPI/MissionAPIWorker.swift | 2 +- .../Mission/MissionAPI/MissionAPIs.swift | 0 .../Repository/MissionRepository.swift | 2 +- .../PickMemberListResponseDTO.swift | 54 ------ .../PickAPI/DataMapping/PickResponseDTO.swift | 22 --- .../APIs/Pick/PickAPI/PickAPIWorker.swift | 107 ------------ .../Sources/APIs/Pick/PickAPI/PickAPIs.swift | 26 --- .../APIs/Pick/Repository/PickRepository.swift | 37 ---- .../CreatePostPresignedURLReqeustDTO.swift | 13 ++ .../CreatePostPresignedURLResponseDTO.swift | 25 +++ .../DataMapping/CreatePostRequestDTO.swift | 0 .../DataMapping/CreatePostResponseDTO.swift | 40 +++++ .../CreatePresignedURLRequestDTO.swift | 0 .../DataMapping/PostDetailResponseDTO.swift | 0 .../DataMapping/PostListQueryDTO.swift | 0 .../DataMapping/PostListResponseDTO.swift | 0 .../Post}/PostAPI/PostAPIs.swift | 3 +- .../Post}/PostAPI/PostListAPIWorker.swift | 22 ++- .../Post}/Repository/PostRepository.swift | 16 +- .../AccountResignResponseDTO.swift | 19 --- .../Repository/AccountResignRepository.swift | 31 ---- .../Resign/ResignAPI/ResignAPIWorker.swift | 43 ----- .../APIs/Resign/ResignAPI/ResignAPIs.swift | 25 --- .../Account/AccountAPI/AccountAPIWorker.swift | 10 -- .../AccountRepository/AccountRepository.swift | 19 --- .../Member/Repository/MemberRepository.swift | 19 --- .../UserDefaults/FamilyUserDefautls.swift | 136 --------------- .../Camera/CameraMissionFeedQuery.swift | 20 --- .../Camera/CameraTodayMssionEntity.swift | 21 --- .../Entities/Camera/CreateFeedQuery.swift | 17 -- .../Entities/Camera/CreateFeedRequest.swift | 25 --- .../Members/CreateMemberPickEntity.swift | 17 ++ .../CreateMemberPresignedEntity.swift} | 6 +- .../CreateMemberPresignedReqeust.swift | 17 ++ .../Entities/Members/DeleteMemberEntity.swift | 17 ++ .../UpdateMemberImageRequest.swift} | 4 +- .../Members/UpdateMemberNameEntity.swift | 34 ++++ .../Members/UpdateMemberNameRequest.swift | 17 ++ .../Mission/MissonTodayContentEntity.swift | 24 +++ .../Sources/Entities/Pick/PickEntity.swift | 16 -- .../Entities/Pick/PickMemberListEntity.swift | 41 ----- .../CreatePostEntity.swift} | 7 +- .../CreatePostPresignedURLEntity.swift | 17 ++ .../Entities/Resign/AccountResignEntity.swift | 16 -- .../AccountResignRepository.swift | 14 -- .../Repositories/CameraRepository.swift | 17 +- .../Repositories/MembersRepository.swift | 17 +- .../Repositories/MissonRepository.swift | 2 +- .../Sources/Repositories/PickRepository.swift | 16 -- .../Repositories/PostListRepository.swift | 7 +- .../DataMapping/AccountNickNameEditDTO.swift | 33 ---- .../Entity/AccountNickNameEditResponse.swift | 19 --- .../AccountNickNameEditParameter.swift | 19 --- .../FetchCameraTodayMissionUseCase.swift | 30 ---- .../EditCameraProfileImageUseCase.swift | 33 ---- .../Repositories/MemberRepository.swift | 5 - .../Trash/Member/UseCases/MemberUseCase.swift | 32 +--- .../Trash/Pick/UseCases/PickUseCase.swift | 38 ----- .../CreateAccountPresingedURLUseCase.swift | 32 ++++ .../Camera/CreateCameraImageUseCase.swift | 29 ---- .../UseCases/Camera/CreateCameraUseCase.swift | 32 ++-- .../FetchCameraUploadImageUseCase.swift | 2 +- .../Members/CreateMembersPickUseCase.swift | 30 ++++ .../CreateMembersPresignedURLUseCase.swift | 48 ++++++ .../Members/DeleteMembersProfileUseCase.swift | 6 +- .../Members/DeleteMembersUseCase.swift | 29 ++++ .../Members/FetchMembersProfileUseCase.swift | 6 +- .../Members/UpdateMembersNameUseCase.swift | 29 ++++ .../Members/UpdateMembersProfileUseCase.swift | 9 +- .../FetchDailyMissonContentUseCase.swift | 28 +++ .../UseCases/Pick/PickMemberUseCase.swift | 31 ---- .../Pick/WhoDidIPickMemberUseCase.swift | 32 ---- .../UseCases/Pick/WhoPickedMeUseCase.swift | 33 ---- .../Post/CreatePostPresingedURLUseCase.swift | 43 +++++ .../UseCases/Post/CreatePostUseCase.swift | 27 +++ .../Resign/DeleteAccountResignUseCase.swift | 27 --- 125 files changed, 1487 insertions(+), 2088 deletions(-) delete mode 100644 14th-team5-iOS/App/Sources/Application/DIContainer/PickDIContainer.swift delete mode 100644 14th-team5-iOS/App/Sources/Application/DIContainer/ResignDIContainer.swift create mode 100644 14th-team5-iOS/App/Sources/Application/Navigator/CameraNavigator.swift create mode 100644 14th-team5-iOS/App/Sources/Application/Navigator/ProfileNavigator.swift rename 14th-team5-iOS/App/Sources/Presentation/Resign/{AccountResignViewCotroller.swift => AccountResignViewController.swift} (76%) create mode 100644 14th-team5-iOS/App/Sources/Presentation/Resign/Cell/ResignReaonsTableViewCell.swift create mode 100644 14th-team5-iOS/App/Sources/Presentation/Resign/Config/ReasonType.swift create mode 100644 14th-team5-iOS/App/Sources/Presentation/Resign/DataSources/AccountResignSectionModel.swift create mode 100644 14th-team5-iOS/App/Sources/Presentation/Resign/Reactor/ResignReasonTableViewCellReactor.swift delete mode 100644 14th-team5-iOS/App/Sources/Presentation/Resign/View/BibbiCheckBoxView.swift delete mode 100644 14th-team5-iOS/Data/Sources/APIs/Camera/DataMapping/CameraDisplayImageResponseDTO.swift delete mode 100644 14th-team5-iOS/Data/Sources/APIs/Camera/DataMapping/CameraDisplayPostResponseDTO.swift delete mode 100644 14th-team5-iOS/Data/Sources/APIs/Camera/DataMapping/CameraTodayMissionResponseDTO.swift delete mode 100644 14th-team5-iOS/Data/Sources/APIs/Camera/DataMapping/CreateFeedRequestDTO.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/DataMapping/CreateMemberPickResponseDTO.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/DataMapping/CreateMemberPresignedURLRequestDTO.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/DataMapping/CreateMemberPresignedURLResponseDTO.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/DataMapping/DeleteMemberResponseDTO.swift rename 14th-team5-iOS/Data/Sources/APIs/Members/{ => MemberAPI}/DataMapping/MembersProfileResponseDTO.swift (94%) rename 14th-team5-iOS/Data/Sources/APIs/{Camera/DataMapping/UpdateProfileImageRequestDTO.swift => Members/MemberAPI/DataMapping/UpdateMemberImageRequestDTO.swift} (55%) create mode 100644 14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/DataMapping/UpdateMemberNameRequestDTO.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/DataMapping/UpdateMemberNameResponseDTO.swift rename 14th-team5-iOS/Data/Sources/{Trash/Mission => APIs/Mission/MissionAPI}/DataMapping/MissionContentResponseDTO.swift (100%) create mode 100644 14th-team5-iOS/Data/Sources/APIs/Mission/MissionAPI/DataMapping/MissonTodayContentResponseDTO.swift rename 14th-team5-iOS/Data/Sources/{Trash => APIs}/Mission/MissionAPI/MissionAPIWorker.swift (93%) rename 14th-team5-iOS/Data/Sources/{Trash => APIs}/Mission/MissionAPI/MissionAPIs.swift (100%) rename 14th-team5-iOS/Data/Sources/{Trash => APIs}/Mission/Repository/MissionRepository.swift (95%) delete mode 100644 14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/DataMapping/PickMemberListResponseDTO.swift delete mode 100644 14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/DataMapping/PickResponseDTO.swift delete mode 100644 14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/PickAPIWorker.swift delete mode 100644 14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/PickAPIs.swift delete mode 100644 14th-team5-iOS/Data/Sources/APIs/Pick/Repository/PickRepository.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/Post/PostAPI/DataMapping/CreatePostPresignedURLReqeustDTO.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/Post/PostAPI/DataMapping/CreatePostPresignedURLResponseDTO.swift rename 14th-team5-iOS/Data/Sources/{Trash/Post/PostList => APIs/Post/PostAPI}/DataMapping/CreatePostRequestDTO.swift (100%) create mode 100644 14th-team5-iOS/Data/Sources/APIs/Post/PostAPI/DataMapping/CreatePostResponseDTO.swift rename 14th-team5-iOS/Data/Sources/{Trash/Post/PostList => APIs/Post/PostAPI}/DataMapping/CreatePresignedURLRequestDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/{Trash/Post/PostList => APIs/Post/PostAPI}/DataMapping/PostDetailResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/{Trash/Post/PostList => APIs/Post/PostAPI}/DataMapping/PostListQueryDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/{Trash/Post/PostList => APIs/Post/PostAPI}/DataMapping/PostListResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/{Trash/Post/PostList => APIs/Post}/PostAPI/PostAPIs.swift (94%) rename 14th-team5-iOS/Data/Sources/{Trash/Post/PostList => APIs/Post}/PostAPI/PostListAPIWorker.swift (66%) rename 14th-team5-iOS/Data/Sources/{Trash/Post/PostList => APIs/Post}/Repository/PostRepository.swift (75%) delete mode 100644 14th-team5-iOS/Data/Sources/APIs/Resign/DataMapping/AccountResignResponseDTO.swift delete mode 100644 14th-team5-iOS/Data/Sources/APIs/Resign/Repository/AccountResignRepository.swift delete mode 100644 14th-team5-iOS/Data/Sources/APIs/Resign/ResignAPI/ResignAPIWorker.swift delete mode 100644 14th-team5-iOS/Data/Sources/APIs/Resign/ResignAPI/ResignAPIs.swift delete mode 100644 14th-team5-iOS/Data/Sources/Trash/UserDefaults/FamilyUserDefautls.swift delete mode 100644 14th-team5-iOS/Domain/Sources/Entities/Camera/CameraMissionFeedQuery.swift delete mode 100644 14th-team5-iOS/Domain/Sources/Entities/Camera/CameraTodayMssionEntity.swift delete mode 100644 14th-team5-iOS/Domain/Sources/Entities/Camera/CreateFeedQuery.swift delete mode 100644 14th-team5-iOS/Domain/Sources/Entities/Camera/CreateFeedRequest.swift create mode 100644 14th-team5-iOS/Domain/Sources/Entities/Members/CreateMemberPickEntity.swift rename 14th-team5-iOS/Domain/Sources/Entities/{Camera/CameraPreSignedEntity.swift => Members/CreateMemberPresignedEntity.swift} (56%) create mode 100644 14th-team5-iOS/Domain/Sources/Entities/Members/CreateMemberPresignedReqeust.swift create mode 100644 14th-team5-iOS/Domain/Sources/Entities/Members/DeleteMemberEntity.swift rename 14th-team5-iOS/Domain/Sources/Entities/{Camera/UpdateProfileImageRequest.swift => Members/UpdateMemberImageRequest.swift} (73%) create mode 100644 14th-team5-iOS/Domain/Sources/Entities/Members/UpdateMemberNameEntity.swift create mode 100644 14th-team5-iOS/Domain/Sources/Entities/Members/UpdateMemberNameRequest.swift create mode 100644 14th-team5-iOS/Domain/Sources/Entities/Mission/MissonTodayContentEntity.swift delete mode 100644 14th-team5-iOS/Domain/Sources/Entities/Pick/PickEntity.swift delete mode 100644 14th-team5-iOS/Domain/Sources/Entities/Pick/PickMemberListEntity.swift rename 14th-team5-iOS/Domain/Sources/Entities/{Camera/CameraPostEntity.swift => PostList/CreatePostEntity.swift} (90%) create mode 100644 14th-team5-iOS/Domain/Sources/Entities/PostList/CreatePostPresignedURLEntity.swift delete mode 100644 14th-team5-iOS/Domain/Sources/Entities/Resign/AccountResignEntity.swift delete mode 100644 14th-team5-iOS/Domain/Sources/Repositories/AccountResignRepository.swift delete mode 100644 14th-team5-iOS/Domain/Sources/Repositories/PickRepository.swift delete mode 100644 14th-team5-iOS/Domain/Sources/Trash/Account/DataMapping/AccountNickNameEditDTO.swift delete mode 100644 14th-team5-iOS/Domain/Sources/Trash/Account/Entity/AccountNickNameEditResponse.swift delete mode 100644 14th-team5-iOS/Domain/Sources/Trash/Account/Parameters/AccountNickNameEditParameter.swift delete mode 100644 14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/Mission/FetchCameraTodayMissionUseCase.swift delete mode 100644 14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/Profile/EditCameraProfileImageUseCase.swift delete mode 100644 14th-team5-iOS/Domain/Sources/Trash/Pick/UseCases/PickUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/UseCases/Account/CreateAccountPresingedURLUseCase.swift delete mode 100644 14th-team5-iOS/Domain/Sources/UseCases/Camera/CreateCameraImageUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/UseCases/Members/CreateMembersPickUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/UseCases/Members/CreateMembersPresignedURLUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/UseCases/Members/DeleteMembersUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/UseCases/Members/UpdateMembersNameUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/UseCases/Misson/FetchDailyMissonContentUseCase.swift delete mode 100644 14th-team5-iOS/Domain/Sources/UseCases/Pick/PickMemberUseCase.swift delete mode 100644 14th-team5-iOS/Domain/Sources/UseCases/Pick/WhoDidIPickMemberUseCase.swift delete mode 100644 14th-team5-iOS/Domain/Sources/UseCases/Pick/WhoPickedMeUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/UseCases/Post/CreatePostPresingedURLUseCase.swift create mode 100644 14th-team5-iOS/Domain/Sources/UseCases/Post/CreatePostUseCase.swift delete mode 100644 14th-team5-iOS/Domain/Sources/UseCases/Resign/DeleteAccountResignUseCase.swift diff --git a/14th-team5-iOS/App/Sources/Application/AppDelegate.swift b/14th-team5-iOS/App/Sources/Application/AppDelegate.swift index a5e7b4d4f..1d699532b 100644 --- a/14th-team5-iOS/App/Sources/Application/AppDelegate.swift +++ b/14th-team5-iOS/App/Sources/Application/AppDelegate.swift @@ -66,13 +66,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate { MainViewDIContainer(), ReactionDIContainer(), RealEmojiDIContainer(), - PickDIContainer(), MissionDIContainer(), MemberDIContainer(), MyDIContainer(), SignOutDIContainer(), - PrivacyDIContainer(), - ResignDIContainer() + PrivacyDIContainer() ] containers.forEach { $0.registerDependencies() diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/CameraDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/CameraDIContainer.swift index 8bf02943b..483c49f9f 100644 --- a/14th-team5-iOS/App/Sources/Application/DIContainer/CameraDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/CameraDIContainer.swift @@ -19,26 +19,10 @@ final class CameraDIContainer: BaseContainer { } - private func makeCreateProfileImageUseCase() -> CreateCameraImageUseCaseProtocol { - return CreateCameraImageUseCase(cameraRepository: makeRepository()) - } - - private func makeCreateCameraUseCase() -> CreateCameraUseCaseProtocol { - return CreateCameraUseCase(cameraRepository: makeRepository()) - } - - private func makeEditCameraProfileImageUseCase() -> EditCameraProfileImageUseCaseProtocol { - return EditCameraProfileImageUseCase(cameraRepository: makeRepository()) - } - private func makeFetchCameraUploadImageUseCase() -> FetchCameraUploadImageUseCaseProtocol { return FetchCameraUploadImageUseCase(cameraRepository: makeRepository()) } - private func makeFetchCameraTodayMissionUseCase() -> FetchCameraTodayMissionUseCaseProtocol { - return FetchCameraTodayMissionUseCase(cameraRepository: makeRepository()) - } - private func makeFetchCameraRealEmojiUpdateUseCase() -> FetchCameraRealEmojiUpdateUseCaseProtocol { return FetchCameraRealEmojiUpdateUseCase(cameraRepostiroy: makeRepository()) } @@ -56,26 +40,12 @@ final class CameraDIContainer: BaseContainer { } func registerDependencies() { - container.register(type: CreateCameraImageUseCaseProtocol.self) { _ in - self.makeCreateProfileImageUseCase() - } - container.register(type: CreateCameraUseCaseProtocol.self) { _ in - self.makeCreateCameraUseCase() - } - - container.register(type: EditCameraProfileImageUseCaseProtocol.self) { _ in - self.makeEditCameraProfileImageUseCase() - } container.register(type: FetchCameraUploadImageUseCaseProtocol.self) { _ in self.makeFetchCameraUploadImageUseCase() } - container.register(type: FetchCameraTodayMissionUseCaseProtocol.self) { _ in - self.makeFetchCameraTodayMissionUseCase() - } - container.register(type: FetchCameraRealEmojiUpdateUseCaseProtocol.self) { _ in self.makeFetchCameraRealEmojiUpdateUseCase() } diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/MissionDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/MissionDIContainer.swift index 72e12a7b0..74c13d14b 100644 --- a/14th-team5-iOS/App/Sources/Application/DIContainer/MissionDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/MissionDIContainer.swift @@ -20,6 +20,10 @@ final class MissionDIContainer: BaseContainer { return CheckMissionAlertShowUseCase(missionRepository: repository) } + private func makeFetchMissionContentUseCase() -> FetchDailyMissonContentUseCaseProtocol { + return FetchDailyMissonContentUseCase(missionRepository: repository) + } + func registerDependencies() { container.register(type: FetchMissionContentUseCaseProtocol.self) { _ in self.makeMissionUseCase() @@ -28,6 +32,10 @@ final class MissionDIContainer: BaseContainer { container.register(type: CheckMissionAlertShowUseCaseProtocol.self) { _ in self.makeCheckMissionAlertShowUseCase() } + + container.register(type: FetchDailyMissonContentUseCaseProtocol.self) { _ in + self.makeFetchMissionContentUseCase() + } } } diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/NavigatorDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/NavigatorDIContainer.swift index daef02ea9..08262ef06 100644 --- a/14th-team5-iOS/App/Sources/Application/DIContainer/NavigatorDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/NavigatorDIContainer.swift @@ -59,6 +59,18 @@ final class NavigatorDIContainer: BaseContainer { ) } + container.register(type: ProfileNavigatorProtocol.self) { _ in + ProfileNavigator( + navigationController: makeUINavigationController() + ) + } + + container.register(type: CameraNavigatorProtocol.self) { _ in + CameraNavigator( + navigationController: makeUINavigationController() + ) + } + container.register(type: MonthlyCalendarNavigatorProtocol.self) { _ in MonthlyCalendarNavigator( navigationController: makeUINavigationController() diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/PickDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/PickDIContainer.swift deleted file mode 100644 index eacd77895..000000000 --- a/14th-team5-iOS/App/Sources/Application/DIContainer/PickDIContainer.swift +++ /dev/null @@ -1,69 +0,0 @@ -// -// PickDIContainer.swift -// App -// -// Created by 김건우 on 6/20/24. -// - -import Core -import Data -import Domain - -final class PickDIContainer: BaseContainer { - - // MARK: - Make UseCase - - private func makePickMemberUseCase() -> PickMemberUseCaseProtocol { - PickMemberUseCase( - pickRepository: makePickRepository() - ) - } - - private func makeWhoDidIPickedMemberUseCase() -> WhoDidIPickMemberUseCaseProtocol { - WhoDidIPickMemberUseCase( - pickRepository: makePickRepository() - ) - } - - private func makeWhoPickedMeUseCase() -> WhoPickedMeUseCaseProtocol { - WhoPickedMeUseCase( - pickRepository: makePickRepository() - ) - } - - // Deprecated - private func makePickUseCase() -> PickUseCaseProtocol { - PickUseCase( - pickRepository: makePickRepository() - ) - } - - - // MARK: - Make Repository - - private func makePickRepository() -> PickRepositoryProtocol { - return PickRepository() - } - - // MARK: - Register - - func registerDependencies() { - container.register(type: PickMemberUseCaseProtocol.self) { _ in - makePickMemberUseCase() - } - - container.register(type: WhoDidIPickMemberUseCaseProtocol.self) { _ in - makeWhoDidIPickedMemberUseCase() - } - - container.register(type: WhoPickedMeUseCaseProtocol.self) { _ in - makeWhoPickedMeUseCase() - } - - // Deprecated - container.register(type: PickUseCaseProtocol.self) { _ in - makePickUseCase() - } - } - -} diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/PostDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/PostDIContainer.swift index baa4bb71c..3a57a49bb 100644 --- a/14th-team5-iOS/App/Sources/Application/DIContainer/PostDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/PostDIContainer.swift @@ -21,6 +21,14 @@ final class PostDIContainer: BaseContainer { private func makeFetchMembersPostListUseCase() -> FetchMembersPostListUseCaseProtocol { return FetchMembersPostListUseCase(postListRepository: postListRepository) } + + private func makeCreatePostUseCase() -> CreatePostUseCaseProtocol { + return CreatePostUseCase(postListRepository: postListRepository) + } + + private func makeCreatePresignedURLUseCase() -> CreatePresignedURLUseCaseProtocol { + return CreatePresignedURLUseCase(postListReposity: postListRepository) + } } extension PostDIContainer { @@ -32,6 +40,14 @@ extension PostDIContainer { container.register(type: FetchMembersPostListUseCaseProtocol.self) { _ in self.makeFetchMembersPostListUseCase() } + + container.register(type: CreatePostUseCaseProtocol.self) { _ in + self.makeCreatePostUseCase() + } + + container.register(type: CreatePresignedURLUseCaseProtocol.self) { _ in + self.makeCreatePresignedURLUseCase() + } } } diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/ProfileDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/ProfileDIContainer.swift index b836920da..90728ca49 100644 --- a/14th-team5-iOS/App/Sources/Application/DIContainer/ProfileDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/ProfileDIContainer.swift @@ -30,6 +30,22 @@ final class ProfileDIContainer: BaseContainer { return UpdateMembersProfileUseCase(membersRepository: makeRepository()) } + private func makeCreateMemberPickUseCase() -> CreateMembersPickUseCaseProtocol { + return CreateMembersPickUseCase(membersRepository: makeRepository()) + } + + private func makeDeleteMembersUseCase() -> DeleteMembersUseCaseProtocol { + return DeleteMembersUseCase(membersRepository: makeRepository()) + } + + private func makeUpdateMembersNameUseCase() -> UpdateMembersNameUseCaseProtocol { + return UpdateMembersNameUseCase(membersRepository: makeRepository()) + } + + private func makeCreateMembersPresignedURLUseCase() -> CreateMembersPresignedURLUseCaseProtocol { + return CreateMembersPresignedURLUseCase(membersRepository: makeRepository()) + } + //TODO: FetchMembersPostListUseCaseProtocol는 PostDIContainer에 추가하기 @@ -45,6 +61,26 @@ final class ProfileDIContainer: BaseContainer { container.register(type: UpdateMembersProfileUseCaseProtocol.self) { _ in self.makeUpdateMembersProfileUseCase() } + + container.register(type: CreateMembersPresignedURLUseCaseProtocol.self) { _ in + self.makeCreateMembersPresignedURLUseCase() + } + + container.register(type: CreateMembersPickUseCaseProtocol.self) { _ in + self.makeCreateMemberPickUseCase() + } + + container.register(type: DeleteMembersUseCaseProtocol.self) { _ in + self.makeDeleteMembersUseCase() + } + + container.register(type: UpdateMembersNameUseCaseProtocol.self) { _ in + self.makeUpdateMembersNameUseCase() + } + + container.register(type: UpdateMembersProfileUseCaseProtocol.self) { _ in + self.makeUpdateMembersProfileUseCase() + } } } diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/ResignDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/ResignDIContainer.swift deleted file mode 100644 index f79cf9807..000000000 --- a/14th-team5-iOS/App/Sources/Application/DIContainer/ResignDIContainer.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// ResignDIContainer.swift -// App -// -// Created by Kim dohyun on 9/12/24. -// - -import Core -import Data -import Domain - - -final class ResignDIContainer: BaseContainer { - - private func makeResignRepository() -> AccountResignRepositoryProtocol { - return AccountResignRepository() - } - - private func makeDeleteAccountResignUseCaseProtocol() -> DeleteAccountResignUseCaseProtocol { - return DeleteAccountResignUseCase( - accountResignRepository: makeResignRepository() - ) - } - - func registerDependencies() { - container.register(type: DeleteAccountResignUseCaseProtocol.self) { _ in - makeDeleteAccountResignUseCaseProtocol() - } - - } - -} diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/AccountResignNavigator.swift b/14th-team5-iOS/App/Sources/Application/Navigator/AccountResignNavigator.swift index ae3bc62d6..e778484bb 100644 --- a/14th-team5-iOS/App/Sources/Application/Navigator/AccountResignNavigator.swift +++ b/14th-team5-iOS/App/Sources/Application/Navigator/AccountResignNavigator.swift @@ -10,6 +10,7 @@ import UIKit protocol AccountResignNavigatorProtocol: BaseNavigator { func toSignIn() + func showErrorToast() } @@ -26,4 +27,8 @@ final class AccountResignNavigator: AccountResignNavigatorProtocol { let vc = SignInViewControllerWrapper().viewController navigationController.setViewControllers([vc], animated: false) } + + func showErrorToast() { + BBToast.style(.error).show() + } } diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/CameraNavigator.swift b/14th-team5-iOS/App/Sources/Application/Navigator/CameraNavigator.swift new file mode 100644 index 000000000..f79bb785c --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/Navigator/CameraNavigator.swift @@ -0,0 +1,38 @@ +// +// CameraNavigator.swift +// App +// +// Created by 김도현 on 11/22/24. +// + +import UIKit + +import DesignSystem +import Core + + + +protocol CameraNavigatorProtocol: BaseNavigator { + func showErrorToast(_ description: String) +} + + +final class CameraNavigator: CameraNavigatorProtocol { + var navigationController: UINavigationController + + init(navigationController: UINavigationController) { + self.navigationController = navigationController + } + func showErrorToast(_ descrption: String) { + let config = BBToastConfiguration(direction: .top(yOffset: 75)) + let viewConfig = BBToastViewConfiguration(minWidth: 100) + BBToast.default( + image: DesignSystemAsset.warning.image, + title: descrption, + viewConfig: viewConfig, + config: config + ).show() + } +} + + diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/ProfileNavigator.swift b/14th-team5-iOS/App/Sources/Application/Navigator/ProfileNavigator.swift new file mode 100644 index 000000000..cdb852078 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/Navigator/ProfileNavigator.swift @@ -0,0 +1,60 @@ +// +// ProfileNavigator.swift +// App +// +// Created by 김도현 on 11/22/24. +// + +import UIKit + +import Core +import DesignSystem + + +protocol ProfileNavigatorProtocol: BaseNavigator { + func showErrorToast(_ description: String) + func toProfileDetail(_ imageURL: URL, nickName: String) + func toPrivacy(_ memberId: String) + func toAccountNickname(_ memberId: String) + func toCamera(_ memberId: String) +} + +final class ProfileNavigator: ProfileNavigatorProtocol { + var navigationController: UINavigationController + + init(navigationController: UINavigationController) { + self.navigationController = navigationController + } + + + func toProfileDetail(_ imageURL: URL, nickName: String) { + let profileDetailViewController = ProfileDetailViewControllerWrapper(profileURL: imageURL, userNickname: nickName).makeViewController() + navigationController.pushViewController(profileDetailViewController, animated: true) + } + + func toPrivacy(_ memberId: String) { + let privacyViewController = PrivacyViewControllerWrapper(memberId: memberId).viewController + navigationController.pushViewController(privacyViewController, animated: true) + } + + func toAccountNickname(_ memberId: String) { + let accountNickNameViewController:AccountNicknameViewController = AccountSignUpDIContainer(memberId: memberId, accountType: .profile).makeNickNameViewController() + navigationController.pushViewController(accountNickNameViewController, animated: true) + } + + func toCamera(_ memberId: String) { + let cameraViewController = CameraViewControllerWrapper(cameraType: .profile, memberId: memberId).viewController + navigationController.pushViewController(cameraViewController, animated: true) + } + + func showErrorToast(_ description: String) { + let config = BBToastConfiguration(direction: .top(yOffset: 75)) + let viewConfig = BBToastViewConfiguration(minWidth: 100) + BBToast.default( + image: DesignSystemAsset.warning.image, + title: description, + viewConfig: viewConfig, + config: config + ).show() + } +} diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Resign/AccountResignViewControllerWrapper.swift b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Resign/AccountResignViewControllerWrapper.swift index 0985e7537..dd5f5db01 100644 --- a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Resign/AccountResignViewControllerWrapper.swift +++ b/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Resign/AccountResignViewControllerWrapper.swift @@ -9,7 +9,7 @@ import Core import Foundation import MacrosInterface -@Wrapper +@Wrapper final class AccountResignViewControllerWrapper { func makeReactor() -> AccountResignViewReactor { diff --git a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/Reactor/AccountSignUpReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/Reactor/AccountSignUpReactor.swift index dc402b8db..f1f2278d5 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/Reactor/AccountSignUpReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/Reactor/AccountSignUpReactor.swift @@ -18,6 +18,8 @@ public final class AccountSignUpReactor: Reactor { private var accountRepository: AccountImpl private let memberId: String private let profileType: AccountLoaction + @Injected var createPresignedURLUseCase: CreateMembersPresignedURLUseCaseProtocol + @Injected var updateMembersNameUseCase: UpdateMembersNameUseCaseProtocol public enum Action { case setNickname(String) @@ -30,8 +32,8 @@ public final class AccountSignUpReactor: Reactor { case didTapDateNextButton case didTapCompletehButton - case profilePresignedURL(String, Data) - case didTapPHAssetsImage(Data) + case profilePresignedURL(String ,Data?) + case didTapPHAssetsImage(Data?) } public enum Mutation { @@ -42,10 +44,10 @@ public final class AccountSignUpReactor: Reactor { case setMonthValue(Int?) case setDayValue(Int?) case didTapDateNextButton - case setEditNickName(AccountNickNameEditResponse?) + case setEditNickName(UpdateMemberNameEntity?) - case setprofilePresignedURL(String) - case setprofileImage(Data) + case setProfilePresignedURL(String) + case setProfileImage(Data?) case didTapCompletehButton(AccessTokenResponse?) case setPHAssetsImage(Data) } @@ -57,7 +59,7 @@ public final class AccountSignUpReactor: Reactor { @Pulse var nicknameButtonTappedFinish: Bool = false var memberId: String - var profileNickNameEditEntity: AccountNickNameEditResponse? + var profileNickNameEditEntity: UpdateMemberNameEntity? var year: Int? var isValidYear: Bool = false var month: Int = 0 @@ -109,59 +111,45 @@ extension AccountSignUpReactor { case let .profilePresignedURL(presignedURL, originImage): let originProfilePath = configureAccountOriginalS3URL(url: presignedURL) return .concat( - .just(.setprofilePresignedURL(originProfilePath)), - .just(.setprofileImage(originImage)) + .just(.setProfileImage(originImage)), + .just(.setProfilePresignedURL(originProfilePath)) ) case let .didTapNickNameButton(nickName): - let parameters: AccountNickNameEditParameter = AccountNickNameEditParameter(name: nickName) - return accountRepository.executeNicknameUpdate(memberId: currentState.memberId, parameter: parameters) - .asObservable() - .flatMap { entity -> Observable in - .just(.setEditNickName(entity)) + let body = UpdateMemberNameRequest(name: nickName) + return updateMembersNameUseCase.execute(memberId: currentState.memberId, body: body) + .flatMap { entity -> Observable in + return .just(.setEditNickName(entity)) } case .didTapCompletehButton: + let originProfilePath = configureAccountOriginalS3URL(url: currentState.profilePresignedURL) let date = getDateToString(year: currentState.year!, month: currentState.month, day: currentState.day) - if self.currentState.profilePresignedURL.isEmpty { return accountRepository.signUp(name: currentState.nickname, date: date, photoURL: nil) .withUnretained(self).flatMap { owner, tokenEntity -> Observable in return Observable.just(Mutation.didTapCompletehButton(tokenEntity)) } } else { - return accountRepository.signUp(name: currentState.nickname, date: date, photoURL: currentState.profilePresignedURL) + let date = getDateToString(year: currentState.year!, month: currentState.month, day: currentState.day) + return accountRepository.signUp(name: currentState.nickname, date: date, photoURL: originProfilePath) .withUnretained(self).flatMap { owner, tokenEntity -> Observable in - return Observable.just(Mutation.didTapCompletehButton(tokenEntity)) - } + return Observable.just(Mutation.didTapCompletehButton(tokenEntity)) + } } case let .didTapPHAssetsImage(profileImage): let originalImage: String = "\(profileImage.hashValue).jpg" - let profileImageEditParameter: CameraDisplayImageParameters = CameraDisplayImageParameters(imageName: originalImage) - - return .concat( - accountRepository.executePresignedImageURLCreate(parameter: profileImageEditParameter) - .withUnretained(self) - .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) - .asObservable() - .flatMap { owner, entity -> Observable in - guard let accountPresignedURL = entity?.imageURL else { return .empty() } - return owner.accountRepository.executeProfileImageUpload(to: accountPresignedURL, data: profileImage) - .asObservable() - .flatMap { isSuccess -> Observable in - let originalPath = owner.configureAccountOriginalS3URL(url: accountPresignedURL) - - if isSuccess { - return .concat( - .just(.setprofilePresignedURL(originalPath)), - .just(.setprofileImage(profileImage)) - ) - } else { - return .empty() - } - - } + let body = CreateMemberPresignedReqeust(imageName: originalImage) + return createPresignedURLUseCase.execute(body: body, imageData: profileImage ?? .empty) + .flatMap { presignedURL -> Observable in + guard let presignedURL = presignedURL?.imageURL else { + return .error(BBUploadError.invalidServerResponse) } - ) + + return .concat( + .just(.setProfilePresignedURL(presignedURL)), + .just(.setProfileImage(profileImage)) + ) + } } } @@ -199,9 +187,9 @@ extension AccountSignUpReactor { } case .didTapDateNextButton: newState.dateButtonTappedFinish = true - case .setprofilePresignedURL(let url): + case .setProfilePresignedURL(let url): newState.profilePresignedURL = url - case let .setprofileImage(profileImage): + case let .setProfileImage(profileImage): newState.profileImage = profileImage case .didTapCompletehButton(let token): if let token = token { diff --git a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountProfileViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountProfileViewController.swift index 3203ec6d1..34041e43d 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountProfileViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountProfileViewController.swift @@ -68,10 +68,9 @@ final class AccountProfileViewController: BaseViewController (String, Data)? in guard let userInfo = notification.userInfo, let presignedURL = userInfo["presignedURL"] as? String, - let originImage = userInfo["originImage"] as? Data else { return nil - } - - return (presignedURL, originImage) + let originalImage = userInfo["originImage"] as? Data else { return nil + } + return (presignedURL, originalImage) } .map { Reactor.Action.profilePresignedURL($0.0, $0.1)} .bind(to: reactor.action) @@ -207,7 +206,7 @@ extension AccountProfileViewController { private func createAlertController(owner: AccountProfileViewController) { let alertController: UIAlertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) let presentCameraAction: UIAlertAction = UIAlertAction(title: "카메라", style: .default) { _ in - let cameraViewController = CameraViewControllerWrapper(cameraType: .profile).viewController + let cameraViewController = CameraViewControllerWrapper(cameraType: .account).viewController owner.navigationController?.pushViewController(cameraViewController, animated: true) } let presentAlbumAction: UIAlertAction = UIAlertAction(title: "앨범", style: .default) { _ in diff --git a/14th-team5-iOS/App/Sources/Presentation/Camera/CameraViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Camera/CameraViewController.swift index 4ad37fba6..db7b6e9e9 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Camera/CameraViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Camera/CameraViewController.swift @@ -232,10 +232,10 @@ public final class CameraViewController: BaseViewController { Observable .zip( - reactor.state.compactMap { $0.feedImageData }.distinctUntilChanged(), + reactor.state.compactMap { $0.imageData }.distinctUntilChanged(), reactor.state.compactMap { $0.cameraType } ) - .filter { $0.1.asPostType == .survival } + .filter { $0.1 == .survival } .withUnretained(self) .bind { let cameraDisplayViewController = CameraDisplayViewControllerWrapper(displayData: $0.1.0).viewController @@ -245,7 +245,7 @@ public final class CameraViewController: BaseViewController { Observable .zip( - reactor.state.compactMap { $0.feedImageData }.distinctUntilChanged(), + reactor.state.compactMap { $0.imageData }.distinctUntilChanged(), reactor.state.compactMap { $0.missionEntity?.missionContent }, reactor.state.map { $0.cameraType.asPostType } ) @@ -267,7 +267,7 @@ public final class CameraViewController: BaseViewController { .bind(to: missionView.missionTitleView.rx.text) .disposed(by: disposeBag) - //TODO: Navigation Bar 제약 조건 이슈 + reactor.pulse(\.$cameraType) .map { $0.setTitle() } .observe(on: MainScheduler.instance) @@ -341,14 +341,14 @@ public final class CameraViewController: BaseViewController { reactor.state - .map { ($0.accountImage, $0.profileImageURLEntity, $0.memberId)} - .filter { $0.0 != nil } - .withUnretained(self) - .subscribe(onNext: { (owner, originEntity) in - let userInfo: [AnyHashable: Any] = ["presignedURL": originEntity.1?.imageURL, "originImage": originEntity.0] + .map { ($0.imageData, $0.memberPresignedEntity) } + .filter { $0.1 != nil} + .subscribe(with: self) { owner, arguments in + let userInfo: [AnyHashable: Any] = ["presignedURL": arguments.1?.imageURL, "originImage": arguments.0] NotificationCenter.default.post(name: .AccountViewPresignURLDismissNotification, object: nil, userInfo: userInfo) owner.dismissCameraViewController() - }).disposed(by: disposeBag) + } + .disposed(by: disposeBag) realEmojiCollectionView diff --git a/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraDisplayViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraDisplayViewReactor.swift index 378266d6c..4a701a582 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraDisplayViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraDisplayViewReactor.swift @@ -17,9 +17,8 @@ public final class CameraDisplayViewReactor: Reactor { public var initialState: State @Injected private var provider: ServiceProviderProtocol - @Injected private var createPresignedCameraUseCase: CreateCameraUseCaseProtocol - @Injected private var uploadImageUseCase: FetchCameraUploadImageUseCaseProtocol - @Injected private var fetchCameraImageUseCase: CreateCameraImageUseCaseProtocol + @Injected private var createPostUseCase: CreatePostUseCaseProtocol + @Injected private var createPresignedURLUseCase: CreatePresignedURLUseCaseProtocol @Navigator private var cameraDisplayNavigator: CameraDisplayNavigatorProtocol public enum Action { @@ -40,9 +39,8 @@ public final class CameraDisplayViewReactor: Reactor { case saveDeviceimage(Data) case setDescription(String) case setTrimedText(String) - case setDisplayEntity(CameraPreSignedEntity?) + case setDisplayEntity(CreatePostPresignedURLEntity?) case setDisplayOriginalEntity(Bool) - case setPostEntity(CameraPostEntity?) } public struct State { @@ -53,9 +51,8 @@ public final class CameraDisplayViewReactor: Reactor { @Pulse var displayData: Data @Pulse var missionTitle: String @Pulse var displaySection: [DisplayEditSectionModel] - @Pulse var displayEntity: CameraPreSignedEntity? + @Pulse var displayEntity: CreatePostPresignedURLEntity? @Pulse var displayOringalEntity: Bool - @Pulse var displayPostEntity: CameraPostEntity? @Pulse var displayText: String } @@ -76,7 +73,6 @@ public final class CameraDisplayViewReactor: Reactor { displaySection: [.displayKeyword([])], displayEntity: nil, displayOringalEntity: false, - displayPostEntity: nil, displayText: "" ) } @@ -85,45 +81,24 @@ public final class CameraDisplayViewReactor: Reactor { switch action { case .viewDidLoad: let fileName = "\(currentState.displayData.hashValue).jpg" - let body = CreatePresignedURLRequest(imageName: fileName) - return .concat( - .just(.setLoading(false)), - .just(.setError(false)), - .just(.setRenderImage(currentState.displayData)), - createPresignedCameraUseCase.execute(body: body) - .withUnretained(self) - .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) - .flatMap { owner, entity -> Observable in - guard let remoteURL = entity?.imageURL else { - return .concat( - .just(.setLoading(true)), - .just(.setError(true)) - ) - } - return owner.uploadImageUseCase.execute(remoteURL, image: owner.currentState.displayData) - .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) - .flatMap { isSuccess -> Observable in - if isSuccess { - return .concat( - .just(.setDisplayEntity(entity)), - .just(.setDisplayOriginalEntity(isSuccess)), - .just(.setLoading(true)), - .just(.setError(false)) - ) - } else { - return .concat( - .just(.setLoading(true)), - .just(.setError(true)) - ) - } - } - } - ) + let body = CreatePostPresignedURLRequest(imageName: fileName) + return createPresignedURLUseCase.execute(body: body, imageData: currentState.displayData) + .flatMap { presingedURL -> Observable in + return .concat( + .just(.setLoading(false)), + .just(.setDisplayEntity(presingedURL)), + .just(.setError(false)), + .just(.setLoading(true)) + ) + }.catch { _ in + return .just(.setError(true)) + } + case let .fetchDisplayImage(description): return .concat( Observable.of(Array(description)) .map { String($0) } - .flatMap { items -> Observable in + .flatMap { items -> Observable in var sectionItem: [DisplayEditItemModel] = [] items.forEach { @@ -158,21 +133,17 @@ public final class CameraDisplayViewReactor: Reactor { guard let presingedURL = currentState.displayEntity?.imageURL else { return .just(.setError(true)) } let remoteURL = configureOriginalS3URL(url: presingedURL) - let query = CreateFeedQuery(type: currentState.cameraType.rawValue) - let body = CreateFeedRequest(imageUrl: remoteURL, content: currentState.displayDescrption, uploadTime: DateFormatter.yyyyMMddTHHmmssXXX.string(from: .now)) + let query = CreatePostQuery(type: currentState.cameraType.rawValue) + let body = CreatePostRequest(imageUrl: remoteURL, content: currentState.displayDescrption, uploadTime: DateFormatter.yyyyMMddTHHmmssXXX.string(from: .now)) - return fetchCameraImageUseCase.execute(query: query, body: body) - .catchAndReturn(nil) + return createPostUseCase.execute(query: query, body: body) .withUnretained(self) - .flatMap { owner, entity -> Observable in + .flatMap { owner, entity -> Observable in if entity == nil { return .just(.setError(true)) } else { owner.cameraDisplayNavigator.toHome() return .concat( - .just(.setLoading(false)), - .just(.setPostEntity(entity)), - .just(.setLoading(true)), .just(.setError(false)), owner.provider.mainService.refreshMain() .flatMap { _ in Observable.empty() } @@ -228,8 +199,6 @@ public final class CameraDisplayViewReactor: Reactor { newState.displayEntity = entity case let .setDisplayOriginalEntity(entity): newState.displayOringalEntity = entity - case let .setPostEntity(entity): - newState.displayPostEntity = entity case let .setError(isError): newState.isError = isError case let .setTrimedText(displayText): diff --git a/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraViewReactor.swift index e3ae14f22..585da4ded 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraViewReactor.swift @@ -19,17 +19,17 @@ public final class CameraViewReactor: Reactor { public var initialState: State - @Injected private var createProfileImageUseCase: CreateCameraUseCaseProtocol @Injected private var uploadImageUseCase: FetchCameraUploadImageUseCaseProtocol - @Injected private var fetchMissionUseCase: FetchCameraTodayMissionUseCaseProtocol + @Injected private var fetchDailyMissionUseCase: FetchDailyMissonContentUseCaseProtocol @Injected private var fetchRealEmojiUpdateUseCase: FetchCameraRealEmojiUpdateUseCaseProtocol @Injected private var fetchRealEmojiCreateUseCase: FetchCameraRealEmojiUploadUseCaseProtocol - @Injected private var editProfileImageUseCase: EditCameraProfileImageUseCaseProtocol @Injected private var fetchRealEmojiListUseCase: FetchCameraRealEmojiListUseCaseProtocol @Injected private var fetchRealEmojiPreSignedUseCase: FetchCameraRealEmojiUseCaseProtocol @Injected private var fetchMyMemberIdUseCase: FetchMyMemberIdUseCaseProtocol @Injected private var provider: ServiceProviderProtocol - + @Injected private var createPresignedURLUseCase: CreateMembersPresignedURLUseCaseProtocol + @Injected private var updateMembersProfileUseCase: UpdateMembersProfileUseCaseProtocol + @Navigator var cameraNavigator: CameraNavigatorProtocol public var cameraType: UploadLocation public var memberId: String @@ -48,20 +48,18 @@ public final class CameraViewReactor: Reactor { case setLoading(Bool) case setPosition(Bool) case setFlashMode(Bool) - case uploadImageToS3(Bool) - case setAccountProfileData(Data) case setPinchZoomScale(CGFloat) case setZoomScale(CGFloat) - case setProfileImageURLResponse(CameraPreSignedEntity?) + case setDailyMissionrResponse(MissonTodayContentEntity?) + case setProfilePresignedResponse(CreateMemberPresignedEntity?) case setProfileMemberResponse(MembersProfileEntity?) case setRealEmojiImageURLResponse(CameraRealEmojiPreSignedEntity?) case setRealEmojiImageCreateResponse(CameraCreateRealEmojiEntity?) case setRealEmojiItems([CameraRealEmojiImageItemEntity?]) case setRealEmojiSection([EmojiSectionItem]) - case setMissionResponse(CameraTodayMssionEntity?) case setErrorAlert(Bool) case setRealEmojiType(Emojis) - case setFeedImageData(Data) + case setImageData(Data) case setUpdateEmojiImage(URL) } @@ -69,21 +67,20 @@ public final class CameraViewReactor: Reactor { @Pulse var isLoading: Bool @Pulse var isFlashMode: Bool @Pulse var isSwitchPosition: Bool - @Pulse var profileImageURLEntity: CameraPreSignedEntity? + @Pulse var missionEntity: MissonTodayContentEntity? + @Pulse var memberPresignedEntity: CreateMemberPresignedEntity? @Pulse var realEmojiURLEntity: CameraRealEmojiPreSignedEntity? @Pulse var realEmojiCreateEntity: CameraCreateRealEmojiEntity? @Pulse var realEmojiEntity: [CameraRealEmojiImageItemEntity?] - @Pulse var missionEntity: CameraTodayMssionEntity? @Pulse var realEmojiSection: [EmojiSectionModel] @Pulse var zoomScale: CGFloat @Pulse var pinchZoomScale: CGFloat - @Pulse var feedImageData: Data? + @Pulse var imageData: Data? var updateEmojiImage: URL? var emojiType: Emojis = .emoji(forIndex: 1) @Pulse var cameraType: UploadLocation = .survival var accountImage: Data? var memberId: String - var isUpload: Bool @Pulse var isError: Bool @Pulse var profileMemberEntity: MembersProfileEntity? } @@ -99,23 +96,14 @@ public final class CameraViewReactor: Reactor { isLoading: true, isFlashMode: false, isSwitchPosition: false, - profileImageURLEntity: nil, - realEmojiURLEntity: nil, - realEmojiCreateEntity: nil, realEmojiEntity: [], - missionEntity: nil, realEmojiSection: [.realEmoji([])], zoomScale: 1.0, pinchZoomScale: 1.0, - feedImageData: nil, - updateEmojiImage: nil, emojiType: emojiType, cameraType: cameraType, - accountImage: nil, memberId: memberId, - isUpload: false, - isError: false, - profileMemberEntity: nil + isError: false ) } @@ -162,14 +150,8 @@ public final class CameraViewReactor: Reactor { newState.isSwitchPosition = isPosition case let .setFlashMode(isFlash): newState.isFlashMode = isFlash - case let .setProfileImageURLResponse(entity): - newState.profileImageURLEntity = entity - case let .uploadImageToS3(isProfileEdit): - newState.isUpload = isProfileEdit case let .setProfileMemberResponse(entity): newState.profileMemberEntity = entity - case let .setAccountProfileData(accountImage): - newState.accountImage = accountImage case let .setRealEmojiImageURLResponse(entity): newState.realEmojiURLEntity = entity case let .setRealEmojiImageCreateResponse(entity): @@ -189,9 +171,11 @@ public final class CameraViewReactor: Reactor { newState.zoomScale = zoomScale case let .setPinchZoomScale(pinchZoomScale): newState.pinchZoomScale = pinchZoomScale - case let .setFeedImageData(feedImage): - newState.feedImageData = feedImage - case let .setMissionResponse(missionEntity): + case let .setImageData(feedImage): + newState.imageData = feedImage + case let .setProfilePresignedResponse(memberPresignedEntity): + newState.memberPresignedEntity = memberPresignedEntity + case let .setDailyMissionrResponse(missionEntity): newState.missionEntity = missionEntity } @@ -258,19 +242,14 @@ extension CameraViewReactor { } ) case .mission: - return fetchMissionUseCase.execute() - .asObservable() - .withUnretained(self) - .flatMap { owner, entity -> Observable in - + return fetchDailyMissionUseCase.execute() + .flatMap { entity -> Observable in return .concat( .just(.setLoading(false)), - .just(.setMissionResponse(entity)), + .just(.setDailyMissionrResponse(entity)), .just(.setLoading(true)) ) } - - default: return .empty() } @@ -282,68 +261,57 @@ extension CameraViewReactor { case .survival, .mission: return .concat( .just(.setLoading(false)), - .just(.setFeedImageData(imageData)), + .just(.setImageData(imageData)), .just(.setLoading(true)) ) + case .account: + let profileImage = "\(imageData.hashValue).jpg" + let body = CreateMemberPresignedReqeust(imageName: profileImage) + + return createPresignedURLUseCase.execute(body: body, imageData: imageData) + .flatMap { entity -> Observable in + guard let _ = entity?.imageURL else { + return .error(BBUploadError.invalidServerResponse) + } + + return .concat( + .just(.setLoading(false)), + .just(.setProfilePresignedResponse(entity)), + .just(.setImageData(imageData)), + .just(.setLoading(true)) + ) + }.catch { [weak self] error in + self?.cameraNavigator.showErrorToast(error.localizedDescription) + return .empty() + } case .profile: let profileImage = "\(imageData.hashValue).jpg" - let body = CreatePresignedURLRequest(imageName: profileImage) + let body = CreateMemberPresignedReqeust(imageName: profileImage) - return .concat( - .just(.setLoading(false)), - createProfileImageUseCase.execute(body: body) - .withUnretained(self) - .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) - .flatMap { owner, entity -> Observable in - guard let remoteURL = entity?.imageURL else { + return createPresignedURLUseCase.execute(body: body, imageData: imageData) + .withUnretained(self) + .flatMap { owner, entity -> Observable in + guard let presignedURL = entity?.imageURL else { + return .error(BBUploadError.invalidServerResponse) + } + let updateImageURL = owner.configureProfileOriginalS3URL(url: presignedURL, with: .profile) + let body = UpdateMemberImageRequest(profileImageUrl: updateImageURL) + return owner.updateMembersProfileUseCase.execute(memberId: owner.memberId, body: body) + .flatMap { entity -> Observable in return .concat( - .just(.setLoading(true)), - .just(.setErrorAlert(true)) + .just(.setLoading(false)), + .just(.setProfileMemberResponse(entity)), + .just(.setLoading(true)) ) + }.catch { [weak self] error in + self?.cameraNavigator.showErrorToast(error.localizedDescription) + return .empty() } - - return owner.uploadImageUseCase.execute(remoteURL, image: imageData) - .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) - .flatMap { isSuccess -> Observable in - - if owner.memberId.isEmpty { - return .concat( - .just(.setProfileImageURLResponse(entity)), - .just(.setAccountProfileData(imageData)), - .just(.setErrorAlert(false)), - .just(.setLoading(true)) - ) - } - - let originalURL = owner.configureProfileOriginalS3URL(url: remoteURL, with: .profile) - let body = UpdateProfileImageRequest(profileImageUrl: originalURL) - - if isSuccess { - return owner.editProfileImageUseCase.execute(memberId: owner.memberId, body: body) - .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) - .flatMap { editEntity -> Observable in - - return .concat( - .just(.setProfileImageURLResponse(entity)), - .just(.uploadImageToS3(isSuccess)), - .just(.setProfileMemberResponse(editEntity)), - .just(.setErrorAlert(false)), - .just(.setLoading(true)) - ) - - } - } else { - return .concat( - .just(.setLoading(true)), - .just(.setErrorAlert(true)) - ) - } - - } - } - - ) + }.catch { [weak self] error in + self?.cameraNavigator.showErrorToast(error.localizedDescription) + return .empty() + } case .realEmoji: guard let memberId = fetchMyMemberIdUseCase.execute() else { return .just(.setErrorAlert(true)) @@ -377,7 +345,6 @@ extension CameraViewReactor { return .concat( .just(.setRealEmojiImageURLResponse(entity)), .just(.setRealEmojiItems(reloadEntity)), - .just(.uploadImageToS3(isSuccess)), .just(.setRealEmojiImageCreateResponse(realEmojiEntity)), .just(.setErrorAlert(false)), .just(.setLoading(true)) @@ -405,7 +372,7 @@ extension CameraViewReactor { .flatMap { owner, entity -> Observable in guard let remoteURL = entity?.imageURL else { return .just(.setErrorAlert(true))} let originalURL = owner.configureProfileOriginalS3URL(url: remoteURL, with: .realEmoji) - + return owner.uploadImageUseCase.execute(remoteURL, image: imageData) .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) .asObservable() @@ -433,8 +400,6 @@ extension CameraViewReactor { } } - - } } diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/Cell/MainFamilyCellReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/Cell/MainFamilyCellReactor.swift index ddcc82a84..bdd47c21e 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/Cell/MainFamilyCellReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/Cell/MainFamilyCellReactor.swift @@ -76,7 +76,7 @@ final class MainFamilyCellReactor: Reactor { let initialState: State - @Injected var pickMemberUseCase: PickMemberUseCaseProtocol + @Injected var pickMemberUseCase: CreateMembersPickUseCaseProtocol @Injected var provider: ServiceProviderProtocol diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift index d4c40dc4c..4d988f0ed 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift @@ -97,7 +97,7 @@ final class MainViewReactor: Reactor { @Navigator var navigator: MainNavigatorProtocol - @Injected var pickUseCase: PickUseCaseProtocol + @Injected var createPickUseCase: CreateMembersPickUseCaseProtocol @Injected var provider: ServiceProviderProtocol @Injected var fetchMainUseCase: FetchMainUseCaseProtocol @Injected var isFirstWidgetAlertUseCase: IsFirstWidgetAlertUseCaseProtocol @@ -200,7 +200,7 @@ extension MainViewReactor { guard let pickedMember = currentState.pickedMember else { return .empty() } - return pickUseCase.executePickMember(memberId: pickedMember.memberId) + return createPickUseCase.execute(memberId: pickedMember.memberId) .compactMap { $0 } .flatMap { response -> Observable in if !response.success { diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileViewController.swift index e769ac8f4..d7e1bb4f4 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileViewController.swift @@ -142,6 +142,7 @@ public final class ProfileViewController: BaseViewController guard let userInfo = notification.userInfo else { return nil } return userInfo["selectImage"] as? Data } + .distinctUntilChanged() .map { Reactor.Action.didSelectPHAssetsImage($0) } .bind(to: reactor.action) .disposed(by: disposeBag) @@ -172,8 +173,8 @@ public final class ProfileViewController: BaseViewController .rx.tap .withLatestFrom(reactor.state.compactMap { $0.profileMemberEntity?.memberId }) .throttle(.milliseconds(300), scheduler: MainScheduler.instance) - .withUnretained(self) - .bind(onNext: { $0.0.transitionNickNameViewController(memberId: $0.1)}) + .map { Reactor.Action.didTappedProfileEditButton($0)} + .bind(to: reactor.action) .disposed(by: disposeBag) reactor.state @@ -246,20 +247,18 @@ public final class ProfileViewController: BaseViewController .rx.tap .throttle(.microseconds(300), scheduler: MainScheduler.instance) .withLatestFrom(reactor.state.compactMap { $0.profileMemberEntity } ) - .withUnretained(self) - .bind { owner, entity in - let profileDetailViewController = ProfileDetailViewControllerWrapper(profileURL: entity.memberImage, userNickname: entity.memberName).makeViewController() - owner.navigationController?.pushViewController(profileDetailViewController, animated: false) - }.disposed(by: disposeBag) + .map { Reactor.Action.didTappedProfileImageView($0.memberImage, $0.memberName)} + .bind(to: reactor.action) + .disposed(by: disposeBag) + navigationBarView.rx.rightButtonTap .withLatestFrom(reactor.state.map { $0.memberId }) - .withUnretained(self) - .bind { owner, memberId in - let privacyViewController = PrivacyViewControllerWrapper(memberId: memberId).viewController - owner.navigationController?.pushViewController(privacyViewController, animated: true) - }.disposed(by: disposeBag) + .throttle(.microseconds(300), scheduler: MainScheduler.instance) + .map { Reactor.Action.didTappedNavigationButton($0)} + .bind(to: reactor.action) + .disposed(by: disposeBag) } } @@ -287,19 +286,13 @@ extension ProfileViewController { ]), for: .normal) } - private func transitionNickNameViewController(memberId: String) { - let accountNickNameViewController:AccountNicknameViewController = AccountSignUpDIContainer(memberId: memberId, accountType: .profile).makeNickNameViewController() - self.navigationController?.pushViewController(accountNickNameViewController, animated: false) - } - private func createAlertController() { let alertController: UIAlertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) let presentCameraAction: UIAlertAction = UIAlertAction(title: "카메라", style: .default) { _ in guard let profileMemberId = self.reactor?.currentState.profileMemberEntity?.memberId else { return } - let cameraViewController = CameraViewControllerWrapper(cameraType: .profile, memberId: profileMemberId).viewController - self.navigationController?.pushViewController(cameraViewController, animated: true) + self.reactor?.action.onNext(.didTappedAlertButton(profileMemberId)) } let presentAlbumAction: UIAlertAction = UIAlertAction(title: "앨범", style: .default) { _ in diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift index 1baccb1ab..9a9325bfa 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift @@ -16,11 +16,11 @@ public final class ProfileViewReactor: Reactor { public var initialState: State @Injected private var fetchMembersProfileUseCase: FetchMembersProfileUseCaseProtocol - @Injected private var createProfilePresignedUseCase: CreateCameraUseCaseProtocol + @Injected private var updateMembersProfileUseCase :UpdateMembersProfileUseCaseProtocol @Injected private var uploadProfileImageUseCase: FetchCameraUploadImageUseCaseProtocol - @Injected private var updateProfileUseCase: UpdateMembersProfileUseCaseProtocol + @Injected private var createPresignedURLUseCase: CreateMembersPresignedURLUseCaseProtocol @Injected private var deleteProfileImageUseCase: DeleteMembersProfileUseCaseProtocol - + @Navigator private var profileNavigator: ProfileNavigatorProtocol private let memberId: String @@ -31,15 +31,17 @@ public final class ProfileViewReactor: Reactor { case viewDidLoad case viewWillAppear case viewDidDisappear - case updateNickNameProfile(Data) case didSelectPHAssetsImage(Data) case didTapInitProfile + case didTappedProfileEditButton(String) + case didTappedProfileImageView(URL, String) + case didTappedNavigationButton(String) + case didTappedAlertButton(String) case didTapSegementControl(BibbiFeedType) } public enum Mutation { case setLoading(Bool) - case setProfilePresingedURL(CameraPreSignedEntity?) case setProfileMemberItems(MembersProfileEntity?) case setProfileFeedType(BibbiFeedType) } @@ -50,7 +52,6 @@ public final class ProfileViewReactor: Reactor { var isUser: Bool var feedType: BibbiFeedType @Pulse var profileMemberEntity: MembersProfileEntity? - @Pulse var profilePresingedURLEntity: CameraPreSignedEntity? } init( @@ -91,40 +92,6 @@ public final class ProfileViewReactor: Reactor { .flatMap { entity -> Observable in .just(.setProfileMemberItems(entity)) } - case let .updateNickNameProfile(imageData): - let profileImage = "\(imageData.hashValue).jpg" - let createPresignedURL = CreatePresignedURLRequest(imageName: profileImage) - return .concat( - .just(.setLoading(false)), - createProfilePresignedUseCase.execute(body: createPresignedURL) - .withUnretained(self) - .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) - .flatMap { owner, entity -> Observable in - guard let remoteURL = entity?.imageURL else { return .empty() } - return owner.uploadProfileImageUseCase.execute(remoteURL, image: imageData) - .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) - .flatMap { isSuccess -> Observable in - let originalPath = owner.configureProfileOriginalS3URL(url: remoteURL) - let profileEditParameter: ProfileImageEditParameter = ProfileImageEditParameter(profileImageUrl: originalPath) - if isSuccess { - return owner.updateProfileUseCase.execute(memberId: owner.currentState.memberId, parameter: profileEditParameter) - .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) - .asObservable() - .flatMap { memberEntity -> Observable in - return .concat( - .just(.setProfilePresingedURL(entity)), - .just(.setProfileMemberItems(memberEntity)), - .just(.setLoading(true)) - - ) - } - } else { - return .empty() - } - - } - }) - case .viewWillAppear: return fetchMembersProfileUseCase.execute(memberId: currentState.memberId) .asObservable() @@ -139,42 +106,31 @@ public final class ProfileViewReactor: Reactor { case let .didSelectPHAssetsImage(assetImage): let imageName: String = "\(assetImage.hashValue).jpg" - let createPresignedURL = CreatePresignedURLRequest(imageName: imageName) - return .concat( - .just(.setLoading(false)), - createProfilePresignedUseCase.execute(body: createPresignedURL) - .withUnretained(self) - .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) - .flatMap { owner, entity -> Observable in - guard let remoteURL = entity?.imageURL else { return .empty() } - return owner.uploadProfileImageUseCase.execute(remoteURL, image: assetImage) - .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) - .flatMap { isSuccess -> Observable in - let originalPath = owner.configureProfileOriginalS3URL(url: remoteURL) - let profileEditParameter: ProfileImageEditParameter = ProfileImageEditParameter(profileImageUrl: originalPath) - if isSuccess { - return owner.updateProfileUseCase.execute(memberId: owner.currentState.memberId, parameter: profileEditParameter) - .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) - .asObservable() - .flatMap { memberEntity -> Observable in - return .concat( - .just(.setProfilePresingedURL(entity)), - .just(.setProfileMemberItems(memberEntity)), - .just(.setLoading(true)) - - ) - } - - } else { - return .empty() - } - - } + let body = CreateMemberPresignedReqeust(imageName: imageName) + + return createPresignedURLUseCase.execute(body: body, imageData: assetImage) + .withUnretained(self) + .flatMap { owner, entity -> Observable in + guard let presignedURL = entity?.imageURL else { + return .error(BBUploadError.invalidServerResponse) } - ) + let updateImageURL = owner.configureProfileOriginalS3URL(url: presignedURL) + let body = UpdateMemberImageRequest(profileImageUrl: updateImageURL) + return owner.updateMembersProfileUseCase.execute(memberId: owner.memberId, body: body) + .flatMap { entity -> Observable in + return .concat( + .just(.setLoading(false)), + .just(.setProfileMemberItems(entity)), + .just(.setLoading(true)) + ) + } + }.catch { [weak self] error in + self?.profileNavigator.showErrorToast(error.localizedDescription) + return .empty() + } + case .didTapInitProfile: return deleteProfileImageUseCase.execute(memberId: memberId) - .asObservable() .flatMap { entity -> Observable in return .concat( .just(.setLoading(false)), @@ -188,6 +144,18 @@ public final class ProfileViewReactor: Reactor { case .viewDidDisappear: return provider.mainService.refreshMain() .flatMap { _ in Observable.empty() } + case let .didTappedProfileImageView(imageURL, nickName): + profileNavigator.toProfileDetail(imageURL, nickName: nickName) + return .empty() + case let .didTappedNavigationButton(memberId): + profileNavigator.toPrivacy(memberId) + return .empty() + case let .didTappedProfileEditButton(memberId): + profileNavigator.toAccountNickname(memberId) + return .empty() + case let .didTappedAlertButton(memberId): + profileNavigator.toCamera(memberId) + return .empty() } } @@ -202,9 +170,6 @@ public final class ProfileViewReactor: Reactor { case let .setProfileMemberItems(entity): provider.managementService.didUpdateFamilyInfo() newState.profileMemberEntity = entity - - case let .setProfilePresingedURL(entity): - newState.profilePresingedURLEntity = entity case let .setProfileFeedType(feedType): newState.feedType = feedType diff --git a/14th-team5-iOS/App/Sources/Presentation/Resign/AccountResignViewCotroller.swift b/14th-team5-iOS/App/Sources/Presentation/Resign/AccountResignViewController.swift similarity index 76% rename from 14th-team5-iOS/App/Sources/Presentation/Resign/AccountResignViewCotroller.swift rename to 14th-team5-iOS/App/Sources/Presentation/Resign/AccountResignViewController.swift index a5ef68302..3adcd6ab3 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Resign/AccountResignViewCotroller.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Resign/AccountResignViewController.swift @@ -11,19 +11,18 @@ import Core import DesignSystem import RxCocoa import ReactorKit +import RxDataSources import SnapKit import Then - -final class AccountResignViewCotroller: BaseViewController { +final class AccountResignViewController: BaseViewController { - //TODO: 텍스트 컬러, 폰트 정해지면 수정 private let resignDesrptionLabel: BBLabel = BBLabel(.body1Regular, textColor: .gray400) private let resignReasonLabel: BBLabel = BBLabel(.head1, textColor: .gray200) private let resignExampleLabel: BBLabel = BBLabel(.body1Regular, textColor: .gray400) + private let reasonTableView: UITableView = UITableView() private let resignIndicatorView: UIActivityIndicatorView = UIActivityIndicatorView(style: .medium) private let confirmButton: UIButton = UIButton() - private let bibbiTermsView: BibbiCheckBoxView = BibbiCheckBoxView(frame: .zero) override func viewDidLoad() { super.viewDidLoad() @@ -31,7 +30,7 @@ final class AccountResignViewCotroller: BaseViewController RxTableViewSectionedReloadDataSource { + return RxTableViewSectionedReloadDataSource { datasource, tableView, indexPath, reactor in + guard let cell = tableView.dequeueReusableCell(withIdentifier: "ResignReaonsTableViewCell") as? ResignReaonsTableViewCell else { + return UITableViewCell() + } + cell.reactor = reactor + return cell + } + } +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Resign/Cell/ResignReaonsTableViewCell.swift b/14th-team5-iOS/App/Sources/Presentation/Resign/Cell/ResignReaonsTableViewCell.swift new file mode 100644 index 000000000..340a34592 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Resign/Cell/ResignReaonsTableViewCell.swift @@ -0,0 +1,70 @@ +// +// ResignReaonsTableViewCell.swift +// App +// +// Created by 김도현 on 11/21/24. +// + +import UIKit + +import Core +import DesignSystem + + + +final class ResignReaonsTableViewCell: BaseTableViewCell { + private let checkBoxButton: UIButton = UIButton(type: .custom) + + + override func awakeFromNib() { + super.awakeFromNib() + } + + + override func setupUI() { + contentView.addSubview(checkBoxButton) + } + + override func setupAttributes() { + + self.do { + $0.backgroundColor = .clear + $0.selectionStyle = .none + } + + checkBoxButton.do { + $0.configuration = .plain() + $0.configuration?.baseBackgroundColor = .clear + $0.configuration?.imagePadding = 10 + $0.configuration?.imagePlacement = .leading + $0.isUserInteractionEnabled = false + } + } + + + override func setupAutoLayout() { + checkBoxButton.snp.makeConstraints { + $0.left.equalToSuperview() + $0.top.equalToSuperview().inset(14) + $0.centerY.equalToSuperview() + } + } + + override func bind(reactor: Reactor) { + reactor.state + .map { $0.reasonType.title } + .distinctUntilChanged() + .bind(with: self) { owner, content in + owner.checkBoxButton.configuration?.attributedTitle = AttributedString(NSAttributedString(string: content, attributes: [ + .foregroundColor: DesignSystemAsset.gray200.color, + .font: DesignSystemFontFamily.Pretendard.regular.font(size: 16), + .kern: -0.3 + ])) + } + .disposed(by: disposeBag) + } + + override func setSelected(_ selected: Bool, animated: Bool) { + checkBoxButton.configuration?.image = selected ? DesignSystemAsset.checkBox.image : DesignSystemAsset.uncheckBox.image + } +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Resign/Config/ReasonType.swift b/14th-team5-iOS/App/Sources/Presentation/Resign/Config/ReasonType.swift new file mode 100644 index 000000000..d84d3f6e1 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Resign/Config/ReasonType.swift @@ -0,0 +1,35 @@ +// +// ReasonType.swift +// App +// +// Created by 김도현 on 11/22/24. +// + +import Foundation + +public enum ReasonType: String, CaseIterable { + case noNeedToShareDaily = "NO_NEED_TO_SHARE_DAILY" + case familyMemberNotUsing = "FAMILY_MEMBER_NOT_USING" + case noPreferWidgetOrNotification = "NO_PREFER_WIDGET_OR_NOTIFICATION" + case serviceUxIsBad = "SERVICE_UX_IS_BAD" + case noFrequencyUse = "NO_FREQUENTLY_USE" + case none + + + public var title: String { + switch self { + case .noNeedToShareDaily: + return "가족과 일상을 공유하고 싶지 않아서" + case .familyMemberNotUsing: + return "가족 구성원이 참여하지 않아서" + case .noPreferWidgetOrNotification: + return "알림 및 위젯 기능을 선호하지 않아서" + case .serviceUxIsBad: + return "서비스 이용이 어렵거나 불편해서" + case .noFrequencyUse: + return "자주 사용하지 않아서" + case .none: + return "" + } + } +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Resign/DataSources/AccountResignSectionModel.swift b/14th-team5-iOS/App/Sources/Presentation/Resign/DataSources/AccountResignSectionModel.swift new file mode 100644 index 000000000..c7c72fdf3 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Resign/DataSources/AccountResignSectionModel.swift @@ -0,0 +1,13 @@ +// +// AccountResignSectionModel.swift +// App +// +// Created by 김도현 on 11/22/24. +// + +import Foundation + +import RxDataSources + + +typealias AccountResignSectionModel = SectionModel diff --git a/14th-team5-iOS/App/Sources/Presentation/Resign/Reactor/AccountResignViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Resign/Reactor/AccountResignViewReactor.swift index 0ec3620bb..70fc6f118 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Resign/Reactor/AccountResignViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Resign/Reactor/AccountResignViewReactor.swift @@ -13,60 +13,64 @@ import ReactorKit import RxSwift final class AccountResignViewReactor: Reactor { - @Injected var deleteAccountResignUseCase: DeleteAccountResignUseCaseProtocol + @Navigator var resignNavigator: AccountResignNavigatorProtocol + @Injected var deleteAccountResignUseCase: DeleteMembersUseCaseProtocol @Injected var updateIsFirstOnboardingUseCase: UpdateIsFirstOnboardingUseCaseProtocol + @Injected var fetchMyMemberIdUseCase: FetchMyMemberIdUseCaseProtocol var initialState: State enum Action { case viewDidLoad - case didTapCheckButton(Bool) + case didTappedReasonButton(ReasonType) case didTapResignButton } enum Mutation { - case setLoading(Bool) - case setSelect(Bool) - case setResignEntity(Bool) + case setReasonItems([ResignReasonTableViewCellReactor]) + case setSelected(ReasonType) } struct State { - var isLoading: Bool - var isSeleced: Bool - var isSuccess: Bool + @Pulse var reasonSectionModel: [AccountResignSectionModel] + var reasonType: ReasonType } init() { self.initialState = State( - isLoading: false, - isSeleced: false, - isSuccess: false + reasonSectionModel: [], + reasonType: .none ) } func mutate(action: Action) -> Observable { switch action { case .viewDidLoad: - return .concat( - .just(.setLoading(true)), - .just(.setLoading(false)) - ) - case let .didTapCheckButton(isSelected): - return .just(.setSelect(isSelected)) + let reasonReactor = ReasonType.allCases.map { ResignReasonTableViewCellReactor(reasonType: $0) } + + return .just(.setReasonItems(reasonReactor)) + + case let .didTappedReasonButton(reasonType): + return .just(.setSelected(reasonType)) + + case .didTapResignButton: MPEvent.Account.withdrawl.track(with: nil) - return deleteAccountResignUseCase.execute() - .asObservable() + + guard let memberId = fetchMyMemberIdUseCase.execute() else { + resignNavigator.showErrorToast() + return .empty() + } + + return deleteAccountResignUseCase.execute(memberId: memberId) .compactMap { $0 } .withUnretained(self) .flatMap { owner, entity -> Observable in if entity.isSuccess { owner.updateIsFirstOnboardingUseCase.execute(false) - return .concat( - .just(.setLoading(true)), - .just(.setResignEntity(entity.isSuccess)), - .just(.setLoading(false)) - ) + App.Repository.token.clearAccessToken() + owner.resignNavigator.toSignIn() + return .empty() } return .empty() } @@ -77,12 +81,11 @@ final class AccountResignViewReactor: Reactor { var newState = state switch mutation { - case let .setLoading(isLoading): - newState.isLoading = isLoading - case let .setSelect(isSelected): - newState.isSeleced = isSelected - case let .setResignEntity(isSuccess): - newState.isSuccess = isSuccess + case let .setSelected(reasonType): + newState.reasonType = reasonType + case let .setReasonItems(items): + let dataSource = AccountResignSectionModel(model: (), items: items) + newState.reasonSectionModel = [dataSource] } return newState diff --git a/14th-team5-iOS/App/Sources/Presentation/Resign/Reactor/ResignReasonTableViewCellReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Resign/Reactor/ResignReasonTableViewCellReactor.swift new file mode 100644 index 000000000..0920fcd26 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Presentation/Resign/Reactor/ResignReasonTableViewCellReactor.swift @@ -0,0 +1,23 @@ +// +// ResignReasonTableViewCellReactor.swift +// App +// +// Created by 김도현 on 11/21/24. +// + +import Foundation + +import ReactorKit + +final class ResignReasonTableViewCellReactor: Reactor { + public typealias Action = NoAction + var initialState: State + + public struct State { + var reasonType: ReasonType + } + + init(reasonType: ReasonType) { + self.initialState = State(reasonType: reasonType) + } +} diff --git a/14th-team5-iOS/App/Sources/Presentation/Resign/View/BibbiCheckBoxView.swift b/14th-team5-iOS/App/Sources/Presentation/Resign/View/BibbiCheckBoxView.swift deleted file mode 100644 index c5c06e6fc..000000000 --- a/14th-team5-iOS/App/Sources/Presentation/Resign/View/BibbiCheckBoxView.swift +++ /dev/null @@ -1,88 +0,0 @@ -// -// BibbiCheckBoxView.swift -// App -// -// Created by Kim dohyun on 10/9/24. -// - -import UIKit - -import DesignSystem -import SnapKit -import Then - -enum ReasonTypes: String, CaseIterable { - case reasonOne = "가족과 일상을 공유하고 싶지 않아서" - case reaosonTwo = "가족 구성원이 참여하지 않아서" - case reasonThree = "알림 및 위젯 기능을 선호하지 않아서" - case reasonFour = "서비스 이용이 어렵거나 불편해서" - case reasonFive = "자주 사용하지 않아서" -} - - -final class BibbiCheckBoxView: UIView { - private let checkStackView: UIStackView = UIStackView() - var checkButtons: [UIButton] = [] - - - - public override init(frame: CGRect) { - super.init(frame: .zero) - makeReasonButtons() - setupUI() - setupAttributes() - setupAutoLayout() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - - func setupUI() { - addSubview(checkStackView) - checkButtons.forEach { - checkStackView.addArrangedSubviews($0) - } - } - - func setupAttributes() { - checkStackView.do { - $0.axis = .vertical - $0.distribution = .equalSpacing - $0.alignment = .leading - $0.isUserInteractionEnabled = true - } - } - - func setupAutoLayout() { - checkStackView.snp.makeConstraints { - $0.edges.equalToSuperview() - } - } - - func makeReasonButtons() { - ReasonTypes.allCases.enumerated() - .forEach { index, reason in - let button = UIButton() - button.tag = index - button.changesSelectionAsPrimaryAction = true - button.configuration = .plain() - button.configuration?.baseBackgroundColor = .clear - button.configuration?.imagePadding = 10 - button.configuration?.imagePlacement = .leading - button.configuration?.attributedTitle = AttributedString(NSAttributedString(string: reason.rawValue, attributes: [ - .foregroundColor: DesignSystemAsset.gray200.color, - .font: DesignSystemFontFamily.Pretendard.regular.font(size: 16), - .kern: -0.3 - ])) - button.configurationUpdateHandler = { button in - button.configuration?.image = button.isSelected ? DesignSystemAsset.checkBox.image : DesignSystemAsset.uncheckBox.image - } - - checkButtons.append(button) - } - } - -} - diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/BBNetworkError.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/BBNetworkError.swift index a39b72e8e..9d0b10a96 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/BBNetworkError.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/BBNetworkError.swift @@ -110,3 +110,33 @@ extension BBNetworkError: LocalizedError { } } } + + +public enum BBUploadError: Error { + case fileNotFound + case fileCorrupted + case unauthorized + case timeout + case invalidServerResponse + case uploadFailed +} + + +extension BBUploadError: LocalizedError { + public var errorDescription: String? { + switch self { + case .fileNotFound: + return "죄송합니다. 요청한 파일을 찾을 수 없습니다.\n 다시 시도해 주세요" + case .fileCorrupted: + return "업로드한 이미지 파일이 손상된 것으로 확인되었습니다.\n 다른 파일을 시도하거나 다시 업로드해 주세요." + case .unauthorized: + return "인증되지 않은 요청입니다.\n 로그인 상태가 유효하지 않거나 인증 토큰이 만료되었을 수 있습니다. 다시 로그인한 후 요청을 시도하세요." + case .timeout: + return "업로드 요청이 시간 초과되었습니다. 다시 시도해 주세요" + case .invalidServerResponse: + return "서버에서 잘못된 응답을 받았습니다." + case .uploadFailed: + return "업로드가 실패했습니다. 다시 시도해 주세요" + } + } +} diff --git a/14th-team5-iOS/Data/Sources/APIs/Camera/CameraAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/Camera/CameraAPIWorker.swift index 10501112e..c78da3430 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Camera/CameraAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Camera/CameraAPIWorker.swift @@ -16,50 +16,7 @@ import RxSwift typealias CameraAPIWorker = CameraAPIs.Worker extension CameraAPIWorker { - - /// 회읜 프로필 이미지 업로드를 하기 위한 Presigned-URL API 요청 Method - /// HTTP Method: POST - /// - Parameters : body (업로드 image Name) - /// - Returns : CameraDisplayImageResponseDTO - public func createProfilePresignedURL(body: CreatePresignedURLReqeustDTO) -> Observable { - let spec = CameraAPIs.createProfilePresignedURL(body: body).spec - - return request(spec) - } - - /// 게시믈 이미지 업로드를 하기 위한 Presigend-URL API 요청 Method 입니다 - /// HTTP Method : POST - /// - Returns : CameraDisplayImageResponseDTO - public func createFeedPresignedURL(body: CreatePresignedURLReqeustDTO) -> Observable { - let spec = CameraAPIs.createFeedPresignedURL(body: body).spec - - return request(spec) - } - - /// 사용자 프로필 이미지를 업데이트 하기 위한 Method 입니다. - /// HTTP Method : PUT - /// - Parameters : MemberId (사용자 멤버 ID) - /// - Returns : MembersProfileResponseDTO - public func updateProfileImage(memberId: String, body: UpdateProfileImageRequestDTO) -> Observable { - let spec = CameraAPIs.updateProfileImage(memberId: memberId, body: body).spec - - return request(spec) - } - - /// 게시물 생성을 하기 위한 Method 입니다. - /// HTTP Method : POST - /// - Parameters : - /// - query : CreateFeedQuery - /// - body : CameraFeedRequestDTO - /// - type: String - /// - available : Bool - /// - Returns : CameraDisplayPostResponseDTO - public func createFeed(query: CreateFeedQuery, body: CreateFeedRequestDTO) -> Observable { - let spec = CameraAPIs.createFeed(type: query.type, body: body).spec - - return request(spec) - } - + /// 리얼 이모지를 업로드 하기 위한 Presigend-URL API 요청 Method 입니다 /// HTTP Method : POST /// - Parameters : @@ -108,15 +65,6 @@ extension CameraAPIWorker { return request(spec) } - /// 일일 미션 조회를 하기 위한 API Method입니다. - /// HTTP Method : GET - /// - Returns : CameraTodayMissionResponseDTO - public func fetchDailyMisson() -> Observable { - let spec = CameraAPIs.fetchTodayMission.spec - - return request(spec) - } - /// 피드, 프로필 이미지를 S3 Bucket에 업로드 하기 위한 Method 입니다. /// HTTP Method : PUT /// - Parameters diff --git a/14th-team5-iOS/Data/Sources/APIs/Camera/CameraAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/Camera/CameraAPIs.swift index ccc516fde..5b38eeed6 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Camera/CameraAPIs.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Camera/CameraAPIs.swift @@ -11,16 +11,8 @@ import Foundation enum CameraAPIs: BBAPI { - /// 회원 프로필 Presigend URL 요청 API - case createProfilePresignedURL(body: CreatePresignedURLReqeustDTO) - /// 회원 프로필 이미지 수정 API - case updateProfileImage(memberId: String, body: UpdateProfileImageRequestDTO) - /// 게시물 사진 Presigned URL 요청 API - case createFeedPresignedURL(body: CreatePresignedURLReqeustDTO) /// S3 Bucket 업로드 API case uploadTos3Bucket(presignedURL: String) - /// 게시물 생성 API - case createFeed(type: String, body: CreateFeedRequestDTO) /// 리얼 이모지 Presigned URL 요청 API case createRealEmojiPresignedURL(memberID: String, body: CreatePresignedURLReqeustDTO) /// 리얼 이미지 추가 API @@ -29,28 +21,11 @@ enum CameraAPIs: BBAPI { case fetchRealEmojiImage(memberId: String) /// 리얼 이모지 수정 API case updateRealEmojiImage(memberId: String, realEmojiId: String, body: UpdateRealEmojiImageRequestDTO) - /// 금일의 일일 미션 조회 API - case fetchTodayMission var spec: Spec { switch self { - case let .createProfilePresignedURL(body): - return Spec(method: .post, path: "/members/image-upload-request", bodyParametersEncodable: body) - case let .updateProfileImage(memberId, body): - return Spec(method: .put, path: "/members/profile-image-url/\(memberId)", bodyParametersEncodable: body) - case let .createFeedPresignedURL(body): - return Spec(method: .post, path: "/posts/image-upload-request", bodyParametersEncodable: body) case let .uploadTos3Bucket(presignedURL): return Spec(method: .put, path: presignedURL) - case let .createFeed(type, body): - return Spec( - method: .post, - path: "/posts", - queryParameters: [ - .type: "\(type)" - ], - bodyParametersEncodable: body - ) case let .createRealEmojiPresignedURL(memberId, body): return Spec(method: .post, path: "/members/\(memberId)/real-emoji/image-upload-request", bodyParametersEncodable: body) case let .createRealEmojiImage(memberId, body): @@ -59,8 +34,6 @@ enum CameraAPIs: BBAPI { return Spec(method: .get, path: "/members/\(memberId)/real-emoji") case let .updateRealEmojiImage(memberId, realEmojiId, body): return Spec(method: .put, path: "/members/\(memberId)/real-emoji/\(realEmojiId)", bodyParametersEncodable: body) - case .fetchTodayMission: - return Spec(method: .get, path: "/missions/today") } diff --git a/14th-team5-iOS/Data/Sources/APIs/Camera/DataMapping/CameraDisplayImageResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Camera/DataMapping/CameraDisplayImageResponseDTO.swift deleted file mode 100644 index 271ba073d..000000000 --- a/14th-team5-iOS/Data/Sources/APIs/Camera/DataMapping/CameraDisplayImageResponseDTO.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// CameraDisplayImageResponseDTO.swift -// Data -// -// Created by Kim dohyun on 6/7/24. -// - -import Foundation - -import Domain - - - -public struct CameraDisplayImageResponseDTO: Decodable { - public var imageURL: String? - - public enum CodingKeys: String, CodingKey { - case imageURL = "url" - } -} - - -extension CameraDisplayImageResponseDTO { - - public func toDomain() -> CameraPreSignedEntity? { - return .init(imageURL: imageURL ?? "") - } -} diff --git a/14th-team5-iOS/Data/Sources/APIs/Camera/DataMapping/CameraDisplayPostResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Camera/DataMapping/CameraDisplayPostResponseDTO.swift deleted file mode 100644 index 606fe86a1..000000000 --- a/14th-team5-iOS/Data/Sources/APIs/Camera/DataMapping/CameraDisplayPostResponseDTO.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// CameraDisplayPostResponseDTO.swift -// Data -// -// Created by Kim dohyun on 6/7/24. -// - -import Foundation - -import Domain - -public struct CameraDisplayPostResponseDTO: Decodable { - - public var postId: String? - public var authorId: String? - public var type: String? - public var missionId: String? - public var commentCount: Int? - public var emojiCount: Int? - public var imageUrl: String? - public var content: String? - public var createdAt: String - -} - - -extension CameraDisplayPostResponseDTO { - - public func toDomain() -> CameraPostEntity { - return .init( - postId: postId ?? "", - authorId: authorId ?? "", - commentCount: commentCount ?? 0, - missionType: type ?? "SURVIVAL", - missionId: missionId ?? "", - emojiCount: emojiCount ?? 0, - imageURL: imageUrl ?? "", - content: content ?? "", - createdAt: createdAt - ) - } -} diff --git a/14th-team5-iOS/Data/Sources/APIs/Camera/DataMapping/CameraTodayMissionResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Camera/DataMapping/CameraTodayMissionResponseDTO.swift deleted file mode 100644 index 97de51ad7..000000000 --- a/14th-team5-iOS/Data/Sources/APIs/Camera/DataMapping/CameraTodayMissionResponseDTO.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// CameraTodayMissionResponseDTO.swift -// Data -// -// Created by Kim dohyun on 6/7/24. -// - -import Foundation - -import Domain - -public struct CameraTodayMissionResponseDTO: Decodable { - - public var missionDate: String - public var missionId: String - public var missionContent: String - - public enum CodingKeys: String, CodingKey { - case missionDate = "date" - case missionId = "id" - case missionContent = "content" - - } -} - - -extension CameraTodayMissionResponseDTO { - func toDomain() -> CameraTodayMssionEntity { - return .init( - missionDate: missionDate.toDate(with: "yyyy-MM-dd"), - missionId: missionId, - missionContent: missionContent - ) - } - -} - diff --git a/14th-team5-iOS/Data/Sources/APIs/Camera/DataMapping/CreateFeedRequestDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Camera/DataMapping/CreateFeedRequestDTO.swift deleted file mode 100644 index 0d01f2b23..000000000 --- a/14th-team5-iOS/Data/Sources/APIs/Camera/DataMapping/CreateFeedRequestDTO.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// CreateFeedRequestDTO.swift -// Data -// -// Created by 김도현 on 11/17/24. -// - -import Foundation - -public struct CreateFeedRequestDTO: Encodable { - public let imageUrl: String - public let content: String - public let uploadTime: String -} - diff --git a/14th-team5-iOS/Data/Sources/APIs/Camera/Repository/CameraRepository.swift b/14th-team5-iOS/Data/Sources/APIs/Camera/Repository/CameraRepository.swift index f883280ec..4a6193f67 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Camera/Repository/CameraRepository.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Camera/Repository/CameraRepository.swift @@ -25,55 +25,28 @@ public final class CameraRepository { extension CameraRepository: CameraRepositoryProtocol { - public func createFeedImage(query: CreateFeedQuery, body: CreateFeedRequest) -> Observable { - let body = CreateFeedRequestDTO(imageUrl: body.imageUrl, content: body.content, uploadTime: body.uploadTime) - - return cameraAPIWorker.createFeed(query: query, body: body) - .map { $0?.toDomain() } - } - public func createEmojiImagePresignedURL(memberID: String, body: CreatePresignedURLRequest) -> Observable { let body = CreatePresignedURLReqeustDTO(imageName: body.imageName) return cameraAPIWorker.createRealEmojiPresignedURL(memberId: memberID, body: body) .map { $0?.toDomain() } } - public func createProfileImagePresignedURL(body: CreatePresignedURLRequest) -> Observable { - let body = CreatePresignedURLReqeustDTO(imageName: body.imageName) - return cameraAPIWorker.createProfilePresignedURL(body: body) - .map { $0?.toDomain() } - } - public func createEmojiImage(memberId: String, body: CreateEmojiImageRequest) -> Observable { let body = CreateEmojiImageReqeustDTO(type: body.type, imageUrl: body.imageUrl) return cameraAPIWorker.createRealEmoji(memberId: memberId, body: body) .map { $0.toDomain() } } - public func fetchDailyMissonItem() -> Observable { - return cameraAPIWorker.fetchDailyMisson() - .map { $0?.toDomain() } - } public func fetchEmojiList(memberId: String) -> Observable<[CameraRealEmojiImageItemEntity?]> { return cameraAPIWorker.fetchRealEmoji(memberId: memberId) .map { $0?.toDomain() ?? [] } } - public func updateUserProfileImage(memberId: String, body: UpdateProfileImageRequest) -> Observable { - let body = UpdateProfileImageRequestDTO(profileImageUrl: body.profileImageUrl) - return cameraAPIWorker.updateProfileImage(memberId: memberId, body: body) - .map { $0?.toDomain() } - } - public func updateEmojiImage(memberId: String, realEmojiId: String, body: UpdateRealEmojiImageRequest) -> Observable { let body = UpdateRealEmojiImageRequestDTO(imageUrl: body.imageUrl) return cameraAPIWorker.updateRealEmoji(memberId: memberId, realEmojiId: realEmojiId, body: body) .map { $0?.toDomain() } } - public func uploadImageToS3Bucket(_ presignedURL: String, image: Data) -> Observable { - return cameraAPIWorker.uploadImageToPresignedURL(presignedURL, image: image) - } - } diff --git a/14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/DataMapping/CreateMemberPickResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/DataMapping/CreateMemberPickResponseDTO.swift new file mode 100644 index 000000000..d4d6fef81 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/DataMapping/CreateMemberPickResponseDTO.swift @@ -0,0 +1,22 @@ +// +// CreateMemberPickResponseDTO.swift +// Data +// +// Created by 김도현 on 11/21/24. +// + +import Foundation + +import Domain + +struct CreateMemberPickResponseDTO: Decodable { + let success: Bool +} + + +extension CreateMemberPickResponseDTO { + func toDomain() -> CreateMemberPickEntity { + return .init(success: success) + } +} + diff --git a/14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/DataMapping/CreateMemberPresignedURLRequestDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/DataMapping/CreateMemberPresignedURLRequestDTO.swift new file mode 100644 index 000000000..c57aeef8e --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/DataMapping/CreateMemberPresignedURLRequestDTO.swift @@ -0,0 +1,12 @@ +// +// CreateMemberPresignedURLRequestDTO.swift +// Data +// +// Created by 김도현 on 11/22/24. +// + +import Foundation + +struct CreateMemberPresignedURLRequestDTO: Encodable { + let imageName: String +} diff --git a/14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/DataMapping/CreateMemberPresignedURLResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/DataMapping/CreateMemberPresignedURLResponseDTO.swift new file mode 100644 index 000000000..2643d00f9 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/DataMapping/CreateMemberPresignedURLResponseDTO.swift @@ -0,0 +1,25 @@ +// +// CreateMemberPresignedURLResponseDTO.swift +// Data +// +// Created by 김도현 on 11/22/24. +// + +import Foundation + +import Domain + + +struct CreateMemberPresignedURLResponseDTO: Decodable { + let imageURL: String? + + enum CodingKeys: String, CodingKey { + case imageURL = "url" + } +} + +extension CreateMemberPresignedURLResponseDTO { + public func toDomain() -> CreateMemberPresignedEntity { + return .init(imageURL: imageURL ?? "") + } +} diff --git a/14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/DataMapping/DeleteMemberResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/DataMapping/DeleteMemberResponseDTO.swift new file mode 100644 index 000000000..5555ec686 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/DataMapping/DeleteMemberResponseDTO.swift @@ -0,0 +1,21 @@ +// +// DeleteMemberResponseDTO.swift +// Data +// +// Created by 김도현 on 11/21/24. +// + +import Foundation + +import Domain + +struct DeleteMemberResponseDTO: Decodable { + let success: Bool +} + + +extension DeleteMemberResponseDTO { + public func toDomain() -> DeleteMemberEntity { + return .init(isSuccess: success) + } +} diff --git a/14th-team5-iOS/Data/Sources/APIs/Members/DataMapping/MembersProfileResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/DataMapping/MembersProfileResponseDTO.swift similarity index 94% rename from 14th-team5-iOS/Data/Sources/APIs/Members/DataMapping/MembersProfileResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/DataMapping/MembersProfileResponseDTO.swift index d1f250ea7..aa69701b2 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Members/DataMapping/MembersProfileResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/DataMapping/MembersProfileResponseDTO.swift @@ -21,7 +21,7 @@ public struct MembersProfileResponseDTO: Decodable { extension MembersProfileResponseDTO { //MARK: 프로필 정보 Entity - func toDomain() -> MembersProfileEntity { + public func toDomain() -> MembersProfileEntity { return .init( memberId: memberId, memberName: name, diff --git a/14th-team5-iOS/Data/Sources/APIs/Camera/DataMapping/UpdateProfileImageRequestDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/DataMapping/UpdateMemberImageRequestDTO.swift similarity index 55% rename from 14th-team5-iOS/Data/Sources/APIs/Camera/DataMapping/UpdateProfileImageRequestDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/DataMapping/UpdateMemberImageRequestDTO.swift index 3d5911e3a..aee5ee56f 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Camera/DataMapping/UpdateProfileImageRequestDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/DataMapping/UpdateMemberImageRequestDTO.swift @@ -1,5 +1,5 @@ // -// UpdateProfileImageRequestDTO.swift +// UpdateMemberImageRequestDTO.swift // Data // // Created by 김도현 on 11/17/24. @@ -8,6 +8,6 @@ import Foundation -public struct UpdateProfileImageRequestDTO: Encodable { +public struct UpdateMemberImageRequestDTO: Encodable { public let profileImageUrl: String } diff --git a/14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/DataMapping/UpdateMemberNameRequestDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/DataMapping/UpdateMemberNameRequestDTO.swift new file mode 100644 index 000000000..a64569440 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/DataMapping/UpdateMemberNameRequestDTO.swift @@ -0,0 +1,13 @@ +// +// UpdateMemberNameRequestDTO.swift +// Data +// +// Created by 김도현 on 11/20/24. +// + +import Foundation + + +struct UpdateMemberNameRequestDTO: Encodable { + let name: String +} diff --git a/14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/DataMapping/UpdateMemberNameResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/DataMapping/UpdateMemberNameResponseDTO.swift new file mode 100644 index 000000000..5027bec09 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/DataMapping/UpdateMemberNameResponseDTO.swift @@ -0,0 +1,33 @@ +// +// UpdateMemberNameResponseDTO.swift +// Data +// +// Created by 김도현 on 11/21/24. +// + +import Foundation + +import Domain + + +struct UpdateMemberNameResponseDTO: Decodable { + let memberId: String + let name: String + let imageUrl: String? + let familyId: String + let familyJoinAt: String + let dayOfBirth: String +} + +extension UpdateMemberNameResponseDTO { + func toDomain() -> UpdateMemberNameEntity { + return .init( + memberId: memberId, + name: name, + imageUrl: URL(string: imageUrl ?? "") ?? URL(fileURLWithPath: ""), + familyId: familyId, + familyJoinAt: familyJoinAt, + dayOfBirth: dayOfBirth + ) + } +} diff --git a/14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/MembersAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/MembersAPIWorker.swift index 1e3ef622c..510122902 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/MembersAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/MembersAPIWorker.swift @@ -8,91 +8,93 @@ import Core import Foundation -import Alamofire -import Domain import RxSwift typealias MembersAPIWorker = MembersAPIs.Worker -extension MembersAPIs { - final class Worker: APIWorker { - static let queue = { - ConcurrentDispatchQueueScheduler(queue: DispatchQueue(label: "ProfileAPIQueue", qos: .utility)) - }() - - override init() { - super.init() - self.id = "ProfileAPIWorker" - } +extension MembersAPIWorker { + + /// 회원 프로필 정보를 조회하기 위한 Method 입니다. + /// HTTP Method : GET + /// - Parameters : memberId (조회할 회원 ID) + /// - Returns : MembersProfileResponseDTO + func fetchMember(memberId: String) -> Observable { + let spec = MembersAPIs.fetchMember(memberId: memberId).spec + return request(spec) } -} - - -extension MembersAPIWorker { - public func fetchProfileMember(memberId: String) -> Single { - let spec = MembersAPIs.profileMember(memberId).spec - let accessToken: String = App.Repository.token.accessToken.value?.accessToken ?? "" - let headers = BibbiHeader.commonHeaders(accessToken: accessToken) - return request(spec: spec, headers: headers) - .subscribe(on: Self.queue) - .map(MembersProfileResponseDTO.self) - .catchAndReturn(nil) - .asSingle() + /// 회원 탈퇴 API 요청 Method 입니다. + /// HTTP Method : DELETE + /// - Parameters: + /// - memberId (탈퇴할 회원 ID) + /// - Returns : AccountResignResponseDTO + func deleteMember(memberId: String) -> Observable { + let spec = MembersAPIs.deleteMember(memberId: memberId).spec + return request(spec) } - public func createProfileImagePresingedURL(parameters: Encodable) -> Single { - let spec = MembersAPIs.profileAlbumUploadImageURL.spec - let accessToken: String = App.Repository.token.accessToken.value?.accessToken ?? "" - let headers = BibbiHeader.commonHeaders(accessToken: accessToken) - return request(spec: spec, headers: headers, jsonEncodable: parameters) - .subscribe(on: Self.queue) - .map(CameraDisplayImageResponseDTO.self) - .catchAndReturn(nil) - .asSingle() + /// 사용자를 콕 찌리기 위한 API 요청 Method 입니다. + /// HTTP Method : POST + /// - Parameters : memberId (콕 찌를 회원 ID) + /// - Returns :PickResponseDTO + func createMemberPick(memberId: String) -> Observable { + let spec = MembersAPIs.createMemberPick(memberId: memberId).spec + + return request(spec) } - public func uploadToProfilePresingedURL(toURL url: String, with imageData: Data) -> Single { - let spec = MembersAPIs.profileUploadToPreSignedURL(url).spec - let accessToken: String = App.Repository.token.accessToken.value?.accessToken ?? "" - let headers = BibbiHeader.commonHeaders(accessToken: accessToken) - return upload(spec: spec, headers: headers, image: imageData) - .subscribe(on: Self.queue) - .catchAndReturn(false) - .map { _ in true } + /// 회읜 프로필 이미지 업로드를 하기 위한 Presigned-URL API 요청 Method + /// HTTP Method : POST + /// - Returns : CreateMemberPresignedURLResponseDTO + func createMemberPresignedURL(body: CreateMemberPresignedURLRequestDTO) -> Observable { + let spec = MembersAPIs.createMemberPresignedURL(body: body).spec + + return request(spec) } - public func updateProfileAlbumImageToS3(memberId: String, parameter: Encodable) -> Single { - let spec = MembersAPIs.profileEditImage(memberId).spec - let accessToken: String = App.Repository.token.accessToken.value?.accessToken ?? "" - let headers = BibbiHeader.commonHeaders(accessToken: accessToken) - return request(spec: spec, headers: headers, jsonEncodable: parameter) - .subscribe(on: Self.queue) - .map(MembersProfileResponseDTO.self) - .catchAndReturn(nil) - .asSingle() + /// 회원 프로필 이름을 수정하기 위한 Method + /// HTTP Method : PUT + /// - Parameters : + /// - memberId (수정할 회원 ID) + /// - UpdateMemberNameRequestDTO (변경할 이름) + func updateMemberName(memberId: String, body: UpdateMemberNameRequestDTO) -> Observable { + let spec = MembersAPIs.updateMemberName(memberId: memberId, body: body).spec + + return request(spec) } - public func deleteProfileImageToS3(memberId: String) -> Single { - let spec = MembersAPIs.profileDeleteImage(memberId).spec - return request(spec: spec) - .subscribe(on: Self.queue) - .map(MembersProfileResponseDTO.self) - .catchAndReturn(nil) - .asSingle() + /// 사용자 프로필 이미지를 변경 하기 위한 Method 입니다. + /// HTTP Method : PUT + /// - Parameters : + /// - memberId (수정할 회원 ID) + /// - + ///- Returns : MembersProfileResponseDTO + func updateMemberProfileImage(memberId: String, body: UpdateMemberImageRequestDTO) -> Observable { + let spec = MembersAPIs.updateMemberProfileImage(memberId: memberId, body: body).spec + + return request(spec) } - public func deleteAccountUser(memberId: String, body: Encodable) -> Single { - let spec = ResignAPIs.accountResign(memberId).spec + /// 사용자 프로필 이미지를 삭제 하기 위한 Method 입니다. + /// HTTP Method : DELETE + /// - Parameters : memberId (프로필 이미지를 삭제할 회원 ID) + /// - Returns : MembersProfileResponseDTO + func deleteMemberProfileImage(memberId: String) -> Observable { + let spec = MembersAPIs.deleteMemberProfileImage(memberId: memberId).spec - return request(spec: spec, jsonEncodable: body) - .subscribe(on: Self.queue) - .map(AccountResignResponseDTO.self) - .catchAndReturn(nil) - .asSingle() + return request(spec) } + /// 프로필 이미지를 S3 Bucket에 업로드 하기 위한 Method 입니다. + /// HTTP Method : PUT + /// - Parameters + /// - presignedURL : 서버에서 발급 받은 Presigned-URL + /// - image : Image Data Type + /// - Returns : 업로드 성공 여부 확인 (Bool) Type + func updateS3MemberImageUpload(_ presignedURL: String, image: Data) -> Observable { + return upload(presignedURL, with: image) + } } diff --git a/14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/MembersAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/MembersAPIs.swift index 31754f3ba..130ea0f70 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/MembersAPIs.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/MembersAPIs.swift @@ -9,35 +9,47 @@ import Foundation import Core -enum MembersAPIs: API { - case profileMember(String) - case profilePost - case profileAlbumUploadImageURL - case profileUploadToPreSignedURL(String) - case profileEditImage(String) - case profileDeleteImage(String) - case accountResign(String) + + +enum MembersAPIs: BBAPI { + /// 회원 프로필 조회 API + case fetchMember(memberId: String) + /// 회원 탈퇴 API + case deleteMember(memberId: String) + /// 회원 콕 찌르기 API + case createMemberPick(memberId: String) + /// 회원 프로필 Presigend URL 요청 API + case createMemberPresignedURL(body: CreateMemberPresignedURLRequestDTO) + /// 회원 이름 수정 API + case updateMemberName(memberId: String, body: UpdateMemberNameRequestDTO) + /// 회원 프로필 이미지 수정 API + case updateMemberProfileImage(memberId: String, body: UpdateMemberImageRequestDTO) + /// 회원 프로필 이미지 삭제 API + case deleteMemberProfileImage(memberId: String) - var spec: APISpec { + + var spec: Spec { switch self { - case let .profileMember(memberId): - return APISpec(method: .get, url: "\(BibbiAPI.hostApi)/members/\(memberId)") - case .profilePost: - return APISpec(method: .get, url: "\(BibbiAPI.hostApi)/posts") - case .profileAlbumUploadImageURL: - return APISpec(method: .post, url: "\(BibbiAPI.hostApi)/members/image-upload-request") - case let .profileUploadToPreSignedURL(url): - return APISpec(method: .put, url: url) - case let .profileEditImage(memberId): - return APISpec(method: .put, url: "\(BibbiAPI.hostApi)/members/profile-image-url/\(memberId)") - case let .profileDeleteImage(memberId): - return APISpec(method: .delete, url: "\(BibbiAPI.hostApi)/members/profile-image-url/\(memberId)") - case let .accountResign(memberId): - return APISpec(method: .delete, url: "\(BibbiAPI.hostApi)/members/\(memberId)") - + case let .fetchMember(memberId): + return Spec(method: .get, path: "/members/\(memberId)") + case let .deleteMember(memberId): + return Spec(method: .delete, path: "/members/\(memberId)") + case let .createMemberPick(memberId): + return Spec(method: .post, path: "/members/\(memberId)/pick") + case let .createMemberPresignedURL(body): + return Spec(method: .post, path: "/members/image-upload-request", bodyParametersEncodable: body) + case let .updateMemberName(memberId, body): + return Spec(method: .put, path: "/members/name/\(memberId)", bodyParametersEncodable: body) + case let .updateMemberProfileImage(memberId, body): + return Spec(method: .put, path: "/members/profile-image-url/\(memberId)", bodyParametersEncodable: body) + case let .deleteMemberProfileImage(memberId): + return Spec(method: .delete, path: "/members/profile-image-url/\(memberId)") } } + final class Worker: BBRxAPIWorker { + init() { } + } } diff --git a/14th-team5-iOS/Data/Sources/APIs/Members/Repositories/MembersRepository.swift b/14th-team5-iOS/Data/Sources/APIs/Members/Repositories/MembersRepository.swift index f20744236..8e5447bb5 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Members/Repositories/MembersRepository.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Members/Repositories/MembersRepository.swift @@ -26,29 +26,57 @@ public final class MembersRepository { extension MembersRepository: MembersRepositoryProtocol { - public func fetchProfileMemberItems(memberId: String) -> Single { - return membersAPIWorker.fetchProfileMember(memberId: memberId) + public func fetchProfileMemberItem(memberId: String) -> Observable { + return membersAPIWorker.fetchMember(memberId: memberId) .map { $0?.toDomain() } - .catchAndReturn(nil) } - public func updataProfileImageToS3(memberId: String, parameter: ProfileImageEditParameter) -> Single { - return membersAPIWorker.updateProfileAlbumImageToS3(memberId: memberId, parameter: parameter) + public func updateMemberNameItem(memberId: String, body: UpdateMemberNameRequest) -> Observable { + let body = UpdateMemberNameRequestDTO(name: body.name) + + return membersAPIWorker.updateMemberName(memberId: memberId, body: body) + .map { $0?.toDomain() } + } + + public func updateMemberProfileImageItem(memberId: String, body: UpdateMemberImageRequest) -> Observable { + let body = UpdateMemberImageRequestDTO(profileImageUrl: body.profileImageUrl) + + return membersAPIWorker.updateMemberProfileImage(memberId: memberId, body: body) .do { guard let userEntity = $0?.toProfileEntity() else { return } self.familyUserDefaults.updateFamilyMember(userEntity) } .map { $0?.toDomain() } - .catchAndReturn(nil) } - public func deleteProfileImageToS3(memberId: String) -> Single { - return membersAPIWorker.deleteProfileImageToS3(memberId: memberId) + public func createMemberPickItem(memberId: String) -> Observable { + return membersAPIWorker.createMemberPick(memberId: memberId) + .map { $0?.toDomain() } + } + + public func creteMemberImagePresignedURL(body: CreateMemberPresignedReqeust) -> Observable { + let body = CreateMemberPresignedURLRequestDTO(imageName: body.imageName) + + return membersAPIWorker.createMemberPresignedURL(body: body) + .map { $0?.toDomain() } + } + + public func deleteMemberProfileImageItem(memberId: String) -> Observable { + return membersAPIWorker.deleteMemberProfileImage(memberId: memberId) .do { guard let userEntity = $0?.toProfileEntity() else { return } self.familyUserDefaults.updateFamilyMember(userEntity) } .map { $0?.toDomain() } - .catchAndReturn(nil) + } + + public func deleteMemberItem(memberId: String) -> Observable { + + return membersAPIWorker.deleteMember(memberId: memberId) + .map { $0?.toDomain() } + } + + public func uploadMemberImageToS3Bucket(_ presignedURL: String, image: Data) -> Observable { + return membersAPIWorker.updateS3MemberImageUpload(presignedURL, image: image) } } diff --git a/14th-team5-iOS/Data/Sources/Trash/Mission/DataMapping/MissionContentResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Mission/MissionAPI/DataMapping/MissionContentResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Trash/Mission/DataMapping/MissionContentResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Mission/MissionAPI/DataMapping/MissionContentResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/Mission/MissionAPI/DataMapping/MissonTodayContentResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Mission/MissionAPI/DataMapping/MissonTodayContentResponseDTO.swift new file mode 100644 index 000000000..773c54b63 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/Mission/MissionAPI/DataMapping/MissonTodayContentResponseDTO.swift @@ -0,0 +1,34 @@ +// +// MissonTodayContentResponseDTO.swift +// Data +// +// Created by 김도현 on 11/25/24. +// + +import Foundation + +import Domain + +struct MissonTodayContentResponseDTO: Decodable { + let date: String + let missionId: String + let missionContent: String + + + enum CodingKeys: String, CodingKey { + case date + case missionId = "id" + case missionContent = "content" + } +} + + +extension MissonTodayContentResponseDTO { + public func toDomain() -> MissonTodayContentEntity { + return .init( + missionId: missionId, + missionContent: missionContent, + date: date + ) + } +} diff --git a/14th-team5-iOS/Data/Sources/Trash/Mission/MissionAPI/MissionAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/Mission/MissionAPI/MissionAPIWorker.swift similarity index 93% rename from 14th-team5-iOS/Data/Sources/Trash/Mission/MissionAPI/MissionAPIWorker.swift rename to 14th-team5-iOS/Data/Sources/APIs/Mission/MissionAPI/MissionAPIWorker.swift index add212220..b15c78f6d 100644 --- a/14th-team5-iOS/Data/Sources/Trash/Mission/MissionAPI/MissionAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Mission/MissionAPI/MissionAPIWorker.swift @@ -29,7 +29,7 @@ extension MissionAPIWorker { /// 미션 일일 정보를 조회하기 위한 API 입니다. /// HTTP Method : GET /// - Returns : CameraTodayMissionResponseDTO - func fetchDailyMisson() -> Observable { + func fetchDailyMisson() -> Observable { let spec = MissionAPIs.fetchDailyMisson.spec return request(spec) diff --git a/14th-team5-iOS/Data/Sources/Trash/Mission/MissionAPI/MissionAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/Mission/MissionAPI/MissionAPIs.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Trash/Mission/MissionAPI/MissionAPIs.swift rename to 14th-team5-iOS/Data/Sources/APIs/Mission/MissionAPI/MissionAPIs.swift diff --git a/14th-team5-iOS/Data/Sources/Trash/Mission/Repository/MissionRepository.swift b/14th-team5-iOS/Data/Sources/APIs/Mission/Repository/MissionRepository.swift similarity index 95% rename from 14th-team5-iOS/Data/Sources/Trash/Mission/Repository/MissionRepository.swift rename to 14th-team5-iOS/Data/Sources/APIs/Mission/Repository/MissionRepository.swift index e0e60f3c9..6adbc53eb 100644 --- a/14th-team5-iOS/Data/Sources/Trash/Mission/Repository/MissionRepository.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Mission/Repository/MissionRepository.swift @@ -29,7 +29,7 @@ extension MissionRepository { .map { $0?.toDomain() } } - public func fetchDailyMissonItem() -> Observable { + public func fetchDailyMissonItem() -> Observable { return missionAPIWorker.fetchDailyMisson() .map { $0?.toDomain() } } diff --git a/14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/DataMapping/PickMemberListResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/DataMapping/PickMemberListResponseDTO.swift deleted file mode 100644 index 217ca268c..000000000 --- a/14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/DataMapping/PickMemberListResponseDTO.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// PickMemberListResponse.swift -// Data -// -// Created by 김건우 on 4/15/24. -// - -import Domain -import Foundation - -public struct PickMemberListResponseDTO: Decodable { - enum CodingKeys: String, CodingKey { - case results - } - public var results: [PickMemberDTO] -} - -extension PickMemberListResponseDTO { - public struct PickMemberDTO: Decodable { - enum CodingKeys: String, CodingKey { - case memberId - case name - case imageUrl - case familyId - case familyJoinAt - case dayOfBirth - } - public var memberId: String - public var name: String - public var imageUrl: String - public var familyId: String - public var familyJoinAt: String - public var dayOfBirth: String - } -} - -extension PickMemberListResponseDTO { - func toDomain() -> PickMemberListEntity { - return .init(results: results.map { $0.toDomain() }) - } -} - -extension PickMemberListResponseDTO.PickMemberDTO { - func toDomain() -> PickMemberEntity { - return .init( - memberId: memberId, - name: name, - imageUrl: imageUrl, - familyId: familyId, - familyJoinedAt: familyJoinAt.toDate(with: .dashYyyyMMdd), - dayOfBirth: dayOfBirth.toDate(with: .dashYyyyMMdd) - ) - } -} diff --git a/14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/DataMapping/PickResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/DataMapping/PickResponseDTO.swift deleted file mode 100644 index 9f49ef6f4..000000000 --- a/14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/DataMapping/PickResponseDTO.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// PickResponseDTO.swift -// Data -// -// Created by 김건우 on 4/15/24. -// - -import Domain -import Foundation - -public struct PickResponseDTO: Decodable { - enum CodingKeys: String, CodingKey { - case success - } - public var success: Bool -} - -extension PickResponseDTO { - func toDomain() -> PickEntity { - return .init(success: success) - } -} diff --git a/14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/PickAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/PickAPIWorker.swift deleted file mode 100644 index 44cb4a698..000000000 --- a/14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/PickAPIWorker.swift +++ /dev/null @@ -1,107 +0,0 @@ -// -// PickAPIWorker.swift -// Data -// -// Created by 김건우 on 4/15/24. -// - -import Core -import Domain -import Foundation - -import RxSwift - -typealias PickAPIWorker = PickAPIs.Worker -extension PickAPIs { - public final class Worker: APIWorker { - static let queue = { - ConcurrentDispatchQueueScheduler(queue: DispatchQueue(label: "PickAPIQueue", qos: .utility)) - }() - - override public init() { - super.init() - self.id = "PickAPIWorker" - } - } -} - - -// MARK: - Extensions - -extension PickAPIWorker { - - - // MARK: - Pick Member - - private func pickMember(spec: APISpec, headers: [APIHeader]?) -> Single { - return request(spec: spec, headers: headers) - .subscribe(on: Self.queue) - .map(PickResponseDTO.self) - .catchAndReturn(nil) - .map { $0?.toDomain() } - .asSingle() - } - - public func pickMember(memberId: String) -> Single { - let spec = PickAPIs.pick(memberId).spec - - return Observable.just(()) - .withLatestFrom(self._headers) - .observe(on: Self.queue) - .withUnretained(self) - .flatMap { $0.0.pickMember(spec: spec, headers: $0.1) } - .asSingle() - } - - - - // MARK: - Who Did I Pick? - - private func fetchWhoDidIPickMember(spec: APISpec, headers: [APIHeader]?) -> Single { - return request(spec: spec, headers: headers) - .subscribe(on: Self.queue) - .map(PickMemberListResponseDTO.self) - .catchAndReturn(nil) - .map { $0?.toDomain() } - .asSingle() - } - - public func fetchWhoDidIPickMember(memberId: String) -> Single { - let spec = PickAPIs.whoDidIPick(memberId).spec - - return Observable.just(()) - .withLatestFrom(self._headers) - .observe(on: Self.queue) - .withUnretained(self) - .flatMap { $0.0.fetchWhoDidIPickMember(spec: spec, headers: $0.1) } - .asSingle() - } - - - - // MARK: - Who Picked Me? - - private func fetchWhoPickedMeMember(spec: APISpec, headers: [APIHeader]?) -> Single { - return request(spec: spec, headers: headers) - .subscribe(on: Self.queue) - .map(PickMemberListResponseDTO.self) - .catchAndReturn(nil) - .map { $0?.toDomain() } - .asSingle() - } - - public func fetchWhoPickedMeMember(memberId: String) -> Single { - let spec = PickAPIs.whoPickedMe(memberId).spec - - return Observable.just(()) - .withLatestFrom(self._headers) - .observe(on: Self.queue) - .withUnretained(self) - .flatMap { $0.0.fetchWhoPickedMeMember(spec: spec, headers: $0.1) } - .asSingle() - } - - - - -} diff --git a/14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/PickAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/PickAPIs.swift deleted file mode 100644 index e71844518..000000000 --- a/14th-team5-iOS/Data/Sources/APIs/Pick/PickAPI/PickAPIs.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// PickAPIs.swift -// Data -// -// Created by 김건우 on 4/15/24. -// - -import Core -import Foundation - -enum PickAPIs: API { - case pick(String) - case whoDidIPick(String) - case whoPickedMe(String) - - var spec: APISpec { - switch self { - case let .pick(memberId): - return APISpec(method: .post, url: "\(BibbiAPI.hostApi)/members/\(memberId)/pick") - case let .whoDidIPick(memberId): - return APISpec(method: .get, url: "\(BibbiAPI.hostApi)/members/\(memberId)/picked") - case let .whoPickedMe(memberId): - return APISpec(method: .get, url: "\(BibbiAPI.hostApi)/members/\(memberId)/pick") - } - } -} diff --git a/14th-team5-iOS/Data/Sources/APIs/Pick/Repository/PickRepository.swift b/14th-team5-iOS/Data/Sources/APIs/Pick/Repository/PickRepository.swift deleted file mode 100644 index 8d42c0ddf..000000000 --- a/14th-team5-iOS/Data/Sources/APIs/Pick/Repository/PickRepository.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// PickRepository.swift -// Data -// -// Created by 김건우 on 4/15/24. -// - -import Core -import Domain -import Foundation - -import RxSwift - -public final class PickRepository: PickRepositoryProtocol { - public let disposeBag: DisposeBag = DisposeBag() - - private let pickApiWorker: PickAPIWorker = PickAPIWorker() - - public init() { } -} - -extension PickRepository { - public func pickMember(memberId: String) -> Observable { - return pickApiWorker.pickMember(memberId: memberId) - .asObservable() - } - - public func whoDidIPickMember(memberId: String) -> Observable { - return pickApiWorker.fetchWhoDidIPickMember(memberId: memberId) - .asObservable() - } - - public func whoPickedMeMember(memberId: String) -> Observable { - return pickApiWorker.fetchWhoPickedMeMember(memberId: memberId) - .asObservable() - } -} diff --git a/14th-team5-iOS/Data/Sources/APIs/Post/PostAPI/DataMapping/CreatePostPresignedURLReqeustDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Post/PostAPI/DataMapping/CreatePostPresignedURLReqeustDTO.swift new file mode 100644 index 000000000..d60d52a18 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/Post/PostAPI/DataMapping/CreatePostPresignedURLReqeustDTO.swift @@ -0,0 +1,13 @@ +// +// CreatePostPresignedURLReqeustDTO.swift +// Data +// +// Created by 김도현 on 11/22/24. +// + +import Foundation + + +struct CreatePostPresignedURLReqeustDTO: Encodable { + let imageName: String +} diff --git a/14th-team5-iOS/Data/Sources/APIs/Post/PostAPI/DataMapping/CreatePostPresignedURLResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Post/PostAPI/DataMapping/CreatePostPresignedURLResponseDTO.swift new file mode 100644 index 000000000..d4c5121f8 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/Post/PostAPI/DataMapping/CreatePostPresignedURLResponseDTO.swift @@ -0,0 +1,25 @@ +// +// CreatePostPresignedURLResponseDTO.swift +// Data +// +// Created by 김도현 on 11/22/24. +// + +import Foundation + +import Domain + + +struct CreatePostPresignedURLResponseDTO: Decodable { + let imageURL: String? + + enum CodingKeys: String, CodingKey { + case imageURL = "url" + } +} + +extension CreatePostPresignedURLResponseDTO { + public func toDomain() -> CreatePostPresignedURLEntity { + return .init(imageURL: imageURL ?? "") + } +} diff --git a/14th-team5-iOS/Data/Sources/Trash/Post/PostList/DataMapping/CreatePostRequestDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Post/PostAPI/DataMapping/CreatePostRequestDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Trash/Post/PostList/DataMapping/CreatePostRequestDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Post/PostAPI/DataMapping/CreatePostRequestDTO.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/Post/PostAPI/DataMapping/CreatePostResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Post/PostAPI/DataMapping/CreatePostResponseDTO.swift new file mode 100644 index 000000000..91d9229c0 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/Post/PostAPI/DataMapping/CreatePostResponseDTO.swift @@ -0,0 +1,40 @@ +// +// CreatePostResponseDTO.swift +// Data +// +// Created by 김도현 on 11/22/24. +// + +import Foundation + +import Domain + + +struct CreatePostResponseDTO: Decodable { + let postId: String? + let authorId: String? + let type: String? + let missionId: String? + let commentCount: Int? + let emojiCount: Int? + let imageUrl: String? + let content: String? + let createdAt: String +} + + +extension CreatePostResponseDTO { + public func toDomain() -> CreatePostEntity { + return .init( + postId: postId ?? "", + authorId: authorId ?? "", + commentCount: commentCount ?? 0, + missionType: type ?? "SURVIVAL", + missionId: missionId ?? "", + emojiCount: emojiCount ?? 0, + imageURL: imageUrl ?? "", + content: content ?? "", + createdAt: createdAt + ) + } +} diff --git a/14th-team5-iOS/Data/Sources/Trash/Post/PostList/DataMapping/CreatePresignedURLRequestDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Post/PostAPI/DataMapping/CreatePresignedURLRequestDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Trash/Post/PostList/DataMapping/CreatePresignedURLRequestDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Post/PostAPI/DataMapping/CreatePresignedURLRequestDTO.swift diff --git a/14th-team5-iOS/Data/Sources/Trash/Post/PostList/DataMapping/PostDetailResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Post/PostAPI/DataMapping/PostDetailResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Trash/Post/PostList/DataMapping/PostDetailResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Post/PostAPI/DataMapping/PostDetailResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/Trash/Post/PostList/DataMapping/PostListQueryDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Post/PostAPI/DataMapping/PostListQueryDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Trash/Post/PostList/DataMapping/PostListQueryDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Post/PostAPI/DataMapping/PostListQueryDTO.swift diff --git a/14th-team5-iOS/Data/Sources/Trash/Post/PostList/DataMapping/PostListResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Post/PostAPI/DataMapping/PostListResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/Trash/Post/PostList/DataMapping/PostListResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Post/PostAPI/DataMapping/PostListResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/Trash/Post/PostList/PostAPI/PostAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/Post/PostAPI/PostAPIs.swift similarity index 94% rename from 14th-team5-iOS/Data/Sources/Trash/Post/PostList/PostAPI/PostAPIs.swift rename to 14th-team5-iOS/Data/Sources/APIs/Post/PostAPI/PostAPIs.swift index 0567cccc4..24b768d54 100644 --- a/14th-team5-iOS/Data/Sources/Trash/Post/PostList/PostAPI/PostAPIs.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Post/PostAPI/PostAPIs.swift @@ -17,7 +17,8 @@ enum PostsAPIs: BBAPI { /// 게시물 단일 조회 API case fetchPostDetail(postId: String) /// 게시물 사진 Presigned URL 요청 API - case createPostPresignedURL(body: CreatePresignedURLRequestDTO) + case createPostPresignedURL(body: CreatePostPresignedURLReqeustDTO) + var spec: Spec { switch self { diff --git a/14th-team5-iOS/Data/Sources/Trash/Post/PostList/PostAPI/PostListAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/Post/PostAPI/PostListAPIWorker.swift similarity index 66% rename from 14th-team5-iOS/Data/Sources/Trash/Post/PostList/PostAPI/PostListAPIWorker.swift rename to 14th-team5-iOS/Data/Sources/APIs/Post/PostAPI/PostListAPIWorker.swift index 3f84eec16..5b8e38b19 100644 --- a/14th-team5-iOS/Data/Sources/Trash/Post/PostList/PostAPI/PostListAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Post/PostAPI/PostListAPIWorker.swift @@ -43,12 +43,12 @@ extension PostAPIWorker { /// 게시물 생성을 하기 위한 Method 입니다. /// HTTP Method : POST /// - Parameters : - /// - query : CreateFeedQuery - /// - body : CameraFeedRequestDTO + /// - query : CreatePostQuery + /// - body : CreatePostRequestDTO /// - type: String /// - available : Bool - /// - Returns : CameraDisplayPostResponseDTO - func createPost(query: CreatePostQuery, body: CreatePostRequestDTO) -> Observable { + /// - Returns : CreatePostResponseDTO + func createPost(query: CreatePostQuery, body: CreatePostRequestDTO) -> Observable { let spec = PostsAPIs.createPost(type: query.type, body: body).spec return request(spec) @@ -56,13 +56,23 @@ extension PostAPIWorker { /// 게시믈 이미지 업로드를 하기 위한 Presigend-URL API 요청 Method 입니다 /// HTTP Method : POST - /// - Returns : CameraDisplayImageResponseDTO - func createPostPresignedURL(body: CreatePresignedURLRequestDTO) -> Observable { + /// - Returns : CreatePostPresignedURLResponseDTO + func createPostPresignedURL(body: CreatePostPresignedURLReqeustDTO) -> Observable { let spec = PostsAPIs.createPostPresignedURL(body: body).spec return request(spec) } + /// 피드 이미지를 S3 Bucket에 업로드 하기 위한 Method 입니다. + /// HTTP Method : PUT + /// - Parameters + /// - presignedURL : 서버에서 발급 받은 Presigned-URL + /// - image : Image Data Type + /// - Returns : 업로드 성공 여부 확인 (Bool) Type + func updateS3PostImageUpload(_ presignedURL: String, image: Data) -> Observable { + return upload(presignedURL, with: image) + } + } diff --git a/14th-team5-iOS/Data/Sources/Trash/Post/PostList/Repository/PostRepository.swift b/14th-team5-iOS/Data/Sources/APIs/Post/Repository/PostRepository.swift similarity index 75% rename from 14th-team5-iOS/Data/Sources/Trash/Post/PostList/Repository/PostRepository.swift rename to 14th-team5-iOS/Data/Sources/APIs/Post/Repository/PostRepository.swift index 7c27289b1..04cf64dab 100644 --- a/14th-team5-iOS/Data/Sources/Trash/Post/PostList/Repository/PostRepository.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Post/Repository/PostRepository.swift @@ -11,7 +11,7 @@ import Domain import RxSwift public final class PostRepository: PostListRepositoryProtocol { - + private let disposeBag: DisposeBag = DisposeBag() private let postAPIWorker: PostAPIWorker = PostAPIWorker() @@ -38,16 +38,20 @@ extension PostRepository { .map { $0?.toDomain() } } - public func createPostItem(query: CreatePostQuery, body: CreatePostRequest) -> Observable { + public func createPostItem(query: CreatePostQuery, body: CreatePostRequest) -> Observable { let body = CreatePostRequestDTO(imageUrl: body.imageUrl, content: body.content, uploadTime: body.uploadTime) return postAPIWorker.createPost(query: query, body: body) - .map { $0.toDomain() } + .map { $0?.toDomain() } } - public func createPostPresignedURLItem(body: CreatePostPresignedURLRequest) -> Observable { - let body = CreatePresignedURLRequestDTO(imageName: body.imageName) + public func createPostPresignedURLItem(body: CreatePostPresignedURLRequest) -> Observable { + let body = CreatePostPresignedURLReqeustDTO(imageName: body.imageName) return postAPIWorker.createPostPresignedURL(body: body) - .map { $0.toDomain() } + .map { $0?.toDomain() } + } + + public func uploadPostImageToS3Bucket(_ presignedURL: String, image: Data) -> Observable { + return postAPIWorker.updateS3PostImageUpload(presignedURL, image: image) } } diff --git a/14th-team5-iOS/Data/Sources/APIs/Resign/DataMapping/AccountResignResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Resign/DataMapping/AccountResignResponseDTO.swift deleted file mode 100644 index e3e66936d..000000000 --- a/14th-team5-iOS/Data/Sources/APIs/Resign/DataMapping/AccountResignResponseDTO.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// AccountResignResponseDTO.swift -// Domain -// -// Created by Kim dohyun on 1/2/24. -// - -import Foundation -import Domain - -public struct AccountResignResponseDTO: Decodable { - public var success: Bool -} - -extension AccountResignResponseDTO { - public func toDomain() -> AccountResignEntity { - return .init(isSuccess: success) - } -} diff --git a/14th-team5-iOS/Data/Sources/APIs/Resign/Repository/AccountResignRepository.swift b/14th-team5-iOS/Data/Sources/APIs/Resign/Repository/AccountResignRepository.swift deleted file mode 100644 index c04a8fe57..000000000 --- a/14th-team5-iOS/Data/Sources/APIs/Resign/Repository/AccountResignRepository.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// AccountResignRepository.swift -// Data -// -// Created by Kim dohyun on 9/12/24. -// - -import Domain -import Foundation - -import RxSwift - -public final class AccountResignRepository: AccountResignRepositoryProtocol { - - public let disposeBag: DisposeBag = DisposeBag() - private let resignApiWorker: ResignAPIWorker = ResignAPIWorker() - public init() { } -} - -extension AccountResignRepository { - - public func deleteAccountResignItem() -> Observable { - let myUserDefaults = MyUserDefaults() - let currentMemberId = myUserDefaults.loadMemberId() ?? "NONE" - - return resignApiWorker.resignUser(memberId: currentMemberId) - .compactMap { $0?.toDomain() } - .asObservable() - - } -} diff --git a/14th-team5-iOS/Data/Sources/APIs/Resign/ResignAPI/ResignAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/Resign/ResignAPI/ResignAPIWorker.swift deleted file mode 100644 index de5345b67..000000000 --- a/14th-team5-iOS/Data/Sources/APIs/Resign/ResignAPI/ResignAPIWorker.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// ResignAPIWorker.swift -// Data -// -// Created by Kim dohyun on 1/2/24. -// - -import Core -import Foundation - -import Alamofire -import Domain -import RxSwift - - -typealias ResignAPIWorker = ResignAPIs.Worker - -extension ResignAPIs { - final class Worker: APIWorker { - static let queue = { - ConcurrentDispatchQueueScheduler(queue: DispatchQueue(label: "ResignAPIQueue", qos: .utility)) - }() - - override init() { - super.init() - self.id = "ResignAPIWorker" - } - } -} - - -extension ResignAPIWorker { - - public func resignUser(memberId: String) -> Single { - let spec = ResignAPIs.accountResign(memberId).spec - - return request(spec: spec) - .subscribe(on: Self.queue) - .map(AccountResignResponseDTO.self) - .catchAndReturn(nil) - .asSingle() - } -} diff --git a/14th-team5-iOS/Data/Sources/APIs/Resign/ResignAPI/ResignAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/Resign/ResignAPI/ResignAPIs.swift deleted file mode 100644 index ba57e42ba..000000000 --- a/14th-team5-iOS/Data/Sources/APIs/Resign/ResignAPI/ResignAPIs.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// ResignAPIs.swift -// Data -// -// Created by Kim dohyun on 1/2/24. -// - -import Core -import Foundation - - -enum ResignAPIs: API { - case accountResign(String) - case accountFcmResign(String) - - var spec: APISpec { - switch self { - case let .accountResign(memeberId): - return APISpec(method: .delete, url: "\(BibbiAPI.hostApi)/members/\(memeberId)") - case let .accountFcmResign(token): - return APISpec(method: .delete, url: "\(BibbiAPI.hostApi)/me/fcm/\(token)") - } - } - -} diff --git a/14th-team5-iOS/Data/Sources/Trash/Account/AccountAPI/AccountAPIWorker.swift b/14th-team5-iOS/Data/Sources/Trash/Account/AccountAPI/AccountAPIWorker.swift index 7bc0b864a..069cfc28f 100644 --- a/14th-team5-iOS/Data/Sources/Trash/Account/AccountAPI/AccountAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Trash/Account/AccountAPI/AccountAPIWorker.swift @@ -68,16 +68,6 @@ extension AccountAPIWorker { .asSingle() } - func updateProfileNickName(accessToken: String, memberId: String, parameter: Encodable) -> Single { - let spec = AccountAPIs.profileNickNameEdit(memberId).spec - - return request(spec: spec, headers: [BibbiAPI.Header.xAppKey, BibbiAPI.Header.xAuthToken(accessToken), BibbiAPI.Header.acceptJson], jsonEncodable: parameter) - .subscribe(on: Self.queue) - .map(AccountNickNameEditDTO.self) - .catchAndReturn(nil) - .asSingle() - } - func accountRefreshToken(parameter: Encodable) -> Single { let spec = AccountAPIs.refreshToken.spec print("RefreshToken: \(parameter)") diff --git a/14th-team5-iOS/Data/Sources/Trash/Account/AccountRepository/AccountRepository.swift b/14th-team5-iOS/Data/Sources/Trash/Account/AccountRepository/AccountRepository.swift index edb2e95ee..3fbf36d3d 100644 --- a/14th-team5-iOS/Data/Sources/Trash/Account/AccountRepository/AccountRepository.swift +++ b/14th-team5-iOS/Data/Sources/Trash/Account/AccountRepository/AccountRepository.swift @@ -23,10 +23,7 @@ public protocol AccountImpl: AnyObject { func kakaoLogin(with snsType: SNS, vc: UIViewController) -> Observable func appleLogin(with snsType: SNS, vc: UIViewController) -> Observable - func executeNicknameUpdate(memberId: String, parameter: AccountNickNameEditParameter) -> Observable func signUp(name: String, date: String, photoURL: String?) -> Observable - func executePresignedImageURLCreate(parameter: CameraDisplayImageParameters) -> Observable - func executeProfileImageUpload(to url: String, data: Data) -> Observable } public final class AccountRepository: AccountImpl { @@ -118,22 +115,6 @@ public final class AccountRepository: AccountImpl { } } - public func executeNicknameUpdate(memberId: String, parameter: AccountNickNameEditParameter) -> Observable { - return apiWorker.updateProfileNickName(accessToken: accessToken, memberId: memberId, parameter: parameter) - .compactMap { $0?.toDomain() } - .asObservable() - } - - public func executePresignedImageURLCreate(parameter: CameraDisplayImageParameters) -> Observable { - return profileWorker.createProfileImagePresingedURL(parameters: parameter) - .compactMap { $0?.toDomain() } - .asObservable() - } - - public func executeProfileImageUpload(to url: String, data: Data) -> Observable { - return profileWorker.uploadToProfilePresingedURL(toURL: url, with: data) - .asObservable() - } private func saveMemberInfo(_ memberInfo: MemberInfo?) { diff --git a/14th-team5-iOS/Data/Sources/Trash/Member/Repository/MemberRepository.swift b/14th-team5-iOS/Data/Sources/Trash/Member/Repository/MemberRepository.swift index a45350fe4..77301bce2 100644 --- a/14th-team5-iOS/Data/Sources/Trash/Member/Repository/MemberRepository.swift +++ b/14th-team5-iOS/Data/Sources/Trash/Member/Repository/MemberRepository.swift @@ -16,21 +16,6 @@ public final class MemberRepository: MemberRepositoryProtocol { } extension MemberRepository { - public func fetchFamilyNameEditorId() -> String { - return familyUserDefaults.loadFamilyNameEditorId() ?? "알 수 없음" - } - - public func fetchUserName(memberId: String) -> String { - return familyUserDefaults.loadFamilyMember(memberId)?.name ?? "알 수 없음" - } - - public func fetchProfileImageUrlString(memberId: String) -> String { - return familyUserDefaults.loadFamilyMember(memberId)?.profileImageURL ?? .unknown - } - - public func checkIsMe(memberId: String) -> Bool { - return myUserDefaults.loadMemberId() == memberId - } public func checkIsValidMember(memberId: String) -> Bool { if let familyMembers = familyUserDefaults.loadFamilyMembers() { @@ -40,8 +25,4 @@ extension MemberRepository { } return false } - - public func fetchMyMemberId() -> String { - return myUserDefaults.loadMemberId() ?? .unknown - } } diff --git a/14th-team5-iOS/Data/Sources/Trash/UserDefaults/FamilyUserDefautls.swift b/14th-team5-iOS/Data/Sources/Trash/UserDefaults/FamilyUserDefautls.swift deleted file mode 100644 index 59775fefc..000000000 --- a/14th-team5-iOS/Data/Sources/Trash/UserDefaults/FamilyUserDefautls.swift +++ /dev/null @@ -1,136 +0,0 @@ -//// -//// FamilyDataRepository.swift -//// Core -//// -//// Created by 마경미 on 02.01.24. -//// -// -//import Foundation -//import Domain -// -//import RxSwift -//import RxCocoa -// -//@available(*, deprecated, renamed: "FamilyInfoUserDefaults", message: "FamilyInfoUserDefaults!!!!!!!!!!!! 쓰세요") -//public class FamilyUserDefaults { -// /// familyIdKey - familyId 저장 -// /// familyId - memberId를 배열로 저장 -// /// 각 memberId - familymember 객체 저장 -// -// private static let familyIdKey = "familyId" -// private static let familyEditorIdKey = "familyEditorId" -// private static let myMemberIdKey = "memberId" -// private static let memberIdsKey = "memberIds" -// private static let dayOfBirths = "dayOfBirths" -// -// private static var userDefaults: UserDefaults { -// UserDefaults.standard -// } -// -// public static func checkIsMyMemberId(memberId: String) -> Bool { -// return memberId == UserDefaults.standard.string(forKey: myMemberIdKey) -// } -// -// public static func returnMyMemberId() -> String { -// return UserDefaults.standard.string(forKey: myMemberIdKey) ?? "" -// } -// -// public static func removeFamilyMembers() { -// UserDefaults.standard.stringArray(forKey: memberIdsKey)?.forEach { -// UserDefaults.standard.removeObject(forKey: $0) -// print($0) -// } -// -// UserDefaults.standard.removeObject(forKey: memberIdsKey) -// } -// -// public static func saveMyMemberId(memberId: String) { -// UserDefaults.standard.setValue(memberId, forKey: myMemberIdKey) -// } -// -// public static func saveFamilyEditorId(familyEditorId: String) { -// UserDefaults.standard.setValue(familyEditorId, forKey: familyEditorIdKey) -// } -// -// public static func getMyMemberId() -> String { -// return UserDefaults.standard.string(forKey: myMemberIdKey) ?? "" -// } -// -// public static func getDateOfBirths() -> [Date] { -// guard let dateOfBirths = userDefaults.array( -// forKey: dayOfBirths -// ) as? [Date] else { -// return [] -// } -// return dateOfBirths -// } -// -// public static func getMemberCount() -> Int { -// return UserDefaults.standard.stringArray(forKey: myMemberIdKey)?.count ?? 0 -// } -// -// public static func saveFamilyMembers(_ familyMembers: [FamilyMemberProfileEntity]) { -// removeFamilyMembers() -// saveMemberIdToUserDefaults(memberIds: familyMembers.map { $0.memberId }) -// saveDayOfBirths(dateOfBirths: familyMembers.map { $0.dayOfBirth ?? Date() }) -// familyMembers.forEach { saveMemberToUserDefaults(familyMember: $0) } -// } -// -// public static func load(memberId: String) -> FamilyMemberProfileEntity? { -// if let data = UserDefaults.standard.data(forKey: memberId) { -// do { -// let decoder = JSONDecoder() -// let person = try decoder.decode(FamilyMemberProfileEntity.self, from: data) -// return person -// } catch { -// print("Error decoding person: \(error.localizedDescription)") -// } -// } -// return nil -// } -// -// public static func loadMemberIds() -> [String] { -// return userDefaults.array(forKey: memberIdsKey) as! [String] -// } -// -// public static func loadFamilyNameEditorId() -> String { -// return userDefaults.string(forKey: familyEditorIdKey) ?? "" -// } -//} -// -//extension FamilyUserDefaults { -// public static func saveMemberToUserDefaults(familyMember: FamilyMemberProfileEntity) { -// do { -// let encoder = JSONEncoder() -// let data = try encoder.encode(familyMember) -// -// UserDefaults.standard.set(data, forKey: familyMember.memberId) -// } catch { -// print("Error encoding person: \(error.localizedDescription)") -// } -// } -// -// private static func saveMemberIdToUserDefaults(memberIds: [String]) { -// UserDefaults.standard.setValue(memberIds, forKey: memberIdsKey) -// } -// -// private static func saveDayOfBirths(dateOfBirths: [Date]) { -// userDefaults.setValue(dateOfBirths, forKey: self.dayOfBirths) -// } -// -// static func loadMembersFromUserDefaults(memberIds: [String]) -> [FamilyMemberProfileEntity] { -// var datas: [FamilyMemberProfileEntity] = [] -// memberIds.forEach { -// if let data = UserDefaults.standard.data(forKey: $0) { -// do { -// let decoder = JSONDecoder() -// let data = try decoder.decode(FamilyMemberProfileEntity.self, from: data) -// return datas.append(data) -// } catch { -// print("Error decoding person: \(error.localizedDescription)") -// } -// } -// } -// return datas -// } -//} diff --git a/14th-team5-iOS/Domain/Sources/Entities/Camera/CameraMissionFeedQuery.swift b/14th-team5-iOS/Domain/Sources/Entities/Camera/CameraMissionFeedQuery.swift deleted file mode 100644 index f3328c49d..000000000 --- a/14th-team5-iOS/Domain/Sources/Entities/Camera/CameraMissionFeedQuery.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// CameraMissionFeedQuery.swift -// Domain -// -// Created by Kim dohyun on 5/2/24. -// - -import Foundation - - -public struct CameraMissionFeedQuery { - public var type: String - public var isUploded: Bool - - - public init(type: String, isUploded: Bool) { - self.type = type - self.isUploded = isUploded - } -} diff --git a/14th-team5-iOS/Domain/Sources/Entities/Camera/CameraTodayMssionEntity.swift b/14th-team5-iOS/Domain/Sources/Entities/Camera/CameraTodayMssionEntity.swift deleted file mode 100644 index b93dfa8d8..000000000 --- a/14th-team5-iOS/Domain/Sources/Entities/Camera/CameraTodayMssionEntity.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// CameraTodayMssionEntity.swift -// Domain -// -// Created by Kim dohyun on 6/14/24. -// - -import Foundation - - -public struct CameraTodayMssionEntity { - public let missionDate: Date - public let missionId: String - public let missionContent: String - - public init(missionDate: Date, missionId: String, missionContent: String) { - self.missionDate = missionDate - self.missionId = missionId - self.missionContent = missionContent - } -} diff --git a/14th-team5-iOS/Domain/Sources/Entities/Camera/CreateFeedQuery.swift b/14th-team5-iOS/Domain/Sources/Entities/Camera/CreateFeedQuery.swift deleted file mode 100644 index 96a434a04..000000000 --- a/14th-team5-iOS/Domain/Sources/Entities/Camera/CreateFeedQuery.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// CreateFeedQuery.swift -// Domain -// -// Created by 김도현 on 11/18/24. -// - -import Foundation - - -public struct CreateFeedQuery { - public let type: String - - public init(type: String) { - self.type = type - } -} diff --git a/14th-team5-iOS/Domain/Sources/Entities/Camera/CreateFeedRequest.swift b/14th-team5-iOS/Domain/Sources/Entities/Camera/CreateFeedRequest.swift deleted file mode 100644 index 400beea0b..000000000 --- a/14th-team5-iOS/Domain/Sources/Entities/Camera/CreateFeedRequest.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// CreateFeedRequest.swift -// Domain -// -// Created by 김도현 on 11/17/24. -// - -import Foundation - -public struct CreateFeedRequest { - public let imageUrl: String - public let content: String - public let uploadTime: String - - - public init( - imageUrl: String, - content: String, - uploadTime: String - ) { - self.imageUrl = imageUrl - self.content = content - self.uploadTime = uploadTime - } -} diff --git a/14th-team5-iOS/Domain/Sources/Entities/Members/CreateMemberPickEntity.swift b/14th-team5-iOS/Domain/Sources/Entities/Members/CreateMemberPickEntity.swift new file mode 100644 index 000000000..f240de846 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Entities/Members/CreateMemberPickEntity.swift @@ -0,0 +1,17 @@ +// +// CreateMemberPickEntity.swift +// Domain +// +// Created by 김도현 on 11/21/24. +// + +import Foundation + + +public struct CreateMemberPickEntity { + public let success: Bool + + public init(success: Bool) { + self.success = success + } +} diff --git a/14th-team5-iOS/Domain/Sources/Entities/Camera/CameraPreSignedEntity.swift b/14th-team5-iOS/Domain/Sources/Entities/Members/CreateMemberPresignedEntity.swift similarity index 56% rename from 14th-team5-iOS/Domain/Sources/Entities/Camera/CameraPreSignedEntity.swift rename to 14th-team5-iOS/Domain/Sources/Entities/Members/CreateMemberPresignedEntity.swift index d1f545b63..877b94da3 100644 --- a/14th-team5-iOS/Domain/Sources/Entities/Camera/CameraPreSignedEntity.swift +++ b/14th-team5-iOS/Domain/Sources/Entities/Members/CreateMemberPresignedEntity.swift @@ -1,13 +1,13 @@ // -// CameraPreSignedEntity.swift +// CreateMemberPresignedEntity.swift // Domain // -// Created by Kim dohyun on 6/13/24. +// Created by 김도현 on 11/22/24. // import Foundation -public struct CameraPreSignedEntity { +public struct CreateMemberPresignedEntity { public let imageURL: String public init(imageURL: String) { diff --git a/14th-team5-iOS/Domain/Sources/Entities/Members/CreateMemberPresignedReqeust.swift b/14th-team5-iOS/Domain/Sources/Entities/Members/CreateMemberPresignedReqeust.swift new file mode 100644 index 000000000..91c779a50 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Entities/Members/CreateMemberPresignedReqeust.swift @@ -0,0 +1,17 @@ +// +// CreateMemberPresignedReqeust.swift +// Domain +// +// Created by 김도현 on 11/22/24. +// + +import Foundation + + +public struct CreateMemberPresignedReqeust { + public let imageName: String + + public init(imageName: String) { + self.imageName = imageName + } +} diff --git a/14th-team5-iOS/Domain/Sources/Entities/Members/DeleteMemberEntity.swift b/14th-team5-iOS/Domain/Sources/Entities/Members/DeleteMemberEntity.swift new file mode 100644 index 000000000..5b70ed702 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Entities/Members/DeleteMemberEntity.swift @@ -0,0 +1,17 @@ +// +// DeleteMemberEntity.swift +// Domain +// +// Created by 김도현 on 11/21/24. +// + +import Foundation + + +public struct DeleteMemberEntity { + public let isSuccess: Bool + + public init(isSuccess: Bool) { + self.isSuccess = isSuccess + } +} diff --git a/14th-team5-iOS/Domain/Sources/Entities/Camera/UpdateProfileImageRequest.swift b/14th-team5-iOS/Domain/Sources/Entities/Members/UpdateMemberImageRequest.swift similarity index 73% rename from 14th-team5-iOS/Domain/Sources/Entities/Camera/UpdateProfileImageRequest.swift rename to 14th-team5-iOS/Domain/Sources/Entities/Members/UpdateMemberImageRequest.swift index 686153969..65508f67d 100644 --- a/14th-team5-iOS/Domain/Sources/Entities/Camera/UpdateProfileImageRequest.swift +++ b/14th-team5-iOS/Domain/Sources/Entities/Members/UpdateMemberImageRequest.swift @@ -1,5 +1,5 @@ // -// UpdateProfileImageRequest.swift +// UpdateMemberImageRequest.swift // Domain // // Created by 김도현 on 11/17/24. @@ -8,7 +8,7 @@ import Foundation -public struct UpdateProfileImageRequest { +public struct UpdateMemberImageRequest { public let profileImageUrl: String public init(profileImageUrl: String) { diff --git a/14th-team5-iOS/Domain/Sources/Entities/Members/UpdateMemberNameEntity.swift b/14th-team5-iOS/Domain/Sources/Entities/Members/UpdateMemberNameEntity.swift new file mode 100644 index 000000000..a4ea60fae --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Entities/Members/UpdateMemberNameEntity.swift @@ -0,0 +1,34 @@ +// +// UpdateMemberNameEntity.swift +// Domain +// +// Created by 김도현 on 11/21/24. +// + +import Foundation + + +public struct UpdateMemberNameEntity { + public let memberId: String + public let name: String + public let imageUrl: URL + public let familyId: String + public let familyJoinAt: String + public let dayOfBirth: String + + public init( + memberId: String, + name: String, + imageUrl: URL, + familyId: String, + familyJoinAt: String, + dayOfBirth: String + ) { + self.memberId = memberId + self.name = name + self.imageUrl = imageUrl + self.familyId = familyId + self.familyJoinAt = familyJoinAt + self.dayOfBirth = dayOfBirth + } +} diff --git a/14th-team5-iOS/Domain/Sources/Entities/Members/UpdateMemberNameRequest.swift b/14th-team5-iOS/Domain/Sources/Entities/Members/UpdateMemberNameRequest.swift new file mode 100644 index 000000000..97e8bd269 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Entities/Members/UpdateMemberNameRequest.swift @@ -0,0 +1,17 @@ +// +// UpdateMemberNameRequest.swift +// Domain +// +// Created by 김도현 on 11/20/24. +// + +import Foundation + + +public struct UpdateMemberNameRequest { + public let name: String + + public init(name: String) { + self.name = name + } +} diff --git a/14th-team5-iOS/Domain/Sources/Entities/Mission/MissonTodayContentEntity.swift b/14th-team5-iOS/Domain/Sources/Entities/Mission/MissonTodayContentEntity.swift new file mode 100644 index 000000000..c37537388 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Entities/Mission/MissonTodayContentEntity.swift @@ -0,0 +1,24 @@ +// +// MissonTodayContentEntity.swift +// Domain +// +// Created by 김도현 on 11/25/24. +// + +import Foundation + +public struct MissonTodayContentEntity { + public let missionId: String + public let missionContent: String + public let date: String + + public init( + missionId: String, + missionContent: String, + date: String + ) { + self.missionId = missionId + self.missionContent = missionContent + self.date = date + } +} diff --git a/14th-team5-iOS/Domain/Sources/Entities/Pick/PickEntity.swift b/14th-team5-iOS/Domain/Sources/Entities/Pick/PickEntity.swift deleted file mode 100644 index b43be1710..000000000 --- a/14th-team5-iOS/Domain/Sources/Entities/Pick/PickEntity.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// PickEntity.swift -// Domain -// -// Created by 김건우 on 4/15/24. -// - -import Foundation - -public struct PickEntity { - public var success: Bool - - public init(success: Bool) { - self.success = success - } -} diff --git a/14th-team5-iOS/Domain/Sources/Entities/Pick/PickMemberListEntity.swift b/14th-team5-iOS/Domain/Sources/Entities/Pick/PickMemberListEntity.swift deleted file mode 100644 index 9c2c2e5bc..000000000 --- a/14th-team5-iOS/Domain/Sources/Entities/Pick/PickMemberListEntity.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// PickMemberListResponseDTO.swift -// Domain -// -// Created by 김건우 on 4/15/24. -// - -import Foundation - -public struct PickMemberListEntity { - public var results: [PickMemberEntity] - - public init(results: [PickMemberEntity]) { - self.results = results - } -} - -public struct PickMemberEntity { - public var memberId: String - public var name: String - public var imageUrl: String - public var familyId: String - public var familyJoinedAt: Date - public var dayOfBirth: Date - - public init( - memberId: String, - name: String, - imageUrl: String, - familyId: String, - familyJoinedAt: Date, - dayOfBirth: Date - ) { - self.memberId = memberId - self.name = name - self.imageUrl = imageUrl - self.familyId = familyId - self.familyJoinedAt = familyJoinedAt - self.dayOfBirth = dayOfBirth - } -} diff --git a/14th-team5-iOS/Domain/Sources/Entities/Camera/CameraPostEntity.swift b/14th-team5-iOS/Domain/Sources/Entities/PostList/CreatePostEntity.swift similarity index 90% rename from 14th-team5-iOS/Domain/Sources/Entities/Camera/CameraPostEntity.swift rename to 14th-team5-iOS/Domain/Sources/Entities/PostList/CreatePostEntity.swift index 1b613a305..b12cf0cf9 100644 --- a/14th-team5-iOS/Domain/Sources/Entities/Camera/CameraPostEntity.swift +++ b/14th-team5-iOS/Domain/Sources/Entities/PostList/CreatePostEntity.swift @@ -1,13 +1,14 @@ // -// CameraPostEntity.swift +// CreatePostEntity.swift // Domain // -// Created by Kim dohyun on 6/14/24. +// Created by 김도현 on 11/22/24. // import Foundation -public struct CameraPostEntity { + +public struct CreatePostEntity { public let postId: String public let authorId: String public let commentCount: Int diff --git a/14th-team5-iOS/Domain/Sources/Entities/PostList/CreatePostPresignedURLEntity.swift b/14th-team5-iOS/Domain/Sources/Entities/PostList/CreatePostPresignedURLEntity.swift new file mode 100644 index 000000000..4bcab689b --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Entities/PostList/CreatePostPresignedURLEntity.swift @@ -0,0 +1,17 @@ +// +// CreatePostPresignedURLEntity.swift +// Domain +// +// Created by 김도현 on 11/22/24. +// + +import Foundation + + +public struct CreatePostPresignedURLEntity { + public let imageURL: String + + public init(imageURL: String) { + self.imageURL = imageURL + } +} diff --git a/14th-team5-iOS/Domain/Sources/Entities/Resign/AccountResignEntity.swift b/14th-team5-iOS/Domain/Sources/Entities/Resign/AccountResignEntity.swift deleted file mode 100644 index 659e3611f..000000000 --- a/14th-team5-iOS/Domain/Sources/Entities/Resign/AccountResignEntity.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// AccountResignEntity.swift -// Domain -// -// Created by Kim dohyun on 9/12/24. -// - -import Foundation - -public struct AccountResignEntity { - public var isSuccess: Bool - - public init(isSuccess: Bool) { - self.isSuccess = isSuccess - } -} diff --git a/14th-team5-iOS/Domain/Sources/Repositories/AccountResignRepository.swift b/14th-team5-iOS/Domain/Sources/Repositories/AccountResignRepository.swift deleted file mode 100644 index 8cd5080ed..000000000 --- a/14th-team5-iOS/Domain/Sources/Repositories/AccountResignRepository.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// AccountResignRepository.swift -// Domain -// -// Created by Kim dohyun on 9/12/24. -// - -import Foundation - -import RxSwift - -public protocol AccountResignRepositoryProtocol { - func deleteAccountResignItem() -> Observable -} diff --git a/14th-team5-iOS/Domain/Sources/Repositories/CameraRepository.swift b/14th-team5-iOS/Domain/Sources/Repositories/CameraRepository.swift index f88501873..6ad157c65 100644 --- a/14th-team5-iOS/Domain/Sources/Repositories/CameraRepository.swift +++ b/14th-team5-iOS/Domain/Sources/Repositories/CameraRepository.swift @@ -14,6 +14,7 @@ public enum UploadLocation { case mission case profile case realEmoji + case account public var isRealEmojiType: Bool { switch self { @@ -33,6 +34,8 @@ public enum UploadLocation { return "images/profile/" case .realEmoji: return "images/real-emoji/" + default: + return "" } } @@ -57,27 +60,19 @@ public enum UploadLocation { return "카메라" case .realEmoji: return "셀피 이미지" + default: + return "" } } } public protocol CameraRepositoryProtocol { var disposeBag: DisposeBag { get } - - /// CREATE 메서드 - func createFeedImage(query: CreateFeedQuery, body: CreateFeedRequest) -> Observable - func createProfileImagePresignedURL(body: CreatePresignedURLRequest) -> Observable + func createEmojiImagePresignedURL(memberID: String, body: CreatePresignedURLRequest) -> Observable func createEmojiImage(memberId: String, body: CreateEmojiImageRequest) -> Observable /// FETCH 메서드 func fetchEmojiList(memberId: String) -> Observable<[CameraRealEmojiImageItemEntity?]> - func fetchDailyMissonItem() -> Observable - - /// UPDATE 메서드 - func updateUserProfileImage(memberId: String, body: UpdateProfileImageRequest) -> Observable func updateEmojiImage(memberId: String, realEmojiId: String, body: UpdateRealEmojiImageRequest) -> Observable - - /// UPLOAD 메서드 - func uploadImageToS3Bucket(_ presignedURL: String, image: Data) -> Observable } diff --git a/14th-team5-iOS/Domain/Sources/Repositories/MembersRepository.swift b/14th-team5-iOS/Domain/Sources/Repositories/MembersRepository.swift index ddaa3877c..ba77710d1 100644 --- a/14th-team5-iOS/Domain/Sources/Repositories/MembersRepository.swift +++ b/14th-team5-iOS/Domain/Sources/Repositories/MembersRepository.swift @@ -13,10 +13,19 @@ import RxSwift public protocol MembersRepositoryProtocol { var disposeBag: DisposeBag { get } - - func fetchProfileMemberItems(memberId: String) -> Single - func updataProfileImageToS3(memberId: String, parameter: ProfileImageEditParameter) -> Single - func deleteProfileImageToS3(memberId: String) -> Single + /// FETCH + func fetchProfileMemberItem(memberId: String) -> Observable + /// UPDATE + func updateMemberNameItem(memberId: String, body: UpdateMemberNameRequest) -> Observable + func updateMemberProfileImageItem(memberId: String, body: UpdateMemberImageRequest) -> Observable + /// CREATE + func createMemberPickItem(memberId: String) -> Observable + func creteMemberImagePresignedURL(body: CreateMemberPresignedReqeust) -> Observable + func deleteMemberProfileImageItem(memberId: String) -> Observable + /// DELETE + func deleteMemberItem(memberId: String) -> Observable + /// UPLOAD + func uploadMemberImageToS3Bucket(_ presignedURL: String, image: Data) -> Observable } diff --git a/14th-team5-iOS/Domain/Sources/Repositories/MissonRepository.swift b/14th-team5-iOS/Domain/Sources/Repositories/MissonRepository.swift index 20b631139..a62ce0e31 100644 --- a/14th-team5-iOS/Domain/Sources/Repositories/MissonRepository.swift +++ b/14th-team5-iOS/Domain/Sources/Repositories/MissonRepository.swift @@ -14,7 +14,7 @@ public protocol MissionRepositoryProtocol { /// FETCH func fetchMissonContentItem(missonId: String) -> Observable - func fetchDailyMissonItem() -> Observable + func fetchDailyMissonItem() -> Observable /// LOCAL DB func isAlreadyShowMissionAlert() -> Observable diff --git a/14th-team5-iOS/Domain/Sources/Repositories/PickRepository.swift b/14th-team5-iOS/Domain/Sources/Repositories/PickRepository.swift deleted file mode 100644 index edc70819c..000000000 --- a/14th-team5-iOS/Domain/Sources/Repositories/PickRepository.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// PickRepository.swift -// Domain -// -// Created by 김건우 on 4/15/24. -// - -import Foundation - -import RxSwift - -public protocol PickRepositoryProtocol { - func pickMember(memberId: String) -> Observable - func whoDidIPickMember(memberId: String) -> Observable - func whoPickedMeMember(memberId: String) -> Observable -} diff --git a/14th-team5-iOS/Domain/Sources/Repositories/PostListRepository.swift b/14th-team5-iOS/Domain/Sources/Repositories/PostListRepository.swift index 13289dbea..7cc9a644c 100644 --- a/14th-team5-iOS/Domain/Sources/Repositories/PostListRepository.swift +++ b/14th-team5-iOS/Domain/Sources/Repositories/PostListRepository.swift @@ -13,6 +13,9 @@ public protocol PostListRepositoryProtocol { func fetchPostList(query: PostListQuery) -> Observable func fetchPostDetailItem(postId: String) -> Observable /// CREATE - func createPostItem(query: CreatePostQuery, body: CreatePostRequest) -> Observable - func createPostPresignedURLItem(body: CreatePostPresignedURLRequest) -> Observable + func createPostItem(query: CreatePostQuery, body: CreatePostRequest) -> Observable + func createPostPresignedURLItem(body: CreatePostPresignedURLRequest) -> Observable + + /// UPLOAD + func uploadPostImageToS3Bucket(_ presignedURL: String, image: Data) -> Observable } diff --git a/14th-team5-iOS/Domain/Sources/Trash/Account/DataMapping/AccountNickNameEditDTO.swift b/14th-team5-iOS/Domain/Sources/Trash/Account/DataMapping/AccountNickNameEditDTO.swift deleted file mode 100644 index 764790d76..000000000 --- a/14th-team5-iOS/Domain/Sources/Trash/Account/DataMapping/AccountNickNameEditDTO.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// AccountNickNameEditDTO.swift -// Domain -// -// Created by Kim dohyun on 12/29/23. -// -import Foundation - -import Core - -public struct AccountNickNameEditDTO: Decodable { - public var memberId: String - public var name: String - public var imageUrl: String? - public var familyId: String - public var dayOfBirth: String - public var familyJoinAt: String -} - - -extension AccountNickNameEditDTO { - public func toDomain() -> AccountNickNameEditResponse { - return .init( - memberId: memberId, - name: name, - imageUrl: URL(string: imageUrl ?? "") ?? URL(fileURLWithPath: ""), - familyId: familyId, - dayOfBirth: dayOfBirth, - familyJoinAt: familyJoinAt.toDate(with: "yyyy-MM-dd").realativeFormatterYYMM() - ) - } - -} diff --git a/14th-team5-iOS/Domain/Sources/Trash/Account/Entity/AccountNickNameEditResponse.swift b/14th-team5-iOS/Domain/Sources/Trash/Account/Entity/AccountNickNameEditResponse.swift deleted file mode 100644 index 5d1e4fd9c..000000000 --- a/14th-team5-iOS/Domain/Sources/Trash/Account/Entity/AccountNickNameEditResponse.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// AccountNickNameEditResponse.swift -// Domain -// -// Created by Kim dohyun on 12/29/23. -// - -import Foundation - - -public struct AccountNickNameEditResponse { - public var memberId: String - public var name: String - public var imageUrl: URL - public var familyId: String - public var dayOfBirth: String - public var familyJoinAt: String - -} diff --git a/14th-team5-iOS/Domain/Sources/Trash/Account/Parameters/AccountNickNameEditParameter.swift b/14th-team5-iOS/Domain/Sources/Trash/Account/Parameters/AccountNickNameEditParameter.swift deleted file mode 100644 index 1cd131d7b..000000000 --- a/14th-team5-iOS/Domain/Sources/Trash/Account/Parameters/AccountNickNameEditParameter.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// AccountNickNameEditParameter.swift -// Domain -// -// Created by Kim dohyun on 12/29/23. -// - -import Foundation - - -public struct AccountNickNameEditParameter: Encodable { - - public var name: String - - public init(name: String) { - self.name = name - } - -} diff --git a/14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/Mission/FetchCameraTodayMissionUseCase.swift b/14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/Mission/FetchCameraTodayMissionUseCase.swift deleted file mode 100644 index 799963063..000000000 --- a/14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/Mission/FetchCameraTodayMissionUseCase.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// FetchCameraTodayMissionUseCase.swift -// Domain -// -// Created by Kim dohyun on 6/14/24. -// - -import Foundation - -import RxSwift -import RxCocoa - - -public protocol FetchCameraTodayMissionUseCaseProtocol { - func execute() -> Observable -} - - -public final class FetchCameraTodayMissionUseCase: FetchCameraTodayMissionUseCaseProtocol { - - private let cameraRepository: any CameraRepositoryProtocol - - public init(cameraRepository: any CameraRepositoryProtocol) { - self.cameraRepository = cameraRepository - } - - public func execute() -> Observable { - return cameraRepository.fetchDailyMissonItem() - } -} diff --git a/14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/Profile/EditCameraProfileImageUseCase.swift b/14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/Profile/EditCameraProfileImageUseCase.swift deleted file mode 100644 index e01c9d942..000000000 --- a/14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/Profile/EditCameraProfileImageUseCase.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// EditCameraProfileImageUseCase.swift -// Domain -// -// Created by Kim dohyun on 6/14/24. -// - -import Foundation - -import RxSwift -import RxCocoa - -public protocol EditCameraProfileImageUseCaseProtocol { - func execute(memberId: String, body: UpdateProfileImageRequest) -> Observable -} - - -public final class EditCameraProfileImageUseCase: EditCameraProfileImageUseCaseProtocol { - - private let cameraRepository: any CameraRepositoryProtocol - - public init(cameraRepository: any CameraRepositoryProtocol) { - self.cameraRepository = cameraRepository - } - - public func execute(memberId: String, body: UpdateProfileImageRequest) -> Observable { - return cameraRepository.updateUserProfileImage(memberId: memberId, body: body) - } - -} - - - diff --git a/14th-team5-iOS/Domain/Sources/Trash/Member/Interfaces/Repositories/MemberRepository.swift b/14th-team5-iOS/Domain/Sources/Trash/Member/Interfaces/Repositories/MemberRepository.swift index f9d0b3fb1..96bfe1322 100644 --- a/14th-team5-iOS/Domain/Sources/Trash/Member/Interfaces/Repositories/MemberRepository.swift +++ b/14th-team5-iOS/Domain/Sources/Trash/Member/Interfaces/Repositories/MemberRepository.swift @@ -8,10 +8,5 @@ import Foundation public protocol MemberRepositoryProtocol { - func fetchFamilyNameEditorId() -> String - func fetchUserName(memberId: String) -> String - func fetchProfileImageUrlString(memberId: String) -> String - func checkIsMe(memberId: String) -> Bool func checkIsValidMember(memberId: String) -> Bool - func fetchMyMemberId() -> String } diff --git a/14th-team5-iOS/Domain/Sources/Trash/Member/UseCases/MemberUseCase.swift b/14th-team5-iOS/Domain/Sources/Trash/Member/UseCases/MemberUseCase.swift index 9f3f7a6f8..7523b1811 100644 --- a/14th-team5-iOS/Domain/Sources/Trash/Member/UseCases/MemberUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/Trash/Member/UseCases/MemberUseCase.swift @@ -8,48 +8,18 @@ import Foundation public protocol MemberUseCaseProtocol { - func executeFetchUserName(memberId: String) -> String - func executeProfileImageUrlString(memberId: String) -> String - func executeProfileImageUrl(memberId: String) -> URL? - func executeCheckIsMe(memberId: String) -> Bool func executeCheckIsValidMember(memberId: String) -> Bool - func executeFetchMyMemberId() -> String - func executeFetchFamilyNameEditorId() -> String } public final class MemberUseCase: MemberUseCaseProtocol { + private let memberRepository: MemberRepositoryProtocol public init(memberRepository: MemberRepositoryProtocol) { self.memberRepository = memberRepository } - public func executeFetchUserName(memberId: String) -> String { - return memberRepository.fetchUserName(memberId: memberId) - } - - public func executeProfileImageUrlString(memberId: String) -> String { - return memberRepository.fetchProfileImageUrlString(memberId: memberId) - } - - public func executeProfileImageUrl(memberId: String) -> URL? { - let urlString = memberRepository.fetchProfileImageUrlString(memberId: memberId) - return URL(string: urlString) - } - - public func executeCheckIsMe(memberId: String) -> Bool { - return memberRepository.checkIsMe(memberId: memberId) - } - public func executeCheckIsValidMember(memberId: String) -> Bool { return memberRepository.checkIsValidMember(memberId: memberId) } - - public func executeFetchMyMemberId() -> String { - return memberRepository.fetchMyMemberId() - } - - public func executeFetchFamilyNameEditorId() -> String { - return memberRepository.fetchFamilyNameEditorId() - } } diff --git a/14th-team5-iOS/Domain/Sources/Trash/Pick/UseCases/PickUseCase.swift b/14th-team5-iOS/Domain/Sources/Trash/Pick/UseCases/PickUseCase.swift deleted file mode 100644 index 0c4a0992e..000000000 --- a/14th-team5-iOS/Domain/Sources/Trash/Pick/UseCases/PickUseCase.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// PickUseCase.swift -// Domain -// -// Created by 김건우 on 4/15/24. -// - -import Foundation - -import RxSwift - -@available(*, deprecated) -public protocol PickUseCaseProtocol { - func executePickMember(memberId: String) -> Observable - func executeFetchWhoDidIPickMember(memberId: String) -> Observable - func executeFetchWhoPickedMeMember(memberId: String) -> Observable -} - -@available(*, deprecated) -public final class PickUseCase: PickUseCaseProtocol { - private let pickRepository: PickRepositoryProtocol - - public init(pickRepository: PickRepositoryProtocol) { - self.pickRepository = pickRepository - } - - public func executePickMember(memberId: String) -> Observable { - return pickRepository.pickMember(memberId: memberId) - } - - public func executeFetchWhoDidIPickMember(memberId: String) -> Observable { - return pickRepository.whoDidIPickMember(memberId: memberId) - } - - public func executeFetchWhoPickedMeMember(memberId: String) -> Observable { - return pickRepository.whoPickedMeMember(memberId: memberId) - } -} diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Account/CreateAccountPresingedURLUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Account/CreateAccountPresingedURLUseCase.swift new file mode 100644 index 000000000..dd19b0c37 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/UseCases/Account/CreateAccountPresingedURLUseCase.swift @@ -0,0 +1,32 @@ +// +// CreateAccountPresingedURLUseCase.swift +// Domain +// +// Created by 김도현 on 11/22/24. +// + +import Foundation + +import Core +import RxSwift + + +public protocol CreateAccountPresingedURLUseCaseProtocol { + func execute(body: CreateMemberPresignedReqeust, imageData: Data) -> Observable +} + + +public final class CreateAccountPresingedURLUseCase: CreateAccountPresingedURLUseCaseProtocol { + + private let membersRepository: any MembersRepositoryProtocol + + + public init(membersRepository: any MembersRepositoryProtocol) { + self.membersRepository = membersRepository + } + + public func execute(body: CreateMemberPresignedReqeust, imageData: Data) -> Observable { + + return .empty() + } +} diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Camera/CreateCameraImageUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Camera/CreateCameraImageUseCase.swift deleted file mode 100644 index 139d07cf2..000000000 --- a/14th-team5-iOS/Domain/Sources/UseCases/Camera/CreateCameraImageUseCase.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// CreateCameraImageUseCase.swift -// Domain -// -// Created by Kim dohyun on 6/14/24. -// - -import Foundation - -import RxSwift -import RxCocoa - - -public protocol CreateCameraImageUseCaseProtocol { - func execute(query: CreateFeedQuery, body: CreateFeedRequest) -> Observable -} - -public final class CreateCameraImageUseCase: CreateCameraImageUseCaseProtocol { - - private let cameraRepository: any CameraRepositoryProtocol - - public init(cameraRepository: any CameraRepositoryProtocol) { - self.cameraRepository = cameraRepository - } - - public func execute(query: CreateFeedQuery, body: CreateFeedRequest) -> Observable { - return cameraRepository.createFeedImage(query: query, body: body) - } -} diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Camera/CreateCameraUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Camera/CreateCameraUseCase.swift index c466fca92..f417a30be 100644 --- a/14th-team5-iOS/Domain/Sources/UseCases/Camera/CreateCameraUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/Camera/CreateCameraUseCase.swift @@ -10,21 +10,21 @@ import Foundation import RxSwift import RxCocoa -public protocol CreateCameraUseCaseProtocol { - func execute(body: CreatePresignedURLRequest) -> Observable -} - -public final class CreateCameraUseCase: CreateCameraUseCaseProtocol { - - private let cameraRepository: any CameraRepositoryProtocol - - public init(cameraRepository: any CameraRepositoryProtocol) { - self.cameraRepository = cameraRepository - } - - public func execute(body: CreatePresignedURLRequest) -> Observable { - return cameraRepository.createProfileImagePresignedURL(body: body) - } -} +//public protocol CreateCameraUseCaseProtocol { +// func execute(body: CreatePresignedURLRequest) -> Observable +//} +// +//public final class CreateCameraUseCase: CreateCameraUseCaseProtocol { +// +// private let cameraRepository: any CameraRepositoryProtocol +// +// public init(cameraRepository: any CameraRepositoryProtocol) { +// self.cameraRepository = cameraRepository +// } +// +// public func execute(body: CreatePresignedURLRequest) -> Observable { +// return cameraRepository.createProfileImagePresignedURL(body: body) +// } +//} diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Camera/FetchCameraUploadImageUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Camera/FetchCameraUploadImageUseCase.swift index cad667673..216fa0a16 100644 --- a/14th-team5-iOS/Domain/Sources/UseCases/Camera/FetchCameraUploadImageUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/Camera/FetchCameraUploadImageUseCase.swift @@ -25,6 +25,6 @@ public final class FetchCameraUploadImageUseCase: FetchCameraUploadImageUseCaseP } public func execute(_ presignedURL: String, image: Data) -> Observable { - return cameraRepository.uploadImageToS3Bucket(presignedURL, image: image) + return .empty() } } diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Members/CreateMembersPickUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Members/CreateMembersPickUseCase.swift new file mode 100644 index 000000000..b6062806f --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/UseCases/Members/CreateMembersPickUseCase.swift @@ -0,0 +1,30 @@ +// +// CreateMembersPickUseCase.swift +// Domain +// +// Created by 김도현 on 11/22/24. +// + +import Foundation + +import RxSwift + +public protocol CreateMembersPickUseCaseProtocol { + func execute(memberId: String) -> Observable +} + + +public final class CreateMembersPickUseCase: CreateMembersPickUseCaseProtocol { + + private let membersRepository: MembersRepositoryProtocol + + + public init(membersRepository: MembersRepositoryProtocol) { + self.membersRepository = membersRepository + } + + public func execute(memberId: String) -> Observable { + return membersRepository.createMemberPickItem(memberId: memberId) + } + +} diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Members/CreateMembersPresignedURLUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Members/CreateMembersPresignedURLUseCase.swift new file mode 100644 index 000000000..eb26f3965 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/UseCases/Members/CreateMembersPresignedURLUseCase.swift @@ -0,0 +1,48 @@ +// +// CreateMembersPresignedURLUseCase.swift +// Domain +// +// Created by 김도현 on 11/24/24. +// + +import Foundation + +import Core +import RxSwift + + +public protocol CreateMembersPresignedURLUseCaseProtocol { + func execute(body: CreateMemberPresignedReqeust, imageData: Data) -> Observable +} + + +public final class CreateMembersPresignedURLUseCase: CreateMembersPresignedURLUseCaseProtocol { + + private let membersRepository: any MembersRepositoryProtocol + + public init(membersRepository: any MembersRepositoryProtocol) { + self.membersRepository = membersRepository + } + + public func execute(body: CreateMemberPresignedReqeust, imageData: Data) -> Observable { + return membersRepository.creteMemberImagePresignedURL(body: body) + .flatMap { [unowned self] presignedURL -> Observable in + guard let remoteURL = presignedURL?.imageURL else { + return .error(BBUploadError.invalidServerResponse) + } + + return self.membersRepository.uploadMemberImageToS3Bucket(remoteURL, image: imageData) + .flatMap { isSucess -> Observable in + if isSucess { + return .just(presignedURL) + } else { + return .error(BBUploadError.uploadFailed) + } + } + } + } + + + + +} diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Members/DeleteMembersProfileUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Members/DeleteMembersProfileUseCase.swift index 6145f3d11..5917af610 100644 --- a/14th-team5-iOS/Domain/Sources/UseCases/Members/DeleteMembersProfileUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/Members/DeleteMembersProfileUseCase.swift @@ -11,7 +11,7 @@ import RxSwift import RxCocoa public protocol DeleteMembersProfileUseCaseProtocol { - func execute(memberId: String) -> Single + func execute(memberId: String) -> Observable } @@ -24,7 +24,7 @@ public final class DeleteMembersProfileUseCase: DeleteMembersProfileUseCaseProto self.membersRepository = membersRepository } - public func execute(memberId: String) -> Single { - return membersRepository.deleteProfileImageToS3(memberId: memberId) + public func execute(memberId: String) -> Observable { + return membersRepository.deleteMemberProfileImageItem(memberId: memberId) } } diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Members/DeleteMembersUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Members/DeleteMembersUseCase.swift new file mode 100644 index 000000000..4b46bed79 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/UseCases/Members/DeleteMembersUseCase.swift @@ -0,0 +1,29 @@ +// +// DeleteMembersUseCase.swift +// Domain +// +// Created by 김도현 on 11/21/24. +// + +import Foundation + +import RxSwift + +public protocol DeleteMembersUseCaseProtocol { + func execute(memberId: String) -> Observable +} + + +public final class DeleteMembersUseCase: DeleteMembersUseCaseProtocol { + + private let membersRepository: MembersRepositoryProtocol + + + public init(membersRepository: MembersRepositoryProtocol) { + self.membersRepository = membersRepository + } + + public func execute(memberId: String) -> Observable { + return membersRepository.deleteMemberItem(memberId: memberId) + } +} diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Members/FetchMembersProfileUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Members/FetchMembersProfileUseCase.swift index 5c79d623e..dc45523e8 100644 --- a/14th-team5-iOS/Domain/Sources/UseCases/Members/FetchMembersProfileUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/Members/FetchMembersProfileUseCase.swift @@ -12,7 +12,7 @@ import RxCocoa public protocol FetchMembersProfileUseCaseProtocol { - func execute(memberId: String) -> Single + func execute(memberId: String) -> Observable } @@ -25,7 +25,7 @@ public final class FetchMembersProfileUseCase: FetchMembersProfileUseCaseProtoco self.membersRepository = membersRepository } - public func execute(memberId: String) -> Single { - return membersRepository.fetchProfileMemberItems(memberId: memberId) + public func execute(memberId: String) -> Observable { + return membersRepository.fetchProfileMemberItem(memberId: memberId) } } diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Members/UpdateMembersNameUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Members/UpdateMembersNameUseCase.swift new file mode 100644 index 000000000..d0a56451d --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/UseCases/Members/UpdateMembersNameUseCase.swift @@ -0,0 +1,29 @@ +// +// UpdateMembersNameUseCase.swift +// Domain +// +// Created by 김도현 on 11/22/24. +// + +import Foundation + +import RxSwift + +public protocol UpdateMembersNameUseCaseProtocol { + func execute(memberId: String, body: UpdateMemberNameRequest) -> Observable +} + + +public final class UpdateMembersNameUseCase: UpdateMembersNameUseCaseProtocol { + + private let membersRepository: MembersRepositoryProtocol + + + public init(membersRepository: MembersRepositoryProtocol) { + self.membersRepository = membersRepository + } + + public func execute(memberId: String, body: UpdateMemberNameRequest) -> Observable { + return membersRepository.updateMemberNameItem(memberId: memberId, body: body) + } +} diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Members/UpdateMembersProfileUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Members/UpdateMembersProfileUseCase.swift index 7e5f98b13..831a88bc0 100644 --- a/14th-team5-iOS/Domain/Sources/UseCases/Members/UpdateMembersProfileUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/Members/UpdateMembersProfileUseCase.swift @@ -7,11 +7,11 @@ import Foundation +import Core import RxSwift -import RxCocoa public protocol UpdateMembersProfileUseCaseProtocol { - func execute(memberId: String, parameter: ProfileImageEditParameter) -> Single + func execute(memberId: String, body: UpdateMemberImageRequest) -> Observable } @@ -23,8 +23,9 @@ public final class UpdateMembersProfileUseCase: UpdateMembersProfileUseCaseProto public init(membersRepository: any MembersRepositoryProtocol) { self.membersRepository = membersRepository } + - public func execute(memberId: String, parameter: ProfileImageEditParameter) -> Single { - return membersRepository.updataProfileImageToS3(memberId: memberId, parameter: parameter) + public func execute(memberId: String, body: UpdateMemberImageRequest) -> Observable { + return membersRepository.updateMemberProfileImageItem(memberId: memberId, body: body) } } diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Misson/FetchDailyMissonContentUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Misson/FetchDailyMissonContentUseCase.swift new file mode 100644 index 000000000..70968dbe4 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/UseCases/Misson/FetchDailyMissonContentUseCase.swift @@ -0,0 +1,28 @@ +// +// FetchDailyMissonContentUseCase.swift +// Domain +// +// Created by 김도현 on 11/25/24. +// + +import Foundation + +import RxSwift + + +public protocol FetchDailyMissonContentUseCaseProtocol { + func execute() -> Observable +} + +public final class FetchDailyMissonContentUseCase: FetchDailyMissonContentUseCaseProtocol { + + private let missionRepository: any MissionRepositoryProtocol + + public init(missionRepository: any MissionRepositoryProtocol) { + self.missionRepository = missionRepository + } + + public func execute() -> Observable { + return missionRepository.fetchDailyMissonItem() + } +} diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Pick/PickMemberUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Pick/PickMemberUseCase.swift deleted file mode 100644 index cd74b977a..000000000 --- a/14th-team5-iOS/Domain/Sources/UseCases/Pick/PickMemberUseCase.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// PickMemberUseCase.swift -// Domain -// -// Created by 김건우 on 6/15/24. -// - -import Foundation - -import RxSwift - -public protocol PickMemberUseCaseProtocol { - func execute(memberId: String) -> Observable -} - -public final class PickMemberUseCase: PickMemberUseCaseProtocol { - - // MARK: - Repositories - private var pickRepository: PickRepositoryProtocol - - // MARK: - Intializer - public init(pickRepository: PickRepositoryProtocol) { - self.pickRepository = pickRepository - } - - // MARK: - Execute - public func execute(memberId: String) -> Observable { - return pickRepository.pickMember(memberId: memberId) - } - -} diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Pick/WhoDidIPickMemberUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Pick/WhoDidIPickMemberUseCase.swift deleted file mode 100644 index f3526ca22..000000000 --- a/14th-team5-iOS/Domain/Sources/UseCases/Pick/WhoDidIPickMemberUseCase.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// WhoDidIPickMemberUseCase.swift -// Domain -// -// Created by 김건우 on 6/15/24. -// - -import Foundation - -import RxSwift - -public protocol WhoDidIPickMemberUseCaseProtocol { - func execute(memberId: String) -> Observable -} - -public final class WhoDidIPickMemberUseCase: WhoDidIPickMemberUseCaseProtocol { - - // MARK: - Repositories - private var pickRepository: PickRepositoryProtocol - - // MARK: - Intializer - public init(pickRepository: PickRepositoryProtocol) { - self.pickRepository = pickRepository - } - - // MARK: - Execute - public func execute(memberId: String) -> Observable { - return pickRepository.whoDidIPickMember(memberId: memberId) - } - -} - diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Pick/WhoPickedMeUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Pick/WhoPickedMeUseCase.swift deleted file mode 100644 index e2ca9ac8b..000000000 --- a/14th-team5-iOS/Domain/Sources/UseCases/Pick/WhoPickedMeUseCase.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// WhoPickedMeUseCase.swift -// Domain -// -// Created by 김건우 on 6/15/24. -// - -import Foundation - -import RxSwift - -public protocol WhoPickedMeUseCaseProtocol { - func execute(memberId: String) -> Observable -} - -public final class WhoPickedMeUseCase: WhoPickedMeUseCaseProtocol { - - // MARK: - Repositories - private var pickRepository: PickRepositoryProtocol - - // MARK: - Intializer - public init(pickRepository: PickRepositoryProtocol) { - self.pickRepository = pickRepository - } - - // MARK: - Execute - public func execute(memberId: String) -> Observable { - return pickRepository.whoPickedMeMember (memberId: memberId) - } - -} - - diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Post/CreatePostPresingedURLUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Post/CreatePostPresingedURLUseCase.swift new file mode 100644 index 000000000..65a2f6061 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/UseCases/Post/CreatePostPresingedURLUseCase.swift @@ -0,0 +1,43 @@ +// +// CreatePostPresingedURLUseCase.swift +// Domain +// +// Created by 김도현 on 11/22/24. +// + +import Foundation + +import Core +import RxSwift + +public protocol CreatePresignedURLUseCaseProtocol { + func execute(body: CreatePostPresignedURLRequest, imageData: Data) -> Observable +} + + +public final class CreatePresignedURLUseCase: CreatePresignedURLUseCaseProtocol { + + private let postListReposity: PostListRepositoryProtocol + + public init(postListReposity: PostListRepositoryProtocol) { + self.postListReposity = postListReposity + } + + + public func execute(body: CreatePostPresignedURLRequest, imageData: Data) -> Observable { + return postListReposity.createPostPresignedURLItem(body: body) + .flatMap { [unowned self] presignedURL -> Observable in + guard let remoteURL = presignedURL?.imageURL else { + return .error(BBUploadError.invalidServerResponse) + } + return self.postListReposity.uploadPostImageToS3Bucket(remoteURL, image: imageData) + .flatMap { isSuccess -> Observable in + if isSuccess { + return .just(presignedURL) + } else { + return .error(BBUploadError.uploadFailed) + } + } + } + } +} diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Post/CreatePostUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Post/CreatePostUseCase.swift new file mode 100644 index 000000000..a0eec89ae --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/UseCases/Post/CreatePostUseCase.swift @@ -0,0 +1,27 @@ +// +// CreatePostUseCase.swift +// Domain +// +// Created by 김도현 on 11/22/24. +// + +import Foundation + +import RxSwift + +public protocol CreatePostUseCaseProtocol { + func execute(query: CreatePostQuery, body: CreatePostRequest) -> Observable +} + + +public final class CreatePostUseCase: CreatePostUseCaseProtocol { + private let postListRepository: PostListRepositoryProtocol + + public init(postListRepository: PostListRepositoryProtocol) { + self.postListRepository = postListRepository + } + + public func execute(query: CreatePostQuery, body: CreatePostRequest) -> Observable { + return postListRepository.createPostItem(query: query, body: body) + } +} diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Resign/DeleteAccountResignUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Resign/DeleteAccountResignUseCase.swift deleted file mode 100644 index cfaa42e28..000000000 --- a/14th-team5-iOS/Domain/Sources/UseCases/Resign/DeleteAccountResignUseCase.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// DeleteAccountResignUseCase.swift -// Domain -// -// Created by Kim dohyun on 9/12/24. -// - -import Foundation - -import RxSwift - -public protocol DeleteAccountResignUseCaseProtocol { - func execute() -> Observable -} - -public final class DeleteAccountResignUseCase: DeleteAccountResignUseCaseProtocol { - - private let accountResignRepository: any AccountResignRepositoryProtocol - - public init(accountResignRepository: any AccountResignRepositoryProtocol) { - self.accountResignRepository = accountResignRepository - } - - public func execute() -> Observable { - return accountResignRepository.deleteAccountResignItem() - } -} From 5d1e064d96ad4da224ba4007ee944af10c49f903 Mon Sep 17 00:00:00 2001 From: g_mi <62610032+akrudal@users.noreply.github.com> Date: Thu, 28 Nov 2024 23:17:03 +0900 Subject: [PATCH 249/263] =?UTF-8?q?Family,=20Reaction,=20RealEmoji=20API?= =?UTF-8?q?=20Worker=EB=A5=BC=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= =?UTF-8?q?=ED=95=A9=EB=8B=88=EB=8B=A4.=20(#714)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: api spec(#704) * refactor: family, reaction, realemoji api worker(#704) * add comments * refactor: folder(#704) * feat: add comments(#704) --- .../FamilyCreatedAtResponseDTO.swift | 0 .../DataMapping/FamilyNameResponseDTO.swift | 0 .../UpdateFamilyNameRequestDTO.swift | 0 .../Family/FamilyAPI/FamilyAPIWorker.swift | 148 ----------------- .../APIs/Family/FamilyAPI/FamilyAPIs.swift | 44 ----- .../Data/Sources/APIs/Family/FamilyAPIs.swift | 32 ++++ .../Sources/APIs/Family/FamilyWorker.swift | 41 +++++ .../FamilyInvitationLinkResponseDTO.swift | 0 .../FamilyInviteViewAPIs.swift | 26 +++ .../FamilyInviteViewWorker.swift | 23 +++ .../CreateFamilyLinkResponseDTO.swift | 24 +++ .../Data/Sources/APIs/Link/LinkAPIs.swift | 24 +++ .../Data/Sources/APIs/Link/LinkWorker.swift | 28 ++++ .../DataMapping/CreateFamilyResponseDTO.swift | 0 .../DataMapping/FamilyInfoResponseDTO.swift} | 4 +- .../DataMapping/JoinFamilyRequestDTO.swift | 0 .../DataMapping/JoinFamilyResponseDTO.swift | 0 .../Data/Sources/APIs/Me/MeAPI.swift | 37 +++++ .../Data/Sources/APIs/Me/MeWorker.swift | 53 ++++++ .../DataMapping/MyRealEmojiResponseDTO.swift | 0 .../UploadRealEmojiRequestDTO.swift | 10 ++ .../MemberRealEmoji/MemberRealEmojiAPIs.swift | 40 +++++ .../MemberRealEmojiWorker.swift | 22 +++ .../CreateMemberPickResponseDTO.swift | 0 .../CreateMemberPresignedURLRequestDTO.swift | 0 .../CreateMemberPresignedURLResponseDTO.swift | 0 .../DataMapping/DeleteMemberResponseDTO.swift | 0 .../DataMapping/FamilyMemberQueryDTO.swift | 12 ++ .../MembersProfileResponseDTO.swift | 0 .../PaginationResponseMembersDTO.swift} | 12 +- .../UpdateMemberImageRequestDTO.swift | 0 .../UpdateMemberNameRequestDTO.swift | 0 .../UpdateMemberNameResponseDTO.swift | 0 .../Members/{MemberAPI => }/MembersAPIs.swift | 13 +- ...ersAPIWorker.swift => MembersWorker.swift} | 22 ++- .../DataMapping/AddRealEmojiRequestDTO.swift | 2 +- .../DataMapping/AddRealEmojiResponseDTO.swift | 0 .../FetchRealEmojiListRequestDTO.swift | 0 .../FetchRealEmojiListResponseDTO.swift | 0 .../RemoveRealEmojiRequestDTO.swift | 0 .../RemoveRealEmojiResponseDTO.swift | 0 .../PostRealEmoji/RealEmojiAPIWorker.swift | 55 +++++++ .../APIs/PostRealEmoji/RealEmojiAPIs.swift | 40 +++++ .../DataMapping/AddReactionRequestDTO.swift | 0 .../DataMapping/AddReactionResponseDTO.swift | 0 .../DataMapping/FetchReactionRequestDTO.swift | 0 .../FetchReactionResponseDTO.swift | 0 .../RemoveReactionRequestDTO.swift | 0 .../RemoveReactionResponseDTO.swift | 0 .../ReactionAPI/ReactionAPIWorker.swift | 76 --------- .../Reaction/ReactionAPI/ReactionAPIs.swift | 29 ---- .../APIs/Reaction/ReactionAPIWorker.swift | 39 +++++ .../Sources/APIs/Reaction/ReactionAPIs.swift | 43 +++++ .../Repository/ReactionRepository.swift | 32 ---- .../RealEmojiAPI/RealEmojiAPIWorker.swift | 97 ----------- .../RealEmojiAPI/RealEmojiAPIs.swift | 34 ---- .../Repository/RealEmojiRepository.swift | 36 ----- .../FamilyRepository.swift | 151 +++++++++--------- .../Repositories/MembersRepository.swift | 2 +- .../Repositories/ReactionRepository.swift | 49 ++++++ .../Repositories/RealEmojiRepository.swift | 55 +++++++ .../AccountRepository/AccountRepository.swift | 2 +- .../Repositories/FamilyRepository.swift | 2 +- .../Repositories/ReactionRepository.swift | 6 +- .../Repositories/RealEmojiRepository.swift | 8 +- .../UseCases/Post/FetchPostListUseCase.swift | 2 +- .../Reaction/CreateReactionUseCase.swift | 10 +- .../Reaction/FetchReactionListUseCase.swift | 8 +- .../Reaction/RemoveReactionUseCase.swift | 10 +- .../RealEmoji/CreateRealEmojiUseCase.swift | 4 +- .../RealEmoji/FetchMyRealEmojiUseCase.swift | 4 +- .../RealEmoji/FetchRealEmojiListUseCase.swift | 4 +- .../RealEmoji/RemoveRealEmojiUseCase.swift | 4 +- 73 files changed, 804 insertions(+), 615 deletions(-) rename 14th-team5-iOS/Data/Sources/APIs/Family/{FamilyAPI => }/DataMapping/FamilyCreatedAtResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/APIs/Family/{FamilyAPI => }/DataMapping/FamilyNameResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/APIs/Family/{FamilyAPI => }/DataMapping/UpdateFamilyNameRequestDTO.swift (100%) delete mode 100644 14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIWorker.swift delete mode 100644 14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIs.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPIs.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/Family/FamilyWorker.swift rename 14th-team5-iOS/Data/Sources/APIs/{Family/FamilyAPI => FamilyInviteView}/DataMapping/FamilyInvitationLinkResponseDTO.swift (100%) create mode 100644 14th-team5-iOS/Data/Sources/APIs/FamilyInviteView/FamilyInviteViewAPIs.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/FamilyInviteView/FamilyInviteViewWorker.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/Link/DataMapping/CreateFamilyLinkResponseDTO.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/Link/LinkAPIs.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/Link/LinkWorker.swift rename 14th-team5-iOS/Data/Sources/APIs/{Family/FamilyAPI => Me}/DataMapping/CreateFamilyResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/APIs/{Family/FamilyAPI/DataMapping/FamilyGroupInfoResponseDTO.swift => Me/DataMapping/FamilyInfoResponseDTO.swift} (84%) rename 14th-team5-iOS/Data/Sources/APIs/{Family/FamilyAPI => Me}/DataMapping/JoinFamilyRequestDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/APIs/{Family/FamilyAPI => Me}/DataMapping/JoinFamilyResponseDTO.swift (100%) create mode 100644 14th-team5-iOS/Data/Sources/APIs/Me/MeAPI.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/Me/MeWorker.swift rename 14th-team5-iOS/Data/Sources/APIs/{RealEmoji/RealEmojiAPI => MemberRealEmoji}/DataMapping/MyRealEmojiResponseDTO.swift (100%) create mode 100644 14th-team5-iOS/Data/Sources/APIs/MemberRealEmoji/DataMapping/UploadRealEmojiRequestDTO.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/MemberRealEmoji/MemberRealEmojiAPIs.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/MemberRealEmoji/MemberRealEmojiWorker.swift rename 14th-team5-iOS/Data/Sources/APIs/Members/{MemberAPI => }/DataMapping/CreateMemberPickResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/APIs/Members/{MemberAPI => }/DataMapping/CreateMemberPresignedURLRequestDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/APIs/Members/{MemberAPI => }/DataMapping/CreateMemberPresignedURLResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/APIs/Members/{MemberAPI => }/DataMapping/DeleteMemberResponseDTO.swift (100%) create mode 100644 14th-team5-iOS/Data/Sources/APIs/Members/DataMapping/FamilyMemberQueryDTO.swift rename 14th-team5-iOS/Data/Sources/APIs/Members/{MemberAPI => }/DataMapping/MembersProfileResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/APIs/{Family/FamilyAPI/DataMapping/FamilyMemberProfileResponseDTO.swift => Members/DataMapping/PaginationResponseMembersDTO.swift} (74%) rename 14th-team5-iOS/Data/Sources/APIs/Members/{MemberAPI => }/DataMapping/UpdateMemberImageRequestDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/APIs/Members/{MemberAPI => }/DataMapping/UpdateMemberNameRequestDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/APIs/Members/{MemberAPI => }/DataMapping/UpdateMemberNameResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/APIs/Members/{MemberAPI => }/MembersAPIs.swift (86%) rename 14th-team5-iOS/Data/Sources/APIs/Members/{MemberAPI/MembersAPIWorker.swift => MembersWorker.swift} (87%) rename 14th-team5-iOS/Data/Sources/APIs/{RealEmoji/RealEmojiAPI => PostRealEmoji}/DataMapping/AddRealEmojiRequestDTO.swift (92%) rename 14th-team5-iOS/Data/Sources/APIs/{RealEmoji/RealEmojiAPI => PostRealEmoji}/DataMapping/AddRealEmojiResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/APIs/{RealEmoji/RealEmojiAPI => PostRealEmoji}/DataMapping/FetchRealEmojiListRequestDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/APIs/{RealEmoji/RealEmojiAPI => PostRealEmoji}/DataMapping/FetchRealEmojiListResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/APIs/{RealEmoji/RealEmojiAPI => PostRealEmoji}/DataMapping/RemoveRealEmojiRequestDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/APIs/{RealEmoji/RealEmojiAPI => PostRealEmoji}/DataMapping/RemoveRealEmojiResponseDTO.swift (100%) create mode 100644 14th-team5-iOS/Data/Sources/APIs/PostRealEmoji/RealEmojiAPIWorker.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/PostRealEmoji/RealEmojiAPIs.swift rename 14th-team5-iOS/Data/Sources/APIs/Reaction/{ReactionAPI => }/DataMapping/AddReactionRequestDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/APIs/Reaction/{ReactionAPI => }/DataMapping/AddReactionResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/APIs/Reaction/{ReactionAPI => }/DataMapping/FetchReactionRequestDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/APIs/Reaction/{ReactionAPI => }/DataMapping/FetchReactionResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/APIs/Reaction/{ReactionAPI => }/DataMapping/RemoveReactionRequestDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/APIs/Reaction/{ReactionAPI => }/DataMapping/RemoveReactionResponseDTO.swift (100%) delete mode 100644 14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/ReactionAPIWorker.swift delete mode 100644 14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/ReactionAPIs.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPIWorker.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPIs.swift delete mode 100644 14th-team5-iOS/Data/Sources/APIs/Reaction/Repository/ReactionRepository.swift delete mode 100644 14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/RealEmojiAPIWorker.swift delete mode 100644 14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/RealEmojiAPIs.swift delete mode 100644 14th-team5-iOS/Data/Sources/APIs/RealEmoji/Repository/RealEmojiRepository.swift rename 14th-team5-iOS/Data/Sources/{APIs/Family/Repository => Repositories}/FamilyRepository.swift (59%) rename 14th-team5-iOS/Data/Sources/{APIs/Members => }/Repositories/MembersRepository.swift (97%) create mode 100644 14th-team5-iOS/Data/Sources/Repositories/ReactionRepository.swift create mode 100644 14th-team5-iOS/Data/Sources/Repositories/RealEmojiRepository.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/FamilyCreatedAtResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Family/DataMapping/FamilyCreatedAtResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/FamilyCreatedAtResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Family/DataMapping/FamilyCreatedAtResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/FamilyNameResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Family/DataMapping/FamilyNameResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/FamilyNameResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Family/DataMapping/FamilyNameResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/UpdateFamilyNameRequestDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Family/DataMapping/UpdateFamilyNameRequestDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/UpdateFamilyNameRequestDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Family/DataMapping/UpdateFamilyNameRequestDTO.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIWorker.swift deleted file mode 100644 index f82ffe34c..000000000 --- a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIWorker.swift +++ /dev/null @@ -1,148 +0,0 @@ -// -// AddFamiliyAPIWorker.swift -// Data -// -// Created by 김건우 on 12/20/23. -// - -import Core -import Domain -import Foundation - -import RxSwift - -typealias FamilyAPIWorker = FamilyAPIs.Worker -extension FamilyAPIs { - public final class Worker: APIWorker { - static let queue = { - ConcurrentDispatchQueueScheduler(queue: DispatchQueue(label: "FamilyAPIQueue", qos: .utility)) - }() - - public override init() { - super.init() - self.id = "FamilyAPIWorker" - } - } -} - - -// MARK: - Extensions - -extension FamilyAPIWorker { - - - // MARK: - Join Family - - public func joinFamily(body: JoinFamilyRequestDTO) -> Single { - let spec = FamilyAPIs.joinFamily.spec - // TODO: - 자동으로 헤더 집어넣게 코드 리팩토링하기 - let accessToken = App.Repository.token.accessToken.value - let headers = BibbiHeader.commonHeaders(accessToken: accessToken?.accessToken) - - return request(spec: spec, headers: headers, jsonEncodable: body) - .subscribe(on: Self.queue) - .map(JoinFamilyResponseDTO.self) - .catchAndReturn(nil) - .asSingle() - } - - - // MARK: - ResignFamily - - public func resignFamily() -> Single { - let spec = FamilyAPIs.resignFamily.spec - - return request(spec: spec) - .subscribe(on: Self.queue) - .map(DefaultResponseDTO.self) - .catchAndReturn(nil) - .asSingle() - } - - - // MARK: - CreateFamily - - public func createFamily() -> Single { - let spec = FamilyAPIs.createFamily.spec - - return request(spec: spec) - .subscribe(on: Self.queue) - .map(CreateFamilyResponseDTO.self) - .catchAndReturn(nil) - .asSingle() - } - - - // MARK: - Fetch Invititaion URL - - public func fetchInvitationLink(familyId: String) -> Single { - let spec = FamilyAPIs.fetchInvitationLink(familyId).spec - - return request(spec: spec) - .subscribe(on: Self.queue) - .map(FamilyInvitationLinkResponseDTO.self) - .catchAndReturn(nil) - .asSingle() - } - - - // MARK: - Fetch FamilyCreatedAt - - public func fetchFamilyCreatedAt(familyId: String) -> Single { - let spec = FamilyAPIs.fetchFamilyCreatedAt(familyId).spec - - return request(spec: spec) - .subscribe(on: Self.queue) - .map(FamilyCreatedAtResponseDTO.self) - .catchAndReturn(nil) - .asSingle() - } - - - - // MARK: - Fetch Family Member - - public func fetchPaginationFamilyMember(query: FamilyPaginationQuery) -> Single { - let page = query.page - let size = query.size - let spec = FamilyAPIs.fetchPaginationFamilyMembers(page, size).spec - - return request(spec: spec) - .subscribe(on: Self.queue) - .map(PaginationResponseFamilyMemberProfileDTO.self) - .catchAndReturn(nil) - .asSingle() - } - - // MARK: - Change Family Name - - public func updateFamilyName( - familyId: String, - body: UpdateFamilyNameRequestDTO - ) -> Single { - let spec = FamilyAPIs.updateFamilyName(familyId).spec - - let accessToken = App.Repository.token.accessToken.value?.accessToken ?? "" - //TODO: Interceptor에서 CommonHedaer 넣어주지 않고 있음 - return request(spec: spec, headers: [BibbiHeader.acceptJson, BibbiHeader.xAppKey, BibbiHeader.xAuthToken(accessToken)], jsonEncodable: body) - .subscribe(on: Self.queue) - .map(FamilyNameResponseDTO.self) - .catchAndReturn(nil) - .asSingle() - } - - // MARK: - Fetch Family Info - - public func fetchFamilyGroupInfo() -> Single { - let spec = FamilyAPIs.fetchFamilyInfo.spec - - return request(spec: spec) - .subscribe(on: Self.queue) - .map(FamilyGroupInfoResponseDTO.self) - .catchAndReturn(nil) - .asSingle() - } -} - - - diff --git a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIs.swift deleted file mode 100644 index a546453ab..000000000 --- a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/FamilyAPIs.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// AddFamiliyAPI.swift -// Data -// -// Created by 김건우 on 12/20/23. -// - -import Core -import Foundation - -enum FamilyAPIs: API { - case joinFamily - case createFamily - case resignFamily - case fetchInvitationLink(String) - case fetchFamilyCreatedAt(String) - case fetchPaginationFamilyMembers(Int, Int) - case updateFamilyName(String) - case fetchFamilyInfo - - var spec: APISpec { - switch self { - case .joinFamily: - return APISpec(method: .post, url: "\(BibbiAPI.hostApi)/me/join-family") - case .resignFamily: - return APISpec(method: .post, url: "\(BibbiAPI.hostApi)/me/quit-family") - case .createFamily: - return APISpec(method: .post, url: "\(BibbiAPI.hostApi)/me/create-family") - case let .fetchInvitationLink(familyId): - return APISpec(method: .post, url: "\(BibbiAPI.hostApi)/links/family/\(familyId)") - case let .fetchFamilyCreatedAt(familyId): - return APISpec(method: .get, url: "\(BibbiAPI.hostApi)/families/\(familyId)/created-at") - case let .fetchPaginationFamilyMembers(page, size): - return APISpec(method: .get, url: "\(BibbiAPI.hostApi)/members?type=FAMILY&page=\(page)&size=\(size)") - case let .updateFamilyName(familyId): - return APISpec(method: .put, url: "\(BibbiAPI.hostApi)/families/\(familyId)/name") - case .fetchFamilyInfo: - return APISpec(method: .get, url: "\(BibbiAPI.hostApi)/me/family-info") - } - } -} - - - diff --git a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPIs.swift new file mode 100644 index 000000000..ae20ffd73 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPIs.swift @@ -0,0 +1,32 @@ +// +// AddFamiliyAPI.swift +// Data +// +// Created by 김건우 on 12/20/23. +// + +import Core + +enum FamilyAPIs: BBAPI { + /// 가족 생성 + case createFamily + /// 가족 그룹 생성 시간 조회 + case fetchFamilyCreatedAt(_ familyId: String) + /// 가족 이름 변경 + case updateFamilyName(_ familyId: String) + + var spec: Spec { + switch self { + case .createFamily: + return .init(method: .post, path: "/families") + case .fetchFamilyCreatedAt(let familyId): + return .init(method: .get, path: "/families/\(familyId)/created-at") + case .updateFamilyName(let familyId): + return .init(method: .put, path: "/families/\(familyId)/name") + } + } + + final class Worker: BBRxAPIWorker { + init() { } + } +} diff --git a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyWorker.swift b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyWorker.swift new file mode 100644 index 000000000..e23504c6d --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyWorker.swift @@ -0,0 +1,41 @@ +// +// AddFamiliyAPIWorker.swift +// Data +// +// Created by 김건우 on 12/20/23. +// + +import Core + +import RxSwift + +typealias FamilyWorker = FamilyAPIs.Worker +extension FamilyWorker { + /// 가족이 생성된 날짜를 조회하기 위한 Method입니다. + /// HTTP Method: GET + /// - Parameters: familyId: String + /// - Returns: FamilyCreatedAtResponseDTO? + func fetchFamilyCreatedAt( + _ familyId: String + ) -> Observable { + let spec = FamilyAPIs.fetchFamilyCreatedAt(familyId).spec + + return request(spec) + } + + /// 가족방 이름을 변경하기 위한 Method입니다. + /// HTTP Method: POST + /// - Parameters: familyId: String, body: UpdateFamilyNameRequestDTO + /// - Returns: FamilyNameResponseDTO? + func updateFamilyName( + _ familyId: String, + body: UpdateFamilyNameRequestDTO + ) -> Observable { + let spec = FamilyAPIs.updateFamilyName(familyId).spec + + return request(spec) + } +} + + + diff --git a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/FamilyInvitationLinkResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/FamilyInviteView/DataMapping/FamilyInvitationLinkResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/FamilyInvitationLinkResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/FamilyInviteView/DataMapping/FamilyInvitationLinkResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/FamilyInviteView/FamilyInviteViewAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/FamilyInviteView/FamilyInviteViewAPIs.swift new file mode 100644 index 000000000..c8517deed --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/FamilyInviteView/FamilyInviteViewAPIs.swift @@ -0,0 +1,26 @@ +// +// FamilyInviteViewAPIs.swift +// Data +// +// Created by 마경미 on 27.11.24. +// + +import Core + +enum FamilyInviteViewAPIs: BBAPI { + /// 가족 초대 링크 정보 조회 + /// 토근 O: 가족 가입 프로세스 앱 페이지용 정보 조회로 이동합니다. + /// 토근 X: 딥링크 웹뷰 페이지용 정보 조회로 이동합니다. + case fetchFamilyInfoWithLink(_ linkId: String) + + var spec: Spec { + switch self { + case .fetchFamilyInfoWithLink(let linkId): + return .init(method: .get, path: "/view/family-invite/\(linkId)") + } + } + + final class Worker: BBRxAPIWorker { + init() { } + } +} diff --git a/14th-team5-iOS/Data/Sources/APIs/FamilyInviteView/FamilyInviteViewWorker.swift b/14th-team5-iOS/Data/Sources/APIs/FamilyInviteView/FamilyInviteViewWorker.swift new file mode 100644 index 000000000..c98458559 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/FamilyInviteView/FamilyInviteViewWorker.swift @@ -0,0 +1,23 @@ +// +// FamilyInviteViewWorker.swift +// Data +// +// Created by 마경미 on 27.11.24. +// + +import RxSwift + +typealias FamilyInviteViewWorker = FamilyInviteViewAPIs.Worker +extension FamilyInviteViewWorker { + /// 가족방 초대 링크를 조회하기 위한 Method입니다. + /// HTTP Method: GET + /// - Parameters: familyId: String + /// - Returns: FamilyInvitationLinkResponseDTO? + func fetchInvitationLink( + familyId: String + ) -> Observable { + let spec = FamilyInviteViewAPIs.fetchFamilyInfoWithLink(familyId).spec + + return request(spec) + } +} diff --git a/14th-team5-iOS/Data/Sources/APIs/Link/DataMapping/CreateFamilyLinkResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Link/DataMapping/CreateFamilyLinkResponseDTO.swift new file mode 100644 index 000000000..eff1741b5 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/Link/DataMapping/CreateFamilyLinkResponseDTO.swift @@ -0,0 +1,24 @@ +// +// df.swift +// Data +// +// Created by 마경미 on 28.11.24. +// + +import Domain + +struct DetailResponseDTO: Decodable { + let familyId: String + let memberId: String +} + +struct CreateFamilyLinkResponseDTO: Decodable { + let linkId: String + let url: String + let type: String + let details: DetailResponseDTO + + func toDomain() -> FamilyInvitationLinkEntity { + return .init(url: url) + } +} diff --git a/14th-team5-iOS/Data/Sources/APIs/Link/LinkAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/Link/LinkAPIs.swift new file mode 100644 index 000000000..fe9164870 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/Link/LinkAPIs.swift @@ -0,0 +1,24 @@ +// +// LinkAPIs.swift +// Data +// +// Created by 마경미 on 28.11.24. +// + +import Core + +enum LinkAPIs: BBAPI { + /// 가족 링크 생성 + case createFamilyLink(_ familyId: String) + + var spec: Spec { + switch self { + case let .createFamilyLink(familyId): + return .init(method: .post, path: "/links/family/\(familyId)") + } + } + + final class Worker: BBRxAPIWorker { + init() { } + } +} diff --git a/14th-team5-iOS/Data/Sources/APIs/Link/LinkWorker.swift b/14th-team5-iOS/Data/Sources/APIs/Link/LinkWorker.swift new file mode 100644 index 000000000..e39fd6f48 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/Link/LinkWorker.swift @@ -0,0 +1,28 @@ +// +// LinkWorker.swift +// Data +// +// Created by 마경미 on 28.11.24. +// + +import Core + +import RxSwift + +typealias LinkWorker = LinkAPIs.Worker +extension LinkWorker { + /// 가족 초대 링크를 생성하는 Method입니다. + /// HTTP Method: POST + /// - Parameters: familyId: String + /// - Returns: CreateFamilyLinkResponseDTO? + func createFamilyLink( + _ familyId: String + ) -> Observable { + let spec = LinkAPIs.createFamilyLink(familyId).spec + + return request(spec) + } +} + + + diff --git a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/CreateFamilyResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Me/DataMapping/CreateFamilyResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/CreateFamilyResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Me/DataMapping/CreateFamilyResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/FamilyGroupInfoResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Me/DataMapping/FamilyInfoResponseDTO.swift similarity index 84% rename from 14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/FamilyGroupInfoResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Me/DataMapping/FamilyInfoResponseDTO.swift index fee984d19..b4544cf1f 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/FamilyGroupInfoResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Me/DataMapping/FamilyInfoResponseDTO.swift @@ -9,14 +9,14 @@ import Foundation import Domain -public struct FamilyGroupInfoResponseDTO: Decodable { +public struct FamilyInfoResponseDTO: Decodable { let familyId: String let familyName: String? let familyNameEditorId: String? let createdAt: String } -extension FamilyGroupInfoResponseDTO { +extension FamilyInfoResponseDTO { func toDomain() -> FamilyGroupInfoEntity { return .init( familyId: familyId, diff --git a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/JoinFamilyRequestDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Me/DataMapping/JoinFamilyRequestDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/JoinFamilyRequestDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Me/DataMapping/JoinFamilyRequestDTO.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/JoinFamilyResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Me/DataMapping/JoinFamilyResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/JoinFamilyResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Me/DataMapping/JoinFamilyResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/Me/MeAPI.swift b/14th-team5-iOS/Data/Sources/APIs/Me/MeAPI.swift new file mode 100644 index 000000000..3470ba86d --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/Me/MeAPI.swift @@ -0,0 +1,37 @@ +// +// MeAPIs.swift +// Data +// +// Created by 마경미 on 27.11.24. +// + +import Core + +// TODO: MeAPIs로 이름 바꾸기 - Trash 지워야 함 +enum MeAPI: BBAPI { + /// 가족 가입하기 + case joinFamily + /// 가족 탈퇴하기 + case resignFamily + /// 가족 생성 및 가족 정보 조회 + case createFamily + /// 내 가족 정보 조회 + case fetchFamilyInfo + + var spec: Spec { + switch self { + case .joinFamily: + return .init(method: .post, path: "/me/join-family") + case .resignFamily: + return .init(method: .post, path: "/me/quit-family") + case .createFamily: + return .init(method: .post, path: "/me/create-family") + case .fetchFamilyInfo: + return .init(method: .get, path: "/me/family-info") + } + } + + final class Worker: BBRxAPIWorker { + init() { } + } +} diff --git a/14th-team5-iOS/Data/Sources/APIs/Me/MeWorker.swift b/14th-team5-iOS/Data/Sources/APIs/Me/MeWorker.swift new file mode 100644 index 000000000..49bea8d0f --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/Me/MeWorker.swift @@ -0,0 +1,53 @@ +// +// MeWorker.swift +// Data +// +// Created by 마경미 on 27.11.24. +// + +import Core + +import RxSwift + +// TODO: MeWorker로 이름 바꾸기 +typealias MeeWorker = MeAPI.Worker +extension MeeWorker { + /// 가족방 탈퇴를 위한 Method입니다. + /// HTTP Method: Post + /// - Returns: DefaultResponseDTO? + func resignFamily() -> Observable { + let spec = MeAPI.resignFamily.spec + + return request(spec) + } + + /// 가족방을 생성하기 위한 Method입니다. + /// HTTP Method: Post + /// - Returns: CreateFamilyResponseDTO? + func createFamily() -> Observable { + let spec = MeAPI.createFamily.spec + + return request(spec) + } + + /// 가족방에 참여하기 위한 Method입니다. + /// HTTP Method: Post + /// - Parameters: body: JOinFamilyRequestDTO + /// - Returns: JoinFamilyResponseDTO? + func joinFamily( + body: JoinFamilyRequestDTO + ) -> Observable { + let spec = MeAPI.joinFamily.spec + + return request(spec) + } + + /// 가족 기본 정보를 조회하기 위한 Method입니다. + /// HTTP Method: GET + /// - Returns: FamilyGroupInfoResponseDTO? + func fetchFamilyInfo() -> Observable { + let spec = MeAPI.fetchFamilyInfo.spec + + return request(spec) + } +} diff --git a/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/DataMapping/MyRealEmojiResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/MemberRealEmoji/DataMapping/MyRealEmojiResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/DataMapping/MyRealEmojiResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/MemberRealEmoji/DataMapping/MyRealEmojiResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/MemberRealEmoji/DataMapping/UploadRealEmojiRequestDTO.swift b/14th-team5-iOS/Data/Sources/APIs/MemberRealEmoji/DataMapping/UploadRealEmojiRequestDTO.swift new file mode 100644 index 000000000..c57afc306 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/MemberRealEmoji/DataMapping/UploadRealEmojiRequestDTO.swift @@ -0,0 +1,10 @@ +// +// UploadRealEmojiRequestDTO.swift +// Data +// +// Created by 마경미 on 28.11.24. +// + +struct UploadRealEmojiRequestDTO: Encodable { + let imageName: String +} diff --git a/14th-team5-iOS/Data/Sources/APIs/MemberRealEmoji/MemberRealEmojiAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/MemberRealEmoji/MemberRealEmojiAPIs.swift new file mode 100644 index 000000000..5609dc9ca --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/MemberRealEmoji/MemberRealEmojiAPIs.swift @@ -0,0 +1,40 @@ +// +// MemberRealEmojiAPIs.swift +// Data +// +// Created by 마경미 on 26.11.24. +// + +import Core + +enum MemberRealEmojiAPIs: BBAPI { + /// 회원의 리얼 이모지 조회 + case fetchMemberRealEmoji(_ memberId: String) + /// 회원의 리얼 이모지 추가 + case addMemberRealEmoji(_ mbmerId: String) + /// 회원의 리얼 이모지 변경 + case updateMemberRealEmoji(_ memberId: String) + /// 리얼 이모지 사진 presigned url 요청 + case uploadRealEmoji(_ mebmerId: String, _ body: UploadRealEmojiRequestDTO) + + var spec: Spec { + switch self { + case .fetchMemberRealEmoji(let memberId): + return .init(method: .get, path: "/members/\(memberId)/real-emoji") + case .addMemberRealEmoji(let memberId): + return .init(method: .post, path: "/members/\(memberId)/real-emoji") + case .updateMemberRealEmoji(let memberId): + return .init(method: .put, path: "/members/\(memberId)/real-emoji") + case .uploadRealEmoji(let memberId, let body): + return .init( + method: .post, + path: "/members/\(memberId)/real-emoji/image-upload-request", + bodyParametersEncodable: body + ) + } + } + + final class Worker: BBRxAPIWorker { + init() { } + } +} diff --git a/14th-team5-iOS/Data/Sources/APIs/MemberRealEmoji/MemberRealEmojiWorker.swift b/14th-team5-iOS/Data/Sources/APIs/MemberRealEmoji/MemberRealEmojiWorker.swift new file mode 100644 index 000000000..9bc6b2720 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/MemberRealEmoji/MemberRealEmojiWorker.swift @@ -0,0 +1,22 @@ +// +// MemberRealEmojiWorker.swift +// Data +// +// Created by 마경미 on 26.11.24. +// + +import RxSwift + +typealias MemberRealEmojiWorker = MemberRealEmojiAPIs.Worker +extension MemberRealEmojiWorker { + + /// 회원에 등록된 리얼 이모지를 조회하는 Method입니다. + /// HTTP Method: GET + /// - Parameters: memberId: String + /// - Returns: MyRealEmojiResponseDTO? + func fetchMyRealEmoji(memberId: String) -> Observable { + let spec = MemberRealEmojiAPIs.fetchMemberRealEmoji(memberId).spec + + return request(spec) + } +} diff --git a/14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/DataMapping/CreateMemberPickResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Members/DataMapping/CreateMemberPickResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/DataMapping/CreateMemberPickResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Members/DataMapping/CreateMemberPickResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/DataMapping/CreateMemberPresignedURLRequestDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Members/DataMapping/CreateMemberPresignedURLRequestDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/DataMapping/CreateMemberPresignedURLRequestDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Members/DataMapping/CreateMemberPresignedURLRequestDTO.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/DataMapping/CreateMemberPresignedURLResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Members/DataMapping/CreateMemberPresignedURLResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/DataMapping/CreateMemberPresignedURLResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Members/DataMapping/CreateMemberPresignedURLResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/DataMapping/DeleteMemberResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Members/DataMapping/DeleteMemberResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/DataMapping/DeleteMemberResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Members/DataMapping/DeleteMemberResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/Members/DataMapping/FamilyMemberQueryDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Members/DataMapping/FamilyMemberQueryDTO.swift new file mode 100644 index 000000000..28c26fa90 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/Members/DataMapping/FamilyMemberQueryDTO.swift @@ -0,0 +1,12 @@ +// +// FamilyMemberQueryDTO.swift +// Data +// +// Created by 마경미 on 27.11.24. +// + +struct FamilyMemberQueryDTO: Encodable { + let type: String + let page: Int + let size: Int +} diff --git a/14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/DataMapping/MembersProfileResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Members/DataMapping/MembersProfileResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/DataMapping/MembersProfileResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Members/DataMapping/MembersProfileResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/FamilyMemberProfileResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Members/DataMapping/PaginationResponseMembersDTO.swift similarity index 74% rename from 14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/FamilyMemberProfileResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Members/DataMapping/PaginationResponseMembersDTO.swift index 325389b96..798830b73 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPI/DataMapping/FamilyMemberProfileResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Members/DataMapping/PaginationResponseMembersDTO.swift @@ -8,7 +8,7 @@ import Domain import Foundation -public struct PaginationResponseFamilyMemberProfileDTO: Decodable { +public struct PaginationResponseMembersDTO: Decodable { private enum CodingKeys: String, CodingKey { case currentPage case totalPage @@ -20,11 +20,11 @@ public struct PaginationResponseFamilyMemberProfileDTO: Decodable { var totalPage: Int var itemPerPage: Int var hasNext: Bool - var results: [FamilyMemberProfileResponseDTO] + var results: [MemberProfileResponseDTO] } -extension PaginationResponseFamilyMemberProfileDTO { - public struct FamilyMemberProfileResponseDTO: Decodable { +extension PaginationResponseMembersDTO { + public struct MemberProfileResponseDTO: Decodable { private enum CodingKeys: String, CodingKey { case memberId case name @@ -38,7 +38,7 @@ extension PaginationResponseFamilyMemberProfileDTO { } } -extension PaginationResponseFamilyMemberProfileDTO { +extension PaginationResponseMembersDTO { func toDomain() -> PaginationResponseFamilyMemberProfileEntity { return .init( results: results.map { $0.toDomain() } @@ -46,7 +46,7 @@ extension PaginationResponseFamilyMemberProfileDTO { } } -extension PaginationResponseFamilyMemberProfileDTO.FamilyMemberProfileResponseDTO { +extension PaginationResponseMembersDTO.MemberProfileResponseDTO { func toDomain() -> FamilyMemberProfileEntity { return .init( memberId: memberId, diff --git a/14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/DataMapping/UpdateMemberImageRequestDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Members/DataMapping/UpdateMemberImageRequestDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/DataMapping/UpdateMemberImageRequestDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Members/DataMapping/UpdateMemberImageRequestDTO.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/DataMapping/UpdateMemberNameRequestDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Members/DataMapping/UpdateMemberNameRequestDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/DataMapping/UpdateMemberNameRequestDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Members/DataMapping/UpdateMemberNameRequestDTO.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/DataMapping/UpdateMemberNameResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Members/DataMapping/UpdateMemberNameResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/DataMapping/UpdateMemberNameResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Members/DataMapping/UpdateMemberNameResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/MembersAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/Members/MembersAPIs.swift similarity index 86% rename from 14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/MembersAPIs.swift rename to 14th-team5-iOS/Data/Sources/APIs/Members/MembersAPIs.swift index 130ea0f70..6424b5380 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/MembersAPIs.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Members/MembersAPIs.swift @@ -5,13 +5,11 @@ // Created by Kim dohyun on 6/5/24. // -import Foundation - import Core - - enum MembersAPIs: BBAPI { + /// 가족 구성원 프로필 조회 API + case fetchFamilyMembers(_ query: FamilyMemberQueryDTO) /// 회원 프로필 조회 API case fetchMember(memberId: String) /// 회원 탈퇴 API @@ -30,6 +28,12 @@ enum MembersAPIs: BBAPI { var spec: Spec { switch self { + case let .fetchFamilyMembers(query): + return .init( + method: .get, + path: "/members", + queryParametersEncodable: query + ) case let .fetchMember(memberId): return Spec(method: .get, path: "/members/\(memberId)") case let .deleteMember(memberId): @@ -52,4 +56,3 @@ enum MembersAPIs: BBAPI { init() { } } } - diff --git a/14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/MembersAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/Members/MembersWorker.swift similarity index 87% rename from 14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/MembersAPIWorker.swift rename to 14th-team5-iOS/Data/Sources/APIs/Members/MembersWorker.swift index 510122902..4bf4a9cf3 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Members/MemberAPI/MembersAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Members/MembersWorker.swift @@ -5,15 +5,14 @@ // Created by Kim dohyun on 6/5/24. // -import Core import Foundation -import RxSwift - +import Core -typealias MembersAPIWorker = MembersAPIs.Worker +import RxSwift -extension MembersAPIWorker { +typealias MembersWorker = MembersAPIs.Worker +extension MembersWorker { /// 회원 프로필 정보를 조회하기 위한 Method 입니다. /// HTTP Method : GET @@ -97,4 +96,17 @@ extension MembersAPIWorker { func updateS3MemberImageUpload(_ presignedURL: String, image: Data) -> Observable { return upload(presignedURL, with: image) } + + /// 가족 프로필 구성원을 조회하기 위한 Method 입니다. + /// HTTP Method : GET + /// - Parameters + /// - query: 페이지네이션을 위한 query dto + /// - Returns : PaginationResponseMembersDTO + func fetchPaginationMembers( + query: FamilyMemberQueryDTO + ) -> Observable { + let spec = MembersAPIs.fetchFamilyMembers(query).spec + + return request(spec) + } } diff --git a/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/DataMapping/AddRealEmojiRequestDTO.swift b/14th-team5-iOS/Data/Sources/APIs/PostRealEmoji/DataMapping/AddRealEmojiRequestDTO.swift similarity index 92% rename from 14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/DataMapping/AddRealEmojiRequestDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/PostRealEmoji/DataMapping/AddRealEmojiRequestDTO.swift index 44b65e49a..22b612090 100644 --- a/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/DataMapping/AddRealEmojiRequestDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/PostRealEmoji/DataMapping/AddRealEmojiRequestDTO.swift @@ -5,7 +5,7 @@ // Created by 마경미 on 28.01.24. // -import Foundation +import Core struct AddRealEmojiParameters: Codable { let postId: String diff --git a/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/DataMapping/AddRealEmojiResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/PostRealEmoji/DataMapping/AddRealEmojiResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/DataMapping/AddRealEmojiResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/PostRealEmoji/DataMapping/AddRealEmojiResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/DataMapping/FetchRealEmojiListRequestDTO.swift b/14th-team5-iOS/Data/Sources/APIs/PostRealEmoji/DataMapping/FetchRealEmojiListRequestDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/DataMapping/FetchRealEmojiListRequestDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/PostRealEmoji/DataMapping/FetchRealEmojiListRequestDTO.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/DataMapping/FetchRealEmojiListResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/PostRealEmoji/DataMapping/FetchRealEmojiListResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/DataMapping/FetchRealEmojiListResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/PostRealEmoji/DataMapping/FetchRealEmojiListResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/DataMapping/RemoveRealEmojiRequestDTO.swift b/14th-team5-iOS/Data/Sources/APIs/PostRealEmoji/DataMapping/RemoveRealEmojiRequestDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/DataMapping/RemoveRealEmojiRequestDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/PostRealEmoji/DataMapping/RemoveRealEmojiRequestDTO.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/DataMapping/RemoveRealEmojiResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/PostRealEmoji/DataMapping/RemoveRealEmojiResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/DataMapping/RemoveRealEmojiResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/PostRealEmoji/DataMapping/RemoveRealEmojiResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/PostRealEmoji/RealEmojiAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/PostRealEmoji/RealEmojiAPIWorker.swift new file mode 100644 index 000000000..6b61071fd --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/PostRealEmoji/RealEmojiAPIWorker.swift @@ -0,0 +1,55 @@ +// +// RealEmojiAPIWorker.swift +// Data +// +// Created by 마경미 on 22.01.24. +// + +import Foundation + +import Core + +import Alamofire +import RxSwift + +typealias PostRealEmojiWorker = PostRealEmojiAPIs.Worker +extension PostRealEmojiWorker { + + /// post에 등록된 리얼 이모지를 조회합니다. + /// HTTP Method: GET + /// - Parameters: FetchRealEmojiQuery + /// - Returns: FetchRealEmojiListResponseDTO? + func fetchRealEmoji( + _ postId: String + ) -> Observable { + let spec = PostRealEmojiAPIs.fetchRealEomjiReactions(postId).spec + + return request(spec) + } + + /// post에 리얼 이모지 리액션을 등록합니다. + /// HTTP Method: POST + /// - Parameters: CreateReactionQuery, CreateReactionRequest + /// - Returns: AddRealEmojiResponseDTO? + func addRealEmoji( + _ postId: String, + body: AddRealEmojiRequestDTO + ) -> Observable { + let spec = PostRealEmojiAPIs.addRealEmojiReaction(postId, body).spec + + return request(spec) + } + + /// post에 리얼 이모지 리액션을 삭제합니다. + /// HTTP Method: POST + /// - Parameters: RemoveRealEmojiQuery + /// - Returns: RemoveRealEmojiResponseDTO? + func removeRealEmoji( + _ postId: String, + _ realEmojiId: String + ) -> Observable { + let spec = PostRealEmojiAPIs.removeRealEmojiReactions(postId, realEmojiId).spec + + return request(spec) + } +} diff --git a/14th-team5-iOS/Data/Sources/APIs/PostRealEmoji/RealEmojiAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/PostRealEmoji/RealEmojiAPIs.swift new file mode 100644 index 000000000..eea34e982 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/PostRealEmoji/RealEmojiAPIs.swift @@ -0,0 +1,40 @@ +// +// RealEmojiAPIS.swift +// Data +// +// Created by 마경미 on 22.01.24. +// + +import Core +import Foundation + +enum PostRealEmojiAPIs: BBAPI { + /// 게시물에 리얼 이모지 등록 + case addRealEmojiReaction(_ postId: String, _ body: AddRealEmojiRequestDTO) + /// 게시물에서 리얼 이모지 삭제 + case removeRealEmojiReactions(_ postId: String, _ realEmojiId: String) + /// 게시물의 리얼 이모지 전체 조회 + case fetchRealEomjiReactions(_ postId: String) + + var spec: Spec { + switch self { + case .addRealEmojiReaction(let postId, let body): + return .init( + method: .post, + path: "/posts/\(postId)/real-emoji", + bodyParametersEncodable: body + ) + case .removeRealEmojiReactions(let postId, let realEmojiId): + return .init( + method: .delete, + path: "/posts/\(postId)/real-emoji/\(realEmojiId)" + ) + case .fetchRealEomjiReactions(let postId): + return .init(method: .get, path: "/posts/\(postId)/real-emoji") + } + } + + final class Worker: BBRxAPIWorker { + init() { } + } +} diff --git a/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/DataMapping/AddReactionRequestDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Reaction/DataMapping/AddReactionRequestDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/DataMapping/AddReactionRequestDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Reaction/DataMapping/AddReactionRequestDTO.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/DataMapping/AddReactionResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Reaction/DataMapping/AddReactionResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/DataMapping/AddReactionResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Reaction/DataMapping/AddReactionResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/DataMapping/FetchReactionRequestDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Reaction/DataMapping/FetchReactionRequestDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/DataMapping/FetchReactionRequestDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Reaction/DataMapping/FetchReactionRequestDTO.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/DataMapping/FetchReactionResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Reaction/DataMapping/FetchReactionResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/DataMapping/FetchReactionResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Reaction/DataMapping/FetchReactionResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/DataMapping/RemoveReactionRequestDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Reaction/DataMapping/RemoveReactionRequestDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/DataMapping/RemoveReactionRequestDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Reaction/DataMapping/RemoveReactionRequestDTO.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/DataMapping/RemoveReactionResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Reaction/DataMapping/RemoveReactionResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/DataMapping/RemoveReactionResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Reaction/DataMapping/RemoveReactionResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/ReactionAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/ReactionAPIWorker.swift deleted file mode 100644 index debd90141..000000000 --- a/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/ReactionAPIWorker.swift +++ /dev/null @@ -1,76 +0,0 @@ -// -// EmojiAPIWorker.swift -// Data -// -// Created by 마경미 on 01.01.24. -// - -import Foundation - -import Core -import Domain - -import RxSwift - -typealias ReactionAPIWorker = ReactionAPIs.Worker -extension ReactionAPIs { - public final class Worker: APIWorker { - static let queue = { - ConcurrentDispatchQueueScheduler(queue: DispatchQueue(label: "ReactionAPIQueue", qos: .utility)) - }() - - public override init() { - super.init() - self.id = "ReactionAPIWorker" - } - - var headers: [APIHeader] { - var headers: [any APIHeader] = [] - - _headers.subscribe(onNext: { result in - if let unwrappedHeaders = result { - headers = unwrappedHeaders - } - }).dispose() - - return headers - } - } -} - -extension ReactionAPIWorker { - func fetchReaction(query: Domain.FetchReactionQuery) -> RxSwift.Single<[EmojiEntity]?> { - let query = FetchReactionRequestDTO(postId: query.postId) - let spec = ReactionAPIs.fetchReactions(query).spec - return request(spec: spec, headers: headers) - .subscribe(on: Self.queue) - .map(FetchReactionResponseDTO.self) - .catchAndReturn(nil) - .map { - $0?.toDomain() - } - .asSingle() - } - - func addReaction(query: Domain.CreateReactionQuery, body: Domain.CreateReactionRequest) -> RxSwift.Single { - let requestDTO = AddReactionRequestDTO(content: body.emojiId) - let spec = ReactionAPIs.addReactions(query.postId).spec - return request(spec: spec, headers: headers, jsonEncodable: requestDTO) - .subscribe(on: Self.queue) - .map(AddReactionResponseDTO.self) - .catchAndReturn(nil) - .map { $0?.toDomain() } - .asSingle() - } - - func removeReaction(query: Domain.RemoveReactionQuery, body: Domain.RemoveReactionRequest) -> RxSwift.Single { - let requestDTO = RemoveReactionRequestDTO(content: body.content.emojiString) - let spec = ReactionAPIs.removeReactions(query.postId).spec - return request(spec: spec, headers: headers, jsonEncodable: requestDTO) - .subscribe(on: Self.queue) - .map(RemoveReactionResponseDTO.self) - .catchAndReturn(nil) - .map { $0?.toDomain() } - .asSingle() - } -} diff --git a/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/ReactionAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/ReactionAPIs.swift deleted file mode 100644 index f181432f6..000000000 --- a/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPI/ReactionAPIs.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// EmojiAPIs.swift -// Data -// -// Created by 마경미 on 01.01.24. -// - -import Core -import Foundation - -enum ReactionAPIs: API { - case addReactions(String) - case removeReactions(String) - case fetchReactions(FetchReactionRequestDTO) - - var spec: APISpec { - switch self { - case let .addReactions(postId): - let urlString = "\(BibbiAPI.hostApi)/posts/\(postId)/reactions" - return APISpec(method: .post, url: urlString) - case let .removeReactions(postId): - let urlString = "\(BibbiAPI.hostApi)/posts/\(postId)/reactions" - return APISpec(method: .delete, url: urlString) - case let .fetchReactions(postId): - let urlString = "\(BibbiAPI.hostApi)/posts/\(postId.postId)/reactions" - return APISpec(method: .get, url: urlString) - } - } -} diff --git a/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPIWorker.swift new file mode 100644 index 000000000..a7531155e --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPIWorker.swift @@ -0,0 +1,39 @@ +// +// EmojiAPIWorker.swift +// Data +// +// Created by 마경미 on 01.01.24. +// + +import Core + +import RxSwift + +typealias ReactionAPIWorker = ReactionAPIs.Worker +extension ReactionAPIWorker { + func fetchReaction( + _ postId: String + ) -> Observable { + let spec = ReactionAPIs.fetchReactions(postId).spec + + return request(spec) + } + + func addReaction( + _ postId: String, + _ body: AddReactionRequestDTO + ) -> Observable { + let spec = ReactionAPIs.addReactions(postId, body).spec + + return request(spec) + } + + func removeReaction( + _ postId: String, + _ body: RemoveReactionRequestDTO + ) -> Observable { + let spec = ReactionAPIs.removeReactions(postId, body).spec + + return request(spec) + } +} diff --git a/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPIs.swift new file mode 100644 index 000000000..723c67a6b --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/Reaction/ReactionAPIs.swift @@ -0,0 +1,43 @@ +// +// EmojiAPIs.swift +// Data +// +// Created by 마경미 on 01.01.24. +// + +import Core + +enum ReactionAPIs: BBAPI { + /// 게시물 일반 반응 추가 + case addReactions(_ postId: String, _ body: AddReactionRequestDTO) + /// 게시물 일반 반응 삭제 + case removeReactions(_ postId: String, _ body: RemoveReactionRequestDTO) + /// 게시물 일반 반응 전체 조회 + case fetchReactions(_ postId: String) + + var spec: Spec { + switch self { + case .addReactions(let postId, let body): + return .init( + method: .post, + path: "/posts/\(postId)/reactions", + bodyParametersEncodable: body + ) + case .removeReactions(let postId, let body): + return .init( + method: .delete, + path: "/posts/\(postId)/reactions", + bodyParametersEncodable: body + ) + case .fetchReactions(let postId): + return .init( + method: .get, + path: "/posts/\(postId)/reactions" + ) + } + } + + final class Worker: BBRxAPIWorker { + init() { } + } +} diff --git a/14th-team5-iOS/Data/Sources/APIs/Reaction/Repository/ReactionRepository.swift b/14th-team5-iOS/Data/Sources/APIs/Reaction/Repository/ReactionRepository.swift deleted file mode 100644 index 24a3a4ed6..000000000 --- a/14th-team5-iOS/Data/Sources/APIs/Reaction/Repository/ReactionRepository.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// ReactionRepository.swift -// Data -// -// Created by 마경미 on 04.06.24. -// - -import Foundation - -import Domain - -import RxSwift - -public final class ReactionRepository: ReactionRepositoryProtocol { - - private let disposeBag: DisposeBag = DisposeBag() - private let reactionAPIWorker: ReactionAPIWorker = ReactionAPIWorker() - - public init () { } - - public func addReaction(query: Domain.CreateReactionQuery, body: Domain.CreateReactionRequest) -> RxSwift.Single { - return reactionAPIWorker.addReaction(query: query, body: body) - } - - public func removeReaction(query: Domain.RemoveReactionQuery, body: Domain.RemoveReactionRequest) -> RxSwift.Single { - return reactionAPIWorker.removeReaction(query: query, body: body) - } - - public func fetchReaction(query: Domain.FetchReactionQuery) -> RxSwift.Single<[Domain.EmojiEntity]?> { - return reactionAPIWorker.fetchReaction(query: query) - } -} diff --git a/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/RealEmojiAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/RealEmojiAPIWorker.swift deleted file mode 100644 index 7c36f155a..000000000 --- a/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/RealEmojiAPIWorker.swift +++ /dev/null @@ -1,97 +0,0 @@ -// -// RealEmojiAPIWorker.swift -// Data -// -// Created by 마경미 on 22.01.24. -// - -import Foundation - -import Core -import Domain - -import Alamofire -import RxSwift - -typealias RealEmojiAPIWorker = RealEmojiAPIs.Worker -extension RealEmojiAPIs { - public final class Worker: APIWorker { - static let queue = { - ConcurrentDispatchQueueScheduler(queue: DispatchQueue(label: "RealEmojiAPIQueue", qos: .utility)) - }() - - public override init() { - super.init() - self.id = "RealEmojiAPIWorker" - } - - var headers: [APIHeader] { - var headers: [any APIHeader] = [] - - _headers.subscribe(onNext: { result in - if let unwrappedHeaders = result { - headers = unwrappedHeaders - } - }).dispose() - - return headers - } - } -} - -extension RealEmojiAPIWorker { - - func fetchRealEmoji(query: FetchRealEmojiQuery) -> Single<[EmojiEntity]?> { - let query = FetchRealEmojiListParameter(postId: query.postId) - let spec = RealEmojiAPIs.fetchRealEmojiList(query).spec - - return request(spec: spec, headers: headers) - .subscribe(on: Self.queue) - .map(FetchRealEmojiListResponseDTO.self) - .catchAndReturn(nil) - .map { - $0?.toDomain() - } - .asSingle() - } - - func fetchMyRealEmoji() -> Single<[MyRealEmojiEntity?]> { - let spec = RealEmojiAPIs.fetchMyRealEmoji.spec - - return request(spec: spec, headers: headers) - .subscribe(on: Self.queue) - .map(MyRealEmojiResponseDTO.self) - .catchAndReturn(nil) - .map { - return $0?.toDomain() ?? Array(repeating: nil, count: 5) - } - .asSingle() - } - - func addRealEmoji(query: CreateReactionQuery, body: CreateReactionRequest) -> Single { - let spec = RealEmojiAPIs.addRealEmoji(.init(postId: query.postId)).spec - let body = AddRealEmojiRequestDTO(realEmojiId: body.emojiId) - - return request(spec: spec, headers: headers, jsonEncodable: body) - .subscribe(on: Self.queue) - .map(AddRealEmojiResponseDTO.self) - .catchAndReturn(nil) - .map { - return $0?.toDomain() - } - .asSingle() - } - - func removeRealEmoji(query: RemoveRealEmojiQuery) -> Single { - let spec = RealEmojiAPIs.removeRealEmoji(.init(postId: query.postId, realEmojiId: query.realEmojiId)).spec - - return request(spec: spec, headers: headers) - .subscribe(on: Self.queue) - .map(RemoveRealEmojiResponseDTO.self) - .catchAndReturn(nil) - .map { - return $0?.toDomain() - } - .asSingle() - } -} diff --git a/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/RealEmojiAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/RealEmojiAPIs.swift deleted file mode 100644 index f7af176af..000000000 --- a/14th-team5-iOS/Data/Sources/APIs/RealEmoji/RealEmojiAPI/RealEmojiAPIs.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// RealEmojiAPIS.swift -// Data -// -// Created by 마경미 on 22.01.24. -// - -import Core -import Foundation - -enum RealEmojiAPIs: API { - case fetchRealEmojiList(FetchRealEmojiListParameter) - case fetchMyRealEmoji - case addRealEmoji(AddRealEmojiParameters) - case removeRealEmoji(RemoveRealEmojiParameters) - - public var spec: APISpec { - switch self { - case .addRealEmoji(let parameter): - let urlString = "\(BibbiAPI.hostApi)/posts/\(parameter.postId)/real-emoji" - return APISpec(method: .post, url: urlString) - case .fetchRealEmojiList(let parameter): - let urlString = "\(BibbiAPI.hostApi)/posts/\(parameter.postId)/real-emoji" - return APISpec(method: .get, url: urlString) - case .fetchMyRealEmoji: - let memberId = App.Repository.member.memberID.value - let urlString = "\(BibbiAPI.hostApi)/members/\(memberId ?? "")/real-emoji" - return APISpec(method: .get, url: urlString) - case .removeRealEmoji(let parameter): - let urlString = "\(BibbiAPI.hostApi)/posts/\(parameter.postId)/real-emoji/\(parameter.realEmojiId)" - return APISpec(method: .delete, url: urlString) - } - } -} diff --git a/14th-team5-iOS/Data/Sources/APIs/RealEmoji/Repository/RealEmojiRepository.swift b/14th-team5-iOS/Data/Sources/APIs/RealEmoji/Repository/RealEmojiRepository.swift deleted file mode 100644 index 951e85b7a..000000000 --- a/14th-team5-iOS/Data/Sources/APIs/RealEmoji/Repository/RealEmojiRepository.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// RealEmojiRepository.swift -// Data -// -// Created by 마경미 on 04.06.24. -// - -import Foundation - -import Domain - -import RxSwift - -public final class RealEmojiRepository: RealEmojiRepositoryProtocol { - - public let disposeBag: DisposeBag = DisposeBag() - private let realEmojiAPIWorker: RealEmojiAPIWorker = RealEmojiAPIWorker() - - public init () { } - - public func fetchMyRealEmoji() -> RxSwift.Single<[Domain.MyRealEmojiEntity?]> { - realEmojiAPIWorker.fetchMyRealEmoji() - } - - public func addRealEmoji(query: Domain.CreateReactionQuery, body: Domain.CreateReactionRequest) -> RxSwift.Single { - realEmojiAPIWorker.addRealEmoji(query: query, body: body) - } - - public func fetchRealEmoji(query: Domain.FetchRealEmojiQuery) -> RxSwift.Single<[Domain.EmojiEntity]?> { - realEmojiAPIWorker.fetchRealEmoji(query: query) - } - - public func removeRealEmoji(query: Domain.RemoveRealEmojiQuery) -> RxSwift.Single { - realEmojiAPIWorker.removeRealEmoji(query: query) - } -} diff --git a/14th-team5-iOS/Data/Sources/APIs/Family/Repository/FamilyRepository.swift b/14th-team5-iOS/Data/Sources/Repositories/FamilyRepository.swift similarity index 59% rename from 14th-team5-iOS/Data/Sources/APIs/Family/Repository/FamilyRepository.swift rename to 14th-team5-iOS/Data/Sources/Repositories/FamilyRepository.swift index f67d0f9eb..55c3da342 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Family/Repository/FamilyRepository.swift +++ b/14th-team5-iOS/Data/Sources/Repositories/FamilyRepository.swift @@ -7,7 +7,6 @@ import Core import Domain -import Foundation import RxSwift @@ -16,8 +15,11 @@ public final class FamilyRepository: FamilyRepositoryProtocol { public let disposeBag: DisposeBag = DisposeBag() - private let familyApiWorker: FamilyAPIWorker = FamilyAPIWorker() - + private let linkWorker: LinkWorker = LinkWorker() + private let meWorker: MeeWorker = MeeWorker() + private let membersWorker: MembersWorker = MembersWorker() + private let familyInviteViewWorker: FamilyInviteViewWorker = FamilyInviteViewWorker() + private let familyWorker: FamilyWorker = FamilyWorker() private let familyUserDefaults: FamilyInfoUserDefaultsType = FamilyInfoUserDefaults() // MARK: - Intializer @@ -25,16 +27,16 @@ public final class FamilyRepository: FamilyRepositoryProtocol { public init() { } } +// TODO: do 뒤에 하는 일 주석으로 적어주세용:) extension FamilyRepository { - - // MARK: - Join Family - - public func joinFamily(body: JoinFamilyRequest) -> Observable { + public func joinFamily( + body: JoinFamilyRequest + ) -> Observable { let body = JoinFamilyRequestDTO(inviteCode: body.inviteCode) - return familyApiWorker.joinFamily(body: body) + return meWorker.joinFamily(body: body) .map { $0?.toDomain() } - .do(onSuccess: { [weak self] in + .do { [weak self] in guard let self else { return } self.familyUserDefaults.saveFamilyId($0?.familyId) self.familyUserDefaults.saveFamilyCreatedAt($0?.createdAt) @@ -44,32 +46,28 @@ extension FamilyRepository { App.Repository.member.familyCreatedAt.accept($0?.createdAt) // TODO: - 로직 분리하기 fetchPaginationFamilyMembers(query: .init()) - }) - .asObservable() + } } - // MARK: - Resign Family - - public func resignFamily() -> Observable { - return familyApiWorker.resignFamily() + public func resignFamily( + ) -> Observable { + return meWorker.resignFamily() .map { $0?.toDomain() } - .do(onSuccess: { [weak self] in + .do { [weak self] in guard let self else { return } if let sucess = $0?.success, sucess { self.familyUserDefaults.remove(forKey: .familyId) self.familyUserDefaults.remove(forKey: .familyName) self.familyUserDefaults.remove(forKey: .familyCreatedAt) } - }) - .asObservable() + } } - // MARK: - Create Family - - public func createFamily() -> Observable { - return familyApiWorker.createFamily() + public func createFamily( + ) -> Observable { + return meWorker.createFamily() .map { $0?.toDomain() } - .do(onSuccess: { [weak self] in + .do { [weak self] in guard let self else { return } self.familyUserDefaults.saveFamilyId($0?.familyId) self.familyUserDefaults.saveFamilyCreatedAt($0?.createdAt) @@ -77,83 +75,88 @@ extension FamilyRepository { // TODO: - 리팩토링된 FamilyUserDefaults로 바꾸기 App.Repository.member.familyId.accept($0?.familyId) App.Repository.member.familyCreatedAt.accept($0?.createdAt) - }) - .asObservable() + } } - // MARK: - Fetch Family ID - - public func fetchFamilyId() -> String? { + public func fetchFamilyId( + ) -> String? { familyUserDefaults.loadFamilyId() } - - // MARK: - Fetch Family CreatedAt - - public func fetchFamilyCreatedAt() -> Observable { + public func fetchFamilyCreatedAt( + ) -> Observable { // 다시 리팩토링하기 if let createdAt = familyUserDefaults.loadFamilyCreatedAt() { return Observable.just(FamilyCreatedAtEntity(createdAt: createdAt)) } else { guard let familyId = familyUserDefaults.loadFamilyId() - else { return .error(NSError()) } // 에러 타입 다시 정의하기 - return familyApiWorker.fetchFamilyCreatedAt(familyId: familyId) + else { return .just(nil) } // 에러 타입 다시 정의하기 + return familyWorker.fetchFamilyCreatedAt(familyId) .map { $0?.toDomain() } - .do(onSuccess: { [weak self] in + .do { [weak self] in guard let self else { return } self.familyUserDefaults.saveFamilyCreatedAt($0?.createdAt) - }) - .asObservable() + } } } - // MARK: - Fetch Invitation Url - - public func fetchInvitationLink() -> Observable { + public func fetchInvitationLink( + ) -> Observable { guard let familyId = familyUserDefaults.loadFamilyId() - else { return .error(NSError()) } // TODO: - Error 타입 정의하기 + else { return .just(nil) } // TODO: - Error 타입 정의하기 - return familyApiWorker.fetchInvitationLink(familyId: familyId) + return linkWorker.createFamilyLink(familyId) .map { $0?.toDomain() } - .asObservable() } - // MARK: - Fetch Family Members - - public func fetchPaginationFamilyMembers(query: FamilyPaginationQuery) -> Observable { - guard - let familyId = familyUserDefaults.loadFamilyId() - else { return .error(NSError()) } // TODO: - Error 타입 정의하기 + public func fetchPaginationFamilyMembers( + query: FamilyPaginationQuery + ) -> Observable { + guard let familyId = familyUserDefaults.loadFamilyId() else { + return .just(nil) + } // TODO: - Error 타입 정의하기 - return familyApiWorker.fetchPaginationFamilyMember(query: query) + let query: FamilyMemberQueryDTO = .init( + type: "FAMILY", + page: query.page, + size: query.size + ) + return membersWorker.fetchPaginationMembers(query: query) .map { $0?.toDomain() } - .do(onSuccess: { [weak self] in + .do { [weak self] in guard let self, let profiles = $0?.results else { return } self.familyUserDefaults.saveFamilyMembers(profiles) - }) - .asObservable() + } } - public func fetchAllFamilyMembers() -> Observable<[FamilyMemberProfileEntity]?> { - return familyApiWorker.fetchPaginationFamilyMember(query: .init()) + public func fetchFamilyMembers( + ) -> Observable<[FamilyMemberProfileEntity]?> { + return membersWorker.fetchPaginationMembers( + query: .init( + type: "FAMILY", + page: 0, + size: 50 + ) + ) .map { $0?.results.map{ $0.toDomain() }} - .do(onSuccess: { [weak self] in + .do { [weak self] in guard let self, let profiles = $0 else { return } self.familyUserDefaults.saveFamilyMembers(profiles) - }) - .asObservable() + } } - public func fetchPaginationFamilyMembers(memberIds: [String]) -> [FamilyMemberProfileEntity] { + public func fetchPaginationFamilyMembers( + memberIds: [String] + ) -> [FamilyMemberProfileEntity] { var results: [FamilyMemberProfileEntity] = [] for memberId in memberIds { guard @@ -168,43 +171,41 @@ extension FamilyRepository { return familyUserDefaults.loadFamilyMembers() } - // MARK: - Fetch Family Name - public func fetchFamilyName() -> String? { familyUserDefaults.loadFamilyName() } - - // MARK: - Update Family Name - - public func updateFamilyName(body: UpdateFamilyNameRequest) -> Observable { + public func updateFamilyName( + body: UpdateFamilyNameRequest + ) -> Observable { let body = UpdateFamilyNameRequestDTO(familyName: body.familyName) - guard - let familyId = familyUserDefaults.loadFamilyId() - else { return .error(NSError()) } // TODO: - Error 타입 정의하기 + guard let familyId = familyUserDefaults.loadFamilyId() else { + return .just(nil) + } // TODO: - Error 타입 정의하기 - return familyApiWorker.updateFamilyName(familyId: familyId, body: body) + return familyWorker.updateFamilyName(familyId, body: body) .map { $0?.toDomain() } - .do(onSuccess: { [weak self] in + .do {[weak self] in guard let self else { return } self.familyUserDefaults.saveFamilyId($0?.familyId) self.familyUserDefaults.saveFamilyName($0?.familyName) self.familyUserDefaults.saveFamilyCreatedAt($0?.createdAt) self.familyUserDefaults.saveFamilyNameEditorId($0?.familyNameEditorId) - }) + } .asObservable() } - public func fetchFamilyGroupInfo() -> Observable { - return familyApiWorker.fetchFamilyGroupInfo() + public func fetchFamilyGroupInfo( + ) -> Observable { + return meWorker.fetchFamilyInfo() .map { $0?.toDomain() } - .do(onSuccess: { [weak self] in + .do { [weak self] in guard let self else { return } self.familyUserDefaults.saveFamilyId($0?.familyId) self.familyUserDefaults.saveFamilyName($0?.familyName) self.familyUserDefaults.saveFamilyNameEditorId($0?.familyNameEditorId) - }) + } .asObservable() } } diff --git a/14th-team5-iOS/Data/Sources/APIs/Members/Repositories/MembersRepository.swift b/14th-team5-iOS/Data/Sources/Repositories/MembersRepository.swift similarity index 97% rename from 14th-team5-iOS/Data/Sources/APIs/Members/Repositories/MembersRepository.swift rename to 14th-team5-iOS/Data/Sources/Repositories/MembersRepository.swift index 8e5447bb5..8145c0e17 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Members/Repositories/MembersRepository.swift +++ b/14th-team5-iOS/Data/Sources/Repositories/MembersRepository.swift @@ -18,7 +18,7 @@ public final class MembersRepository { public var disposeBag: DisposeBag = DisposeBag() private let familyUserDefaults: FamilyInfoUserDefaults = FamilyInfoUserDefaults() - private let membersAPIWorker: MembersAPIWorker = MembersAPIWorker() + private let membersAPIWorker: MembersWorker = MembersWorker() public init() { } } diff --git a/14th-team5-iOS/Data/Sources/Repositories/ReactionRepository.swift b/14th-team5-iOS/Data/Sources/Repositories/ReactionRepository.swift new file mode 100644 index 000000000..0e3ad7492 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/Repositories/ReactionRepository.swift @@ -0,0 +1,49 @@ +// +// ReactionRepository.swift +// Data +// +// Created by 마경미 on 04.06.24. +// + +import Foundation + +import Domain + +import RxSwift + +public final class ReactionRepository: ReactionRepositoryProtocol { + + private let disposeBag: DisposeBag = DisposeBag() + private let reactionAPIWorker: ReactionAPIWorker = ReactionAPIWorker() + + public init () { } + + public func addReaction( + query: CreateReactionQuery, + body: CreateReactionRequest + ) -> Observable { + let body: AddReactionRequestDTO = .init( + content: body.emojiId + ) + return reactionAPIWorker.addReaction(query.postId, body) + .map { $0?.toDomain() } + } + + public func removeReaction( + query: RemoveReactionQuery, + body: RemoveReactionRequest + ) -> Observable { + let body: RemoveReactionRequestDTO = .init( + content: body.content.emojiString + ) + return reactionAPIWorker.removeReaction(query.postId, body) + .map { $0?.toDomain() } + } + + public func fetchReaction( + query: FetchReactionQuery + ) -> Observable<[EmojiEntity]?> { + return reactionAPIWorker.fetchReaction(query.postId) + .map { $0?.toDomain() } + } +} diff --git a/14th-team5-iOS/Data/Sources/Repositories/RealEmojiRepository.swift b/14th-team5-iOS/Data/Sources/Repositories/RealEmojiRepository.swift new file mode 100644 index 000000000..b33a366e0 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/Repositories/RealEmojiRepository.swift @@ -0,0 +1,55 @@ +// +// RealEmojiRepository.swift +// Data +// +// Created by 마경미 on 04.06.24. +// + +import Foundation + +import Domain + +import RxSwift + +public final class RealEmojiRepository: RealEmojiRepositoryProtocol { + + public let disposeBag: DisposeBag = DisposeBag() + + private let myUserDefaults: MyUserDefaults = .init() + private let postRealEmojiWorker: PostRealEmojiWorker = PostRealEmojiWorker() + private let memberRealEmojiWorker: MemberRealEmojiWorker = MemberRealEmojiWorker() + + public init () { } + + public func fetchMyRealEmoji( + ) -> Observable<[MyRealEmojiEntity?]> { + guard let myMemberId: String = myUserDefaults.loadMemberId() else { + return .just([]) + } + return memberRealEmojiWorker.fetchMyRealEmoji(memberId: myMemberId) + .map { $0?.toDomain() ?? Array(repeating: nil, count: 5) } + } + + public func addRealEmoji( + query: CreateReactionQuery, + body: CreateReactionRequest + ) -> Observable { + let body: AddRealEmojiRequestDTO = .init(realEmojiId: body.emojiId) + return postRealEmojiWorker.addRealEmoji(query.postId, body: body) + .map { $0?.toDomain() } + } + + public func fetchRealEmoji( + query: FetchRealEmojiQuery + ) -> Observable<[EmojiEntity]?> { + postRealEmojiWorker.fetchRealEmoji(query.postId) + .map { $0?.toDomain() } + } + + public func removeRealEmoji( + query: RemoveRealEmojiQuery + ) -> Observable { + postRealEmojiWorker.removeRealEmoji(query.postId, query.realEmojiId) + .map { $0?.toDomain() } + } +} diff --git a/14th-team5-iOS/Data/Sources/Trash/Account/AccountRepository/AccountRepository.swift b/14th-team5-iOS/Data/Sources/Trash/Account/AccountRepository/AccountRepository.swift index 3fbf36d3d..37a8bed5f 100644 --- a/14th-team5-iOS/Data/Sources/Trash/Account/AccountRepository/AccountRepository.swift +++ b/14th-team5-iOS/Data/Sources/Trash/Account/AccountRepository/AccountRepository.swift @@ -34,7 +34,7 @@ public final class AccountRepository: AccountImpl { let signInHelper = AccountSignInHelper() private let apiWorker = AccountAPIWorker() - private let profileWorker = MembersAPIWorker() + private let profileWorker = MembersWorker() private let meApiWorekr = MeAPIWorker() private let fetchMemberInfo = PublishRelay() diff --git a/14th-team5-iOS/Domain/Sources/Repositories/FamilyRepository.swift b/14th-team5-iOS/Domain/Sources/Repositories/FamilyRepository.swift index 7a1e8e277..7617aae27 100644 --- a/14th-team5-iOS/Domain/Sources/Repositories/FamilyRepository.swift +++ b/14th-team5-iOS/Domain/Sources/Repositories/FamilyRepository.swift @@ -25,7 +25,7 @@ public protocol FamilyRepositoryProtocol { func updateFamilyName(body: UpdateFamilyNameRequest) -> Observable func loadAllFamilyMembers() -> [FamilyMemberProfileEntity]? - func fetchAllFamilyMembers() -> Observable<[FamilyMemberProfileEntity]?> + func fetchFamilyMembers() -> Observable<[FamilyMemberProfileEntity]?> func fetchPaginationFamilyMembers(query: FamilyPaginationQuery) -> Observable func fetchPaginationFamilyMembers(memberIds: [String]) -> [FamilyMemberProfileEntity] } diff --git a/14th-team5-iOS/Domain/Sources/Repositories/ReactionRepository.swift b/14th-team5-iOS/Domain/Sources/Repositories/ReactionRepository.swift index 069b61e82..0c94cda36 100644 --- a/14th-team5-iOS/Domain/Sources/Repositories/ReactionRepository.swift +++ b/14th-team5-iOS/Domain/Sources/Repositories/ReactionRepository.swift @@ -10,7 +10,7 @@ import Foundation import RxSwift public protocol ReactionRepositoryProtocol { - func addReaction(query: CreateReactionQuery, body: CreateReactionRequest) -> Single - func removeReaction(query: RemoveReactionQuery, body: RemoveReactionRequest) -> Single - func fetchReaction(query: FetchReactionQuery) -> Single<[EmojiEntity]?> + func addReaction(query: CreateReactionQuery, body: CreateReactionRequest) -> Observable + func removeReaction(query: RemoveReactionQuery, body: RemoveReactionRequest) -> Observable + func fetchReaction(query: FetchReactionQuery) -> Observable<[EmojiEntity]?> } diff --git a/14th-team5-iOS/Domain/Sources/Repositories/RealEmojiRepository.swift b/14th-team5-iOS/Domain/Sources/Repositories/RealEmojiRepository.swift index 247e3a2b9..4b8dc0577 100644 --- a/14th-team5-iOS/Domain/Sources/Repositories/RealEmojiRepository.swift +++ b/14th-team5-iOS/Domain/Sources/Repositories/RealEmojiRepository.swift @@ -10,8 +10,8 @@ import Foundation import RxSwift public protocol RealEmojiRepositoryProtocol { - func fetchMyRealEmoji() -> Single<[MyRealEmojiEntity?]> - func addRealEmoji(query: CreateReactionQuery, body: CreateReactionRequest) -> Single - func fetchRealEmoji(query: FetchRealEmojiQuery) -> Single<[EmojiEntity]?> - func removeRealEmoji(query: RemoveRealEmojiQuery) -> Single + func fetchMyRealEmoji() -> Observable<[MyRealEmojiEntity?]> + func addRealEmoji(query: CreateReactionQuery, body: CreateReactionRequest) -> Observable + func fetchRealEmoji(query: FetchRealEmojiQuery) -> Observable<[EmojiEntity]?> + func removeRealEmoji(query: RemoveRealEmojiQuery) -> Observable } diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Post/FetchPostListUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Post/FetchPostListUseCase.swift index a80203800..beec5f5e0 100644 --- a/14th-team5-iOS/Domain/Sources/UseCases/Post/FetchPostListUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/Post/FetchPostListUseCase.swift @@ -72,7 +72,7 @@ extension FetchPostListUseCase { if let members = members { return self.updatePostsWithMembers(posts: posts, members: members) } else { - return self.familyRepository.fetchAllFamilyMembers() + return self.familyRepository.fetchFamilyMembers() .flatMap { membersFromApi -> Single<[PostEntity]?> in return self.updatePostsWithMembers(posts: posts, members: membersFromApi) } diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Reaction/CreateReactionUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Reaction/CreateReactionUseCase.swift index 979c20772..7991a039b 100644 --- a/14th-team5-iOS/Domain/Sources/UseCases/Reaction/CreateReactionUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/Reaction/CreateReactionUseCase.swift @@ -10,7 +10,10 @@ import Foundation import RxSwift public protocol CreateReactionUseCaseProtocol { - func execute(query: CreateReactionQuery, body: CreateReactionRequest) -> Single + func execute( + query: CreateReactionQuery, + body: CreateReactionRequest + ) -> Observable } public final class CreateReactionUseCase: CreateReactionUseCaseProtocol { @@ -20,7 +23,10 @@ public final class CreateReactionUseCase: CreateReactionUseCaseProtocol { self.reactionRepository = reactionRepository } - public func execute(query: CreateReactionQuery, body: CreateReactionRequest) -> Single { + public func execute( + query: CreateReactionQuery, + body: CreateReactionRequest + ) -> Observable { return reactionRepository.addReaction(query: query, body: body) } } diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Reaction/FetchReactionListUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Reaction/FetchReactionListUseCase.swift index cf6370b79..dabb3157b 100644 --- a/14th-team5-iOS/Domain/Sources/UseCases/Reaction/FetchReactionListUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/Reaction/FetchReactionListUseCase.swift @@ -10,7 +10,9 @@ import Foundation import RxSwift public protocol FetchReactionListUseCaseProtocol { - func execute(query: FetchReactionQuery) -> Single<[EmojiEntity]?> + func execute( + query: FetchReactionQuery + ) -> Observable<[EmojiEntity]?> } public final class FetchReactionListUseCase: FetchReactionListUseCaseProtocol { @@ -20,7 +22,9 @@ public final class FetchReactionListUseCase: FetchReactionListUseCaseProtocol { self.reactionRepository = reactionRepository } - public func execute(query: FetchReactionQuery) -> Single<[EmojiEntity]?> { + public func execute( + query: FetchReactionQuery + ) -> Observable<[EmojiEntity]?> { return reactionRepository.fetchReaction(query: query) } } diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Reaction/RemoveReactionUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Reaction/RemoveReactionUseCase.swift index a2b78cb86..79cf9c2b4 100644 --- a/14th-team5-iOS/Domain/Sources/UseCases/Reaction/RemoveReactionUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/Reaction/RemoveReactionUseCase.swift @@ -10,7 +10,10 @@ import Foundation import RxSwift public protocol RemoveReactionUseCaseProtocol { - func execute(query: RemoveReactionQuery, body: RemoveReactionRequest) -> Single + func execute( + query: RemoveReactionQuery, + body: RemoveReactionRequest + ) -> Observable } public final class RemoveReactionUseCase: RemoveReactionUseCaseProtocol { @@ -20,7 +23,10 @@ public final class RemoveReactionUseCase: RemoveReactionUseCaseProtocol { self.reactionRepository = reactionRepository } - public func execute(query: RemoveReactionQuery, body: RemoveReactionRequest) -> Single { + public func execute( + query: RemoveReactionQuery, + body: RemoveReactionRequest + ) -> Observable { return reactionRepository.removeReaction(query: query, body: body) } } diff --git a/14th-team5-iOS/Domain/Sources/UseCases/RealEmoji/CreateRealEmojiUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/RealEmoji/CreateRealEmojiUseCase.swift index 344aff3c4..d84101eb5 100644 --- a/14th-team5-iOS/Domain/Sources/UseCases/RealEmoji/CreateRealEmojiUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/RealEmoji/CreateRealEmojiUseCase.swift @@ -10,7 +10,7 @@ import Foundation import RxSwift public protocol CreateRealEmojiUseCaseProtocol { - func execute(query: CreateReactionQuery, body: CreateReactionRequest) -> Single + func execute(query: CreateReactionQuery, body: CreateReactionRequest) -> Observable } public class CreateRealEmojiUseCase: CreateRealEmojiUseCaseProtocol { @@ -21,7 +21,7 @@ public class CreateRealEmojiUseCase: CreateRealEmojiUseCaseProtocol { self.realEmojiRepository = realEmojiRepository } - public func execute(query: CreateReactionQuery, body: CreateReactionRequest) -> Single { + public func execute(query: CreateReactionQuery, body: CreateReactionRequest) -> Observable { return realEmojiRepository.addRealEmoji(query: query, body: body) } } diff --git a/14th-team5-iOS/Domain/Sources/UseCases/RealEmoji/FetchMyRealEmojiUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/RealEmoji/FetchMyRealEmojiUseCase.swift index 18e962ddd..786bed352 100644 --- a/14th-team5-iOS/Domain/Sources/UseCases/RealEmoji/FetchMyRealEmojiUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/RealEmoji/FetchMyRealEmojiUseCase.swift @@ -10,7 +10,7 @@ import Foundation import RxSwift public protocol FetchMyRealEmojiUseCaseProtocol { - func execute() -> Single<[MyRealEmojiEntity?]> + func execute() -> Observable<[MyRealEmojiEntity?]> } public class FetchMyRealEmojiUseCase: FetchMyRealEmojiUseCaseProtocol { @@ -21,7 +21,7 @@ public class FetchMyRealEmojiUseCase: FetchMyRealEmojiUseCaseProtocol { self.realEmojiRepository = realEmojiRepository } - public func execute() -> Single<[MyRealEmojiEntity?]> { + public func execute() -> Observable<[MyRealEmojiEntity?]> { return realEmojiRepository.fetchMyRealEmoji() } } diff --git a/14th-team5-iOS/Domain/Sources/UseCases/RealEmoji/FetchRealEmojiListUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/RealEmoji/FetchRealEmojiListUseCase.swift index 7b7549ecf..66e737dff 100644 --- a/14th-team5-iOS/Domain/Sources/UseCases/RealEmoji/FetchRealEmojiListUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/RealEmoji/FetchRealEmojiListUseCase.swift @@ -10,7 +10,7 @@ import Foundation import RxSwift public protocol FetchRealEmojiListUseCaseProtocol { - func execute(query: FetchRealEmojiQuery) -> Single<[EmojiEntity]?> + func execute(query: FetchRealEmojiQuery) -> Observable<[EmojiEntity]?> } public class FetchRealEmojiListUseCase: FetchRealEmojiListUseCaseProtocol { @@ -21,7 +21,7 @@ public class FetchRealEmojiListUseCase: FetchRealEmojiListUseCaseProtocol { self.realEmojiRepository = realEmojiRepository } - public func execute(query: FetchRealEmojiQuery) -> Single<[EmojiEntity]?> { + public func execute(query: FetchRealEmojiQuery) -> Observable<[EmojiEntity]?> { return realEmojiRepository.fetchRealEmoji(query: query) } } diff --git a/14th-team5-iOS/Domain/Sources/UseCases/RealEmoji/RemoveRealEmojiUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/RealEmoji/RemoveRealEmojiUseCase.swift index 1b7de13ef..71077033e 100644 --- a/14th-team5-iOS/Domain/Sources/UseCases/RealEmoji/RemoveRealEmojiUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/RealEmoji/RemoveRealEmojiUseCase.swift @@ -10,7 +10,7 @@ import Foundation import RxSwift public protocol RemoveRealEmojiUseCaseProtocol { - func execute(query: RemoveRealEmojiQuery) -> Single + func execute(query: RemoveRealEmojiQuery) -> Observable } public class RemoveRealEmojiUseCase: RemoveRealEmojiUseCaseProtocol { @@ -21,7 +21,7 @@ public class RemoveRealEmojiUseCase: RemoveRealEmojiUseCaseProtocol { self.realEmojiRepository = realEmojiRepository } - public func execute(query: RemoveRealEmojiQuery) -> Single { + public func execute(query: RemoveRealEmojiQuery) -> Observable { return realEmojiRepository.removeRealEmoji(query: query) } } From d65a86681dd23e6d95ca87bee330cdd98db3c2cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=84?= Date: Thu, 28 Nov 2024 23:36:50 +0900 Subject: [PATCH 250/263] =?UTF-8?q?fix:=20PickRepository,=20ResignReposito?= =?UTF-8?q?ry=20=ED=8C=8C=EC=9D=BC=20=EB=B0=8F=20=ED=8F=B4=EB=8D=94=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=20(#716)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Application PickDIContainer, ResignDIContainer 의존성 주입 코드 추가 - ProfileDIContainer 내부 Pick, Resign 관련 의존성 코드 제거 - BBNetworkHeader, Interceptor AccessToken 타입으로 변경 - PickRepository, ResignRepository 파일 추가 - CreateMemberPickUseCase, DeleteMemberUseCase Repository 변경 * fix: PickRepository, ResignRepository 폴더 이동 --- .../App/Sources/Application/AppDelegate.swift | 4 ++- .../DIContainer/PickDICotainer.swift | 30 +++++++++++++++++ .../DIContainer/ProfileDIContainer.swift | 16 ---------- .../DIContainer/ResignDIContainer.swift | 32 +++++++++++++++++++ .../Network/BBNetworkInterceptor.swift | 9 +++--- .../Network/Components/BBNetworkHeader.swift | 4 +-- .../Sources/APIs/Members/MembersAPIs.swift | 2 +- .../Repositories/MembersRepository.swift | 11 ------- .../Sources/Repositories/PickRepository.swift | 27 ++++++++++++++++ .../Repositories/ResignRepository.swift | 27 ++++++++++++++++ .../Repositories/MembersRepository.swift | 3 -- .../Sources/Repositories/PickRepository.swift | 15 +++++++++ .../Repositories/ResignRepository.swift | 15 +++++++++ .../Members/CreateMembersPickUseCase.swift | 8 ++--- .../Members/DeleteMembersUseCase.swift | 8 ++--- 15 files changed, 165 insertions(+), 46 deletions(-) create mode 100644 14th-team5-iOS/App/Sources/Application/DIContainer/PickDICotainer.swift create mode 100644 14th-team5-iOS/App/Sources/Application/DIContainer/ResignDIContainer.swift create mode 100644 14th-team5-iOS/Data/Sources/Repositories/PickRepository.swift create mode 100644 14th-team5-iOS/Data/Sources/Repositories/ResignRepository.swift create mode 100644 14th-team5-iOS/Domain/Sources/Repositories/PickRepository.swift create mode 100644 14th-team5-iOS/Domain/Sources/Repositories/ResignRepository.swift diff --git a/14th-team5-iOS/App/Sources/Application/AppDelegate.swift b/14th-team5-iOS/App/Sources/Application/AppDelegate.swift index 1d699532b..2530fdbca 100644 --- a/14th-team5-iOS/App/Sources/Application/AppDelegate.swift +++ b/14th-team5-iOS/App/Sources/Application/AppDelegate.swift @@ -70,7 +70,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { MemberDIContainer(), MyDIContainer(), SignOutDIContainer(), - PrivacyDIContainer() + PrivacyDIContainer(), + PickDICotainer(), + ResignDIContainer(), ] containers.forEach { $0.registerDependencies() diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/PickDICotainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/PickDICotainer.swift new file mode 100644 index 000000000..80d75f335 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/PickDICotainer.swift @@ -0,0 +1,30 @@ +// +// PickDICotainer.swift +// App +// +// Created by 김도현 on 11/27/24. +// + +import Core +import Domain +import Data + +import UIKit + +final class PickDICotainer: BaseContainer { + + private func makeRepository() -> PickRepositoryProtocol { + return PickRepository() + } + + private func makeCreateMemberPickUseCase() -> CreateMembersPickUseCaseProtocol { + return CreateMembersPickUseCase(pickRepository: makeRepository()) + } + + func registerDependencies() { + container.register(type: CreateMembersPickUseCaseProtocol.self) { _ in + self.makeCreateMemberPickUseCase() + } + } + +} diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/ProfileDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/ProfileDIContainer.swift index 90728ca49..db4ffc3b0 100644 --- a/14th-team5-iOS/App/Sources/Application/DIContainer/ProfileDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/ProfileDIContainer.swift @@ -30,14 +30,6 @@ final class ProfileDIContainer: BaseContainer { return UpdateMembersProfileUseCase(membersRepository: makeRepository()) } - private func makeCreateMemberPickUseCase() -> CreateMembersPickUseCaseProtocol { - return CreateMembersPickUseCase(membersRepository: makeRepository()) - } - - private func makeDeleteMembersUseCase() -> DeleteMembersUseCaseProtocol { - return DeleteMembersUseCase(membersRepository: makeRepository()) - } - private func makeUpdateMembersNameUseCase() -> UpdateMembersNameUseCaseProtocol { return UpdateMembersNameUseCase(membersRepository: makeRepository()) } @@ -66,14 +58,6 @@ final class ProfileDIContainer: BaseContainer { self.makeCreateMembersPresignedURLUseCase() } - container.register(type: CreateMembersPickUseCaseProtocol.self) { _ in - self.makeCreateMemberPickUseCase() - } - - container.register(type: DeleteMembersUseCaseProtocol.self) { _ in - self.makeDeleteMembersUseCase() - } - container.register(type: UpdateMembersNameUseCaseProtocol.self) { _ in self.makeUpdateMembersNameUseCase() } diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/ResignDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/ResignDIContainer.swift new file mode 100644 index 000000000..4044b2766 --- /dev/null +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/ResignDIContainer.swift @@ -0,0 +1,32 @@ +// +// ResignDIContainer.swift +// App +// +// Created by 김도현 on 11/27/24. +// + +import Core +import Domain +import Data + +import UIKit + + +final class ResignDIContainer: BaseContainer { + private func makeRepository() -> ResignRepositoryProtocol { + return ResignRepository() + } + + private func makeDeleteMembersUseCase() -> DeleteMembersUseCaseProtocol { + return DeleteMembersUseCase(resignRepository: makeRepository()) + } + + func registerDependencies() { + container.register(type: DeleteMembersUseCaseProtocol.self) { _ in + self.makeDeleteMembersUseCase() + } + } + + + +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/BBNetworkInterceptor.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/BBNetworkInterceptor.swift index 1991888ed..e95ba321c 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/BBNetworkInterceptor.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/BBNetworkInterceptor.swift @@ -39,17 +39,18 @@ extension BBNetworkDefaultInterceptor: RequestInterceptor { return } - guard let authToken: AuthToken = KeychainWrapper.standard.object(forKey: .accessToken) else { + guard let authToken: AccessToken = KeychainWrapper.standard.object(forKey: .accessToken), + let refreshToken = authToken.refreshToken else { completion(.doNotRetry) return } - var refreshedAuthToken: AuthToken? = nil - refreshAuthToken(authToken.refreshToken) { dataResponse in + var refreshedAuthToken: AccessToken? = nil + refreshAuthToken(refreshToken) { dataResponse in switch dataResponse.result { case let .success(data): - refreshedAuthToken = data?.decode(AuthToken.self) + refreshedAuthToken = data?.decode(AccessToken.self) KeychainWrapper.standard.set(refreshedAuthToken, forKey: "accessToken") completion(.retry) 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 index ecd264ae3..eb99b4b04 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/Components/BBNetworkHeader.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBNetwork/Network/Components/BBNetworkHeader.swift @@ -86,8 +86,8 @@ private extension BBNetworkHeader { } func fetchXAuthTokenValue() -> String { - if let authToken: AuthToken = KeychainWrapper.standard[.accessToken] { - return authToken.accessToken + if let authToken: AccessToken = KeychainWrapper.standard[.accessToken] { + return authToken.accessToken ?? "" } return "" } diff --git a/14th-team5-iOS/Data/Sources/APIs/Members/MembersAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/Members/MembersAPIs.swift index 6424b5380..b4463f11e 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Members/MembersAPIs.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Members/MembersAPIs.swift @@ -41,7 +41,7 @@ enum MembersAPIs: BBAPI { case let .createMemberPick(memberId): return Spec(method: .post, path: "/members/\(memberId)/pick") case let .createMemberPresignedURL(body): - return Spec(method: .post, path: "/members/image-upload-request", bodyParametersEncodable: body) + return Spec(method: .post, path: "/members/image-upload-request", bodyParametersEncodable: body, headers: .default) case let .updateMemberName(memberId, body): return Spec(method: .put, path: "/members/name/\(memberId)", bodyParametersEncodable: body) case let .updateMemberProfileImage(memberId, body): diff --git a/14th-team5-iOS/Data/Sources/Repositories/MembersRepository.swift b/14th-team5-iOS/Data/Sources/Repositories/MembersRepository.swift index 8145c0e17..78b0c8b43 100644 --- a/14th-team5-iOS/Data/Sources/Repositories/MembersRepository.swift +++ b/14th-team5-iOS/Data/Sources/Repositories/MembersRepository.swift @@ -49,11 +49,6 @@ extension MembersRepository: MembersRepositoryProtocol { .map { $0?.toDomain() } } - public func createMemberPickItem(memberId: String) -> Observable { - return membersAPIWorker.createMemberPick(memberId: memberId) - .map { $0?.toDomain() } - } - public func creteMemberImagePresignedURL(body: CreateMemberPresignedReqeust) -> Observable { let body = CreateMemberPresignedURLRequestDTO(imageName: body.imageName) @@ -70,12 +65,6 @@ extension MembersRepository: MembersRepositoryProtocol { .map { $0?.toDomain() } } - public func deleteMemberItem(memberId: String) -> Observable { - - return membersAPIWorker.deleteMember(memberId: memberId) - .map { $0?.toDomain() } - } - public func uploadMemberImageToS3Bucket(_ presignedURL: String, image: Data) -> Observable { return membersAPIWorker.updateS3MemberImageUpload(presignedURL, image: image) } diff --git a/14th-team5-iOS/Data/Sources/Repositories/PickRepository.swift b/14th-team5-iOS/Data/Sources/Repositories/PickRepository.swift new file mode 100644 index 000000000..7f712f7f9 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/Repositories/PickRepository.swift @@ -0,0 +1,27 @@ +// +// PickRepository.swift +// Data +// +// Created by 김도현 on 11/27/24. +// + +import Foundation + +import Domain +import RxSwift + +public final class PickRepository { + + public let disposeBag: DisposeBag = DisposeBag() + private let membersAPIWorker: MembersWorker = MembersWorker() + + public init() { } +} + + +extension PickRepository: PickRepositoryProtocol { + public func createMemberPickItem(memberId: String) -> Observable { + return membersAPIWorker.createMemberPick(memberId: memberId) + .map { $0?.toDomain() } + } +} diff --git a/14th-team5-iOS/Data/Sources/Repositories/ResignRepository.swift b/14th-team5-iOS/Data/Sources/Repositories/ResignRepository.swift new file mode 100644 index 000000000..54b7c0dff --- /dev/null +++ b/14th-team5-iOS/Data/Sources/Repositories/ResignRepository.swift @@ -0,0 +1,27 @@ +// +// ResignRepository.swift +// Data +// +// Created by 김도현 on 11/27/24. +// + +import Foundation + +import Domain +import RxSwift + +public final class ResignRepository { + private let disposeBag: DisposeBag = DisposeBag() + private let membersAPIWorker: MembersWorker = MembersWorker() + + public init() { } +} + + +extension ResignRepository: ResignRepositoryProtocol { + public func deleteMemberItem(memberId: String) -> Observable { + + return membersAPIWorker.deleteMember(memberId: memberId) + .map { $0?.toDomain() } + } +} diff --git a/14th-team5-iOS/Domain/Sources/Repositories/MembersRepository.swift b/14th-team5-iOS/Domain/Sources/Repositories/MembersRepository.swift index ba77710d1..8ae3734a7 100644 --- a/14th-team5-iOS/Domain/Sources/Repositories/MembersRepository.swift +++ b/14th-team5-iOS/Domain/Sources/Repositories/MembersRepository.swift @@ -19,11 +19,8 @@ public protocol MembersRepositoryProtocol { func updateMemberNameItem(memberId: String, body: UpdateMemberNameRequest) -> Observable func updateMemberProfileImageItem(memberId: String, body: UpdateMemberImageRequest) -> Observable /// CREATE - func createMemberPickItem(memberId: String) -> Observable func creteMemberImagePresignedURL(body: CreateMemberPresignedReqeust) -> Observable func deleteMemberProfileImageItem(memberId: String) -> Observable - /// DELETE - func deleteMemberItem(memberId: String) -> Observable /// UPLOAD func uploadMemberImageToS3Bucket(_ presignedURL: String, image: Data) -> Observable } diff --git a/14th-team5-iOS/Domain/Sources/Repositories/PickRepository.swift b/14th-team5-iOS/Domain/Sources/Repositories/PickRepository.swift new file mode 100644 index 000000000..ed0108d00 --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Repositories/PickRepository.swift @@ -0,0 +1,15 @@ +// +// PickRepository.swift +// Domain +// +// Created by 김도현 on 11/27/24. +// + +import Foundation + +import RxSwift + +public protocol PickRepositoryProtocol { + /// CREATE + func createMemberPickItem(memberId: String) -> Observable +} diff --git a/14th-team5-iOS/Domain/Sources/Repositories/ResignRepository.swift b/14th-team5-iOS/Domain/Sources/Repositories/ResignRepository.swift new file mode 100644 index 000000000..691d220df --- /dev/null +++ b/14th-team5-iOS/Domain/Sources/Repositories/ResignRepository.swift @@ -0,0 +1,15 @@ +// +// ResignRepository.swift +// Domain +// +// Created by 김도현 on 11/27/24. +// + +import Foundation + +import RxSwift + +public protocol ResignRepositoryProtocol { + /// DELETE + func deleteMemberItem(memberId: String) -> Observable +} diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Members/CreateMembersPickUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Members/CreateMembersPickUseCase.swift index b6062806f..c35655be6 100644 --- a/14th-team5-iOS/Domain/Sources/UseCases/Members/CreateMembersPickUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/Members/CreateMembersPickUseCase.swift @@ -16,15 +16,15 @@ public protocol CreateMembersPickUseCaseProtocol { public final class CreateMembersPickUseCase: CreateMembersPickUseCaseProtocol { - private let membersRepository: MembersRepositoryProtocol + private let pickRepository: PickRepositoryProtocol - public init(membersRepository: MembersRepositoryProtocol) { - self.membersRepository = membersRepository + public init(pickRepository: PickRepositoryProtocol) { + self.pickRepository = pickRepository } public func execute(memberId: String) -> Observable { - return membersRepository.createMemberPickItem(memberId: memberId) + return pickRepository.createMemberPickItem(memberId: memberId) } } diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Members/DeleteMembersUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Members/DeleteMembersUseCase.swift index 4b46bed79..1662b3ff8 100644 --- a/14th-team5-iOS/Domain/Sources/UseCases/Members/DeleteMembersUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/Members/DeleteMembersUseCase.swift @@ -16,14 +16,14 @@ public protocol DeleteMembersUseCaseProtocol { public final class DeleteMembersUseCase: DeleteMembersUseCaseProtocol { - private let membersRepository: MembersRepositoryProtocol + private let resignRepository: ResignRepositoryProtocol - public init(membersRepository: MembersRepositoryProtocol) { - self.membersRepository = membersRepository + public init(resignRepository: ResignRepositoryProtocol) { + self.resignRepository = resignRepository } public func execute(memberId: String) -> Observable { - return membersRepository.deleteMemberItem(memberId: memberId) + return resignRepository.deleteMemberItem(memberId: memberId) } } From 82b79b7cc053a4f767b39509d27503a482bc414e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=84?= Date: Tue, 10 Dec 2024 11:18:45 +0900 Subject: [PATCH 251/263] =?UTF-8?q?fix:=20PostListQueryDTO=EC=97=90=20memb?= =?UTF-8?q?erId=20=EA=B0=92=20=EC=B6=94=EA=B0=80=20(#720)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Presentation/Profile/Reactor/ProfileViewReactor.swift | 2 +- .../Data/Sources/APIs/Post/Repository/PostRepository.swift | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift index 9a9325bfa..c93bb600f 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift @@ -16,7 +16,7 @@ public final class ProfileViewReactor: Reactor { public var initialState: State @Injected private var fetchMembersProfileUseCase: FetchMembersProfileUseCaseProtocol - @Injected private var updateMembersProfileUseCase :UpdateMembersProfileUseCaseProtocol + @Injected private var updateMembersProfileUseCase: UpdateMembersProfileUseCaseProtocol @Injected private var uploadProfileImageUseCase: FetchCameraUploadImageUseCaseProtocol @Injected private var createPresignedURLUseCase: CreateMembersPresignedURLUseCaseProtocol @Injected private var deleteProfileImageUseCase: DeleteMembersProfileUseCaseProtocol diff --git a/14th-team5-iOS/Data/Sources/APIs/Post/Repository/PostRepository.swift b/14th-team5-iOS/Data/Sources/APIs/Post/Repository/PostRepository.swift index 04cf64dab..290ed5884 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Post/Repository/PostRepository.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Post/Repository/PostRepository.swift @@ -25,6 +25,7 @@ extension PostRepository { page: query.page, size: query.size, date: query.date, + memberId: query.memberId, type: query.type.rawValue, sort: query.type.rawValue ) From b3352e04b7dad3d464feb254e48478a98824686c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=84?= Date: Tue, 10 Dec 2024 13:05:35 +0900 Subject: [PATCH 252/263] =?UTF-8?q?feat:=20BBAlert=20secondaryAction=20Par?= =?UTF-8?q?ameters=20=EC=B6=94=EA=B0=80=20(#718)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - BBAlert uploadFailed AlertView UI 추가 - CameraNavigator, CameraDisplayNavigator errorAlert Method 추가 - CameraRepository uploadEmoji Method 추가 --- .../Navigator/CameraDisplayNavigator.swift | 26 +++- .../Navigator/CameraNavigator.swift | 49 ++++++- .../Reactor/CameraDisplayViewReactor.swift | 8 +- .../Camera/Reactor/CameraViewReactor.swift | 132 +++++++----------- .../Bibbi/BBCommons/BBAlert/BBAlert.swift | 45 ++++-- .../BBCommons/BBAlert/BBAlertStyle.swift | 1 + .../Sources/APIs/Camera/CameraAPIWorker.swift | 2 +- .../Camera/Repository/CameraRepository.swift | 7 +- .../UploadFailed.imageset/Contents.json | 12 ++ .../UploadFailed.imageset/UploadFailed.svg | 25 ++++ .../Repositories/CameraRepository.swift | 4 +- .../FetchCameraRealEmojiUseCase.swift | 16 ++- 12 files changed, 224 insertions(+), 103 deletions(-) create mode 100644 14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/Character/UploadFailed.imageset/Contents.json create mode 100644 14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/Character/UploadFailed.imageset/UploadFailed.svg diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/CameraDisplayNavigator.swift b/14th-team5-iOS/App/Sources/Application/Navigator/CameraDisplayNavigator.swift index 292603cbc..32cebdeea 100644 --- a/14th-team5-iOS/App/Sources/Application/Navigator/CameraDisplayNavigator.swift +++ b/14th-team5-iOS/App/Sources/Application/Navigator/CameraDisplayNavigator.swift @@ -11,6 +11,8 @@ import UIKit protocol CameraDisplayNavigatorProtocol: BaseNavigator { func toHome() + func toCamera() + func showErrorAlert() } final class CameraDisplayNavigator: CameraDisplayNavigatorProtocol { @@ -24,9 +26,31 @@ final class CameraDisplayNavigator: CameraDisplayNavigatorProtocol { self.navigationController = navigationController } + func showErrorAlert() { + let confirmHandler: BBAlertActionHandler = { [weak self] alert in + self?.toCamera() + alert?.close() + } + let cancelHandler: BBAlertActionHandler = { [weak self] alert in + self?.toHome() + alert?.close() + } + + BBAlert.style( + .uploadFailed, + primaryAction: confirmHandler, + secondaryAction: cancelHandler + ).show() + } + + func toCamera() { + navigationController.popViewController(animated: true) + } + + //MARK: - Configure func toHome() { let vc = MainViewControllerWrapper().viewController - navigationController.setViewControllers([vc], animated: true) + navigationController.setViewControllers([vc], animated: false) } } diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/CameraNavigator.swift b/14th-team5-iOS/App/Sources/Application/Navigator/CameraNavigator.swift index f79bb785c..da34040b7 100644 --- a/14th-team5-iOS/App/Sources/Application/Navigator/CameraNavigator.swift +++ b/14th-team5-iOS/App/Sources/Application/Navigator/CameraNavigator.swift @@ -9,20 +9,49 @@ import UIKit import DesignSystem import Core +import Domain protocol CameraNavigatorProtocol: BaseNavigator { + func showErrorAlert(_ type: UploadLocation) func showErrorToast(_ description: String) + func toCamera(_ type: UploadLocation) + func toHome() } -final class CameraNavigator: CameraNavigatorProtocol { +final class CameraNavigator: CameraNavigatorProtocol { var navigationController: UINavigationController init(navigationController: UINavigationController) { self.navigationController = navigationController } + + func showErrorAlert(_ type: UploadLocation) { + let confirmHandler: BBAlertActionHandler = makeConfirmHandler(for: type) + let cancelHandler: BBAlertActionHandler = { [weak self] alert in + self?.toHome() + alert?.close() + } + + BBAlert.style( + .uploadFailed, + primaryAction: confirmHandler, + secondaryAction: cancelHandler + ).show() + } + + func toCamera(_ type: UploadLocation) { + let vc = CameraViewControllerWrapper(cameraType: type).viewController + navigationController.pushViewController(vc, animated: true) + } + + func toHome() { + let vc = MainViewControllerWrapper().viewController + navigationController.setViewControllers([vc], animated: false) + } + func showErrorToast(_ descrption: String) { let config = BBToastConfiguration(direction: .top(yOffset: 75)) let viewConfig = BBToastViewConfiguration(minWidth: 100) @@ -36,3 +65,21 @@ final class CameraNavigator: CameraNavigatorProtocol { } +private extension CameraNavigator { + func makeConfirmHandler(for type: UploadLocation) -> BBAlertActionHandler { + switch type { + case .survival, .mission: + return { [weak self] alert in + self?.toCamera(type) + alert?.close() + } + case .profile, .realEmoji: + return { alert in + alert?.close() + } + default: + return nil + } + } +} + diff --git a/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraDisplayViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraDisplayViewReactor.swift index 4a701a582..e67e4d733 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraDisplayViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraDisplayViewReactor.swift @@ -90,8 +90,9 @@ public final class CameraDisplayViewReactor: Reactor { .just(.setError(false)), .just(.setLoading(true)) ) - }.catch { _ in - return .just(.setError(true)) + }.catchError(with: self) { owner, _ in + owner.cameraDisplayNavigator.showErrorAlert() + return .empty() } case let .fetchDisplayImage(description): @@ -149,6 +150,9 @@ public final class CameraDisplayViewReactor: Reactor { .flatMap { _ in Observable.empty() } ) } + }.catchError(with: self) { owner, _ in + owner.cameraDisplayNavigator.showErrorAlert() + return .empty() } case .hideDisplayEditCell: return .concat( diff --git a/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraViewReactor.swift index 585da4ded..0bb9812bf 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraViewReactor.swift @@ -54,13 +54,11 @@ public final class CameraViewReactor: Reactor { case setProfilePresignedResponse(CreateMemberPresignedEntity?) case setProfileMemberResponse(MembersProfileEntity?) case setRealEmojiImageURLResponse(CameraRealEmojiPreSignedEntity?) - case setRealEmojiImageCreateResponse(CameraCreateRealEmojiEntity?) case setRealEmojiItems([CameraRealEmojiImageItemEntity?]) case setRealEmojiSection([EmojiSectionItem]) case setErrorAlert(Bool) case setRealEmojiType(Emojis) case setImageData(Data) - case setUpdateEmojiImage(URL) } public struct State { @@ -70,13 +68,11 @@ public final class CameraViewReactor: Reactor { @Pulse var missionEntity: MissonTodayContentEntity? @Pulse var memberPresignedEntity: CreateMemberPresignedEntity? @Pulse var realEmojiURLEntity: CameraRealEmojiPreSignedEntity? - @Pulse var realEmojiCreateEntity: CameraCreateRealEmojiEntity? @Pulse var realEmojiEntity: [CameraRealEmojiImageItemEntity?] @Pulse var realEmojiSection: [EmojiSectionModel] @Pulse var zoomScale: CGFloat @Pulse var pinchZoomScale: CGFloat @Pulse var imageData: Data? - var updateEmojiImage: URL? var emojiType: Emojis = .emoji(forIndex: 1) @Pulse var cameraType: UploadLocation = .survival var accountImage: Data? @@ -154,8 +150,6 @@ public final class CameraViewReactor: Reactor { newState.profileMemberEntity = entity case let .setRealEmojiImageURLResponse(entity): newState.realEmojiURLEntity = entity - case let .setRealEmojiImageCreateResponse(entity): - newState.realEmojiCreateEntity = entity case let .setRealEmojiItems(items): newState.realEmojiEntity = items case let .setRealEmojiSection(section): @@ -165,8 +159,6 @@ public final class CameraViewReactor: Reactor { newState.isError = isError case let .setRealEmojiType(emojiType): newState.emojiType = emojiType - case let .setUpdateEmojiImage(realEmoji): - newState.updateEmojiImage = realEmoji case let .setZoomScale(zoomScale): newState.zoomScale = zoomScale case let .setPinchZoomScale(pinchZoomScale): @@ -304,12 +296,14 @@ extension CameraViewReactor { .just(.setProfileMemberResponse(entity)), .just(.setLoading(true)) ) - }.catch { [weak self] error in - self?.cameraNavigator.showErrorToast(error.localizedDescription) + }.catchError(with: self) { owner, _ in + let type = owner.currentState.cameraType + owner.cameraNavigator.showErrorAlert(type) return .empty() } - }.catch { [weak self] error in - self?.cameraNavigator.showErrorToast(error.localizedDescription) + }.catchError(with: self) { owner, _ in + let type = owner.currentState.cameraType + owner.cameraNavigator.showErrorAlert(type) return .empty() } case .realEmoji: @@ -320,84 +314,52 @@ extension CameraViewReactor { let realEmojiImage = "\(imageData.hashValue).jpg" let body = CreatePresignedURLRequest(imageName: realEmojiImage) if currentState.realEmojiEntity[currentState.emojiType.rawValue - 1] == nil { - return .concat( - .just(.setLoading(false)), - fetchRealEmojiPreSignedUseCase.execute(memberId: memberId, body: body) - .withUnretained(self) - .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) - .flatMap { owner, entity -> Observable in - guard let remoteURL = entity?.imageURL else { return .just(.setErrorAlert(true))} - - return owner.uploadImageUseCase.execute(remoteURL, image: imageData) - .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) - .flatMap { isSuccess -> Observable in - let originalURL = owner.configureProfileOriginalS3URL(url: remoteURL, with: .realEmoji) - let body = CreateEmojiImageRequest(type: owner.currentState.emojiType.emojiString, imageUrl: originalURL) - if isSuccess { - return owner.fetchRealEmojiCreateUseCase.execute(memberId: memberId, body: body) - .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) - .flatMap { realEmojiEntity -> Observable in - guard let createRealEmojiEntity = realEmojiEntity else { return .just(.setErrorAlert(true))} - owner.provider.realEmojiGlobalState.createRealEmojiImage(indexPath: owner.currentState.emojiType.rawValue - 1, image: createRealEmojiEntity.realEmojiImageURL, emojiType: createRealEmojiEntity.realEmojiType) - return owner.fetchRealEmojiListUseCase.execute(memberId: memberId) - .asObservable() - .flatMap { reloadEntity -> Observable in - return .concat( - .just(.setRealEmojiImageURLResponse(entity)), - .just(.setRealEmojiItems(reloadEntity)), - .just(.setRealEmojiImageCreateResponse(realEmojiEntity)), - .just(.setErrorAlert(false)), - .just(.setLoading(true)) - ) - } - - } - } else { - return .just(.setErrorAlert(true)) - } - + return fetchRealEmojiPreSignedUseCase.execute(memberId: memberId, body: body, imageData: imageData) + .withUnretained(self) + .flatMap { owner, remoteURL -> Observable in + let originalURL = owner.configureProfileOriginalS3URL(url: remoteURL.imageURL, with: .realEmoji) + let body = CreateEmojiImageRequest(type: owner.currentState.emojiType.emojiString, imageUrl: originalURL) + return owner.fetchRealEmojiCreateUseCase.execute(memberId: memberId, body: body) + .flatMap { entity -> Observable in + guard let entity else { + return .empty() } - - } - - ) + owner.provider.realEmojiGlobalState.createRealEmojiImage(indexPath: owner.currentState.emojiType.rawValue - 1, image: entity.realEmojiImageURL, emojiType: entity.realEmojiType) + return .empty() + } + }.catchError(with: self) { owner, _ in + let type = owner.currentState.cameraType + owner.cameraNavigator.showErrorAlert(type) + return .empty() + } } else { let realEmojiImage = "\(imageData.hashValue).jpg" let body = CreatePresignedURLRequest(imageName: realEmojiImage) - return .concat( - .just(.setLoading(false)), - fetchRealEmojiPreSignedUseCase.execute(memberId: memberId, body: body) - .withUnretained(self) - .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) - .flatMap { owner, entity -> Observable in - guard let remoteURL = entity?.imageURL else { return .just(.setErrorAlert(true))} - let originalURL = owner.configureProfileOriginalS3URL(url: remoteURL, with: .realEmoji) - - return owner.uploadImageUseCase.execute(remoteURL, image: imageData) - .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background)) - .asObservable() - .flatMap { isSuccess -> Observable in - if isSuccess { - let body = UpdateRealEmojiImageRequest(imageUrl: originalURL) - return owner.fetchRealEmojiUpdateUseCase.execute(memberId: memberId, realEmojiId: owner.currentState.realEmojiEntity[owner.currentState.emojiType.rawValue - 1]?.realEmojiId ?? "", body: body) - .flatMap { updateRealEmojiEntity -> Observable in - guard let updateEntity = updateRealEmojiEntity else { return .just(.setErrorAlert(true))} - owner.provider.realEmojiGlobalState.updateRealEmojiImage(indexPath: owner.currentState.emojiType.rawValue - 1, image: updateEntity.realEmojiImageURL) - return .concat( - .just(.setUpdateEmojiImage(updateEntity.realEmojiImageURL)), - .just(.setLoading(true)), - .just(.setErrorAlert(false)) - ) - } - } else { - return .empty() - } - } - - } - - ) + return fetchRealEmojiPreSignedUseCase.execute(memberId: memberId, body: body, imageData: imageData) + .withUnretained(self) + .flatMap { owner, remoteURL -> Observable in + let originalURL = owner.configureProfileOriginalS3URL(url: remoteURL.imageURL, with: .realEmoji) + let body = UpdateRealEmojiImageRequest(imageUrl: originalURL) + let realEmojiId = owner.currentState.realEmojiEntity[owner.currentState.emojiType.rawValue - 1]?.realEmojiId ?? "" + return owner.fetchRealEmojiUpdateUseCase.execute(memberId: memberId, realEmojiId: realEmojiId, body: body) + .flatMap { entity -> Observable in + guard let entity else { + return .empty() + } + owner.provider.realEmojiGlobalState.updateRealEmojiImage(indexPath: owner.currentState.emojiType.rawValue - 1, image: entity.realEmojiImageURL) + + return .empty() + }.catchError(with: self) { owner, _ in + let type = owner.currentState.cameraType + owner.cameraNavigator.showErrorAlert(type) + return .empty() + } + }.catchError(with: self) { owner, _ in + let type = owner.currentState.cameraType + owner.cameraNavigator.showErrorAlert(type) + return .empty() + } } } } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlert.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlert.swift index 4991b3f2f..9d8b5e045 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlert.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlert.swift @@ -154,14 +154,15 @@ public class BBAlert { /// - Returns: BBAlert public static func style( _ style: BBAlertStyle, - primaryAction action: BBAlertActionHandler = nil, + primaryAction primaryHandler: BBAlertActionHandler = nil, + secondaryAction secondaryHandler: BBAlertActionHandler = nil, config: BBAlertConfiguration = BBAlertConfiguration() ) -> BBAlert { switch style { case .logout: let actions = [ BBAlertAction(title: "취소", style: .cancel), - BBAlertAction(title: "확인", handler: action) + BBAlertAction(title: "확인", handler: primaryHandler) ] let viewConfig = BBAlertViewConfiguration( minHeight: 145, @@ -180,7 +181,7 @@ public class BBAlert { case .makeNewFamily: let actions = [ BBAlertAction(title: "취소", style: .cancel), - BBAlertAction(title: "확인", handler: action) + BBAlertAction(title: "확인", handler: primaryHandler) ] let viewConfig = BBAlertViewConfiguration( minHeight: 181, @@ -199,7 +200,7 @@ public class BBAlert { case .resetFamilyName: let actions = [ BBAlertAction(title: "취소", style: .cancel), - BBAlertAction(title: "확인", handler: action) + BBAlertAction(title: "확인", handler: primaryHandler) ] let viewConfig = BBAlertViewConfiguration( minHeight: 181, @@ -217,7 +218,7 @@ public class BBAlert { case .widget: let actions = [ - BBAlertAction(title: "확인하기", handler: action), + BBAlertAction(title: "확인하기", handler: primaryHandler), BBAlertAction(title: "닫기", style: .cancel) ] let viewConfig = BBAlertViewConfiguration( @@ -237,7 +238,7 @@ public class BBAlert { case .mission: let actions = [ - BBAlertAction(title: "미션 사진 찍기", handler: action), + BBAlertAction(title: "미션 사진 찍기", handler: primaryHandler), BBAlertAction(title: "닫기", style: .cancel) ] let viewConfig = BBAlertViewConfiguration( @@ -257,7 +258,7 @@ public class BBAlert { case let .picking(name): let actions = [ - BBAlertAction(title: "지금 하기", handler: action), + BBAlertAction(title: "지금 하기", handler: primaryHandler), BBAlertAction(title: "다음에 하기", style: .cancel) ] let viewConfig = BBAlertViewConfiguration( @@ -277,7 +278,7 @@ public class BBAlert { case .takePhoto: let actions = [ - BBAlertAction(title: "생존 신고 먼저하기", handler: action), + BBAlertAction(title: "생존 신고 먼저하기", handler: primaryHandler), BBAlertAction(title: "다음에 하기", style: .cancel) ] let viewConfig = BBAlertViewConfiguration( @@ -295,6 +296,34 @@ public class BBAlert { ) return BBAlert(view: view, actions: actions, config: config) + case .uploadFailed: + + let actions = [ + BBAlertAction(title: "다시 촬영하기", handler: primaryHandler), + BBAlertAction( + title: "홈으로 이동", + style: .custom( + titleFontStyle: .body1Bold, + titleColor: .gray400, + backgroundColor: .gray700 + ), + handler: secondaryHandler + ) + ] + let viewConfig = BBAlertViewConfiguration( + minHeight: 384, + buttonAxis: .vertical + ) + let view = DefaultAlertView( + child: ImageAlertView( + image: DesignSystemAsset.uploadFailed.image, + title: "업로드에 실패했어요", + subtitle: "같은 문제가 반복된다면 앱을 껐다 켜거나\n 삭제하고 다시 설치해주세요.", + viewConfig: viewConfig + ), + viewConfig: viewConfig + ) + return BBAlert(view: view, actions: actions, config: config) } } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertStyle.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertStyle.swift index 400e12484..ef58397ec 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertStyle.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBAlert/BBAlertStyle.swift @@ -24,6 +24,7 @@ extension BBAlert { case mission case picking(name: String) case takePhoto + case uploadFailed } } diff --git a/14th-team5-iOS/Data/Sources/APIs/Camera/CameraAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/Camera/CameraAPIWorker.swift index c78da3430..bec4df922 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Camera/CameraAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Camera/CameraAPIWorker.swift @@ -23,7 +23,7 @@ extension CameraAPIWorker { /// - MemberId (사용자 멤버 ID) /// - body: CreatePresignedURLReqeustDTO (업로드 Image Name) /// - Returns : CameraRealEmojiPreSignedResponseDTO - public func createRealEmojiPresignedURL(memberId: String, body: CreatePresignedURLReqeustDTO) -> Observable { + public func createRealEmojiPresignedURL(memberId: String, body: CreatePresignedURLReqeustDTO) -> Observable { let spec = CameraAPIs.createRealEmojiPresignedURL(memberID: memberId, body: body).spec return request(spec) diff --git a/14th-team5-iOS/Data/Sources/APIs/Camera/Repository/CameraRepository.swift b/14th-team5-iOS/Data/Sources/APIs/Camera/Repository/CameraRepository.swift index 4a6193f67..4646f9c94 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Camera/Repository/CameraRepository.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Camera/Repository/CameraRepository.swift @@ -25,10 +25,10 @@ public final class CameraRepository { extension CameraRepository: CameraRepositoryProtocol { - public func createEmojiImagePresignedURL(memberID: String, body: CreatePresignedURLRequest) -> Observable { + public func createEmojiImagePresignedURL(memberID: String, body: CreatePresignedURLRequest) -> Observable { let body = CreatePresignedURLReqeustDTO(imageName: body.imageName) return cameraAPIWorker.createRealEmojiPresignedURL(memberId: memberID, body: body) - .map { $0?.toDomain() } + .map { $0.toDomain() } } public func createEmojiImage(memberId: String, body: CreateEmojiImageRequest) -> Observable { @@ -49,4 +49,7 @@ extension CameraRepository: CameraRepositoryProtocol { .map { $0?.toDomain() } } + public func uploadEmojiImageToS3Bucket(_ presignedURL: String, image: Data) -> Observable { + return cameraAPIWorker.upload(presignedURL, with: image) + } } diff --git a/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/Character/UploadFailed.imageset/Contents.json b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/Character/UploadFailed.imageset/Contents.json new file mode 100644 index 000000000..e92a0c7f2 --- /dev/null +++ b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/Character/UploadFailed.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "UploadFailed.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/Character/UploadFailed.imageset/UploadFailed.svg b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/Character/UploadFailed.imageset/UploadFailed.svg new file mode 100644 index 000000000..8f5a5af63 --- /dev/null +++ b/14th-team5-iOS/DesignSystem/Resources/Assets.xcassets/Character/UploadFailed.imageset/UploadFailed.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/14th-team5-iOS/Domain/Sources/Repositories/CameraRepository.swift b/14th-team5-iOS/Domain/Sources/Repositories/CameraRepository.swift index 6ad157c65..51bdbce4a 100644 --- a/14th-team5-iOS/Domain/Sources/Repositories/CameraRepository.swift +++ b/14th-team5-iOS/Domain/Sources/Repositories/CameraRepository.swift @@ -69,10 +69,12 @@ public enum UploadLocation { public protocol CameraRepositoryProtocol { var disposeBag: DisposeBag { get } - func createEmojiImagePresignedURL(memberID: String, body: CreatePresignedURLRequest) -> Observable + func createEmojiImagePresignedURL(memberID: String, body: CreatePresignedURLRequest) -> Observable func createEmojiImage(memberId: String, body: CreateEmojiImageRequest) -> Observable /// FETCH 메서드 func fetchEmojiList(memberId: String) -> Observable<[CameraRealEmojiImageItemEntity?]> func updateEmojiImage(memberId: String, realEmojiId: String, body: UpdateRealEmojiImageRequest) -> Observable + + func uploadEmojiImageToS3Bucket(_ presignedURL: String, image: Data) -> Observable } diff --git a/14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/RealEmoji/FetchCameraRealEmojiUseCase.swift b/14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/RealEmoji/FetchCameraRealEmojiUseCase.swift index ede13a632..77e56c4e6 100644 --- a/14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/RealEmoji/FetchCameraRealEmojiUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/Trash/Camera/UseCases/RealEmoji/FetchCameraRealEmojiUseCase.swift @@ -6,13 +6,14 @@ // import Foundation +import Core import RxSwift import RxCocoa public protocol FetchCameraRealEmojiUseCaseProtocol { - func execute(memberId: String, body: CreatePresignedURLRequest) -> Observable + func execute(memberId: String, body: CreatePresignedURLRequest, imageData: Data) -> Observable } public final class FetchCameraRealEmojiUseCase: FetchCameraRealEmojiUseCaseProtocol { @@ -23,7 +24,18 @@ public final class FetchCameraRealEmojiUseCase: FetchCameraRealEmojiUseCaseProto self.cameraRepository = cameraRepository } - public func execute(memberId: String, body: CreatePresignedURLRequest) -> Observable { + public func execute(memberId: String, body: CreatePresignedURLRequest, imageData: Data) -> Observable { + return cameraRepository.createEmojiImagePresignedURL(memberID: memberId, body: body) + .flatMap { [unowned self] presignedURL -> Observable in + + return self.cameraRepository.uploadEmojiImageToS3Bucket(presignedURL.imageURL, image: imageData) + .flatMap { isSuccess -> Observable in + if isSuccess { + return .just(presignedURL) + } + return .error(BBUploadError.uploadFailed) + } + } } } From 6238afbd635a064e2472ed175f274a5907b39bc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=84?= Date: Thu, 12 Dec 2024 19:20:00 +0900 Subject: [PATCH 253/263] =?UTF-8?q?fix:=20=EA=B0=80=EC=A1=B1=EB=B0=A9=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EC=84=A4=EC=A0=95=20=ED=99=94=EB=A9=B4?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EA=B0=80=EC=A1=B1=EB=B0=A9=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=20=EC=84=A4=EC=A0=95=20API=20=ED=98=B8=EC=B6=9C=20?= =?UTF-8?q?=EC=8B=9C=20400=20=EC=97=90=EB=9F=AC=20=EB=9C=A8=EB=8A=94=20?= =?UTF-8?q?=EC=9D=B4=EC=8A=88=EB=A5=BC=20=EC=88=98=EC=A0=95=ED=95=B4?= =?UTF-8?q?=EC=9A=94=20(#722)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: FamilyAPIs, FamilyWorker UpdateFamilyNameRequestBodyDTO 추가 * fix: UpdateFamilyNameRequestDTO encode Method 추가 --- .../UpdateFamilyNameRequestDTO.swift | 9 +++ .../Data/Sources/APIs/Family/FamilyAPIs.swift | 6 +- .../Sources/APIs/Family/FamilyWorker.swift | 2 +- Bibbi.xcworkspace/contents.xcworkspacedata | 21 ------- .../xcschemes/Bibbi-Workspace.xcscheme | 60 ++++++++++++++----- 5 files changed, 57 insertions(+), 41 deletions(-) diff --git a/14th-team5-iOS/Data/Sources/APIs/Family/DataMapping/UpdateFamilyNameRequestDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Family/DataMapping/UpdateFamilyNameRequestDTO.swift index c125f23d9..9cf946a99 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Family/DataMapping/UpdateFamilyNameRequestDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Family/DataMapping/UpdateFamilyNameRequestDTO.swift @@ -9,4 +9,13 @@ import Foundation public struct UpdateFamilyNameRequestDTO: Encodable { let familyName: String? + + private enum CodingKeys: String, CodingKey { + case familyName + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(familyName, forKey: .familyName) + } } diff --git a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPIs.swift index ae20ffd73..38c8162ca 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPIs.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPIs.swift @@ -13,7 +13,7 @@ enum FamilyAPIs: BBAPI { /// 가족 그룹 생성 시간 조회 case fetchFamilyCreatedAt(_ familyId: String) /// 가족 이름 변경 - case updateFamilyName(_ familyId: String) + case updateFamilyName(_ familyId: String, body: UpdateFamilyNameRequestDTO) var spec: Spec { switch self { @@ -21,8 +21,8 @@ enum FamilyAPIs: BBAPI { return .init(method: .post, path: "/families") case .fetchFamilyCreatedAt(let familyId): return .init(method: .get, path: "/families/\(familyId)/created-at") - case .updateFamilyName(let familyId): - return .init(method: .put, path: "/families/\(familyId)/name") + case .updateFamilyName(let familyId, let body): + return .init(method: .put, path: "/families/\(familyId)/name", bodyParametersEncodable: body) } } diff --git a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyWorker.swift b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyWorker.swift index e23504c6d..c9a1a3f05 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyWorker.swift @@ -31,7 +31,7 @@ extension FamilyWorker { _ familyId: String, body: UpdateFamilyNameRequestDTO ) -> Observable { - let spec = FamilyAPIs.updateFamilyName(familyId).spec + let spec = FamilyAPIs.updateFamilyName(familyId, body: body).spec return request(spec) } diff --git a/Bibbi.xcworkspace/contents.xcworkspacedata b/Bibbi.xcworkspace/contents.xcworkspacedata index 7bb900224..7e570dad8 100644 --- a/Bibbi.xcworkspace/contents.xcworkspacedata +++ b/Bibbi.xcworkspace/contents.xcworkspacedata @@ -26,9 +26,6 @@ - - @@ -38,9 +35,6 @@ - - @@ -50,9 +44,6 @@ - - @@ -86,27 +77,15 @@ - - - - - - - - diff --git a/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme b/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme index 1781b4e89..5fd01fa86 100644 --- a/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme +++ b/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme @@ -20,6 +20,20 @@ ReferencedContainer = "container:Tuist/.build/tuist-derived/Alamofire/Alamofire.xcodeproj"> + + + + @@ -560,9 +574,9 @@ buildForAnalyzing = "YES"> @@ -574,9 +588,9 @@ buildForAnalyzing = "YES"> @@ -588,9 +602,9 @@ buildForAnalyzing = "YES"> @@ -602,9 +616,9 @@ buildForAnalyzing = "YES"> @@ -616,9 +630,9 @@ buildForAnalyzing = "YES"> @@ -630,9 +644,9 @@ buildForAnalyzing = "YES"> @@ -644,9 +658,9 @@ buildForAnalyzing = "YES"> @@ -1140,6 +1154,20 @@ ReferencedContainer = "container:Tuist/.build/tuist-derived/SnapKit/SnapKit.xcodeproj"> + + + + Date: Tue, 17 Dec 2024 20:52:36 +0900 Subject: [PATCH 254/263] =?UTF-8?q?API=20Worker=EB=A5=BC=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A7=81=ED=95=B4=EC=9A=94=20(#725)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor api workers * fix the query url * fix: miss body --- .../DIContainer/OAuthDIContainer.swift | 2 +- .../Home/Reactor/MainViewReactor.swift | 10 +- .../PostDetail/Reactor/PostReactor.swift | 3 +- .../Privacy/Reactor/PrivacyViewReactor.swift | 19 +-- .../Privacy/View/BibbiInquireBannerView.swift | 14 -- .../Profile/ProfileViewController.swift | 9 +- .../FamilyWidget/FamilyWidgetEntry.swift | 2 +- .../FamilyWidget/FamilyWidgetView.swift | 2 +- .../APIs/App/AppAPI/AppAPIWorker.swift | 45 ------ .../Sources/APIs/App/AppAPI/AppAPIs.swift | 22 --- .../{CalendarAPI => }/CalendarAPIWorker.swift | 0 .../{CalendarAPI => }/CalendarAPIs.swift | 0 .../ArrayResponseCalendarResponseDTO.swift | 0 ...rrayResponseDailyCalendarResponseDTO.swift | 0 ...ayResponseMonthlyCalendarResponseDTO.swift | 0 .../DataMapping/BannerResponseDTO.swift | 0 .../FamilyMonthlyStatisticsResponseDTO.swift | 0 .../Comment/CommentAPI/CommentAPIWorker.swift | 37 +++-- .../APIs/Comment/CommentAPI/CommentAPIs.swift | 8 ++ .../CreatePostCommentReqeustDTO.swift | 2 +- ...ift => DeletePostCommentResponseDTO.swift} | 6 +- .../GetPostCommentResponseDTO.swift | 29 ++++ ...nationResponsePostCommentResponseDTO.swift | 65 --------- .../DataMapping/PostCommentResponseDTO.swift | 17 +-- .../UpdatePostCommentRequestDTO.swift | 2 +- .../Sources/APIs/DefaultResponseDTO.swift | 18 +++ ...milyWorker.swift => FamilyAPIWorker.swift} | 4 +- .../Data/Sources/APIs/Family/FamilyAPIs.swift | 16 ++- ....swift => FamilyInviteViewAPIWorker.swift} | 4 +- .../FamilyInviteViewAPIs.swift | 5 +- .../{LinkWorker.swift => LinkAPIWorker.swift} | 4 +- .../Data/Sources/APIs/Link/LinkAPIs.swift | 5 +- .../DataMapping/MainNightResponseDTO.swift | 0 .../DataMapping/MainRequestDTO.swift | 0 .../DataMapping/MainResponseDTO.swift | 0 .../MainViewAPI/MainViewAPIWorker.swift | 59 -------- .../MainView/MainViewAPI/MainViewAPIs.swift | 25 ---- .../APIs/MainView/MainViewAPIWorker.swift | 33 +++++ .../Sources/APIs/MainView/MainViewAPIs.swift | 34 +++++ .../Repository/MainViewRepository.swift | 51 ------- .../DataMapping/AddFCMTokenRequestDTO.swift | 0 .../DataMapping/AppVersionResponseDTO.swift | 19 +-- .../Data/Sources/APIs/Me/MeAPI.swift | 49 ++++++- .../Me/{MeWorker.swift => MeeAPIWorker.swift} | 43 +++++- ...r.swift => MemberRealEmojiAPIWorker.swift} | 4 +- ...ersWorker.swift => MembersAPIWorker.swift} | 4 +- .../MissionContentResponseDTO.swift | 0 .../MissonTodayContentResponseDTO.swift | 0 .../{MissionAPI => }/MissionAPIWorker.swift | 4 +- .../{MissionAPI => }/MissionAPIs.swift | 0 .../Sources/APIs/OAuth/AuthAPIWorker.swift | 47 ++++++ .../Data/Sources/APIs/OAuth/AuthAPIs.swift | 43 ++++++ .../DataMapping/AuthResultResponseDTO.swift | 16 +-- .../NativeSocialLoginRequestDTO.swift | 12 ++ .../DataMapping/RefreshTokenRequestDTO.swift | 13 ++ .../DataMapping/RefreshTokenResponseDTO.swift | 24 ++++ .../OAuth/DataMapping/SignUpRequestDTO.swift | 14 ++ .../OAuth/DataMapping/SignUpResponseDTO.swift | 24 ++++ .../CreateNewMemberRequestDTO.swift | 19 --- .../DataMapping/DefaultResponseDTO.swift | 22 --- .../NativeSocialLoginRequestDTO.swift | 15 -- .../RefreshAccessTokenRequestDTO.swift | 16 --- .../APIs/OAuth/OAuthAPI/OAuthAPIWorker.swift | 99 ------------- .../APIs/OAuth/OAuthAPI/OAuthAPIs.swift | 35 ----- .../OAuthAPI/Repository/OAuthRepository.swift | 133 ----------------- .../CreatePostPresignedURLReqeustDTO.swift | 0 .../CreatePostPresignedURLResponseDTO.swift | 0 .../DataMapping/CreatePostRequestDTO.swift | 0 .../DataMapping/CreatePostResponseDTO.swift | 0 .../CreatePresignedURLRequestDTO.swift | 0 .../DataMapping/PostDetailResponseDTO.swift | 0 .../DataMapping/PostListQueryDTO.swift | 0 .../DataMapping/PostListResponseDTO.swift | 0 .../APIs/Post/{PostAPI => }/PostAPIs.swift | 0 .../{PostAPI => }/PostListAPIWorker.swift | 2 - .../GetRecentPostsResponseDTO.swift} | 4 +- .../RecentFamilyPostRequestDTO.swift | 0 .../Widget/WidgetAPI/WidgetAPIWorker.swift | 51 ------- .../APIs/Widget/WidgetAPI/WidgetAPIs.swift | 22 --- .../Sources/APIs/Widget/WidgetAPIWorker.swift | 20 +++ .../Data/Sources/APIs/Widget/WidgetAPIs.swift | 27 ++++ .../AppRepository.swift | 11 +- .../Sources/Repositories/AuthRepository.swift | 135 ++++++++++++++++++ .../CalendarRepository.swift | 0 .../CameraRepository.swift | 0 .../CommentRepository.swift | 2 +- .../Repositories/FamilyRepository.swift | 12 +- .../Repositories/MainViewRepository.swift | 53 +++++++ .../Repositories/MembersRepository.swift | 4 +- .../MissionRepository.swift | 8 +- .../MyRepository.swift | 0 .../Sources/Repositories/PickRepository.swift | 4 +- .../PostRepository.swift | 32 ++++- .../PrivacyRepository.swift | 0 .../Repositories/RealEmojiRepository.swift | 2 +- .../Repositories/ResignRepository.swift | 2 +- .../WidgetRepository.swift | 5 +- .../AccountRepository/AccountRepository.swift | 2 +- .../Trash/Account/MeAPI/MeAPIWorker.swift | 1 + ...ostEntity.swift => WidgetPostEntity.swift} | 2 +- .../Sources/Repositories/AppRepository.swift | 2 +- .../Repositories/MainViewRepository.swift | 4 +- .../Repositories/MissonRepository.swift | 4 +- .../Repositories/OAuthRepository.swift | 10 +- .../Repositories/WidgetRepository.swift | 2 +- .../UseCases/App/FetchAppVersionUseCase.swift | 4 +- .../UseCases/MainView/FetchMainUseCase.swift | 4 +- .../MainView/FetchNightMainViewUsecase.swift | 4 +- .../FetchDailyMissonContentUseCase.swift | 4 +- .../Misson/FetchMissionContentUseCase.swift | 4 +- .../OAuth/DeleteFCMTokenUseCase.swift | 4 +- .../OAuth/RefreshAccessTokenUseCase.swift | 4 +- .../OAuth/RegisterNewFCMTokenUseCase.swift | 4 +- .../OAuth/RegisterNewMemberUseCase.swift | 4 +- .../UseCases/OAuth/SignInUseCase.swift | 4 +- .../Widget/FetchRecentFamilyPostUseCase.swift | 4 +- Bibbi.xcworkspace/contents.xcworkspacedata | 21 +++ .../xcschemes/Bibbi-Workspace.xcscheme | 60 +++----- 118 files changed, 835 insertions(+), 920 deletions(-) delete mode 100644 14th-team5-iOS/Data/Sources/APIs/App/AppAPI/AppAPIWorker.swift delete mode 100644 14th-team5-iOS/Data/Sources/APIs/App/AppAPI/AppAPIs.swift rename 14th-team5-iOS/Data/Sources/APIs/Calendar/{CalendarAPI => }/CalendarAPIWorker.swift (100%) rename 14th-team5-iOS/Data/Sources/APIs/Calendar/{CalendarAPI => }/CalendarAPIs.swift (100%) rename 14th-team5-iOS/Data/Sources/APIs/Calendar/{CalendarAPI => }/DataMapping/ArrayResponseCalendarResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/APIs/Calendar/{CalendarAPI => }/DataMapping/ArrayResponseDailyCalendarResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/APIs/Calendar/{CalendarAPI => }/DataMapping/ArrayResponseMonthlyCalendarResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/APIs/Calendar/{CalendarAPI => }/DataMapping/BannerResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/APIs/Calendar/{CalendarAPI => }/DataMapping/FamilyMonthlyStatisticsResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/{PostCommentDeleteResponseDTO.swift => DeletePostCommentResponseDTO.swift} (65%) create mode 100644 14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/GetPostCommentResponseDTO.swift delete mode 100644 14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/PaginationResponsePostCommentResponseDTO.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/DefaultResponseDTO.swift rename 14th-team5-iOS/Data/Sources/APIs/Family/{FamilyWorker.swift => FamilyAPIWorker.swift} (93%) rename 14th-team5-iOS/Data/Sources/APIs/FamilyInviteView/{FamilyInviteViewWorker.swift => FamilyInviteViewAPIWorker.swift} (83%) rename 14th-team5-iOS/Data/Sources/APIs/Link/{LinkWorker.swift => LinkAPIWorker.swift} (87%) rename 14th-team5-iOS/Data/Sources/APIs/MainView/{MainViewAPI => }/DataMapping/MainNightResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/APIs/MainView/{MainViewAPI => }/DataMapping/MainRequestDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/APIs/MainView/{MainViewAPI => }/DataMapping/MainResponseDTO.swift (100%) delete mode 100644 14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/MainViewAPIWorker.swift delete mode 100644 14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/MainViewAPIs.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPIWorker.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPIs.swift delete mode 100644 14th-team5-iOS/Data/Sources/APIs/MainView/Repository/MainViewRepository.swift rename 14th-team5-iOS/Data/Sources/APIs/{OAuth/OAuthAPI => Me}/DataMapping/AddFCMTokenRequestDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/APIs/{App/AppAPI => Me}/DataMapping/AppVersionResponseDTO.swift (54%) rename 14th-team5-iOS/Data/Sources/APIs/Me/{MeWorker.swift => MeeAPIWorker.swift} (51%) rename 14th-team5-iOS/Data/Sources/APIs/MemberRealEmoji/{MemberRealEmojiWorker.swift => MemberRealEmojiAPIWorker.swift} (83%) rename 14th-team5-iOS/Data/Sources/APIs/Members/{MembersWorker.swift => MembersAPIWorker.swift} (98%) rename 14th-team5-iOS/Data/Sources/APIs/Mission/{MissionAPI => }/DataMapping/MissionContentResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/APIs/Mission/{MissionAPI => }/DataMapping/MissonTodayContentResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/APIs/Mission/{MissionAPI => }/MissionAPIWorker.swift (95%) rename 14th-team5-iOS/Data/Sources/APIs/Mission/{MissionAPI => }/MissionAPIs.swift (100%) create mode 100644 14th-team5-iOS/Data/Sources/APIs/OAuth/AuthAPIWorker.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/OAuth/AuthAPIs.swift rename 14th-team5-iOS/Data/Sources/APIs/OAuth/{OAuthAPI => }/DataMapping/AuthResultResponseDTO.swift (50%) create mode 100644 14th-team5-iOS/Data/Sources/APIs/OAuth/DataMapping/NativeSocialLoginRequestDTO.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/OAuth/DataMapping/RefreshTokenRequestDTO.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/OAuth/DataMapping/RefreshTokenResponseDTO.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/OAuth/DataMapping/SignUpRequestDTO.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/OAuth/DataMapping/SignUpResponseDTO.swift delete mode 100644 14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/DataMapping/CreateNewMemberRequestDTO.swift delete mode 100644 14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/DataMapping/DefaultResponseDTO.swift delete mode 100644 14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/DataMapping/NativeSocialLoginRequestDTO.swift delete mode 100644 14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/DataMapping/RefreshAccessTokenRequestDTO.swift delete mode 100644 14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/OAuthAPIWorker.swift delete mode 100644 14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/OAuthAPIs.swift delete mode 100644 14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/Repository/OAuthRepository.swift rename 14th-team5-iOS/Data/Sources/APIs/Post/{PostAPI => }/DataMapping/CreatePostPresignedURLReqeustDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/APIs/Post/{PostAPI => }/DataMapping/CreatePostPresignedURLResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/APIs/Post/{PostAPI => }/DataMapping/CreatePostRequestDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/APIs/Post/{PostAPI => }/DataMapping/CreatePostResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/APIs/Post/{PostAPI => }/DataMapping/CreatePresignedURLRequestDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/APIs/Post/{PostAPI => }/DataMapping/PostDetailResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/APIs/Post/{PostAPI => }/DataMapping/PostListQueryDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/APIs/Post/{PostAPI => }/DataMapping/PostListResponseDTO.swift (100%) rename 14th-team5-iOS/Data/Sources/APIs/Post/{PostAPI => }/PostAPIs.swift (100%) rename 14th-team5-iOS/Data/Sources/APIs/Post/{PostAPI => }/PostListAPIWorker.swift (99%) rename 14th-team5-iOS/Data/Sources/APIs/Widget/{WidgetAPI/DataMaaping/RecentFamilyPostResponseDTO.swift => DataMaaping/GetRecentPostsResponseDTO.swift} (85%) rename 14th-team5-iOS/Data/Sources/APIs/Widget/{WidgetAPI => }/DataMaaping/RecentFamilyPostRequestDTO.swift (100%) delete mode 100644 14th-team5-iOS/Data/Sources/APIs/Widget/WidgetAPI/WidgetAPIWorker.swift delete mode 100644 14th-team5-iOS/Data/Sources/APIs/Widget/WidgetAPI/WidgetAPIs.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/Widget/WidgetAPIWorker.swift create mode 100644 14th-team5-iOS/Data/Sources/APIs/Widget/WidgetAPIs.swift rename 14th-team5-iOS/Data/Sources/{APIs/App/Repository => Repositories}/AppRepository.swift (85%) create mode 100644 14th-team5-iOS/Data/Sources/Repositories/AuthRepository.swift rename 14th-team5-iOS/Data/Sources/{APIs/Calendar/Repository => Repositories}/CalendarRepository.swift (100%) rename 14th-team5-iOS/Data/Sources/{APIs/Camera/Repository => Repositories}/CameraRepository.swift (100%) rename 14th-team5-iOS/Data/Sources/{APIs/Comment/Repository => Repositories}/CommentRepository.swift (96%) create mode 100644 14th-team5-iOS/Data/Sources/Repositories/MainViewRepository.swift rename 14th-team5-iOS/Data/Sources/{APIs/Mission/Repository => Repositories}/MissionRepository.swift (93%) rename 14th-team5-iOS/Data/Sources/{APIs/My/Repository => Repositories}/MyRepository.swift (100%) rename 14th-team5-iOS/Data/Sources/{APIs/Post/Repository => Repositories}/PostRepository.swift (57%) rename 14th-team5-iOS/Data/Sources/{APIs/Privacy/Repository => Repositories}/PrivacyRepository.swift (100%) rename 14th-team5-iOS/Data/Sources/{APIs/Widget/Repository => Repositories}/WidgetRepository.swift (84%) rename 14th-team5-iOS/Domain/Sources/Entities/Widget/{RecentFamilyPostEntity.swift => WidgetPostEntity.swift} (93%) diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/OAuthDIContainer.swift b/14th-team5-iOS/App/Sources/Application/DIContainer/OAuthDIContainer.swift index 80cafa06c..09bbc1708 100644 --- a/14th-team5-iOS/App/Sources/Application/DIContainer/OAuthDIContainer.swift +++ b/14th-team5-iOS/App/Sources/Application/DIContainer/OAuthDIContainer.swift @@ -47,7 +47,7 @@ final class OAuthDIContainer: BaseContainer { // MARK: - Make Repository private func makeOAuthRepository() -> OAuthRepositoryProtocol { - return OAuthRepository() + return AuthRepository() } diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift index 4d988f0ed..96edf8693 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift @@ -131,10 +131,7 @@ extension MainViewReactor { case .fetchMainUseCase: return fetchMainUseCase.execute() .asObservable() - .flatMap { result -> Observable in - guard let data = result else { - return Observable.empty() - } + .flatMap { data -> Observable in return Observable.concat( .just(.updateMainData(data)), .just(.setBalloonText), @@ -145,10 +142,7 @@ extension MainViewReactor { case .fetchMainNightUseCase: return fetchMainNightUseCase.execute() .asObservable() - .flatMap { result -> Observable in - guard let data = result else { - return .empty() - } + .flatMap { data -> Observable in return .just(.updateMainNight(data)) } case .setTimer(let isInTime, let time): diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostReactor.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostReactor.swift index 1e6a99198..d1f6a0f66 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostReactor.swift @@ -70,10 +70,9 @@ extension PostReactor { return fetchMissionUseCase.execute(missionId: missionId) .asObservable() .flatMap { entity -> Observable in - guard let originEntity = entity else { return .empty() } return .concat( .just(.setSelectedPostIndex(index)), - .just(.setMissionContent(originEntity)) + .just(.setMissionContent(entity)) ) } diff --git a/14th-team5-iOS/App/Sources/Presentation/Privacy/Reactor/PrivacyViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Privacy/Reactor/PrivacyViewReactor.swift index 957cf4459..b49e66237 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Privacy/Reactor/PrivacyViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Privacy/Reactor/PrivacyViewReactor.swift @@ -12,8 +12,6 @@ import Domain import Data import ReactorKit - - public final class PrivacyViewReactor: Reactor { public var initialState: State @Injected var fetchAppVersionUseCase: FetchAppVersionUseCaseProtocol @@ -70,17 +68,20 @@ public final class PrivacyViewReactor: Reactor { .just(.setLoading(true)), .merge( fetchAppVersionUseCase.execute() - .asObservable() .withUnretained(self) .flatMap { owner, appVersionEntity -> Observable in - owner.fetchPrivacyItemsUseCase.execute() - .asObservable() + return owner.fetchPrivacyItemsUseCase.execute() .flatMap { privateInfo -> Observable in - guard let appVersionEntity = appVersionEntity else { return .empty() } var sectionItem: [PrivacyItemModel] = [] privateInfo.forEach { - sectionItem.append(.privacyWithAuthItem(PrivacyCellReactor(descrption: $0, isCheck: appVersionEntity.latest))) - + sectionItem.append( + .privacyWithAuthItem( + PrivacyCellReactor( + descrption: $0, + isCheck: appVersionEntity.latest + ) + ) + ) } return .concat( @@ -88,7 +89,7 @@ public final class PrivacyViewReactor: Reactor { .just(.setBibbiAppInfo(appVersionEntity)) ) } - + }, fetchAuthorizationItemUseCase.execute() diff --git a/14th-team5-iOS/App/Sources/Presentation/Privacy/View/BibbiInquireBannerView.swift b/14th-team5-iOS/App/Sources/Presentation/Privacy/View/BibbiInquireBannerView.swift index feef9bc16..05297fe79 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Privacy/View/BibbiInquireBannerView.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Privacy/View/BibbiInquireBannerView.swift @@ -13,8 +13,6 @@ import SnapKit import Then final class BibbiInquireBannerView: UIView { - - private let mainLogoView: UIImageView = UIImageView() private let subLogoView: UIImageView = UIImageView() private let descrptionLabel: UILabel = BBLabel(.body1Bold, textAlignment: .left) @@ -27,7 +25,6 @@ final class BibbiInquireBannerView: UIView { setupUI() setupAttributes() setupAutoLayout() - print("Device Size: \(UIScreen.main.bounds.size.width)") } required init?(coder: NSCoder) { @@ -38,8 +35,6 @@ final class BibbiInquireBannerView: UIView { addSubviews(mainLogoView, subLogoView, arrowImageView, descrptionLabel, subtitleLabel) } - - private func setupAttributes() { mainLogoView.do { @@ -68,10 +63,6 @@ final class BibbiInquireBannerView: UIView { $0.text = "의견 남기러 가기" $0.textColor = .blue500 } - - - - } @@ -107,12 +98,7 @@ final class BibbiInquireBannerView: UIView { $0.width.height.equalTo(11) $0.centerY.equalTo(subtitleLabel) } - - - } - - } diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileViewController.swift index d7e1bb4f4..a37b1af05 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileViewController.swift @@ -263,7 +263,7 @@ public final class ProfileViewController: BaseViewController } // 기본 이미지가 true 이고 닉네임 변경 할 경우 redraw -extension ProfileViewController { +extension ProfileViewController { private func setupProfileImage(url: URL) { let processor = DownsamplingImageProcessor(size: profileView.bounds.size) @@ -324,10 +324,11 @@ extension ProfileViewController: PHPickerViewControllerDelegate { picker.dismiss(animated: true, completion: nil) if let imageProvider = itemProvider, imageProvider.canLoadObject(ofClass: UIImage.self) { - imageProvider.loadObject(ofClass: UIImage.self) { image, error in - guard let photoImage: UIImage = image as? UIImage, + imageProvider.loadObject(ofClass: UIImage.self) { [weak self] image, error in + guard let self = self, + let photoImage: UIImage = image as? UIImage, let originalData: Data = photoImage.jpegData(compressionQuality: 1.0) else { return } - imageProvider.didSelectProfileImageWithProcessing(photo: originalData, error: error) + self.reactor?.action.onNext(.didSelectPHAssetsImage(originalData)) } } diff --git a/14th-team5-iOS/App/WidgetExtension/Sources/FamilyWidget/FamilyWidgetEntry.swift b/14th-team5-iOS/App/WidgetExtension/Sources/FamilyWidget/FamilyWidgetEntry.swift index bccb73f3f..27d49333a 100644 --- a/14th-team5-iOS/App/WidgetExtension/Sources/FamilyWidget/FamilyWidgetEntry.swift +++ b/14th-team5-iOS/App/WidgetExtension/Sources/FamilyWidget/FamilyWidgetEntry.swift @@ -10,5 +10,5 @@ import Domain struct FamilyWidgetEntry: TimelineEntry { let date: Date - let family: RecentFamilyPostEntity? + let family: WidgetPostEntity? } diff --git a/14th-team5-iOS/App/WidgetExtension/Sources/FamilyWidget/FamilyWidgetView.swift b/14th-team5-iOS/App/WidgetExtension/Sources/FamilyWidget/FamilyWidgetView.swift index a3896d0e8..23dce9e5e 100644 --- a/14th-team5-iOS/App/WidgetExtension/Sources/FamilyWidget/FamilyWidgetView.swift +++ b/14th-team5-iOS/App/WidgetExtension/Sources/FamilyWidget/FamilyWidgetView.swift @@ -153,7 +153,7 @@ struct FamilyWidgetView: View { } // MARK: 가족중 일부가 사진을 올렸을 때 뷰 - private func getPhotoView(info: RecentFamilyPostEntity) -> some View { + private func getPhotoView(info: WidgetPostEntity) -> some View { ZStack { if let postImageUrl = info.postImageUrl { diff --git a/14th-team5-iOS/Data/Sources/APIs/App/AppAPI/AppAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/App/AppAPI/AppAPIWorker.swift deleted file mode 100644 index 9c3693d07..000000000 --- a/14th-team5-iOS/Data/Sources/APIs/App/AppAPI/AppAPIWorker.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// AppAPIWorker.swift -// Data -// -// Created by 김건우 on 7/10/24. -// - -import Core -import Domain -import Foundation - -import RxSwift - -public typealias AppAPIWorker = AppAPIs.Worker -extension AppAPIs { - public final class Worker: APIWorker { - static let queue = { - ConcurrentDispatchQueueScheduler(queue: DispatchQueue(label: "AppAPIQueue", qos: .utility)) - }() - - public override init() { - super.init() - self.id = "AppAPIWorker" - } - } -} - -// MARK: - Extensions - -extension AppAPIWorker { - - - // MARK: - Fetch App Info - - public func fetchAppVersion(appKey: String) -> Single { - let spec = AppAPIs.appVersion(appKey).spec - - return request(spec: spec) - .subscribe(on: Self.queue) - .map(AppVersionResponseDTO.self) - .catchAndReturn(nil) - .asSingle() - } - -} diff --git a/14th-team5-iOS/Data/Sources/APIs/App/AppAPI/AppAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/App/AppAPI/AppAPIs.swift deleted file mode 100644 index 34a7a714f..000000000 --- a/14th-team5-iOS/Data/Sources/APIs/App/AppAPI/AppAPIs.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// AppAPIs.swift -// Data -// -// Created by 김건우 on 7/10/24. -// - -import Core -import Foundation - -public enum AppAPIs: API { - - case appVersion(String) - - public var spec: APISpec { - switch self { - case let .appVersion(appKey): - return APISpec(method: .get, url: "\(BibbiAPI.hostApi)/me/app-version?appKey=\(appKey)") - } - } - -} diff --git a/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/CalendarAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPIWorker.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/CalendarAPIWorker.swift rename to 14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPIWorker.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/CalendarAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPIs.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/CalendarAPIs.swift rename to 14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPIs.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/DataMapping/ArrayResponseCalendarResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Calendar/DataMapping/ArrayResponseCalendarResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/DataMapping/ArrayResponseCalendarResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Calendar/DataMapping/ArrayResponseCalendarResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/DataMapping/ArrayResponseDailyCalendarResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Calendar/DataMapping/ArrayResponseDailyCalendarResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/DataMapping/ArrayResponseDailyCalendarResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Calendar/DataMapping/ArrayResponseDailyCalendarResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/DataMapping/ArrayResponseMonthlyCalendarResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Calendar/DataMapping/ArrayResponseMonthlyCalendarResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/DataMapping/ArrayResponseMonthlyCalendarResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Calendar/DataMapping/ArrayResponseMonthlyCalendarResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/DataMapping/BannerResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Calendar/DataMapping/BannerResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/DataMapping/BannerResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Calendar/DataMapping/BannerResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/DataMapping/FamilyMonthlyStatisticsResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Calendar/DataMapping/FamilyMonthlyStatisticsResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/APIs/Calendar/CalendarAPI/DataMapping/FamilyMonthlyStatisticsResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Calendar/DataMapping/FamilyMonthlyStatisticsResponseDTO.swift 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 2fc752db8..a41b62cf5 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/CommentAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/CommentAPIWorker.swift @@ -12,24 +12,17 @@ import Foundation import RxSwift typealias CommentAPIWorker = CommentAPIs.Worker -extension CommentAPIs { - public final class Worker: BBRxAPIWorker { - public init() { super.init() } - } -} - - -// MARK: - Extensions extension CommentAPIWorker { - - // MARK: - Fetch Comment - + /// 게시물에 등록 된 댓글을 페이지네이션으로 조회하는 Method입니다. + /// HTTP Method: GET + /// - Parameters: postId: String + /// - Returns: GetPostCommentResponseDTO func fetchComment( postId: String, query: PostCommentPaginationQuery - ) -> Observable { + ) -> Observable { let page = query.page let size = query.size let sort = query.sort.rawValue @@ -39,8 +32,10 @@ extension CommentAPIWorker { } - // MARK: - Create Comment - + /// 게시물에 댓글을 등록하는 Method입니다. + /// HTTP Method: POST + /// - Parameters: postId: String + /// - Returns: PostCommentResponseDTO func createComment( postId: String, body: CreatePostCommentReqeustDTO @@ -51,8 +46,10 @@ extension CommentAPIWorker { } - // MARK: - Update Comment - + /// 게시물에 등록된 댓글을 수정하는 Method입니다. + /// HTTP Method: PUT + /// - Parameters: postId: String, commentId: String + /// - Returns: PostCommentResponseDTO func updateComment( postId: String, commentId: String, @@ -64,12 +61,14 @@ extension CommentAPIWorker { } - // MARK: - Delete Comment - + /// 게시물에 등록된 댓글을 삭제하는 Method입니다. + /// HTTP Method: Delete + /// - Parameters: postId: String, commentId: String + /// - Returns: DeletePostCommentResponseDTO func deleteComment( postId: String, commentId: String - ) -> Observable { + ) -> Observable { let spec = CommentAPIs.deletePostComment(postId: postId, commentId: commentId).spec 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 5f25ffdb1..1412ff4f7 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/CommentAPIs.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/CommentAPIs.swift @@ -9,9 +9,13 @@ import Core import Foundation 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: Spec { @@ -47,4 +51,8 @@ enum CommentAPIs: BBAPI { ) } } + + final class Worker: BBRxAPIWorker { + init() { } + } } diff --git a/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/CreatePostCommentReqeustDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/CreatePostCommentReqeustDTO.swift index 6e99829c1..6e908e76d 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/CreatePostCommentReqeustDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/CreatePostCommentReqeustDTO.swift @@ -8,5 +8,5 @@ import Foundation public struct CreatePostCommentReqeustDTO: Encodable { - var content: String + let content: String } diff --git a/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/PostCommentDeleteResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/DeletePostCommentResponseDTO.swift similarity index 65% rename from 14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/PostCommentDeleteResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/DeletePostCommentResponseDTO.swift index 7ce370f22..c2fe026b5 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/PostCommentDeleteResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/DeletePostCommentResponseDTO.swift @@ -8,11 +8,11 @@ import Domain import Foundation -public struct PostCommentDeleteResponseDTO: Decodable { - var success: Bool +public struct DeletePostCommentResponseDTO: Decodable { + let success: Bool } -extension PostCommentDeleteResponseDTO { +extension DeletePostCommentResponseDTO { func toDomain() -> PostCommentDeleteEntity { return .init(success: success) } diff --git a/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/GetPostCommentResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/GetPostCommentResponseDTO.swift new file mode 100644 index 000000000..cfcb6b01e --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/GetPostCommentResponseDTO.swift @@ -0,0 +1,29 @@ +// +// PaginationResponsePostCommentResponseDTO.swift +// Data +// +// Created by 김건우 on 1/17/24. +// + +import Domain +import Foundation + +public struct GetPostCommentResponseDTO: Decodable { + let currentPage: Int + let totalPage: Int + let itemPerPage: Int + let hasNext: Bool + let results: [PostCommentResponseDTO] +} + +extension GetPostCommentResponseDTO { + func toDomain() -> PaginationResponsePostCommentEntity { + return .init( + currentPage: currentPage, + totalPage: totalPage, + itemPerPage: itemPerPage, + hasNext: hasNext, + results: results.map { $0.toDomain() } + ) + } +} diff --git a/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/PaginationResponsePostCommentResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/PaginationResponsePostCommentResponseDTO.swift deleted file mode 100644 index b1e7d3d31..000000000 --- a/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/PaginationResponsePostCommentResponseDTO.swift +++ /dev/null @@ -1,65 +0,0 @@ -// -// PaginationResponsePostCommentResponseDTO.swift -// Data -// -// Created by 김건우 on 1/17/24. -// - -import Domain -import Foundation - -public struct PaginationResponsePostCommentResponseDTO: Decodable { - private enum CodingKeys: String, CodingKey { - case currentPage - case totalPage - case itemPerPage - case hasNext - case results - } - var currentPage: Int - var totalPage: Int - var itemPerPage: Int - var hasNext: Bool - var results: [PostCommentResponseDTO] -} - -extension PaginationResponsePostCommentResponseDTO { - public struct PostCommentResponseDTO: Decodable { - private enum CodingKeys: String, CodingKey { - case commentId - case postId - case memberId - case comment - case createdAt - } - var commentId: String - var postId: String - var memberId: String - var comment: String - var createdAt: String - } -} - -extension PaginationResponsePostCommentResponseDTO { - func toDomain() -> PaginationResponsePostCommentEntity { - return .init( - currentPage: currentPage, - totalPage: totalPage, - itemPerPage: itemPerPage, - hasNext: hasNext, - results: results.map { $0.toDomain() } - ) - } -} - -extension PaginationResponsePostCommentResponseDTO.PostCommentResponseDTO { - func toDomain() -> PostCommentEntity { - return .init( - commentId: commentId, - postId: postId, - memberId: memberId, - comment: comment, - createdAt: createdAt.iso8601ToDate() - ) - } -} diff --git a/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/PostCommentResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/PostCommentResponseDTO.swift index c9221ad1b..43a38a4bb 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/PostCommentResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/PostCommentResponseDTO.swift @@ -9,18 +9,11 @@ import Domain import Foundation public struct PostCommentResponseDTO: Decodable { - private enum CodingKeys: String, CodingKey { - case commentId - case postId - case memberId - case comment - case createdAt - } - var commentId: String - var postId: String - var memberId: String - var comment: String - var createdAt: String + let commentId: String + let postId: String + let memberId: String + let comment: String + let createdAt: String } extension PostCommentResponseDTO { diff --git a/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/UpdatePostCommentRequestDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/UpdatePostCommentRequestDTO.swift index 81dde4523..77b79c531 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/UpdatePostCommentRequestDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Comment/CommentAPI/DataMapping/UpdatePostCommentRequestDTO.swift @@ -8,5 +8,5 @@ import Foundation public struct UpdatePostCommentReqeustDTO: Encodable { - var content: String + let content: String } diff --git a/14th-team5-iOS/Data/Sources/APIs/DefaultResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/DefaultResponseDTO.swift new file mode 100644 index 000000000..5eea2d6fa --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/DefaultResponseDTO.swift @@ -0,0 +1,18 @@ +// +// DefaultResponseDTO.swift +// Data +// +// Created by 마경미 on 15.12.24. +// + +import Domain + +struct DefaultResponseDTO: Decodable { + let success: Bool +} + +extension DefaultResponseDTO { + func toDomain() -> DefaultEntity { + return .init(success: self.success) + } +} diff --git a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyWorker.swift b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPIWorker.swift similarity index 93% rename from 14th-team5-iOS/Data/Sources/APIs/Family/FamilyWorker.swift rename to 14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPIWorker.swift index c9a1a3f05..24bfa0a19 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPIWorker.swift @@ -9,8 +9,8 @@ import Core import RxSwift -typealias FamilyWorker = FamilyAPIs.Worker -extension FamilyWorker { +typealias FamilyAPIWorker = FamilyAPIs.Worker +extension FamilyAPIWorker { /// 가족이 생성된 날짜를 조회하기 위한 Method입니다. /// HTTP Method: GET /// - Parameters: familyId: String diff --git a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPIs.swift index 38c8162ca..4eea1bf65 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPIs.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Family/FamilyAPIs.swift @@ -18,11 +18,21 @@ enum FamilyAPIs: BBAPI { var spec: Spec { switch self { case .createFamily: - return .init(method: .post, path: "/families") + return .init( + method: .post, + path: "/families" + ) case .fetchFamilyCreatedAt(let familyId): - return .init(method: .get, path: "/families/\(familyId)/created-at") + return .init( + method: .get, + path: "/families/\(familyId)/created-at" + ) case .updateFamilyName(let familyId, let body): - return .init(method: .put, path: "/families/\(familyId)/name", bodyParametersEncodable: body) + return .init( + method: .put, + path: "/families/\(familyId)/name", + bodyParametersEncodable: body + ) } } diff --git a/14th-team5-iOS/Data/Sources/APIs/FamilyInviteView/FamilyInviteViewWorker.swift b/14th-team5-iOS/Data/Sources/APIs/FamilyInviteView/FamilyInviteViewAPIWorker.swift similarity index 83% rename from 14th-team5-iOS/Data/Sources/APIs/FamilyInviteView/FamilyInviteViewWorker.swift rename to 14th-team5-iOS/Data/Sources/APIs/FamilyInviteView/FamilyInviteViewAPIWorker.swift index c98458559..80bf4df13 100644 --- a/14th-team5-iOS/Data/Sources/APIs/FamilyInviteView/FamilyInviteViewWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/FamilyInviteView/FamilyInviteViewAPIWorker.swift @@ -7,8 +7,8 @@ import RxSwift -typealias FamilyInviteViewWorker = FamilyInviteViewAPIs.Worker -extension FamilyInviteViewWorker { +typealias FamilyInviteViewAPIWorker = FamilyInviteViewAPIs.Worker +extension FamilyInviteViewAPIWorker { /// 가족방 초대 링크를 조회하기 위한 Method입니다. /// HTTP Method: GET /// - Parameters: familyId: String diff --git a/14th-team5-iOS/Data/Sources/APIs/FamilyInviteView/FamilyInviteViewAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/FamilyInviteView/FamilyInviteViewAPIs.swift index c8517deed..87a384065 100644 --- a/14th-team5-iOS/Data/Sources/APIs/FamilyInviteView/FamilyInviteViewAPIs.swift +++ b/14th-team5-iOS/Data/Sources/APIs/FamilyInviteView/FamilyInviteViewAPIs.swift @@ -16,7 +16,10 @@ enum FamilyInviteViewAPIs: BBAPI { var spec: Spec { switch self { case .fetchFamilyInfoWithLink(let linkId): - return .init(method: .get, path: "/view/family-invite/\(linkId)") + return .init( + method: .get, + path: "/view/family-invite/\(linkId)" + ) } } diff --git a/14th-team5-iOS/Data/Sources/APIs/Link/LinkWorker.swift b/14th-team5-iOS/Data/Sources/APIs/Link/LinkAPIWorker.swift similarity index 87% rename from 14th-team5-iOS/Data/Sources/APIs/Link/LinkWorker.swift rename to 14th-team5-iOS/Data/Sources/APIs/Link/LinkAPIWorker.swift index e39fd6f48..4c30d6960 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Link/LinkWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Link/LinkAPIWorker.swift @@ -9,8 +9,8 @@ import Core import RxSwift -typealias LinkWorker = LinkAPIs.Worker -extension LinkWorker { +typealias LinkAPIWorker = LinkAPIs.Worker +extension LinkAPIWorker { /// 가족 초대 링크를 생성하는 Method입니다. /// HTTP Method: POST /// - Parameters: familyId: String diff --git a/14th-team5-iOS/Data/Sources/APIs/Link/LinkAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/Link/LinkAPIs.swift index fe9164870..2cc69f518 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Link/LinkAPIs.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Link/LinkAPIs.swift @@ -14,7 +14,10 @@ enum LinkAPIs: BBAPI { var spec: Spec { switch self { case let .createFamilyLink(familyId): - return .init(method: .post, path: "/links/family/\(familyId)") + return .init( + method: .post, + path: "/links/family/\(familyId)" + ) } } diff --git a/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/DataMapping/MainNightResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/MainView/DataMapping/MainNightResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/DataMapping/MainNightResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/MainView/DataMapping/MainNightResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/DataMapping/MainRequestDTO.swift b/14th-team5-iOS/Data/Sources/APIs/MainView/DataMapping/MainRequestDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/DataMapping/MainRequestDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/MainView/DataMapping/MainRequestDTO.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/DataMapping/MainResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/MainView/DataMapping/MainResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/DataMapping/MainResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/MainView/DataMapping/MainResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/MainViewAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/MainViewAPIWorker.swift deleted file mode 100644 index 6313231a8..000000000 --- a/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/MainViewAPIWorker.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// MainAPIWorker.swift -// Data -// -// Created by 마경미 on 20.04.24. -// - -import Foundation - -import Core -import Domain - -import RxSwift - -typealias MainAPIWorker = MainViewAPIs.Worker -extension MainViewAPIs { - final class Worker: APIWorker { - static let queue = { - ConcurrentDispatchQueueScheduler(queue: DispatchQueue(label: "MainAPIWorker", qos: .utility)) - }() - - override init() { - super.init() - self.id = "MainAPIWorker" - } - - var headers: [APIHeader] { - var headers: [any APIHeader] = [] - - _headers.subscribe(onNext: { result in - if let unwrappedHeaders = result { - headers = unwrappedHeaders - } - }).dispose() - - return headers - } - } -} - -extension MainAPIWorker { - func fetchMain() -> Single { - let spec = MainViewAPIs.fetchMain.spec - return request(spec: spec, headers: headers) - .subscribe(on: Self.queue) - .map(MainResponseDTO.self) - .catchAndReturn(nil) - .asSingle() - } - - func fetchMainNight() -> Single { - let spec = MainViewAPIs.fetchMainNight.spec - return request(spec: spec, headers: headers) - .subscribe(on: Self.queue) - .map(MainNightResponseDTO.self) - .catchAndReturn(nil) - .asSingle() - } -} diff --git a/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/MainViewAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/MainViewAPIs.swift deleted file mode 100644 index ba2346705..000000000 --- a/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPI/MainViewAPIs.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// MainAPIs.swift -// Data -// -// Created by 마경미 on 20.04.24. -// - -import Core -import Foundation - -enum MainViewAPIs: API { - case fetchMain - case fetchMainNight - - var spec: APISpec { - switch self { - case .fetchMain: - let urlString = "\(BibbiAPI.hostApi)/view/main/daytime-page" - return APISpec(method: .get, url: urlString) - case .fetchMainNight: - let urlString = "\(BibbiAPI.hostApi)/view/main/nighttime-page" - return APISpec(method: .get, url: urlString) - } - } -} diff --git a/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPIWorker.swift new file mode 100644 index 000000000..535257bc2 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPIWorker.swift @@ -0,0 +1,33 @@ +// +// MainAPIWorker.swift +// Data +// +// Created by 마경미 on 20.04.24. +// + +import Foundation + +import Core +import Domain + +import RxSwift + +typealias MainAPIWorker = MainViewAPIs.Worker + +extension MainAPIWorker { + /// 생존신고 활성화 시간에 main 화면 데이터를 조회하기 위한 Method입니다. + /// HTTP Method: GET + /// - Returns: MainResponseDTO + func fetchMain() -> Observable { + let spec = MainViewAPIs.fetchMain.spec + return request(spec) + } + + /// 생존신고 비활성화 시간에 main 화면 데이터를 조회하기 위한 Method입니다. + /// HTTP Method: GET + /// - Returns: MainNightResponseDTO + func fetchMainNight() -> Observable { + let spec = MainViewAPIs.fetchMainNight.spec + return request(spec) + } +} diff --git a/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPIs.swift new file mode 100644 index 000000000..4cd774562 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/MainView/MainViewAPIs.swift @@ -0,0 +1,34 @@ +// +// MainAPIs.swift +// Data +// +// Created by 마경미 on 20.04.24. +// + +import Core + +enum MainViewAPIs: BBAPI { + /// 주간의 메인페이지 조회 + case fetchMain + /// 야간의 메인페이지 조회 + case fetchMainNight + + var spec: Spec { + switch self { + case .fetchMain: + return .init( + method: .get, + path: "/view/main/daytime-page" + ) + case .fetchMainNight: + return .init( + method: .get, + path: "/view/main/nighttime-page" + ) + } + } + + final class Worker: BBRxAPIWorker { + init() { } + } +} diff --git a/14th-team5-iOS/Data/Sources/APIs/MainView/Repository/MainViewRepository.swift b/14th-team5-iOS/Data/Sources/APIs/MainView/Repository/MainViewRepository.swift deleted file mode 100644 index c1b92c31f..000000000 --- a/14th-team5-iOS/Data/Sources/APIs/MainView/Repository/MainViewRepository.swift +++ /dev/null @@ -1,51 +0,0 @@ -// -// MainRepository.swift -// Data -// -// Created by 마경미 on 20.04.24. -// - -import Foundation - -import Domain - -import RxSwift - -public final class MainViewRepository: MainViewRepositoryProtocol { - public let disposeBag: DisposeBag = DisposeBag() - - private let familyUserDefaults: FamilyInfoUserDefaults = FamilyInfoUserDefaults() - private let mainApiWorker: MainAPIWorker = MainAPIWorker() - - public init() { } -} - -extension MainViewRepository { - public func fetchMain() -> Observable { - return mainApiWorker.fetchMain() - .map { $0?.toDomain() } - .do(onSuccess: { [weak self] in - guard - let self, - let profiles = $0?.mainFamilyProfileDatas else { - return - } - self.familyUserDefaults.saveFamilyMembers(profiles) - }) - .asObservable() - } - - public func fetchMainNight() -> Observable { - return mainApiWorker.fetchMainNight() - .map { $0?.toDomain() } - .do(onSuccess: { [weak self] in - guard - let self, - let profiles = $0?.mainFamilyProfileDatas else { - return - } - self.familyUserDefaults.saveFamilyMembers(profiles) - }) - .asObservable() - } -} diff --git a/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/DataMapping/AddFCMTokenRequestDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Me/DataMapping/AddFCMTokenRequestDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/DataMapping/AddFCMTokenRequestDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Me/DataMapping/AddFCMTokenRequestDTO.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/App/AppAPI/DataMapping/AppVersionResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Me/DataMapping/AppVersionResponseDTO.swift similarity index 54% rename from 14th-team5-iOS/Data/Sources/APIs/App/AppAPI/DataMapping/AppVersionResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Me/DataMapping/AppVersionResponseDTO.swift index 5c4a64372..939aa57e5 100644 --- a/14th-team5-iOS/Data/Sources/APIs/App/AppAPI/DataMapping/AppVersionResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Me/DataMapping/AppVersionResponseDTO.swift @@ -8,19 +8,12 @@ import Domain import Foundation -public struct AppVersionResponseDTO: Decodable { - private enum CodingKeys: String, CodingKey { - case appKey - case appVersion - case latest - case inReview - case inService - } - var appKey: String - var appVersion: String - var latest: Bool - var inReview: Bool - var inService: Bool +struct AppVersionResponseDTO: Decodable { + let appKey: String + let appVersion: String + let latest: Bool + let inReview: Bool + let inService: Bool } extension AppVersionResponseDTO { diff --git a/14th-team5-iOS/Data/Sources/APIs/Me/MeAPI.swift b/14th-team5-iOS/Data/Sources/APIs/Me/MeAPI.swift index 3470ba86d..f73c83245 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Me/MeAPI.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Me/MeAPI.swift @@ -10,24 +10,61 @@ import Core // TODO: MeAPIs로 이름 바꾸기 - Trash 지워야 함 enum MeAPI: BBAPI { /// 가족 가입하기 - case joinFamily + case joinFamily(_ body: JoinFamilyRequestDTO) /// 가족 탈퇴하기 case resignFamily /// 가족 생성 및 가족 정보 조회 case createFamily /// 내 가족 정보 조회 case fetchFamilyInfo + /// fcm 토큰 등록 + case registerFCMToken + /// fcm 토큰 삭제 + case deleteFCMToken(_ token: String) + /// 내 접속 버전 조회 + case appVersion(_ appKey: String) var spec: Spec { switch self { - case .joinFamily: - return .init(method: .post, path: "/me/join-family") + case let .joinFamily(body): + return .init( + method: .post, + path: "/me/join-family", + bodyParametersEncodable: body + ) case .resignFamily: - return .init(method: .post, path: "/me/quit-family") + return .init( + method: .post, + path: "/me/quit-family" + ) case .createFamily: - return .init(method: .post, path: "/me/create-family") + return .init( + method: .post, + path: "/me/create-family" + ) case .fetchFamilyInfo: - return .init(method: .get, path: "/me/family-info") + return .init( + method: .get, + path: "/me/family-info" + ) + case .registerFCMToken: + return .init( + method: .post, + path: "/me/fcm" + ) + case let .deleteFCMToken(token): + return .init( + method: .delete, + path: "/me/fcm/\(token)" + ) + case let .appVersion(version): + return .init( + method: .get, + path: "/me/app-version", + queryParameters: [ + "appKey": "\(version)" + ] + ) } } diff --git a/14th-team5-iOS/Data/Sources/APIs/Me/MeWorker.swift b/14th-team5-iOS/Data/Sources/APIs/Me/MeeAPIWorker.swift similarity index 51% rename from 14th-team5-iOS/Data/Sources/APIs/Me/MeWorker.swift rename to 14th-team5-iOS/Data/Sources/APIs/Me/MeeAPIWorker.swift index 49bea8d0f..aef734692 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Me/MeWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Me/MeeAPIWorker.swift @@ -10,8 +10,8 @@ import Core import RxSwift // TODO: MeWorker로 이름 바꾸기 -typealias MeeWorker = MeAPI.Worker -extension MeeWorker { +typealias MeeAPIWorker = MeAPI.Worker +extension MeeAPIWorker { /// 가족방 탈퇴를 위한 Method입니다. /// HTTP Method: Post /// - Returns: DefaultResponseDTO? @@ -37,7 +37,7 @@ extension MeeWorker { func joinFamily( body: JoinFamilyRequestDTO ) -> Observable { - let spec = MeAPI.joinFamily.spec + let spec = MeAPI.joinFamily(body).spec return request(spec) } @@ -50,4 +50,41 @@ extension MeeWorker { return request(spec) } + + /// FCM 토큰을 등록하기 위한 Method입니다. + /// HTTP Method: POST + /// - parameters: body: AddFCMTokenRequestDTO + /// - Returns: DefaultResponseDTO + func registerNewFCMToken( + body: AddFCMTokenRequestDTO + ) -> Observable { + let spec = MeAPI.registerFCMToken.spec + + return request(spec) + } + + + /// 등록된 FCM 토큰을 삭제하기 위한 Method입니다. + /// HTTP Method: Delete + /// - parameters: token: String + /// - Returns: DefaultResponseDTO + func deleteFCMToken( + fcmToken token: String + ) -> Observable { + let spec = MeAPI.deleteFCMToken(token).spec + + return request(spec) + } + + /// 현재 설치된 앱 버전 정보를 조회하기 위한 Method입니다. + /// HTTP Method: Get + /// - parameters: appKey: String + /// - Returns: AppVersionResponseDTO + func fetchAppVersion( + appKey: String + ) -> Observable { + let spec = MeAPI.appVersion(appKey).spec + + return request(spec) + } } diff --git a/14th-team5-iOS/Data/Sources/APIs/MemberRealEmoji/MemberRealEmojiWorker.swift b/14th-team5-iOS/Data/Sources/APIs/MemberRealEmoji/MemberRealEmojiAPIWorker.swift similarity index 83% rename from 14th-team5-iOS/Data/Sources/APIs/MemberRealEmoji/MemberRealEmojiWorker.swift rename to 14th-team5-iOS/Data/Sources/APIs/MemberRealEmoji/MemberRealEmojiAPIWorker.swift index 9bc6b2720..088fade50 100644 --- a/14th-team5-iOS/Data/Sources/APIs/MemberRealEmoji/MemberRealEmojiWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/MemberRealEmoji/MemberRealEmojiAPIWorker.swift @@ -7,8 +7,8 @@ import RxSwift -typealias MemberRealEmojiWorker = MemberRealEmojiAPIs.Worker -extension MemberRealEmojiWorker { +typealias MemberRealEmojiAPIWorker = MemberRealEmojiAPIs.Worker +extension MemberRealEmojiAPIWorker { /// 회원에 등록된 리얼 이모지를 조회하는 Method입니다. /// HTTP Method: GET diff --git a/14th-team5-iOS/Data/Sources/APIs/Members/MembersWorker.swift b/14th-team5-iOS/Data/Sources/APIs/Members/MembersAPIWorker.swift similarity index 98% rename from 14th-team5-iOS/Data/Sources/APIs/Members/MembersWorker.swift rename to 14th-team5-iOS/Data/Sources/APIs/Members/MembersAPIWorker.swift index 4bf4a9cf3..5fef4f0fe 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Members/MembersWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Members/MembersAPIWorker.swift @@ -11,8 +11,8 @@ import Core import RxSwift -typealias MembersWorker = MembersAPIs.Worker -extension MembersWorker { +typealias MembersAPIWorker = MembersAPIs.Worker +extension MembersAPIWorker { /// 회원 프로필 정보를 조회하기 위한 Method 입니다. /// HTTP Method : GET diff --git a/14th-team5-iOS/Data/Sources/APIs/Mission/MissionAPI/DataMapping/MissionContentResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Mission/DataMapping/MissionContentResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/APIs/Mission/MissionAPI/DataMapping/MissionContentResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Mission/DataMapping/MissionContentResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/Mission/MissionAPI/DataMapping/MissonTodayContentResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Mission/DataMapping/MissonTodayContentResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/APIs/Mission/MissionAPI/DataMapping/MissonTodayContentResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Mission/DataMapping/MissonTodayContentResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/Mission/MissionAPI/MissionAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/Mission/MissionAPIWorker.swift similarity index 95% rename from 14th-team5-iOS/Data/Sources/APIs/Mission/MissionAPI/MissionAPIWorker.swift rename to 14th-team5-iOS/Data/Sources/APIs/Mission/MissionAPIWorker.swift index b15c78f6d..ef7d144d0 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Mission/MissionAPI/MissionAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Mission/MissionAPIWorker.swift @@ -20,7 +20,7 @@ extension MissionAPIWorker { /// HTTP Method : GET /// - Parameters : missonid(미션 ID) /// - Returns : MissionContentResponseDTO - func fetchMissonContent(missonId: String) -> Observable { + func fetchMissonContent(missonId: String) -> Observable { let spec = MissionAPIs.fetchMissonContent(missonId: missonId).spec return request(spec) @@ -29,7 +29,7 @@ extension MissionAPIWorker { /// 미션 일일 정보를 조회하기 위한 API 입니다. /// HTTP Method : GET /// - Returns : CameraTodayMissionResponseDTO - func fetchDailyMisson() -> Observable { + func fetchDailyMisson() -> Observable { let spec = MissionAPIs.fetchDailyMisson.spec return request(spec) diff --git a/14th-team5-iOS/Data/Sources/APIs/Mission/MissionAPI/MissionAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/Mission/MissionAPIs.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/APIs/Mission/MissionAPI/MissionAPIs.swift rename to 14th-team5-iOS/Data/Sources/APIs/Mission/MissionAPIs.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/OAuth/AuthAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/OAuth/AuthAPIWorker.swift new file mode 100644 index 000000000..9422c906a --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/OAuth/AuthAPIWorker.swift @@ -0,0 +1,47 @@ +// +// OAuthAPIWorker.swift +// Data +// +// Created by 김건우 on 6/6/24. +// + +import Core +import Domain +import Foundation + +import RxSwift + +typealias AuthAPIWorker = AuthAPIs.Worker +extension AuthAPIWorker { + /// 리프레시 토큰으로 새로운 토큰을 발급받는 Method입니다. + /// HTTP Method: POST + /// - Parameters: body: RefreshAcceessTokenRequestDTO + /// - Returns: AuthResultResponseDTO + func refreshAccessToken(body: RefreshTokenRequestDTO) -> Observable { + let spec = AuthAPIs.refreshToken.spec + return request(spec) + } + + /// 회원가입을 위한 Method입니다 + /// HTTP Method: POST + /// - Parameters: body: SignUpRequestDTO + /// - Returns: SignUpResponseDTO + func registerNewMember(body: SignUpRequestDTO) -> Observable { + let spec = AuthAPIs.registerMember.spec + + // TODO: 헤더 테스트 해보기 + return request(spec) + } + + + /// apple, kakao등 소셜 로그인 Method입니다. + /// HTTP Method: POST + /// - Parameters: type: SignInType, body: NativeSocialLoginRequestDTO + /// - Returns: NativeSocialLoginResponseDTO + func signIn(_ type: SignInType, body: NativeSocialLoginRequestDTO) -> Observable { + let spec = AuthAPIs.signIn(type).spec + + // TODO: 헤더 테스트 해보기 + return request(spec) + } +} diff --git a/14th-team5-iOS/Data/Sources/APIs/OAuth/AuthAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/OAuth/AuthAPIs.swift new file mode 100644 index 000000000..73b5b0b5e --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/OAuth/AuthAPIs.swift @@ -0,0 +1,43 @@ +// +// OAuthAPIs.swift +// Data +// +// Created by 김건우 on 6/6/24. +// + +import Core +import Domain +import Foundation + +enum AuthAPIs: BBAPI { + // 토큰 재발행 + case refreshToken + /// 회원가입 + case registerMember + /// 네이티브 소셜 로그인 + case signIn(SignInType) + + var spec: Spec { + switch self { + case .refreshToken: + return .init( + method: .post, + path: "/auth/refresh" + ) + case .registerMember: + return .init( + method: .post, + path: "/auth/register" + ) + case let .signIn(type): + return .init( + method: .post, + path: "/auth/social/\(type)" + ) + } + } + + final class Worker: BBRxAPIWorker { + init() { } + } +} diff --git a/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/DataMapping/AuthResultResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/OAuth/DataMapping/AuthResultResponseDTO.swift similarity index 50% rename from 14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/DataMapping/AuthResultResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/OAuth/DataMapping/AuthResultResponseDTO.swift index 1d58ec3b8..02a759b4a 100644 --- a/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/DataMapping/AuthResultResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/OAuth/DataMapping/AuthResultResponseDTO.swift @@ -6,20 +6,14 @@ // import Domain -import Foundation -public struct AuthResultResponseDTO: Decodable { - private enum CodingKeys: String, CodingKey { - case accessToken - case refreshToken - case isTemporaryToken - } - var accessToken: String - var refreshToken: String - var isTemporaryToken: Bool +struct NativeSocialLoginResponseDTO: Decodable { + let accessToken: String + let refreshToken: String + let isTemporaryToken: Bool } -extension AuthResultResponseDTO { +extension NativeSocialLoginResponseDTO { public func toDomain() -> AuthResultEntity { return .init( refreshToken: self.refreshToken, diff --git a/14th-team5-iOS/Data/Sources/APIs/OAuth/DataMapping/NativeSocialLoginRequestDTO.swift b/14th-team5-iOS/Data/Sources/APIs/OAuth/DataMapping/NativeSocialLoginRequestDTO.swift new file mode 100644 index 000000000..0f4362d5d --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/OAuth/DataMapping/NativeSocialLoginRequestDTO.swift @@ -0,0 +1,12 @@ +// +// NativeSocialLoginRequestDTO.swift +// Data +// +// Created by 김건우 on 6/6/24. +// + +import Foundation + +struct NativeSocialLoginRequestDTO: Encodable { + let accessToken: String +} diff --git a/14th-team5-iOS/Data/Sources/APIs/OAuth/DataMapping/RefreshTokenRequestDTO.swift b/14th-team5-iOS/Data/Sources/APIs/OAuth/DataMapping/RefreshTokenRequestDTO.swift new file mode 100644 index 000000000..f09355abc --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/OAuth/DataMapping/RefreshTokenRequestDTO.swift @@ -0,0 +1,13 @@ +// +// RefreshAccessTokenRequestDTO.swift +// Data +// +// Created by 김건우 on 6/6/24. +// + +import Domain +import Foundation + +public struct RefreshTokenRequestDTO: Encodable { + let refreshToken: String +} diff --git a/14th-team5-iOS/Data/Sources/APIs/OAuth/DataMapping/RefreshTokenResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/OAuth/DataMapping/RefreshTokenResponseDTO.swift new file mode 100644 index 000000000..83f31d967 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/OAuth/DataMapping/RefreshTokenResponseDTO.swift @@ -0,0 +1,24 @@ +// +// RefreshTokenResponseDTO.swift +// Data +// +// Created by 마경미 on 15.12.24. +// + +import Domain + +struct RefreshTokenResponseDTO: Decodable { + let accessToken: String + let refreshToken: String + let isTemporaryToken: Bool +} + +extension RefreshTokenResponseDTO { + func toDomain() -> AuthResultEntity { + return .init( + refreshToken: self.refreshToken, + accessToken: self.accessToken, + isTemporaryToken: self.isTemporaryToken + ) + } +} diff --git a/14th-team5-iOS/Data/Sources/APIs/OAuth/DataMapping/SignUpRequestDTO.swift b/14th-team5-iOS/Data/Sources/APIs/OAuth/DataMapping/SignUpRequestDTO.swift new file mode 100644 index 000000000..10ee07359 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/OAuth/DataMapping/SignUpRequestDTO.swift @@ -0,0 +1,14 @@ +// +// CreateNewMemberRequestDTO.swift +// Data +// +// Created by 김건우 on 6/6/24. +// + +import Foundation + +struct SignUpRequestDTO: Encodable { + let memberName: String + let dayOfBirth: String + let profileImageUrl: String +} diff --git a/14th-team5-iOS/Data/Sources/APIs/OAuth/DataMapping/SignUpResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/OAuth/DataMapping/SignUpResponseDTO.swift new file mode 100644 index 000000000..c716aab26 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/OAuth/DataMapping/SignUpResponseDTO.swift @@ -0,0 +1,24 @@ +// +// SignUpResponseDTO.swift +// Data +// +// Created by 마경미 on 15.12.24. +// + +import Domain + +struct SignUpResponseDTO: Decodable { + let accessToken: String + let refreshToken: String + let isTemporaryToken: Bool +} + +extension SignUpResponseDTO { + func toDomain() -> AuthResultEntity { + return .init( + refreshToken: self.refreshToken, + accessToken: self.accessToken, + isTemporaryToken: self.isTemporaryToken + ) + } +} diff --git a/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/DataMapping/CreateNewMemberRequestDTO.swift b/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/DataMapping/CreateNewMemberRequestDTO.swift deleted file mode 100644 index 7f85c8f54..000000000 --- a/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/DataMapping/CreateNewMemberRequestDTO.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// CreateNewMemberRequestDTO.swift -// Data -// -// Created by 김건우 on 6/6/24. -// - -import Foundation - -public struct CreateNewMemberRequestDTO: Encodable { - private enum CodingKeys: String, CodingKey { - case memberName - case dayOfBirth - case profileImageUrl - } - var memberName: String - var dayOfBirth: String - var profileImageUrl: String -} diff --git a/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/DataMapping/DefaultResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/DataMapping/DefaultResponseDTO.swift deleted file mode 100644 index a14e6d2b7..000000000 --- a/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/DataMapping/DefaultResponseDTO.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// DefaultResponseDTO.swift -// Data -// -// Created by 김건우 on 6/6/24. -// - -import Domain -import Foundation - -public struct DefaultResponseDTO: Decodable { - private enum CodingKeys: String, CodingKey { - case success - } - var success: Bool -} - -extension DefaultResponseDTO { - func toDomain() -> DefaultEntity { - return .init(success: self.success) - } -} diff --git a/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/DataMapping/NativeSocialLoginRequestDTO.swift b/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/DataMapping/NativeSocialLoginRequestDTO.swift deleted file mode 100644 index 879a86ab8..000000000 --- a/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/DataMapping/NativeSocialLoginRequestDTO.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// NativeSocialLoginRequestDTO.swift -// Data -// -// Created by 김건우 on 6/6/24. -// - -import Foundation - -public struct NativeSocialLoginRequestDTO: Encodable { - private enum CodingKeys: String, CodingKey { - case accessToken - } - var accessToken: String -} diff --git a/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/DataMapping/RefreshAccessTokenRequestDTO.swift b/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/DataMapping/RefreshAccessTokenRequestDTO.swift deleted file mode 100644 index 0ab28a00f..000000000 --- a/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/DataMapping/RefreshAccessTokenRequestDTO.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// RefreshAccessTokenRequestDTO.swift -// Data -// -// Created by 김건우 on 6/6/24. -// - -import Domain -import Foundation - -public struct RefreshAccessTokenRequestDTO: Encodable { - private enum CodingKeys: String, CodingKey { - case refreshToken - } - var refreshToken: String -} diff --git a/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/OAuthAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/OAuthAPIWorker.swift deleted file mode 100644 index 172d1c6a1..000000000 --- a/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/OAuthAPIWorker.swift +++ /dev/null @@ -1,99 +0,0 @@ -// -// OAuthAPIWorker.swift -// Data -// -// Created by 김건우 on 6/6/24. -// - -import Core -import Domain -import Foundation - -import RxSwift - -public typealias OAuthAPIWorker = OAuthAPIs.Worker -extension OAuthAPIs { - final public class Worker: APIWorker { - static let queue = { - ConcurrentDispatchQueueScheduler(queue: DispatchQueue(label: "OAuthAPIQueue", qos: .utility)) - }() - - override init() { - super.init() - self.id = "OAuthAPIWorker" - } - } -} - -// MARK: - Extensions - -extension OAuthAPIWorker { - - // MARK: - Refresh Access Token - - public func refreshAccessToken(body: RefreshAccessTokenRequestDTO) -> Single { - let spec = OAuthAPIs.refreshToken.spec - let headers = BibbiHeader.commonHeaders() // TODO: - Header 없애기 - - return request(spec: spec, headers: headers, jsonEncodable: body) - .subscribe(on: Self.queue) - .map(AuthResultResponseDTO.self) - .catchAndReturn(nil) - .asSingle() - } - - - // MARK: - Register New Member - - public func registerNewMember(body: CreateNewMemberRequestDTO) -> Single { - let spec = OAuthAPIs.registerMember.spec - - return request(spec: spec, jsonEncodable: body) - .subscribe(on: Self.queue) - .map(AuthResultResponseDTO.self) - .catchAndReturn(nil) - .asSingle() - } - - - // MARK: - Sign In With SNS - - public func signIn(_ type: SignInType, body: NativeSocialLoginRequestDTO) -> Single { - let spec = OAuthAPIs.signIn(type).spec - - return request(spec: spec, jsonEncodable: body) - .subscribe(on: Self.queue) - .map(AuthResultResponseDTO.self) - .catchAndReturn(nil) - .asSingle() - } - - - - // MARK: - Register FCM Token - - public func registerNewFCMToken(body: AddFCMTokenRequestDTO) -> Single { - let spec = OAuthAPIs.registerFCMToken.spec - - return request(spec: spec, jsonEncodable: body) - .subscribe(on: Self.queue) - .map(DefaultResponseDTO.self) - .catchAndReturn(nil) - .asSingle() - } - - - // MARK: - Delete FCM Token - - public func deleteFCMToken(fcmToken token: String) -> Single { - let spec = OAuthAPIs.deleteFCMToken(token).spec - - return request(spec: spec) - .subscribe(on: Self.queue) - .map(DefaultResponseDTO.self) - .catchAndReturn(nil) - .asSingle() - } - - -} diff --git a/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/OAuthAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/OAuthAPIs.swift deleted file mode 100644 index abdb96967..000000000 --- a/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/OAuthAPIs.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// OAuthAPIs.swift -// Data -// -// Created by 김건우 on 6/6/24. -// - -import Core -import Domain -import Foundation - -public enum OAuthAPIs: API { - case refreshToken - case registerMember - case signIn(SignInType) - - case registerFCMToken - case deleteFCMToken(String) - - public var spec: APISpec { - switch self { - case .refreshToken: - return APISpec(method: .post, url: "\(BibbiAPI.hostApi)/auth/refresh") - case .registerMember: - return APISpec(method: .post, url: "\(BibbiAPI.hostApi)/auth/register") - case let .signIn(type): - return APISpec(method: .post, url: "\(BibbiAPI.hostApi)/auth/social/\(type.rawValue)") - - case .registerFCMToken: - return APISpec(method: .post, url: "\(BibbiAPI.hostApi)/me/fcm") - case let .deleteFCMToken(token): - return APISpec(method: .delete, url: "\(BibbiAPI.hostApi)/me/fcm/\(token)") - } - } -} 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 deleted file mode 100644 index 98a960184..000000000 --- a/14th-team5-iOS/Data/Sources/APIs/OAuth/OAuthAPI/Repository/OAuthRepository.swift +++ /dev/null @@ -1,133 +0,0 @@ -// -// OAuthRepository.swift -// Data -// -// Created by 김건우 on 6/6/24. -// - -import Core -import Domain - -import RxSwift -import RxCocoa - -public final class OAuthRepository: OAuthRepositoryProtocol { - - // MARK: - Properties - public var disposeBag = DisposeBag() - - public var oAuthApiWorker = OAuthAPIWorker() - public var tokenKeychainStorage = TokenKeychain() - - // MARK: - Intializer - public init() { } - -} - -extension OAuthRepository { - - // MARK: - Refresh Access Token - - public func refreshAccessToken(body: RefreshAccessTokenRequest) -> Observable { - let body = RefreshAccessTokenRequestDTO( - refreshToken: body.refreshToken - ) - return oAuthApiWorker.refreshAccessToken(body: body) - .observe(on: RxScheduler.main) - .map { $0?.toDomain() } - .do(onSuccess: { [weak self] in - guard - let keychain = self?.tokenKeychainStorage - else { return } - let accessToken = AccessToken( - accessToken: $0?.accessToken, - refreshToken: $0?.refreshToken, - isTemporaryToken: $0?.isTemporaryToken - ) - keychain.saveAccessToken(accessToken) - }) - .asObservable() - } - - - // MARK: - Register New Member - - public func registerNewMember(body: CreateNewMemberRequest) -> Observable { - let body = CreateNewMemberRequestDTO( - memberName: body.memberName, - dayOfBirth: body.dayOfBirth, - profileImageUrl: body.profileImageUrl - ) - return oAuthApiWorker.registerNewMember(body: body) - .observe(on: RxScheduler.main) - .map { $0?.toDomain() } - .do(onSuccess: { [weak self] in - guard - let keychain = self?.tokenKeychainStorage - else { return } - let accessToken = AccessToken( - accessToken: $0?.accessToken, - refreshToken: $0?.refreshToken, - isTemporaryToken: $0?.isTemporaryToken - ) - keychain.saveAccessToken(accessToken) - }) - .asObservable() - } - - - // MARK: - Sign In With SNS - - public func signIn(_ type: SignInType, body: NativeSocialLoginRequest) -> Observable { - let body = NativeSocialLoginRequestDTO( - accessToken: body.accessToken - ) - return oAuthApiWorker.signIn(type, body: body) - .observe(on: RxScheduler.main) - .map { $0?.toDomain() } - .do(onSuccess: { [weak self] in - guard - let keychain = self?.tokenKeychainStorage - else { return } - let accessToken = AccessToken( - accessToken: $0?.accessToken, - refreshToken: $0?.refreshToken, - isTemporaryToken: $0?.isTemporaryToken - ) - keychain.saveAccessToken(accessToken) - }) - .asObservable() - } - - - // MARK: - Register FCM Token - - public func registerNewFCMToken(body: AddFCMTokenRequest) -> Observable { - let body = AddFCMTokenRequestDTO( - fcmToken: body.fcmToken - ) - return oAuthApiWorker.registerNewFCMToken(body: body) - .observe(on: RxScheduler.main) - .map { $0?.toDomain() } - .asObservable() - } - - - // MARK: - Delete FCM Token - - public func deleteFCMToken() -> Observable { - - guard - let fcmToken = tokenKeychainStorage.loadFCMToken() - else { - return Observable.just(DefaultEntity(success: false)) - } - - return oAuthApiWorker.deleteFCMToken(fcmToken: fcmToken) - .observe(on: RxScheduler.main) - .map { $0?.toDomain() } - .asObservable() - - } - -} diff --git a/14th-team5-iOS/Data/Sources/APIs/Post/PostAPI/DataMapping/CreatePostPresignedURLReqeustDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Post/DataMapping/CreatePostPresignedURLReqeustDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/APIs/Post/PostAPI/DataMapping/CreatePostPresignedURLReqeustDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Post/DataMapping/CreatePostPresignedURLReqeustDTO.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/Post/PostAPI/DataMapping/CreatePostPresignedURLResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Post/DataMapping/CreatePostPresignedURLResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/APIs/Post/PostAPI/DataMapping/CreatePostPresignedURLResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Post/DataMapping/CreatePostPresignedURLResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/Post/PostAPI/DataMapping/CreatePostRequestDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Post/DataMapping/CreatePostRequestDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/APIs/Post/PostAPI/DataMapping/CreatePostRequestDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Post/DataMapping/CreatePostRequestDTO.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/Post/PostAPI/DataMapping/CreatePostResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Post/DataMapping/CreatePostResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/APIs/Post/PostAPI/DataMapping/CreatePostResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Post/DataMapping/CreatePostResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/Post/PostAPI/DataMapping/CreatePresignedURLRequestDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Post/DataMapping/CreatePresignedURLRequestDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/APIs/Post/PostAPI/DataMapping/CreatePresignedURLRequestDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Post/DataMapping/CreatePresignedURLRequestDTO.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/Post/PostAPI/DataMapping/PostDetailResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Post/DataMapping/PostDetailResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/APIs/Post/PostAPI/DataMapping/PostDetailResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Post/DataMapping/PostDetailResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/Post/PostAPI/DataMapping/PostListQueryDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Post/DataMapping/PostListQueryDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/APIs/Post/PostAPI/DataMapping/PostListQueryDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Post/DataMapping/PostListQueryDTO.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/Post/PostAPI/DataMapping/PostListResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Post/DataMapping/PostListResponseDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/APIs/Post/PostAPI/DataMapping/PostListResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Post/DataMapping/PostListResponseDTO.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/Post/PostAPI/PostAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/Post/PostAPIs.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/APIs/Post/PostAPI/PostAPIs.swift rename to 14th-team5-iOS/Data/Sources/APIs/Post/PostAPIs.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/Post/PostAPI/PostListAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/Post/PostListAPIWorker.swift similarity index 99% rename from 14th-team5-iOS/Data/Sources/APIs/Post/PostAPI/PostListAPIWorker.swift rename to 14th-team5-iOS/Data/Sources/APIs/Post/PostListAPIWorker.swift index 5b8e38b19..85908962b 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Post/PostAPI/PostListAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Post/PostListAPIWorker.swift @@ -16,8 +16,6 @@ import RxSwift typealias PostAPIWorker = PostsAPIs.Worker - - extension PostAPIWorker { /// 게시물 전체 조회하기 위한 Method 입니다. diff --git a/14th-team5-iOS/Data/Sources/APIs/Widget/WidgetAPI/DataMaaping/RecentFamilyPostResponseDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Widget/DataMaaping/GetRecentPostsResponseDTO.swift similarity index 85% rename from 14th-team5-iOS/Data/Sources/APIs/Widget/WidgetAPI/DataMaaping/RecentFamilyPostResponseDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Widget/DataMaaping/GetRecentPostsResponseDTO.swift index cbd8c5076..0f5ad1201 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Widget/WidgetAPI/DataMaaping/RecentFamilyPostResponseDTO.swift +++ b/14th-team5-iOS/Data/Sources/APIs/Widget/DataMaaping/GetRecentPostsResponseDTO.swift @@ -9,14 +9,14 @@ import Foundation import Domain -struct RecentFamilyPostResponseDTO: Codable { +struct GetRecentPostsResponseDTO: Codable { let authorName: String let authorProfileImageUrl: String? let postId: String? let postImageUrl: String? let postContent: String? - func toDomain() -> RecentFamilyPostEntity { + func toDomain() -> WidgetPostEntity { return .init(authorName: authorName, authorProfileImageUrl: authorProfileImageUrl, postId: postId, diff --git a/14th-team5-iOS/Data/Sources/APIs/Widget/WidgetAPI/DataMaaping/RecentFamilyPostRequestDTO.swift b/14th-team5-iOS/Data/Sources/APIs/Widget/DataMaaping/RecentFamilyPostRequestDTO.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/APIs/Widget/WidgetAPI/DataMaaping/RecentFamilyPostRequestDTO.swift rename to 14th-team5-iOS/Data/Sources/APIs/Widget/DataMaaping/RecentFamilyPostRequestDTO.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/Widget/WidgetAPI/WidgetAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/Widget/WidgetAPI/WidgetAPIWorker.swift deleted file mode 100644 index bdec40477..000000000 --- a/14th-team5-iOS/Data/Sources/APIs/Widget/WidgetAPI/WidgetAPIWorker.swift +++ /dev/null @@ -1,51 +0,0 @@ -// -// WidgetAPIWorker.swift -// Data -// -// Created by 마경미 on 05.06.24. -// - -import Foundation - -import Core -import Domain - -import RxSwift - -typealias WidgetAPIWorker = WidgetAPIs.Worker -extension WidgetAPIs { - public final class Worker: APIWorker { - static let queue = { - ConcurrentDispatchQueueScheduler(queue: DispatchQueue(label: "WidgetAPIQueue", qos: .utility)) - }() - - public override init() { - super.init() - self.id = "WidgetAPIWorker" - } - - var headers: [APIHeader] { - guard let token = App.Repository.token.keychain.string(forKey: "accessToken") else { - return [] - } - - let headers = [BibbiAPI.Header.xAppKey, BibbiAPI.Header.xAuthToken(token), BibbiAPI.Header.acceptJson] - return headers - } - } -} - -extension WidgetAPIWorker { - func fetchRecentFamilyPost() -> Observable { - - let spec = WidgetAPIs.fetchRecentFamilyPost.spec - let parameters = RecentFamilyPostParameter() - - return request(spec: spec, headers: headers, parameters: parameters) - .subscribe(on: Self.queue) - .map(RecentFamilyPostResponseDTO.self) - .catchAndReturn(nil) - .map { $0?.toDomain() } - .asObservable() - } -} diff --git a/14th-team5-iOS/Data/Sources/APIs/Widget/WidgetAPI/WidgetAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/Widget/WidgetAPI/WidgetAPIs.swift deleted file mode 100644 index ae13870c9..000000000 --- a/14th-team5-iOS/Data/Sources/APIs/Widget/WidgetAPI/WidgetAPIs.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// WidgetAPIs.swift -// Data -// -// Created by 마경미 on 05.06.24. -// - -import Foundation - -import Core - -enum WidgetAPIs: API { - case fetchRecentFamilyPost - - public var spec: APISpec { - switch self { - case .fetchRecentFamilyPost: - let urlString = "\(BibbiAPI.hostApi)/widgets/single-recent-family-post" - return APISpec(method: .get, url: urlString) - } - } -} diff --git a/14th-team5-iOS/Data/Sources/APIs/Widget/WidgetAPIWorker.swift b/14th-team5-iOS/Data/Sources/APIs/Widget/WidgetAPIWorker.swift new file mode 100644 index 000000000..9bf59470c --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/Widget/WidgetAPIWorker.swift @@ -0,0 +1,20 @@ +// +// WidgetAPIWorker.swift +// Data +// +// Created by 마경미 on 05.06.24. +// + +import RxSwift + +typealias WidgetAPIWorker = WidgetAPIs.Worker +extension WidgetAPIWorker { + /// 가장 최근에 올라온 가족의 게시글 하나를 조회하는 Method입니다. + /// HTTP Method: GET + /// - Returns: GetRecentPostsResponseDTO + func fetchRecentFamilyPost() -> Observable { + let spec = WidgetAPIs.fetchRecentFamilyPost.spec + + return request(spec) + } +} diff --git a/14th-team5-iOS/Data/Sources/APIs/Widget/WidgetAPIs.swift b/14th-team5-iOS/Data/Sources/APIs/Widget/WidgetAPIs.swift new file mode 100644 index 000000000..210da2320 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/APIs/Widget/WidgetAPIs.swift @@ -0,0 +1,27 @@ +// +// WidgetAPIs.swift +// Data +// +// Created by 마경미 on 05.06.24. +// + +import Core + +enum WidgetAPIs: BBAPI { + /// 당일 최근 가족 게시물 타입 위젯 조회 + case fetchRecentFamilyPost + + var spec: Spec { + switch self { + case .fetchRecentFamilyPost: + return .init( + method: .get, + path: "/widgets/single-recent-family-post" + ) + } + } + + final class Worker: BBRxAPIWorker { + init() { } + } +} diff --git a/14th-team5-iOS/Data/Sources/APIs/App/Repository/AppRepository.swift b/14th-team5-iOS/Data/Sources/Repositories/AppRepository.swift similarity index 85% rename from 14th-team5-iOS/Data/Sources/APIs/App/Repository/AppRepository.swift rename to 14th-team5-iOS/Data/Sources/Repositories/AppRepository.swift index 2708349de..c8dbfd030 100644 --- a/14th-team5-iOS/Data/Sources/APIs/App/Repository/AppRepository.swift +++ b/14th-team5-iOS/Data/Sources/Repositories/AppRepository.swift @@ -13,10 +13,10 @@ import RxSwift public final class AppRepository: AppRepositoryProtocol { // MARK: - Properties - public let disposeBag = DisposeBag() + private let disposeBag = DisposeBag() // MARK: - APIWorker - private let appApiWorker = AppAPIWorker() + private let meAPIWorker = MeeAPIWorker() // MARK: - UserDefaults private let appUserDefaults = AppUserDefaults() @@ -31,13 +31,12 @@ public final class AppRepository: AppRepositoryProtocol { extension AppRepository { - public func fetchAppVersion() -> Observable { + public func fetchAppVersion() -> Observable { // TODO: - xAppKey 불러오는 코드 다시 작성하기 let appKey = "7b159d28-b106-4b6d-a490-1fd654ce40c2" - return appApiWorker.fetchAppVersion(appKey: appKey) - .map { $0?.toDomain() } - .asObservable() + return meAPIWorker.fetchAppVersion(appKey: appKey) + .map { $0.toDomain() } } public func loadIsFirstFamilyManagement() -> RxSwift.Observable { diff --git a/14th-team5-iOS/Data/Sources/Repositories/AuthRepository.swift b/14th-team5-iOS/Data/Sources/Repositories/AuthRepository.swift new file mode 100644 index 000000000..f9cef0338 --- /dev/null +++ b/14th-team5-iOS/Data/Sources/Repositories/AuthRepository.swift @@ -0,0 +1,135 @@ +// +// OAuthRepository.swift +// Data +// +// Created by 김건우 on 6/6/24. +// + +import Core +import Domain +import Foundation + +import RxSwift +import RxCocoa + +public final class AuthRepository: OAuthRepositoryProtocol { + + // MARK: - Properties + private let disposeBag = DisposeBag() + + private let meAPIWorker = MeeAPIWorker() + private let authAPIWorker = AuthAPIWorker() + private let tokenKeychainStorage = TokenKeychain() + + // MARK: - Intializer + public init() { } +} + +extension AuthRepository { + + // MARK: - Refresh Access Token + + public func refreshAccessToken( + body: RefreshAccessTokenRequest + ) -> Observable { + let requestBody = RefreshTokenRequestDTO( + refreshToken: body.refreshToken + ) + + return authAPIWorker.refreshAccessToken(body: requestBody) + .flatMap { [weak self] response -> Observable in + guard let self = self else { + return Observable.error(NSError(domain: "RefreshError", code: -1, userInfo: nil)) + } + + let accessToken = AuthToken( + accessToken: response.accessToken, + refreshToken: response.refreshToken, + isTemporaryToken: response.isTemporaryToken + ) + + self.tokenKeychainStorage.saveAuthToken(accessToken) + return Observable.just(response.toDomain()) + } + } + + + // MARK: - Register New Member + + public func registerNewMember( + body: CreateNewMemberRequest + ) -> Observable { + let body = SignUpRequestDTO( + memberName: body.memberName, + dayOfBirth: body.dayOfBirth, + profileImageUrl: body.profileImageUrl + ) + + return authAPIWorker.registerNewMember(body: body) + .flatMap { [weak self] response -> Observable in + guard let keychain = self?.tokenKeychainStorage else { + return Observable.error(NSError(domain: "RegisterError", code: -1, userInfo: nil)) + } + let accessToken = AuthToken( + accessToken: response.accessToken, + refreshToken: response.refreshToken, + isTemporaryToken: response.isTemporaryToken + ) + keychain.saveAuthToken(accessToken) + + return Observable.just(response.toDomain()) + } + } + + + // MARK: - Sign In With SNS + + public func signIn( + _ type: SignInType, + body: NativeSocialLoginRequest + ) -> Observable { + let body = NativeSocialLoginRequestDTO( + accessToken: body.accessToken + ) + return authAPIWorker.signIn(type, body: body) + .flatMap { [weak self] response -> Observable in + guard let keychain = self?.tokenKeychainStorage else { + return Observable.error(NSError(domain: "SignInError", code: -1, userInfo: nil)) + } + let accessToken = AuthToken( + accessToken: response.accessToken, + refreshToken: response.refreshToken, + isTemporaryToken: response.isTemporaryToken + ) + keychain.saveAuthToken(accessToken) + + return Observable.just(response.toDomain()) + } + } + + + // MARK: - Register FCM Token + public func registerNewFCMToken( + body: AddFCMTokenRequest + ) -> Observable { + let body = AddFCMTokenRequestDTO( + fcmToken: body.fcmToken + ) + + return meAPIWorker.registerNewFCMToken(body: body) + .map { $0.toDomain() } + } + + + public func deleteFCMToken() -> Observable { + + guard + let fcmToken = tokenKeychainStorage.loadFCMToken() + else { + return Observable.just(DefaultEntity(success: false)) + } + + return meAPIWorker.deleteFCMToken(fcmToken: fcmToken) + .map { $0.toDomain() } + } +} diff --git a/14th-team5-iOS/Data/Sources/APIs/Calendar/Repository/CalendarRepository.swift b/14th-team5-iOS/Data/Sources/Repositories/CalendarRepository.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/APIs/Calendar/Repository/CalendarRepository.swift rename to 14th-team5-iOS/Data/Sources/Repositories/CalendarRepository.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/Camera/Repository/CameraRepository.swift b/14th-team5-iOS/Data/Sources/Repositories/CameraRepository.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/APIs/Camera/Repository/CameraRepository.swift rename to 14th-team5-iOS/Data/Sources/Repositories/CameraRepository.swift diff --git a/14th-team5-iOS/Data/Sources/APIs/Comment/Repository/CommentRepository.swift b/14th-team5-iOS/Data/Sources/Repositories/CommentRepository.swift similarity index 96% rename from 14th-team5-iOS/Data/Sources/APIs/Comment/Repository/CommentRepository.swift rename to 14th-team5-iOS/Data/Sources/Repositories/CommentRepository.swift index 26fb6a41c..458bf6557 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Comment/Repository/CommentRepository.swift +++ b/14th-team5-iOS/Data/Sources/Repositories/CommentRepository.swift @@ -11,7 +11,7 @@ import Foundation import RxSwift public final class CommentRepository: CommentRepositoryProtocol { - public let disposeBag: DisposeBag = DisposeBag() + private let disposeBag: DisposeBag = DisposeBag() private let commentApiWorker: CommentAPIWorker = CommentAPIWorker() diff --git a/14th-team5-iOS/Data/Sources/Repositories/FamilyRepository.swift b/14th-team5-iOS/Data/Sources/Repositories/FamilyRepository.swift index 55c3da342..06d4b8c4b 100644 --- a/14th-team5-iOS/Data/Sources/Repositories/FamilyRepository.swift +++ b/14th-team5-iOS/Data/Sources/Repositories/FamilyRepository.swift @@ -13,13 +13,13 @@ import RxSwift public final class FamilyRepository: FamilyRepositoryProtocol { // MARK: - Properties - public let disposeBag: DisposeBag = DisposeBag() + private let disposeBag: DisposeBag = DisposeBag() - private let linkWorker: LinkWorker = LinkWorker() - private let meWorker: MeeWorker = MeeWorker() - private let membersWorker: MembersWorker = MembersWorker() - private let familyInviteViewWorker: FamilyInviteViewWorker = FamilyInviteViewWorker() - private let familyWorker: FamilyWorker = FamilyWorker() + private let linkWorker: LinkAPIWorker = LinkAPIWorker() + private let meWorker: MeeAPIWorker = MeeAPIWorker() + private let membersWorker: MembersAPIWorker = MembersAPIWorker() + private let familyInviteViewWorker: FamilyInviteViewAPIWorker = FamilyInviteViewAPIWorker() + private let familyWorker: FamilyAPIWorker = FamilyAPIWorker() private let familyUserDefaults: FamilyInfoUserDefaultsType = FamilyInfoUserDefaults() // MARK: - Intializer diff --git a/14th-team5-iOS/Data/Sources/Repositories/MainViewRepository.swift b/14th-team5-iOS/Data/Sources/Repositories/MainViewRepository.swift new file mode 100644 index 000000000..e2d0291ac --- /dev/null +++ b/14th-team5-iOS/Data/Sources/Repositories/MainViewRepository.swift @@ -0,0 +1,53 @@ +// +// MainRepository.swift +// Data +// +// Created by 마경미 on 20.04.24. +// + +import Foundation + +import Domain + +import RxSwift + +public final class MainViewRepository: MainViewRepositoryProtocol { + public let disposeBag: DisposeBag = DisposeBag() + + private let familyUserDefaults: FamilyInfoUserDefaults = FamilyInfoUserDefaults() + private let mainApiWorker: MainAPIWorker = MainAPIWorker() + + public init() { } +} + +extension MainViewRepository { + public func fetchMain() -> Observable { + return mainApiWorker.fetchMain() + .flatMap { [weak self] response -> Observable in + guard let self else { + return Observable.error( + NSError(domain: "MainError", code: -1, userInfo: nil) + ) + } + let entities = response.toDomain() + let profiles = entities.mainFamilyProfileDatas + self.familyUserDefaults.saveFamilyMembers(profiles) + return Observable.just(entities) + } + } + + public func fetchMainNight() -> Observable { + return mainApiWorker.fetchMainNight() + .flatMap { [weak self] response -> Observable in + guard let self else { + return Observable.error( + NSError(domain: "MainError", code: -1, userInfo: nil) + ) + } + let entities = response.toDomain() + let profiles = entities.mainFamilyProfileDatas + self.familyUserDefaults.saveFamilyMembers(profiles) + return Observable.just(entities) + } + } +} diff --git a/14th-team5-iOS/Data/Sources/Repositories/MembersRepository.swift b/14th-team5-iOS/Data/Sources/Repositories/MembersRepository.swift index 78b0c8b43..a3850a06a 100644 --- a/14th-team5-iOS/Data/Sources/Repositories/MembersRepository.swift +++ b/14th-team5-iOS/Data/Sources/Repositories/MembersRepository.swift @@ -15,10 +15,10 @@ import RxCocoa public final class MembersRepository { - public var disposeBag: DisposeBag = DisposeBag() + public let disposeBag: DisposeBag = DisposeBag() private let familyUserDefaults: FamilyInfoUserDefaults = FamilyInfoUserDefaults() - private let membersAPIWorker: MembersWorker = MembersWorker() + private let membersAPIWorker: MembersAPIWorker = MembersAPIWorker() public init() { } } diff --git a/14th-team5-iOS/Data/Sources/APIs/Mission/Repository/MissionRepository.swift b/14th-team5-iOS/Data/Sources/Repositories/MissionRepository.swift similarity index 93% rename from 14th-team5-iOS/Data/Sources/APIs/Mission/Repository/MissionRepository.swift rename to 14th-team5-iOS/Data/Sources/Repositories/MissionRepository.swift index 6adbc53eb..9701c234f 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Mission/Repository/MissionRepository.swift +++ b/14th-team5-iOS/Data/Sources/Repositories/MissionRepository.swift @@ -24,14 +24,14 @@ public final class MissionRepository: MissionRepositoryProtocol { extension MissionRepository { - public func fetchMissonContentItem(missonId: String) -> Observable { + public func fetchMissonContentItem(missonId: String) -> Observable { return missionAPIWorker.fetchMissonContent(missonId: missonId) - .map { $0?.toDomain() } + .map { $0.toDomain() } } - public func fetchDailyMissonItem() -> Observable { + public func fetchDailyMissonItem() -> Observable { return missionAPIWorker.fetchDailyMisson() - .map { $0?.toDomain() } + .map { $0.toDomain() } } public func isAlreadyShowMissionAlert() -> Observable { diff --git a/14th-team5-iOS/Data/Sources/APIs/My/Repository/MyRepository.swift b/14th-team5-iOS/Data/Sources/Repositories/MyRepository.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/APIs/My/Repository/MyRepository.swift rename to 14th-team5-iOS/Data/Sources/Repositories/MyRepository.swift diff --git a/14th-team5-iOS/Data/Sources/Repositories/PickRepository.swift b/14th-team5-iOS/Data/Sources/Repositories/PickRepository.swift index 7f712f7f9..e0f3f1ebe 100644 --- a/14th-team5-iOS/Data/Sources/Repositories/PickRepository.swift +++ b/14th-team5-iOS/Data/Sources/Repositories/PickRepository.swift @@ -12,8 +12,8 @@ import RxSwift public final class PickRepository { - public let disposeBag: DisposeBag = DisposeBag() - private let membersAPIWorker: MembersWorker = MembersWorker() + private let disposeBag: DisposeBag = DisposeBag() + private let membersAPIWorker: MembersAPIWorker = MembersAPIWorker() public init() { } } diff --git a/14th-team5-iOS/Data/Sources/APIs/Post/Repository/PostRepository.swift b/14th-team5-iOS/Data/Sources/Repositories/PostRepository.swift similarity index 57% rename from 14th-team5-iOS/Data/Sources/APIs/Post/Repository/PostRepository.swift rename to 14th-team5-iOS/Data/Sources/Repositories/PostRepository.swift index 290ed5884..8b7cccbab 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Post/Repository/PostRepository.swift +++ b/14th-team5-iOS/Data/Sources/Repositories/PostRepository.swift @@ -20,7 +20,9 @@ public final class PostRepository: PostListRepositoryProtocol { extension PostRepository { - public func fetchPostList(query: PostListQuery) -> Observable { + public func fetchPostList( + query: PostListQuery + ) -> Observable { let query = PostListQueryDTO( page: query.page, size: query.size, @@ -34,25 +36,41 @@ extension PostRepository { .map { $0?.toDomain() } } - public func fetchPostDetailItem(postId: String) -> Observable { + public func fetchPostDetailItem( + postId: String + ) -> Observable { return postAPIWorker.fetchPostDetail(postId: postId) .map { $0?.toDomain() } } - public func createPostItem(query: CreatePostQuery, body: CreatePostRequest) -> Observable { - let body = CreatePostRequestDTO(imageUrl: body.imageUrl, content: body.content, uploadTime: body.uploadTime) + public func createPostItem( + query: CreatePostQuery, + body: CreatePostRequest + ) -> Observable { + let body = CreatePostRequestDTO( + imageUrl: body.imageUrl, + content: body.content, + uploadTime: body.uploadTime + ) return postAPIWorker.createPost(query: query, body: body) .map { $0?.toDomain() } } - public func createPostPresignedURLItem(body: CreatePostPresignedURLRequest) -> Observable { - let body = CreatePostPresignedURLReqeustDTO(imageName: body.imageName) + public func createPostPresignedURLItem( + body: CreatePostPresignedURLRequest + ) -> Observable { + let body = CreatePostPresignedURLReqeustDTO( + imageName: body.imageName + ) return postAPIWorker.createPostPresignedURL(body: body) .map { $0?.toDomain() } } - public func uploadPostImageToS3Bucket(_ presignedURL: String, image: Data) -> Observable { + public func uploadPostImageToS3Bucket( + _ presignedURL: String, + image: Data + ) -> Observable { return postAPIWorker.updateS3PostImageUpload(presignedURL, image: image) } } diff --git a/14th-team5-iOS/Data/Sources/APIs/Privacy/Repository/PrivacyRepository.swift b/14th-team5-iOS/Data/Sources/Repositories/PrivacyRepository.swift similarity index 100% rename from 14th-team5-iOS/Data/Sources/APIs/Privacy/Repository/PrivacyRepository.swift rename to 14th-team5-iOS/Data/Sources/Repositories/PrivacyRepository.swift diff --git a/14th-team5-iOS/Data/Sources/Repositories/RealEmojiRepository.swift b/14th-team5-iOS/Data/Sources/Repositories/RealEmojiRepository.swift index b33a366e0..b812c7898 100644 --- a/14th-team5-iOS/Data/Sources/Repositories/RealEmojiRepository.swift +++ b/14th-team5-iOS/Data/Sources/Repositories/RealEmojiRepository.swift @@ -17,7 +17,7 @@ public final class RealEmojiRepository: RealEmojiRepositoryProtocol { private let myUserDefaults: MyUserDefaults = .init() private let postRealEmojiWorker: PostRealEmojiWorker = PostRealEmojiWorker() - private let memberRealEmojiWorker: MemberRealEmojiWorker = MemberRealEmojiWorker() + private let memberRealEmojiWorker: MemberRealEmojiAPIWorker = MemberRealEmojiAPIWorker() public init () { } diff --git a/14th-team5-iOS/Data/Sources/Repositories/ResignRepository.swift b/14th-team5-iOS/Data/Sources/Repositories/ResignRepository.swift index 54b7c0dff..fe7f26d94 100644 --- a/14th-team5-iOS/Data/Sources/Repositories/ResignRepository.swift +++ b/14th-team5-iOS/Data/Sources/Repositories/ResignRepository.swift @@ -12,7 +12,7 @@ import RxSwift public final class ResignRepository { private let disposeBag: DisposeBag = DisposeBag() - private let membersAPIWorker: MembersWorker = MembersWorker() + private let membersAPIWorker: MembersAPIWorker = MembersAPIWorker() public init() { } } diff --git a/14th-team5-iOS/Data/Sources/APIs/Widget/Repository/WidgetRepository.swift b/14th-team5-iOS/Data/Sources/Repositories/WidgetRepository.swift similarity index 84% rename from 14th-team5-iOS/Data/Sources/APIs/Widget/Repository/WidgetRepository.swift rename to 14th-team5-iOS/Data/Sources/Repositories/WidgetRepository.swift index 75dd5ebce..2dd65ca99 100644 --- a/14th-team5-iOS/Data/Sources/APIs/Widget/Repository/WidgetRepository.swift +++ b/14th-team5-iOS/Data/Sources/Repositories/WidgetRepository.swift @@ -13,13 +13,14 @@ import RxSwift public final class WidgetRepository: WidgetRepositoryProtocol { - public let disposeBag: DisposeBag = DisposeBag() + private let disposeBag: DisposeBag = DisposeBag() private let widgetAPIWorker: WidgetAPIWorker = WidgetAPIWorker() public init () { } - public func fetchRecentFamilyPost(completion: @escaping (Result) -> Void) { + public func fetchRecentFamilyPost(completion: @escaping (Result) -> Void) { widgetAPIWorker.fetchRecentFamilyPost() + .map { $0.toDomain() } .subscribe( onNext: { result in completion(.success(result)) diff --git a/14th-team5-iOS/Data/Sources/Trash/Account/AccountRepository/AccountRepository.swift b/14th-team5-iOS/Data/Sources/Trash/Account/AccountRepository/AccountRepository.swift index 37a8bed5f..3fbf36d3d 100644 --- a/14th-team5-iOS/Data/Sources/Trash/Account/AccountRepository/AccountRepository.swift +++ b/14th-team5-iOS/Data/Sources/Trash/Account/AccountRepository/AccountRepository.swift @@ -34,7 +34,7 @@ public final class AccountRepository: AccountImpl { let signInHelper = AccountSignInHelper() private let apiWorker = AccountAPIWorker() - private let profileWorker = MembersWorker() + private let profileWorker = MembersAPIWorker() private let meApiWorekr = MeAPIWorker() private let fetchMemberInfo = PublishRelay() diff --git a/14th-team5-iOS/Data/Sources/Trash/Account/MeAPI/MeAPIWorker.swift b/14th-team5-iOS/Data/Sources/Trash/Account/MeAPI/MeAPIWorker.swift index 5afa38b23..49c5085d6 100644 --- a/14th-team5-iOS/Data/Sources/Trash/Account/MeAPI/MeAPIWorker.swift +++ b/14th-team5-iOS/Data/Sources/Trash/Account/MeAPI/MeAPIWorker.swift @@ -31,6 +31,7 @@ extension MeAPIs { } extension MeAPIWorker: MeRepositoryProtocol, JoinFamilyRepository, FCMRepositoryProtocol { + private func saveFcmToken(headers: [APIHeader]?, token: FCMToken) -> Single { let spec = MeAPIs.saveFcmToken.spec diff --git a/14th-team5-iOS/Domain/Sources/Entities/Widget/RecentFamilyPostEntity.swift b/14th-team5-iOS/Domain/Sources/Entities/Widget/WidgetPostEntity.swift similarity index 93% rename from 14th-team5-iOS/Domain/Sources/Entities/Widget/RecentFamilyPostEntity.swift rename to 14th-team5-iOS/Domain/Sources/Entities/Widget/WidgetPostEntity.swift index 41ad38079..33afacc66 100644 --- a/14th-team5-iOS/Domain/Sources/Entities/Widget/RecentFamilyPostEntity.swift +++ b/14th-team5-iOS/Domain/Sources/Entities/Widget/WidgetPostEntity.swift @@ -7,7 +7,7 @@ import Foundation -public struct RecentFamilyPostEntity: Codable { +public struct WidgetPostEntity: Codable { public var authorName: String public var authorProfileImageUrl: String? public var postId: String? diff --git a/14th-team5-iOS/Domain/Sources/Repositories/AppRepository.swift b/14th-team5-iOS/Domain/Sources/Repositories/AppRepository.swift index 85d11b1f4..97b1526dd 100644 --- a/14th-team5-iOS/Domain/Sources/Repositories/AppRepository.swift +++ b/14th-team5-iOS/Domain/Sources/Repositories/AppRepository.swift @@ -10,7 +10,7 @@ import Foundation import RxSwift public protocol AppRepositoryProtocol { - func fetchAppVersion() -> Observable + func fetchAppVersion() -> Observable func loadIsFirstFamilyManagement() -> Observable func saveIsFirstFamilyManagement(isFirst: Bool) -> Void diff --git a/14th-team5-iOS/Domain/Sources/Repositories/MainViewRepository.swift b/14th-team5-iOS/Domain/Sources/Repositories/MainViewRepository.swift index 06f5a37bd..29f042a35 100644 --- a/14th-team5-iOS/Domain/Sources/Repositories/MainViewRepository.swift +++ b/14th-team5-iOS/Domain/Sources/Repositories/MainViewRepository.swift @@ -10,6 +10,6 @@ import Foundation import RxSwift public protocol MainViewRepositoryProtocol { - func fetchMain() -> Observable - func fetchMainNight() -> Observable + func fetchMain() -> Observable + func fetchMainNight() -> Observable } diff --git a/14th-team5-iOS/Domain/Sources/Repositories/MissonRepository.swift b/14th-team5-iOS/Domain/Sources/Repositories/MissonRepository.swift index a62ce0e31..0c0202a56 100644 --- a/14th-team5-iOS/Domain/Sources/Repositories/MissonRepository.swift +++ b/14th-team5-iOS/Domain/Sources/Repositories/MissonRepository.swift @@ -13,8 +13,8 @@ import RxSwift public protocol MissionRepositoryProtocol { /// FETCH - func fetchMissonContentItem(missonId: String) -> Observable - func fetchDailyMissonItem() -> Observable + func fetchMissonContentItem(missonId: String) -> Observable + func fetchDailyMissonItem() -> Observable /// LOCAL DB func isAlreadyShowMissionAlert() -> Observable diff --git a/14th-team5-iOS/Domain/Sources/Repositories/OAuthRepository.swift b/14th-team5-iOS/Domain/Sources/Repositories/OAuthRepository.swift index a4e39f66f..71a02bba8 100644 --- a/14th-team5-iOS/Domain/Sources/Repositories/OAuthRepository.swift +++ b/14th-team5-iOS/Domain/Sources/Repositories/OAuthRepository.swift @@ -10,9 +10,9 @@ import Foundation import RxSwift public protocol OAuthRepositoryProtocol { - func refreshAccessToken(body: RefreshAccessTokenRequest) -> Observable - func registerNewMember(body: CreateNewMemberRequest) -> Observable - func signIn(_ type: SignInType, body: NativeSocialLoginRequest) -> Observable - func registerNewFCMToken(body: AddFCMTokenRequest) -> Observable - func deleteFCMToken() -> Observable + func refreshAccessToken(body: RefreshAccessTokenRequest) -> Observable + func registerNewMember(body: CreateNewMemberRequest) -> Observable + func signIn(_ type: SignInType, body: NativeSocialLoginRequest) -> Observable + func registerNewFCMToken(body: AddFCMTokenRequest) -> Observable + func deleteFCMToken() -> Observable } diff --git a/14th-team5-iOS/Domain/Sources/Repositories/WidgetRepository.swift b/14th-team5-iOS/Domain/Sources/Repositories/WidgetRepository.swift index 9cd7f9ffa..cfdf7f2e3 100644 --- a/14th-team5-iOS/Domain/Sources/Repositories/WidgetRepository.swift +++ b/14th-team5-iOS/Domain/Sources/Repositories/WidgetRepository.swift @@ -8,5 +8,5 @@ import Foundation public protocol WidgetRepositoryProtocol { - func fetchRecentFamilyPost(completion: @escaping (Result) -> Void) + func fetchRecentFamilyPost(completion: @escaping (Result) -> Void) } diff --git a/14th-team5-iOS/Domain/Sources/UseCases/App/FetchAppVersionUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/App/FetchAppVersionUseCase.swift index a0c89d1ff..f971868ec 100644 --- a/14th-team5-iOS/Domain/Sources/UseCases/App/FetchAppVersionUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/App/FetchAppVersionUseCase.swift @@ -10,7 +10,7 @@ import Foundation import RxSwift public protocol FetchAppVersionUseCaseProtocol { - func execute() -> Observable + func execute() -> Observable } public class FetchAppVersionUseCase: FetchAppVersionUseCaseProtocol { @@ -25,7 +25,7 @@ public class FetchAppVersionUseCase: FetchAppVersionUseCaseProtocol { // MARK: - Execute - public func execute() -> Observable { + public func execute() -> Observable { appRepository.fetchAppVersion() } diff --git a/14th-team5-iOS/Domain/Sources/UseCases/MainView/FetchMainUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/MainView/FetchMainUseCase.swift index 3e881c69a..b0fa03609 100644 --- a/14th-team5-iOS/Domain/Sources/UseCases/MainView/FetchMainUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/MainView/FetchMainUseCase.swift @@ -10,7 +10,7 @@ import Foundation import RxSwift public protocol FetchMainUseCaseProtocol { - func execute() -> Observable + func execute() -> Observable } public final class FetchMainUseCase: FetchMainUseCaseProtocol { @@ -20,7 +20,7 @@ public final class FetchMainUseCase: FetchMainUseCaseProtocol { self.mainRepository = mainRepository } - public func execute() -> Observable { + public func execute() -> Observable { return mainRepository.fetchMain() } } diff --git a/14th-team5-iOS/Domain/Sources/UseCases/MainView/FetchNightMainViewUsecase.swift b/14th-team5-iOS/Domain/Sources/UseCases/MainView/FetchNightMainViewUsecase.swift index 2ed78b6d8..ce51d8594 100644 --- a/14th-team5-iOS/Domain/Sources/UseCases/MainView/FetchNightMainViewUsecase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/MainView/FetchNightMainViewUsecase.swift @@ -10,7 +10,7 @@ import Foundation import RxSwift public protocol FetchNightMainViewUseCaseProtocol { - func execute() -> Observable + func execute() -> Observable } public final class FetchNightMainViewUseCase: FetchNightMainViewUseCaseProtocol { @@ -20,7 +20,7 @@ public final class FetchNightMainViewUseCase: FetchNightMainViewUseCaseProtocol self.mainRepository = mainRepository } - public func execute() -> Observable { + public func execute() -> Observable { return mainRepository.fetchMainNight() } } diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Misson/FetchDailyMissonContentUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Misson/FetchDailyMissonContentUseCase.swift index 70968dbe4..e38c2ae13 100644 --- a/14th-team5-iOS/Domain/Sources/UseCases/Misson/FetchDailyMissonContentUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/Misson/FetchDailyMissonContentUseCase.swift @@ -11,7 +11,7 @@ import RxSwift public protocol FetchDailyMissonContentUseCaseProtocol { - func execute() -> Observable + func execute() -> Observable } public final class FetchDailyMissonContentUseCase: FetchDailyMissonContentUseCaseProtocol { @@ -22,7 +22,7 @@ public final class FetchDailyMissonContentUseCase: FetchDailyMissonContentUseCas self.missionRepository = missionRepository } - public func execute() -> Observable { + public func execute() -> Observable { return missionRepository.fetchDailyMissonItem() } } diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Misson/FetchMissionContentUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Misson/FetchMissionContentUseCase.swift index 670a33f9e..6cb3f40b0 100644 --- a/14th-team5-iOS/Domain/Sources/UseCases/Misson/FetchMissionContentUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/Misson/FetchMissionContentUseCase.swift @@ -12,7 +12,7 @@ import RxCocoa public protocol FetchMissionContentUseCaseProtocol { - func execute(missionId: String) -> Observable + func execute(missionId: String) -> Observable } public final class FetchMissionContentUseCase: FetchMissionContentUseCaseProtocol { @@ -22,7 +22,7 @@ public final class FetchMissionContentUseCase: FetchMissionContentUseCaseProtoco self.missionRepository = missionRepository } - public func execute(missionId: String) -> Observable { + public func execute(missionId: String) -> Observable { return missionRepository.fetchMissonContentItem(missonId: missionId) } diff --git a/14th-team5-iOS/Domain/Sources/UseCases/OAuth/DeleteFCMTokenUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/OAuth/DeleteFCMTokenUseCase.swift index c92be7b0a..054d8ffa7 100644 --- a/14th-team5-iOS/Domain/Sources/UseCases/OAuth/DeleteFCMTokenUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/OAuth/DeleteFCMTokenUseCase.swift @@ -10,7 +10,7 @@ import Foundation import RxSwift public protocol DeleteFCMTokenUseCaseProtocol { - func execute() -> Observable + func execute() -> Observable } public class DeleteFCMTokenUseCase: DeleteFCMTokenUseCaseProtocol { @@ -24,7 +24,7 @@ public class DeleteFCMTokenUseCase: DeleteFCMTokenUseCaseProtocol { } // MARK: - Execute - public func execute() -> Observable { + public func execute() -> Observable { return oauthRepository.deleteFCMToken() } } diff --git a/14th-team5-iOS/Domain/Sources/UseCases/OAuth/RefreshAccessTokenUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/OAuth/RefreshAccessTokenUseCase.swift index 9c0a65d76..7754843bc 100644 --- a/14th-team5-iOS/Domain/Sources/UseCases/OAuth/RefreshAccessTokenUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/OAuth/RefreshAccessTokenUseCase.swift @@ -10,7 +10,7 @@ import Foundation import RxSwift public protocol RefreshAccessTokenUseCaseProtocol { - func execute(body: RefreshAccessTokenRequest) -> Observable + func execute(body: RefreshAccessTokenRequest) -> Observable } public final class RefreshAccessTokenUseCase: RefreshAccessTokenUseCaseProtocol { @@ -24,7 +24,7 @@ public final class RefreshAccessTokenUseCase: RefreshAccessTokenUseCaseProtocol } // MARK: - Execute - public func execute(body: RefreshAccessTokenRequest) -> Observable { + public func execute(body: RefreshAccessTokenRequest) -> Observable { return oauthRepository.refreshAccessToken(body: body) } } diff --git a/14th-team5-iOS/Domain/Sources/UseCases/OAuth/RegisterNewFCMTokenUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/OAuth/RegisterNewFCMTokenUseCase.swift index eff633b07..b424c9b0e 100644 --- a/14th-team5-iOS/Domain/Sources/UseCases/OAuth/RegisterNewFCMTokenUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/OAuth/RegisterNewFCMTokenUseCase.swift @@ -10,7 +10,7 @@ import Foundation import RxSwift public protocol RegisterNewFCMTokenUseCaseProtocol { - func execute(body: AddFCMTokenRequest) -> Observable + func execute(body: AddFCMTokenRequest) -> Observable } public class RegisterNewFCMTokenUseCase: RegisterNewFCMTokenUseCaseProtocol { @@ -24,7 +24,7 @@ public class RegisterNewFCMTokenUseCase: RegisterNewFCMTokenUseCaseProtocol { } // MARK: - Execute - public func execute(body: AddFCMTokenRequest) -> Observable { + public func execute(body: AddFCMTokenRequest) -> Observable { return oauthRepository.registerNewFCMToken(body: body) } } diff --git a/14th-team5-iOS/Domain/Sources/UseCases/OAuth/RegisterNewMemberUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/OAuth/RegisterNewMemberUseCase.swift index 567c1aed1..440d33e4d 100644 --- a/14th-team5-iOS/Domain/Sources/UseCases/OAuth/RegisterNewMemberUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/OAuth/RegisterNewMemberUseCase.swift @@ -10,7 +10,7 @@ import Foundation import RxSwift public protocol RegisterNewMemberUseCaseProtocol { - func execute(body: CreateNewMemberRequest) -> Observable + func execute(body: CreateNewMemberRequest) -> Observable } public class RegisterNewMemberUseCase: RegisterNewMemberUseCaseProtocol { @@ -24,7 +24,7 @@ public class RegisterNewMemberUseCase: RegisterNewMemberUseCaseProtocol { } // MARK: - Execute - public func execute(body: CreateNewMemberRequest) -> Observable { + public func execute(body: CreateNewMemberRequest) -> Observable { return oauthRepository.registerNewMember(body: body) } } diff --git a/14th-team5-iOS/Domain/Sources/UseCases/OAuth/SignInUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/OAuth/SignInUseCase.swift index c867945e3..8d89132b0 100644 --- a/14th-team5-iOS/Domain/Sources/UseCases/OAuth/SignInUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/OAuth/SignInUseCase.swift @@ -10,7 +10,7 @@ import Foundation import RxSwift public protocol SignInUseCaseProtocol { - func execute(_ type: SignInType, body: NativeSocialLoginRequest) -> Observable + func execute(_ type: SignInType, body: NativeSocialLoginRequest) -> Observable } public class SignInUseCase: SignInUseCaseProtocol { @@ -27,7 +27,7 @@ public class SignInUseCase: SignInUseCaseProtocol { public func execute( _ type: SignInType, body: NativeSocialLoginRequest - ) -> Observable { + ) -> Observable { return oauthRepository.signIn(type, body: body) } } diff --git a/14th-team5-iOS/Domain/Sources/UseCases/Widget/FetchRecentFamilyPostUseCase.swift b/14th-team5-iOS/Domain/Sources/UseCases/Widget/FetchRecentFamilyPostUseCase.swift index 1d0507363..72764af8a 100644 --- a/14th-team5-iOS/Domain/Sources/UseCases/Widget/FetchRecentFamilyPostUseCase.swift +++ b/14th-team5-iOS/Domain/Sources/UseCases/Widget/FetchRecentFamilyPostUseCase.swift @@ -8,7 +8,7 @@ import Foundation public protocol FetchRecentFamilyPostUseCaseProtocol { - func excute(completion: @escaping (Result) -> Void) + func excute(completion: @escaping (Result) -> Void) } public class FetchRecentFamilyPostUseCase: FetchRecentFamilyPostUseCaseProtocol { @@ -18,7 +18,7 @@ public class FetchRecentFamilyPostUseCase: FetchRecentFamilyPostUseCaseProtocol self.widgetRepository = widgetRepository } - public func excute(completion: @escaping (Result) -> Void) { + public func excute(completion: @escaping (Result) -> Void) { return widgetRepository.fetchRecentFamilyPost(completion: completion) } } diff --git a/Bibbi.xcworkspace/contents.xcworkspacedata b/Bibbi.xcworkspace/contents.xcworkspacedata index 7e570dad8..7bb900224 100644 --- a/Bibbi.xcworkspace/contents.xcworkspacedata +++ b/Bibbi.xcworkspace/contents.xcworkspacedata @@ -26,6 +26,9 @@ + + @@ -35,6 +38,9 @@ + + @@ -44,6 +50,9 @@ + + @@ -77,15 +86,27 @@ + + + + + + + + diff --git a/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme b/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme index 5fd01fa86..1781b4e89 100644 --- a/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme +++ b/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme @@ -20,20 +20,6 @@ ReferencedContainer = "container:Tuist/.build/tuist-derived/Alamofire/Alamofire.xcodeproj"> - - - - @@ -574,9 +560,9 @@ buildForAnalyzing = "YES"> @@ -588,9 +574,9 @@ buildForAnalyzing = "YES"> @@ -602,9 +588,9 @@ buildForAnalyzing = "YES"> @@ -616,9 +602,9 @@ buildForAnalyzing = "YES"> @@ -630,9 +616,9 @@ buildForAnalyzing = "YES"> @@ -644,9 +630,9 @@ buildForAnalyzing = "YES"> @@ -658,9 +644,9 @@ buildForAnalyzing = "YES"> @@ -1154,20 +1140,6 @@ ReferencedContainer = "container:Tuist/.build/tuist-derived/SnapKit/SnapKit.xcodeproj"> - - - - Date: Fri, 20 Dec 2024 17:46:38 +0900 Subject: [PATCH 255/263] =?UTF-8?q?feat:=20Firebase=20Crashlytics=20?= =?UTF-8?q?=EB=AA=A8=EB=93=88=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EB=82=B4?= =?UTF-8?q?=EB=B6=80=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EB=B0=98=EC=98=81=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80=20(#727)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Core모듈 내부 BBAnalyticsLogParametable, BBAnalyticsLogType, BBEventAnalyticsLog, BBLogManager 추가 - Tuist TuistScript+Templates dsym Upload를 하기 위한 Script 파일 추가 - 각 ViewController에 Analytics Event log 메서드 추가 - Crashlytics Log Method 추가 * feat: github Action PRD Lane dsym Upload Method 추가 * fix: ViewController Analytics LogEvent Method Reactor로 코드 이전 --- .../App/Sources/Application/AppDelegate.swift | 1 - .../AccountProfileViewController.swift | 1 + .../MonthlyCalendarViewController.swift | 6 ++ .../Camera/CameraDisplayViewController.swift | 1 + .../Camera/CameraViewController.swift | 1 + .../JoinFamilyViewController.swift | 6 +- .../ViewControllers/MainViewController.swift | 1 + .../FamilyNameSettingViewReactor.swift | 2 +- .../FamilyNameSettingViewController.swift | 5 ++ .../ManagementViewController.swift | 6 ++ .../ViewControllers/PostViewController.swift | 1 + .../Privacy/PrivacyViewController.swift | 2 + .../Privacy/Reactor/PrivacyViewReactor.swift | 1 + .../ProfileFeedPageViewController.swift | 15 +++- .../Profile/ProfileViewController.swift | 2 + .../Profile/Reactor/ProfileViewReactor.swift | 1 + .../Resign/AccountResignViewController.swift | 2 + .../BBNetwork/Network/BBNetworkError.swift | 25 +++++++ .../Core/Sources/Extensions/String+Ext.swift | 36 ++++++++++ .../Logger/BBAnalyticsLogParametable.swift | 16 +++++ .../Utils/Logger/BBAnalyticsLogType.swift | 38 ++++++++++ .../Utils/Logger/BBEventAnalyticsLog.swift | 64 +++++++++++++++++ .../Sources/Utils/Logger/BBLogManager.swift | 69 +++++++++++++++++++ .../FamilyUserDefaults.swift | 1 + .../AccountRepository/AccountRepository.swift | 2 +- .../ConfigurationName+Templates.swift | 3 + .../Modular+Templates.swift | 12 +++- .../ModuleType+Templates.swift | 3 +- .../Project+Templates.swift | 2 +- .../TargetScript+Templates.swift | 41 +++++++++++ fastlane/Fastfile | 16 +++++ 31 files changed, 370 insertions(+), 12 deletions(-) create mode 100644 14th-team5-iOS/Core/Sources/Utils/Logger/BBAnalyticsLogParametable.swift create mode 100644 14th-team5-iOS/Core/Sources/Utils/Logger/BBAnalyticsLogType.swift create mode 100644 14th-team5-iOS/Core/Sources/Utils/Logger/BBEventAnalyticsLog.swift create mode 100644 14th-team5-iOS/Core/Sources/Utils/Logger/BBLogManager.swift create mode 100644 Tuist/ProjectDescriptionHelpers/TargetScript+Templates.swift diff --git a/14th-team5-iOS/App/Sources/Application/AppDelegate.swift b/14th-team5-iOS/App/Sources/Application/AppDelegate.swift index 2530fdbca..c74310ea8 100644 --- a/14th-team5-iOS/App/Sources/Application/AppDelegate.swift +++ b/14th-team5-iOS/App/Sources/Application/AppDelegate.swift @@ -13,7 +13,6 @@ import UIKit import AuthenticationServices import Firebase import FirebaseCore -import FirebaseAnalytics import FirebaseMessaging import KakaoSDKAuth import RxKakaoSDKAuth diff --git a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountProfileViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountProfileViewController.swift index 34041e43d..aefdcb320 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountProfileViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountProfileViewController.swift @@ -60,6 +60,7 @@ final class AccountProfileViewController: BaseViewController { public override func viewDidLoad() { super.viewDidLoad() + BBLogManager.analytics(logType: BBEventAnalyticsLog.viewPage(pageName: .camera)) setupCameraPermission() } diff --git a/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/ViewController/JoinFamilyViewController.swift b/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/ViewController/JoinFamilyViewController.swift index 88376c15d..ed373ab94 100644 --- a/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/ViewController/JoinFamilyViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/ViewController/JoinFamilyViewController.swift @@ -77,13 +77,17 @@ final class JoinFamilyViewController: BaseViewController { private func bindInput(reactor: JoinFamilyReactor) { makeFamilyButton.rx.tap - .do(onNext: { MPEvent.Account.creatGroup.track(with: nil) }) + .do(onNext: { + MPEvent.Account.creatGroup.track(with: nil) + BBLogManager.analytics(logType: BBEventAnalyticsLog.clickFamilyButton(entry: .createFamilyGroup)) + }) .throttle(RxConst.milliseconds300Interval, scheduler: MainScheduler.instance) .withUnretained(self) .bind(onNext: { $0.0.newGroupAlertController()}) .disposed(by: disposeBag) joinFamilyButton.rx.tap + .do { _ in BBLogManager.analytics(logType: BBEventAnalyticsLog.clickFamilyButton(entry: .inviteFamily)) } .map { Reactor.Action.joinFamily } .bind(to: reactor.action) .disposed(by: disposeBag) 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 7f9fc2b1d..defd104c8 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift @@ -30,6 +30,7 @@ final class MainViewController: BBNavigationViewController, UIC override func viewDidLoad() { super.viewDidLoad() + BBLogManager.analytics(logType: BBEventAnalyticsLog.viewPage(pageName: .main)) } override func bind(reactor: MainViewReactor) { 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 21191a200..67a250d72 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Management/Reactor/FamilyNameSettingViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Management/Reactor/FamilyNameSettingViewReactor.swift @@ -100,7 +100,7 @@ public final class FamilyNameSettingViewReactor: Reactor { case let .didTapUpdateFamilyGroupNickname(type): let familyName = type == .initial ? nil : currentState.familyGroupNickName let updateFamilyBody = UpdateFamilyNameRequest(familyName: familyName) - + BBLogManager.analytics(logType: BBEventAnalyticsLog.clickFamilyButton(entry: .familyNameSetting)) return updateFamilyNameUseCase.execute(body: updateFamilyBody) .asObservable() .compactMap { $0 } diff --git a/14th-team5-iOS/App/Sources/Presentation/Management/ViewController/FamilyNameSettingViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Management/ViewController/FamilyNameSettingViewController.swift index 76211340f..709723769 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Management/ViewController/FamilyNameSettingViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Management/ViewController/FamilyNameSettingViewController.swift @@ -24,6 +24,11 @@ final class FamilyNameSettingViewController: BBNavigationViewController { override func viewDidLoad() { super.viewDidLoad() + BBLogManager.analytics(logType: BBEventAnalyticsLog.viewPage(pageName: .postDetail)) self.navigationController?.navigationBar.isHidden = true } diff --git a/14th-team5-iOS/App/Sources/Presentation/Privacy/PrivacyViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Privacy/PrivacyViewController.swift index 0fc776c1c..d2216a9cd 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Privacy/PrivacyViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Privacy/PrivacyViewController.swift @@ -222,6 +222,7 @@ extension PrivacyViewController { let confirmAction = UIAlertAction(title: "확인", style: .default) { [weak self]_ in guard let self else { return } + BBLogManager.analytics(logType: BBEventAnalyticsLog.clickAccountButton(entry: .logout)) self.reactor?.action.onNext(.didTapLogoutButton) } @@ -242,6 +243,7 @@ extension PrivacyViewController { let confirmAction = UIAlertAction(title: "확인", style: .default) { [weak self ]_ in guard let self else { return } self.reactor?.action.onNext(.didTapFamilyUserResign) + BBLogManager.analytics(logType: BBEventAnalyticsLog.clickAccountButton(entry: .familyResign)) } [cancelAction, confirmAction].forEach(resignAlertController.addAction(_:)) diff --git a/14th-team5-iOS/App/Sources/Presentation/Privacy/Reactor/PrivacyViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Privacy/Reactor/PrivacyViewReactor.swift index b49e66237..a9cb2741c 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Privacy/Reactor/PrivacyViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Privacy/Reactor/PrivacyViewReactor.swift @@ -64,6 +64,7 @@ public final class PrivacyViewReactor: Reactor { public func mutate(action: Action) -> Observable { switch action { case .viewDidLoad: + BBLogManager.analytics(logType: BBEventAnalyticsLog.viewPage(pageName: .setting)) return .concat( .just(.setLoading(true)), .merge( diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileFeedPageViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileFeedPageViewController.swift index 0a77b5599..2beeba523 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileFeedPageViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileFeedPageViewController.swift @@ -84,13 +84,19 @@ extension ProfileFeedPageViewController: ReactorKit.View { extension ProfileFeedPageViewController: UIPageViewControllerDelegate, UIPageViewControllerDataSource { func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { guard let index = feedViewControllers.firstIndex(of: viewController), - index - 1 >= 0 else { return nil } + index - 1 >= 0 else { + BBLogManager.sendError(error: BBCrashError.indexOutBounds) + return nil + } return feedViewControllers[index - 1] } func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { guard let index = feedViewControllers.firstIndex(of: viewController), - index + 1 != feedViewControllers.count else { return nil } + index + 1 != feedViewControllers.count else { + BBLogManager.sendError(error: BBCrashError.indexOutBounds) + return nil + } return feedViewControllers[index + 1] } @@ -99,7 +105,10 @@ extension ProfileFeedPageViewController: UIPageViewControllerDelegate, UIPageVie func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) { guard let currentViewController = pageViewController.viewControllers?.first, - let currentIndex = feedViewControllers.firstIndex(of: currentViewController) else { return } + let currentIndex = feedViewControllers.firstIndex(of: currentViewController) else { + BBLogManager.sendError(error: BBCrashError.indexOutBounds) + return + } reactor?.action.onNext(.updatePageViewController(currentIndex)) } diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileViewController.swift index a37b1af05..103f03c6f 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileViewController.swift @@ -50,6 +50,7 @@ public final class ProfileViewController: BaseViewController public override func viewDidLoad() { super.viewDidLoad() + BBLogManager.analytics(logType: BBEventAnalyticsLog.viewPage(pageName: .profile)) } public override func setupUI() { @@ -131,6 +132,7 @@ public final class ProfileViewController: BaseViewController profileView.circleButton .rx.tap + .do { _ in BBLogManager.analytics(logType: BBEventAnalyticsLog.clickAccountButton(entry: .profileImageEdit))} .throttle(.milliseconds(300), scheduler: MainScheduler.instance) .withUnretained(self) .bind(onNext: {$0.0.createAlertController()}) diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift index c93bb600f..bee7eebe7 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift @@ -151,6 +151,7 @@ public final class ProfileViewReactor: Reactor { profileNavigator.toPrivacy(memberId) return .empty() case let .didTappedProfileEditButton(memberId): + BBLogManager.analytics(logType: BBEventAnalyticsLog.clickAccountButton(entry: .profileNickNameEdit)) profileNavigator.toAccountNickname(memberId) return .empty() case let .didTappedAlertButton(memberId): diff --git a/14th-team5-iOS/App/Sources/Presentation/Resign/AccountResignViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Resign/AccountResignViewController.swift index 3adcd6ab3..c280927b6 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Resign/AccountResignViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Resign/AccountResignViewController.swift @@ -26,6 +26,7 @@ final class AccountResignViewController: BaseViewController String { + var result = "" + var previousStringWasCapitalized = false + var previousStringWasNumber = false + + for (index, string) in self.enumerated() { + var mutableString = String(string) + + if !mutableString.isAlphabet { + if index != 0, + !previousStringWasNumber { + mutableString = "_" + mutableString + } + previousStringWasNumber = true + } else if mutableString == mutableString.uppercased() { + mutableString = mutableString.lowercased() + + if index != 0, + !previousStringWasCapitalized { + mutableString = "_" + mutableString + } + previousStringWasCapitalized = true + } else { + previousStringWasCapitalized = false + previousStringWasNumber = false + } + result += mutableString + } + return result + } + + public var isAlphabet: Bool { + let alphabetSet = CharacterSet.uppercaseLetters.union(.lowercaseLetters).union(.whitespacesAndNewlines) + return self.rangeOfCharacter(from: alphabetSet.inverted) == nil + } + public func toDate(with format: String = "yyyy-MM-dd") -> Date { let dateFormatter = DateFormatter.withFormat(format) guard let date = dateFormatter.date(from: self) else { return .now } diff --git a/14th-team5-iOS/Core/Sources/Utils/Logger/BBAnalyticsLogParametable.swift b/14th-team5-iOS/Core/Sources/Utils/Logger/BBAnalyticsLogParametable.swift new file mode 100644 index 000000000..05fb56c91 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Utils/Logger/BBAnalyticsLogParametable.swift @@ -0,0 +1,16 @@ +// +// BBAnalyticsLogParametable.swift +// Core +// +// Created by 김도현 on 12/19/24. +// + +import Foundation + +public protocol BBAnalyticsLogParametable: RawRepresentable, CustomStringConvertible where RawValue == String { } + +public extension BBAnalyticsLogParametable { + var description: String { + self.rawValue + } +} diff --git a/14th-team5-iOS/Core/Sources/Utils/Logger/BBAnalyticsLogType.swift b/14th-team5-iOS/Core/Sources/Utils/Logger/BBAnalyticsLogType.swift new file mode 100644 index 000000000..2983502e9 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Utils/Logger/BBAnalyticsLogType.swift @@ -0,0 +1,38 @@ +// +// BBAnalyticsLogType.swift +// Core +// +// Created by 김도현 on 12/19/24. +// + +import Foundation + +public protocol BBAnalyticsLogType { + var name: String { get } + var params: [String: Any] { get } +} + +public extension BBAnalyticsLogType { + var name: String { + Mirror(reflecting: self) + .children + .first? + .label? + .toSnakeCase() ?? String(describing: self).toSnakeCase() + } + + var params: [String: Any] { + var dict: [String: Any] = [:] + + let enumMirror = Mirror(reflecting: self) + + guard let associated = enumMirror.children.first else { return dict } + + for enumParams in Mirror(reflecting: associated.value).children { + guard let label = enumParams.label?.toSnakeCase() else { continue } + dict[label] = enumParams.value + } + + return dict + } +} diff --git a/14th-team5-iOS/Core/Sources/Utils/Logger/BBEventAnalyticsLog.swift b/14th-team5-iOS/Core/Sources/Utils/Logger/BBEventAnalyticsLog.swift new file mode 100644 index 000000000..a50023cf1 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Utils/Logger/BBEventAnalyticsLog.swift @@ -0,0 +1,64 @@ +// +// BBEventAnalyticsLog.swift +// Core +// +// Created by 김도현 on 12/19/24. +// + +import Foundation + +public enum BBEventAnalyticsLog: BBAnalyticsLogType { + case viewPage(pageName: PageName) + case clickAccountButton(entry: AccountButtonEntry) + case clickFamilyButton(entry: FamilyButtonEntry) + case clickCameraButton(entry: CameraButtonEntry) +} + + +public extension BBEventAnalyticsLog { + enum PageName: String, BBAnalyticsLogParametable { + case main + case calendar = "calendar" + case camera = "camera" + case cameraDetail = "camera_detail" + case familyManagement = "family_management" + case familyGroupNameSetting = "family_group_name_setting" + case postDetail = "post_detail" + case profile + case setting + case resign + + public var description: String { + self.rawValue + } + } + + enum AccountButtonEntry: String, BBAnalyticsLogParametable { + case profileImageEdit = "profile_image_edit" + case profileNickNameEdit = "profile_nickname_edit" + case logout + case familyResign = "family_resign" + case resign + + + public var description: String { + self.rawValue + } + } + + enum FamilyButtonEntry: String, BBAnalyticsLogParametable { + case createFamilyGroup = "create_family_group" + case familyNameSetting = "family_name_setting" + case inviteFamily = "invite_family" + + + public var description: String { + self.rawValue + } + } + + enum CameraButtonEntry: String, BBAnalyticsLogParametable { + case shutter + } + +} diff --git a/14th-team5-iOS/Core/Sources/Utils/Logger/BBLogManager.swift b/14th-team5-iOS/Core/Sources/Utils/Logger/BBLogManager.swift new file mode 100644 index 000000000..7ee49ae13 --- /dev/null +++ b/14th-team5-iOS/Core/Sources/Utils/Logger/BBLogManager.swift @@ -0,0 +1,69 @@ +// +// BBLogManager.swift +// Core +// +// Created by 김도현 on 12/12/24. +// + +import FirebaseCrashlytics +import FirebaseAnalytics + + +public enum BBLogManager { + + public static func setMemberId( + memberId: String, + function: String = #function, + fileName: String = #file + ) { + Crashlytics.crashlytics().setUserID(memberId) + Analytics.setUserID(memberId) + + BBLogger.logDebug( + function: function, + fileName: fileName, + category: "Analytics", + message: "Firebase 멤버 ID 설정" + ) + } + + public static func analytics( + logType: any BBAnalyticsLogType + ) { + Analytics.logEvent(logType.name, parameters: logType.params) + } + + public static func sendError( + message: String, + function: String = #function, + fileName: String = #file + ) { + Crashlytics.crashlytics().log(message) + + BBLogger.logError( + function: function, + fileName: fileName, + category: "Analytics Error", + message: message + ) + } + + public static func sendError( + error: any Error, + function: String = #function, + fileName: String = #file + ) { + Crashlytics.crashlytics().setCrashlyticsCollectionEnabled(true) + Crashlytics.crashlytics().record(error: error) + + BBLogger.logError( + function: function, + fileName: fileName, + category: "Analytics Error", + message: error.localizedDescription + ) + + } + +} + diff --git a/14th-team5-iOS/Data/Sources/Storages/UserDefaults/FamilyUserDefaults/FamilyUserDefaults.swift b/14th-team5-iOS/Data/Sources/Storages/UserDefaults/FamilyUserDefaults/FamilyUserDefaults.swift index f64c845fc..2e46e00f4 100644 --- a/14th-team5-iOS/Data/Sources/Storages/UserDefaults/FamilyUserDefaults/FamilyUserDefaults.swift +++ b/14th-team5-iOS/Data/Sources/Storages/UserDefaults/FamilyUserDefaults/FamilyUserDefaults.swift @@ -88,6 +88,7 @@ final public class FamilyInfoUserDefaults: FamilyInfoUserDefaultsType { public func loadFamilyId() -> String? { guard let familyId: String = userDefaults[.familyId] else { + BBLogManager.sendError(message: "not Found FamilyID") return nil } return familyId diff --git a/14th-team5-iOS/Data/Sources/Trash/Account/AccountRepository/AccountRepository.swift b/14th-team5-iOS/Data/Sources/Trash/Account/AccountRepository/AccountRepository.swift index 3fbf36d3d..d38200909 100644 --- a/14th-team5-iOS/Data/Sources/Trash/Account/AccountRepository/AccountRepository.swift +++ b/14th-team5-iOS/Data/Sources/Trash/Account/AccountRepository/AccountRepository.swift @@ -123,7 +123,7 @@ public final class AccountRepository: AccountImpl { App.Repository.member.memberID.accept(memberInfo.memberId) App.Repository.member.familyId.accept(memberInfo.familyId) App.Repository.member.nickname.accept(memberInfo.name) - + BBLogManager.setMemberId(memberId: memberInfo.memberId) let member: FamilyMemberProfileEntity = FamilyMemberProfileEntity(memberId: memberInfo.memberId, profileImageURL: memberInfo.imageUrl, name: memberInfo.name) } diff --git a/Tuist/ProjectDescriptionHelpers/ConfigurationName+Templates.swift b/Tuist/ProjectDescriptionHelpers/ConfigurationName+Templates.swift index 1ce903a32..8912029a5 100644 --- a/Tuist/ProjectDescriptionHelpers/ConfigurationName+Templates.swift +++ b/Tuist/ProjectDescriptionHelpers/ConfigurationName+Templates.swift @@ -26,16 +26,19 @@ extension Configuration { case .dev: return .debug( name: BuildTarget.dev.configurationName, + settings: ["DEBUG_INFORMATION_FORMAT": "dwarf-with-dsym"], xcconfig: .relativeToXCConfig(type: .dev) ) case .stg: return .release( name: BuildTarget.stg.configurationName, + settings: ["DEBUG_INFORMATION_FORMAT": "dwarf-with-dsym"], xcconfig: .relativeToXCConfig(type: .stg) ) case .prd: return .release( name: BuildTarget.prd.configurationName, + settings: ["DEBUG_INFORMATION_FORMAT": "dwarf-with-dsym"], xcconfig: .relativeToXCConfig(type: .prd) ) } diff --git a/Tuist/ProjectDescriptionHelpers/Modular+Templates.swift b/Tuist/ProjectDescriptionHelpers/Modular+Templates.swift index ef25909d5..3485ca015 100644 --- a/Tuist/ProjectDescriptionHelpers/Modular+Templates.swift +++ b/Tuist/ProjectDescriptionHelpers/Modular+Templates.swift @@ -19,7 +19,12 @@ public struct ModularFactory { var infoPlist: InfoPlist? var sources: SourceFilesList? var resources: ResourceFileElements? - var settings: Settings? + var settings: Settings? = .settings( + base: [ + "DEBUG_INFORMATION_FORMAT": "dwarf-with-dsym", + "OTHER_LDFLAGS": "$(inherited) -ObjC" + ] + ) var entitlements: Entitlements? @@ -87,6 +92,7 @@ extension Target { sources: factory.sources, resources: factory.resources, entitlements: factory.entitlements, + scripts: [.firebaseInfoByConfiguration, .firebaseCrashlytics], dependencies: factory.dependencies, settings: factory.settings ) @@ -109,7 +115,7 @@ extension Target { return .target( name: layer.rawValue, destinations: .iOS, - product: factory.products.isFramework ? .staticFramework : .framework, + product: .staticFramework, bundleId: "com.\(layer.rawValue).project".lowercased(), deploymentTargets: factory.deploymentTargets, infoPlist: factory.infoPlist, @@ -123,7 +129,7 @@ extension Target { return .target( name: layer.rawValue, destinations: .iOS, - product: factory.products.isLibrary ? .framework : .staticFramework, + product: .staticFramework, bundleId: "com.\(layer.rawValue).project".lowercased(), deploymentTargets: factory.deploymentTargets, infoPlist: factory.infoPlist, diff --git a/Tuist/ProjectDescriptionHelpers/ModuleType+Templates.swift b/Tuist/ProjectDescriptionHelpers/ModuleType+Templates.swift index b2cbfc759..f0eb1198c 100644 --- a/Tuist/ProjectDescriptionHelpers/ModuleType+Templates.swift +++ b/Tuist/ProjectDescriptionHelpers/ModuleType+Templates.swift @@ -41,7 +41,6 @@ public enum ModuleLayer: String, CaseIterable, ModuleType { case .App: return [ .target(name: "WidgetExtension"), - .external(name: "FirebaseAnalytics"), .external(name: "FirebaseMessaging"), .external(name: "Mixpanel"), .external(name: "RxDataSources"), @@ -68,6 +67,8 @@ public enum ModuleLayer: String, CaseIterable, ModuleType { case .Core: return [ .with(.DesignSystem), + .external(name: "FirebaseAnalytics", condition: .when(.all)), + .external(name: "FirebaseCrashlytics", condition: .when(.all)), .external(name: "SnapKit", condition: .when(.all)), .external(name: "Then", condition: .when(.all)), .external(name: "Kingfisher", condition: .when(.all)), diff --git a/Tuist/ProjectDescriptionHelpers/Project+Templates.swift b/Tuist/ProjectDescriptionHelpers/Project+Templates.swift index 98094bae9..ebeb7e1e9 100644 --- a/Tuist/ProjectDescriptionHelpers/Project+Templates.swift +++ b/Tuist/ProjectDescriptionHelpers/Project+Templates.swift @@ -58,7 +58,7 @@ extension Project { name: name, settings: .settings( base: [ - "OTHER_LDFLAGS": ["-ObjC"], + "OTHER_LDFLAGS": "$(inherited) -ObjC", "MARKETING_VERSION": "1.2.4", "CURRENT_PROJECT_VERSION": "1", "VERSIONING_SYSTEM": "apple-generic" diff --git a/Tuist/ProjectDescriptionHelpers/TargetScript+Templates.swift b/Tuist/ProjectDescriptionHelpers/TargetScript+Templates.swift new file mode 100644 index 000000000..0971d746a --- /dev/null +++ b/Tuist/ProjectDescriptionHelpers/TargetScript+Templates.swift @@ -0,0 +1,41 @@ +// +// TargetScript+Templates.swift +// ProjectDescriptionHelpers +// +// Created by 김도현 on 12/12/24. +// + +import ProjectDescription +import Foundation + + +public extension TargetScript { + static let firebaseCrashlytics = TargetScript.post( + script: """ + case "${CONFIGURATION}" in + "PRD" | "Release" ) + BASE_PATH=$(realpath "${SRCROOT}/../..") + "${BASE_PATH}/Tuist/.build/checkouts/firebase-ios-sdk/Crashlytics/run" -gsp "${BASE_PATH}/14th-team5-iOS/App/Resources/GoogleService-Info.plist" -p ios "${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}" + esac + """, + name: "Firebase Crashlytics", + inputPaths: [ + "${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${TARGET_NAME}", + "$(SRCROOT)/$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)" + ], + basedOnDependencyAnalysis: false + ) + + static let firebaseInfoByConfiguration = TargetScript.post( + script: """ + case "${CONFIGURATION}" in + "PRD" ) + cp -r "$SRCROOT/Resources/GoogleService-Info.plist" "${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/GoogleService-Info.plist" + ;; + *) + esac + """, + name: "Firebase Info copy by Configuration", + basedOnDependencyAnalysis: false + ) +} diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 48f224b90..f64b4173f 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -156,6 +156,11 @@ platform :ios do new_build_number = build_num + 1 + version = get_version_number( + xcodeproj: "14th-team5-iOS/App/App.xcodeproj", + target: "#{PRD_SCHEME}" + ) + increment_build_number( xcodeproj: "14th-team5-iOS/App/App.xcodeproj", @@ -186,6 +191,17 @@ platform :ios do } ) + download_dsyms( + app_identifier: "com.5ing.bibbi", + version: version, + build_number: new_build_number, + output_directory: "./14th-team5-iOS" + ) + + upload_symbols_to_crashlytics( + gsp_path: "14th-team5-iOS/App/Resources/GoogleService-Info.plist" + ) + upload_to_testflight(skip_waiting_for_build_processing: true) From 4c8e7e14782d62656fa1190d45a92862f1f28717 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=84?= Date: Fri, 20 Dec 2024 18:26:53 +0900 Subject: [PATCH 256/263] =?UTF-8?q?feat:=20BBToolTip=20Action=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EB=B0=8F=20=EB=A7=88=EC=9D=B4=EA=B7=B8=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EC=9E=91=EC=97=85=20=ED=95=B4=EC=9A=94=20?= =?UTF-8?q?=20(#693)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: BBBaseToolTipView, BBThumbnailToolTipView 추가 - BBAnimatable 로직 수정 - BBToolTipType xPosition, yPosition 분리 - BBToolTipAction Nested Type 추가 * feat: BBTextToolTipView, BBThumbnailToolTipView 모듈 분리 - BBBaseToolTIpView 내부 drawable Method 추가 - BBToolTip Class 추가 - BBDrawable protocol, extension 제거 * fix: BBToolTip createToolTip Method parameter completionHandler 추가 * feat: BBToolTip 관련 에러처리 로직 수정 - ToolTip 관련 주석 추가 * feat: BBToolTipConfiguration maxWidth, maxHeight Properties 추가 - BBToolTip Layout을 frame 기반으로 로직 수정 * feat: BBTextToolTipView, BBThumbnailToolTipView Width, height 동적으로 정의하기 위해 intrinsicContentSize 재정의 - BBTooltip contentView ContentView intrinsicContentSize 기반으로 로직 수정 - BBToolTipConfiguration maxWidth, maxHeight Properties 제거 * fix: BBDrawable Protocol 제거 - BBToolTip property, Intializer 수정 * fix: MemoriesCalendarPageTitleView 기존 TooltipView 로직 수정 - BBTextToolTipView TouchControl 클릭시 Tooltip 사라지는 애니메이션 로직 제거 * fix: BBAnimatable, BBTextToolTipView, BBThumbnailToolTipView, BBToolTIpView 코드리뷰 반영 - ManagementTableHeaderView ToolTipView 추가 --- .../View/MemoriesCalendarPageTitleView.swift | 19 +-- .../ManagementTableHeaderReactor.swift | 27 +++- .../View/ManagementTableHeaderView.swift | 17 ++- .../BBCommons/BBToolTip/BBAnimatable.swift | 52 ++++--- ...Drawable.swift => BBBaseToolTipView.swift} | 64 ++++----- .../BBToolTip/BBTextToolTipView.swift | 77 +++++++++++ .../BBToolTip/BBThumbnailToolTipView.swift | 111 +++++++++++++++ .../Bibbi/BBCommons/BBToolTip/BBToolTip.swift | 94 +++++++++++++ .../BBToolTip/BBToolTipConfiguration.swift | 26 ++-- .../BBCommons/BBToolTip/BBToolTipType.swift | 32 +++-- .../BBCommons/BBToolTip/BBToolTipView.swift | 127 ------------------ 11 files changed, 428 insertions(+), 218 deletions(-) rename 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToolTip/{BBDrawable.swift => BBBaseToolTipView.swift} (59%) create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToolTip/BBTextToolTipView.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToolTip/BBThumbnailToolTipView.swift create mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToolTip/BBToolTip.swift delete mode 100644 14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToolTip/BBToolTipView.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/MemoriesCalendarPageTitleView.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/MemoriesCalendarPageTitleView.swift index 4c550a018..f13c3200a 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/MemoriesCalendarPageTitleView.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/View/MemoriesCalendarPageTitleView.swift @@ -21,7 +21,7 @@ final public class MemoriesCalendarPageTitleView: BaseView Observable { + switch action { + case .didTappedToolTipButton: + return .just(.setToolTipHidden(!currentState.isHidden)) + } + } + + public func reduce(state: State, mutation: Mutation) -> State { + var newState = state + switch mutation { + case let .setToolTipHidden(isHidden): + newState.isHidden = isHidden + } + return newState + } } diff --git a/14th-team5-iOS/App/Sources/Presentation/Management/View/ManagementTableHeaderView.swift b/14th-team5-iOS/App/Sources/Presentation/Management/View/ManagementTableHeaderView.swift index e1c9fd2d9..680d5662f 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Management/View/ManagementTableHeaderView.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Management/View/ManagementTableHeaderView.swift @@ -20,7 +20,7 @@ public final class ManagementTableHeaderView: BaseView Void)? = nil) { + switch style { + case .waitingSurvivalImage: + self.contentView = BBThumbnailToolTipView(toolTipType: style) + default: + self.contentView = BBTextToolTipView(toolTipType: style) + } + + completion?() + } +} diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToolTip/BBToolTipConfiguration.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToolTip/BBToolTipConfiguration.swift index f46711b80..6f4cfd0f2 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToolTip/BBToolTipConfiguration.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToolTip/BBToolTipConfiguration.swift @@ -12,27 +12,30 @@ import DesignSystem /// BBToolTip에 (UI, Width, Height) Properties를 설정하기 위한 구조체입니다. public struct BBToolTipConfiguration { /// ToolTip Corner Radius - public var cornerRadius: CGFloat + public let cornerRadius: CGFloat /// TooTip TextFont Foreground Color - public var foregroundColor: UIColor + public let foregroundColor: UIColor /// TooTip Background Color - public var backgroundColor: UIColor - /// ToolTip Arrow Position - public var position: BBToolTipVerticalPosition + public let backgroundColor: UIColor + /// ToolTip Arrow YPosition + public let yPosition: BBToolTipVerticalPosition + /// ToolTip Arrow XPosition + public let xPosition: BBToolTipHorizontalPosition /// ToolTip Text Font - public var font: BBFontStyle + public let font: BBFontStyle /// ToolTip Content Text - public var contentText: String + public let contentText: String /// ToolTip Arrow Width - public var arrowWidth: CGFloat + public let arrowWidth: CGFloat /// ToolTip Arrow Height - public var arrowHeight: CGFloat + public let arrowHeight: CGFloat public init( cornerRadius: CGFloat = 12, foregroundColor: UIColor = .bibbiBlack, backgroundColor: UIColor = .mainYellow, - position: BBToolTipVerticalPosition = .top, + yPosition: BBToolTipVerticalPosition = .bottom, + xPosition: BBToolTipHorizontalPosition = .center, font: BBFontStyle = .body2Regular, contentText: String = "", arrowWidth: CGFloat = 15, @@ -41,7 +44,8 @@ public struct BBToolTipConfiguration { self.cornerRadius = cornerRadius self.foregroundColor = foregroundColor self.backgroundColor = backgroundColor - self.position = position + self.xPosition = xPosition + self.yPosition = yPosition self.font = font self.contentText = contentText self.arrowWidth = arrowWidth diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToolTip/BBToolTipType.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToolTip/BBToolTipType.swift index 21b428435..4cabc0c7d 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToolTip/BBToolTipType.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToolTip/BBToolTipType.swift @@ -11,7 +11,7 @@ import DesignSystem /// BBToolTip의 Style을 설정하기 위한 Nested types입니다. /// 해당 **BBToolTipType** 을 통해 BBToolTip의 Layout을 구성합니다. -public enum BBToolTipType { +public enum BBToolTipType: Equatable { /// 홈 화면 inactive Camera Button State ToolTip Type case inactiveCameraTime /// 홈 화면 active Camera Button State ToolTip Type @@ -52,70 +52,80 @@ public enum BBToolTipType { return .init( foregroundColor: .bibbiWhite, backgroundColor: .gray700, - position: .bottom, + yPosition: .bottom, + xPosition: .center, contentText: "오늘의 생존신고는 완료되었어요" ) case .activeCameraTime: return .init( foregroundColor: .bibbiWhite, backgroundColor: .gray700, - position: .bottom, + yPosition: .bottom, + xPosition: .center, contentText: "하루에 한 번 사진을 올릴 수 있어요" ) case .familyNameEdit: return .init( foregroundColor: .bibbiBlack, backgroundColor: .mainYellow, - position: .bottom, + yPosition: .bottom, + xPosition: .right, contentText: "가족 방 이름을 변경해보세요!" ) case .inactiveSurvivalCameraNoUpload: return .init( foregroundColor: .bibbiWhite, backgroundColor: .gray700, - position: .bottom, + yPosition: .bottom, + xPosition: .center, contentText: "생존신고 후 미션 사진을 올릴 수 있어요" ) case .inactiveMissionCameraPostUpload: return .init( foregroundColor: .bibbiWhite, backgroundColor: .gray700, - position: .bottom, + yPosition: .bottom, + xPosition: .center, contentText: "오늘의 미션은 완료되었어요" ) case .inactiveMissionCamera: return .init( foregroundColor: .bibbiWhite, backgroundColor: .gray700, - position: .bottom, + yPosition: .bottom, + xPosition: .center, contentText: "아직 미션 사진을 찍을 수 없어요" ) case .activeMissionCamera: return .init( foregroundColor: .bibbiWhite, backgroundColor: .gray700, - position: .bottom, + yPosition: .bottom, + xPosition: .center, contentText: "미션 사진을 찍으러 가볼까요?" ) case .contributor: return .init( foregroundColor: .bibbiWhite, backgroundColor: .gray700, - position: .top, + yPosition: .top, + xPosition: .midLeft, contentText: "생존신고 횟수가 동일한 경우\n이모지, 댓글 수를 합산해서 등수를 정해요" ) case .monthlyCalendar: return .init( foregroundColor: .bibbiWhite, backgroundColor: .gray700, - position: .top, + yPosition: .top, + xPosition: .midLeft, contentText: "모두가 참여한 날과 업로드한 사진 수로\n이 달의 친밀도를 측정합니다" ) case let .waitingSurvivalImage(contentText, profile): return .init( foregroundColor: .bibbiBlack, backgroundColor: .mainYellow, - position: .bottom, + yPosition: .bottom, + xPosition: .center, contentText: "\(contentText)님 외 \(profile.count - 1)명이 기다리고 있어요" ) } diff --git a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToolTip/BBToolTipView.swift b/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToolTip/BBToolTipView.swift deleted file mode 100644 index 22612f967..000000000 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBCommons/BBToolTip/BBToolTipView.swift +++ /dev/null @@ -1,127 +0,0 @@ -// -// BBToolTipView.swift -// Core -// -// Created by Kim dohyun on 9/13/24. -// - -import UIKit - -import DesignSystem -import Kingfisher -import SnapKit -import Then - - -public final class BBToolTipView: UIView, BBDrawable, BBComponentPresentable { - - //MARK: Properties - private let contentLabel: BBLabel = BBLabel() - private let profileStackView: UIStackView = UIStackView() - public var toolTipType: BBToolTipType = .activeCameraTime { - didSet { - setupToolTipContent() - setupAutoLayout(toolTipType) - setNeedsDisplay() - } - } - - public init() { - super.init(frame: .zero) - setupToolTipUI() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - public override func draw(_ rect: CGRect) { - super.draw(rect) - guard let context = UIGraphicsGetCurrentContext() else { return } - context.saveGState() - drawToolTip(rect, type: toolTipType, context: context) - context.restoreGState() - } - - //MARK: Configure - private func setupToolTipContent() { - - profileStackView.do { - $0.spacing = -4 - $0.distribution = .fillEqually - } - - contentLabel.do { - $0.text = toolTipType.configure.contentText - $0.fontStyle = toolTipType.configure.font - $0.textAlignment = .center - $0.numberOfLines = 0 - $0.textColor = toolTipType.configure.foregroundColor - $0.sizeToFit() - } - - self.do { - $0.backgroundColor = .clear - } - } - - private func setupToolTipUI() { - addSubviews(contentLabel, profileStackView) - } - - - private func setupWaitingToolTipUI(imageURL: [URL]) { - imageURL.forEach { - createProfileImageView(imageURL: $0) - } - } - - private func createProfileImageView(imageURL: URL) { - let imageView = UIImageView(frame: .init(x: 0, y: 0, width: 20, height: 20)) - imageView.contentMode = .scaleAspectFill - imageView.layer.borderColor = UIColor.mainYellow.cgColor - imageView.layer.borderWidth = 2 - imageView.clipsToBounds = true - imageView.layer.cornerRadius = 10 - imageView.kf.setImage(with: imageURL) - profileStackView.addArrangedSubview(imageView) - } - - private func setupAutoLayout(_ type: BBToolTipType) { - let arrowHeight = toolTipType.configure.arrowHeight - let textPadding: CGFloat = 10 - - switch type { - case .monthlyCalendar, .contributor: - contentLabel.snp.remakeConstraints { - $0.left.equalToSuperview().inset(16) - $0.right.equalToSuperview().inset(16) - $0.top.equalToSuperview().inset((arrowHeight + textPadding)) - $0.bottom.equalToSuperview().inset(textPadding) - } - case let .waitingSurvivalImage(_ ,imageURL): - setupWaitingToolTipUI(imageURL: imageURL) - - profileStackView.snp.remakeConstraints { - $0.width.equalTo(24 * imageURL.count) - $0.left.equalToSuperview().offset(16) - $0.height.equalTo(24) - $0.centerY.equalTo(contentLabel) - } - - contentLabel.snp.remakeConstraints { - $0.left.equalTo(profileStackView.snp.right).offset(2) - $0.right.equalToSuperview().inset(16) - $0.bottom.equalToSuperview().inset((arrowHeight + textPadding)) - $0.top.equalToSuperview().inset(textPadding) - } - default: - contentLabel.snp.remakeConstraints { - $0.left.equalToSuperview().inset(16) - $0.right.equalToSuperview().inset(16) - $0.bottom.equalToSuperview().inset((arrowHeight + textPadding)) - $0.top.equalToSuperview().inset(textPadding) - } - } - } -} From 7bb1929c44c9741b1f600973572eca77ba50ede4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=84?= Date: Tue, 24 Dec 2024 15:11:38 +0900 Subject: [PATCH 257/263] =?UTF-8?q?build:=20App=20Version=20Target=201.2.5?= =?UTF-8?q?=EB=A1=9C=20=EC=84=A4=EC=A0=95=ED=95=B4=EC=9A=94=20(#731)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * build: Tuist Project+Templates MARKETING_VERSION 1.2.5로 수정 - CFBundleShortVersionString 1.2.5로 수정 - Bundle+Ext App Key Property Value 1.2.5 Version으로 수정 - BBNetwork App Key Value 1.2.5 Version 으로 수정 * fix: AccountProfileViewController NotificationCenter 이벤트 중복 방출 코드 수정 --- 14th-team5-iOS/App/Project.swift | 2 +- .../ViewControllers/AccountProfileViewController.swift | 7 ++++--- 14th-team5-iOS/Core/Sources/Extensions/Bundle+Ext.swift | 2 +- 14th-team5-iOS/Core/Sources/Trash/BBNetwork/API.swift | 2 +- .../Data/Sources/Repositories/AppRepository.swift | 2 +- Tuist/ProjectDescriptionHelpers/Project+Templates.swift | 2 +- 6 files changed, 9 insertions(+), 8 deletions(-) diff --git a/14th-team5-iOS/App/Project.swift b/14th-team5-iOS/App/Project.swift index 3d188ab46..fd9fea690 100644 --- a/14th-team5-iOS/App/Project.swift +++ b/14th-team5-iOS/App/Project.swift @@ -19,7 +19,7 @@ private let targets: [Target] = [ "CFBundleDisplayName": .string("Bibbi"), "CFBundleVersion": .string("1"), "CFBuildVersion": .string("0"), - "CFBundleShortVersionString": .string("1.2.4"), + "CFBundleShortVersionString": .string("1.2.5"), "UILaunchStoryboardName": .string("LaunchScreen"), "UISupportedInterfaceOrientations": .array([.string("UIInterfaceOrientationPortrait")]), "UIUserInterfaceStyle": .string("Dark"), diff --git a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountProfileViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountProfileViewController.swift index aefdcb320..f664557ff 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountProfileViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountProfileViewController.swift @@ -229,10 +229,11 @@ extension AccountProfileViewController: PHPickerViewControllerDelegate { let itemProvider = results.first?.itemProvider picker.dismiss(animated: true) if let imageProvider = itemProvider, imageProvider.canLoadObject(ofClass: UIImage.self) { - imageProvider.loadObject(ofClass: UIImage.self) { image, error in - guard let photoImage: UIImage = image as? UIImage, + imageProvider.loadObject(ofClass: UIImage.self) { [weak self] image, error in + guard let self = self, + let photoImage: UIImage = image as? UIImage, let originalData: Data = photoImage.jpegData(compressionQuality: 1.0) else { return } - imageProvider.didSelectProfileImageWithProcessing(photo: originalData, error: error) + self.reactor?.action.onNext(.didTapPHAssetsImage(originalData)) } } } diff --git a/14th-team5-iOS/Core/Sources/Extensions/Bundle+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/Bundle+Ext.swift index ab0e4667f..27224aafc 100644 --- a/14th-team5-iOS/Core/Sources/Extensions/Bundle+Ext.swift +++ b/14th-team5-iOS/Core/Sources/Extensions/Bundle+Ext.swift @@ -65,6 +65,6 @@ extension Bundle { } public var xAppKey: String { - "da91623d-fe55-4115-8e14-aa7581761963" + "39d7cdd1-218c-4abd-addb-ab4efff8a369" } } diff --git a/14th-team5-iOS/Core/Sources/Trash/BBNetwork/API.swift b/14th-team5-iOS/Core/Sources/Trash/BBNetwork/API.swift index adc6f6706..a72bba239 100644 --- a/14th-team5-iOS/Core/Sources/Trash/BBNetwork/API.swift +++ b/14th-team5-iOS/Core/Sources/Trash/BBNetwork/API.swift @@ -53,7 +53,7 @@ public enum BibbiAPI { public var value: String { switch self { case let .auth(token): return "Bearer \(token)" - case .xAppKey: return "da91623d-fe55-4115-8e14-aa7581761963" // TODO: - 번들에서 가져오기 + case .xAppKey: return "39d7cdd1-218c-4abd-addb-ab4efff8a369" // 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/Data/Sources/Repositories/AppRepository.swift b/14th-team5-iOS/Data/Sources/Repositories/AppRepository.swift index c8dbfd030..609eb0418 100644 --- a/14th-team5-iOS/Data/Sources/Repositories/AppRepository.swift +++ b/14th-team5-iOS/Data/Sources/Repositories/AppRepository.swift @@ -33,7 +33,7 @@ extension AppRepository { public func fetchAppVersion() -> Observable { // TODO: - xAppKey 불러오는 코드 다시 작성하기 - let appKey = "7b159d28-b106-4b6d-a490-1fd654ce40c2" + let appKey = "39d7cdd1-218c-4abd-addb-ab4efff8a369" return meAPIWorker.fetchAppVersion(appKey: appKey) .map { $0.toDomain() } diff --git a/Tuist/ProjectDescriptionHelpers/Project+Templates.swift b/Tuist/ProjectDescriptionHelpers/Project+Templates.swift index ebeb7e1e9..481c114a5 100644 --- a/Tuist/ProjectDescriptionHelpers/Project+Templates.swift +++ b/Tuist/ProjectDescriptionHelpers/Project+Templates.swift @@ -59,7 +59,7 @@ extension Project { settings: .settings( base: [ "OTHER_LDFLAGS": "$(inherited) -ObjC", - "MARKETING_VERSION": "1.2.4", + "MARKETING_VERSION": "1.2.5", "CURRENT_PROJECT_VERSION": "1", "VERSIONING_SYSTEM": "apple-generic" ], From 558ff159409e7b17ec15f3fc435702ee2519b706 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=84?= Date: Wed, 25 Dec 2024 21:19:30 +0900 Subject: [PATCH 258/263] =?UTF-8?q?fix:=20Core=20Module=20Simulator=20Buil?= =?UTF-8?q?d=EC=8B=9C=20Core=20Module=EC=97=90=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EB=90=9C=20Firebase=20Crashlytics=20Crash=20issue=20=EB=B0=9C?= =?UTF-8?q?=EC=83=9D=20=ED=95=B4=EA=B2=B0=20(#733)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 14th-team5-iOS/Core/Project.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/14th-team5-iOS/Core/Project.swift b/14th-team5-iOS/Core/Project.swift index 29a07792d..a0f2196ca 100644 --- a/14th-team5-iOS/Core/Project.swift +++ b/14th-team5-iOS/Core/Project.swift @@ -13,7 +13,12 @@ private let targets: [Target] = [ layer: .Core, factory: .init( products: .framework(.static), - dependencies: ModuleLayer.Core.dependencies + dependencies: ModuleLayer.Core.dependencies, + settings: .settings( + base: [ + "EXCLUDED_ARCHS[sdk=iphonesimulator*]": "arm64" + ] + ) ) ) ] From da9b6d0f39a578bcc05f2d83326c203867270edf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=84?= Date: Sun, 29 Dec 2024 19:12:04 +0900 Subject: [PATCH 259/263] =?UTF-8?q?fix:=20github=20Action=20=ED=99=98?= =?UTF-8?q?=EA=B2=BD=EC=97=90=EC=84=9C=20FirebaseCrashlytics=20=EC=9D=B4?= =?UTF-8?q?=EC=8A=88=20=EC=88=98=EC=A0=95=ED=95=B4=EC=9A=94=20(#735)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: swift.yml 파일 if 구문 제거 - Fastfile XcodeBuild 인수 전달 Parameters 추가 * fix: swift.yml fix Branch Push Event Trigger 추가 * fix: Fastfile Syntax Error 수정 * feat: Derived data 삭제 명령어 추가 - xcargs Rosetta 환경으로 설정 * fix: Package.swift Firebase Version Upgrade * feat: Tuist Modular+Templates Util Target 추가 - ModuleType+Templates Util 모듈 의존성 추가 - BBLogManger, Mixpanel 폴더 Util 모듈로 이동 * fix: TargetScript+Templates Configuration 환경에 따라 Service Info 가져오는 Script 제거 - ModuleType+Templates FirebaseAnalyticsWithoutAdIdSupport로 변경 * feat: Github Actions make Google Service info 생성 명령어 추가 * fix: Github Actions Google Service Info Path 경로 출력 명령어 추가 * feat: GitHub Action mise 설치 명령어 추가 * fix: uses 키워드 제거 및 오타 수정 * fix: mise version 4.8.1 upgrade - swift.yml mac-os 최신 환경에서 실행 하도록 수정 - xcode-version 최신 환경에서 실행 하도록 수정 * fix: Github Actions Develop Merge 했을때 배포하도록 수정 - Derived Data 삭제하는 명령어 제거 * fix: push 이벤트에 fix 브런치 제거 * fix: ProfileViewController Crashlytics 테스트 코드 제거 --- .github/workflows/swift.yml | 15 ++++++++---- .mise.toml | 2 +- .../AccountProfileViewController.swift | 1 + .../MonthlyCalendarViewController.swift | 3 ++- .../Camera/CameraDisplayViewController.swift | 1 + .../Camera/CameraViewController.swift | 2 +- .../Reactor/CameraDisplayViewReactor.swift | 1 + .../Reactor/InputFamilyLinkReactor.swift | 1 + .../Reactor/JoinFamilyReactor.swift | 1 + .../JoinFamilyViewController.swift | 1 + .../Home/Reactor/MainFamilyViewReactor.swift | 1 + .../Home/Reactor/MainViewReactor.swift | 1 + .../ViewControllers/MainViewController.swift | 1 + .../FamilyNameSettingViewReactor.swift | 1 + .../Reactor/ManagementReactor.swift | 1 + .../FamilyNameSettingViewController.swift | 1 + .../ManagementViewController.swift | 1 + .../OnBoarding/OnBoardingReactor.swift | 1 + .../ViewControllers/PostViewController.swift | 1 + .../Privacy/PrivacyViewController.swift | 1 + .../Privacy/Reactor/PrivacyViewReactor.swift | 1 + .../ProfileFeedPageViewController.swift | 1 + .../Profile/ProfileViewController.swift | 1 + .../Profile/Reactor/ProfileViewReactor.swift | 1 + .../Resign/AccountResignViewController.swift | 1 + .../Reactor/AccountResignViewReactor.swift | 1 + 14th-team5-iOS/Core/Project.swift | 7 +----- .../Sources/Utils/{Logger => }/BBLogger.swift | 0 .../FamilyUserDefaults.swift | 1 + .../AccountRepository/AccountRepository.swift | 1 + .../Reaction/CreateReactionRequest.swift | 1 - 14th-team5-iOS/Util/Project.swift | 24 +++++++++++++++++++ 14th-team5-iOS/Util/Resources/empty.txt | 0 .../Logger/BBAnalyticsLogParametable.swift | 0 .../Sources}/Logger/BBAnalyticsLogType.swift | 1 + .../Sources}/Logger/BBEventAnalyticsLog.swift | 0 .../Sources}/Logger/BBLogManager.swift | 1 + .../Mixpanel/MixPanelService+Account.swift | 0 .../Mixpanel/MixPanelService+Camera.swift | 0 .../Mixpanel/MixPanelService+Family.swift | 0 .../Mixpanel/MixPanelService+Home.swift | 0 .../Sources}/Mixpanel/MixpanelService.swift | 1 + Tuist/Package.swift | 2 +- .../Modular+Templates.swift | 21 +++++++++++++--- .../ModuleType+Templates.swift | 12 +++++++--- .../TargetDependency+Templates.swift | 2 ++ .../TargetScript+Templates.swift | 13 ---------- fastlane/Fastfile | 15 ++++++------ 48 files changed, 105 insertions(+), 41 deletions(-) rename 14th-team5-iOS/Core/Sources/Utils/{Logger => }/BBLogger.swift (100%) create mode 100644 14th-team5-iOS/Util/Project.swift create mode 100644 14th-team5-iOS/Util/Resources/empty.txt rename 14th-team5-iOS/{Core/Sources/Utils => Util/Sources}/Logger/BBAnalyticsLogParametable.swift (100%) rename 14th-team5-iOS/{Core/Sources/Utils => Util/Sources}/Logger/BBAnalyticsLogType.swift (98%) rename 14th-team5-iOS/{Core/Sources/Utils => Util/Sources}/Logger/BBEventAnalyticsLog.swift (100%) rename 14th-team5-iOS/{Core/Sources/Utils => Util/Sources}/Logger/BBLogManager.swift (99%) rename 14th-team5-iOS/{Core/Sources/Utils => Util/Sources}/Mixpanel/MixPanelService+Account.swift (100%) rename 14th-team5-iOS/{Core/Sources/Utils => Util/Sources}/Mixpanel/MixPanelService+Camera.swift (100%) rename 14th-team5-iOS/{Core/Sources/Utils => Util/Sources}/Mixpanel/MixPanelService+Family.swift (100%) rename 14th-team5-iOS/{Core/Sources/Utils => Util/Sources}/Mixpanel/MixPanelService+Home.swift (100%) rename 14th-team5-iOS/{Core/Sources/Utils => Util/Sources}/Mixpanel/MixpanelService.swift (99%) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index c857f7a39..806724090 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -13,7 +13,7 @@ on: jobs: build: - runs-on: macos-13 + runs-on: macos-latest strategy: matrix: xcodebuild-scheme: ['App'] @@ -25,11 +25,10 @@ jobs: with: ruby-version: '3.2.0' - - name: Setup Xcode version uses: maxim-lobanov/setup-xcode@v1 with: - xcode-version: '15.0' + xcode-version: latest-stable - name: Checkout branch uses: actions/checkout@v3 @@ -61,6 +60,15 @@ jobs: - name: Tuist Generate Commnad run: tuist generate + - name: make Google Service Info + run: | + echo "${{ secrets.GOOGLE_INFO_PLIST }}" | base64 --decode > ${{ secrets.GOOGLE_SERVICE_INFO_PLIST_PATH }} + + - name: Verify GoogleService-Info.plist creation + run: | + ls -l ${{ secrets.GOOGLE_SERVICE_INFO_PLIST_PATH }} + echo "GOOGLE_SERVICE_INFO_PLIST_PATH: ${{ secrets.GOOGLE_SERVICE_INFO_PLIST_PATH }}" + - name: fastlane upload_prd_testflight if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/heads/release') }} env: @@ -81,7 +89,6 @@ jobs: WIDGET_NAME: ${{secrets.WIDGET_NAME}} run: fastlane github_action_prd_upload_testflight - - name: fastlane upload_stg_testflight if: ${{ github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'develop' }} env: diff --git a/.mise.toml b/.mise.toml index b5e4598cb..97922179b 100644 --- a/.mise.toml +++ b/.mise.toml @@ -1,2 +1,2 @@ [tools] -tuist = "4.3.4" +tuist = "4.8.1" diff --git a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountProfileViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountProfileViewController.swift index f664557ff..f281ab680 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountProfileViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountProfileViewController.swift @@ -8,6 +8,7 @@ import UIKit import Core import DesignSystem +import Util import Domain import PhotosUI diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/MonthlyCalendarViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/MonthlyCalendarViewController.swift index b76c150fb..a01a5c838 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/MonthlyCalendarViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/MonthlyCalendarViewController.swift @@ -5,8 +5,9 @@ // Created by 김건우 on 12/6/23. // -import Core import UIKit +import Util +import Core import FSCalendar import ReactorKit diff --git a/14th-team5-iOS/App/Sources/Presentation/Camera/CameraDisplayViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Camera/CameraDisplayViewController.swift index b5fceb613..8a695c2aa 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Camera/CameraDisplayViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Camera/CameraDisplayViewController.swift @@ -9,6 +9,7 @@ import UIKit import Core import DesignSystem +import Util import Photos import RxDataSources import RxSwift diff --git a/14th-team5-iOS/App/Sources/Presentation/Camera/CameraViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Camera/CameraViewController.swift index 829c9ce3d..dc14852f0 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Camera/CameraViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Camera/CameraViewController.swift @@ -9,7 +9,7 @@ import AVFoundation import UIKit import Core -import Data +import Util import DesignSystem import ReactorKit import RxSwift diff --git a/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraDisplayViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraDisplayViewReactor.swift index e67e4d733..a44e5b0c7 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraDisplayViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraDisplayViewReactor.swift @@ -9,6 +9,7 @@ import Foundation import Data import Domain +import Util import DesignSystem import ReactorKit import Core 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 16a3d32b7..dc92c3d5a 100644 --- a/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Reactor/InputFamilyLinkReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Reactor/InputFamilyLinkReactor.swift @@ -9,6 +9,7 @@ import Foundation import Domain import Core +import Util import ReactorKit diff --git a/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Reactor/JoinFamilyReactor.swift b/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Reactor/JoinFamilyReactor.swift index 8a8c8f412..ec3aec844 100644 --- a/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Reactor/JoinFamilyReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Reactor/JoinFamilyReactor.swift @@ -8,6 +8,7 @@ import Core import Domain import Foundation +import Util import ReactorKit diff --git a/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/ViewController/JoinFamilyViewController.swift b/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/ViewController/JoinFamilyViewController.swift index ed373ab94..d64684514 100644 --- a/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/ViewController/JoinFamilyViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/ViewController/JoinFamilyViewController.swift @@ -9,6 +9,7 @@ import UIKit import Core import DesignSystem +import Util import ReactorKit import RxCocoa diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainFamilyViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainFamilyViewReactor.swift index 5264a8b67..cc1e0307b 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainFamilyViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainFamilyViewReactor.swift @@ -9,6 +9,7 @@ import Foundation import Core import Domain +import Util import ReactorKit diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift index 96edf8693..e6c3d57a2 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift @@ -10,6 +10,7 @@ import UIKit import Core import Domain import DesignSystem +import Util import ReactorKit import RxDataSources 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 defd104c8..38c526752 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift @@ -10,6 +10,7 @@ import UIKit import Core import Domain import DesignSystem +import Util import RxDataSources import RxCocoa 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 67a250d72..1f7084c60 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Management/Reactor/FamilyNameSettingViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Management/Reactor/FamilyNameSettingViewReactor.swift @@ -8,6 +8,7 @@ import Foundation import Core +import Util import Domain import ReactorKit 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 e9b5226b3..dde2cefd7 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Management/Reactor/ManagementReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Management/Reactor/ManagementReactor.swift @@ -8,6 +8,7 @@ import Core import DesignSystem import Domain +import Util import UIKit import ReactorKit diff --git a/14th-team5-iOS/App/Sources/Presentation/Management/ViewController/FamilyNameSettingViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Management/ViewController/FamilyNameSettingViewController.swift index 709723769..ed35cbee4 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Management/ViewController/FamilyNameSettingViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Management/ViewController/FamilyNameSettingViewController.swift @@ -6,6 +6,7 @@ // import Core +import Util import DesignSystem import UIKit diff --git a/14th-team5-iOS/App/Sources/Presentation/Management/ViewController/ManagementViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Management/ViewController/ManagementViewController.swift index 6a45c762e..2252ad730 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Management/ViewController/ManagementViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Management/ViewController/ManagementViewController.swift @@ -8,6 +8,7 @@ import Core import DesignSystem import UIKit +import Util import ReactorKit import RxCocoa diff --git a/14th-team5-iOS/App/Sources/Presentation/OnBoarding/OnBoardingReactor.swift b/14th-team5-iOS/App/Sources/Presentation/OnBoarding/OnBoardingReactor.swift index 75ff0d406..4f08dce5b 100644 --- a/14th-team5-iOS/App/Sources/Presentation/OnBoarding/OnBoardingReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/OnBoarding/OnBoardingReactor.swift @@ -11,6 +11,7 @@ import UIKit import Core import Data import Domain +import Util import ReactorKit diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/PostViewController.swift b/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/PostViewController.swift index e1cc4e77f..13b641abe 100644 --- a/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/PostViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/PostViewController.swift @@ -8,6 +8,7 @@ import UIKit import Core import Domain +import Util import RxDataSources import RxSwift diff --git a/14th-team5-iOS/App/Sources/Presentation/Privacy/PrivacyViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Privacy/PrivacyViewController.swift index d2216a9cd..138c90dc8 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Privacy/PrivacyViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Privacy/PrivacyViewController.swift @@ -8,6 +8,7 @@ import UIKit import Core +import Util import DesignSystem import RxSwift import RxCocoa diff --git a/14th-team5-iOS/App/Sources/Presentation/Privacy/Reactor/PrivacyViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Privacy/Reactor/PrivacyViewReactor.swift index a9cb2741c..875a01fb3 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Privacy/Reactor/PrivacyViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Privacy/Reactor/PrivacyViewReactor.swift @@ -10,6 +10,7 @@ import Foundation import Core import Domain import Data +import Util import ReactorKit public final class PrivacyViewReactor: Reactor { diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileFeedPageViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileFeedPageViewController.swift index 2beeba523..a727e8708 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileFeedPageViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileFeedPageViewController.swift @@ -9,6 +9,7 @@ import UIKit import Core import RxSwift +import Util import ReactorKit import RxCocoa diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileViewController.swift index 103f03c6f..b3d5bed07 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileViewController.swift @@ -8,6 +8,7 @@ import UIKit import Core +import Util import DesignSystem import Kingfisher import PhotosUI diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift index bee7eebe7..12afee652 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift @@ -9,6 +9,7 @@ import Foundation import Core import Domain +import Util import ReactorKit diff --git a/14th-team5-iOS/App/Sources/Presentation/Resign/AccountResignViewController.swift b/14th-team5-iOS/App/Sources/Presentation/Resign/AccountResignViewController.swift index c280927b6..9f9e5332b 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Resign/AccountResignViewController.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Resign/AccountResignViewController.swift @@ -9,6 +9,7 @@ import UIKit import Core import DesignSystem +import Util import RxCocoa import ReactorKit import RxDataSources diff --git a/14th-team5-iOS/App/Sources/Presentation/Resign/Reactor/AccountResignViewReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Resign/Reactor/AccountResignViewReactor.swift index 70fc6f118..9f53964b4 100644 --- a/14th-team5-iOS/App/Sources/Presentation/Resign/Reactor/AccountResignViewReactor.swift +++ b/14th-team5-iOS/App/Sources/Presentation/Resign/Reactor/AccountResignViewReactor.swift @@ -9,6 +9,7 @@ import Foundation import Core import Domain +import Util import ReactorKit import RxSwift diff --git a/14th-team5-iOS/Core/Project.swift b/14th-team5-iOS/Core/Project.swift index a0f2196ca..29a07792d 100644 --- a/14th-team5-iOS/Core/Project.swift +++ b/14th-team5-iOS/Core/Project.swift @@ -13,12 +13,7 @@ private let targets: [Target] = [ layer: .Core, factory: .init( products: .framework(.static), - dependencies: ModuleLayer.Core.dependencies, - settings: .settings( - base: [ - "EXCLUDED_ARCHS[sdk=iphonesimulator*]": "arm64" - ] - ) + dependencies: ModuleLayer.Core.dependencies ) ) ] diff --git a/14th-team5-iOS/Core/Sources/Utils/Logger/BBLogger.swift b/14th-team5-iOS/Core/Sources/Utils/BBLogger.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Utils/Logger/BBLogger.swift rename to 14th-team5-iOS/Core/Sources/Utils/BBLogger.swift diff --git a/14th-team5-iOS/Data/Sources/Storages/UserDefaults/FamilyUserDefaults/FamilyUserDefaults.swift b/14th-team5-iOS/Data/Sources/Storages/UserDefaults/FamilyUserDefaults/FamilyUserDefaults.swift index 2e46e00f4..36e145a44 100644 --- a/14th-team5-iOS/Data/Sources/Storages/UserDefaults/FamilyUserDefaults/FamilyUserDefaults.swift +++ b/14th-team5-iOS/Data/Sources/Storages/UserDefaults/FamilyUserDefaults/FamilyUserDefaults.swift @@ -9,6 +9,7 @@ import Foundation import Core import Domain +import Util public protocol FamilyInfoUserDefaultsType: UserDefaultsType { diff --git a/14th-team5-iOS/Data/Sources/Trash/Account/AccountRepository/AccountRepository.swift b/14th-team5-iOS/Data/Sources/Trash/Account/AccountRepository/AccountRepository.swift index d38200909..88eb4fba2 100644 --- a/14th-team5-iOS/Data/Sources/Trash/Account/AccountRepository/AccountRepository.swift +++ b/14th-team5-iOS/Data/Sources/Trash/Account/AccountRepository/AccountRepository.swift @@ -12,6 +12,7 @@ import UIKit import ReactorKit import RxCocoa import RxSwift +import Util public enum AccountLoaction { case profile diff --git a/14th-team5-iOS/Domain/Sources/Entities/Reaction/CreateReactionRequest.swift b/14th-team5-iOS/Domain/Sources/Entities/Reaction/CreateReactionRequest.swift index b433b8e94..08f19d26f 100644 --- a/14th-team5-iOS/Domain/Sources/Entities/Reaction/CreateReactionRequest.swift +++ b/14th-team5-iOS/Domain/Sources/Entities/Reaction/CreateReactionRequest.swift @@ -6,7 +6,6 @@ // import Foundation -import Core public struct CreateReactionRequest { public let emojiId: String diff --git a/14th-team5-iOS/Util/Project.swift b/14th-team5-iOS/Util/Project.swift new file mode 100644 index 000000000..c4b0da2d8 --- /dev/null +++ b/14th-team5-iOS/Util/Project.swift @@ -0,0 +1,24 @@ +// +// Project.swift +// 14th-team5-iOSManifests +// +// Created by 김도현 on 12/26/24. +// + +import ProjectDescription +import ProjectDescriptionHelpers + + +private let targets: [Target] = [ + .makeModular( + layer: .Util, + factory: .init( + products: .framework(.static), + dependencies: ModuleLayer.Util.dependencies + ) + ) +] + +private let util = Project.makeApp(name: ModuleLayer.Util.rawValue, target: targets) + + diff --git a/14th-team5-iOS/Util/Resources/empty.txt b/14th-team5-iOS/Util/Resources/empty.txt new file mode 100644 index 000000000..e69de29bb diff --git a/14th-team5-iOS/Core/Sources/Utils/Logger/BBAnalyticsLogParametable.swift b/14th-team5-iOS/Util/Sources/Logger/BBAnalyticsLogParametable.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Utils/Logger/BBAnalyticsLogParametable.swift rename to 14th-team5-iOS/Util/Sources/Logger/BBAnalyticsLogParametable.swift diff --git a/14th-team5-iOS/Core/Sources/Utils/Logger/BBAnalyticsLogType.swift b/14th-team5-iOS/Util/Sources/Logger/BBAnalyticsLogType.swift similarity index 98% rename from 14th-team5-iOS/Core/Sources/Utils/Logger/BBAnalyticsLogType.swift rename to 14th-team5-iOS/Util/Sources/Logger/BBAnalyticsLogType.swift index 2983502e9..562612413 100644 --- a/14th-team5-iOS/Core/Sources/Utils/Logger/BBAnalyticsLogType.swift +++ b/14th-team5-iOS/Util/Sources/Logger/BBAnalyticsLogType.swift @@ -6,6 +6,7 @@ // import Foundation +import Core public protocol BBAnalyticsLogType { var name: String { get } diff --git a/14th-team5-iOS/Core/Sources/Utils/Logger/BBEventAnalyticsLog.swift b/14th-team5-iOS/Util/Sources/Logger/BBEventAnalyticsLog.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Utils/Logger/BBEventAnalyticsLog.swift rename to 14th-team5-iOS/Util/Sources/Logger/BBEventAnalyticsLog.swift diff --git a/14th-team5-iOS/Core/Sources/Utils/Logger/BBLogManager.swift b/14th-team5-iOS/Util/Sources/Logger/BBLogManager.swift similarity index 99% rename from 14th-team5-iOS/Core/Sources/Utils/Logger/BBLogManager.swift rename to 14th-team5-iOS/Util/Sources/Logger/BBLogManager.swift index 7ee49ae13..07b653044 100644 --- a/14th-team5-iOS/Core/Sources/Utils/Logger/BBLogManager.swift +++ b/14th-team5-iOS/Util/Sources/Logger/BBLogManager.swift @@ -7,6 +7,7 @@ import FirebaseCrashlytics import FirebaseAnalytics +import Core public enum BBLogManager { diff --git a/14th-team5-iOS/Core/Sources/Utils/Mixpanel/MixPanelService+Account.swift b/14th-team5-iOS/Util/Sources/Mixpanel/MixPanelService+Account.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Utils/Mixpanel/MixPanelService+Account.swift rename to 14th-team5-iOS/Util/Sources/Mixpanel/MixPanelService+Account.swift diff --git a/14th-team5-iOS/Core/Sources/Utils/Mixpanel/MixPanelService+Camera.swift b/14th-team5-iOS/Util/Sources/Mixpanel/MixPanelService+Camera.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Utils/Mixpanel/MixPanelService+Camera.swift rename to 14th-team5-iOS/Util/Sources/Mixpanel/MixPanelService+Camera.swift diff --git a/14th-team5-iOS/Core/Sources/Utils/Mixpanel/MixPanelService+Family.swift b/14th-team5-iOS/Util/Sources/Mixpanel/MixPanelService+Family.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Utils/Mixpanel/MixPanelService+Family.swift rename to 14th-team5-iOS/Util/Sources/Mixpanel/MixPanelService+Family.swift diff --git a/14th-team5-iOS/Core/Sources/Utils/Mixpanel/MixPanelService+Home.swift b/14th-team5-iOS/Util/Sources/Mixpanel/MixPanelService+Home.swift similarity index 100% rename from 14th-team5-iOS/Core/Sources/Utils/Mixpanel/MixPanelService+Home.swift rename to 14th-team5-iOS/Util/Sources/Mixpanel/MixPanelService+Home.swift diff --git a/14th-team5-iOS/Core/Sources/Utils/Mixpanel/MixpanelService.swift b/14th-team5-iOS/Util/Sources/Mixpanel/MixpanelService.swift similarity index 99% rename from 14th-team5-iOS/Core/Sources/Utils/Mixpanel/MixpanelService.swift rename to 14th-team5-iOS/Util/Sources/Mixpanel/MixpanelService.swift index a28abb49c..7e3de3203 100644 --- a/14th-team5-iOS/Core/Sources/Utils/Mixpanel/MixpanelService.swift +++ b/14th-team5-iOS/Util/Sources/Mixpanel/MixpanelService.swift @@ -7,6 +7,7 @@ import Foundation import Mixpanel +import Core // MARK: Mixpanel Protocol protocol MixpanelTrackable { diff --git a/Tuist/Package.swift b/Tuist/Package.swift index 7200682b6..a1dcf1a07 100644 --- a/Tuist/Package.swift +++ b/Tuist/Package.swift @@ -23,7 +23,7 @@ let package = Package( .package(url: "https://github.com/ReactiveX/RxSwift.git", from: "6.6.0"), .package(url: "https://github.com/SnapKit/SnapKit.git", from: "5.6.0"), .package(url: "https://github.com/devxoul/Then.git", from: "3.0.0"), - .package(url: "https://github.com/firebase/firebase-ios-sdk.git", from: "10.24.0"), + .package(url: "https://github.com/firebase/firebase-ios-sdk.git", from: "10.25.0"), .package(url: "https://github.com/RxSwiftCommunity/RxDataSources.git", from: "5.0.0"), .package(url: "https://github.com/kakao/kakao-ios-sdk.git", from: "2.22.0"), .package(url: "https://github.com/kakao/kakao-ios-sdk-rx.git", from: "2.22.0"), diff --git a/Tuist/ProjectDescriptionHelpers/Modular+Templates.swift b/Tuist/ProjectDescriptionHelpers/Modular+Templates.swift index 3485ca015..913c50230 100644 --- a/Tuist/ProjectDescriptionHelpers/Modular+Templates.swift +++ b/Tuist/ProjectDescriptionHelpers/Modular+Templates.swift @@ -92,7 +92,7 @@ extension Target { sources: factory.sources, resources: factory.resources, entitlements: factory.entitlements, - scripts: [.firebaseInfoByConfiguration, .firebaseCrashlytics], + scripts: [.firebaseCrashlytics], dependencies: factory.dependencies, settings: factory.settings ) @@ -115,7 +115,7 @@ extension Target { return .target( name: layer.rawValue, destinations: .iOS, - product: .staticFramework, + product: factory.products.isFramework ? .staticFramework : .framework, bundleId: "com.\(layer.rawValue).project".lowercased(), deploymentTargets: factory.deploymentTargets, infoPlist: factory.infoPlist, @@ -125,11 +125,26 @@ extension Target { dependencies: factory.dependencies, settings: factory.settings ) + case .Util: + return .target( + name: layer.rawValue, + destinations: .iOS, + product: factory.products.isFramework ? .staticFramework : .framework, + bundleId: "com.\(layer.rawValue).project".lowercased(), + deploymentTargets: factory.deploymentTargets, + infoPlist: factory.infoPlist, + sources: factory.sources, + resources: factory.resources, + entitlements: factory.entitlements, + dependencies: factory.dependencies, + settings: factory.settings + ) + case .Core: return .target( name: layer.rawValue, destinations: .iOS, - product: .staticFramework, + product: factory.products.isLibrary ? .framework : .staticFramework, bundleId: "com.\(layer.rawValue).project".lowercased(), deploymentTargets: factory.deploymentTargets, infoPlist: factory.infoPlist, diff --git a/Tuist/ProjectDescriptionHelpers/ModuleType+Templates.swift b/Tuist/ProjectDescriptionHelpers/ModuleType+Templates.swift index f0eb1198c..b4b4d1969 100644 --- a/Tuist/ProjectDescriptionHelpers/ModuleType+Templates.swift +++ b/Tuist/ProjectDescriptionHelpers/ModuleType+Templates.swift @@ -32,6 +32,7 @@ public enum ModuleLayer: String, CaseIterable, ModuleType { case App case Data case Domain + case Util case Core case DesignSystem @@ -44,15 +45,22 @@ public enum ModuleLayer: String, CaseIterable, ModuleType { .external(name: "FirebaseMessaging"), .external(name: "Mixpanel"), .external(name: "RxDataSources"), - .with(.Core), + .with(.Util), .with(.Data), .external(name: "ReactorKit"), .external(name: "Lottie"), .external(name: "Macros") ] + case .Util: + return [ + .external(name: "FirebaseAnalyticsWithoutAdIdSupport"), + .external(name: "FirebaseCrashlytics"), + .with(.Core) + ] case .Data: return [ .with(.Domain), + .with(.Util), .external(name: "Alamofire"), .external(name: "KakaoSDK"), .external(name: "RxKakaoSDK"), @@ -67,8 +75,6 @@ public enum ModuleLayer: String, CaseIterable, ModuleType { case .Core: return [ .with(.DesignSystem), - .external(name: "FirebaseAnalytics", condition: .when(.all)), - .external(name: "FirebaseCrashlytics", condition: .when(.all)), .external(name: "SnapKit", condition: .when(.all)), .external(name: "Then", condition: .when(.all)), .external(name: "Kingfisher", condition: .when(.all)), diff --git a/Tuist/ProjectDescriptionHelpers/TargetDependency+Templates.swift b/Tuist/ProjectDescriptionHelpers/TargetDependency+Templates.swift index cd3549e51..6b891cdb6 100644 --- a/Tuist/ProjectDescriptionHelpers/TargetDependency+Templates.swift +++ b/Tuist/ProjectDescriptionHelpers/TargetDependency+Templates.swift @@ -18,6 +18,8 @@ extension TargetDependency { return .project(target: moduleName, path: .relativeToRoot("14th-team5-iOS/\(moduleName)")) case .Domain: return .project(target: moduleName, path: .relativeToRoot("14th-team5-iOS/\(moduleName)")) + case .Util: + return .project(target: moduleName, path: .relativeToRoot("14th-team5-iOS/\(moduleName)")) case .Core: return .project(target: moduleName, path: .relativeToRoot("14th-team5-iOS/\(moduleName)")) case .DesignSystem: diff --git a/Tuist/ProjectDescriptionHelpers/TargetScript+Templates.swift b/Tuist/ProjectDescriptionHelpers/TargetScript+Templates.swift index 0971d746a..88c281502 100644 --- a/Tuist/ProjectDescriptionHelpers/TargetScript+Templates.swift +++ b/Tuist/ProjectDescriptionHelpers/TargetScript+Templates.swift @@ -25,17 +25,4 @@ public extension TargetScript { ], basedOnDependencyAnalysis: false ) - - static let firebaseInfoByConfiguration = TargetScript.post( - script: """ - case "${CONFIGURATION}" in - "PRD" ) - cp -r "$SRCROOT/Resources/GoogleService-Info.plist" "${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/GoogleService-Info.plist" - ;; - *) - esac - """, - name: "Firebase Info copy by Configuration", - basedOnDependencyAnalysis: false - ) } diff --git a/fastlane/Fastfile b/fastlane/Fastfile index f64b4173f..3ebee1f1d 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -96,7 +96,8 @@ platform :ios do "com.5ing.bibbi" => "match AppStore com.5ing.bibbi", "com.5ing.bibbi.widget" => "match AppStore com.5ing.bibbi.widget" } - } + }, + xcargs: "arch=x86_64" ) upload_to_testflight(skip_waiting_for_build_processing: true) @@ -106,8 +107,8 @@ platform :ios do message: "⭐️ 테스트 플라이트 업로드 성공", channel: "#알림-ios-배포", slack_url: "#{SLACK_HOOK_URL}", - username: "대신 배포해주는 고양이", - icon_url: "https://avatars.githubusercontent.com/u/21079970?v=4", + username: "Bibbi bot", + icon_url: "https://avatars.githubusercontent.com/u/160627812?s=200&v=4", payload: { "Build Date" => Time.new.to_s, }, @@ -209,8 +210,8 @@ platform :ios do message: "⭐️ 테스트 플라이트 업로드 성공", channel: "#알림-ios-배포", slack_url: "#{SLACK_HOOK_URL}", - username: "대신 배포해주는 고양이", - icon_url: "https://avatars.githubusercontent.com/u/21079970?v=4", + username: "Bibbi bot", + icon_url: "https://avatars.githubusercontent.com/u/160627812?s=200&v=4", payload: { "Build Date" => Time.new.to_s, }, @@ -358,8 +359,8 @@ platform :ios do message: "❗️ 테스트 플라이트 업로드 실패", channel: "#알림-ios-배포", slack_url: "#{SLACK_HOOK_URL}", - username: "대신 배포해주는 고양이", - icon_url: "https://avatars.githubusercontent.com/u/21079970?v=4", + username: "Bibbi bot", + icon_url: "https://avatars.githubusercontent.com/u/160627812?s=200&v=4", success: false, attachment_properties: { fields: [ From b9396a3f6fd04e05acd0ef8f3d0471a05b867e61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=84?= Date: Sun, 29 Dec 2024 21:35:24 +0900 Subject: [PATCH 260/263] =?UTF-8?q?fix:=20github=20Action=20PRD=20Lane=20v?= =?UTF-8?q?ersion=20number=20Method=20=EC=A0=9C=EA=B1=B0=20(#738)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastlane/Fastfile | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 3ebee1f1d..9b0727756 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -157,11 +157,6 @@ platform :ios do new_build_number = build_num + 1 - version = get_version_number( - xcodeproj: "14th-team5-iOS/App/App.xcodeproj", - target: "#{PRD_SCHEME}" - ) - increment_build_number( xcodeproj: "14th-team5-iOS/App/App.xcodeproj", @@ -189,18 +184,8 @@ platform :ios do "com.5ing.bibbi" => "match AppStore com.5ing.bibbi", "com.5ing.bibbi.widget" => "match AppStore com.5ing.bibbi.widget" } - } - ) - - download_dsyms( - app_identifier: "com.5ing.bibbi", - version: version, - build_number: new_build_number, - output_directory: "./14th-team5-iOS" - ) - - upload_symbols_to_crashlytics( - gsp_path: "14th-team5-iOS/App/Resources/GoogleService-Info.plist" + }, + xcargs: "arch=x86_64" ) upload_to_testflight(skip_waiting_for_build_processing: true) From fae4f9f4e53f323c0bcf00defff6feceb76b013e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=84?= Date: Sun, 29 Dec 2024 22:44:13 +0900 Subject: [PATCH 261/263] =?UTF-8?q?Revert=20"fix:=20Core=20Module=20Simula?= =?UTF-8?q?tor=20Build=EC=8B=9C=20Core=20Module=EC=97=90=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EB=90=9C=20Firebase=20Crashlytics=20Crash=20issue=20?= =?UTF-8?q?=EB=B0=9C=EC=83=9D=20=ED=95=B4=EA=B2=B0=20(#733)"=20(#739)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 558ff159409e7b17ec15f3fc435702ee2519b706. From 390184ae34068f3a290956e74296729bb3827e2b Mon Sep 17 00:00:00 2001 From: g_mi <62610032+akrudal@users.noreply.github.com> Date: Tue, 14 Jan 2025 23:26:42 +0900 Subject: [PATCH 262/263] =?UTF-8?q?fix:=20iOS=2017=EB=B2=84=EC=A0=84?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=95=B1=201.2.5=EA=B0=80=20=ED=8A=95?= =?UTF-8?q?=EA=B8=B0=EB=8A=94=20=ED=98=84=EC=83=81=EC=9D=84=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=ED=95=B4=EC=9A=94=20(#745)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Revert "fix: Core Module Simulator Build시 Core Module에 의존된 Firebase Crashlytics Crash issue 발생 해결 (#733)" This reverts commit 558ff159409e7b17ec15f3fc435702ee2519b706. * rename app to bibbi * rename app to bibbi * fix: App-Key 1.2.6 버전으로 수정 --------- Co-authored-by: Do-hyun-Kim --- 14th-team5-iOS/{App => Bibbi}/Project.swift | 12 +- .../AppIcon.appiconset/100.png | Bin .../AppIcon.appiconset/1024.png | Bin .../AppIcon.appiconset/114.png | Bin .../AppIcon.appiconset/120.png | Bin .../AppIcon.appiconset/128.png | Bin .../AppIcon.appiconset/144.png | Bin .../AppIcon.appiconset/152.png | Bin .../Assets.xcassets/AppIcon.appiconset/16.png | Bin .../AppIcon.appiconset/167.png | Bin .../AppIcon.appiconset/172.png | Bin .../AppIcon.appiconset/180.png | Bin .../AppIcon.appiconset/196.png | Bin .../Assets.xcassets/AppIcon.appiconset/20.png | Bin .../AppIcon.appiconset/216.png | Bin .../AppIcon.appiconset/256.png | Bin .../Assets.xcassets/AppIcon.appiconset/29.png | Bin .../Assets.xcassets/AppIcon.appiconset/32.png | Bin .../Assets.xcassets/AppIcon.appiconset/40.png | Bin .../Assets.xcassets/AppIcon.appiconset/48.png | Bin .../Assets.xcassets/AppIcon.appiconset/50.png | Bin .../AppIcon.appiconset/512.png | Bin .../Assets.xcassets/AppIcon.appiconset/55.png | Bin .../Assets.xcassets/AppIcon.appiconset/57.png | Bin .../Assets.xcassets/AppIcon.appiconset/58.png | Bin .../Assets.xcassets/AppIcon.appiconset/60.png | Bin .../Assets.xcassets/AppIcon.appiconset/64.png | Bin .../Assets.xcassets/AppIcon.appiconset/66.png | Bin .../Assets.xcassets/AppIcon.appiconset/72.png | Bin .../Assets.xcassets/AppIcon.appiconset/76.png | Bin .../Assets.xcassets/AppIcon.appiconset/80.png | Bin .../Assets.xcassets/AppIcon.appiconset/87.png | Bin .../Assets.xcassets/AppIcon.appiconset/88.png | Bin .../Assets.xcassets/AppIcon.appiconset/92.png | Bin .../AppIcon.appiconset/Contents.json | 0 .../Resources/Assets.xcassets/Contents.json | 0 .../splash.imageset/Contents.json | 0 .../splash.imageset/iOSSplash.svg | 0 .../Resources/GoogleService-Info.plist | 0 .../Resources/LaunchScreen.storyboard | 0 .../Resources/PrivacyInfo.xcprivacy | 0 .../{App => Bibbi}/Resources/empty.txt | 0 .../{App => Bibbi}/Resources/fire.json | 0 .../{App => Bibbi}/Resources/loading.json | 0 .../Sources/Application/AppDelegate.swift | 0 .../DIContainer/AppDIContainer.swift | 0 .../DIContainer/CalendarDIContainer.swift | 0 .../DIContainer/CameraDIContainer.swift | 0 .../DIContainer/CommentDIContainer.swift | 0 .../DIContainer/FamilyDIContainer.swift | 0 .../DIContainer/MainViewDIContainer.swift | 0 .../DIContainer/MemberDIContainer.swift | 0 .../DIContainer/MissionDIContainer.swift | 0 .../DIContainer/MyDIContainer.swift | 0 .../DIContainer/NavigatorDIContainer.swift | 0 .../DIContainer/OAuthDIContainer.swift | 0 .../DIContainer/PickDICotainer.swift | 0 .../DIContainer/PostDIContainer.swift | 0 .../DIContainer/PrivacyDIContainer.swift | 0 .../DIContainer/ProfileDIContainer.swift | 0 .../DIContainer/ReactionDIContainer.swift | 0 .../DIContainer/RealEmojiDIContainer.swift | 0 .../DIContainer/ResignDIContainer.swift | 0 .../DIContainer/SignInDIContainer.swift | 0 .../DIContainer/SignOutDIContainer.swift | 0 .../Navigator/AccountProfileNavigator.swift | 0 .../Navigator/AccountResignNavigator.swift | 0 .../Navigator/AccountSignInNavigator.swift | 0 .../Application/Navigator/AppNavigator.swift | 0 .../Navigator/CameraDisplayNavigator.swift | 0 .../Navigator/CameraNavigator.swift | 0 .../Navigator/CommentNavigator.swift | 0 .../Navigator/DailyCalendarNavigator.swift | 0 .../Navigator/FamilyEntranceNavigator.swift | 0 .../Application/Navigator/HomeNavigator.swift | 0 .../Navigator/InputFamilyLinkNavigator.swift | 0 .../Navigator/JoinFamilyNavigator.swift | 0 .../Application/Navigator/MainNavigator.swift | 0 .../Navigator/ManagementNavigator.swift | 0 .../Navigator/MonthlyCalendarNavigator.swift | 0 .../Navigator/OnboardingNavigator.swift | 0 .../Navigator/PrivacyNavigator.swift | 0 .../Navigator/ProfileNavigator.swift | 0 .../Navigator/SplashNavigator.swift | 0 .../DailyCalendarViewControllerWrapper.swift | 0 ...MonthlyCalendarViewControllerWrapper.swift | 0 .../CameraDisplayViewControllerWrapper.swift | 0 .../Camera/CameraViewControllerWrapper.swift | 0 .../CommentViewControllerWrapper.swift | 0 .../FamilyEntranceControllerWrapper.swift | 0 ...InputFamilyLinkViewControllerWrapper.swift | 0 .../JoinFamilyViewControllerWrapper.swift | 0 .../MainFamilyViewControllerWrapper.swift | 0 .../Main/MainPostViewControllerWrapper.swift | 0 .../Main/MainViewControllerWrapper.swift | 0 ...milyNameSettingViewControllerWrapper.swift | 0 .../ManagementViewControllerWrapper.swift | 0 .../OnboardingViewControllerWrapper.swift | 0 .../PostDetail/PostDetailCellWrapper.swift | 0 .../PostDetailViewControllerWrapper.swift | 0 ...ReactionMembersViewControllerWrapper.swift | 0 .../ReactionViewControllerWrapper.swift | 0 ...SelectableEmojiViewControllerWrapper.swift | 0 .../PrivacyViewControllerWrapper.swift | 0 .../ProfileDetailViewControllerWrapper.swift | 0 ...ProfileFeedPageViewControllerWrapper.swift | 0 .../ProfileFeedViewControllerWrapper.swift | 0 .../ProfileViewControllerWrapper.swift | 0 .../AccountResignViewControllerWrapper.swift | 0 .../SignIn/SignInViewControllerWrapper.swift | 0 .../Splash/SplashViewControllerWrapper.swift | 0 .../WebContentViewControllerWrapper.swift | 0 .../Sources/Application/SceneDelegate.swift | 0 .../Sources/Font/Pretendard-Black.otf | Bin .../Sources/Font/Pretendard-Bold.otf | Bin .../Sources/Font/Pretendard-ExtraBold.otf | Bin .../Sources/Font/Pretendard-ExtraLight.otf | Bin .../Sources/Font/Pretendard-Light.otf | Bin .../Sources/Font/Pretendard-Medium.otf | Bin .../Sources/Font/Pretendard-Regular.otf | Bin .../Sources/Font/Pretendard-SemiBold.otf | Bin .../Sources/Font/Pretendard-Thin.otf | Bin .../AccountSignInDIContainer.swift | 0 .../AccountSignIn/AccountSignInReactor.swift | 0 .../AccountSignIn/AccountSignInStrings.swift | 0 .../AccountSignInViewController.swift | 0 .../AccountSignUp/AccountSignUpStrings.swift | 0 .../AccountSignUpDIContainer.swift | 0 .../Reactor/AccountSignUpReactor.swift | 0 .../AccountDateViewController.swift | 0 .../AccountNicknameViewController.swift | 0 .../AccountProfileViewController.swift | 0 .../AccountSignUpViewController.swift | 0 .../Config/CalendarImageCell+Type.swift | 0 .../DailyCalendarSectionModel.swift | 0 .../MonthlyCalendarSectionModel.swift | 0 .../MemoriesCalendarPostHeaderDelegate.swift | 0 .../RxFSCalendarDelegateProxy.swift | 0 .../RxMemoriesCalendarPostDelegateProxy.swift | 0 .../Cell/MemoriesCalendarCellReactor.swift | 0 .../Cell/MemoriesCalendarPageReactor.swift | 0 .../MemoriesCalendarPostCellReactor.swift | 0 .../Reactor/DailyCalendarViewReactor.swift | 0 .../MemoriesCalendarPostHeaderReactor.swift | 0 .../MemoriesCalendarPostImageReactor.swift | 0 .../MemoriesCalendarTitleViewReactor.swift | 0 .../Reactor/MonthlyCalendarViewReactor.swift | 0 .../Calendar/View/BannerView.swift | 0 .../View/Cell/MemoriesCalendarCell.swift | 0 .../Cell/MemoriesCalendarPageViewCell.swift | 0 .../MemoriesCalendarPlaceholderCell.swift | 0 .../View/Cell/MemoriesCalendarPostCell.swift | 0 .../View/MemoriesCalendarPageTitleView.swift | 0 .../View/MemoriesCalendarPostHeaderView.swift | 0 .../View/MemoriesCalendarPostImageView.swift | 0 .../BannerHostingViewController.swift | 0 .../DailyCalendarViewController.swift | 0 .../MonthlyCalendarViewController.swift | 0 .../Calendar/ViewModel/BannerViewModel.swift | 0 .../Camera/CameraDisplayViewController.swift | 0 .../Camera/CameraViewController.swift | 0 .../Camera/Cell/BibbiRealEmojiViewCell.swift | 0 .../BibbiRealEmojiCellReactor.swift | 0 .../CellReactor/DisplayEditCellReactor.swift | 0 .../Cell/DisplayEditCollectionViewCell.swift | 0 .../DataSources/DisplayEditSectionModel.swift | 0 .../DataSources/EmojiSectionModel.swift | 0 .../Reactor/CameraDisplayViewReactor.swift | 0 .../Camera/Reactor/CameraViewReactor.swift | 0 .../Camera/View/BibbiMissionView.swift | 0 .../DataSource/CommentSectionModel.swift | 0 .../Delegate/CommentTextFieldDelegate.swift | 0 .../RxCommentTextFieldDelegateProxy.swift | 0 .../Reactor/Cell/CommentCellReactor.swift | 0 .../Comment/Reactor/CommentTableReactor.swift | 0 .../Reactor/CommentTextFieldReactor.swift | 0 .../Comment/Reactor/CommentViewReactor.swift | 0 .../Comment/View/Cell/CommentCell.swift | 0 .../Comment/View/CommentTableView.swift | 0 .../Comment/View/CommentTextFieldView.swift | 0 .../Comment/View/CommentTopBarView.swift | 0 .../Comment/View/NoneCommentView.swift | 0 .../CommentViewController.swift | 0 .../Dependency/WebContentDIContainer.swift | 0 .../Content/Reactor/WebContentReactor.swift | 0 .../Content/WebContentViewController.swift | 0 .../InputFamilyLinkDIContainer.swift | 0 .../Dependency/JoinFamilyDIContainer.swift | 0 .../Dependency/JoinedFamilyDIContainer.swift | 0 .../Reactor/FamilyEntranceReactor.swift | 0 .../Reactor/InputFamilyLinkReactor.swift | 0 .../Reactor/JoinFamilyReactor.swift | 0 .../Strings/JoinFamilyStrings.swift | 0 .../Strings/inviteFamilyLinkStrings.swift | 0 .../Strings/joinedFamilyStrings.swift | 0 .../FamilyEntranceViewController.swift | 0 .../InputFamilyLinkViewController.swift | 0 .../JoinFamilyViewController.swift | 0 .../Views/MakeNewFamilyView.swift | 0 .../Presentation/Home/Model/Balloon.swift | 0 .../Home/Model/DescriptionText.swift | 0 .../Home/Model/FamilySection.swift | 0 .../Presentation/Home/Model/PostSection.swift | 0 .../Home/Model/String/HomeCGFloat.swift | 0 .../Home/Model/String/HomeString.swift | 0 .../Home/Reactor/BalloonReactor.swift | 0 .../Reactor/Cell/MainFamilyCellReactor.swift | 0 .../Reactor/Cell/MainPostCellReactor.swift | 0 .../Reactor/ContributorProfileReactor.swift | 0 .../Home/Reactor/ContributorReactor.swift | 0 .../Home/Reactor/MainCameraReactor.swift | 0 .../Home/Reactor/MainFamilyViewReactor.swift | 0 .../Home/Reactor/MainPostViewReactor.swift | 0 .../Home/Reactor/MainViewReactor.swift | 0 .../Home/Reactor/TimerReactor.swift | 0 .../MainFamilyViewController.swift | 0 .../MainPostViewController.swift | 0 .../ViewControllers/MainViewController.swift | 0 .../SegmentPageViewController.swift | 0 .../Presentation/Home/Views/BalloonView.swift | 0 .../Home/Views/ContributorProfileView.swift | 0 .../Home/Views/ContributorView.swift | 0 .../Home/Views/InviteFamilyView.swift | 0 .../Home/Views/MainCameraButtonView.swift | 0 .../Views/MainFamilyCollectionViewCell.swift | 0 .../Views/MainPostCollectionViewCell.swift | 0 .../Home/Views/NoPostTodayView.swift | 0 .../Presentation/Home/Views/TimerView.swift | 0 .../Activity/CopyInvitationUrlActivity.swift | 0 .../Activity/UrlActivityItemSource.swift | 0 .../Config/FamilyMemberCellKind.swift | 0 .../DataSource/FamilyMemberSectionModel.swift | 0 .../Delegate/ManagementTableDelegate.swift | 0 .../ManagementTableHeaderDelegate.swift | 0 .../Delegate/SharingContainerDelegate.swift | 0 .../RxManagementTableDelegateProxy.swift | 0 ...RxManagementTableHeaderDelegateProxy.swift | 0 .../RxSharingContainerDelegateProxy.swift | 0 .../Cell/FamilyMemberCellReactor.swift | 0 .../FamilyNameSettingViewReactor.swift | 0 .../Reactor/ManagementReactor.swift | 0 .../ManagementTableHeaderReactor.swift | 0 .../Reactor/ManagementTableReactor.swift | 0 .../Reactor/SharingContainerReactor.swift | 0 .../View/Cell/FamilyMemberCell.swift | 0 .../View/JoinFamilyGroupEditorView.swift | 0 .../View/ManagementTableHeaderView.swift | 0 .../Management/View/ManagementTableView.swift | 0 .../View/SharingContainerView.swift | 0 .../FamilyNameSettingViewController.swift | 0 .../ManagementViewController.swift | 0 .../Dependency/OnBoardingDIContainer.swift | 0 .../Presentation/OnBoarding/OnBoarding.swift | 0 .../OnBoarding/OnBoardingReactor.swift | 0 .../OnBoarding/OnBoardingStrings.swift | 0 .../OnBoarding/OnBoardingViewCell.swift | 0 .../OnBoarding/OnBoardingViewController.swift | 0 .../PostDetail/Model/ReactionSection.swift | 0 .../Model/SectionOfPostDetail.swift | 0 .../PostDetail/Model/String/PostCGFloat.swift | 0 .../PostDetail/Model/String/PostString.swift | 0 .../Reactor/MissionTextReactor.swift | 0 .../Reactor/PostDetailViewReactor.swift | 0 .../PostDetail/Reactor/PostReactor.swift | 0 .../Reactor/ReactionMemberViewReactor.swift | 0 .../Reactor/ReactionViewReactor.swift | 0 .../Reactor/SelectableEmojiReactor.swift | 0 .../PostDetail/Reactor/TempCellReactor.swift | 0 .../ViewControllers/PostViewController.swift | 0 .../ReactionMembersViewController.swift | 0 .../ReactionViewController.swift | 0 .../SelectableEmojiViewController.swift | 0 .../Views/AddCommentCollectionViewCell.swift | 0 .../Views/AddReactionCollectionViewCell.swift | 0 .../PostDetail/Views/MissionTextView.swift | 0 .../Views/PostDetailCollectionViewCell.swift | 0 .../PostDetail/Views/PostNavigationView.swift | 0 .../Views/ReactionCollectionViewCell.swift | 0 .../Views/RightAlignedFlowLayout.swift | 0 .../SeletableEmojiCollectionViewCell.swift | 0 .../Cell/CellReactor/PrivacyCellReactor.swift | 0 .../DataSources/PrivacySectionModel.swift | 0 .../PrivacyAuthorizationTableViewCell.swift | 0 .../Privacy/Cell/PrivacyTableViewCell.swift | 0 .../Privacy/PrivacyViewController.swift | 0 .../Privacy/Reactor/PrivacyViewReactor.swift | 0 ...PrivacyAuthorizationHeaderFooterView.swift | 0 .../PrivacyHeaderFooterView.swift | 0 .../Privacy/View/BibbiInquireBannerView.swift | 0 .../CellReactor/ProfileFeedCellReactor.swift | 0 .../ProfileFeedDescrptionCellReactor.swift | 0 .../ProfileFeedEmptyCellReactor.swift | 0 .../Cell/ProfileFeedCollectionViewCell.swift | 0 .../Cell/ProfileFeedDescrptionCell.swift | 0 .../ProfileFeedEmptyCollectionViewCell.swift | 0 .../ProfileFeedDescrptionSectionModel.swift | 0 .../DataSources/ProfileFeedSectionModel.swift | 0 .../Profile/ProfileDetailViewController.swift | 0 .../ProfileFeedPageViewController.swift | 0 .../Profile/ProfileFeedViewController.swift | 0 .../Profile/ProfileViewController.swift | 0 .../Reactor/ProfileDetailViewReactor.swift | 0 .../Reactor/ProfileFeedPageViewReactor.swift | 0 .../Reactor/ProfileFeedViewReactor.swift | 0 .../Profile/Reactor/ProfileViewReactor.swift | 0 .../Profile/View/BibbiProfileView.swift | 0 .../Resign/AccountResignViewController.swift | 0 .../Cell/ResignReaonsTableViewCell.swift | 0 .../Resign/Config/ReasonType.swift | 0 .../AccountResignSectionModel.swift | 0 .../Reactor/AccountResignViewReactor.swift | 0 .../ResignReasonTableViewCellReactor.swift | 0 .../Splash/Dependency/SplashDIContainer.swift | 0 .../Presentation/Splash/SplashReactor.swift | 0 .../Splash/SplashViewController.swift | 0 .../Trash/AirplaneLottieView.swift | 0 .../Trash/BibbiFetchFailureView.swift | 0 .../Trash/BlurAiraplaneLottieView.swift | 0 .../DIContainer/CalendarCellDIContainer.swift | 0 .../CalendarImageCellDIContainer.swift | 0 .../CalendarPostCellDIContainer.swift | 0 .../DIContainer/CalendarPostDIContainer.swift | 0 .../MonthlyCalendarDIConatainer.swift | 0 .../FamilyManagementDIContainer.swift | 0 .../InvitationUrlContainerDIContainer.swift | 0 .../Dependencygg/PostCommentDIContainer.swift | 0 .../Trash/Manager/DeepLinkManager.swift | 0 .../WidgetExtension/Resources/empty.txt | 0 .../Sources/FamilyWidget/Family.swift | 0 .../Sources/FamilyWidget/FamilyService.swift | 0 .../Sources/FamilyWidget/FamilyWidget.swift | 0 .../FamilyWidgetDIContainer.swift | 0 .../FamilyWidget/FamilyWidgetEntry.swift | 0 .../FamilyWidgetTimelineProvider.swift | 0 .../FamilyWidget/FamilyWidgetView.swift | 0 14th-team5-iOS/Core/Project.swift | 2 +- .../Core/Sources/Extensions/Bundle+Ext.swift | 2 +- .../Core/Sources/Trash/BBNetwork/API.swift | 2 +- 14th-team5-iOS/Data/Project.swift | 2 +- .../Sources/Repositories/AppRepository.swift | 2 +- .../Kakao/AccountKakaoSignInHelper.swift | 2 +- 14th-team5-iOS/DesignSystem/Project.swift | 2 +- 14th-team5-iOS/Domain/Project.swift | 2 +- 14th-team5-iOS/Util/Project.swift | 2 +- Bibbi.xcworkspace/contents.xcworkspacedata | 129 -- .../xcschemes/Bibbi-Workspace.xcscheme | 1476 ----------------- Plugins/App/Plugin.swift | 4 +- Tuist/Package.swift | 8 +- .../DeploymentTarget+Templates.swift | 2 +- .../Modular+Templates.swift | 4 +- .../ModuleType+Templates.swift | 4 +- .../Package+Templates.swift | 2 +- .../Products+Templates.swift | 4 +- .../Project+Templates.swift | 4 +- .../ResourceFileElements+Templates.swift | 2 +- .../SourceFilesList+Templates.swift | 2 +- .../TargetDependency+Templates.swift | 2 +- .../TargetScript+Templates.swift | 4 +- Workspace.swift | 2 +- 359 files changed, 37 insertions(+), 1642 deletions(-) rename 14th-team5-iOS/{App => Bibbi}/Project.swift (92%) rename 14th-team5-iOS/{App => Bibbi}/Resources/Assets.xcassets/AppIcon.appiconset/100.png (100%) rename 14th-team5-iOS/{App => Bibbi}/Resources/Assets.xcassets/AppIcon.appiconset/1024.png (100%) rename 14th-team5-iOS/{App => Bibbi}/Resources/Assets.xcassets/AppIcon.appiconset/114.png (100%) rename 14th-team5-iOS/{App => Bibbi}/Resources/Assets.xcassets/AppIcon.appiconset/120.png (100%) rename 14th-team5-iOS/{App => Bibbi}/Resources/Assets.xcassets/AppIcon.appiconset/128.png (100%) rename 14th-team5-iOS/{App => Bibbi}/Resources/Assets.xcassets/AppIcon.appiconset/144.png (100%) rename 14th-team5-iOS/{App => Bibbi}/Resources/Assets.xcassets/AppIcon.appiconset/152.png (100%) rename 14th-team5-iOS/{App => Bibbi}/Resources/Assets.xcassets/AppIcon.appiconset/16.png (100%) rename 14th-team5-iOS/{App => Bibbi}/Resources/Assets.xcassets/AppIcon.appiconset/167.png (100%) rename 14th-team5-iOS/{App => Bibbi}/Resources/Assets.xcassets/AppIcon.appiconset/172.png (100%) rename 14th-team5-iOS/{App => Bibbi}/Resources/Assets.xcassets/AppIcon.appiconset/180.png (100%) rename 14th-team5-iOS/{App => Bibbi}/Resources/Assets.xcassets/AppIcon.appiconset/196.png (100%) rename 14th-team5-iOS/{App => Bibbi}/Resources/Assets.xcassets/AppIcon.appiconset/20.png (100%) rename 14th-team5-iOS/{App => Bibbi}/Resources/Assets.xcassets/AppIcon.appiconset/216.png (100%) rename 14th-team5-iOS/{App => Bibbi}/Resources/Assets.xcassets/AppIcon.appiconset/256.png (100%) rename 14th-team5-iOS/{App => Bibbi}/Resources/Assets.xcassets/AppIcon.appiconset/29.png (100%) rename 14th-team5-iOS/{App => Bibbi}/Resources/Assets.xcassets/AppIcon.appiconset/32.png (100%) rename 14th-team5-iOS/{App => Bibbi}/Resources/Assets.xcassets/AppIcon.appiconset/40.png (100%) rename 14th-team5-iOS/{App => Bibbi}/Resources/Assets.xcassets/AppIcon.appiconset/48.png (100%) rename 14th-team5-iOS/{App => Bibbi}/Resources/Assets.xcassets/AppIcon.appiconset/50.png (100%) rename 14th-team5-iOS/{App => Bibbi}/Resources/Assets.xcassets/AppIcon.appiconset/512.png (100%) rename 14th-team5-iOS/{App => Bibbi}/Resources/Assets.xcassets/AppIcon.appiconset/55.png (100%) rename 14th-team5-iOS/{App => Bibbi}/Resources/Assets.xcassets/AppIcon.appiconset/57.png (100%) rename 14th-team5-iOS/{App => Bibbi}/Resources/Assets.xcassets/AppIcon.appiconset/58.png (100%) rename 14th-team5-iOS/{App => Bibbi}/Resources/Assets.xcassets/AppIcon.appiconset/60.png (100%) rename 14th-team5-iOS/{App => Bibbi}/Resources/Assets.xcassets/AppIcon.appiconset/64.png (100%) rename 14th-team5-iOS/{App => Bibbi}/Resources/Assets.xcassets/AppIcon.appiconset/66.png (100%) rename 14th-team5-iOS/{App => Bibbi}/Resources/Assets.xcassets/AppIcon.appiconset/72.png (100%) rename 14th-team5-iOS/{App => Bibbi}/Resources/Assets.xcassets/AppIcon.appiconset/76.png (100%) rename 14th-team5-iOS/{App => Bibbi}/Resources/Assets.xcassets/AppIcon.appiconset/80.png (100%) rename 14th-team5-iOS/{App => Bibbi}/Resources/Assets.xcassets/AppIcon.appiconset/87.png (100%) rename 14th-team5-iOS/{App => Bibbi}/Resources/Assets.xcassets/AppIcon.appiconset/88.png (100%) rename 14th-team5-iOS/{App => Bibbi}/Resources/Assets.xcassets/AppIcon.appiconset/92.png (100%) rename 14th-team5-iOS/{App => Bibbi}/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename 14th-team5-iOS/{App => Bibbi}/Resources/Assets.xcassets/Contents.json (100%) rename 14th-team5-iOS/{App => Bibbi}/Resources/Assets.xcassets/splash.imageset/Contents.json (100%) rename 14th-team5-iOS/{App => Bibbi}/Resources/Assets.xcassets/splash.imageset/iOSSplash.svg (100%) rename 14th-team5-iOS/{App => Bibbi}/Resources/GoogleService-Info.plist (100%) rename 14th-team5-iOS/{App => Bibbi}/Resources/LaunchScreen.storyboard (100%) rename 14th-team5-iOS/{App => Bibbi}/Resources/PrivacyInfo.xcprivacy (100%) rename 14th-team5-iOS/{App => Bibbi}/Resources/empty.txt (100%) rename 14th-team5-iOS/{App => Bibbi}/Resources/fire.json (100%) rename 14th-team5-iOS/{App => Bibbi}/Resources/loading.json (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/AppDelegate.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/DIContainer/AppDIContainer.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/DIContainer/CalendarDIContainer.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/DIContainer/CameraDIContainer.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/DIContainer/CommentDIContainer.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/DIContainer/FamilyDIContainer.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/DIContainer/MainViewDIContainer.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/DIContainer/MemberDIContainer.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/DIContainer/MissionDIContainer.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/DIContainer/MyDIContainer.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/DIContainer/NavigatorDIContainer.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/DIContainer/OAuthDIContainer.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/DIContainer/PickDICotainer.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/DIContainer/PostDIContainer.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/DIContainer/PrivacyDIContainer.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/DIContainer/ProfileDIContainer.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/DIContainer/ReactionDIContainer.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/DIContainer/RealEmojiDIContainer.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/DIContainer/ResignDIContainer.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/DIContainer/SignInDIContainer.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/DIContainer/SignOutDIContainer.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/Navigator/AccountProfileNavigator.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/Navigator/AccountResignNavigator.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/Navigator/AccountSignInNavigator.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/Navigator/AppNavigator.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/Navigator/CameraDisplayNavigator.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/Navigator/CameraNavigator.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/Navigator/CommentNavigator.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/Navigator/DailyCalendarNavigator.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/Navigator/FamilyEntranceNavigator.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/Navigator/HomeNavigator.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/Navigator/InputFamilyLinkNavigator.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/Navigator/JoinFamilyNavigator.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/Navigator/MainNavigator.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/Navigator/ManagementNavigator.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/Navigator/MonthlyCalendarNavigator.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/Navigator/OnboardingNavigator.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/Navigator/PrivacyNavigator.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/Navigator/ProfileNavigator.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/Navigator/SplashNavigator.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/Navigator/Wrapper/Calendar/DailyCalendarViewControllerWrapper.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/Navigator/Wrapper/Calendar/MonthlyCalendarViewControllerWrapper.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/Navigator/Wrapper/Camera/CameraDisplayViewControllerWrapper.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/Navigator/Wrapper/Camera/CameraViewControllerWrapper.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/Navigator/Wrapper/Comment/CommentViewControllerWrapper.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/Navigator/Wrapper/FamilyEntrance/FamilyEntranceControllerWrapper.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/Navigator/Wrapper/FamilyEntrance/InputFamilyLinkViewControllerWrapper.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/Navigator/Wrapper/FamilyEntrance/JoinFamilyViewControllerWrapper.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/Navigator/Wrapper/Main/MainFamilyViewControllerWrapper.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/Navigator/Wrapper/Main/MainPostViewControllerWrapper.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/Navigator/Wrapper/Main/MainViewControllerWrapper.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/Navigator/Wrapper/Management/FamilyNameSettingViewControllerWrapper.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/Navigator/Wrapper/Management/ManagementViewControllerWrapper.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/Navigator/Wrapper/Onboarding/OnboardingViewControllerWrapper.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/Navigator/Wrapper/PostDetail/PostDetailCellWrapper.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/Navigator/Wrapper/PostDetail/PostDetailViewControllerWrapper.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/Navigator/Wrapper/PostDetail/ReactionMembersViewControllerWrapper.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/Navigator/Wrapper/PostDetail/ReactionViewControllerWrapper.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/Navigator/Wrapper/PostDetail/SelectableEmojiViewControllerWrapper.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/Navigator/Wrapper/Privacy/PrivacyViewControllerWrapper.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/Navigator/Wrapper/Profile/ProfileDetailViewControllerWrapper.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/Navigator/Wrapper/Profile/ProfileFeedPageViewControllerWrapper.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/Navigator/Wrapper/Profile/ProfileFeedViewControllerWrapper.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/Navigator/Wrapper/Profile/ProfileViewControllerWrapper.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/Navigator/Wrapper/Resign/AccountResignViewControllerWrapper.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/Navigator/Wrapper/SignIn/SignInViewControllerWrapper.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/Navigator/Wrapper/Splash/SplashViewControllerWrapper.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/Navigator/Wrapper/WebContent/WebContentViewControllerWrapper.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Application/SceneDelegate.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Font/Pretendard-Black.otf (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Font/Pretendard-Bold.otf (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Font/Pretendard-ExtraBold.otf (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Font/Pretendard-ExtraLight.otf (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Font/Pretendard-Light.otf (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Font/Pretendard-Medium.otf (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Font/Pretendard-Regular.otf (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Font/Pretendard-SemiBold.otf (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Font/Pretendard-Thin.otf (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Account/AccountSignIn/AccountSignInDIContainer.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Account/AccountSignIn/AccountSignInReactor.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Account/AccountSignIn/AccountSignInStrings.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Account/AccountSignIn/AccountSignInViewController.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Account/AccountSignUp/AccountSignUpStrings.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Account/AccountSignUp/Dependeyncy/AccountSignUpDIContainer.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Account/AccountSignUp/Reactor/AccountSignUpReactor.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountDateViewController.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountNicknameViewController.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountProfileViewController.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountSignUpViewController.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Calendar/Config/CalendarImageCell+Type.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Calendar/Reactive/DataSource/DailyCalendarSectionModel.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Calendar/Reactive/DataSource/MonthlyCalendarSectionModel.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Calendar/Reactive/DelegateProxy/Delegate/MemoriesCalendarPostHeaderDelegate.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Calendar/Reactive/DelegateProxy/RxFSCalendarDelegateProxy.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Calendar/Reactive/DelegateProxy/RxMemoriesCalendarPostDelegateProxy.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Calendar/Reactor/Cell/MemoriesCalendarCellReactor.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Calendar/Reactor/Cell/MemoriesCalendarPageReactor.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Calendar/Reactor/Cell/MemoriesCalendarPostCellReactor.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Calendar/Reactor/DailyCalendarViewReactor.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Calendar/Reactor/MemoriesCalendarPostHeaderReactor.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Calendar/Reactor/MemoriesCalendarPostImageReactor.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Calendar/Reactor/MemoriesCalendarTitleViewReactor.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Calendar/Reactor/MonthlyCalendarViewReactor.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Calendar/View/BannerView.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Calendar/View/Cell/MemoriesCalendarCell.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Calendar/View/Cell/MemoriesCalendarPageViewCell.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Calendar/View/Cell/MemoriesCalendarPlaceholderCell.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Calendar/View/Cell/MemoriesCalendarPostCell.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Calendar/View/MemoriesCalendarPageTitleView.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Calendar/View/MemoriesCalendarPostHeaderView.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Calendar/View/MemoriesCalendarPostImageView.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Calendar/ViewController/BannerHostingViewController.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Calendar/ViewController/DailyCalendarViewController.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Calendar/ViewController/MonthlyCalendarViewController.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Calendar/ViewModel/BannerViewModel.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Camera/CameraDisplayViewController.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Camera/CameraViewController.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Camera/Cell/BibbiRealEmojiViewCell.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Camera/Cell/CellReactor/BibbiRealEmojiCellReactor.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Camera/Cell/CellReactor/DisplayEditCellReactor.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Camera/Cell/DisplayEditCollectionViewCell.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Camera/DataSources/DisplayEditSectionModel.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Camera/DataSources/EmojiSectionModel.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Camera/Reactor/CameraDisplayViewReactor.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Camera/Reactor/CameraViewReactor.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Camera/View/BibbiMissionView.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Comment/Reactive/DataSource/CommentSectionModel.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Comment/Reactive/DelegateProxy/Delegate/CommentTextFieldDelegate.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Comment/Reactive/DelegateProxy/RxCommentTextFieldDelegateProxy.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Comment/Reactor/Cell/CommentCellReactor.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Comment/Reactor/CommentTableReactor.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Comment/Reactor/CommentTextFieldReactor.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Comment/Reactor/CommentViewReactor.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Comment/View/Cell/CommentCell.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Comment/View/CommentTableView.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Comment/View/CommentTextFieldView.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Comment/View/CommentTopBarView.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Comment/View/NoneCommentView.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Comment/ViewController/CommentViewController.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Content/Dependency/WebContentDIContainer.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Content/Reactor/WebContentReactor.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Content/WebContentViewController.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/FamilyEntrance/Dependency/InputFamilyLinkDIContainer.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/FamilyEntrance/Dependency/JoinFamilyDIContainer.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/FamilyEntrance/Dependency/JoinedFamilyDIContainer.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/FamilyEntrance/Reactor/FamilyEntranceReactor.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/FamilyEntrance/Reactor/InputFamilyLinkReactor.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/FamilyEntrance/Reactor/JoinFamilyReactor.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/FamilyEntrance/Strings/JoinFamilyStrings.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/FamilyEntrance/Strings/inviteFamilyLinkStrings.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/FamilyEntrance/Strings/joinedFamilyStrings.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/FamilyEntrance/ViewController/FamilyEntranceViewController.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/FamilyEntrance/ViewController/InputFamilyLinkViewController.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/FamilyEntrance/ViewController/JoinFamilyViewController.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/FamilyEntrance/Views/MakeNewFamilyView.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Home/Model/Balloon.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Home/Model/DescriptionText.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Home/Model/FamilySection.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Home/Model/PostSection.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Home/Model/String/HomeCGFloat.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Home/Model/String/HomeString.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Home/Reactor/BalloonReactor.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Home/Reactor/Cell/MainFamilyCellReactor.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Home/Reactor/Cell/MainPostCellReactor.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Home/Reactor/ContributorProfileReactor.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Home/Reactor/ContributorReactor.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Home/Reactor/MainCameraReactor.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Home/Reactor/MainFamilyViewReactor.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Home/Reactor/MainPostViewReactor.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Home/Reactor/MainViewReactor.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Home/Reactor/TimerReactor.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Home/ViewControllers/MainFamilyViewController.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Home/ViewControllers/MainPostViewController.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Home/ViewControllers/MainViewController.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Home/ViewControllers/SegmentPageViewController.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Home/Views/BalloonView.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Home/Views/ContributorProfileView.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Home/Views/ContributorView.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Home/Views/InviteFamilyView.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Home/Views/MainCameraButtonView.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Home/Views/MainFamilyCollectionViewCell.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Home/Views/MainPostCollectionViewCell.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Home/Views/NoPostTodayView.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Home/Views/TimerView.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Management/Activity/CopyInvitationUrlActivity.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Management/Activity/UrlActivityItemSource.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Management/Config/FamilyMemberCellKind.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Management/Reactive/DataSource/FamilyMemberSectionModel.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Management/Reactive/DelegateProxy/Delegate/ManagementTableDelegate.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Management/Reactive/DelegateProxy/Delegate/ManagementTableHeaderDelegate.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Management/Reactive/DelegateProxy/Delegate/SharingContainerDelegate.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Management/Reactive/DelegateProxy/RxManagementTableDelegateProxy.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Management/Reactive/DelegateProxy/RxManagementTableHeaderDelegateProxy.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Management/Reactive/DelegateProxy/RxSharingContainerDelegateProxy.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Management/Reactor/Cell/FamilyMemberCellReactor.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Management/Reactor/FamilyNameSettingViewReactor.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Management/Reactor/ManagementReactor.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Management/Reactor/ManagementTableHeaderReactor.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Management/Reactor/ManagementTableReactor.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Management/Reactor/SharingContainerReactor.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Management/View/Cell/FamilyMemberCell.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Management/View/JoinFamilyGroupEditorView.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Management/View/ManagementTableHeaderView.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Management/View/ManagementTableView.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Management/View/SharingContainerView.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Management/ViewController/FamilyNameSettingViewController.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Management/ViewController/ManagementViewController.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/OnBoarding/Dependency/OnBoardingDIContainer.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/OnBoarding/OnBoarding.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/OnBoarding/OnBoardingReactor.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/OnBoarding/OnBoardingStrings.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/OnBoarding/OnBoardingViewCell.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/OnBoarding/OnBoardingViewController.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/PostDetail/Model/ReactionSection.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/PostDetail/Model/SectionOfPostDetail.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/PostDetail/Model/String/PostCGFloat.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/PostDetail/Model/String/PostString.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/PostDetail/Reactor/MissionTextReactor.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/PostDetail/Reactor/PostDetailViewReactor.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/PostDetail/Reactor/PostReactor.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/PostDetail/Reactor/ReactionMemberViewReactor.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/PostDetail/Reactor/ReactionViewReactor.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/PostDetail/Reactor/SelectableEmojiReactor.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/PostDetail/Reactor/TempCellReactor.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/PostDetail/ViewControllers/PostViewController.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/PostDetail/ViewControllers/ReactionMembersViewController.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/PostDetail/ViewControllers/ReactionViewController.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/PostDetail/ViewControllers/SelectableEmojiViewController.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/PostDetail/Views/AddCommentCollectionViewCell.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/PostDetail/Views/AddReactionCollectionViewCell.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/PostDetail/Views/MissionTextView.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/PostDetail/Views/PostDetailCollectionViewCell.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/PostDetail/Views/PostNavigationView.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/PostDetail/Views/ReactionCollectionViewCell.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/PostDetail/Views/RightAlignedFlowLayout.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/PostDetail/Views/SeletableEmojiCollectionViewCell.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Privacy/Cell/CellReactor/PrivacyCellReactor.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Privacy/Cell/DataSources/PrivacySectionModel.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Privacy/Cell/PrivacyAuthorizationTableViewCell.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Privacy/Cell/PrivacyTableViewCell.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Privacy/PrivacyViewController.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Privacy/Reactor/PrivacyViewReactor.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Privacy/ReusableView/PrivacyAuthorizationHeaderFooterView.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Privacy/ReusableView/PrivacyHeaderFooterView.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Privacy/View/BibbiInquireBannerView.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Profile/Cell/CellReactor/ProfileFeedCellReactor.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Profile/Cell/CellReactor/ProfileFeedDescrptionCellReactor.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Profile/Cell/CellReactor/ProfileFeedEmptyCellReactor.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Profile/Cell/ProfileFeedCollectionViewCell.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Profile/Cell/ProfileFeedDescrptionCell.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Profile/Cell/ProfileFeedEmptyCollectionViewCell.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Profile/DataSources/ProfileFeedDescrptionSectionModel.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Profile/DataSources/ProfileFeedSectionModel.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Profile/ProfileDetailViewController.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Profile/ProfileFeedPageViewController.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Profile/ProfileFeedViewController.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Profile/ProfileViewController.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Profile/Reactor/ProfileDetailViewReactor.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Profile/Reactor/ProfileFeedPageViewReactor.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Profile/Reactor/ProfileFeedViewReactor.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Profile/View/BibbiProfileView.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Resign/AccountResignViewController.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Resign/Cell/ResignReaonsTableViewCell.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Resign/Config/ReasonType.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Resign/DataSources/AccountResignSectionModel.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Resign/Reactor/AccountResignViewReactor.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Resign/Reactor/ResignReasonTableViewCellReactor.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Splash/Dependency/SplashDIContainer.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Splash/SplashReactor.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Splash/SplashViewController.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Trash/AirplaneLottieView.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Trash/BibbiFetchFailureView.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Trash/BlurAiraplaneLottieView.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Trash/DIContainer/CalendarCellDIContainer.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Trash/DIContainer/CalendarImageCellDIContainer.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Trash/DIContainer/CalendarPostCellDIContainer.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Trash/DIContainer/CalendarPostDIContainer.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Trash/DIContainer/MonthlyCalendarDIConatainer.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Trash/Dependency/FamilyManagementDIContainer.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Trash/Dependency/InvitationUrlContainerDIContainer.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Trash/Dependencygg/PostCommentDIContainer.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/Sources/Presentation/Trash/Manager/DeepLinkManager.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/WidgetExtension/Resources/empty.txt (100%) rename 14th-team5-iOS/{App => Bibbi}/WidgetExtension/Sources/FamilyWidget/Family.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/WidgetExtension/Sources/FamilyWidget/FamilyService.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/WidgetExtension/Sources/FamilyWidget/FamilyWidget.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/WidgetExtension/Sources/FamilyWidget/FamilyWidgetDIContainer.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/WidgetExtension/Sources/FamilyWidget/FamilyWidgetEntry.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/WidgetExtension/Sources/FamilyWidget/FamilyWidgetTimelineProvider.swift (100%) rename 14th-team5-iOS/{App => Bibbi}/WidgetExtension/Sources/FamilyWidget/FamilyWidgetView.swift (100%) delete mode 100644 Bibbi.xcworkspace/contents.xcworkspacedata delete mode 100644 Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme diff --git a/14th-team5-iOS/App/Project.swift b/14th-team5-iOS/Bibbi/Project.swift similarity index 92% rename from 14th-team5-iOS/App/Project.swift rename to 14th-team5-iOS/Bibbi/Project.swift index fd9fea690..081e79721 100644 --- a/14th-team5-iOS/App/Project.swift +++ b/14th-team5-iOS/Bibbi/Project.swift @@ -5,21 +5,21 @@ // Created by Kim dohyun on 2023/11/14. // -import ProjectDescription +@preconcurrency import ProjectDescription import ProjectDescriptionHelpers private let targets: [Target] = [ .makeModular( - layer: .App, + layer: .Bibbi, factory: .init( - products: .app, - dependencies: ModuleLayer.App.dependencies, + products: .bibbi, + dependencies: ModuleLayer.Bibbi.dependencies, bundleId: "com.5ing.bibbi", infoPlist: .extendingDefault(with: [ "CFBundleDisplayName": .string("Bibbi"), "CFBundleVersion": .string("1"), "CFBuildVersion": .string("0"), - "CFBundleShortVersionString": .string("1.2.5"), + "CFBundleShortVersionString": .string("1.2.6"), "UILaunchStoryboardName": .string("LaunchScreen"), "UISupportedInterfaceOrientations": .array([.string("UIInterfaceOrientationPortrait")]), "UIUserInterfaceStyle": .string("Dark"), @@ -89,4 +89,4 @@ private let targets: [Target] = [ ] -private let app = Project.makeApp(name: ModuleLayer.App.rawValue, target: targets) +private let bibbi = Project.makeApp(name: ModuleLayer.Bibbi.rawValue, target: targets) diff --git a/14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/100.png b/14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/100.png similarity index 100% rename from 14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/100.png rename to 14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/100.png diff --git a/14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/1024.png b/14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/1024.png similarity index 100% rename from 14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/1024.png rename to 14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/1024.png diff --git a/14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/114.png b/14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/114.png similarity index 100% rename from 14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/114.png rename to 14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/114.png diff --git a/14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/120.png b/14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/120.png similarity index 100% rename from 14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/120.png rename to 14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/120.png diff --git a/14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/128.png b/14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/128.png similarity index 100% rename from 14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/128.png rename to 14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/128.png diff --git a/14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/144.png b/14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/144.png similarity index 100% rename from 14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/144.png rename to 14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/144.png diff --git a/14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/152.png b/14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/152.png similarity index 100% rename from 14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/152.png rename to 14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/152.png diff --git a/14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/16.png b/14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/16.png similarity index 100% rename from 14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/16.png rename to 14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/16.png diff --git a/14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/167.png b/14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/167.png similarity index 100% rename from 14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/167.png rename to 14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/167.png diff --git a/14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/172.png b/14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/172.png similarity index 100% rename from 14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/172.png rename to 14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/172.png diff --git a/14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/180.png b/14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/180.png similarity index 100% rename from 14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/180.png rename to 14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/180.png diff --git a/14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/196.png b/14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/196.png similarity index 100% rename from 14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/196.png rename to 14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/196.png diff --git a/14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/20.png b/14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/20.png similarity index 100% rename from 14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/20.png rename to 14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/20.png diff --git a/14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/216.png b/14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/216.png similarity index 100% rename from 14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/216.png rename to 14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/216.png diff --git a/14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/256.png b/14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/256.png similarity index 100% rename from 14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/256.png rename to 14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/256.png diff --git a/14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/29.png b/14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/29.png similarity index 100% rename from 14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/29.png rename to 14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/29.png diff --git a/14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/32.png b/14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/32.png similarity index 100% rename from 14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/32.png rename to 14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/32.png diff --git a/14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/40.png b/14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/40.png similarity index 100% rename from 14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/40.png rename to 14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/40.png diff --git a/14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/48.png b/14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/48.png similarity index 100% rename from 14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/48.png rename to 14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/48.png diff --git a/14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/50.png b/14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/50.png similarity index 100% rename from 14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/50.png rename to 14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/50.png diff --git a/14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/512.png b/14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/512.png similarity index 100% rename from 14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/512.png rename to 14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/512.png diff --git a/14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/55.png b/14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/55.png similarity index 100% rename from 14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/55.png rename to 14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/55.png diff --git a/14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/57.png b/14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/57.png similarity index 100% rename from 14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/57.png rename to 14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/57.png diff --git a/14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/58.png b/14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/58.png similarity index 100% rename from 14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/58.png rename to 14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/58.png diff --git a/14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/60.png b/14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/60.png similarity index 100% rename from 14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/60.png rename to 14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/60.png diff --git a/14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/64.png b/14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/64.png similarity index 100% rename from 14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/64.png rename to 14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/64.png diff --git a/14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/66.png b/14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/66.png similarity index 100% rename from 14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/66.png rename to 14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/66.png diff --git a/14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/72.png b/14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/72.png similarity index 100% rename from 14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/72.png rename to 14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/72.png diff --git a/14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/76.png b/14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/76.png similarity index 100% rename from 14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/76.png rename to 14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/76.png diff --git a/14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/80.png b/14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/80.png similarity index 100% rename from 14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/80.png rename to 14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/80.png diff --git a/14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/87.png b/14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/87.png similarity index 100% rename from 14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/87.png rename to 14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/87.png diff --git a/14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/88.png b/14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/88.png similarity index 100% rename from 14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/88.png rename to 14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/88.png diff --git a/14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/92.png b/14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/92.png similarity index 100% rename from 14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/92.png rename to 14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/92.png diff --git a/14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from 14th-team5-iOS/App/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json rename to 14th-team5-iOS/Bibbi/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/14th-team5-iOS/App/Resources/Assets.xcassets/Contents.json b/14th-team5-iOS/Bibbi/Resources/Assets.xcassets/Contents.json similarity index 100% rename from 14th-team5-iOS/App/Resources/Assets.xcassets/Contents.json rename to 14th-team5-iOS/Bibbi/Resources/Assets.xcassets/Contents.json diff --git a/14th-team5-iOS/App/Resources/Assets.xcassets/splash.imageset/Contents.json b/14th-team5-iOS/Bibbi/Resources/Assets.xcassets/splash.imageset/Contents.json similarity index 100% rename from 14th-team5-iOS/App/Resources/Assets.xcassets/splash.imageset/Contents.json rename to 14th-team5-iOS/Bibbi/Resources/Assets.xcassets/splash.imageset/Contents.json diff --git a/14th-team5-iOS/App/Resources/Assets.xcassets/splash.imageset/iOSSplash.svg b/14th-team5-iOS/Bibbi/Resources/Assets.xcassets/splash.imageset/iOSSplash.svg similarity index 100% rename from 14th-team5-iOS/App/Resources/Assets.xcassets/splash.imageset/iOSSplash.svg rename to 14th-team5-iOS/Bibbi/Resources/Assets.xcassets/splash.imageset/iOSSplash.svg diff --git a/14th-team5-iOS/App/Resources/GoogleService-Info.plist b/14th-team5-iOS/Bibbi/Resources/GoogleService-Info.plist similarity index 100% rename from 14th-team5-iOS/App/Resources/GoogleService-Info.plist rename to 14th-team5-iOS/Bibbi/Resources/GoogleService-Info.plist diff --git a/14th-team5-iOS/App/Resources/LaunchScreen.storyboard b/14th-team5-iOS/Bibbi/Resources/LaunchScreen.storyboard similarity index 100% rename from 14th-team5-iOS/App/Resources/LaunchScreen.storyboard rename to 14th-team5-iOS/Bibbi/Resources/LaunchScreen.storyboard diff --git a/14th-team5-iOS/App/Resources/PrivacyInfo.xcprivacy b/14th-team5-iOS/Bibbi/Resources/PrivacyInfo.xcprivacy similarity index 100% rename from 14th-team5-iOS/App/Resources/PrivacyInfo.xcprivacy rename to 14th-team5-iOS/Bibbi/Resources/PrivacyInfo.xcprivacy diff --git a/14th-team5-iOS/App/Resources/empty.txt b/14th-team5-iOS/Bibbi/Resources/empty.txt similarity index 100% rename from 14th-team5-iOS/App/Resources/empty.txt rename to 14th-team5-iOS/Bibbi/Resources/empty.txt diff --git a/14th-team5-iOS/App/Resources/fire.json b/14th-team5-iOS/Bibbi/Resources/fire.json similarity index 100% rename from 14th-team5-iOS/App/Resources/fire.json rename to 14th-team5-iOS/Bibbi/Resources/fire.json diff --git a/14th-team5-iOS/App/Resources/loading.json b/14th-team5-iOS/Bibbi/Resources/loading.json similarity index 100% rename from 14th-team5-iOS/App/Resources/loading.json rename to 14th-team5-iOS/Bibbi/Resources/loading.json diff --git a/14th-team5-iOS/App/Sources/Application/AppDelegate.swift b/14th-team5-iOS/Bibbi/Sources/Application/AppDelegate.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/AppDelegate.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/AppDelegate.swift diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/AppDIContainer.swift b/14th-team5-iOS/Bibbi/Sources/Application/DIContainer/AppDIContainer.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/DIContainer/AppDIContainer.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/DIContainer/AppDIContainer.swift diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/CalendarDIContainer.swift b/14th-team5-iOS/Bibbi/Sources/Application/DIContainer/CalendarDIContainer.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/DIContainer/CalendarDIContainer.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/DIContainer/CalendarDIContainer.swift diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/CameraDIContainer.swift b/14th-team5-iOS/Bibbi/Sources/Application/DIContainer/CameraDIContainer.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/DIContainer/CameraDIContainer.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/DIContainer/CameraDIContainer.swift diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/CommentDIContainer.swift b/14th-team5-iOS/Bibbi/Sources/Application/DIContainer/CommentDIContainer.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/DIContainer/CommentDIContainer.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/DIContainer/CommentDIContainer.swift diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/FamilyDIContainer.swift b/14th-team5-iOS/Bibbi/Sources/Application/DIContainer/FamilyDIContainer.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/DIContainer/FamilyDIContainer.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/DIContainer/FamilyDIContainer.swift diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/MainViewDIContainer.swift b/14th-team5-iOS/Bibbi/Sources/Application/DIContainer/MainViewDIContainer.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/DIContainer/MainViewDIContainer.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/DIContainer/MainViewDIContainer.swift diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/MemberDIContainer.swift b/14th-team5-iOS/Bibbi/Sources/Application/DIContainer/MemberDIContainer.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/DIContainer/MemberDIContainer.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/DIContainer/MemberDIContainer.swift diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/MissionDIContainer.swift b/14th-team5-iOS/Bibbi/Sources/Application/DIContainer/MissionDIContainer.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/DIContainer/MissionDIContainer.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/DIContainer/MissionDIContainer.swift diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/MyDIContainer.swift b/14th-team5-iOS/Bibbi/Sources/Application/DIContainer/MyDIContainer.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/DIContainer/MyDIContainer.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/DIContainer/MyDIContainer.swift diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/NavigatorDIContainer.swift b/14th-team5-iOS/Bibbi/Sources/Application/DIContainer/NavigatorDIContainer.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/DIContainer/NavigatorDIContainer.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/DIContainer/NavigatorDIContainer.swift diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/OAuthDIContainer.swift b/14th-team5-iOS/Bibbi/Sources/Application/DIContainer/OAuthDIContainer.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/DIContainer/OAuthDIContainer.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/DIContainer/OAuthDIContainer.swift diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/PickDICotainer.swift b/14th-team5-iOS/Bibbi/Sources/Application/DIContainer/PickDICotainer.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/DIContainer/PickDICotainer.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/DIContainer/PickDICotainer.swift diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/PostDIContainer.swift b/14th-team5-iOS/Bibbi/Sources/Application/DIContainer/PostDIContainer.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/DIContainer/PostDIContainer.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/DIContainer/PostDIContainer.swift diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/PrivacyDIContainer.swift b/14th-team5-iOS/Bibbi/Sources/Application/DIContainer/PrivacyDIContainer.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/DIContainer/PrivacyDIContainer.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/DIContainer/PrivacyDIContainer.swift diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/ProfileDIContainer.swift b/14th-team5-iOS/Bibbi/Sources/Application/DIContainer/ProfileDIContainer.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/DIContainer/ProfileDIContainer.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/DIContainer/ProfileDIContainer.swift diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/ReactionDIContainer.swift b/14th-team5-iOS/Bibbi/Sources/Application/DIContainer/ReactionDIContainer.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/DIContainer/ReactionDIContainer.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/DIContainer/ReactionDIContainer.swift diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/RealEmojiDIContainer.swift b/14th-team5-iOS/Bibbi/Sources/Application/DIContainer/RealEmojiDIContainer.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/DIContainer/RealEmojiDIContainer.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/DIContainer/RealEmojiDIContainer.swift diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/ResignDIContainer.swift b/14th-team5-iOS/Bibbi/Sources/Application/DIContainer/ResignDIContainer.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/DIContainer/ResignDIContainer.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/DIContainer/ResignDIContainer.swift diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/SignInDIContainer.swift b/14th-team5-iOS/Bibbi/Sources/Application/DIContainer/SignInDIContainer.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/DIContainer/SignInDIContainer.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/DIContainer/SignInDIContainer.swift diff --git a/14th-team5-iOS/App/Sources/Application/DIContainer/SignOutDIContainer.swift b/14th-team5-iOS/Bibbi/Sources/Application/DIContainer/SignOutDIContainer.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/DIContainer/SignOutDIContainer.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/DIContainer/SignOutDIContainer.swift diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/AccountProfileNavigator.swift b/14th-team5-iOS/Bibbi/Sources/Application/Navigator/AccountProfileNavigator.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Navigator/AccountProfileNavigator.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/Navigator/AccountProfileNavigator.swift diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/AccountResignNavigator.swift b/14th-team5-iOS/Bibbi/Sources/Application/Navigator/AccountResignNavigator.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Navigator/AccountResignNavigator.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/Navigator/AccountResignNavigator.swift diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/AccountSignInNavigator.swift b/14th-team5-iOS/Bibbi/Sources/Application/Navigator/AccountSignInNavigator.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Navigator/AccountSignInNavigator.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/Navigator/AccountSignInNavigator.swift diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/AppNavigator.swift b/14th-team5-iOS/Bibbi/Sources/Application/Navigator/AppNavigator.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Navigator/AppNavigator.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/Navigator/AppNavigator.swift diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/CameraDisplayNavigator.swift b/14th-team5-iOS/Bibbi/Sources/Application/Navigator/CameraDisplayNavigator.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Navigator/CameraDisplayNavigator.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/Navigator/CameraDisplayNavigator.swift diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/CameraNavigator.swift b/14th-team5-iOS/Bibbi/Sources/Application/Navigator/CameraNavigator.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Navigator/CameraNavigator.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/Navigator/CameraNavigator.swift diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/CommentNavigator.swift b/14th-team5-iOS/Bibbi/Sources/Application/Navigator/CommentNavigator.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Navigator/CommentNavigator.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/Navigator/CommentNavigator.swift diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/DailyCalendarNavigator.swift b/14th-team5-iOS/Bibbi/Sources/Application/Navigator/DailyCalendarNavigator.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Navigator/DailyCalendarNavigator.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/Navigator/DailyCalendarNavigator.swift diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/FamilyEntranceNavigator.swift b/14th-team5-iOS/Bibbi/Sources/Application/Navigator/FamilyEntranceNavigator.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Navigator/FamilyEntranceNavigator.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/Navigator/FamilyEntranceNavigator.swift diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/HomeNavigator.swift b/14th-team5-iOS/Bibbi/Sources/Application/Navigator/HomeNavigator.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Navigator/HomeNavigator.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/Navigator/HomeNavigator.swift diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/InputFamilyLinkNavigator.swift b/14th-team5-iOS/Bibbi/Sources/Application/Navigator/InputFamilyLinkNavigator.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Navigator/InputFamilyLinkNavigator.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/Navigator/InputFamilyLinkNavigator.swift diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/JoinFamilyNavigator.swift b/14th-team5-iOS/Bibbi/Sources/Application/Navigator/JoinFamilyNavigator.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Navigator/JoinFamilyNavigator.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/Navigator/JoinFamilyNavigator.swift diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/MainNavigator.swift b/14th-team5-iOS/Bibbi/Sources/Application/Navigator/MainNavigator.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Navigator/MainNavigator.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/Navigator/MainNavigator.swift diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/ManagementNavigator.swift b/14th-team5-iOS/Bibbi/Sources/Application/Navigator/ManagementNavigator.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Navigator/ManagementNavigator.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/Navigator/ManagementNavigator.swift diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/MonthlyCalendarNavigator.swift b/14th-team5-iOS/Bibbi/Sources/Application/Navigator/MonthlyCalendarNavigator.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Navigator/MonthlyCalendarNavigator.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/Navigator/MonthlyCalendarNavigator.swift diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/OnboardingNavigator.swift b/14th-team5-iOS/Bibbi/Sources/Application/Navigator/OnboardingNavigator.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Navigator/OnboardingNavigator.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/Navigator/OnboardingNavigator.swift diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/PrivacyNavigator.swift b/14th-team5-iOS/Bibbi/Sources/Application/Navigator/PrivacyNavigator.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Navigator/PrivacyNavigator.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/Navigator/PrivacyNavigator.swift diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/ProfileNavigator.swift b/14th-team5-iOS/Bibbi/Sources/Application/Navigator/ProfileNavigator.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Navigator/ProfileNavigator.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/Navigator/ProfileNavigator.swift diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/SplashNavigator.swift b/14th-team5-iOS/Bibbi/Sources/Application/Navigator/SplashNavigator.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Navigator/SplashNavigator.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/Navigator/SplashNavigator.swift diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Calendar/DailyCalendarViewControllerWrapper.swift b/14th-team5-iOS/Bibbi/Sources/Application/Navigator/Wrapper/Calendar/DailyCalendarViewControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Calendar/DailyCalendarViewControllerWrapper.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/Navigator/Wrapper/Calendar/DailyCalendarViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Calendar/MonthlyCalendarViewControllerWrapper.swift b/14th-team5-iOS/Bibbi/Sources/Application/Navigator/Wrapper/Calendar/MonthlyCalendarViewControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Calendar/MonthlyCalendarViewControllerWrapper.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/Navigator/Wrapper/Calendar/MonthlyCalendarViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Camera/CameraDisplayViewControllerWrapper.swift b/14th-team5-iOS/Bibbi/Sources/Application/Navigator/Wrapper/Camera/CameraDisplayViewControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Camera/CameraDisplayViewControllerWrapper.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/Navigator/Wrapper/Camera/CameraDisplayViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Camera/CameraViewControllerWrapper.swift b/14th-team5-iOS/Bibbi/Sources/Application/Navigator/Wrapper/Camera/CameraViewControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Camera/CameraViewControllerWrapper.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/Navigator/Wrapper/Camera/CameraViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Comment/CommentViewControllerWrapper.swift b/14th-team5-iOS/Bibbi/Sources/Application/Navigator/Wrapper/Comment/CommentViewControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Comment/CommentViewControllerWrapper.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/Navigator/Wrapper/Comment/CommentViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/FamilyEntrance/FamilyEntranceControllerWrapper.swift b/14th-team5-iOS/Bibbi/Sources/Application/Navigator/Wrapper/FamilyEntrance/FamilyEntranceControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/FamilyEntrance/FamilyEntranceControllerWrapper.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/Navigator/Wrapper/FamilyEntrance/FamilyEntranceControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/FamilyEntrance/InputFamilyLinkViewControllerWrapper.swift b/14th-team5-iOS/Bibbi/Sources/Application/Navigator/Wrapper/FamilyEntrance/InputFamilyLinkViewControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/FamilyEntrance/InputFamilyLinkViewControllerWrapper.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/Navigator/Wrapper/FamilyEntrance/InputFamilyLinkViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/FamilyEntrance/JoinFamilyViewControllerWrapper.swift b/14th-team5-iOS/Bibbi/Sources/Application/Navigator/Wrapper/FamilyEntrance/JoinFamilyViewControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/FamilyEntrance/JoinFamilyViewControllerWrapper.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/Navigator/Wrapper/FamilyEntrance/JoinFamilyViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Main/MainFamilyViewControllerWrapper.swift b/14th-team5-iOS/Bibbi/Sources/Application/Navigator/Wrapper/Main/MainFamilyViewControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Main/MainFamilyViewControllerWrapper.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/Navigator/Wrapper/Main/MainFamilyViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Main/MainPostViewControllerWrapper.swift b/14th-team5-iOS/Bibbi/Sources/Application/Navigator/Wrapper/Main/MainPostViewControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Main/MainPostViewControllerWrapper.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/Navigator/Wrapper/Main/MainPostViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Main/MainViewControllerWrapper.swift b/14th-team5-iOS/Bibbi/Sources/Application/Navigator/Wrapper/Main/MainViewControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Main/MainViewControllerWrapper.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/Navigator/Wrapper/Main/MainViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Management/FamilyNameSettingViewControllerWrapper.swift b/14th-team5-iOS/Bibbi/Sources/Application/Navigator/Wrapper/Management/FamilyNameSettingViewControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Management/FamilyNameSettingViewControllerWrapper.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/Navigator/Wrapper/Management/FamilyNameSettingViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Management/ManagementViewControllerWrapper.swift b/14th-team5-iOS/Bibbi/Sources/Application/Navigator/Wrapper/Management/ManagementViewControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Management/ManagementViewControllerWrapper.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/Navigator/Wrapper/Management/ManagementViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Onboarding/OnboardingViewControllerWrapper.swift b/14th-team5-iOS/Bibbi/Sources/Application/Navigator/Wrapper/Onboarding/OnboardingViewControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Onboarding/OnboardingViewControllerWrapper.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/Navigator/Wrapper/Onboarding/OnboardingViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/PostDetail/PostDetailCellWrapper.swift b/14th-team5-iOS/Bibbi/Sources/Application/Navigator/Wrapper/PostDetail/PostDetailCellWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/PostDetail/PostDetailCellWrapper.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/Navigator/Wrapper/PostDetail/PostDetailCellWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/PostDetail/PostDetailViewControllerWrapper.swift b/14th-team5-iOS/Bibbi/Sources/Application/Navigator/Wrapper/PostDetail/PostDetailViewControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/PostDetail/PostDetailViewControllerWrapper.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/Navigator/Wrapper/PostDetail/PostDetailViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/PostDetail/ReactionMembersViewControllerWrapper.swift b/14th-team5-iOS/Bibbi/Sources/Application/Navigator/Wrapper/PostDetail/ReactionMembersViewControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/PostDetail/ReactionMembersViewControllerWrapper.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/Navigator/Wrapper/PostDetail/ReactionMembersViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/PostDetail/ReactionViewControllerWrapper.swift b/14th-team5-iOS/Bibbi/Sources/Application/Navigator/Wrapper/PostDetail/ReactionViewControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/PostDetail/ReactionViewControllerWrapper.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/Navigator/Wrapper/PostDetail/ReactionViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/PostDetail/SelectableEmojiViewControllerWrapper.swift b/14th-team5-iOS/Bibbi/Sources/Application/Navigator/Wrapper/PostDetail/SelectableEmojiViewControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/PostDetail/SelectableEmojiViewControllerWrapper.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/Navigator/Wrapper/PostDetail/SelectableEmojiViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Privacy/PrivacyViewControllerWrapper.swift b/14th-team5-iOS/Bibbi/Sources/Application/Navigator/Wrapper/Privacy/PrivacyViewControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Privacy/PrivacyViewControllerWrapper.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/Navigator/Wrapper/Privacy/PrivacyViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Profile/ProfileDetailViewControllerWrapper.swift b/14th-team5-iOS/Bibbi/Sources/Application/Navigator/Wrapper/Profile/ProfileDetailViewControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Profile/ProfileDetailViewControllerWrapper.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/Navigator/Wrapper/Profile/ProfileDetailViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Profile/ProfileFeedPageViewControllerWrapper.swift b/14th-team5-iOS/Bibbi/Sources/Application/Navigator/Wrapper/Profile/ProfileFeedPageViewControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Profile/ProfileFeedPageViewControllerWrapper.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/Navigator/Wrapper/Profile/ProfileFeedPageViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Profile/ProfileFeedViewControllerWrapper.swift b/14th-team5-iOS/Bibbi/Sources/Application/Navigator/Wrapper/Profile/ProfileFeedViewControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Profile/ProfileFeedViewControllerWrapper.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/Navigator/Wrapper/Profile/ProfileFeedViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Profile/ProfileViewControllerWrapper.swift b/14th-team5-iOS/Bibbi/Sources/Application/Navigator/Wrapper/Profile/ProfileViewControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Profile/ProfileViewControllerWrapper.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/Navigator/Wrapper/Profile/ProfileViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Resign/AccountResignViewControllerWrapper.swift b/14th-team5-iOS/Bibbi/Sources/Application/Navigator/Wrapper/Resign/AccountResignViewControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Resign/AccountResignViewControllerWrapper.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/Navigator/Wrapper/Resign/AccountResignViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/SignIn/SignInViewControllerWrapper.swift b/14th-team5-iOS/Bibbi/Sources/Application/Navigator/Wrapper/SignIn/SignInViewControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/SignIn/SignInViewControllerWrapper.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/Navigator/Wrapper/SignIn/SignInViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Splash/SplashViewControllerWrapper.swift b/14th-team5-iOS/Bibbi/Sources/Application/Navigator/Wrapper/Splash/SplashViewControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/Splash/SplashViewControllerWrapper.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/Navigator/Wrapper/Splash/SplashViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/WebContent/WebContentViewControllerWrapper.swift b/14th-team5-iOS/Bibbi/Sources/Application/Navigator/Wrapper/WebContent/WebContentViewControllerWrapper.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/Navigator/Wrapper/WebContent/WebContentViewControllerWrapper.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/Navigator/Wrapper/WebContent/WebContentViewControllerWrapper.swift diff --git a/14th-team5-iOS/App/Sources/Application/SceneDelegate.swift b/14th-team5-iOS/Bibbi/Sources/Application/SceneDelegate.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Application/SceneDelegate.swift rename to 14th-team5-iOS/Bibbi/Sources/Application/SceneDelegate.swift diff --git a/14th-team5-iOS/App/Sources/Font/Pretendard-Black.otf b/14th-team5-iOS/Bibbi/Sources/Font/Pretendard-Black.otf similarity index 100% rename from 14th-team5-iOS/App/Sources/Font/Pretendard-Black.otf rename to 14th-team5-iOS/Bibbi/Sources/Font/Pretendard-Black.otf diff --git a/14th-team5-iOS/App/Sources/Font/Pretendard-Bold.otf b/14th-team5-iOS/Bibbi/Sources/Font/Pretendard-Bold.otf similarity index 100% rename from 14th-team5-iOS/App/Sources/Font/Pretendard-Bold.otf rename to 14th-team5-iOS/Bibbi/Sources/Font/Pretendard-Bold.otf diff --git a/14th-team5-iOS/App/Sources/Font/Pretendard-ExtraBold.otf b/14th-team5-iOS/Bibbi/Sources/Font/Pretendard-ExtraBold.otf similarity index 100% rename from 14th-team5-iOS/App/Sources/Font/Pretendard-ExtraBold.otf rename to 14th-team5-iOS/Bibbi/Sources/Font/Pretendard-ExtraBold.otf diff --git a/14th-team5-iOS/App/Sources/Font/Pretendard-ExtraLight.otf b/14th-team5-iOS/Bibbi/Sources/Font/Pretendard-ExtraLight.otf similarity index 100% rename from 14th-team5-iOS/App/Sources/Font/Pretendard-ExtraLight.otf rename to 14th-team5-iOS/Bibbi/Sources/Font/Pretendard-ExtraLight.otf diff --git a/14th-team5-iOS/App/Sources/Font/Pretendard-Light.otf b/14th-team5-iOS/Bibbi/Sources/Font/Pretendard-Light.otf similarity index 100% rename from 14th-team5-iOS/App/Sources/Font/Pretendard-Light.otf rename to 14th-team5-iOS/Bibbi/Sources/Font/Pretendard-Light.otf diff --git a/14th-team5-iOS/App/Sources/Font/Pretendard-Medium.otf b/14th-team5-iOS/Bibbi/Sources/Font/Pretendard-Medium.otf similarity index 100% rename from 14th-team5-iOS/App/Sources/Font/Pretendard-Medium.otf rename to 14th-team5-iOS/Bibbi/Sources/Font/Pretendard-Medium.otf diff --git a/14th-team5-iOS/App/Sources/Font/Pretendard-Regular.otf b/14th-team5-iOS/Bibbi/Sources/Font/Pretendard-Regular.otf similarity index 100% rename from 14th-team5-iOS/App/Sources/Font/Pretendard-Regular.otf rename to 14th-team5-iOS/Bibbi/Sources/Font/Pretendard-Regular.otf diff --git a/14th-team5-iOS/App/Sources/Font/Pretendard-SemiBold.otf b/14th-team5-iOS/Bibbi/Sources/Font/Pretendard-SemiBold.otf similarity index 100% rename from 14th-team5-iOS/App/Sources/Font/Pretendard-SemiBold.otf rename to 14th-team5-iOS/Bibbi/Sources/Font/Pretendard-SemiBold.otf diff --git a/14th-team5-iOS/App/Sources/Font/Pretendard-Thin.otf b/14th-team5-iOS/Bibbi/Sources/Font/Pretendard-Thin.otf similarity index 100% rename from 14th-team5-iOS/App/Sources/Font/Pretendard-Thin.otf rename to 14th-team5-iOS/Bibbi/Sources/Font/Pretendard-Thin.otf diff --git a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignIn/AccountSignInDIContainer.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Account/AccountSignIn/AccountSignInDIContainer.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Account/AccountSignIn/AccountSignInDIContainer.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Account/AccountSignIn/AccountSignInDIContainer.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignIn/AccountSignInReactor.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Account/AccountSignIn/AccountSignInReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Account/AccountSignIn/AccountSignInReactor.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Account/AccountSignIn/AccountSignInReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignIn/AccountSignInStrings.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Account/AccountSignIn/AccountSignInStrings.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Account/AccountSignIn/AccountSignInStrings.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Account/AccountSignIn/AccountSignInStrings.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignIn/AccountSignInViewController.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Account/AccountSignIn/AccountSignInViewController.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Account/AccountSignIn/AccountSignInViewController.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Account/AccountSignIn/AccountSignInViewController.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/AccountSignUpStrings.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Account/AccountSignUp/AccountSignUpStrings.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/AccountSignUpStrings.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Account/AccountSignUp/AccountSignUpStrings.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/Dependeyncy/AccountSignUpDIContainer.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Account/AccountSignUp/Dependeyncy/AccountSignUpDIContainer.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/Dependeyncy/AccountSignUpDIContainer.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Account/AccountSignUp/Dependeyncy/AccountSignUpDIContainer.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/Reactor/AccountSignUpReactor.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Account/AccountSignUp/Reactor/AccountSignUpReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/Reactor/AccountSignUpReactor.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Account/AccountSignUp/Reactor/AccountSignUpReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountDateViewController.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountDateViewController.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountDateViewController.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountDateViewController.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountNicknameViewController.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountNicknameViewController.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountNicknameViewController.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountNicknameViewController.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountProfileViewController.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountProfileViewController.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountProfileViewController.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountProfileViewController.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountSignUpViewController.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountSignUpViewController.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountSignUpViewController.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Account/AccountSignUp/ViewControllers/AccountSignUpViewController.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Config/CalendarImageCell+Type.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Calendar/Config/CalendarImageCell+Type.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Calendar/Config/CalendarImageCell+Type.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Calendar/Config/CalendarImageCell+Type.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactive/DataSource/DailyCalendarSectionModel.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Calendar/Reactive/DataSource/DailyCalendarSectionModel.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Calendar/Reactive/DataSource/DailyCalendarSectionModel.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Calendar/Reactive/DataSource/DailyCalendarSectionModel.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactive/DataSource/MonthlyCalendarSectionModel.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Calendar/Reactive/DataSource/MonthlyCalendarSectionModel.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Calendar/Reactive/DataSource/MonthlyCalendarSectionModel.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Calendar/Reactive/DataSource/MonthlyCalendarSectionModel.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactive/DelegateProxy/Delegate/MemoriesCalendarPostHeaderDelegate.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Calendar/Reactive/DelegateProxy/Delegate/MemoriesCalendarPostHeaderDelegate.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Calendar/Reactive/DelegateProxy/Delegate/MemoriesCalendarPostHeaderDelegate.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Calendar/Reactive/DelegateProxy/Delegate/MemoriesCalendarPostHeaderDelegate.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactive/DelegateProxy/RxFSCalendarDelegateProxy.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Calendar/Reactive/DelegateProxy/RxFSCalendarDelegateProxy.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Calendar/Reactive/DelegateProxy/RxFSCalendarDelegateProxy.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Calendar/Reactive/DelegateProxy/RxFSCalendarDelegateProxy.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactive/DelegateProxy/RxMemoriesCalendarPostDelegateProxy.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Calendar/Reactive/DelegateProxy/RxMemoriesCalendarPostDelegateProxy.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Calendar/Reactive/DelegateProxy/RxMemoriesCalendarPostDelegateProxy.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Calendar/Reactive/DelegateProxy/RxMemoriesCalendarPostDelegateProxy.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/Cell/MemoriesCalendarCellReactor.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Calendar/Reactor/Cell/MemoriesCalendarCellReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/Cell/MemoriesCalendarCellReactor.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Calendar/Reactor/Cell/MemoriesCalendarCellReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/Cell/MemoriesCalendarPageReactor.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Calendar/Reactor/Cell/MemoriesCalendarPageReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/Cell/MemoriesCalendarPageReactor.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Calendar/Reactor/Cell/MemoriesCalendarPageReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/Cell/MemoriesCalendarPostCellReactor.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Calendar/Reactor/Cell/MemoriesCalendarPostCellReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/Cell/MemoriesCalendarPostCellReactor.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Calendar/Reactor/Cell/MemoriesCalendarPostCellReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/DailyCalendarViewReactor.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Calendar/Reactor/DailyCalendarViewReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/DailyCalendarViewReactor.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Calendar/Reactor/DailyCalendarViewReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/MemoriesCalendarPostHeaderReactor.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Calendar/Reactor/MemoriesCalendarPostHeaderReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/MemoriesCalendarPostHeaderReactor.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Calendar/Reactor/MemoriesCalendarPostHeaderReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/MemoriesCalendarPostImageReactor.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Calendar/Reactor/MemoriesCalendarPostImageReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/MemoriesCalendarPostImageReactor.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Calendar/Reactor/MemoriesCalendarPostImageReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/MemoriesCalendarTitleViewReactor.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Calendar/Reactor/MemoriesCalendarTitleViewReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/MemoriesCalendarTitleViewReactor.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Calendar/Reactor/MemoriesCalendarTitleViewReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/MonthlyCalendarViewReactor.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Calendar/Reactor/MonthlyCalendarViewReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Calendar/Reactor/MonthlyCalendarViewReactor.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Calendar/Reactor/MonthlyCalendarViewReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/BannerView.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Calendar/View/BannerView.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Calendar/View/BannerView.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Calendar/View/BannerView.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/MemoriesCalendarCell.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Calendar/View/Cell/MemoriesCalendarCell.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/MemoriesCalendarCell.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Calendar/View/Cell/MemoriesCalendarCell.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/MemoriesCalendarPageViewCell.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Calendar/View/Cell/MemoriesCalendarPageViewCell.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/MemoriesCalendarPageViewCell.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Calendar/View/Cell/MemoriesCalendarPageViewCell.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/MemoriesCalendarPlaceholderCell.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Calendar/View/Cell/MemoriesCalendarPlaceholderCell.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/MemoriesCalendarPlaceholderCell.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Calendar/View/Cell/MemoriesCalendarPlaceholderCell.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/MemoriesCalendarPostCell.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Calendar/View/Cell/MemoriesCalendarPostCell.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Calendar/View/Cell/MemoriesCalendarPostCell.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Calendar/View/Cell/MemoriesCalendarPostCell.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/MemoriesCalendarPageTitleView.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Calendar/View/MemoriesCalendarPageTitleView.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Calendar/View/MemoriesCalendarPageTitleView.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Calendar/View/MemoriesCalendarPageTitleView.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/MemoriesCalendarPostHeaderView.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Calendar/View/MemoriesCalendarPostHeaderView.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Calendar/View/MemoriesCalendarPostHeaderView.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Calendar/View/MemoriesCalendarPostHeaderView.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/View/MemoriesCalendarPostImageView.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Calendar/View/MemoriesCalendarPostImageView.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Calendar/View/MemoriesCalendarPostImageView.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Calendar/View/MemoriesCalendarPostImageView.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/BannerHostingViewController.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Calendar/ViewController/BannerHostingViewController.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/BannerHostingViewController.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Calendar/ViewController/BannerHostingViewController.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/DailyCalendarViewController.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Calendar/ViewController/DailyCalendarViewController.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/DailyCalendarViewController.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Calendar/ViewController/DailyCalendarViewController.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/MonthlyCalendarViewController.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Calendar/ViewController/MonthlyCalendarViewController.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Calendar/ViewController/MonthlyCalendarViewController.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Calendar/ViewController/MonthlyCalendarViewController.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Calendar/ViewModel/BannerViewModel.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Calendar/ViewModel/BannerViewModel.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Calendar/ViewModel/BannerViewModel.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Calendar/ViewModel/BannerViewModel.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Camera/CameraDisplayViewController.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Camera/CameraDisplayViewController.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Camera/CameraDisplayViewController.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Camera/CameraDisplayViewController.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Camera/CameraViewController.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Camera/CameraViewController.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Camera/CameraViewController.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Camera/CameraViewController.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Camera/Cell/BibbiRealEmojiViewCell.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Camera/Cell/BibbiRealEmojiViewCell.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Camera/Cell/BibbiRealEmojiViewCell.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Camera/Cell/BibbiRealEmojiViewCell.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Camera/Cell/CellReactor/BibbiRealEmojiCellReactor.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Camera/Cell/CellReactor/BibbiRealEmojiCellReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Camera/Cell/CellReactor/BibbiRealEmojiCellReactor.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Camera/Cell/CellReactor/BibbiRealEmojiCellReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Camera/Cell/CellReactor/DisplayEditCellReactor.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Camera/Cell/CellReactor/DisplayEditCellReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Camera/Cell/CellReactor/DisplayEditCellReactor.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Camera/Cell/CellReactor/DisplayEditCellReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Camera/Cell/DisplayEditCollectionViewCell.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Camera/Cell/DisplayEditCollectionViewCell.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Camera/Cell/DisplayEditCollectionViewCell.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Camera/Cell/DisplayEditCollectionViewCell.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Camera/DataSources/DisplayEditSectionModel.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Camera/DataSources/DisplayEditSectionModel.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Camera/DataSources/DisplayEditSectionModel.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Camera/DataSources/DisplayEditSectionModel.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Camera/DataSources/EmojiSectionModel.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Camera/DataSources/EmojiSectionModel.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Camera/DataSources/EmojiSectionModel.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Camera/DataSources/EmojiSectionModel.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraDisplayViewReactor.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Camera/Reactor/CameraDisplayViewReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraDisplayViewReactor.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Camera/Reactor/CameraDisplayViewReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraViewReactor.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Camera/Reactor/CameraViewReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Camera/Reactor/CameraViewReactor.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Camera/Reactor/CameraViewReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Camera/View/BibbiMissionView.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Camera/View/BibbiMissionView.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Camera/View/BibbiMissionView.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Camera/View/BibbiMissionView.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Comment/Reactive/DataSource/CommentSectionModel.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Comment/Reactive/DataSource/CommentSectionModel.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Comment/Reactive/DataSource/CommentSectionModel.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Comment/Reactive/DataSource/CommentSectionModel.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Comment/Reactive/DelegateProxy/Delegate/CommentTextFieldDelegate.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Comment/Reactive/DelegateProxy/Delegate/CommentTextFieldDelegate.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Comment/Reactive/DelegateProxy/Delegate/CommentTextFieldDelegate.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Comment/Reactive/DelegateProxy/Delegate/CommentTextFieldDelegate.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Comment/Reactive/DelegateProxy/RxCommentTextFieldDelegateProxy.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Comment/Reactive/DelegateProxy/RxCommentTextFieldDelegateProxy.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Comment/Reactive/DelegateProxy/RxCommentTextFieldDelegateProxy.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Comment/Reactive/DelegateProxy/RxCommentTextFieldDelegateProxy.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Comment/Reactor/Cell/CommentCellReactor.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Comment/Reactor/Cell/CommentCellReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Comment/Reactor/Cell/CommentCellReactor.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Comment/Reactor/Cell/CommentCellReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Comment/Reactor/CommentTableReactor.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Comment/Reactor/CommentTableReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Comment/Reactor/CommentTableReactor.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Comment/Reactor/CommentTableReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Comment/Reactor/CommentTextFieldReactor.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Comment/Reactor/CommentTextFieldReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Comment/Reactor/CommentTextFieldReactor.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Comment/Reactor/CommentTextFieldReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Comment/Reactor/CommentViewReactor.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Comment/Reactor/CommentViewReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Comment/Reactor/CommentViewReactor.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Comment/Reactor/CommentViewReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Comment/View/Cell/CommentCell.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Comment/View/Cell/CommentCell.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Comment/View/Cell/CommentCell.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Comment/View/Cell/CommentCell.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Comment/View/CommentTableView.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Comment/View/CommentTableView.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Comment/View/CommentTableView.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Comment/View/CommentTableView.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Comment/View/CommentTextFieldView.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Comment/View/CommentTextFieldView.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Comment/View/CommentTextFieldView.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Comment/View/CommentTextFieldView.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Comment/View/CommentTopBarView.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Comment/View/CommentTopBarView.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Comment/View/CommentTopBarView.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Comment/View/CommentTopBarView.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Comment/View/NoneCommentView.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Comment/View/NoneCommentView.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Comment/View/NoneCommentView.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Comment/View/NoneCommentView.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Comment/ViewController/CommentViewController.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Comment/ViewController/CommentViewController.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Comment/ViewController/CommentViewController.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Comment/ViewController/CommentViewController.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Content/Dependency/WebContentDIContainer.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Content/Dependency/WebContentDIContainer.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Content/Dependency/WebContentDIContainer.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Content/Dependency/WebContentDIContainer.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Content/Reactor/WebContentReactor.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Content/Reactor/WebContentReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Content/Reactor/WebContentReactor.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Content/Reactor/WebContentReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Content/WebContentViewController.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Content/WebContentViewController.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Content/WebContentViewController.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Content/WebContentViewController.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Dependency/InputFamilyLinkDIContainer.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/FamilyEntrance/Dependency/InputFamilyLinkDIContainer.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Dependency/InputFamilyLinkDIContainer.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/FamilyEntrance/Dependency/InputFamilyLinkDIContainer.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Dependency/JoinFamilyDIContainer.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/FamilyEntrance/Dependency/JoinFamilyDIContainer.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Dependency/JoinFamilyDIContainer.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/FamilyEntrance/Dependency/JoinFamilyDIContainer.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Dependency/JoinedFamilyDIContainer.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/FamilyEntrance/Dependency/JoinedFamilyDIContainer.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Dependency/JoinedFamilyDIContainer.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/FamilyEntrance/Dependency/JoinedFamilyDIContainer.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Reactor/FamilyEntranceReactor.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/FamilyEntrance/Reactor/FamilyEntranceReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Reactor/FamilyEntranceReactor.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/FamilyEntrance/Reactor/FamilyEntranceReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Reactor/InputFamilyLinkReactor.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/FamilyEntrance/Reactor/InputFamilyLinkReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Reactor/InputFamilyLinkReactor.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/FamilyEntrance/Reactor/InputFamilyLinkReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Reactor/JoinFamilyReactor.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/FamilyEntrance/Reactor/JoinFamilyReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Reactor/JoinFamilyReactor.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/FamilyEntrance/Reactor/JoinFamilyReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Strings/JoinFamilyStrings.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/FamilyEntrance/Strings/JoinFamilyStrings.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Strings/JoinFamilyStrings.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/FamilyEntrance/Strings/JoinFamilyStrings.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Strings/inviteFamilyLinkStrings.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/FamilyEntrance/Strings/inviteFamilyLinkStrings.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Strings/inviteFamilyLinkStrings.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/FamilyEntrance/Strings/inviteFamilyLinkStrings.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Strings/joinedFamilyStrings.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/FamilyEntrance/Strings/joinedFamilyStrings.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Strings/joinedFamilyStrings.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/FamilyEntrance/Strings/joinedFamilyStrings.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/ViewController/FamilyEntranceViewController.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/FamilyEntrance/ViewController/FamilyEntranceViewController.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/ViewController/FamilyEntranceViewController.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/FamilyEntrance/ViewController/FamilyEntranceViewController.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/ViewController/InputFamilyLinkViewController.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/FamilyEntrance/ViewController/InputFamilyLinkViewController.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/ViewController/InputFamilyLinkViewController.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/FamilyEntrance/ViewController/InputFamilyLinkViewController.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/ViewController/JoinFamilyViewController.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/FamilyEntrance/ViewController/JoinFamilyViewController.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/ViewController/JoinFamilyViewController.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/FamilyEntrance/ViewController/JoinFamilyViewController.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Views/MakeNewFamilyView.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/FamilyEntrance/Views/MakeNewFamilyView.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/FamilyEntrance/Views/MakeNewFamilyView.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/FamilyEntrance/Views/MakeNewFamilyView.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Model/Balloon.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Home/Model/Balloon.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Home/Model/Balloon.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Home/Model/Balloon.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Model/DescriptionText.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Home/Model/DescriptionText.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Home/Model/DescriptionText.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Home/Model/DescriptionText.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Model/FamilySection.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Home/Model/FamilySection.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Home/Model/FamilySection.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Home/Model/FamilySection.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Model/PostSection.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Home/Model/PostSection.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Home/Model/PostSection.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Home/Model/PostSection.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Model/String/HomeCGFloat.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Home/Model/String/HomeCGFloat.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Home/Model/String/HomeCGFloat.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Home/Model/String/HomeCGFloat.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Model/String/HomeString.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Home/Model/String/HomeString.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Home/Model/String/HomeString.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Home/Model/String/HomeString.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/BalloonReactor.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Home/Reactor/BalloonReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Home/Reactor/BalloonReactor.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Home/Reactor/BalloonReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/Cell/MainFamilyCellReactor.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Home/Reactor/Cell/MainFamilyCellReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Home/Reactor/Cell/MainFamilyCellReactor.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Home/Reactor/Cell/MainFamilyCellReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/Cell/MainPostCellReactor.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Home/Reactor/Cell/MainPostCellReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Home/Reactor/Cell/MainPostCellReactor.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Home/Reactor/Cell/MainPostCellReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/ContributorProfileReactor.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Home/Reactor/ContributorProfileReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Home/Reactor/ContributorProfileReactor.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Home/Reactor/ContributorProfileReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/ContributorReactor.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Home/Reactor/ContributorReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Home/Reactor/ContributorReactor.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Home/Reactor/ContributorReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainCameraReactor.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Home/Reactor/MainCameraReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainCameraReactor.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Home/Reactor/MainCameraReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainFamilyViewReactor.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Home/Reactor/MainFamilyViewReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainFamilyViewReactor.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Home/Reactor/MainFamilyViewReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainPostViewReactor.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Home/Reactor/MainPostViewReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainPostViewReactor.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Home/Reactor/MainPostViewReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Home/Reactor/MainViewReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Home/Reactor/MainViewReactor.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Home/Reactor/MainViewReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Reactor/TimerReactor.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Home/Reactor/TimerReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Home/Reactor/TimerReactor.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Home/Reactor/TimerReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainFamilyViewController.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Home/ViewControllers/MainFamilyViewController.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainFamilyViewController.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Home/ViewControllers/MainFamilyViewController.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainPostViewController.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Home/ViewControllers/MainPostViewController.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainPostViewController.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Home/ViewControllers/MainPostViewController.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Home/ViewControllers/MainViewController.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/MainViewController.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Home/ViewControllers/MainViewController.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/SegmentPageViewController.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Home/ViewControllers/SegmentPageViewController.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Home/ViewControllers/SegmentPageViewController.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Home/ViewControllers/SegmentPageViewController.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Views/BalloonView.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Home/Views/BalloonView.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Home/Views/BalloonView.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Home/Views/BalloonView.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Views/ContributorProfileView.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Home/Views/ContributorProfileView.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Home/Views/ContributorProfileView.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Home/Views/ContributorProfileView.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Views/ContributorView.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Home/Views/ContributorView.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Home/Views/ContributorView.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Home/Views/ContributorView.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Views/InviteFamilyView.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Home/Views/InviteFamilyView.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Home/Views/InviteFamilyView.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Home/Views/InviteFamilyView.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Views/MainCameraButtonView.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Home/Views/MainCameraButtonView.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Home/Views/MainCameraButtonView.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Home/Views/MainCameraButtonView.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Views/MainFamilyCollectionViewCell.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Home/Views/MainFamilyCollectionViewCell.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Home/Views/MainFamilyCollectionViewCell.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Home/Views/MainFamilyCollectionViewCell.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Views/MainPostCollectionViewCell.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Home/Views/MainPostCollectionViewCell.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Home/Views/MainPostCollectionViewCell.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Home/Views/MainPostCollectionViewCell.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Views/NoPostTodayView.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Home/Views/NoPostTodayView.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Home/Views/NoPostTodayView.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Home/Views/NoPostTodayView.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Home/Views/TimerView.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Home/Views/TimerView.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Home/Views/TimerView.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Home/Views/TimerView.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Management/Activity/CopyInvitationUrlActivity.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Management/Activity/CopyInvitationUrlActivity.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Management/Activity/CopyInvitationUrlActivity.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Management/Activity/CopyInvitationUrlActivity.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Management/Activity/UrlActivityItemSource.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Management/Activity/UrlActivityItemSource.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Management/Activity/UrlActivityItemSource.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Management/Activity/UrlActivityItemSource.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Management/Config/FamilyMemberCellKind.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Management/Config/FamilyMemberCellKind.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Management/Config/FamilyMemberCellKind.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Management/Config/FamilyMemberCellKind.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Management/Reactive/DataSource/FamilyMemberSectionModel.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Management/Reactive/DataSource/FamilyMemberSectionModel.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Management/Reactive/DataSource/FamilyMemberSectionModel.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Management/Reactive/DataSource/FamilyMemberSectionModel.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Management/Reactive/DelegateProxy/Delegate/ManagementTableDelegate.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Management/Reactive/DelegateProxy/Delegate/ManagementTableDelegate.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Management/Reactive/DelegateProxy/Delegate/ManagementTableDelegate.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Management/Reactive/DelegateProxy/Delegate/ManagementTableDelegate.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Management/Reactive/DelegateProxy/Delegate/ManagementTableHeaderDelegate.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Management/Reactive/DelegateProxy/Delegate/ManagementTableHeaderDelegate.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Management/Reactive/DelegateProxy/Delegate/ManagementTableHeaderDelegate.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Management/Reactive/DelegateProxy/Delegate/ManagementTableHeaderDelegate.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Management/Reactive/DelegateProxy/Delegate/SharingContainerDelegate.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Management/Reactive/DelegateProxy/Delegate/SharingContainerDelegate.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Management/Reactive/DelegateProxy/Delegate/SharingContainerDelegate.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Management/Reactive/DelegateProxy/Delegate/SharingContainerDelegate.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Management/Reactive/DelegateProxy/RxManagementTableDelegateProxy.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Management/Reactive/DelegateProxy/RxManagementTableDelegateProxy.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Management/Reactive/DelegateProxy/RxManagementTableDelegateProxy.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Management/Reactive/DelegateProxy/RxManagementTableDelegateProxy.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Management/Reactive/DelegateProxy/RxManagementTableHeaderDelegateProxy.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Management/Reactive/DelegateProxy/RxManagementTableHeaderDelegateProxy.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Management/Reactive/DelegateProxy/RxManagementTableHeaderDelegateProxy.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Management/Reactive/DelegateProxy/RxManagementTableHeaderDelegateProxy.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Management/Reactive/DelegateProxy/RxSharingContainerDelegateProxy.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Management/Reactive/DelegateProxy/RxSharingContainerDelegateProxy.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Management/Reactive/DelegateProxy/RxSharingContainerDelegateProxy.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Management/Reactive/DelegateProxy/RxSharingContainerDelegateProxy.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Management/Reactor/Cell/FamilyMemberCellReactor.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Management/Reactor/Cell/FamilyMemberCellReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Management/Reactor/Cell/FamilyMemberCellReactor.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Management/Reactor/Cell/FamilyMemberCellReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Management/Reactor/FamilyNameSettingViewReactor.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Management/Reactor/FamilyNameSettingViewReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Management/Reactor/FamilyNameSettingViewReactor.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Management/Reactor/FamilyNameSettingViewReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Management/Reactor/ManagementReactor.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Management/Reactor/ManagementReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Management/Reactor/ManagementReactor.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Management/Reactor/ManagementReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Management/Reactor/ManagementTableHeaderReactor.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Management/Reactor/ManagementTableHeaderReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Management/Reactor/ManagementTableHeaderReactor.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Management/Reactor/ManagementTableHeaderReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Management/Reactor/ManagementTableReactor.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Management/Reactor/ManagementTableReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Management/Reactor/ManagementTableReactor.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Management/Reactor/ManagementTableReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Management/Reactor/SharingContainerReactor.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Management/Reactor/SharingContainerReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Management/Reactor/SharingContainerReactor.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Management/Reactor/SharingContainerReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Management/View/Cell/FamilyMemberCell.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Management/View/Cell/FamilyMemberCell.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Management/View/Cell/FamilyMemberCell.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Management/View/Cell/FamilyMemberCell.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Management/View/JoinFamilyGroupEditorView.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Management/View/JoinFamilyGroupEditorView.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Management/View/JoinFamilyGroupEditorView.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Management/View/JoinFamilyGroupEditorView.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Management/View/ManagementTableHeaderView.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Management/View/ManagementTableHeaderView.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Management/View/ManagementTableHeaderView.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Management/View/ManagementTableHeaderView.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Management/View/ManagementTableView.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Management/View/ManagementTableView.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Management/View/ManagementTableView.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Management/View/ManagementTableView.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Management/View/SharingContainerView.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Management/View/SharingContainerView.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Management/View/SharingContainerView.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Management/View/SharingContainerView.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Management/ViewController/FamilyNameSettingViewController.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Management/ViewController/FamilyNameSettingViewController.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Management/ViewController/FamilyNameSettingViewController.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Management/ViewController/FamilyNameSettingViewController.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Management/ViewController/ManagementViewController.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Management/ViewController/ManagementViewController.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Management/ViewController/ManagementViewController.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Management/ViewController/ManagementViewController.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/OnBoarding/Dependency/OnBoardingDIContainer.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/OnBoarding/Dependency/OnBoardingDIContainer.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/OnBoarding/Dependency/OnBoardingDIContainer.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/OnBoarding/Dependency/OnBoardingDIContainer.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/OnBoarding/OnBoarding.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/OnBoarding/OnBoarding.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/OnBoarding/OnBoarding.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/OnBoarding/OnBoarding.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/OnBoarding/OnBoardingReactor.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/OnBoarding/OnBoardingReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/OnBoarding/OnBoardingReactor.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/OnBoarding/OnBoardingReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/OnBoarding/OnBoardingStrings.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/OnBoarding/OnBoardingStrings.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/OnBoarding/OnBoardingStrings.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/OnBoarding/OnBoardingStrings.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/OnBoarding/OnBoardingViewCell.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/OnBoarding/OnBoardingViewCell.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/OnBoarding/OnBoardingViewCell.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/OnBoarding/OnBoardingViewCell.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/OnBoarding/OnBoardingViewController.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/OnBoarding/OnBoardingViewController.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/OnBoarding/OnBoardingViewController.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/OnBoarding/OnBoardingViewController.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Model/ReactionSection.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/PostDetail/Model/ReactionSection.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/PostDetail/Model/ReactionSection.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/PostDetail/Model/ReactionSection.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Model/SectionOfPostDetail.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/PostDetail/Model/SectionOfPostDetail.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/PostDetail/Model/SectionOfPostDetail.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/PostDetail/Model/SectionOfPostDetail.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Model/String/PostCGFloat.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/PostDetail/Model/String/PostCGFloat.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/PostDetail/Model/String/PostCGFloat.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/PostDetail/Model/String/PostCGFloat.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Model/String/PostString.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/PostDetail/Model/String/PostString.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/PostDetail/Model/String/PostString.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/PostDetail/Model/String/PostString.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/MissionTextReactor.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/PostDetail/Reactor/MissionTextReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/MissionTextReactor.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/PostDetail/Reactor/MissionTextReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostDetailViewReactor.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/PostDetail/Reactor/PostDetailViewReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostDetailViewReactor.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/PostDetail/Reactor/PostDetailViewReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostReactor.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/PostDetail/Reactor/PostReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/PostReactor.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/PostDetail/Reactor/PostReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/ReactionMemberViewReactor.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/PostDetail/Reactor/ReactionMemberViewReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/ReactionMemberViewReactor.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/PostDetail/Reactor/ReactionMemberViewReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/ReactionViewReactor.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/PostDetail/Reactor/ReactionViewReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/ReactionViewReactor.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/PostDetail/Reactor/ReactionViewReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/SelectableEmojiReactor.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/PostDetail/Reactor/SelectableEmojiReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/SelectableEmojiReactor.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/PostDetail/Reactor/SelectableEmojiReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/TempCellReactor.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/PostDetail/Reactor/TempCellReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/PostDetail/Reactor/TempCellReactor.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/PostDetail/Reactor/TempCellReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/PostViewController.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/PostDetail/ViewControllers/PostViewController.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/PostViewController.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/PostDetail/ViewControllers/PostViewController.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/ReactionMembersViewController.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/PostDetail/ViewControllers/ReactionMembersViewController.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/ReactionMembersViewController.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/PostDetail/ViewControllers/ReactionMembersViewController.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/ReactionViewController.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/PostDetail/ViewControllers/ReactionViewController.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/ReactionViewController.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/PostDetail/ViewControllers/ReactionViewController.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/SelectableEmojiViewController.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/PostDetail/ViewControllers/SelectableEmojiViewController.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/PostDetail/ViewControllers/SelectableEmojiViewController.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/PostDetail/ViewControllers/SelectableEmojiViewController.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/AddCommentCollectionViewCell.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/PostDetail/Views/AddCommentCollectionViewCell.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/AddCommentCollectionViewCell.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/PostDetail/Views/AddCommentCollectionViewCell.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/AddReactionCollectionViewCell.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/PostDetail/Views/AddReactionCollectionViewCell.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/AddReactionCollectionViewCell.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/PostDetail/Views/AddReactionCollectionViewCell.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/MissionTextView.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/PostDetail/Views/MissionTextView.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/MissionTextView.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/PostDetail/Views/MissionTextView.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/PostDetailCollectionViewCell.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/PostDetail/Views/PostDetailCollectionViewCell.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/PostDetailCollectionViewCell.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/PostDetail/Views/PostDetailCollectionViewCell.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/PostNavigationView.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/PostDetail/Views/PostNavigationView.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/PostNavigationView.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/PostDetail/Views/PostNavigationView.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/ReactionCollectionViewCell.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/PostDetail/Views/ReactionCollectionViewCell.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/ReactionCollectionViewCell.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/PostDetail/Views/ReactionCollectionViewCell.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/RightAlignedFlowLayout.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/PostDetail/Views/RightAlignedFlowLayout.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/RightAlignedFlowLayout.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/PostDetail/Views/RightAlignedFlowLayout.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/SeletableEmojiCollectionViewCell.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/PostDetail/Views/SeletableEmojiCollectionViewCell.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/PostDetail/Views/SeletableEmojiCollectionViewCell.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/PostDetail/Views/SeletableEmojiCollectionViewCell.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Privacy/Cell/CellReactor/PrivacyCellReactor.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Privacy/Cell/CellReactor/PrivacyCellReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Privacy/Cell/CellReactor/PrivacyCellReactor.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Privacy/Cell/CellReactor/PrivacyCellReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Privacy/Cell/DataSources/PrivacySectionModel.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Privacy/Cell/DataSources/PrivacySectionModel.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Privacy/Cell/DataSources/PrivacySectionModel.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Privacy/Cell/DataSources/PrivacySectionModel.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Privacy/Cell/PrivacyAuthorizationTableViewCell.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Privacy/Cell/PrivacyAuthorizationTableViewCell.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Privacy/Cell/PrivacyAuthorizationTableViewCell.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Privacy/Cell/PrivacyAuthorizationTableViewCell.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Privacy/Cell/PrivacyTableViewCell.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Privacy/Cell/PrivacyTableViewCell.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Privacy/Cell/PrivacyTableViewCell.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Privacy/Cell/PrivacyTableViewCell.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Privacy/PrivacyViewController.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Privacy/PrivacyViewController.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Privacy/PrivacyViewController.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Privacy/PrivacyViewController.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Privacy/Reactor/PrivacyViewReactor.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Privacy/Reactor/PrivacyViewReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Privacy/Reactor/PrivacyViewReactor.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Privacy/Reactor/PrivacyViewReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Privacy/ReusableView/PrivacyAuthorizationHeaderFooterView.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Privacy/ReusableView/PrivacyAuthorizationHeaderFooterView.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Privacy/ReusableView/PrivacyAuthorizationHeaderFooterView.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Privacy/ReusableView/PrivacyAuthorizationHeaderFooterView.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Privacy/ReusableView/PrivacyHeaderFooterView.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Privacy/ReusableView/PrivacyHeaderFooterView.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Privacy/ReusableView/PrivacyHeaderFooterView.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Privacy/ReusableView/PrivacyHeaderFooterView.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Privacy/View/BibbiInquireBannerView.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Privacy/View/BibbiInquireBannerView.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Privacy/View/BibbiInquireBannerView.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Privacy/View/BibbiInquireBannerView.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/Cell/CellReactor/ProfileFeedCellReactor.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Profile/Cell/CellReactor/ProfileFeedCellReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Profile/Cell/CellReactor/ProfileFeedCellReactor.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Profile/Cell/CellReactor/ProfileFeedCellReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/Cell/CellReactor/ProfileFeedDescrptionCellReactor.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Profile/Cell/CellReactor/ProfileFeedDescrptionCellReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Profile/Cell/CellReactor/ProfileFeedDescrptionCellReactor.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Profile/Cell/CellReactor/ProfileFeedDescrptionCellReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/Cell/CellReactor/ProfileFeedEmptyCellReactor.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Profile/Cell/CellReactor/ProfileFeedEmptyCellReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Profile/Cell/CellReactor/ProfileFeedEmptyCellReactor.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Profile/Cell/CellReactor/ProfileFeedEmptyCellReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/Cell/ProfileFeedCollectionViewCell.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Profile/Cell/ProfileFeedCollectionViewCell.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Profile/Cell/ProfileFeedCollectionViewCell.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Profile/Cell/ProfileFeedCollectionViewCell.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/Cell/ProfileFeedDescrptionCell.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Profile/Cell/ProfileFeedDescrptionCell.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Profile/Cell/ProfileFeedDescrptionCell.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Profile/Cell/ProfileFeedDescrptionCell.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/Cell/ProfileFeedEmptyCollectionViewCell.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Profile/Cell/ProfileFeedEmptyCollectionViewCell.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Profile/Cell/ProfileFeedEmptyCollectionViewCell.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Profile/Cell/ProfileFeedEmptyCollectionViewCell.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/DataSources/ProfileFeedDescrptionSectionModel.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Profile/DataSources/ProfileFeedDescrptionSectionModel.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Profile/DataSources/ProfileFeedDescrptionSectionModel.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Profile/DataSources/ProfileFeedDescrptionSectionModel.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/DataSources/ProfileFeedSectionModel.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Profile/DataSources/ProfileFeedSectionModel.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Profile/DataSources/ProfileFeedSectionModel.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Profile/DataSources/ProfileFeedSectionModel.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileDetailViewController.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Profile/ProfileDetailViewController.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Profile/ProfileDetailViewController.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Profile/ProfileDetailViewController.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileFeedPageViewController.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Profile/ProfileFeedPageViewController.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Profile/ProfileFeedPageViewController.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Profile/ProfileFeedPageViewController.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileFeedViewController.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Profile/ProfileFeedViewController.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Profile/ProfileFeedViewController.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Profile/ProfileFeedViewController.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/ProfileViewController.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Profile/ProfileViewController.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Profile/ProfileViewController.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Profile/ProfileViewController.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileDetailViewReactor.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Profile/Reactor/ProfileDetailViewReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileDetailViewReactor.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Profile/Reactor/ProfileDetailViewReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileFeedPageViewReactor.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Profile/Reactor/ProfileFeedPageViewReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileFeedPageViewReactor.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Profile/Reactor/ProfileFeedPageViewReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileFeedViewReactor.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Profile/Reactor/ProfileFeedViewReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileFeedViewReactor.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Profile/Reactor/ProfileFeedViewReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Profile/Reactor/ProfileViewReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Profile/View/BibbiProfileView.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Profile/View/BibbiProfileView.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Profile/View/BibbiProfileView.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Profile/View/BibbiProfileView.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Resign/AccountResignViewController.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Resign/AccountResignViewController.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Resign/AccountResignViewController.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Resign/AccountResignViewController.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Resign/Cell/ResignReaonsTableViewCell.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Resign/Cell/ResignReaonsTableViewCell.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Resign/Cell/ResignReaonsTableViewCell.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Resign/Cell/ResignReaonsTableViewCell.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Resign/Config/ReasonType.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Resign/Config/ReasonType.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Resign/Config/ReasonType.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Resign/Config/ReasonType.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Resign/DataSources/AccountResignSectionModel.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Resign/DataSources/AccountResignSectionModel.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Resign/DataSources/AccountResignSectionModel.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Resign/DataSources/AccountResignSectionModel.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Resign/Reactor/AccountResignViewReactor.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Resign/Reactor/AccountResignViewReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Resign/Reactor/AccountResignViewReactor.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Resign/Reactor/AccountResignViewReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Resign/Reactor/ResignReasonTableViewCellReactor.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Resign/Reactor/ResignReasonTableViewCellReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Resign/Reactor/ResignReasonTableViewCellReactor.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Resign/Reactor/ResignReasonTableViewCellReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Splash/Dependency/SplashDIContainer.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Splash/Dependency/SplashDIContainer.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Splash/Dependency/SplashDIContainer.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Splash/Dependency/SplashDIContainer.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Splash/SplashReactor.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Splash/SplashReactor.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Splash/SplashReactor.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Splash/SplashReactor.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Splash/SplashViewController.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Splash/SplashViewController.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Splash/SplashViewController.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Splash/SplashViewController.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Trash/AirplaneLottieView.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Trash/AirplaneLottieView.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Trash/AirplaneLottieView.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Trash/AirplaneLottieView.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Trash/BibbiFetchFailureView.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Trash/BibbiFetchFailureView.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Trash/BibbiFetchFailureView.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Trash/BibbiFetchFailureView.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Trash/BlurAiraplaneLottieView.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Trash/BlurAiraplaneLottieView.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Trash/BlurAiraplaneLottieView.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Trash/BlurAiraplaneLottieView.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Trash/DIContainer/CalendarCellDIContainer.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Trash/DIContainer/CalendarCellDIContainer.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Trash/DIContainer/CalendarCellDIContainer.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Trash/DIContainer/CalendarCellDIContainer.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Trash/DIContainer/CalendarImageCellDIContainer.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Trash/DIContainer/CalendarImageCellDIContainer.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Trash/DIContainer/CalendarImageCellDIContainer.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Trash/DIContainer/CalendarImageCellDIContainer.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Trash/DIContainer/CalendarPostCellDIContainer.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Trash/DIContainer/CalendarPostCellDIContainer.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Trash/DIContainer/CalendarPostCellDIContainer.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Trash/DIContainer/CalendarPostCellDIContainer.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Trash/DIContainer/CalendarPostDIContainer.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Trash/DIContainer/CalendarPostDIContainer.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Trash/DIContainer/CalendarPostDIContainer.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Trash/DIContainer/CalendarPostDIContainer.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Trash/DIContainer/MonthlyCalendarDIConatainer.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Trash/DIContainer/MonthlyCalendarDIConatainer.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Trash/DIContainer/MonthlyCalendarDIConatainer.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Trash/DIContainer/MonthlyCalendarDIConatainer.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Trash/Dependency/FamilyManagementDIContainer.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Trash/Dependency/FamilyManagementDIContainer.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Trash/Dependency/FamilyManagementDIContainer.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Trash/Dependency/FamilyManagementDIContainer.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Trash/Dependency/InvitationUrlContainerDIContainer.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Trash/Dependency/InvitationUrlContainerDIContainer.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Trash/Dependency/InvitationUrlContainerDIContainer.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Trash/Dependency/InvitationUrlContainerDIContainer.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Trash/Dependencygg/PostCommentDIContainer.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Trash/Dependencygg/PostCommentDIContainer.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Trash/Dependencygg/PostCommentDIContainer.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Trash/Dependencygg/PostCommentDIContainer.swift diff --git a/14th-team5-iOS/App/Sources/Presentation/Trash/Manager/DeepLinkManager.swift b/14th-team5-iOS/Bibbi/Sources/Presentation/Trash/Manager/DeepLinkManager.swift similarity index 100% rename from 14th-team5-iOS/App/Sources/Presentation/Trash/Manager/DeepLinkManager.swift rename to 14th-team5-iOS/Bibbi/Sources/Presentation/Trash/Manager/DeepLinkManager.swift diff --git a/14th-team5-iOS/App/WidgetExtension/Resources/empty.txt b/14th-team5-iOS/Bibbi/WidgetExtension/Resources/empty.txt similarity index 100% rename from 14th-team5-iOS/App/WidgetExtension/Resources/empty.txt rename to 14th-team5-iOS/Bibbi/WidgetExtension/Resources/empty.txt diff --git a/14th-team5-iOS/App/WidgetExtension/Sources/FamilyWidget/Family.swift b/14th-team5-iOS/Bibbi/WidgetExtension/Sources/FamilyWidget/Family.swift similarity index 100% rename from 14th-team5-iOS/App/WidgetExtension/Sources/FamilyWidget/Family.swift rename to 14th-team5-iOS/Bibbi/WidgetExtension/Sources/FamilyWidget/Family.swift diff --git a/14th-team5-iOS/App/WidgetExtension/Sources/FamilyWidget/FamilyService.swift b/14th-team5-iOS/Bibbi/WidgetExtension/Sources/FamilyWidget/FamilyService.swift similarity index 100% rename from 14th-team5-iOS/App/WidgetExtension/Sources/FamilyWidget/FamilyService.swift rename to 14th-team5-iOS/Bibbi/WidgetExtension/Sources/FamilyWidget/FamilyService.swift diff --git a/14th-team5-iOS/App/WidgetExtension/Sources/FamilyWidget/FamilyWidget.swift b/14th-team5-iOS/Bibbi/WidgetExtension/Sources/FamilyWidget/FamilyWidget.swift similarity index 100% rename from 14th-team5-iOS/App/WidgetExtension/Sources/FamilyWidget/FamilyWidget.swift rename to 14th-team5-iOS/Bibbi/WidgetExtension/Sources/FamilyWidget/FamilyWidget.swift diff --git a/14th-team5-iOS/App/WidgetExtension/Sources/FamilyWidget/FamilyWidgetDIContainer.swift b/14th-team5-iOS/Bibbi/WidgetExtension/Sources/FamilyWidget/FamilyWidgetDIContainer.swift similarity index 100% rename from 14th-team5-iOS/App/WidgetExtension/Sources/FamilyWidget/FamilyWidgetDIContainer.swift rename to 14th-team5-iOS/Bibbi/WidgetExtension/Sources/FamilyWidget/FamilyWidgetDIContainer.swift diff --git a/14th-team5-iOS/App/WidgetExtension/Sources/FamilyWidget/FamilyWidgetEntry.swift b/14th-team5-iOS/Bibbi/WidgetExtension/Sources/FamilyWidget/FamilyWidgetEntry.swift similarity index 100% rename from 14th-team5-iOS/App/WidgetExtension/Sources/FamilyWidget/FamilyWidgetEntry.swift rename to 14th-team5-iOS/Bibbi/WidgetExtension/Sources/FamilyWidget/FamilyWidgetEntry.swift diff --git a/14th-team5-iOS/App/WidgetExtension/Sources/FamilyWidget/FamilyWidgetTimelineProvider.swift b/14th-team5-iOS/Bibbi/WidgetExtension/Sources/FamilyWidget/FamilyWidgetTimelineProvider.swift similarity index 100% rename from 14th-team5-iOS/App/WidgetExtension/Sources/FamilyWidget/FamilyWidgetTimelineProvider.swift rename to 14th-team5-iOS/Bibbi/WidgetExtension/Sources/FamilyWidget/FamilyWidgetTimelineProvider.swift diff --git a/14th-team5-iOS/App/WidgetExtension/Sources/FamilyWidget/FamilyWidgetView.swift b/14th-team5-iOS/Bibbi/WidgetExtension/Sources/FamilyWidget/FamilyWidgetView.swift similarity index 100% rename from 14th-team5-iOS/App/WidgetExtension/Sources/FamilyWidget/FamilyWidgetView.swift rename to 14th-team5-iOS/Bibbi/WidgetExtension/Sources/FamilyWidget/FamilyWidgetView.swift diff --git a/14th-team5-iOS/Core/Project.swift b/14th-team5-iOS/Core/Project.swift index 29a07792d..95a79e78a 100644 --- a/14th-team5-iOS/Core/Project.swift +++ b/14th-team5-iOS/Core/Project.swift @@ -5,7 +5,7 @@ // Created by Kim dohyun on 2023/11/14. // -import ProjectDescription +@preconcurrency import ProjectDescription import ProjectDescriptionHelpers private let targets: [Target] = [ diff --git a/14th-team5-iOS/Core/Sources/Extensions/Bundle+Ext.swift b/14th-team5-iOS/Core/Sources/Extensions/Bundle+Ext.swift index 27224aafc..1fb186080 100644 --- a/14th-team5-iOS/Core/Sources/Extensions/Bundle+Ext.swift +++ b/14th-team5-iOS/Core/Sources/Extensions/Bundle+Ext.swift @@ -65,6 +65,6 @@ extension Bundle { } public var xAppKey: String { - "39d7cdd1-218c-4abd-addb-ab4efff8a369" + "4c9b1c45-9dba-4813-b7c0-b079c08befe6" } } diff --git a/14th-team5-iOS/Core/Sources/Trash/BBNetwork/API.swift b/14th-team5-iOS/Core/Sources/Trash/BBNetwork/API.swift index a72bba239..d287cdb3a 100644 --- a/14th-team5-iOS/Core/Sources/Trash/BBNetwork/API.swift +++ b/14th-team5-iOS/Core/Sources/Trash/BBNetwork/API.swift @@ -53,7 +53,7 @@ public enum BibbiAPI { public var value: String { switch self { case let .auth(token): return "Bearer \(token)" - case .xAppKey: return "39d7cdd1-218c-4abd-addb-ab4efff8a369" // TODO: - 번들에서 가져오기 + case .xAppKey: return "4c9b1c45-9dba-4813-b7c0-b079c08befe6" // 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/Data/Project.swift b/14th-team5-iOS/Data/Project.swift index f3b252715..f9d5e5187 100644 --- a/14th-team5-iOS/Data/Project.swift +++ b/14th-team5-iOS/Data/Project.swift @@ -5,7 +5,7 @@ // Created by Kim dohyun on 2023/11/14. // -import ProjectDescription +@preconcurrency import ProjectDescription import ProjectDescriptionHelpers diff --git a/14th-team5-iOS/Data/Sources/Repositories/AppRepository.swift b/14th-team5-iOS/Data/Sources/Repositories/AppRepository.swift index 609eb0418..52a4cce1e 100644 --- a/14th-team5-iOS/Data/Sources/Repositories/AppRepository.swift +++ b/14th-team5-iOS/Data/Sources/Repositories/AppRepository.swift @@ -33,7 +33,7 @@ extension AppRepository { public func fetchAppVersion() -> Observable { // TODO: - xAppKey 불러오는 코드 다시 작성하기 - let appKey = "39d7cdd1-218c-4abd-addb-ab4efff8a369" + let appKey = "4c9b1c45-9dba-4813-b7c0-b079c08befe6" return meAPIWorker.fetchAppVersion(appKey: appKey) .map { $0.toDomain() } diff --git a/14th-team5-iOS/Data/Sources/Trash/Account/AccountAPI/Helpers/Kakao/AccountKakaoSignInHelper.swift b/14th-team5-iOS/Data/Sources/Trash/Account/AccountAPI/Helpers/Kakao/AccountKakaoSignInHelper.swift index 5dd6d5eba..caf1124a1 100644 --- a/14th-team5-iOS/Data/Sources/Trash/Account/AccountAPI/Helpers/Kakao/AccountKakaoSignInHelper.swift +++ b/14th-team5-iOS/Data/Sources/Trash/Account/AccountAPI/Helpers/Kakao/AccountKakaoSignInHelper.swift @@ -45,7 +45,7 @@ final class AccountKakaoSignInHelper: AccountSignInHelperType { } else { return UserApi.shared.rx.loginWithKakaoAccount(prompts: [.Login]) .map { [weak self] response in - self?._signInState.accept(AccountSignInStateInfo(snsType: .kakao)) + self?._signInState.accept(AccountSignInStateInfo(snsType: .kakao, snsToken: response.accessToken)) return .success } .catch { error in diff --git a/14th-team5-iOS/DesignSystem/Project.swift b/14th-team5-iOS/DesignSystem/Project.swift index edc4ecc96..108a9faff 100644 --- a/14th-team5-iOS/DesignSystem/Project.swift +++ b/14th-team5-iOS/DesignSystem/Project.swift @@ -5,7 +5,7 @@ // Created by Kim dohyun on 2023/11/14. // -import ProjectDescription +@preconcurrency import ProjectDescription import ProjectDescriptionHelpers private let targets: [Target] = [ diff --git a/14th-team5-iOS/Domain/Project.swift b/14th-team5-iOS/Domain/Project.swift index 75924e0e3..893fe232d 100644 --- a/14th-team5-iOS/Domain/Project.swift +++ b/14th-team5-iOS/Domain/Project.swift @@ -5,7 +5,7 @@ // Created by Kim dohyun on 2023/11/14. // -import ProjectDescription +@preconcurrency import ProjectDescription import ProjectDescriptionHelpers diff --git a/14th-team5-iOS/Util/Project.swift b/14th-team5-iOS/Util/Project.swift index c4b0da2d8..ccc0f1c58 100644 --- a/14th-team5-iOS/Util/Project.swift +++ b/14th-team5-iOS/Util/Project.swift @@ -5,7 +5,7 @@ // Created by 김도현 on 12/26/24. // -import ProjectDescription +@preconcurrency import ProjectDescription import ProjectDescriptionHelpers diff --git a/Bibbi.xcworkspace/contents.xcworkspacedata b/Bibbi.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 7bb900224..000000000 --- a/Bibbi.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,129 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme b/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme deleted file mode 100644 index 1781b4e89..000000000 --- a/Bibbi.xcworkspace/xcshareddata/xcschemes/Bibbi-Workspace.xcscheme +++ /dev/null @@ -1,1476 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Plugins/App/Plugin.swift b/Plugins/App/Plugin.swift index 86a1e55b8..f6c1572da 100644 --- a/Plugins/App/Plugin.swift +++ b/Plugins/App/Plugin.swift @@ -1,3 +1,3 @@ -import ProjectDescription +@preconcurrency import ProjectDescription -let plugin = Plugin(name: "MyPlugin") \ No newline at end of file +let plugin = Plugin(name: "MyPlugin") diff --git a/Tuist/Package.swift b/Tuist/Package.swift index a1dcf1a07..0863b208e 100644 --- a/Tuist/Package.swift +++ b/Tuist/Package.swift @@ -1,9 +1,9 @@ // swift-tools-version: 5.9 -import PackageDescription +@preconcurrency import PackageDescription import CompilerPluginSupport #if TUIST - import ProjectDescription +@preconcurrency import ProjectDescription let packageSettings = PackageSettings( // Customize the product types for specific package product @@ -18,7 +18,7 @@ import CompilerPluginSupport #endif let package = Package( - name: "App", + name: "Bibbi", dependencies: [ .package(url: "https://github.com/ReactiveX/RxSwift.git", from: "6.6.0"), .package(url: "https://github.com/SnapKit/SnapKit.git", from: "5.6.0"), @@ -35,7 +35,7 @@ let package = Package( .package(url: "https://github.com/bibbi-team/bibbi-iOS-package.git", from: "0.1.0") ], targets: [ .target( - name: "App", + name: "Bibbi", dependencies: [ .product(name: "Macros", package: "bibbi-iOS-package") ] diff --git a/Tuist/ProjectDescriptionHelpers/DeploymentTarget+Templates.swift b/Tuist/ProjectDescriptionHelpers/DeploymentTarget+Templates.swift index 87f19168f..47282a2a2 100644 --- a/Tuist/ProjectDescriptionHelpers/DeploymentTarget+Templates.swift +++ b/Tuist/ProjectDescriptionHelpers/DeploymentTarget+Templates.swift @@ -5,7 +5,7 @@ // Created by Kim dohyun on 11/15/23. // -import ProjectDescription +@preconcurrency import ProjectDescription extension DeploymentTargets { public static let `defualt` = DeploymentTargets.iOS("15.0") diff --git a/Tuist/ProjectDescriptionHelpers/Modular+Templates.swift b/Tuist/ProjectDescriptionHelpers/Modular+Templates.swift index 913c50230..0586ed4ac 100644 --- a/Tuist/ProjectDescriptionHelpers/Modular+Templates.swift +++ b/Tuist/ProjectDescriptionHelpers/Modular+Templates.swift @@ -5,7 +5,7 @@ // Created by Kim dohyun on 2023/11/14. // -import ProjectDescription +@preconcurrency import ProjectDescription public struct ModularFactory { @@ -81,7 +81,7 @@ extension Target { public static func makeModular(layer: ModuleLayer, factory: ModularFactory) -> Target { switch layer { - case .App: + case .Bibbi: return .target( name: layer.rawValue, destinations: [.iPhone], diff --git a/Tuist/ProjectDescriptionHelpers/ModuleType+Templates.swift b/Tuist/ProjectDescriptionHelpers/ModuleType+Templates.swift index b4b4d1969..c2d566a1c 100644 --- a/Tuist/ProjectDescriptionHelpers/ModuleType+Templates.swift +++ b/Tuist/ProjectDescriptionHelpers/ModuleType+Templates.swift @@ -29,7 +29,7 @@ public enum ExtensionsLayer: String, ModuleType { public enum ModuleLayer: String, CaseIterable, ModuleType { - case App + case Bibbi case Data case Domain case Util @@ -39,7 +39,7 @@ public enum ModuleLayer: String, CaseIterable, ModuleType { public var dependencies: [TargetDependency] { switch self { - case .App: + case .Bibbi: return [ .target(name: "WidgetExtension"), .external(name: "FirebaseMessaging"), diff --git a/Tuist/ProjectDescriptionHelpers/Package+Templates.swift b/Tuist/ProjectDescriptionHelpers/Package+Templates.swift index 42894af77..5599dd51f 100644 --- a/Tuist/ProjectDescriptionHelpers/Package+Templates.swift +++ b/Tuist/ProjectDescriptionHelpers/Package+Templates.swift @@ -5,7 +5,7 @@ // Created by Kim dohyun on 11/16/23. // -import ProjectDescription +@preconcurrency import ProjectDescription import ProjectDescriptionHelpers diff --git a/Tuist/ProjectDescriptionHelpers/Products+Templates.swift b/Tuist/ProjectDescriptionHelpers/Products+Templates.swift index 4e17dbeca..407331abf 100644 --- a/Tuist/ProjectDescriptionHelpers/Products+Templates.swift +++ b/Tuist/ProjectDescriptionHelpers/Products+Templates.swift @@ -21,7 +21,7 @@ public enum ProductsType: Equatable { } var isApp: Bool { - return (self == .app) + return (self == .bibbi) } var isExtensions: Bool { @@ -31,7 +31,7 @@ public enum ProductsType: Equatable { case appExtension case library(ProductState) case framework(ProductState) - case app + case bibbi case unitTests case uiTests diff --git a/Tuist/ProjectDescriptionHelpers/Project+Templates.swift b/Tuist/ProjectDescriptionHelpers/Project+Templates.swift index 481c114a5..76c8c02d3 100644 --- a/Tuist/ProjectDescriptionHelpers/Project+Templates.swift +++ b/Tuist/ProjectDescriptionHelpers/Project+Templates.swift @@ -23,7 +23,7 @@ public struct AppFactory { public init( name: String = "bibbi", platform: Platform = .iOS, - products: [ProductsType] = [.app, .uiTests, .unitTests], + products: [ProductsType] = [.bibbi, .uiTests, .unitTests], dependencies: [TargetDependency] = [], bundleId: String, deploymentTarget: DeploymentTargets? = .defualt, @@ -59,7 +59,7 @@ extension Project { settings: .settings( base: [ "OTHER_LDFLAGS": "$(inherited) -ObjC", - "MARKETING_VERSION": "1.2.5", + "MARKETING_VERSION": "1.2.6", "CURRENT_PROJECT_VERSION": "1", "VERSIONING_SYSTEM": "apple-generic" ], diff --git a/Tuist/ProjectDescriptionHelpers/ResourceFileElements+Templates.swift b/Tuist/ProjectDescriptionHelpers/ResourceFileElements+Templates.swift index 0bc59827d..9168ac839 100644 --- a/Tuist/ProjectDescriptionHelpers/ResourceFileElements+Templates.swift +++ b/Tuist/ProjectDescriptionHelpers/ResourceFileElements+Templates.swift @@ -5,7 +5,7 @@ // Created by Kim dohyun on 11/15/23. // -import ProjectDescription +@preconcurrency import ProjectDescription extension ResourceFileElements { diff --git a/Tuist/ProjectDescriptionHelpers/SourceFilesList+Templates.swift b/Tuist/ProjectDescriptionHelpers/SourceFilesList+Templates.swift index 07994f0a2..74491d2c4 100644 --- a/Tuist/ProjectDescriptionHelpers/SourceFilesList+Templates.swift +++ b/Tuist/ProjectDescriptionHelpers/SourceFilesList+Templates.swift @@ -5,7 +5,7 @@ // Created by Kim dohyun on 11/15/23. // -import ProjectDescription +@preconcurrency import ProjectDescription extension SourceFilesList { diff --git a/Tuist/ProjectDescriptionHelpers/TargetDependency+Templates.swift b/Tuist/ProjectDescriptionHelpers/TargetDependency+Templates.swift index 6b891cdb6..872eeb866 100644 --- a/Tuist/ProjectDescriptionHelpers/TargetDependency+Templates.swift +++ b/Tuist/ProjectDescriptionHelpers/TargetDependency+Templates.swift @@ -12,7 +12,7 @@ extension TargetDependency { public static func with(_ type: ModuleLayer) -> Self { let moduleName = type.rawValue switch type { - case .App: + case .Bibbi: return .project(target: moduleName, path: .relativeToRoot("14th-team5-iOS/\(moduleName)")) case .Data: return .project(target: moduleName, path: .relativeToRoot("14th-team5-iOS/\(moduleName)")) diff --git a/Tuist/ProjectDescriptionHelpers/TargetScript+Templates.swift b/Tuist/ProjectDescriptionHelpers/TargetScript+Templates.swift index 88c281502..869d64b82 100644 --- a/Tuist/ProjectDescriptionHelpers/TargetScript+Templates.swift +++ b/Tuist/ProjectDescriptionHelpers/TargetScript+Templates.swift @@ -5,7 +5,7 @@ // Created by 김도현 on 12/12/24. // -import ProjectDescription +@preconcurrency import ProjectDescription import Foundation @@ -15,7 +15,7 @@ public extension TargetScript { case "${CONFIGURATION}" in "PRD" | "Release" ) BASE_PATH=$(realpath "${SRCROOT}/../..") - "${BASE_PATH}/Tuist/.build/checkouts/firebase-ios-sdk/Crashlytics/run" -gsp "${BASE_PATH}/14th-team5-iOS/App/Resources/GoogleService-Info.plist" -p ios "${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}" + "${BASE_PATH}/Tuist/.build/checkouts/firebase-ios-sdk/Crashlytics/run" -gsp "${BASE_PATH}/14th-team5-iOS/Bibbi/Resources/GoogleService-Info.plist" -p ios "${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}" esac """, name: "Firebase Crashlytics", diff --git a/Workspace.swift b/Workspace.swift index d7909505d..1ffcd3ee0 100644 --- a/Workspace.swift +++ b/Workspace.swift @@ -5,7 +5,7 @@ // Created by Kim dohyun on 2023/11/14. // -import ProjectDescription +@preconcurrency import ProjectDescription import ProjectDescriptionHelpers private let projects: [Path] = ModuleLayer.allCases.map { moduleType in From 204a27bd28ab9a3f230de6b8fcac1e2d56cef687 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=84?= Date: Wed, 15 Jan 2025 18:19:30 +0900 Subject: [PATCH 263/263] =?UTF-8?q?Revert=20"fix:=20Core=20Module=20Simula?= =?UTF-8?q?tor=20Build=EC=8B=9C=20Core=20Module=EC=97=90=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EB=90=9C=20Firebase=20Crashlytics=20Crash=20issue=20?= =?UTF-8?q?=EB=B0=9C=EC=83=9D=20=ED=95=B4=EA=B2=B0=20(#733)"=20(#746)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 558ff159409e7b17ec15f3fc435702ee2519b706.