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/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/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/Splash/SplashReactor.swift b/14th-team5-iOS/App/Sources/Presentation/Splash/SplashReactor.swift index fd1df3bdb..acb0bb94b 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 @@ -33,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 @@ -43,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() - .flatMap { appVersionInfo in - + .flatMap { [unowned self] appVersionInfo -> Observable in guard let appVersionInfo = appVersionInfo else { return Observable.just(Mutation.setUpdateNeeded(nil)) } @@ -61,25 +66,34 @@ public final class SplashReactor: Reactor { App.Repository.token.accessToken .flatMap { token -> Observable in guard let _ = token else { + 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 familyId = memberInfo.familyId else { + let _ = memberInfo.familyId else { + self.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)) + return self.fetchFamilyCreatedAtUseCase.execute() + .flatMap { familyInfo -> Observable in + + if memberInfo.familyId != nil { + if UserDefaults.standard.inviteCode != nil { + self.splashNavigator.toJoined() + return .just(.setMemberInfo(memberInfo)) + } else { + self.splashNavigator.toHome() + return .just(.setMemberInfo(nil)) + } + } + + self.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/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/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/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/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..b66b49841 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBServices/PostGlobalState.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBServices/PostGlobalState.swift @@ -11,21 +11,17 @@ import RxSwift public enum PostEvent { case pushProfileViewController(String) - case renewalPostCommentCount(Int) + case renewalCommentCount(Int) case receiveMissionContent(String) } 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,28 +30,20 @@ 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) } public func renewalPostCommentCount(_ count: Int) -> Observable { - event.onNext(.renewalPostCommentCount(count)) + event.onNext(.renewalCommentCount(count)) 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 e5f21266a..2a53263ee 100644 --- a/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ServiceProvider.swift +++ b/14th-team5-iOS/Core/Sources/Bibbi/BBServices/ServiceProvider.swift @@ -9,16 +9,15 @@ import Foundation public protocol ServiceProviderProtocol: AnyObject { + var alertService: AlertServiceType { get } 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 } var realEmojiGlobalState: RealEmojiGlobalStateType { get } var profilePageGlobalState: ProfileFeedGlobalStateType { get } @@ -26,21 +25,18 @@ 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) + 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/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 - } -} 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/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/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/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/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/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/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 } 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) ?? "알 수 없음" } } 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" ],