Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/develop' into release
Browse files Browse the repository at this point in the history
* origin/develop:
  fix: SplashViewReactor 화면 전환 issue 수정 (#702)
  fix: MyUserDefaults memberId, userName Type Annotation String으로 수정 (#700)
  fix: 로그인 버튼 클릭시 touch event cancel 현상 수정해요 (#695)
  fix: QA issue를 수정합니다(#696)
  feat: AlertService 구현 (#692)
  refactor: Calendar 관련 뷰 컨트롤러, 리액터 등 부가 코드 리팩토링 (#682)
Do-hyun-Kim committed Nov 12, 2024
2 parents aaa5872 + 406dd0e commit 0a9efb4
Showing 81 changed files with 2,303 additions and 2,036 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/swift.yml
Original file line number Diff line number Diff line change
@@ -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 }}
2 changes: 1 addition & 1 deletion 14th-team5-iOS/App/Project.swift
Original file line number Diff line number Diff line change
@@ -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"),
Original file line number Diff line number Diff line change
@@ -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()
}

}


Original file line number Diff line number Diff line change
@@ -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)
}

}
Original file line number Diff line number Diff line change
@@ -32,7 +32,7 @@ final class DailyCalendarViewControllerWrapper {

func makeReactor() -> DailyCalendarViewReactor {
DailyCalendarViewReactor(
date: date,
initialSelection: date,
notificationDeepLink: link
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// CalendarType.swift
// App
//
// Created by 김건우 on 10/16/24.
//

import Foundation

public enum MomoriesCalendarType {
case daily
case month
}
Original file line number Diff line number Diff line change
@@ -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)
}
Original file line number Diff line number Diff line change
@@ -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<FSCalendar, FSCalendarDelegate> {
return RxFSCalendarDelegateProxy.proxy(for: self.base)
}

/// 캘린더에서 셀을 선택하면 Date가 담긴 스트림이 흐릅니다.
var didSelect: Observable<Date> {
return delegate.methodInvoked(#selector(FSCalendarDelegate.calendar(_:didSelect:at:)))
.debug("calendar(_:didSelect:at:) 메서드 호출 성공")
.map { $0[1] as! Date }
}

/// 캘린더의 바운즈(bounds)가 변하면 CGRect이 담긴 스트림이 흐릅니다.
var boundingRectWillChange: Observable<CGRect> {
return delegate.methodInvoked(#selector(FSCalendarDelegate.calendar(_:boundingRectWillChange:animated:)))
.debug("calendar(_:boundingRectWillChange:animated:) 메서드 호출 성공")
.map { $0[1] as! CGRect }
}

/// 캘린더의 현재 보이는 페이지가 변하면 Date가 담긴 스트림이 흐릅니다.
var calendarCurrentPageDidChange: Observable<Date> {
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]
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//
// RxMemoriesCalendarPostDelegate.swift
// App
//
// Created by 김건우 on 10/18/24.
//

import Foundation

import RxCocoa
import RxSwift

final class RxMemoriesCalendarPostDelegateProxy: DelegateProxy<MemoriesCalendarPostHeaderView, MemoriesCalendarPostHeaderDelegate>, 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<MemoriesCalendarPostHeaderView, MemoriesCalendarPostHeaderDelegate> {
return RxMemoriesCalendarPostDelegateProxy.proxy(for: self.base)
}

/// 프로필 버튼을 클릭하면 빈 항목이 담긴 스트림이 흐릅니다.
var didTapProfileImageButton: ControlEvent<Void> {
let source = delegate.methodInvoked(#selector(MemoriesCalendarPostHeaderDelegate.didTapProfileImageButton(_:event:)))
.map { _ in () }
return ControlEvent(events: source)
}

}

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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<Mutation>) -> Observable<Mutation> {
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<Mutation>.just(.didSelect(true))
} else {
return Observable<Mutation>.just(.didSelect(false))
}
}
}

