diff --git a/MLS/MLSAppFeature/Sources/MLSAppFeature/Coordinator/AppCoordinator.swift b/MLS/MLSAppFeature/Sources/MLSAppFeature/Coordinator/AppCoordinator.swift index 7c9b0173..edcf8585 100644 --- a/MLS/MLSAppFeature/Sources/MLSAppFeature/Coordinator/AppCoordinator.swift +++ b/MLS/MLSAppFeature/Sources/MLSAppFeature/Coordinator/AppCoordinator.swift @@ -44,16 +44,16 @@ public final class AppCoordinator: AppCoordinatorProtocol { // MARK: - Public Methods public func showMainTab(selectedIndex: Int = 0) { let tabItems: [TabItem] = [ + TabItem(title: "도감", icon: DesignSystemAsset.image(named: "dictionary")), TabItem(title: "추천", icon: DesignSystemAsset.image(named: "favorite") ), - TabItem(title: "도감", icon: DesignSystemAsset.image(named: "dictionary")), TabItem(title: "북마크", icon: DesignSystemAsset.image(named: "bookmarkList")), TabItem(title: "MY", icon: DesignSystemAsset.image(named: "mypage")) ] let tabBar = BottomTabBarController( viewControllers: [ - recommendationMainFactory.make(), dictionaryMainViewFactory.make(), + recommendationMainFactory.make(), bookmarkMainFactory.make(bottomInset: 64), myPageMainFactory.make() ], diff --git a/MLS/MLSAppFeature/Sources/MLSAppFeature/Dependency/FactoryAssembly.swift b/MLS/MLSAppFeature/Sources/MLSAppFeature/Dependency/FactoryAssembly.swift index 6eb3b6d3..e95bb306 100644 --- a/MLS/MLSAppFeature/Sources/MLSAppFeature/Dependency/FactoryAssembly.swift +++ b/MLS/MLSAppFeature/Sources/MLSAppFeature/Dependency/FactoryAssembly.swift @@ -57,8 +57,7 @@ public enum FactoryAssembly { type: SocialLoginUseCase.self ), userDefaultsRepository: DIContainer.resolve( - type: UserDefaultsRepository.self, name: "authUserDefaultsRepository" - ) + type: UserDefaultsRepository.self) ) } DIContainer.register(type: DictionaryDetailFactory.self) { @@ -184,7 +183,12 @@ public enum FactoryAssembly { type: DictionaryNotificationFactory.self ), loginFactory: DIContainer.resolve(type: LoginFactory.self), - fetchProfileUseCase: DIContainer.resolve( + userDefaultsRepository: DIContainer + .resolve( + type: DictionaryUserDefaultsRepository.self + ), + fetchProfileUseCase: DIContainer + .resolve( type: FetchProfileUseCase.self ) ) @@ -432,7 +436,7 @@ public enum FactoryAssembly { DIContainer.register(type: RecommendationMainFactory.self) { RecommendationMainFactoryImpl( repository: DIContainer.resolve(type: RecommendationRepository.self), - bookmarkRepository: DIContainer.resolve(type: BookmarkRepository.self), + bookmarkRepository: DIContainer.resolve(type: BookmarkRepository.self), recommendationUserDefaultsRepository: DIContainer.resolve(type: RecommendationUserDefaultsRepository.self), makeLoginVC: { DIContainer.resolve(type: LoginFactory.self).make(exitRoute: .pop) }, diff --git a/MLS/MLSAppFeature/Sources/MLSAppFeature/Dependency/RepositoryAssembly.swift b/MLS/MLSAppFeature/Sources/MLSAppFeature/Dependency/RepositoryAssembly.swift index 1d8eb1b4..020b9ed6 100644 --- a/MLS/MLSAppFeature/Sources/MLSAppFeature/Dependency/RepositoryAssembly.swift +++ b/MLS/MLSAppFeature/Sources/MLSAppFeature/Dependency/RepositoryAssembly.swift @@ -38,11 +38,11 @@ public enum RepositoryAssembly { DIContainer.register(type: BookmarkRepository.self) { BookmarkRepositoryImpl() } - DIContainer.register(type: MLSAuthFeatureInterface.UserDefaultsRepository.self, name: "authUserDefaultsRepository") { - MLSAuthFeature.UserDefaultsRepositoryImpl() + DIContainer.register(type: UserDefaultsRepository.self) { + UserDefaultsRepositoryImpl() } - DIContainer.register(type: MLSDictionaryFeatureInterface.UserDefaultsRepository.self, name: "dictionaryUserDefaultsRepository") { - MLSDictionaryFeature.UserDefaultsRepositoryImpl() + DIContainer.register(type: DictionaryUserDefaultsRepository.self) { + UserDefaultsRepositoryImpl() } DIContainer.register(type: AlarmRepository.self) { AlarmRepositoryImpl( @@ -87,5 +87,8 @@ public enum RepositoryAssembly { DIContainer.register(type: UpdateSkipRepositoryProtocol.self) { UpdateSkipRepository() } - } + DIContainer.register(type: RecommendationUserDefaultsRepository.self) { + RecommendationUserDefaultsRepositoryImpl() + } + } } diff --git a/MLS/MLSAppFeature/Sources/MLSAppFeature/Dependency/UseCaseAssembly.swift b/MLS/MLSAppFeature/Sources/MLSAppFeature/Dependency/UseCaseAssembly.swift index 2fa2b568..65cffb41 100644 --- a/MLS/MLSAppFeature/Sources/MLSAppFeature/Dependency/UseCaseAssembly.swift +++ b/MLS/MLSAppFeature/Sources/MLSAppFeature/Dependency/UseCaseAssembly.swift @@ -24,15 +24,14 @@ public enum UseCaseAssembly { type: TokenRepository.self ), userDefaultsRepository: DIContainer.resolve( - type: UserDefaultsRepository.self, name: "authUserDefaultsRepository" - ) + type: UserDefaultsRepository.self) ) } DIContainer.register(type: SocialSignUpUseCaseImpl.self) { SocialSignUpUseCaseImpl( authRepository: DIContainer.resolve(type: AuthAPIRepository.self), tokenRepository: DIContainer.resolve(type: TokenRepository.self), - userDefaultsRepository: DIContainer.resolve(type: UserDefaultsRepository.self, name: "authUserDefaultsRepository") + userDefaultsRepository: DIContainer.resolve(type: UserDefaultsRepository.self) ) } DIContainer.register(type: CheckNotificationPermissionUseCase.self) { @@ -71,7 +70,7 @@ public enum UseCaseAssembly { ParseItemFilterResultUseCaseImpl() } DIContainer.register(type: FetchVisitDictionaryDetailUseCase.self) { - FetchVisitDictionaryDetailUseCaseImpl(repository: DIContainer.resolve(type: UserDefaultsRepository.self, name: "dictionaryUserDefaultsRepository")) + FetchVisitDictionaryDetailUseCaseImpl(repository: DIContainer.resolve(type: DictionaryUserDefaultsRepository.self)) } DIContainer.register(type: SocialSignUpUseCase.self) { SocialSignUpUseCaseImpl( @@ -80,7 +79,7 @@ public enum UseCaseAssembly { tokenRepository: DIContainer .resolve(type: TokenRepository.self), userDefaultsRepository: DIContainer - .resolve(type: UserDefaultsRepository.self, name: "authUserDefaultsRepository") + .resolve(type: UserDefaultsRepository.self) ) } DIContainer.register(type: UpdateCheckerUseCaseProtocol.self) { diff --git a/MLS/MLSAppFeature/Sources/MLSAppFeature/Launcher/AppLauncher.swift b/MLS/MLSAppFeature/Sources/MLSAppFeature/Launcher/AppLauncher.swift index 61f46513..10e6ad59 100644 --- a/MLS/MLSAppFeature/Sources/MLSAppFeature/Launcher/AppLauncher.swift +++ b/MLS/MLSAppFeature/Sources/MLSAppFeature/Launcher/AppLauncher.swift @@ -80,7 +80,9 @@ private extension AppLauncher { object: nil, queue: .main ) { [weak self] _ in - self?.showForceUpdateAlert() + Task { @MainActor in + self?.showForceUpdateAlert() + } } case .optional(let latestVersion): GuideAlertFactory.show( diff --git a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tabbar/BottomTabBarController.swift b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tabbar/BottomTabBarController.swift index faa4ea56..06be2ab5 100644 --- a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tabbar/BottomTabBarController.swift +++ b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tabbar/BottomTabBarController.swift @@ -6,6 +6,13 @@ public final class BottomTabBarController: UITabBarController { // MARK: - Type private enum Constant { static let horizontalInset: CGFloat = 24 + static let commentBottomMargin: CGFloat = -8 + static let commentWidth: CGFloat = 152 + static let commentHeight: CGFloat = 28 + static let buttonSize: CGFloat = 64 + static let arrowInset: CGFloat = 16 + @MainActor static let spacing: CGFloat = (UIScreen.main.bounds.width - (horizontalInset * 2) - (buttonSize * 4)) / 3 + @MainActor static let commentLeadingOffset: CGFloat = horizontalInset + buttonSize + spacing + (buttonSize / 2) - arrowInset } // MARK: - Components @@ -17,6 +24,12 @@ public final class BottomTabBarController: UITabBarController { view.backgroundColor = .systemBackground return view }() + private let recommendationCommentView: UIImageView = { + let view = UIImageView() + view.image = DesignSystemAsset.image(named: "recommendationComment") + view.isHidden = true + return view + }() // MARK: - Init public init(viewControllers: [UIViewController], tabItems: [TabItem]? = nil, initialIndex: Int = 0) { @@ -50,6 +63,7 @@ private extension BottomTabBarController { view.addSubview(tabBarBackground) view.addSubview(customTabBar) view.addSubview(divider) + view.addSubview(recommendationCommentView) } func setupConstraints() { @@ -67,6 +81,13 @@ private extension BottomTabBarController { make.horizontalEdges.bottom.equalToSuperview() make.top.equalTo(divider.snp.top) } + + recommendationCommentView.snp.makeConstraints { make in + make.bottom.equalTo(customTabBar.snp.top).offset(Constant.commentBottomMargin) + make.leading.equalTo(view.snp.leading).offset(Constant.commentLeadingOffset) + make.width.equalTo(Constant.commentWidth) + make.height.equalTo(Constant.commentHeight) + } } func configureUI(controllers: [UIViewController]) { @@ -132,4 +153,8 @@ public extension BottomTabBarController { customTabBar.selectTab(index: index) } } + + func checkRecommendationFirstLaunch(isFirst: Bool) { + recommendationCommentView.isHidden = isFirst + } } diff --git a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Toast.swift b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Toast.swift index a889b4c6..fdc07250 100644 --- a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Toast.swift +++ b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Toast.swift @@ -4,8 +4,8 @@ import SnapKit public final class Toast: UIView { private enum Constant { - static let verticalEdgesInset: CGFloat = 16 static let horizontalEdges: CGFloat = 16 + static let height: CGFloat = 44 static let cornerRadius: CGFloat = 8 } @@ -37,23 +37,24 @@ public final class Toast: UIView { private extension Toast { func addViews() { addSubview(self.toastContentView) - toastContentView.addSubview(self.label) + self.toastContentView.addSubview(self.label) } func setupConstraints() { - toastContentView.snp.makeConstraints { make in + self.toastContentView.snp.makeConstraints { make in make.edges.equalToSuperview() + make.height.equalTo(Constant.height) } - label.snp.makeConstraints { make in - make.verticalEdges.equalToSuperview().inset(Constant.verticalEdgesInset) + self.label.snp.makeConstraints { make in make.horizontalEdges.equalToSuperview().inset(Constant.horizontalEdges) + make.centerY.equalToSuperview() } } func configureUI(message: String?) { layer.cornerRadius = Constant.cornerRadius clipsToBounds = true - label.attributedText = .makeStyledString(font: .b_s_r, text: message, color: .whiteMLS) + self.label.attributedText = .makeStyledString(font: .b_s_r, text: message, color: .whiteMLS) } } diff --git a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Layouts/Factory/GuideAlertFactory.swift b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Layouts/Factory/GuideAlertFactory.swift index a19c01ca..4df58a32 100644 --- a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Layouts/Factory/GuideAlertFactory.swift +++ b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Layouts/Factory/GuideAlertFactory.swift @@ -16,9 +16,10 @@ public enum GuideAlertFactory { ctaText: String, cancelText: String? = nil, ctaAction: @escaping () -> Void, - cancelAction: (() -> Void)? = nil + cancelAction: (() -> Void)? = nil, + ctaRatio: Double = 0.7 ) { - let alert = GuideAlert(mainText: mainText, ctaText: ctaText, cancelText: cancelText) + let alert = GuideAlert(mainText: mainText, ctaText: ctaText, cancelText: cancelText, ctaRatio: ctaRatio) presentAlert(alert: alert, ctaAction: ctaAction, cancelAction: cancelAction) } diff --git a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Layouts/ToLoginView.swift b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Layouts/ToLoginView.swift index 3cad3a23..f911f0d5 100644 --- a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Layouts/ToLoginView.swift +++ b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Layouts/ToLoginView.swift @@ -36,8 +36,18 @@ public final class ToLoginView: UIView { // MARK: - Components public let imageView = UIImageView() - private let mainLabel = UILabel() - private let subLabel = UILabel() + + private let mainLabel: UILabel = { + let label = UILabel() + label.numberOfLines = 0 + return label + }() + + private let subLabel: UILabel = { + let label = UILabel() + label.numberOfLines = 0 + return label + }() public let button = CommonButton() diff --git a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Resources/Image.xcassets/image/recommendationComment.imageset/Contents.json b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Resources/Image.xcassets/image/recommendationComment.imageset/Contents.json new file mode 100644 index 00000000..97b8a027 --- /dev/null +++ b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Resources/Image.xcassets/image/recommendationComment.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "recommendationComment.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Resources/Image.xcassets/image/recommendationComment.imageset/recommendationComment.png b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Resources/Image.xcassets/image/recommendationComment.imageset/recommendationComment.png new file mode 100644 index 00000000..bc409637 Binary files /dev/null and b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Resources/Image.xcassets/image/recommendationComment.imageset/recommendationComment.png differ diff --git a/MLS/MLSDictionaryFeature/Sources/MLSDictionaryFeature/Data/Repositories/UserDefaultsRepositoryImpl.swift b/MLS/MLSDictionaryFeature/Sources/MLSDictionaryFeature/Data/Repositories/UserDefaultsRepositoryImpl.swift index 3b31c140..ef50a367 100644 --- a/MLS/MLSDictionaryFeature/Sources/MLSDictionaryFeature/Data/Repositories/UserDefaultsRepositoryImpl.swift +++ b/MLS/MLSDictionaryFeature/Sources/MLSDictionaryFeature/Data/Repositories/UserDefaultsRepositoryImpl.swift @@ -4,8 +4,9 @@ import MLSDictionaryFeatureInterface import RxSwift -public final class UserDefaultsRepositoryImpl: UserDefaultsRepository { +public final class UserDefaultsRepositoryImpl: DictionaryUserDefaultsRepository { private let dictionaryDetailkey = "dictionaryDetailkey" + private let recommendationkey = "recommendationkey" public init() {} @@ -25,4 +26,9 @@ public final class UserDefaultsRepositoryImpl: UserDefaultsRepository { return Disposables.create() } } + + public func checkFirstLaunchRecommendation() -> Observable { + let hasVisited = UserDefaults.standard.bool(forKey: recommendationkey) + return .just(hasVisited) + } } diff --git a/MLS/MLSDictionaryFeature/Sources/MLSDictionaryFeature/Domain/Usecases/FetchVisitDictionaryDetailUseCaseImpl.swift b/MLS/MLSDictionaryFeature/Sources/MLSDictionaryFeature/Domain/Usecases/FetchVisitDictionaryDetailUseCaseImpl.swift index 0e760ec1..5b6dcc62 100644 --- a/MLS/MLSDictionaryFeature/Sources/MLSDictionaryFeature/Domain/Usecases/FetchVisitDictionaryDetailUseCaseImpl.swift +++ b/MLS/MLSDictionaryFeature/Sources/MLSDictionaryFeature/Domain/Usecases/FetchVisitDictionaryDetailUseCaseImpl.swift @@ -4,8 +4,8 @@ import MLSDictionaryFeatureInterface import RxSwift public class FetchVisitDictionaryDetailUseCaseImpl: FetchVisitDictionaryDetailUseCase { - private let repository: UserDefaultsRepository - public init(repository: UserDefaultsRepository) { + private let repository: DictionaryUserDefaultsRepository + public init(repository: DictionaryUserDefaultsRepository) { self.repository = repository } diff --git a/MLS/MLSDictionaryFeature/Sources/MLSDictionaryFeature/Presentation/DictionaryMain/DictionaryMainReactor.swift b/MLS/MLSDictionaryFeature/Sources/MLSDictionaryFeature/Presentation/DictionaryMain/DictionaryMainReactor.swift index 14c09a57..09c2c0cb 100644 --- a/MLS/MLSDictionaryFeature/Sources/MLSDictionaryFeature/Presentation/DictionaryMain/DictionaryMainReactor.swift +++ b/MLS/MLSDictionaryFeature/Sources/MLSDictionaryFeature/Presentation/DictionaryMain/DictionaryMainReactor.swift @@ -23,6 +23,7 @@ public final class DictionaryMainReactor: Reactor { case navigateTo(Route) case setLogin(Bool) case setCurrentTab(oldIndex: Int, newIndex: Int) + case setIsFirstRecommendationLaunch(Bool) } public struct State { @@ -31,20 +32,24 @@ public final class DictionaryMainReactor: Reactor { var sections: [String] { return type.pageTabList.map { $0.title } } + var isLogin = false var currentPageIndex = 0 var oldPageIndex = 0 + var isFirstRecommendationLaunch = false } // MARK: - properties public var initialState: State var disposeBag = DisposeBag() + private let userDefaultsRepository: DictionaryUserDefaultsRepository private let fetchProfileUseCase: FetchProfileUseCase // MARK: - init - public init(fetchProfileUseCase: FetchProfileUseCase) { + public init(userDefaultsRepository: DictionaryUserDefaultsRepository, fetchProfileUseCase: FetchProfileUseCase) { self.initialState = State() + self.userDefaultsRepository = userDefaultsRepository self.fetchProfileUseCase = fetchProfileUseCase } @@ -52,9 +57,18 @@ public final class DictionaryMainReactor: Reactor { public func mutate(action: Action) -> Observable { switch action { case .viewWillAppear: - return fetchProfileUseCase.execute() - .map { .setLogin($0 != nil) } + let firstRecommendation = userDefaultsRepository + .checkFirstLaunchRecommendation() + .map(Mutation.setIsFirstRecommendationLaunch) + + let fetchProfile = fetchProfileUseCase.execute() + .map { Mutation.setLogin($0 != nil) } .catchAndReturn(.setLogin(false)) + + return Observable.merge( + firstRecommendation, + fetchProfile + ) case .searchButtonTapped: return .just(.navigateTo(.search)) case .notificationButtonTapped: @@ -69,13 +83,15 @@ public final class DictionaryMainReactor: Reactor { var newState = state switch mutation { - case .navigateTo(let route): + case let .navigateTo(route): newState.route = route case let .setLogin(isLogin): newState.isLogin = isLogin case let .setCurrentTab(oldIndex, newIndex): newState.oldPageIndex = oldIndex newState.currentPageIndex = newIndex + case let .setIsFirstRecommendationLaunch(isFirst): + newState.isFirstRecommendationLaunch = isFirst } return newState diff --git a/MLS/MLSDictionaryFeature/Sources/MLSDictionaryFeature/Presentation/DictionaryMain/DictionaryMainViewController.swift b/MLS/MLSDictionaryFeature/Sources/MLSDictionaryFeature/Presentation/DictionaryMain/DictionaryMainViewController.swift index f1c16414..b4644698 100644 --- a/MLS/MLSDictionaryFeature/Sources/MLSDictionaryFeature/Presentation/DictionaryMain/DictionaryMainViewController.swift +++ b/MLS/MLSDictionaryFeature/Sources/MLSDictionaryFeature/Presentation/DictionaryMain/DictionaryMainViewController.swift @@ -193,6 +193,17 @@ public extension DictionaryMainViewController { owner.moveToTab(oldIndex: oldIndex, newIndex: newIndex) }) .disposed(by: disposeBag) + + reactor.state + .observe(on: MainScheduler.instance) + .map { $0.isFirstRecommendationLaunch } + .distinctUntilChanged() + .withUnretained(self) + .subscribe { owner, isFirst in + (owner.tabBarController as? BottomTabBarController)? + .checkRecommendationFirstLaunch(isFirst: isFirst) + } + .disposed(by: disposeBag) } } diff --git a/MLS/MLSDictionaryFeature/Sources/MLSDictionaryFeature/Presentation/DictionaryMain/DictionaryMainViewFactoryImpl.swift b/MLS/MLSDictionaryFeature/Sources/MLSDictionaryFeature/Presentation/DictionaryMain/DictionaryMainViewFactoryImpl.swift index 6be090e8..8cd5fa49 100644 --- a/MLS/MLSDictionaryFeature/Sources/MLSDictionaryFeature/Presentation/DictionaryMain/DictionaryMainViewFactoryImpl.swift +++ b/MLS/MLSDictionaryFeature/Sources/MLSDictionaryFeature/Presentation/DictionaryMain/DictionaryMainViewFactoryImpl.swift @@ -8,18 +8,20 @@ public final class DictionaryMainViewFactoryImpl: DictionaryMainViewFactory { private let searchFactory: DictionarySearchFactory private let notificationFactory: DictionaryNotificationFactory private let loginFactory: LoginFactory + private let userDefaultsRepository: DictionaryUserDefaultsRepository private let fetchProfileUseCase: FetchProfileUseCase - public init(dictionaryMainListFactory: DictionaryMainListFactory, searchFactory: DictionarySearchFactory, notificationFactory: DictionaryNotificationFactory, loginFactory: LoginFactory, fetchProfileUseCase: FetchProfileUseCase) { + public init(dictionaryMainListFactory: DictionaryMainListFactory, searchFactory: DictionarySearchFactory, notificationFactory: DictionaryNotificationFactory, loginFactory: LoginFactory, userDefaultsRepository: DictionaryUserDefaultsRepository, fetchProfileUseCase: FetchProfileUseCase) { self.dictionaryMainListFactory = dictionaryMainListFactory self.searchFactory = searchFactory self.notificationFactory = notificationFactory self.loginFactory = loginFactory + self.userDefaultsRepository = userDefaultsRepository self.fetchProfileUseCase = fetchProfileUseCase } public func make() -> BaseViewController { - let reactor = DictionaryMainReactor(fetchProfileUseCase: fetchProfileUseCase) + let reactor = DictionaryMainReactor(userDefaultsRepository: userDefaultsRepository, fetchProfileUseCase: fetchProfileUseCase) let viewController = DictionaryMainViewController(dictionaryMainListFactory: dictionaryMainListFactory, searchFactory: searchFactory, notificationFactory: notificationFactory, loginFactory: loginFactory, reactor: reactor, ) viewController.isBottomTabbarHidden = false viewController.reactor = reactor diff --git a/MLS/MLSDictionaryFeature/Sources/MLSDictionaryFeatureInterface/Repositories/UserDefaultsRepository.swift b/MLS/MLSDictionaryFeature/Sources/MLSDictionaryFeatureInterface/Repositories/DictionaryUserDefaultsRepository.swift similarity index 51% rename from MLS/MLSDictionaryFeature/Sources/MLSDictionaryFeatureInterface/Repositories/UserDefaultsRepository.swift rename to MLS/MLSDictionaryFeature/Sources/MLSDictionaryFeatureInterface/Repositories/DictionaryUserDefaultsRepository.swift index 33ef87bb..21e559dc 100644 --- a/MLS/MLSDictionaryFeature/Sources/MLSDictionaryFeatureInterface/Repositories/UserDefaultsRepository.swift +++ b/MLS/MLSDictionaryFeature/Sources/MLSDictionaryFeatureInterface/Repositories/DictionaryUserDefaultsRepository.swift @@ -1,6 +1,7 @@ import RxSwift -public protocol UserDefaultsRepository { +public protocol DictionaryUserDefaultsRepository { func fetchDictionaryDetail() -> Observable func saveDictionaryDetail() -> Completable + func checkFirstLaunchRecommendation() -> Observable } diff --git a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Data/Repositories/RecommendationUserDefaultsRepositoryImpl.swift b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Data/Repositories/RecommendationUserDefaultsRepositoryImpl.swift new file mode 100644 index 00000000..95b70313 --- /dev/null +++ b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Data/Repositories/RecommendationUserDefaultsRepositoryImpl.swift @@ -0,0 +1,18 @@ +import Foundation + +import MLSRecommendationFeatureInterface + +import RxSwift + +public final class RecommendationUserDefaultsRepositoryImpl: RecommendationUserDefaultsRepository { + private let recommendationkey = "recommendationkey" + + public init() {} + + public func saveFirstLaunch() -> Completable { + if !UserDefaults.standard.bool(forKey: recommendationkey) { + UserDefaults.standard.set(true, forKey: recommendationkey) + } + return .empty() + } +} diff --git a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainFactoryImpl.swift b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainFactoryImpl.swift index 60cd147f..0efaf1cf 100644 --- a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainFactoryImpl.swift +++ b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainFactoryImpl.swift @@ -7,6 +7,7 @@ import MLSRecommendationFeatureInterface public struct RecommendationMainFactoryImpl: RecommendationMainFactory { private let repository: RecommendationRepository private let bookmarkRepository: BookmarkRepository + private let recommendationUserDefaultsRepository: RecommendationUserDefaultsRepository private let makeLoginVC: (() -> UIViewController)? private let makeCharacterSettingVC: (() -> UIViewController)? private let makeSearchVC: (() -> UIViewController)? @@ -17,6 +18,7 @@ public struct RecommendationMainFactoryImpl: RecommendationMainFactory { public init( repository: RecommendationRepository, bookmarkRepository: BookmarkRepository, + recommendationUserDefaultsRepository: RecommendationUserDefaultsRepository, makeLoginVC: (() -> UIViewController)? = nil, makeCharacterSettingVC: (() -> UIViewController)? = nil, makeSearchVC: (() -> UIViewController)? = nil, @@ -26,6 +28,7 @@ public struct RecommendationMainFactoryImpl: RecommendationMainFactory { ) { self.repository = repository self.bookmarkRepository = bookmarkRepository + self.recommendationUserDefaultsRepository = recommendationUserDefaultsRepository self.makeLoginVC = makeLoginVC self.makeCharacterSettingVC = makeCharacterSettingVC self.makeSearchVC = makeSearchVC @@ -36,7 +39,7 @@ public struct RecommendationMainFactoryImpl: RecommendationMainFactory { public func make() -> BaseViewController { let vc = RecommendationMainViewController() - vc.reactor = RecommendationMainReactor(repository: repository, bookmarkRepository: bookmarkRepository) + vc.reactor = RecommendationMainReactor(repository: repository, bookmarkRepository: bookmarkRepository, userDefaultsRepository: recommendationUserDefaultsRepository) vc.onLoginTapped = makeLoginVC vc.onEditTapped = makeCharacterSettingVC vc.onSearchTapped = makeSearchVC diff --git a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainReactor.swift b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainReactor.swift index b89d2847..e34cad2c 100644 --- a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainReactor.swift +++ b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainReactor.swift @@ -7,7 +7,6 @@ import RxCocoa import RxSwift final class RecommendationMainReactor: Reactor { - // MARK: - Reactor enum Action { case viewWillAppear @@ -20,6 +19,7 @@ final class RecommendationMainReactor: Reactor { case none case added(RecommendationMap) case deleted(RecommendationMap) + case showAlert } enum Mutation { @@ -53,11 +53,13 @@ final class RecommendationMainReactor: Reactor { private let repository: RecommendationRepository private let bookmarkRepository: BookmarkRepository + private let userDefaultsRepository: RecommendationUserDefaultsRepository // MARK: - Init - init(repository: RecommendationRepository, bookmarkRepository: BookmarkRepository) { + init(repository: RecommendationRepository, bookmarkRepository: BookmarkRepository, userDefaultsRepository: RecommendationUserDefaultsRepository) { self.repository = repository self.bookmarkRepository = bookmarkRepository + self.userDefaultsRepository = userDefaultsRepository self.initialState = State() } @@ -65,35 +67,56 @@ final class RecommendationMainReactor: Reactor { func mutate(action: Action) -> Observable { switch action { case .viewWillAppear: + let saveFirstRecommendationLaunch = userDefaultsRepository + .saveFirstLaunch() + .andThen(Observable.empty()) + let fetchAll = repository.fetchProfile() .flatMap { [weak self] profile -> Observable in guard let self else { return .empty() } + let setLogin = Observable.just(Mutation.setLogin(true)) let setProfile = Observable.just(Mutation.setProfile(profile)) - let setJobName: Observable - if let jobId = profile.jobId { - setJobName = repository.fetchJobName(jobId: jobId) - .map { Mutation.setJobName($0) } - .catch { error in - print("⚠️ [Recommendation] fetchJobName 실패: \(error)") - return .empty() - } - } else { - setJobName = .empty() + + // 프로필 미입력 상태 + guard + let level = profile.level, + let jobId = profile.jobId + else { + return Observable.concat([ + setLogin, + setProfile, + .just(.setUIEvent(.showAlert)) + ]) } - let setRecommendations: Observable - if let level = profile.level, level >= 1, let jobId = profile.jobId { - setRecommendations = repository.fetchRecommendations(level: level, jobId: jobId, limit: 5) - .map { Mutation.setRecommendations($0) } - .catch { error in - print("⚠️ [Recommendation] fetchRecommendations 실패: \(error)") - return .empty() - } - } else { - setRecommendations = .empty() + + // 프로필 입력 완료 상태 + let setJobName = repository.fetchJobName(jobId: jobId) + .map(Mutation.setJobName) + .catch { error in + print("⚠️ [Recommendation] fetchJobName 실패: \(error)") + return .empty() + } + + let setRecommendations = repository.fetchRecommendations( + level: level, + jobId: jobId, + limit: 5 + ) + .map(Mutation.setRecommendations) + .catch { error in + print("⚠️ [Recommendation] fetchRecommendations 실패: \(error)") + return .empty() } - let parallelRequests = Observable.merge([setJobName, setRecommendations]) - return Observable.concat([setLogin, setProfile, parallelRequests]) + + return Observable.concat([ + setLogin, + setProfile, + Observable.merge([ + setJobName, + setRecommendations + ]) + ]) } .catch { error in print("⚠️ [Recommendation] fetchProfile 실패: \(error)") @@ -102,7 +125,10 @@ final class RecommendationMainReactor: Reactor { return Observable.concat([ .just(.setLoading(true)), - fetchAll, + Observable.merge([ + fetchAll, + saveFirstRecommendationLaunch + ]), .just(.setLoading(false)) ]) @@ -121,17 +147,17 @@ final class RecommendationMainReactor: Reactor { var newState = state switch mutation { - case .setProfile(let profile): + case let .setProfile(profile): newState.profile = profile - case .setJobName(let jobName): + case let .setJobName(jobName): newState.jobName = jobName - case .setRecommendations(let maps): + case let .setRecommendations(maps): newState.recommendations = maps - case .setLoading(let isLoading): + case let .setLoading(isLoading): newState.isLoading = isLoading case .informationButtonToggle: newState.informationButtonIsOn.toggle() - case .setLogin(let isLogin): + case let .setLogin(isLogin): newState.isLogin = isLogin case let .updateBookmarkId(mapId, bookmarkId): newState.togglingBookmarkIds.remove(mapId) @@ -160,7 +186,8 @@ final class RecommendationMainReactor: Reactor { private extension RecommendationMainReactor { func handleToggleBookmark(mapId: Int) -> Observable { guard !currentState.togglingBookmarkIds.contains(mapId), - let map = currentState.recommendations.first(where: { $0.mapId == mapId }) else { + let map = currentState.recommendations.first(where: { $0.mapId == mapId }) + else { return .empty() } diff --git a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainViewController.swift b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainViewController.swift index f66b5773..1f76138f 100644 --- a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainViewController.swift +++ b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainViewController.swift @@ -10,7 +10,6 @@ import RxSwift import SnapKit final class RecommendationMainViewController: BaseViewController, View { - typealias Reactor = RecommendationMainReactor // MARK: - Properties @@ -144,6 +143,15 @@ extension RecommendationMainViewController { owner.presentDeleteSnackBar(map: map) case .none: break + case .showAlert: + GuideAlertFactory.show(mainText: "레벨과 직업을 모두 입력하면\n맞춤 사냥터를 추천받을 수 있어요", ctaText: "입력하기", cancelText: "도감보러 가기", ctaAction: { + guard let vc = owner.onEditTapped?() else { return } + owner.navigationController?.pushViewController(vc, animated: true) + }, cancelAction: { + if let tabBarController = owner.tabBarController as? BottomTabBarController { + tabBarController.selectTab(index: 0) + } + }, ctaRatio: 0.5) } }) .disposed(by: disposeBag) @@ -206,7 +214,7 @@ private extension RecommendationMainViewController { buttonAction: { [weak self] in guard let self, let bookmarkId = self.reactor?.currentState.recommendations - .first(where: { $0.mapId == map.mapId })?.bookmarkId else { return } + .first(where: { $0.mapId == map.mapId })?.bookmarkId else { return } let vc = self.onBookmarkModalTapped?([bookmarkId]) { isAdd in if isAdd { ToastFactory.createToast(message: "컬렉션에 추가되었어요. 북마크 탭에서 확인 할 수 있어요.") diff --git a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeatureInterface/Repositories/RecommendationRepository 2.swift b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeatureInterface/Repositories/RecommendationRepository 2.swift new file mode 100644 index 00000000..cdf87b34 --- /dev/null +++ b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeatureInterface/Repositories/RecommendationRepository 2.swift @@ -0,0 +1,5 @@ +import RxSwift + +public protocol RecommendationUserDefaultsRepository { + func saveFirstLaunch() -> Completable +}