diff --git a/MLS/MLS.xcodeproj/project.pbxproj b/MLS/MLS.xcodeproj/project.pbxproj index 49b4a614..e69e579b 100644 --- a/MLS/MLS.xcodeproj/project.pbxproj +++ b/MLS/MLS.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 085A7F752DAF99570046663F /* .swiftlint.yml in Resources */ = {isa = PBXBuildFile; fileRef = 085A7F742DAF99570046663F /* .swiftlint.yml */; }; 088636972FB4664E00006D4A /* MLSRecommendationFeatureTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 088636962FB4664E00006D4A /* MLSRecommendationFeatureTesting */; }; + 0898D45E2FD913470031ED17 /* MLSBookmarkFeatureTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 0898D45D2FD913470031ED17 /* MLSBookmarkFeatureTesting */; }; 08DA51B42E1B9827009097A6 /* FirebaseFirestore in Frameworks */ = {isa = PBXBuildFile; productRef = 08DA51B32E1B9827009097A6 /* FirebaseFirestore */; }; 08DA51B62E1B9827009097A6 /* FirebaseMessaging in Frameworks */ = {isa = PBXBuildFile; productRef = 08DA51B52E1B9827009097A6 /* FirebaseMessaging */; }; 08ED49282DCFDED4002C21A2 /* RxCocoa in Frameworks */ = {isa = PBXBuildFile; productRef = 08ED49272DCFDED4002C21A2 /* RxCocoa */; }; @@ -289,6 +290,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 0898D45E2FD913470031ED17 /* MLSBookmarkFeatureTesting in Frameworks */, 08F7DC802F9DEA8100EF5C06 /* MLSRecommendationFeature in Frameworks */, 08F7DC822F9DEA8100EF5C06 /* MLSRecommendationFeatureInterface in Frameworks */, 088636972FB4664E00006D4A /* MLSRecommendationFeatureTesting in Frameworks */, @@ -561,6 +563,7 @@ 08F7DC832F9DEA8100EF5C06 /* MLSRecommendationFeatureInterface */, 08F7DC852F9DEA8100EF5C06 /* MLSCore */, 088636962FB4664E00006D4A /* MLSRecommendationFeatureTesting */, + 0898D45D2FD913470031ED17 /* MLSBookmarkFeatureTesting */, ); productName = MLSRecommendationFeatureExample; productReference = 08F7DC612F9DEA8000EF5C06 /* MLSRecommendationFeatureExample.app */; @@ -1785,6 +1788,10 @@ isa = XCSwiftPackageProductDependency; productName = MLSRecommendationFeatureTesting; }; + 0898D45D2FD913470031ED17 /* MLSBookmarkFeatureTesting */ = { + isa = XCSwiftPackageProductDependency; + productName = MLSBookmarkFeatureTesting; + }; 08DA51B32E1B9827009097A6 /* FirebaseFirestore */ = { isa = XCSwiftPackageProductDependency; package = 08DA51B22E1B9827009097A6 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; diff --git a/MLS/MLS/Application/ViewController.swift b/MLS/MLS/Application/ViewController.swift deleted file mode 100644 index d5b12d72..00000000 --- a/MLS/MLS/Application/ViewController.swift +++ /dev/null @@ -1,13 +0,0 @@ -import UIKit - -import Data - -import RxCocoa -import RxSwift - -class ViewController: UIViewController { - override func viewDidLoad() { - super.viewDidLoad() - view.backgroundColor = .red - } -} diff --git a/MLS/MLSAppFeature/Package.swift b/MLS/MLSAppFeature/Package.swift index 0f4711aa..aff5bedf 100644 --- a/MLS/MLSAppFeature/Package.swift +++ b/MLS/MLSAppFeature/Package.swift @@ -37,7 +37,9 @@ let package = Package( // Interface 모듈 (도메인 모델 및 프로토콜) .target( name: "MLSAppFeatureInterface", - dependencies: [] + dependencies: [ + .product(name: "MLSCore", package: "MLSCore") + ] ), // Feature 모듈 (실제 구현) .target( diff --git a/MLS/MLSAppFeature/Sources/MLSAppFeature/AppInfo.swift b/MLS/MLSAppFeature/Sources/MLSAppFeature/AppInfo.swift new file mode 100644 index 00000000..e62a49bc --- /dev/null +++ b/MLS/MLSAppFeature/Sources/MLSAppFeature/AppInfo.swift @@ -0,0 +1,5 @@ +enum AppInfo { + // TODO: 앱스토어 출시 후 실제 앱 ID로 교체 + static let appStoreID = "6477212894" + static let appStoreURL = "itms-apps://itunes.apple.com/app/id\(appStoreID)" +} diff --git a/MLS/MLSAppFeature/Sources/MLSAppFeature/Coordinator/AppCoordinator.swift b/MLS/MLSAppFeature/Sources/MLSAppFeature/Coordinator/AppCoordinator.swift index 023293cb..0487aef1 100644 --- a/MLS/MLSAppFeature/Sources/MLSAppFeature/Coordinator/AppCoordinator.swift +++ b/MLS/MLSAppFeature/Sources/MLSAppFeature/Coordinator/AppCoordinator.swift @@ -42,7 +42,7 @@ public final class AppCoordinator: AppCoordinatorProtocol { } // MARK: - Public Methods - public func showMainTab() { + public func showMainTab(selectedIndex: Int = 0) { let tabItems: [TabItem] = [ TabItem(title: "추천", icon: UIImage(systemName: "star.fill") ?? UIImage()), TabItem(title: "도감", icon: DesignSystemAsset.image(named: "dictionary")), @@ -56,7 +56,8 @@ public final class AppCoordinator: AppCoordinatorProtocol { bookmarkMainFactory.make(bottomInset: 64), myPageMainFactory.make() ], - tabItems: tabItems + tabItems: tabItems, + initialIndex: selectedIndex ) let navigationController = UINavigationController(rootViewController: tabBar) diff --git a/MLS/MLSAppFeature/Sources/MLSAppFeature/Dependency/FactoryAssembly.swift b/MLS/MLSAppFeature/Sources/MLSAppFeature/Dependency/FactoryAssembly.swift index 8f1ef38e..6eb3b6d3 100644 --- a/MLS/MLSAppFeature/Sources/MLSAppFeature/Dependency/FactoryAssembly.swift +++ b/MLS/MLSAppFeature/Sources/MLSAppFeature/Dependency/FactoryAssembly.swift @@ -73,7 +73,7 @@ public enum FactoryAssembly { type: DetailOnBoardingFactory.self ), appCoordinator: { - DIContainer.resolve(type: AppCoordinator.self) + DIContainer.resolve(type: AppCoordinatorProtocol.self) }, dictionaryDetailAPIRepository: DIContainer.resolve( type: DictionaryDetailAPIRepository.self @@ -415,15 +415,14 @@ public enum FactoryAssembly { DIContainer .resolve(type: CheckValidLevelUseCase.self), authRepository: DIContainer - .resolve(type: AuthAPIRepository.self) - ) - } - DIContainer.register(type: SelectImageFactory.self) { - SelectImageFactoryImpl( + .resolve(type: AuthAPIRepository.self), myPageRepository: DIContainer .resolve(type: MyPageRepository.self) ) } + DIContainer.register(type: SelectImageFactory.self) { + SelectImageFactoryImpl() + } DIContainer.register(type: DetailOnBoardingFactory.self) { DetailOnBoardingFactoryImpl() } @@ -432,9 +431,30 @@ public enum FactoryAssembly { } DIContainer.register(type: RecommendationMainFactory.self) { RecommendationMainFactoryImpl( - repository: DIContainer.resolve( - type: RecommendationRepository.self - ) + repository: DIContainer.resolve(type: RecommendationRepository.self), + bookmarkRepository: DIContainer.resolve(type: BookmarkRepository.self), + makeLoginVC: { + DIContainer.resolve(type: LoginFactory.self).make(exitRoute: .pop) + }, + makeCharacterSettingVC: { + DIContainer.resolve(type: SetCharacterFactory.self).make() + }, + makeSearchVC: { + DIContainer.resolve(type: DictionarySearchFactory.self).make() + }, + makeNotificationVC: { + DIContainer.resolve(type: DictionaryNotificationFactory.self).make() + }, + makeDetailVC: { mapId in + DIContainer.resolve(type: DictionaryDetailFactory.self).make( + type: .map, id: mapId, bookmarkRelay: nil, loginRelay: nil + ) + }, + makeBookmarkModalVC: { bookmarkIds, onComplete in + DIContainer.resolve(type: BookmarkModalFactory.self).make( + bookmarkIds: bookmarkIds, onComplete: onComplete + ) + } ) } } diff --git a/MLS/MLSAppFeature/Sources/MLSAppFeature/Dependency/RepositoryAssembly.swift b/MLS/MLSAppFeature/Sources/MLSAppFeature/Dependency/RepositoryAssembly.swift index ac64a654..1d8eb1b4 100644 --- a/MLS/MLSAppFeature/Sources/MLSAppFeature/Dependency/RepositoryAssembly.swift +++ b/MLS/MLSAppFeature/Sources/MLSAppFeature/Dependency/RepositoryAssembly.swift @@ -1,3 +1,4 @@ +import MLSAppFeatureInterface import MLSAuthFeature import MLSAuthFeatureInterface import MLSBookmarkFeature @@ -80,5 +81,11 @@ public enum RepositoryAssembly { DIContainer.register(type: BookmarkUserDefaultsRepository.self) { BookmarkUserDefaultsRepositoryImpl() } + DIContainer.register(type: AppStoreRepositoryProtocol.self) { + AppStoreRepository() + } + DIContainer.register(type: UpdateSkipRepositoryProtocol.self) { + UpdateSkipRepository() + } } } diff --git a/MLS/MLSAppFeature/Sources/MLSAppFeature/Dependency/UseCaseAssembly.swift b/MLS/MLSAppFeature/Sources/MLSAppFeature/Dependency/UseCaseAssembly.swift index f7f52fd8..2fa2b568 100644 --- a/MLS/MLSAppFeature/Sources/MLSAppFeature/Dependency/UseCaseAssembly.swift +++ b/MLS/MLSAppFeature/Sources/MLSAppFeature/Dependency/UseCaseAssembly.swift @@ -1,3 +1,4 @@ +import MLSAppFeatureInterface import MLSAuthFeature import MLSAuthFeatureInterface import MLSCore @@ -82,5 +83,12 @@ public enum UseCaseAssembly { .resolve(type: UserDefaultsRepository.self, name: "authUserDefaultsRepository") ) } + DIContainer.register(type: UpdateCheckerUseCaseProtocol.self) { + UpdateCheckerUseCase( + appID: AppInfo.appStoreID, + appStoreRepository: DIContainer.resolve(type: AppStoreRepositoryProtocol.self), + skipRepository: DIContainer.resolve(type: UpdateSkipRepositoryProtocol.self) + ) + } } } diff --git a/MLS/MLSAppFeature/Sources/MLSAppFeature/Launcher/AppLauncher.swift b/MLS/MLSAppFeature/Sources/MLSAppFeature/Launcher/AppLauncher.swift index e6d8c598..61f46513 100644 --- a/MLS/MLSAppFeature/Sources/MLSAppFeature/Launcher/AppLauncher.swift +++ b/MLS/MLSAppFeature/Sources/MLSAppFeature/Launcher/AppLauncher.swift @@ -3,12 +3,14 @@ import UIKit import MLSAppFeatureInterface import MLSAuthFeatureInterface import MLSCore +import MLSDesignSystem import RxSwift @MainActor public final class AppLauncher { private let disposeBag = DisposeBag() + private var forceUpdateObserver: NSObjectProtocol? public init() {} @@ -26,16 +28,18 @@ public final class AppLauncher { authRepository.reissueToken(refreshToken: refreshToken) .observe(on: MainScheduler.instance) .subscribe( - onNext: { _ in + onNext: { [weak self] _ in Task { @MainActor in let coordinator = DIContainer.resolve(type: AppCoordinatorProtocol.self) coordinator.showMainTab() + self?.checkUpdateIfNeeded() } }, - onError: { _ in + onError: { [weak self] _ in Task { @MainActor in let coordinator = DIContainer.resolve(type: AppCoordinatorProtocol.self) coordinator.showLogin(exitRoute: .home) + self?.checkUpdateIfNeeded() } } ) @@ -44,6 +48,7 @@ public final class AppLauncher { case .failure: let coordinator = DIContainer.resolve(type: AppCoordinatorProtocol.self) coordinator.showLogin(exitRoute: .home) + checkUpdateIfNeeded() } } @@ -51,3 +56,66 @@ public final class AppLauncher { DependencyAssembler.register() } } + +// MARK: - Update Check +private extension AppLauncher { + func checkUpdateIfNeeded() { + guard + let versionString = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String, + let currentVersion = Version(versionString: versionString) + else { return } + + Task { @MainActor in + let useCase = DIContainer.resolve(type: UpdateCheckerUseCaseProtocol.self) + guard let status = try? await useCase.checkUpdate(currentVersion: currentVersion) else { return } + + switch status { + case .force: + showForceUpdateAlert() + if let existingObserver = forceUpdateObserver { + NotificationCenter.default.removeObserver(existingObserver) + } + forceUpdateObserver = NotificationCenter.default.addObserver( + forName: UIApplication.didBecomeActiveNotification, + object: nil, + queue: .main + ) { [weak self] _ in + self?.showForceUpdateAlert() + } + case .optional(let latestVersion): + GuideAlertFactory.show( + mainText: "앱 버전이 달라요.\n보다 좋은 서비스를 위해 업데이트 해주세요.", + ctaText: "업데이트 하기", + cancelText: "나중에", + ctaAction: { [weak self] in self?.openAppStore() }, + cancelAction: { useCase.skipUpdate(version: latestVersion) } + ) + case .none: + break + } + } + } + + func showForceUpdateAlert() { + GuideAlertFactory.show( + mainText: "앱 버전이 달라요.\n보다 좋은 서비스를 위해 업데이트 해주세요.", + ctaText: "업데이트 하기", + cancelText: nil, + ctaAction: { [weak self] in self?.openAppStore() } + ) + } + + func openAppStore() { + guard let url = URL(string: AppInfo.appStoreURL) else { + showForceUpdateAlert() + return + } + UIApplication.shared.open(url, options: [:]) { [weak self] success in + if !success { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + self?.showForceUpdateAlert() + } + } + } + } +} diff --git a/MLS/MLSAppFeature/Sources/MLSAppFeatureInterface/Domain/Navigation/AppCoordinatorProtocol.swift b/MLS/MLSAppFeature/Sources/MLSAppFeatureInterface/Domain/Navigation/AppCoordinatorProtocol.swift index 94bf5903..6f04b060 100644 --- a/MLS/MLSAppFeature/Sources/MLSAppFeatureInterface/Domain/Navigation/AppCoordinatorProtocol.swift +++ b/MLS/MLSAppFeature/Sources/MLSAppFeatureInterface/Domain/Navigation/AppCoordinatorProtocol.swift @@ -1,8 +1,15 @@ +import MLSCore import UIKit @MainActor public protocol AppCoordinatorProtocol: AnyObject { var window: UIWindow? { get set } - func showMainTab() + func showMainTab(selectedIndex: Int) func showLogin(exitRoute: LoginExitRoute) } + +public extension AppCoordinatorProtocol { + func showMainTab() { + showMainTab(selectedIndex: 0) + } +} diff --git a/MLS/MLSAuthFeature/Package.swift b/MLS/MLSAuthFeature/Package.swift index f08b56b4..7dea5260 100644 --- a/MLS/MLSAuthFeature/Package.swift +++ b/MLS/MLSAuthFeature/Package.swift @@ -36,7 +36,6 @@ let package = Package( .target( name: "MLSAuthFeatureInterface", dependencies: [ - .product(name: "MLSAppFeatureInterface", package: "MLSAppFeature"), .product(name: "MLSCore", package: "MLSCore"), .product(name: "MLSDesignSystem", package: "MLSDesignSystem"), .product(name: "RxSwift", package: "RxSwift") diff --git a/MLS/MLSAuthFeature/Sources/MLSAuthFeatureInterface/Factories/LoginFactory.swift b/MLS/MLSAuthFeature/Sources/MLSAuthFeatureInterface/Factories/LoginFactory.swift index fc5095a0..a7a7cd36 100644 --- a/MLS/MLSAuthFeature/Sources/MLSAuthFeatureInterface/Factories/LoginFactory.swift +++ b/MLS/MLSAuthFeature/Sources/MLSAuthFeatureInterface/Factories/LoginFactory.swift @@ -1,4 +1,3 @@ -import MLSAppFeatureInterface import MLSCore public protocol LoginFactory { diff --git a/MLS/MLSAppFeature/Sources/MLSAppFeatureInterface/Domain/Navigation/LoginExitRoute.swift b/MLS/MLSCore/Sources/MLSCore/Utils/LoginExitRoute.swift similarity index 80% rename from MLS/MLSAppFeature/Sources/MLSAppFeatureInterface/Domain/Navigation/LoginExitRoute.swift rename to MLS/MLSCore/Sources/MLSCore/Utils/LoginExitRoute.swift index 6807d771..ea927460 100644 --- a/MLS/MLSAppFeature/Sources/MLSAppFeatureInterface/Domain/Navigation/LoginExitRoute.swift +++ b/MLS/MLSCore/Sources/MLSCore/Utils/LoginExitRoute.swift @@ -1,5 +1,3 @@ -import UIKit - public enum LoginExitRoute { case pop case home diff --git a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/DictionaryDetailListView.swift b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/DictionaryDetailListView.swift index a3b9c42e..0c0c16a9 100644 --- a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/DictionaryDetailListView.swift +++ b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/DictionaryDetailListView.swift @@ -76,7 +76,7 @@ private extension DictionaryDetailListView { func makeButton(label: UILabel) -> UIButton { let button = UIButton() - let icon = UIImageView(image: UIImage(named: "rightArrow")) + let icon = UIImageView(image: DesignSystemAsset.image(named: "rightArrow")) button.addSubview(label) button.addSubview(icon) diff --git a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/DropDownBox/DropDownBox.swift b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/DropDownBox/DropDownBox.swift index 7be4f3f8..a1a3c2ce 100644 --- a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/DropDownBox/DropDownBox.swift +++ b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/DropDownBox/DropDownBox.swift @@ -169,6 +169,17 @@ extension DropDownBox: UITableViewDataSource, UITableViewDelegate { } } +// MARK: - Public Methods +public extension DropDownBox { + func selectItem(id: Int) { + guard let index = items.firstIndex(where: { $0.id == id }) else { return } + selectedIndex = index + inputBox.textField.attributedText = .makeStyledString( + font: .b_m_r, text: items[index].name, alignment: .left, lineHeight: 1.0 + ) + } +} + // MARK: Model extension DropDownBox { public struct Item { diff --git a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/ErrorMessage.swift b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/ErrorMessage.swift index 973bb586..d4a3dca1 100644 --- a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/ErrorMessage.swift +++ b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/ErrorMessage.swift @@ -15,7 +15,7 @@ public final class ErrorMessage: UIView { // MARK: - Properties private let iconView: UIImageView = { let view = UIImageView() - view.image = UIImage(named: "error") + view.image = DesignSystemAsset.image(named: "error") return view }() diff --git a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/FloatingActionButton.swift b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/FloatingActionButton.swift index 841459a8..ee884ebf 100644 --- a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/FloatingActionButton.swift +++ b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/FloatingActionButton.swift @@ -22,7 +22,7 @@ public final class FloatingActionButton: UIButton { // MARK: - SetUp private extension FloatingActionButton { func configureUI() { - setImage(UIImage(named: "fab"), for: .normal) + setImage(DesignSystemAsset.image(named: "fab"), for: .normal) layer.cornerRadius = 24 clipsToBounds = true addTarget(self, action: #selector(buttonTapped), for: .touchUpInside) diff --git a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/GuideAlert.swift b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/GuideAlert.swift index 13e1f05e..11571cbb 100644 --- a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/GuideAlert.swift +++ b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/GuideAlert.swift @@ -18,7 +18,7 @@ public class GuideAlert: UIView { // MARK: - Components private let warningIconView: UIImageView = { let view = UIImageView() - view.image = UIImage(named: "warning") + view.image = DesignSystemAsset.image(named: "warning") return view }() @@ -34,7 +34,7 @@ public class GuideAlert: UIView { // MARK: - init public init(mainText: String, ctaText: String, cancelText: String?, ctaRatio: Double = 0.7) { - mainTextLabel.attributedText = .makeStyledString(font: .sub_l_b, text: mainText) + mainTextLabel.attributedText = .makeStyledString(font: .sub_m_sb, text: mainText) self.ctaButton = CommonButton(style: .normal, title: ctaText, disabledTitle: nil) self.cancelButton = cancelText.map { CommonButton(style: .border, title: $0, disabledTitle: nil) } mainTextLabel.numberOfLines = 0 diff --git a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tabbar/BottomTabBarController.swift b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tabbar/BottomTabBarController.swift index c2cea63a..faa4ea56 100644 --- a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tabbar/BottomTabBarController.swift +++ b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tabbar/BottomTabBarController.swift @@ -12,6 +12,11 @@ public final class BottomTabBarController: UITabBarController { private let divider = DividerView() private let tabItems: [TabItem] private let customTabBar: BottomTabBar + private let tabBarBackground: UIView = { + let view = UIView() + view.backgroundColor = .systemBackground + return view + }() // MARK: - Init public init(viewControllers: [UIViewController], tabItems: [TabItem]? = nil, initialIndex: Int = 0) { @@ -42,6 +47,7 @@ public final class BottomTabBarController: UITabBarController { // MARK: - SetUp private extension BottomTabBarController { func addViews() { + view.addSubview(tabBarBackground) view.addSubview(customTabBar) view.addSubview(divider) } @@ -56,15 +62,23 @@ private extension BottomTabBarController { make.horizontalEdges.equalToSuperview().inset(Constant.horizontalInset) make.bottom.equalTo(view.safeAreaLayoutGuide) } + + tabBarBackground.snp.makeConstraints { make in + make.horizontalEdges.bottom.equalToSuperview() + make.top.equalTo(divider.snp.top) + } } func configureUI(controllers: [UIViewController]) { viewControllers = controllers.map { - if $0 is UINavigationController { - return $0 + let nav: UINavigationController + if let existing = $0 as? UINavigationController { + nav = existing } else { - return UINavigationController(rootViewController: $0) + nav = UINavigationController(rootViewController: $0) } + nav.delegate = self + return nav } tabBar.isHidden = true @@ -77,6 +91,17 @@ private extension BottomTabBarController { } } +extension BottomTabBarController: UINavigationControllerDelegate { + public func navigationController( + _ navigationController: UINavigationController, + willShow viewController: UIViewController, + animated: Bool + ) { + let isRoot = navigationController.viewControllers.count == 1 + setHidden(hidden: !isRoot, animated: isRoot && animated) + } +} + public extension BottomTabBarController { func setHidden(hidden: Bool, animated: Bool = false) { guard customTabBar.isHidden != hidden else { return } @@ -85,15 +110,19 @@ public extension BottomTabBarController { UIView.animate(withDuration: 0.3) { self.customTabBar.alpha = hidden ? 0 : 1 self.divider.alpha = hidden ? 0 : 1 + self.tabBarBackground.alpha = hidden ? 0 : 1 } completion: { _ in self.customTabBar.isHidden = hidden self.divider.isHidden = hidden + self.tabBarBackground.isHidden = hidden } } else { customTabBar.isHidden = hidden customTabBar.alpha = hidden ? 0 : 1 divider.isHidden = hidden divider.alpha = hidden ? 0 : 1 + tabBarBackground.isHidden = hidden + tabBarBackground.alpha = hidden ? 0 : 1 } } diff --git a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/TextButton.swift b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/TextButton.swift index 3b05ec06..29f54a58 100644 --- a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/TextButton.swift +++ b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/TextButton.swift @@ -16,7 +16,7 @@ public final class TextButton: UIButton { // MARK: - Properties public let iconView: UIImageView = { let view = UIImageView() - view.image = UIImage(named: "edit")?.withRenderingMode(.alwaysTemplate) + view.image = DesignSystemAsset.image(named: "edit").withRenderingMode(.alwaysTemplate) view.tintColor = .neutral700 return view }() diff --git a/MLS/MLSDictionaryFeature/Package.swift b/MLS/MLSDictionaryFeature/Package.swift index c4827025..65ccfc58 100644 --- a/MLS/MLSDictionaryFeature/Package.swift +++ b/MLS/MLSDictionaryFeature/Package.swift @@ -72,6 +72,7 @@ let package = Package( .product(name: "MLSAuthFeatureInterface", package: "MLSAuthFeature"), .product(name: "MLSBookmarkFeatureInterface", package: "MLSBookmarkFeature"), .product(name: "MLSMyPageFeatureInterface", package: "MLSMyPageFeature"), + .product(name: "MLSCore", package: "MLSCore"), .product(name: "RxSwift", package: "RxSwift") ], swiftSettings: [.swiftLanguageMode(.v5)] diff --git a/MLS/MLSDictionaryFeature/Sources/MLSDictionaryFeature/Presentation/DictionaryDetail/DictionaryDetailBaseViewController.swift b/MLS/MLSDictionaryFeature/Sources/MLSDictionaryFeature/Presentation/DictionaryDetail/DictionaryDetailBaseViewController.swift index d2523e73..f4e0ae5e 100644 --- a/MLS/MLSDictionaryFeature/Sources/MLSDictionaryFeature/Presentation/DictionaryDetail/DictionaryDetailBaseViewController.swift +++ b/MLS/MLSDictionaryFeature/Sources/MLSDictionaryFeature/Presentation/DictionaryDetail/DictionaryDetailBaseViewController.swift @@ -336,7 +336,7 @@ private extension DictionaryDetailBaseViewController { .observe(on: MainScheduler.instance) .withUnretained(self) .bind { owner, _ in - owner.appCoordinator.showMainTab() + owner.appCoordinator.showMainTab(selectedIndex: 1) } .disposed(by: disposeBag) diff --git a/MLS/MLSDictionaryFeature/Sources/MLSDictionaryFeatureTesting/Mock/MockAppCoordinator.swift b/MLS/MLSDictionaryFeature/Sources/MLSDictionaryFeatureTesting/Mock/MockAppCoordinator.swift index 105b5447..4e5605f4 100644 --- a/MLS/MLSDictionaryFeature/Sources/MLSDictionaryFeatureTesting/Mock/MockAppCoordinator.swift +++ b/MLS/MLSDictionaryFeature/Sources/MLSDictionaryFeatureTesting/Mock/MockAppCoordinator.swift @@ -2,13 +2,14 @@ import UIKit import MLSAppFeatureInterface import MLSAuthFeatureInterface +import MLSCore public final class MockAppCoordinator: AppCoordinatorProtocol { public var window: UIWindow? public init() {} - public func showMainTab() { + public func showMainTab(selectedIndex: Int) { } public func showLogin(exitRoute: LoginExitRoute) { diff --git a/MLS/MLSMyPageFeature/Package.swift b/MLS/MLSMyPageFeature/Package.swift index 96ac88da..3654f6aa 100644 --- a/MLS/MLSMyPageFeature/Package.swift +++ b/MLS/MLSMyPageFeature/Package.swift @@ -64,7 +64,10 @@ let package = Package( name: "MLSMyPageFeatureTesting", dependencies: [ "MLSMyPageFeatureInterface", + .product(name: "MLSAppFeatureInterface", package: "MLSAppFeature"), .product(name: "MLSAppFeatureTesting", package: "MLSAppFeature"), + .product(name: "MLSAuthFeatureInterface", package: "MLSAuthFeature"), + .product(name: "MLSCore", package: "MLSCore"), .product(name: "RxSwift", package: "RxSwift") ], swiftSettings: [.swiftLanguageMode(.v5)] diff --git a/MLS/MLSMyPageFeature/Sources/MLSMyPageFeature/Presentation/SelectImage/SelectImageFactoryImpl.swift b/MLS/MLSMyPageFeature/Sources/MLSMyPageFeature/Presentation/SelectImage/SelectImageFactoryImpl.swift index 6cda50e2..4e5e06c0 100644 --- a/MLS/MLSMyPageFeature/Sources/MLSMyPageFeature/Presentation/SelectImage/SelectImageFactoryImpl.swift +++ b/MLS/MLSMyPageFeature/Sources/MLSMyPageFeature/Presentation/SelectImage/SelectImageFactoryImpl.swift @@ -3,15 +3,11 @@ import MLSDesignSystem import MLSMyPageFeatureInterface public struct SelectImageFactoryImpl: SelectImageFactory { - private let myPageRepository: MyPageRepository - - public init(myPageRepository: MyPageRepository) { - self.myPageRepository = myPageRepository - } + public init() {} public func make() -> BaseViewController & ModalPresentable { let viewController = SelectImageViewContoller() - viewController.reactor = SelectImageReactor(myPageRepository: myPageRepository) + viewController.reactor = SelectImageReactor() viewController.isBottomTabbarHidden = true return viewController } diff --git a/MLS/MLSMyPageFeature/Sources/MLSMyPageFeature/Presentation/SelectImage/SelectImageReactor.swift b/MLS/MLSMyPageFeature/Sources/MLSMyPageFeature/Presentation/SelectImage/SelectImageReactor.swift index 18add4dd..80fe4e3d 100644 --- a/MLS/MLSMyPageFeature/Sources/MLSMyPageFeature/Presentation/SelectImage/SelectImageReactor.swift +++ b/MLS/MLSMyPageFeature/Sources/MLSMyPageFeature/Presentation/SelectImage/SelectImageReactor.swift @@ -2,7 +2,6 @@ import MLSDesignSystem import MLSMyPageFeatureInterface import ReactorKit -import RxCocoa import RxSwift public final class SelectImageReactor: Reactor { @@ -44,12 +43,9 @@ public final class SelectImageReactor: Reactor { public var initialState: State var disposeBag = DisposeBag() - private let myPageRepository: MyPageRepository - // MARK: - init - public init(myPageRepository: MyPageRepository) { + public init() { self.initialState = State() - self.myPageRepository = myPageRepository } // MARK: - Reactor Methods @@ -58,9 +54,8 @@ public final class SelectImageReactor: Reactor { case .cancelButtonTapped: return .just(.navigateTo(route: .dismiss)) case .applyButtonTapped: - guard let url = currentState.selectedImage?.url else { return .empty() } - return myPageRepository.updateProfileImage(url: url) - .andThen(.just(.navigateTo(route: .dismissWithSave))) + guard currentState.selectedImage != nil else { return .empty() } + return .just(.navigateTo(route: .dismissWithSave)) case .imageTapped(let index): let image = currentState.images[index] return .just(.selectImage(image)) diff --git a/MLS/MLSMyPageFeature/Sources/MLSMyPageFeature/Presentation/SelectImage/SelectImageViewContoller.swift b/MLS/MLSMyPageFeature/Sources/MLSMyPageFeature/Presentation/SelectImage/SelectImageViewContoller.swift index f73ee509..1e2af1dd 100644 --- a/MLS/MLSMyPageFeature/Sources/MLSMyPageFeature/Presentation/SelectImage/SelectImageViewContoller.swift +++ b/MLS/MLSMyPageFeature/Sources/MLSMyPageFeature/Presentation/SelectImage/SelectImageViewContoller.swift @@ -17,6 +17,7 @@ public final class SelectImageViewContoller: BaseViewController, ModalPresentabl public typealias Reactor = SelectImageReactor public var disposeBag = DisposeBag() + public var onImageSelected: ((String) -> Void)? // MARK: - Components @@ -91,6 +92,9 @@ extension SelectImageViewContoller { case .dismiss: owner.dismissCurrentModal() case .dismissWithSave: + if let url = reactor.currentState.selectedImage?.url { + owner.onImageSelected?(url) + } owner.dismissCurrentModal() default: break diff --git a/MLS/MLSMyPageFeature/Sources/MLSMyPageFeature/Presentation/SetCharacter/SetCharacterFactoryImpl.swift b/MLS/MLSMyPageFeature/Sources/MLSMyPageFeature/Presentation/SetCharacter/SetCharacterFactoryImpl.swift index b7157662..fe7304fb 100644 --- a/MLS/MLSMyPageFeature/Sources/MLSMyPageFeature/Presentation/SetCharacter/SetCharacterFactoryImpl.swift +++ b/MLS/MLSMyPageFeature/Sources/MLSMyPageFeature/Presentation/SetCharacter/SetCharacterFactoryImpl.swift @@ -6,15 +6,18 @@ public struct SetCharacterFactoryImpl: SetCharacterFactory { private let checkEmptyUseCase: CheckEmptyLevelAndRoleUseCase private let checkValidLevelUseCase: CheckValidLevelUseCase private let authRepository: AuthAPIRepository + private let myPageRepository: MyPageRepository public init( checkEmptyUseCase: CheckEmptyLevelAndRoleUseCase, checkValidLevelUseCase: CheckValidLevelUseCase, - authRepository: AuthAPIRepository + authRepository: AuthAPIRepository, + myPageRepository: MyPageRepository ) { self.checkEmptyUseCase = checkEmptyUseCase self.checkValidLevelUseCase = checkValidLevelUseCase self.authRepository = authRepository + self.myPageRepository = myPageRepository } public func make() -> BaseViewController { @@ -22,7 +25,8 @@ public struct SetCharacterFactoryImpl: SetCharacterFactory { viewController.reactor = SetCharacterReactor( checkEmptyUseCase: checkEmptyUseCase, checkValidLevelUseCase: checkValidLevelUseCase, - authRepository: authRepository + authRepository: authRepository, + myPageRepository: myPageRepository ) viewController.isBottomTabbarHidden = true return viewController diff --git a/MLS/MLSMyPageFeature/Sources/MLSMyPageFeature/Presentation/SetCharacter/SetCharacterReactor.swift b/MLS/MLSMyPageFeature/Sources/MLSMyPageFeature/Presentation/SetCharacter/SetCharacterReactor.swift index e409653b..0c969190 100644 --- a/MLS/MLSMyPageFeature/Sources/MLSMyPageFeature/Presentation/SetCharacter/SetCharacterReactor.swift +++ b/MLS/MLSMyPageFeature/Sources/MLSMyPageFeature/Presentation/SetCharacter/SetCharacterReactor.swift @@ -45,17 +45,20 @@ public final class SetCharacterReactor: Reactor { private let checkEmptyUseCase: CheckEmptyLevelAndRoleUseCase private let checkValidLevelUseCase: CheckValidLevelUseCase private let authRepository: AuthAPIRepository + private let myPageRepository: MyPageRepository var disposeBag = DisposeBag() // MARK: - init public init( checkEmptyUseCase: CheckEmptyLevelAndRoleUseCase, checkValidLevelUseCase: CheckValidLevelUseCase, - authRepository: AuthAPIRepository + authRepository: AuthAPIRepository, + myPageRepository: MyPageRepository ) { self.checkEmptyUseCase = checkEmptyUseCase self.checkValidLevelUseCase = checkValidLevelUseCase self.authRepository = authRepository + self.myPageRepository = myPageRepository self.initialState = State() } @@ -63,11 +66,28 @@ public final class SetCharacterReactor: Reactor { public func mutate(action: Action) -> Observable { switch action { case .viewWillAppear: - return authRepository.fetchJobList() - .map { response in - .setJobList(jobList: response.jobList) + return Observable.zip( + authRepository.fetchJobList(), + myPageRepository.fetchProfile().catchAndReturn(nil) + ) + .flatMap { [weak self] jobResponse, profile -> Observable in + guard let self else { return .empty() } + var mutations: [Observable] = [ + .just(.setJobList(jobList: jobResponse.jobList)) + ] + if let level = profile?.level { + mutations.append(.just(.setLevel(level))) + mutations.append(.just(.setLevelValid(checkValidLevelUseCase.execute(level: level)))) } - .catchAndReturn(.navigateTo(route: .error)) + if let jobId = profile?.jobId, + let job = jobResponse.jobList.first(where: { $0.id == jobId }) { + mutations.append(.just(.setRole(job))) + } + let isEnabled = checkEmptyUseCase.execute(level: profile?.level, job: profile?.jobName) + mutations.append(.just(.setButtonEnabled(isEnabled))) + return Observable.concat(mutations) + } + .catchAndReturn(.navigateTo(route: .error)) case .backButtonTapped: return Observable.just(.navigateTo(route: .dismiss)) case .applyButtonTapped: diff --git a/MLS/MLSMyPageFeature/Sources/MLSMyPageFeature/Presentation/SetCharacter/SetCharacterViewController.swift b/MLS/MLSMyPageFeature/Sources/MLSMyPageFeature/Presentation/SetCharacter/SetCharacterViewController.swift index 3aa22ea7..06a98eb7 100644 --- a/MLS/MLSMyPageFeature/Sources/MLSMyPageFeature/Presentation/SetCharacter/SetCharacterViewController.swift +++ b/MLS/MLSMyPageFeature/Sources/MLSMyPageFeature/Presentation/SetCharacter/SetCharacterViewController.swift @@ -117,6 +117,28 @@ public extension SetCharacterViewController { .bind(to: mainView.nextButton.rx.isEnabled) .disposed(by: disposeBag) + reactor.state + .map { $0.level } + .compactMap { $0 } + .take(1) + .observe(on: MainScheduler.instance) + .withUnretained(self) + .subscribe { owner, level in + owner.mainView.inputBox.textField.text = "\(level)" + } + .disposed(by: disposeBag) + + reactor.state + .map { $0.job } + .compactMap { $0 } + .take(1) + .observe(on: MainScheduler.instance) + .withUnretained(self) + .subscribe { owner, job in + owner.mainView.dropDownBox.selectItem(id: job.id) + } + .disposed(by: disposeBag) + rx.viewDidAppear .take(1) .flatMapLatest { _ in reactor.pulse(\.$route) } diff --git a/MLS/MLSMyPageFeature/Sources/MLSMyPageFeature/Presentation/SetProfile/SetProfileReactor.swift b/MLS/MLSMyPageFeature/Sources/MLSMyPageFeature/Presentation/SetProfile/SetProfileReactor.swift index d9d6058f..0c12ba17 100644 --- a/MLS/MLSMyPageFeature/Sources/MLSMyPageFeature/Presentation/SetProfile/SetProfileReactor.swift +++ b/MLS/MLSMyPageFeature/Sources/MLSMyPageFeature/Presentation/SetProfile/SetProfileReactor.swift @@ -23,6 +23,7 @@ public final class SetProfileReactor: Reactor { case showBottomSheet case inputNickName(String) case beginEditingNickName + case imageSelected(String) case logout case withdraw } @@ -37,6 +38,7 @@ public final class SetProfileReactor: Reactor { case beginEditting case cancelEditting case completeEditting + case setPendingImageUrl(String?) } // MARK: - State @@ -47,6 +49,8 @@ public final class SetProfileReactor: Reactor { var isEditingNickName = false var profile: MyPageResponse? var nickName = "" + var pendingImageUrl: String? + var wasUpdated = false } // MARK: - Properties @@ -80,12 +84,15 @@ public final class SetProfileReactor: Reactor { .flatMap { Observable.from($0) } case .beginEditingNickName: return .just(.beginSetText(true)) + case .imageSelected(let url): + return .just(.setPendingImageUrl(url)) case .backButtonTapped: switch currentState.setProfileState { case .edit: return .just(.cancelEditting) case .normal: - return .just(.toNavigate(.dismiss)) + let route: Route = currentState.wasUpdated ? .dismissWithUpdate : .dismiss + return .just(.toNavigate(route)) } case .editButtonTapped: switch currentState.setProfileState { @@ -93,8 +100,15 @@ public final class SetProfileReactor: Reactor { if currentState.isShowError { return .empty() } else { - return myPageRepository.updateNickName(nickName: currentState.nickName) - .flatMap { profile in + let saveImage: Completable + if let url = currentState.pendingImageUrl { + saveImage = myPageRepository.updateProfileImage(url: url) + } else { + saveImage = .empty() + } + return saveImage + .andThen(myPageRepository.updateNickName(nickName: currentState.nickName)) + .flatMap { profile -> Observable in Observable.concat([ .just(.setProfile(profile)), .just(.completeEditting) @@ -133,15 +147,23 @@ public final class SetProfileReactor: Reactor { newState.isEditingNickName = isEditing case .cancelEditting: newState.setProfileState = .normal + newState.nickName = state.profile?.nickname ?? "" + newState.pendingImageUrl = nil case .beginEditting: newState.setProfileState = .edit + newState.isShowError = false + newState.isEditingNickName = false case .completeEditting: - newState.route = .dismissWithUpdate + newState.setProfileState = .normal + newState.pendingImageUrl = nil + newState.wasUpdated = true case .setProfile(let profile): newState.profile = profile newState.nickName = profile?.nickname ?? "" case .setNickName(let nickname): newState.nickName = nickname + case .setPendingImageUrl(let url): + newState.pendingImageUrl = url } return newState diff --git a/MLS/MLSMyPageFeature/Sources/MLSMyPageFeature/Presentation/SetProfile/SetProfileViewController.swift b/MLS/MLSMyPageFeature/Sources/MLSMyPageFeature/Presentation/SetProfile/SetProfileViewController.swift index 4a59646b..e0d85b13 100644 --- a/MLS/MLSMyPageFeature/Sources/MLSMyPageFeature/Presentation/SetProfile/SetProfileViewController.swift +++ b/MLS/MLSMyPageFeature/Sources/MLSMyPageFeature/Presentation/SetProfile/SetProfileViewController.swift @@ -49,17 +49,6 @@ public final class SetProfileViewController: BaseViewController, View { setupConstraints() } - public override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - (tabBarController as? BottomTabBarController)? - .setHidden(hidden: true, animated: false) - } - - public override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - (tabBarController as? BottomTabBarController)? - .setHidden(hidden: false, animated: true) - } } // MARK: - Setup @@ -138,8 +127,12 @@ extension SetProfileViewController { .observe(on: MainScheduler.instance) .bind(onNext: { owner, state in owner.view.backgroundColor = state == .edit ? .whiteMLS : .neutral100 + owner.mainView.backButton.isHidden = state == .edit owner.mainView.setCountHidden(state: state) owner.mainView.setState(state: state) + if state == .edit { + owner.mainView.nickNameInputBox.textField.text = reactor.currentState.profile?.nickname ?? "" + } }) .disposed(by: disposeBag) @@ -155,6 +148,17 @@ extension SetProfileViewController { }) .disposed(by: disposeBag) + reactor.state + .map(\.pendingImageUrl) + .distinctUntilChanged() + .compactMap { $0 } + .withUnretained(self) + .observe(on: MainScheduler.instance) + .bind(onNext: { owner, url in + owner.mainView.setImage(imageUrl: url) + }) + .disposed(by: disposeBag) + reactor.state .compactMap(\.nickName) .distinctUntilChanged() @@ -196,16 +200,13 @@ extension SetProfileViewController { case .imageBottomSheet: let viewController = owner.selectImageFactory.make() - if let viewController = viewController as? UIViewController { - viewController.rx - .methodInvoked(#selector(UIViewController.viewDidDisappear)) - .take(1) - .map { _ in Reactor.Action.viewWillAppear } - .bind(to: reactor.action) - .disposed(by: owner.disposeBag) + if let selectImageVC = viewController as? SelectImageViewContoller { + selectImageVC.onImageSelected = { url in + reactor.action.onNext(.imageSelected(url)) + } } - owner.presentModal(viewController) + owner.presentModal(viewController, hideTabBar: true) case .dismiss: owner.didReturn.accept(false) owner.navigationController?.popViewController(animated: true) diff --git a/MLS/MLSRecommendationFeature/Package.swift b/MLS/MLSRecommendationFeature/Package.swift index 0878963e..2b894f96 100644 --- a/MLS/MLSRecommendationFeature/Package.swift +++ b/MLS/MLSRecommendationFeature/Package.swift @@ -24,6 +24,8 @@ let package = Package( dependencies: [ .package(path: "../MLSCore"), .package(path: "../MLSDesignSystem"), + .package(path: "../MLSBookmarkFeature"), + .package(path: "../MLSDictionaryFeature"), .package(url: "https://github.com/ReactorKit/ReactorKit.git", from: "3.2.0"), .package(url: "https://github.com/ReactiveX/RxSwift.git", from: "6.7.0"), .package(url: "https://github.com/RxSwiftCommunity/RxKeyboard.git", from: "2.0.0"), @@ -47,6 +49,8 @@ let package = Package( "MLSRecommendationFeatureInterface", .product(name: "MLSCore", package: "MLSCore"), .product(name: "MLSDesignSystem", package: "MLSDesignSystem"), + .product(name: "MLSBookmarkFeatureInterface", package: "MLSBookmarkFeature"), + .product(name: "MLSDictionaryFeatureInterface", package: "MLSDictionaryFeature"), .product(name: "ReactorKit", package: "ReactorKit"), .product(name: "RxSwift", package: "RxSwift"), .product(name: "RxCocoa", package: "RxSwift"), diff --git a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Data/DTOs/JobDTO.swift b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Data/DTOs/JobDTO.swift index 375b28f8..5fb8b4cf 100644 --- a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Data/DTOs/JobDTO.swift +++ b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Data/DTOs/JobDTO.swift @@ -1,10 +1,3 @@ -struct JobResponseDTO: Decodable { - let success: Bool - let code: String? - let message: String? - let data: JobDTO? -} - struct JobDTO: Decodable { let jobId: Int let jobName: String diff --git a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Data/DTOs/RecommendationResponseDTO.swift b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Data/DTOs/RecommendationResponseDTO.swift deleted file mode 100644 index 151a5e74..00000000 --- a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Data/DTOs/RecommendationResponseDTO.swift +++ /dev/null @@ -1,6 +0,0 @@ -struct RecommendationResponseDTO: Decodable { - let success: Bool - let code: String? - let message: String? - let data: [RecommendationMapDTO]? -} diff --git a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Data/DTOs/UserProfileDTO.swift b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Data/DTOs/UserProfileDTO.swift index 2e87b83e..1a280a3c 100644 --- a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Data/DTOs/UserProfileDTO.swift +++ b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Data/DTOs/UserProfileDTO.swift @@ -1,12 +1,5 @@ import MLSRecommendationFeatureInterface -struct UserProfileResponseDTO: Decodable { - let success: Bool - let code: String? - let message: String? - let data: UserProfileDTO? -} - struct UserProfileDTO: Decodable { let id: String? let provider: String? diff --git a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Data/Endpoints/RecommendationEndPoint.swift b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Data/Endpoints/RecommendationEndPoint.swift index db59aa51..7de6070e 100644 --- a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Data/Endpoints/RecommendationEndPoint.swift +++ b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Data/Endpoints/RecommendationEndPoint.swift @@ -3,7 +3,7 @@ import MLSCore enum RecommendationEndPoint { static let base = "https://mapleland.2megabytes.me" - static func fetchRecommendations(level: Int, jobId: Int, limit: Int?) -> ResponsableEndPoint { + static func fetchRecommendations(level: Int, jobId: Int, limit: Int?) -> ResponsableEndPoint<[RecommendationMapDTO]> { .init( baseURL: base, path: "/api/v1/maps/recommendations", @@ -12,7 +12,7 @@ enum RecommendationEndPoint { ) } - static func fetchProfile() -> ResponsableEndPoint { + static func fetchProfile() -> ResponsableEndPoint { .init( baseURL: base, path: "/api/v1/auth/me", @@ -20,13 +20,14 @@ enum RecommendationEndPoint { ) } - static func fetchJob(jobId: Int) -> ResponsableEndPoint { + static func fetchJob(jobId: Int) -> ResponsableEndPoint { .init( baseURL: base, path: "/api/v1/jobs/\(jobId)", method: .GET ) } + } private extension RecommendationEndPoint { diff --git a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Data/Repositories/RecommendationRepositoryImpl.swift b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Data/Repositories/RecommendationRepositoryImpl.swift index 4b5b3c7d..2568b242 100644 --- a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Data/Repositories/RecommendationRepositoryImpl.swift +++ b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Data/Repositories/RecommendationRepositoryImpl.swift @@ -14,18 +14,19 @@ public final class RecommendationRepositoryImpl: RecommendationRepository { public func fetchProfile() -> Observable { let endpoint = RecommendationEndPoint.fetchProfile() return provider.requestData(endPoint: endpoint, interceptor: interceptor) - .compactMap { $0.data?.toDomain() } + .map { $0.toDomain() } } public func fetchJobName(jobId: Int) -> Observable { let endpoint = RecommendationEndPoint.fetchJob(jobId: jobId) return provider.requestData(endPoint: endpoint, interceptor: interceptor) - .compactMap { $0.data?.jobName } + .map { $0.jobName } } public func fetchRecommendations(level: Int, jobId: Int, limit: Int?) -> Observable<[RecommendationMap]> { let endpoint = RecommendationEndPoint.fetchRecommendations(level: level, jobId: jobId, limit: limit) return provider.requestData(endPoint: endpoint, interceptor: interceptor) - .map { $0.data?.toDomain() ?? [] } + .map { $0.toDomain() } } + } diff --git a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainFactoryImpl.swift b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainFactoryImpl.swift index 15ccbaea..60cd147f 100644 --- a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainFactoryImpl.swift +++ b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainFactoryImpl.swift @@ -1,16 +1,48 @@ +import UIKit + +import MLSBookmarkFeatureInterface import MLSCore import MLSRecommendationFeatureInterface public struct RecommendationMainFactoryImpl: RecommendationMainFactory { private let repository: RecommendationRepository + private let bookmarkRepository: BookmarkRepository + private let makeLoginVC: (() -> UIViewController)? + private let makeCharacterSettingVC: (() -> UIViewController)? + private let makeSearchVC: (() -> UIViewController)? + private let makeNotificationVC: (() -> UIViewController)? + private let makeDetailVC: ((_ mapId: Int) -> UIViewController)? + private let makeBookmarkModalVC: ((_ bookmarkIds: [Int], _ onComplete: ((Bool) -> Void)?) -> UIViewController)? - public init(repository: RecommendationRepository) { + public init( + repository: RecommendationRepository, + bookmarkRepository: BookmarkRepository, + makeLoginVC: (() -> UIViewController)? = nil, + makeCharacterSettingVC: (() -> UIViewController)? = nil, + makeSearchVC: (() -> UIViewController)? = nil, + makeNotificationVC: (() -> UIViewController)? = nil, + makeDetailVC: ((_ mapId: Int) -> UIViewController)? = nil, + makeBookmarkModalVC: ((_ bookmarkIds: [Int], _ onComplete: ((Bool) -> Void)?) -> UIViewController)? = nil + ) { self.repository = repository + self.bookmarkRepository = bookmarkRepository + self.makeLoginVC = makeLoginVC + self.makeCharacterSettingVC = makeCharacterSettingVC + self.makeSearchVC = makeSearchVC + self.makeNotificationVC = makeNotificationVC + self.makeDetailVC = makeDetailVC + self.makeBookmarkModalVC = makeBookmarkModalVC } public func make() -> BaseViewController { let vc = RecommendationMainViewController() - vc.reactor = RecommendationMainReactor(repository: repository) + vc.reactor = RecommendationMainReactor(repository: repository, bookmarkRepository: bookmarkRepository) + vc.onLoginTapped = makeLoginVC + vc.onEditTapped = makeCharacterSettingVC + vc.onSearchTapped = makeSearchVC + vc.onNotificationTapped = makeNotificationVC + vc.onDetailTapped = makeDetailVC + vc.onBookmarkModalTapped = makeBookmarkModalVC return vc } } diff --git a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainReactor.swift b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainReactor.swift index 683b3131..b89d2847 100644 --- a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainReactor.swift +++ b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainReactor.swift @@ -1,3 +1,5 @@ +import MLSBookmarkFeatureInterface +import MLSDictionaryFeatureInterface import MLSRecommendationFeatureInterface import ReactorKit @@ -10,6 +12,14 @@ final class RecommendationMainReactor: Reactor { enum Action { case viewWillAppear case informationButtonTapped + case toggleBookmark(mapId: Int) + case undoLastDeletedBookmark + } + + enum UIEvent { + case none + case added(RecommendationMap) + case deleted(RecommendationMap) } enum Mutation { @@ -18,6 +28,11 @@ final class RecommendationMainReactor: Reactor { case setRecommendations([RecommendationMap]) case setLoading(Bool) case informationButtonToggle + case setLogin(Bool) + case updateBookmarkId(mapId: Int, bookmarkId: Int?) + case setLastDeleted(RecommendationMap?) + case setUIEvent(UIEvent) + case setTogglingBookmark(mapId: Int) } struct State { @@ -26,6 +41,10 @@ final class RecommendationMainReactor: Reactor { var recommendations: [RecommendationMap] = [] var isLoading: Bool = false var informationButtonIsOn: Bool = false + var isLogin: Bool = false + var lastDeleted: RecommendationMap? + var togglingBookmarkIds: Set = [] + @Pulse var uiEvent: UIEvent = .none } // MARK: - Properties @@ -33,10 +52,12 @@ final class RecommendationMainReactor: Reactor { var disposeBag = DisposeBag() private let repository: RecommendationRepository + private let bookmarkRepository: BookmarkRepository // MARK: - Init - init(repository: RecommendationRepository) { + init(repository: RecommendationRepository, bookmarkRepository: BookmarkRepository) { self.repository = repository + self.bookmarkRepository = bookmarkRepository self.initialState = State() } @@ -47,12 +68,16 @@ final class RecommendationMainReactor: Reactor { 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 { _ in .empty() } + .catch { error in + print("⚠️ [Recommendation] fetchJobName 실패: \(error)") + return .empty() + } } else { setJobName = .empty() } @@ -60,14 +85,20 @@ final class RecommendationMainReactor: Reactor { 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 { _ in .empty() } + .catch { error in + print("⚠️ [Recommendation] fetchRecommendations 실패: \(error)") + return .empty() + } } else { setRecommendations = .empty() } let parallelRequests = Observable.merge([setJobName, setRecommendations]) - return Observable.concat([setProfile, parallelRequests]) + return Observable.concat([setLogin, setProfile, parallelRequests]) + } + .catch { error in + print("⚠️ [Recommendation] fetchProfile 실패: \(error)") + return Observable.just(.setLogin(false)) } - .catch { _ in .empty() } return Observable.concat([ .just(.setLoading(true)), @@ -77,6 +108,12 @@ final class RecommendationMainReactor: Reactor { case .informationButtonTapped: return .just(.informationButtonToggle) + + case let .toggleBookmark(mapId): + return handleToggleBookmark(mapId: mapId) + + case .undoLastDeletedBookmark: + return handleUndoLastDeleted() } } @@ -94,7 +131,87 @@ final class RecommendationMainReactor: Reactor { newState.isLoading = isLoading case .informationButtonToggle: newState.informationButtonIsOn.toggle() + case .setLogin(let isLogin): + newState.isLogin = isLogin + case let .updateBookmarkId(mapId, bookmarkId): + newState.togglingBookmarkIds.remove(mapId) + if let index = newState.recommendations.firstIndex(where: { $0.mapId == mapId }) { + let old = newState.recommendations[index] + newState.recommendations[index] = RecommendationMap( + mapId: old.mapId, + score: old.score, + iconUrl: old.iconUrl, + nameKr: old.nameKr, + bookmarkId: bookmarkId + ) + } + case let .setLastDeleted(map): + newState.lastDeleted = map + case let .setUIEvent(event): + newState.uiEvent = event + case let .setTogglingBookmark(mapId): + newState.togglingBookmarkIds.insert(mapId) } return newState } } + +// MARK: - Methods +private extension RecommendationMainReactor { + func handleToggleBookmark(mapId: Int) -> Observable { + guard !currentState.togglingBookmarkIds.contains(mapId), + let map = currentState.recommendations.first(where: { $0.mapId == mapId }) else { + return .empty() + } + + if let bookmarkId = map.bookmarkId { + return Observable.concat([ + .just(.setTogglingBookmark(mapId: mapId)), + bookmarkRepository.deleteBookmark(bookmarkId: bookmarkId) + .flatMap { _ -> Observable in + .from([ + .setLastDeleted(map), + .updateBookmarkId(mapId: mapId, bookmarkId: nil), + .setUIEvent(.deleted(map)) + ]) + } + .catch { _ in .just(.updateBookmarkId(mapId: mapId, bookmarkId: bookmarkId)) } + ]) + } else { + return Observable.concat([ + .just(.setTogglingBookmark(mapId: mapId)), + bookmarkRepository.setBookmark(resourceId: mapId, type: .map) + .flatMap { newBookmarkId -> Observable in + let updated = RecommendationMap( + mapId: map.mapId, score: map.score, + iconUrl: map.iconUrl, nameKr: map.nameKr, + bookmarkId: newBookmarkId + ) + return .from([ + .updateBookmarkId(mapId: mapId, bookmarkId: newBookmarkId), + .setUIEvent(.added(updated)) + ]) + } + .catch { _ in .just(.updateBookmarkId(mapId: mapId, bookmarkId: nil)) } + ]) + } + } + + func handleUndoLastDeleted() -> Observable { + guard let last = currentState.lastDeleted else { return .empty() } + return bookmarkRepository.setBookmark(resourceId: last.mapId, type: .map) + .flatMap { newBookmarkId -> Observable in + let restored = RecommendationMap( + mapId: last.mapId, score: last.score, + iconUrl: last.iconUrl, nameKr: last.nameKr, + bookmarkId: newBookmarkId + ) + return .from([ + .setLastDeleted(nil), + .updateBookmarkId(mapId: last.mapId, bookmarkId: newBookmarkId), + .setUIEvent(.added(restored)) + ]) + } + .catch { _ in .empty() } + } +} diff --git a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainViewController.swift b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainViewController.swift index 5046ef77..f66b5773 100644 --- a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainViewController.swift +++ b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainViewController.swift @@ -15,6 +15,12 @@ final class RecommendationMainViewController: BaseViewController, View { // MARK: - Properties var disposeBag = DisposeBag() + var onLoginTapped: (() -> UIViewController?)? + var onEditTapped: (() -> UIViewController?)? + var onSearchTapped: (() -> UIViewController?)? + var onNotificationTapped: (() -> UIViewController?)? + var onDetailTapped: ((_ mapId: Int) -> UIViewController?)? + var onBookmarkModalTapped: ((_ bookmarkIds: [Int], _ onComplete: ((Bool) -> Void)?) -> UIViewController)? private var mainView = RecommendationMainView() } @@ -54,6 +60,7 @@ extension RecommendationMainViewController { func bind(reactor: Reactor) { bindUserActions(reactor: reactor) bindViewState(reactor: reactor) + bindUIEvents(reactor: reactor) } func bindUserActions(reactor: Reactor) { @@ -62,10 +69,42 @@ extension RecommendationMainViewController { .bind(to: reactor.action) .disposed(by: disposeBag) + mainView.header.firstIconButton.rx.tap + .withUnretained(self) + .subscribe { owner, _ in + guard let vc = owner.onSearchTapped?() else { return } + owner.navigationController?.pushViewController(vc, animated: true) + } + .disposed(by: disposeBag) + + mainView.header.secondIconButton.rx.tap + .withUnretained(self) + .subscribe { owner, _ in + guard let vc = owner.onNotificationTapped?() else { return } + owner.navigationController?.pushViewController(vc, animated: true) + } + .disposed(by: disposeBag) + rx.viewWillAppear .map { Reactor.Action.viewWillAppear } .bind(to: reactor.action) .disposed(by: disposeBag) + + mainView.emptyView.button.rx.tap + .withUnretained(self) + .subscribe { owner, _ in + guard let vc = owner.onLoginTapped?() else { return } + owner.navigationController?.pushViewController(vc, animated: true) + } + .disposed(by: disposeBag) + + mainView.profileView.editButton.rx.tap + .withUnretained(self) + .subscribe { owner, _ in + guard let vc = owner.onEditTapped?() else { return } + owner.navigationController?.pushViewController(vc, animated: true) + } + .disposed(by: disposeBag) } func bindProfile(reactor: Reactor) { @@ -91,6 +130,25 @@ extension RecommendationMainViewController { .disposed(by: disposeBag) } + func bindUIEvents(reactor: Reactor) { + rx.viewDidAppear + .take(1) + .flatMapLatest { _ in reactor.pulse(\.$uiEvent) } + .observe(on: MainScheduler.instance) + .withUnretained(self) + .subscribe(onNext: { owner, event in + switch event { + case .added(let map): + owner.presentAddSnackBar(map: map) + case .deleted(let map): + owner.presentDeleteSnackBar(map: map) + case .none: + break + } + }) + .disposed(by: disposeBag) + } + func bindViewState(reactor: Reactor) { reactor.state .observe(on: MainScheduler.instance) @@ -110,12 +168,22 @@ extension RecommendationMainViewController { } .disposed(by: disposeBag) + reactor.state + .observe(on: MainScheduler.instance) + .map { $0.isLogin } + .distinctUntilChanged() + .withUnretained(self) + .subscribe { owner, isLogin in + owner.mainView.updateLoginState(isLogin: isLogin) + } + .disposed(by: disposeBag) + bindProfile(reactor: reactor) reactor.state .observe(on: MainScheduler.instance) .map { $0.recommendations } - .distinctUntilChanged { $0.map(\.mapId) == $1.map(\.mapId) } + .distinctUntilChanged { $0.map { ($0.mapId, $0.bookmarkId) }.elementsEqual($1.map { ($0.mapId, $0.bookmarkId) }) { $0 == $1 } } .withUnretained(self) .subscribe { owner, _ in owner.mainView.collectionView.reloadData() @@ -124,8 +192,66 @@ extension RecommendationMainViewController { } } +// MARK: - SnackBar +private extension RecommendationMainViewController { + func presentAddSnackBar(map: RecommendationMap) { + ImageLoader.shared.loadImage(stringURL: map.iconUrl) { [weak self] image in + guard let self, let image else { return } + SnackBarFactory.createSnackBar( + type: .normal, + image: image, + imageBackgroundColor: .clear, + text: "사냥터를 북마크에 추가했어요.", + buttonText: "컬렉션 추가", + buttonAction: { [weak self] in + guard let self, + let bookmarkId = self.reactor?.currentState.recommendations + .first(where: { $0.mapId == map.mapId })?.bookmarkId else { return } + let vc = self.onBookmarkModalTapped?([bookmarkId]) { isAdd in + if isAdd { + ToastFactory.createToast(message: "컬렉션에 추가되었어요. 북마크 탭에서 확인 할 수 있어요.") + } + } + guard let vc else { return } + vc.modalPresentationStyle = .pageSheet + if let sheet = vc.sheetPresentationController { + sheet.detents = [.medium(), .large()] + sheet.prefersGrabberVisible = true + sheet.preferredCornerRadius = 16 + } + self.present(vc, animated: true) + } + ) + } + } + + func presentDeleteSnackBar(map: RecommendationMap) { + ImageLoader.shared.loadImage(stringURL: map.iconUrl) { [weak self] image in + guard let self, let image else { return } + SnackBarFactory.createSnackBar( + type: .delete, + image: image, + imageBackgroundColor: .clear, + text: "사냥터를 북마크에서 삭제했어요.", + buttonText: "되돌리기", + buttonAction: { [weak self] in + self?.reactor?.action.onNext(.undoLastDeletedBookmark) + } + ) + } + } +} + // MARK: - UICollectionViewDelegate, UICollectionViewDataSource extension RecommendationMainViewController: UICollectionViewDelegate, UICollectionViewDataSource { + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + guard let recommendations = reactor?.currentState.recommendations, + indexPath.item < recommendations.count else { return } + let map = recommendations[indexPath.item] + guard let vc = onDetailTapped?(map.mapId) else { return } + navigationController?.pushViewController(vc, animated: true) + } + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return reactor?.currentState.recommendations.count ?? 0 } @@ -141,6 +267,9 @@ extension RecommendationMainViewController: UICollectionViewDelegate, UICollecti cell.cardView.setMainText(text: map.nameKr) cell.cardView.setType(type: .recommended(rank: indexPath.row + 1)) cell.cardView.isIconSelected = map.isBookmarked + cell.cardView.onIconTapped = { [weak self] in + self?.reactor?.action.onNext(.toggleBookmark(mapId: map.mapId)) + } ImageLoader.shared.loadImage(stringURL: map.iconUrl) { [weak self] image in guard let self, let image else { return } diff --git a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/Views/CardListCell.swift b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/Views/CardListCell.swift index f3364133..029eb6c1 100644 --- a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/Views/CardListCell.swift +++ b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/Views/CardListCell.swift @@ -20,6 +20,11 @@ final class CardListCell: UICollectionViewCell { required init?(coder: NSCoder) { fatalError("\(#file), \(#function) Error") } + + override func prepareForReuse() { + super.prepareForReuse() + cardView.onIconTapped = nil + } } // MARK: - SetUp diff --git a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/Views/RecommendationMainView.swift b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/Views/RecommendationMainView.swift index d9f881b9..c1649ab7 100644 --- a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/Views/RecommendationMainView.swift +++ b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/Views/RecommendationMainView.swift @@ -61,6 +61,8 @@ internal final class RecommendationMainView: UIView { return view }() + internal let emptyView = ToLoginView(type: .recommend) + // MARK: - init init() { super.init(frame: .zero) @@ -85,6 +87,7 @@ private extension RecommendationMainView { informationButton.addSubview(informationLabel) informationButton.addSubview(informationIconView) grayBackgroundView.addSubview(collectionView) + addSubview(emptyView) } func setupConstraints() { @@ -123,5 +126,21 @@ private extension RecommendationMainView { make.horizontalEdges.equalToSuperview().inset(Constant.collectionViewHorizontalInset) make.bottom.equalToSuperview() } + + emptyView.snp.makeConstraints { make in + make.top.equalTo(header.snp.bottom) + make.horizontalEdges.equalToSuperview() + make.bottom.equalToSuperview().inset(Constant.bottomTabHeight) + } + emptyView.isHidden = true + } +} + +// MARK: - Login State +extension RecommendationMainView { + func updateLoginState(isLogin: Bool) { + profileView.isHidden = !isLogin + grayBackgroundView.isHidden = !isLogin + emptyView.isHidden = isLogin } } diff --git a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeatureTesting/FailingMockRecommendationRepository.swift b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeatureTesting/FailingMockRecommendationRepository.swift index 01b83327..5046718a 100644 --- a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeatureTesting/FailingMockRecommendationRepository.swift +++ b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeatureTesting/FailingMockRecommendationRepository.swift @@ -18,6 +18,7 @@ public final class FailingMockRecommendationRepository: RecommendationRepository public func fetchRecommendations(level: Int, jobId: Int, limit: Int?) -> Observable<[RecommendationMap]> { return .error(RecommendationRepositoryError.fetchFailed) } + } public enum RecommendationRepositoryError: Error { diff --git a/MLS/MLSRecommendationFeatureExample/SceneDelegate.swift b/MLS/MLSRecommendationFeatureExample/SceneDelegate.swift index 3b80d692..1fbe974e 100644 --- a/MLS/MLSRecommendationFeatureExample/SceneDelegate.swift +++ b/MLS/MLSRecommendationFeatureExample/SceneDelegate.swift @@ -1,5 +1,6 @@ import UIKit +import MLSBookmarkFeatureTesting import MLSCore import MLSRecommendationFeature import MLSRecommendationFeatureInterface @@ -30,7 +31,38 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { private func registerDependencies() { DIContainer.register(type: RecommendationMainFactory.self) { RecommendationMainFactoryImpl( - repository: MockRecommendationRepository() + repository: MockRecommendationRepository(), + bookmarkRepository: MockBookmarkRepository(), + makeLoginVC: { + let vc = UIViewController() + vc.view.backgroundColor = .white + return vc + }, + makeCharacterSettingVC: { + let vc = UIViewController() + vc.view.backgroundColor = .white + return vc + }, + makeSearchVC: { + let vc = UIViewController() + vc.view.backgroundColor = .white + return vc + }, + makeNotificationVC: { + let vc = UIViewController() + vc.view.backgroundColor = .white + return vc + }, + makeDetailVC: { _ in + let vc = UIViewController() + vc.view.backgroundColor = .white + return vc + }, + makeBookmarkModalVC: { _, _ in + let vc = UIViewController() + vc.view.backgroundColor = .white + return vc + } ) } }