return Observable<Mutation>.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
}

}
Original file line number Diff line number Diff line change
@@ -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<Mutation> {
switch action {
case .viewDidLoad:
let yearMonth = initialState.yearMonth
return Observable<Mutation>.merge(
setCalendarBannrInfo(yearMonth: yearMonth),
setStatisticsSummary(yearMonth: yearMonth),
setMonthlyCalendar(yearMonth: yearMonth)
)

case let .didSelect(date):
navigator.toDailyCalendar(selection: date)
return Observable<Mutation>.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<Mutation> {
return fetchCalendarBannerUseCase.execute(yearMonth: yearMonth)
.flatMap { Observable<Mutation>.just(.setBannerInfo($0)) }
}

func setStatisticsSummary(yearMonth: String) -> Observable<Mutation> {
return fetchStatisticsSummaryUseCase.execute(yearMonth: yearMonth)
.flatMap { Observable<Mutation>.just(.setStatisticsSummary($0)) }
}

func setMonthlyCalendar(yearMonth: String) -> Observable<Mutation> {
return fetchMonthlyCalendarUseCase.execute(yearMonth: yearMonth)
.flatMap { Observable<Mutation>.just(.setMonthlyCalendar($0)) }
}

}
Original file line number Diff line number Diff line change
@@ -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<Mutation> {
switch action {
case .viewDidLoad:
let memberId = initialState.dailyPost.authorId
return Observable<Mutation>.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<Mutation>.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<Mutation> {
let memberName = fetchUserNameUseCase.execute(memberId: memberId)
return Observable<Mutation>.just(.setMemberName(memberName))
}

func setProfileImageUrl(memberId: String) -> Observable<Mutation> {
let imageUrl = fetchProfileImageUrlUseCase.execute(memberId: memberId)
if let url = imageUrl {
return Observable<Mutation>.just(.setProfileImageUrl(url))
}
return Observable<Mutation>.empty()
}

func setContentDatasource(post: DailyCalendarEntity) -> Observable<Mutation> {
var sectionItem: [DisplayEditItemModel] = []
post.postContent?.forEach {
sectionItem.append(.fetchDisplayItem(.init(title: String($0), radius: 10, font: .head2Bold)))
}
return Observable<Mutation>.just(.setContentDatasource(sectionItem))
}

}
Original file line number Diff line number Diff line change
@@ -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<Mutation>) -> Observable<Mutation> {
let toastMutation = provider.toastGlobalState.event
let eventMutation = provider.postGlobalState.event
.flatMap { event -> Observable<Mutation> in
switch event {
case let .showAllFamilyUploadedToastView(uploaded):
return Observable<Mutation>.just(.setAllUploadedToastMessageView(uploaded))
}
}

let postMutation = provider.postGlobalState.event
.flatMap { event -> Observable<Mutation> in
switch event {
case let .pushProfileViewController(memberId):
return Observable<Mutation>.just(.pushProfileViewController(memberId))
case let .renewalPostCommentCount(count):
case let .renewalCommentCount(count):
return Observable<Mutation>.just(.renewCommentCount(count))
default:
return .empty()
}
}

return Observable<Mutation>.merge(mutation, toastMutation, postMutation)
return Observable<Mutation>.merge(mutation, eventMutation)
}


// MARK: - Mutate

public func mutate(action: Action) -> Observable<Mutation> {
switch action {
case .popViewController:
provider.toastGlobalState.clearToastMessageEvent()
return Observable<Mutation>.just(.popViewController)

case let .dateSelected(date):
// 처음 이벤트를 받거나 썸네일 이미지가 존재하는 셀에 한하여
if !hasReceivedSelectionEvent || hasThumbnailImages.contains(date) {
hasReceivedSelectionEvent = true
// 셀 클릭 이벤트 방출
provider.calendarGlabalState.didSelectDate(date)
return Observable<Mutation>.just(.setSelectionHaptic)
}
return Observable<Mutation>.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<Mutation>.empty()
}

return Observable.concat(
Observable<Mutation>.just(.setDailyCalendar(posts)),
Observable<Mutation>.just(.setImageIndex(0)),
Observable<Mutation>.just(.clearNotificationDeepLink)
)
}
if hasFetchedDailyPosts.contains(date) {
provider.calendarService.didSelect(date: date)
return fetchDailyPost(with: yearMonthDay)
}
return Observable<Mutation>.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<Mutation>.empty()
}
case let .fetchMonthlyCalendar(date):
return fetchMonthlyCalendars(with: date)

case let .imageIndex(index):
return Observable<Mutation>.just(.setImageIndex(index))
case let .updateVisiblePost(index):
return Observable<Mutation>.just(.setVisiblePost(index))

case let .renewEmoji(index):
guard let dataSource = currentState.displayDailyCalendar.first else {
return Observable<Mutation>.empty()
}
let post = dataSource.items[index]
return Observable<Mutation>.just(.setVisiblePost(post))
case .backToMonthly:
provider.calendarService.removePreviousSelection()
navigator.backToMonthly()
return Observable<Mutation>.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<Mutation> {
fetchDailyPostsUseCase.execute(yearMonthDay: yearMonthDay)
.flatMap(with: self) {
return Observable.concat(
Observable<Mutation>.just(.setDailyPosts($1.results)),
Observable<Mutation>.just(.setVisiblePost(0)),
Observable<Mutation>.just(.clearNotificationDeepLink) // 삭제하기
)
}
}

func fetchMonthlyCalendars(with date: Date) -> Observable<Mutation> {
let (prev, curr, next) = makePrevCurrNextYearMonth(date)

let monthlyCalendars: Observable<Mutation> = 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<Mutation> {
fetchMonthlyCalendarUseCase.execute(yearMonth: yearMonth)
.flatMap(with: self) {
$0.hasFetchedDailyPosts.append(contentsOf: $1.results.map(\.date))
$0.hasFetchedMonthlyCalendars.append(yearMonth.toDate(with: .dashYyyyMM))
return Observable<Mutation>.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)
}

}
Original file line number Diff line number Diff line change
@@ -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<Mutation> {
return .empty()
}


// MARK: - Reduce

public func reduce(state: State, mutation: Mutation) -> State {
return state
}

}
Original file line number Diff line number Diff line change
@@ -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() }

}

Original file line number Diff line number Diff line change
@@ -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<Mutation> {
switch action {
case .didTapTipButton:
return Observable<Mutation>.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
}

}
Original file line number Diff line number Diff line change
@@ -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<Mutation>) -> Observable<Mutation> {
let eventMutation = provider.calendarGlabalState.event
.flatMap { event -> Observable<Mutation> in
switch event {
case let .pushCalendarPostVC(date):
return Observable<Mutation>.just(.pushDailyCalendarViewController(date))

case let .didTapInfoButton(sourceView):
return Observable<Mutation>.just(.setInfoPopover(sourceView))

default:
return Observable<Mutation>.empty()
}
}

return Observable<Mutation>.merge(mutation, eventMutation)
init() {
self.initialState = State(pageDatasource: [.init(model: (), items: [])])
}

// MARK: - Mutate

public func mutate(action: Action) -> Observable<Mutation> {
switch action {
case .popViewController:
provider.toastGlobalState.clearLastSelectedDate()
return Observable<Mutation>.just(.popViewController)

case let .addCalendarItems(items):
let indexPath = IndexPath(item: items.count-1, section: 0)

return Observable<Mutation>.concat(
Observable<Mutation>.just(.setCalendarItems(items)),
Observable<Mutation>.just(.setCalendarPageIndexPath(indexPath))
)

case .viewDidLoad:
return fetchFamilyCreatedAtUseCase.execute()
.flatMap(with: self) {
guard let createdAt = $1?.createdAt
else {
return Observable<Mutation>.just(.setCalendarPage($0.createCalendarPageItems(from: ._20240101)))
}
return Observable<Mutation>.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
}

}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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(

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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,96 +51,75 @@ 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() {
dayLabel.snp.makeConstraints {
$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,17 +163,41 @@ 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
}
}
}


// 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
}

}
Original file line number Diff line number Diff line change
@@ -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<MemoriesCalendarPageReactor> {

// 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())
}

}
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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<MemoriesCalendarPostCellReactor> {

// MARK: - Typealias

typealias RxDataSource = RxCollectionViewSectionedReloadDataSource<DisplayEditSectionModel>


// 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)
}

}
Original file line number Diff line number Diff line change
@@ -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<MemoriesCalendarTitleViewReactor> {

// 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)개의 추억"
}

}
Original file line number Diff line number Diff line change
@@ -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<MemoriesCalendarPostHeaderReactor> {

// 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으로 빼기
}
}
}

}

Original file line number Diff line number Diff line change
@@ -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<MemoriesCalendarPostImageReactor> {

// 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)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//
// BannerViewController.swift
// App
//
// Created by 김건우 on 10/16/24.
//

import SwiftUI

import Then

final class BannerHostingViewController: UIHostingController<BannerView> {

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)
}

}
Loading

0 comments on commit 0a9efb4

Please sign in to comment.