From 39362eda950e4e206184b0935c75bb02defd9cce Mon Sep 17 00:00:00 2001 From: JunYoung Date: Wed, 10 Jun 2026 01:27:29 +0900 Subject: [PATCH 01/19] =?UTF-8?q?fix/#339:=20AppCoordinator=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EB=B6=88=EC=9D=BC=EC=B9=98=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?(AppCoordinator=20->=20AppCoordinatorProtocol)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/MLSAppFeature/Dependency/FactoryAssembly.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MLS/MLSAppFeature/Sources/MLSAppFeature/Dependency/FactoryAssembly.swift b/MLS/MLSAppFeature/Sources/MLSAppFeature/Dependency/FactoryAssembly.swift index 8f1ef38e..9f5e7b27 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 From 66e674c472f8200eb2d83fe8f6e8ce715ffd9c7a Mon Sep 17 00:00:00 2001 From: JunYoung Date: Wed, 10 Jun 2026 01:44:37 +0900 Subject: [PATCH 02/19] =?UTF-8?q?fix/#339:=20=EC=B6=94=EC=B2=9C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20DTO=20=EC=9D=B4=EC=A4=91=20=EB=9E=98?= =?UTF-8?q?=ED=95=91=20=EC=A0=9C=EA=B1=B0=20=EB=B0=8F=20=EC=97=90=EB=9F=AC?= =?UTF-8?q?=20=EB=A1=9C=EA=B9=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MLS/MLS/Application/ViewController.swift | 13 ------------- .../Data/DTOs/JobDTO.swift | 7 ------- .../Data/DTOs/RecommendationResponseDTO.swift | 6 ------ .../Data/DTOs/UserProfileDTO.swift | 7 ------- .../Data/Endpoints/RecommendationEndPoint.swift | 6 +++--- .../RecommendationRepositoryImpl.swift | 6 +++--- .../RecommendationMainReactor.swift | 15 ++++++++++++--- 7 files changed, 18 insertions(+), 42 deletions(-) delete mode 100644 MLS/MLS/Application/ViewController.swift delete mode 100644 MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Data/DTOs/RecommendationResponseDTO.swift 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/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..fdf3c617 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,7 +20,7 @@ enum RecommendationEndPoint { ) } - static func fetchJob(jobId: Int) -> ResponsableEndPoint { + static func fetchJob(jobId: Int) -> ResponsableEndPoint { .init( baseURL: base, path: "/api/v1/jobs/\(jobId)", diff --git a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Data/Repositories/RecommendationRepositoryImpl.swift b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Data/Repositories/RecommendationRepositoryImpl.swift index 4b5b3c7d..b4dde7b3 100644 --- a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Data/Repositories/RecommendationRepositoryImpl.swift +++ b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Data/Repositories/RecommendationRepositoryImpl.swift @@ -14,18 +14,18 @@ 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/RecommendationMainReactor.swift b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainReactor.swift index 683b3131..e46b6803 100644 --- a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainReactor.swift +++ b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainReactor.swift @@ -52,7 +52,10 @@ final class RecommendationMainReactor: Reactor { 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 +63,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]) } - .catch { _ in .empty() } + .catch { error in + print("⚠️ [Recommendation] fetchProfile 실패: \(error)") + return .empty() + } return Observable.concat([ .just(.setLoading(true)), From c213889f08c8528941ecf887fcdca9626e70df81 Mon Sep 17 00:00:00 2001 From: JunYoung Date: Wed, 10 Jun 2026 01:57:00 +0900 Subject: [PATCH 03/19] =?UTF-8?q?feat/#339:=20=EC=B6=94=EC=B2=9C=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EB=B9=84=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EB=B7=B0=20=EB=B0=8F=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=ED=95=98=EA=B8=B0=20=EB=84=A4=EB=B9=84=EA=B2=8C=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Dependency/FactoryAssembly.swift | 10 +++++-- .../RecommendationMainFactoryImpl.swift | 14 +++++++++- .../RecommendationMainReactor.swift | 9 ++++-- .../RecommendationMainViewController.swift | 28 +++++++++++++++++++ .../Views/RecommendationMainView.swift | 19 +++++++++++++ .../SceneDelegate.swift | 12 +++++++- 6 files changed, 85 insertions(+), 7 deletions(-) diff --git a/MLS/MLSAppFeature/Sources/MLSAppFeature/Dependency/FactoryAssembly.swift b/MLS/MLSAppFeature/Sources/MLSAppFeature/Dependency/FactoryAssembly.swift index 9f5e7b27..ee0690cc 100644 --- a/MLS/MLSAppFeature/Sources/MLSAppFeature/Dependency/FactoryAssembly.swift +++ b/MLS/MLSAppFeature/Sources/MLSAppFeature/Dependency/FactoryAssembly.swift @@ -432,9 +432,13 @@ public enum FactoryAssembly { } DIContainer.register(type: RecommendationMainFactory.self) { RecommendationMainFactoryImpl( - repository: DIContainer.resolve( - type: RecommendationRepository.self - ) + repository: DIContainer.resolve(type: RecommendationRepository.self), + makeLoginVC: { + DIContainer.resolve(type: LoginFactory.self).make(exitRoute: .pop) + }, + makeCharacterSettingVC: { + DIContainer.resolve(type: SetCharacterFactory.self).make() + } ) } } diff --git a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainFactoryImpl.swift b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainFactoryImpl.swift index 15ccbaea..2ace0c9e 100644 --- a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainFactoryImpl.swift +++ b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainFactoryImpl.swift @@ -1,16 +1,28 @@ +import UIKit + import MLSCore import MLSRecommendationFeatureInterface public struct RecommendationMainFactoryImpl: RecommendationMainFactory { private let repository: RecommendationRepository + private let makeLoginVC: (() -> UIViewController)? + private let makeCharacterSettingVC: (() -> UIViewController)? - public init(repository: RecommendationRepository) { + public init( + repository: RecommendationRepository, + makeLoginVC: (() -> UIViewController)? = nil, + makeCharacterSettingVC: (() -> UIViewController)? = nil + ) { self.repository = repository + self.makeLoginVC = makeLoginVC + self.makeCharacterSettingVC = makeCharacterSettingVC } public func make() -> BaseViewController { let vc = RecommendationMainViewController() vc.reactor = RecommendationMainReactor(repository: repository) + vc.onLoginTapped = makeLoginVC + vc.onEditTapped = makeCharacterSettingVC return vc } } diff --git a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainReactor.swift b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainReactor.swift index e46b6803..bf1dbb5e 100644 --- a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainReactor.swift +++ b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainReactor.swift @@ -18,6 +18,7 @@ final class RecommendationMainReactor: Reactor { case setRecommendations([RecommendationMap]) case setLoading(Bool) case informationButtonToggle + case setLogin(Bool) } struct State { @@ -26,6 +27,7 @@ final class RecommendationMainReactor: Reactor { var recommendations: [RecommendationMap] = [] var isLoading: Bool = false var informationButtonIsOn: Bool = false + var isLogin: Bool = false } // MARK: - Properties @@ -47,6 +49,7 @@ 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 { @@ -71,11 +74,11 @@ final class RecommendationMainReactor: Reactor { 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 .empty() + return Observable.just(.setLogin(false)) } return Observable.concat([ @@ -103,6 +106,8 @@ final class RecommendationMainReactor: Reactor { newState.isLoading = isLoading case .informationButtonToggle: newState.informationButtonIsOn.toggle() + case .setLogin(let isLogin): + newState.isLogin = isLogin } return newState } diff --git a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainViewController.swift b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainViewController.swift index 5046ef77..9dd82b46 100644 --- a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainViewController.swift +++ b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainViewController.swift @@ -15,6 +15,8 @@ final class RecommendationMainViewController: BaseViewController, View { // MARK: - Properties var disposeBag = DisposeBag() + var onLoginTapped: (() -> UIViewController?)? + var onEditTapped: (() -> UIViewController?)? private var mainView = RecommendationMainView() } @@ -66,6 +68,22 @@ extension RecommendationMainViewController { .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) { @@ -110,6 +128,16 @@ 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 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/MLSRecommendationFeatureExample/SceneDelegate.swift b/MLS/MLSRecommendationFeatureExample/SceneDelegate.swift index 3b80d692..043d6703 100644 --- a/MLS/MLSRecommendationFeatureExample/SceneDelegate.swift +++ b/MLS/MLSRecommendationFeatureExample/SceneDelegate.swift @@ -30,7 +30,17 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { private func registerDependencies() { DIContainer.register(type: RecommendationMainFactory.self) { RecommendationMainFactoryImpl( - repository: MockRecommendationRepository() + repository: MockRecommendationRepository(), + makeLoginVC: { + let vc = UIViewController() + vc.view.backgroundColor = .white + return vc + }, + makeCharacterSettingVC: { + let vc = UIViewController() + vc.view.backgroundColor = .white + return vc + } ) } } From 3a0f3378f7fbbf73ebab807e19d0565d2e0fb702 Mon Sep 17 00:00:00 2001 From: JunYoung Date: Wed, 10 Jun 2026 02:01:53 +0900 Subject: [PATCH 04/19] =?UTF-8?q?feat/#339:=20=EC=B6=94=EC=B2=9C=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=ED=97=A4=EB=8D=94=20=EA=B2=80=EC=83=89/?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=EB=B2=84=ED=8A=BC=20=EB=84=A4=EB=B9=84?= =?UTF-8?q?=EA=B2=8C=EC=9D=B4=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Dependency/FactoryAssembly.swift | 6 ++++++ .../RecommendationMainFactoryImpl.swift | 10 +++++++++- .../RecommendationMainViewController.swift | 18 ++++++++++++++++++ .../SceneDelegate.swift | 10 ++++++++++ 4 files changed, 43 insertions(+), 1 deletion(-) diff --git a/MLS/MLSAppFeature/Sources/MLSAppFeature/Dependency/FactoryAssembly.swift b/MLS/MLSAppFeature/Sources/MLSAppFeature/Dependency/FactoryAssembly.swift index ee0690cc..a89ff878 100644 --- a/MLS/MLSAppFeature/Sources/MLSAppFeature/Dependency/FactoryAssembly.swift +++ b/MLS/MLSAppFeature/Sources/MLSAppFeature/Dependency/FactoryAssembly.swift @@ -438,6 +438,12 @@ public enum FactoryAssembly { }, makeCharacterSettingVC: { DIContainer.resolve(type: SetCharacterFactory.self).make() + }, + makeSearchVC: { + DIContainer.resolve(type: DictionarySearchFactory.self).make() + }, + makeNotificationVC: { + DIContainer.resolve(type: DictionaryNotificationFactory.self).make() } ) } diff --git a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainFactoryImpl.swift b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainFactoryImpl.swift index 2ace0c9e..f2016126 100644 --- a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainFactoryImpl.swift +++ b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainFactoryImpl.swift @@ -7,15 +7,21 @@ public struct RecommendationMainFactoryImpl: RecommendationMainFactory { private let repository: RecommendationRepository private let makeLoginVC: (() -> UIViewController)? private let makeCharacterSettingVC: (() -> UIViewController)? + private let makeSearchVC: (() -> UIViewController)? + private let makeNotificationVC: (() -> UIViewController)? public init( repository: RecommendationRepository, makeLoginVC: (() -> UIViewController)? = nil, - makeCharacterSettingVC: (() -> UIViewController)? = nil + makeCharacterSettingVC: (() -> UIViewController)? = nil, + makeSearchVC: (() -> UIViewController)? = nil, + makeNotificationVC: (() -> UIViewController)? = nil ) { self.repository = repository self.makeLoginVC = makeLoginVC self.makeCharacterSettingVC = makeCharacterSettingVC + self.makeSearchVC = makeSearchVC + self.makeNotificationVC = makeNotificationVC } public func make() -> BaseViewController { @@ -23,6 +29,8 @@ public struct RecommendationMainFactoryImpl: RecommendationMainFactory { vc.reactor = RecommendationMainReactor(repository: repository) vc.onLoginTapped = makeLoginVC vc.onEditTapped = makeCharacterSettingVC + vc.onSearchTapped = makeSearchVC + vc.onNotificationTapped = makeNotificationVC return vc } } diff --git a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainViewController.swift b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainViewController.swift index 9dd82b46..ad38f219 100644 --- a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainViewController.swift +++ b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainViewController.swift @@ -17,6 +17,8 @@ final class RecommendationMainViewController: BaseViewController, View { var disposeBag = DisposeBag() var onLoginTapped: (() -> UIViewController?)? var onEditTapped: (() -> UIViewController?)? + var onSearchTapped: (() -> UIViewController?)? + var onNotificationTapped: (() -> UIViewController?)? private var mainView = RecommendationMainView() } @@ -64,6 +66,22 @@ 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) diff --git a/MLS/MLSRecommendationFeatureExample/SceneDelegate.swift b/MLS/MLSRecommendationFeatureExample/SceneDelegate.swift index 043d6703..d47921e3 100644 --- a/MLS/MLSRecommendationFeatureExample/SceneDelegate.swift +++ b/MLS/MLSRecommendationFeatureExample/SceneDelegate.swift @@ -40,6 +40,16 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { 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 } ) } From 5bd524cd5665bafeda7762c5739804cb03bee839 Mon Sep 17 00:00:00 2001 From: JunYoung Date: Wed, 10 Jun 2026 12:59:02 +0900 Subject: [PATCH 05/19] =?UTF-8?q?refactor/#339:=20LoginExitRoute=EB=A5=BC?= =?UTF-8?q?=20MLSAppFeatureInterface=EC=97=90=EC=84=9C=20MLSCore=EB=A1=9C?= =?UTF-8?q?=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 피처 예시 앱이 앱 레이어(MLSAppFeatureInterface)를 의존하는 문제를 해결. LoginExitRoute를 MLSCore로 이동하고 관련 타깃 의존성을 정리. --- MLS/MLS.xcodeproj/project.pbxproj | 7 +++++++ MLS/MLSAppFeature/Package.swift | 4 +++- .../Domain/Navigation/AppCoordinatorProtocol.swift | 1 + MLS/MLSAuthFeature/Package.swift | 1 - .../MLSAuthFeatureInterface/Factories/LoginFactory.swift | 1 - .../Sources/MLSCore/Utils}/LoginExitRoute.swift | 2 -- MLS/MLSDictionaryFeature/Package.swift | 1 + .../Mock/MockAppCoordinator.swift | 1 + MLS/MLSMyPageFeature/Package.swift | 3 +++ 9 files changed, 16 insertions(+), 5 deletions(-) rename MLS/{MLSAppFeature/Sources/MLSAppFeatureInterface/Domain/Navigation => MLSCore/Sources/MLSCore/Utils}/LoginExitRoute.swift (80%) 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/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/MLSAppFeatureInterface/Domain/Navigation/AppCoordinatorProtocol.swift b/MLS/MLSAppFeature/Sources/MLSAppFeatureInterface/Domain/Navigation/AppCoordinatorProtocol.swift index 94bf5903..f28f119d 100644 --- a/MLS/MLSAppFeature/Sources/MLSAppFeatureInterface/Domain/Navigation/AppCoordinatorProtocol.swift +++ b/MLS/MLSAppFeature/Sources/MLSAppFeatureInterface/Domain/Navigation/AppCoordinatorProtocol.swift @@ -1,3 +1,4 @@ +import MLSCore import UIKit @MainActor 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/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/MLSDictionaryFeatureTesting/Mock/MockAppCoordinator.swift b/MLS/MLSDictionaryFeature/Sources/MLSDictionaryFeatureTesting/Mock/MockAppCoordinator.swift index 105b5447..20ae084a 100644 --- a/MLS/MLSDictionaryFeature/Sources/MLSDictionaryFeatureTesting/Mock/MockAppCoordinator.swift +++ b/MLS/MLSDictionaryFeature/Sources/MLSDictionaryFeatureTesting/Mock/MockAppCoordinator.swift @@ -2,6 +2,7 @@ import UIKit import MLSAppFeatureInterface import MLSAuthFeatureInterface +import MLSCore public final class MockAppCoordinator: AppCoordinatorProtocol { public var window: UIWindow? 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)] From 620125f2f729d939b2aed92bebc70a2d329caba2 Mon Sep 17 00:00:00 2001 From: JunYoung Date: Wed, 10 Jun 2026 12:59:21 +0900 Subject: [PATCH 06/19] =?UTF-8?q?feat/#339:=20=EC=B6=94=EC=B2=9C=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EB=B6=81=EB=A7=88=ED=81=AC=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - BookmarkRepository(MLSBookmarkFeatureInterface)를 사용해 북마크 추가/삭제 처리 - 북마크 토글 후 셀 UI 즉시 반영 (distinctUntilChanged mapId+bookmarkId 비교) - 북마크 추가/삭제 시 스낵바 표시, 컬렉션 추가 및 되돌리기 지원 - 셀 탭 시 상세 화면(DictionaryDetail)으로 이동 - 예시 앱에 Mock 의존성 및 클로저 주입 추가 --- .../Dependency/FactoryAssembly.swift | 11 +++ MLS/MLSRecommendationFeature/Package.swift | 4 + .../Endpoints/RecommendationEndPoint.swift | 1 + .../RecommendationRepositoryImpl.swift | 1 + .../RecommendationMainFactoryImpl.swift | 16 +++- .../RecommendationMainReactor.swift | 93 ++++++++++++++++++- .../RecommendationMainViewController.swift | 85 ++++++++++++++++- .../Views/CardListCell.swift | 5 + .../FailingMockRecommendationRepository.swift | 1 + .../SceneDelegate.swift | 12 +++ 10 files changed, 225 insertions(+), 4 deletions(-) diff --git a/MLS/MLSAppFeature/Sources/MLSAppFeature/Dependency/FactoryAssembly.swift b/MLS/MLSAppFeature/Sources/MLSAppFeature/Dependency/FactoryAssembly.swift index a89ff878..29625e9e 100644 --- a/MLS/MLSAppFeature/Sources/MLSAppFeature/Dependency/FactoryAssembly.swift +++ b/MLS/MLSAppFeature/Sources/MLSAppFeature/Dependency/FactoryAssembly.swift @@ -433,6 +433,7 @@ public enum FactoryAssembly { DIContainer.register(type: RecommendationMainFactory.self) { RecommendationMainFactoryImpl( repository: DIContainer.resolve(type: RecommendationRepository.self), + bookmarkRepository: DIContainer.resolve(type: BookmarkRepository.self), makeLoginVC: { DIContainer.resolve(type: LoginFactory.self).make(exitRoute: .pop) }, @@ -444,6 +445,16 @@ public enum FactoryAssembly { }, 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/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/Endpoints/RecommendationEndPoint.swift b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Data/Endpoints/RecommendationEndPoint.swift index fdf3c617..7de6070e 100644 --- a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Data/Endpoints/RecommendationEndPoint.swift +++ b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Data/Endpoints/RecommendationEndPoint.swift @@ -27,6 +27,7 @@ enum RecommendationEndPoint { 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 b4dde7b3..2568b242 100644 --- a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Data/Repositories/RecommendationRepositoryImpl.swift +++ b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Data/Repositories/RecommendationRepositoryImpl.swift @@ -28,4 +28,5 @@ public final class RecommendationRepositoryImpl: RecommendationRepository { return provider.requestData(endPoint: endpoint, interceptor: interceptor) .map { $0.toDomain() } } + } diff --git a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainFactoryImpl.swift b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainFactoryImpl.swift index f2016126..60cd147f 100644 --- a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainFactoryImpl.swift +++ b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainFactoryImpl.swift @@ -1,36 +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, + bookmarkRepository: BookmarkRepository, makeLoginVC: (() -> UIViewController)? = nil, makeCharacterSettingVC: (() -> UIViewController)? = nil, makeSearchVC: (() -> UIViewController)? = nil, - makeNotificationVC: (() -> 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 bf1dbb5e..d69d5a90 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 { @@ -19,6 +29,9 @@ final class RecommendationMainReactor: Reactor { case setLoading(Bool) case informationButtonToggle case setLogin(Bool) + case updateBookmarkId(mapId: Int, bookmarkId: Int?) + case setLastDeleted(RecommendationMap?) + case setUIEvent(UIEvent) } struct State { @@ -28,6 +41,8 @@ final class RecommendationMainReactor: Reactor { var isLoading: Bool = false var informationButtonIsOn: Bool = false var isLogin: Bool = false + var lastDeleted: RecommendationMap? + @Pulse var uiEvent: UIEvent = .none } // MARK: - Properties @@ -35,10 +50,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() } @@ -89,6 +106,12 @@ final class RecommendationMainReactor: Reactor { case .informationButtonTapped: return .just(.informationButtonToggle) + + case let .toggleBookmark(mapId): + return handleToggleBookmark(mapId: mapId) + + case .undoLastDeletedBookmark: + return handleUndoLastDeleted() } } @@ -108,7 +131,75 @@ final class RecommendationMainReactor: Reactor { newState.informationButtonIsOn.toggle() case .setLogin(let isLogin): newState.isLogin = isLogin + case let .updateBookmarkId(mapId, bookmarkId): + 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 } return newState } } + +// MARK: - Methods +private extension RecommendationMainReactor { + func handleToggleBookmark(mapId: Int) -> Observable { + guard let map = currentState.recommendations.first(where: { $0.mapId == mapId }) else { + return .empty() + } + + if let bookmarkId = map.bookmarkId { + return bookmarkRepository.deleteBookmark(bookmarkId: bookmarkId) + .flatMap { _ -> Observable in + .from([ + .setLastDeleted(map), + .updateBookmarkId(mapId: mapId, bookmarkId: nil), + .setUIEvent(.deleted(map)) + ]) + } + .catch { _ in .empty() } + } else { + return 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 .empty() } + } + } + + 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 ad38f219..f66b5773 100644 --- a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainViewController.swift +++ b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainViewController.swift @@ -19,6 +19,8 @@ final class RecommendationMainViewController: BaseViewController, View { 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() } @@ -58,6 +60,7 @@ extension RecommendationMainViewController { func bind(reactor: Reactor) { bindUserActions(reactor: reactor) bindViewState(reactor: reactor) + bindUIEvents(reactor: reactor) } func bindUserActions(reactor: Reactor) { @@ -127,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) @@ -161,7 +183,7 @@ extension RecommendationMainViewController { 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() @@ -170,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 } @@ -187,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/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 d47921e3..7f9f6c2e 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 @@ -31,6 +32,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { DIContainer.register(type: RecommendationMainFactory.self) { RecommendationMainFactoryImpl( repository: MockRecommendationRepository(), + bookmarkRepository: MockBookmarkRepository(), makeLoginVC: { let vc = UIViewController() vc.view.backgroundColor = .white @@ -50,6 +52,16 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { let vc = UIViewController() vc.view.backgroundColor = .white return vc + }, + makeDetailVC: { mapId in + let vc = UIViewController() + vc.view.backgroundColor = .white + return vc + }, + makeBookmarkModalVC: { _, _ in + let vc = UIViewController() + vc.view.backgroundColor = .white + return vc } ) } From 2404af2fb82d4b11afd5561258da80958703c82b Mon Sep 17 00:00:00 2001 From: JunYoung Date: Wed, 10 Jun 2026 13:44:37 +0900 Subject: [PATCH 07/19] =?UTF-8?q?feat/#339:=20=ED=83=AD=EB=B0=94=20?= =?UTF-8?q?=EC=9E=90=EB=8F=99=20=EC=88=A8=EA=B9=80=20=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?(push=20=EC=8B=9C=20=EC=88=A8=EA=B9=80,=20root=20=EB=B3=B5?= =?UTF-8?q?=EA=B7=80=20=EC=8B=9C=20=ED=91=9C=EC=8B=9C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BottomTabBarController가 각 탭의 NavigationController delegate로 등록되어 스택 depth를 감지해 자동으로 탭바 표시/숨김 처리. --- .../Tabbar/BottomTabBarController.swift | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tabbar/BottomTabBarController.swift b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tabbar/BottomTabBarController.swift index c2cea63a..5615df4d 100644 --- a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tabbar/BottomTabBarController.swift +++ b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tabbar/BottomTabBarController.swift @@ -60,11 +60,14 @@ private extension BottomTabBarController { 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 +80,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 } From 56748f4c4d3bfb59569dffa8f30e6a407edcba51 Mon Sep 17 00:00:00 2001 From: JunYoung Date: Wed, 10 Jun 2026 13:51:29 +0900 Subject: [PATCH 08/19] =?UTF-8?q?fix/#339:=20MLSDesignSystem=20SPM=20?= =?UTF-8?q?=EB=AA=A8=EB=93=88=20=EC=97=90=EC=85=8B=20=EB=B2=88=EB=93=A4=20?= =?UTF-8?q?=EC=B0=B8=EC=A1=B0=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EB=B0=8F=20=ED=83=AD=EB=B0=94=20=EB=B0=B0=EA=B2=BD=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UIImage(named:) -> DesignSystemAsset.image(named:)로 변경하여 SPM 모듈 번들에서 이미지를 올바르게 로드하도록 수정. 탭바 양옆 투명 영역에 배경 뷰 추가. --- .../Components/DictionaryDetailListView.swift | 2 +- .../MLSDesignSystem/Components/ErrorMessage.swift | 2 +- .../Components/FloatingActionButton.swift | 2 +- .../MLSDesignSystem/Components/GuideAlert.swift | 2 +- .../Tabbar/BottomTabBarController.swift | 15 +++++++++++++++ .../MLSDesignSystem/Components/TextButton.swift | 2 +- 6 files changed, 20 insertions(+), 5 deletions(-) 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/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..16b73a95 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 }() diff --git a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tabbar/BottomTabBarController.swift b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tabbar/BottomTabBarController.swift index 5615df4d..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,6 +62,11 @@ 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]) { @@ -99,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 }() From c88f23b897a90061be8e232ceaccbe237815bfe3 Mon Sep 17 00:00:00 2001 From: JunYoung Date: Wed, 10 Jun 2026 14:18:26 +0900 Subject: [PATCH 09/19] =?UTF-8?q?feat/#339:=20=EC=95=B1=20=EB=B2=84?= =?UTF-8?q?=EC=A0=84=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=EC=B2=B4?= =?UTF-8?q?=ED=81=AC=20=EB=B0=8F=20=EC=96=BC=EB=9F=BF=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 앱 시작 시 App Store 버전과 비교하여 강제/선택 업데이트 얼럿 표시. 강제 업데이트는 포그라운드 복귀 시 얼럿 재표시, URL 열기 실패 시에도 재표시. --- .../Sources/MLSAppFeature/AppInfo.swift | 5 ++ .../Dependency/RepositoryAssembly.swift | 7 ++ .../Dependency/UseCaseAssembly.swift | 8 +++ .../MLSAppFeature/Launcher/AppLauncher.swift | 69 ++++++++++++++++++- .../Components/GuideAlert.swift | 2 +- 5 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 MLS/MLSAppFeature/Sources/MLSAppFeature/AppInfo.swift 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/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..381213b9 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,63 @@ 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() + 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/MLSDesignSystem/Sources/MLSDesignSystem/Components/GuideAlert.swift b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/GuideAlert.swift index 16b73a95..11571cbb 100644 --- a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/GuideAlert.swift +++ b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/GuideAlert.swift @@ -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 From 27defeb659f40c9d4a2bb47d275e1c7f29ae2bf0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 10 Jun 2026 05:25:48 +0000 Subject: [PATCH 10/19] style/#339: Apply SwiftLint autocorrect --- MLS/MLSRecommendationFeatureExample/SceneDelegate.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MLS/MLSRecommendationFeatureExample/SceneDelegate.swift b/MLS/MLSRecommendationFeatureExample/SceneDelegate.swift index 7f9f6c2e..1fbe974e 100644 --- a/MLS/MLSRecommendationFeatureExample/SceneDelegate.swift +++ b/MLS/MLSRecommendationFeatureExample/SceneDelegate.swift @@ -53,7 +53,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { vc.view.backgroundColor = .white return vc }, - makeDetailVC: { mapId in + makeDetailVC: { _ in let vc = UIViewController() vc.view.backgroundColor = .white return vc From 93ef49831a7aa4575e6e99aa3f82a8c5b876ec3a Mon Sep 17 00:00:00 2001 From: JunYoung Date: Wed, 10 Jun 2026 22:49:37 +0900 Subject: [PATCH 11/19] =?UTF-8?q?fix/#339:=20=EB=8F=84=EA=B0=90=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=EB=8F=84=EA=B0=90=20=EB=B2=84=ED=8A=BC=20?= =?UTF-8?q?=ED=83=AD=20=EC=9D=B4=EB=8F=99=20=EC=9D=B8=EB=8D=B1=EC=8A=A4=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(=EC=B6=94=EC=B2=9C=E2=86=92=EB=8F=84?= =?UTF-8?q?=EA=B0=90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MLSAppFeature/Coordinator/AppCoordinator.swift | 5 +++-- .../Domain/Navigation/AppCoordinatorProtocol.swift | 8 +++++++- .../DictionaryDetailBaseViewController.swift | 2 +- .../Mock/MockAppCoordinator.swift | 2 +- 4 files changed, 12 insertions(+), 5 deletions(-) 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/MLSAppFeatureInterface/Domain/Navigation/AppCoordinatorProtocol.swift b/MLS/MLSAppFeature/Sources/MLSAppFeatureInterface/Domain/Navigation/AppCoordinatorProtocol.swift index f28f119d..6f04b060 100644 --- a/MLS/MLSAppFeature/Sources/MLSAppFeatureInterface/Domain/Navigation/AppCoordinatorProtocol.swift +++ b/MLS/MLSAppFeature/Sources/MLSAppFeatureInterface/Domain/Navigation/AppCoordinatorProtocol.swift @@ -4,6 +4,12 @@ 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/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 20ae084a..4e5605f4 100644 --- a/MLS/MLSDictionaryFeature/Sources/MLSDictionaryFeatureTesting/Mock/MockAppCoordinator.swift +++ b/MLS/MLSDictionaryFeature/Sources/MLSDictionaryFeatureTesting/Mock/MockAppCoordinator.swift @@ -9,7 +9,7 @@ public final class MockAppCoordinator: AppCoordinatorProtocol { public init() {} - public func showMainTab() { + public func showMainTab(selectedIndex: Int) { } public func showLogin(exitRoute: LoginExitRoute) { From 18c0c4d9b740b74e4fdd3976bfe0a8b59c201ea4 Mon Sep 17 00:00:00 2001 From: JunYoung Date: Wed, 10 Jun 2026 22:49:45 +0900 Subject: [PATCH 12/19] =?UTF-8?q?fix/#339:=20=EC=BA=90=EB=A6=AD=ED=84=B0?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=20=ED=99=94=EB=A9=B4=20=EA=B8=B0=EC=A1=B4?= =?UTF-8?q?=20=EB=A0=88=EB=B2=A8/=EC=A7=81=EC=97=85=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EB=AF=B8=EC=9E=85=EB=A0=A5=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Dependency/FactoryAssembly.swift | 4 ++- .../Components/DropDownBox/DropDownBox.swift | 11 +++++++ .../SetCharacterFactoryImpl.swift | 8 +++-- .../SetCharacter/SetCharacterReactor.swift | 30 +++++++++++++++---- .../SetCharacterViewController.swift | 22 ++++++++++++++ 5 files changed, 67 insertions(+), 8 deletions(-) diff --git a/MLS/MLSAppFeature/Sources/MLSAppFeature/Dependency/FactoryAssembly.swift b/MLS/MLSAppFeature/Sources/MLSAppFeature/Dependency/FactoryAssembly.swift index 29625e9e..11ad3b3b 100644 --- a/MLS/MLSAppFeature/Sources/MLSAppFeature/Dependency/FactoryAssembly.swift +++ b/MLS/MLSAppFeature/Sources/MLSAppFeature/Dependency/FactoryAssembly.swift @@ -415,7 +415,9 @@ public enum FactoryAssembly { DIContainer .resolve(type: CheckValidLevelUseCase.self), authRepository: DIContainer - .resolve(type: AuthAPIRepository.self) + .resolve(type: AuthAPIRepository.self), + myPageRepository: DIContainer + .resolve(type: MyPageRepository.self) ) } DIContainer.register(type: SelectImageFactory.self) { 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/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) } From e8e7e1f518338bfbaa6a48ec502223c5ba5c99fe Mon Sep 17 00:00:00 2001 From: JunYoung Date: Wed, 10 Jun 2026 22:49:49 +0900 Subject: [PATCH 13/19] =?UTF-8?q?fix/#339:=20=ED=94=84=EB=A1=9C=ED=95=84?= =?UTF-8?q?=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=84=A0=ED=83=9D=20=EB=AA=A8?= =?UTF-8?q?=EB=8B=AC=20=EC=97=B4=EB=A6=B4=20=EB=95=8C=20=ED=83=AD=EB=B0=94?= =?UTF-8?q?=20=EC=9E=AC=EB=85=B8=EC=B6=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SetProfile/SetProfileViewController.swift | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/MLS/MLSMyPageFeature/Sources/MLSMyPageFeature/Presentation/SetProfile/SetProfileViewController.swift b/MLS/MLSMyPageFeature/Sources/MLSMyPageFeature/Presentation/SetProfile/SetProfileViewController.swift index 4a59646b..99d0b157 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 @@ -140,6 +129,9 @@ extension SetProfileViewController { owner.view.backgroundColor = state == .edit ? .whiteMLS : .neutral100 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) From ea8aeb90296014d9dd395ea1314bb62886313efb Mon Sep 17 00:00:00 2001 From: JunYoung Date: Wed, 10 Jun 2026 22:50:02 +0900 Subject: [PATCH 14/19] =?UTF-8?q?fix/#339:=20=ED=94=84=EB=A1=9C=ED=95=84?= =?UTF-8?q?=20=EB=8B=89=EB=84=A4=EC=9E=84=20=EC=88=98=EC=A0=95=20=EC=B7=A8?= =?UTF-8?q?=EC=86=8C=20=EC=8B=9C=20=EB=AF=B8=EC=A0=80=EC=9E=A5=20=EA=B0=92?= =?UTF-8?q?=EC=9D=B4=20=ED=91=9C=EC=8B=9C=EB=90=98=EB=8A=94=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Presentation/SetProfile/SetProfileReactor.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/MLS/MLSMyPageFeature/Sources/MLSMyPageFeature/Presentation/SetProfile/SetProfileReactor.swift b/MLS/MLSMyPageFeature/Sources/MLSMyPageFeature/Presentation/SetProfile/SetProfileReactor.swift index d9d6058f..3ec96ac1 100644 --- a/MLS/MLSMyPageFeature/Sources/MLSMyPageFeature/Presentation/SetProfile/SetProfileReactor.swift +++ b/MLS/MLSMyPageFeature/Sources/MLSMyPageFeature/Presentation/SetProfile/SetProfileReactor.swift @@ -133,6 +133,7 @@ public final class SetProfileReactor: Reactor { newState.isEditingNickName = isEditing case .cancelEditting: newState.setProfileState = .normal + newState.nickName = state.profile?.nickname ?? "" case .beginEditting: newState.setProfileState = .edit case .completeEditting: From f03ca2f79120f7a04c622e87cc3e360befb65989 Mon Sep 17 00:00:00 2001 From: JunYoung Date: Wed, 10 Jun 2026 22:50:06 +0900 Subject: [PATCH 15/19] =?UTF-8?q?fix/#339:=20=EA=B0=95=EC=A0=9C=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=EC=98=B5=EC=A0=80?= =?UTF-8?q?=EB=B2=84=20=EC=A4=91=EB=B3=B5=20=EB=93=B1=EB=A1=9D=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20=EB=A9=94=EB=AA=A8=EB=A6=AC=20?= =?UTF-8?q?=EB=88=84=EC=88=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/MLSAppFeature/Launcher/AppLauncher.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/MLS/MLSAppFeature/Sources/MLSAppFeature/Launcher/AppLauncher.swift b/MLS/MLSAppFeature/Sources/MLSAppFeature/Launcher/AppLauncher.swift index 381213b9..61f46513 100644 --- a/MLS/MLSAppFeature/Sources/MLSAppFeature/Launcher/AppLauncher.swift +++ b/MLS/MLSAppFeature/Sources/MLSAppFeature/Launcher/AppLauncher.swift @@ -72,6 +72,9 @@ private extension AppLauncher { switch status { case .force: showForceUpdateAlert() + if let existingObserver = forceUpdateObserver { + NotificationCenter.default.removeObserver(existingObserver) + } forceUpdateObserver = NotificationCenter.default.addObserver( forName: UIApplication.didBecomeActiveNotification, object: nil, From 2971e3327ad85fde153fc308124accc85f815b42 Mon Sep 17 00:00:00 2001 From: JunYoung Date: Wed, 10 Jun 2026 22:50:16 +0900 Subject: [PATCH 16/19] =?UTF-8?q?fix/#339:=20=EB=B6=81=EB=A7=88=ED=81=AC?= =?UTF-8?q?=20=EB=B2=84=ED=8A=BC=20=EC=97=B0=EC=86=8D=20=ED=83=AD=20?= =?UTF-8?q?=EC=8B=9C=20=EC=A4=91=EB=B3=B5=20API=20=EC=9A=94=EC=B2=AD=20?= =?UTF-8?q?=EB=B0=A9=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RecommendationMainReactor.swift | 58 +++++++++++-------- 1 file changed, 35 insertions(+), 23 deletions(-) diff --git a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainReactor.swift b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainReactor.swift index d69d5a90..b89d2847 100644 --- a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainReactor.swift +++ b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainReactor.swift @@ -32,6 +32,7 @@ final class RecommendationMainReactor: Reactor { case updateBookmarkId(mapId: Int, bookmarkId: Int?) case setLastDeleted(RecommendationMap?) case setUIEvent(UIEvent) + case setTogglingBookmark(mapId: Int) } struct State { @@ -42,6 +43,7 @@ final class RecommendationMainReactor: Reactor { var informationButtonIsOn: Bool = false var isLogin: Bool = false var lastDeleted: RecommendationMap? + var togglingBookmarkIds: Set = [] @Pulse var uiEvent: UIEvent = .none } @@ -132,6 +134,7 @@ final class RecommendationMainReactor: Reactor { 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( @@ -146,6 +149,8 @@ final class RecommendationMainReactor: Reactor { newState.lastDeleted = map case let .setUIEvent(event): newState.uiEvent = event + case let .setTogglingBookmark(mapId): + newState.togglingBookmarkIds.insert(mapId) } return newState } @@ -154,34 +159,41 @@ final class RecommendationMainReactor: Reactor { // MARK: - Methods private extension RecommendationMainReactor { func handleToggleBookmark(mapId: Int) -> Observable { - guard let map = currentState.recommendations.first(where: { $0.mapId == mapId }) else { + guard !currentState.togglingBookmarkIds.contains(mapId), + let map = currentState.recommendations.first(where: { $0.mapId == mapId }) else { return .empty() } if let bookmarkId = map.bookmarkId { - return bookmarkRepository.deleteBookmark(bookmarkId: bookmarkId) - .flatMap { _ -> Observable in - .from([ - .setLastDeleted(map), - .updateBookmarkId(mapId: mapId, bookmarkId: nil), - .setUIEvent(.deleted(map)) - ]) - } - .catch { _ in .empty() } + 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 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 .empty() } + 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)) } + ]) } } From 0ab14856beb53c90e5eab137a3333ca03aad3634 Mon Sep 17 00:00:00 2001 From: JunYoung Date: Wed, 10 Jun 2026 22:59:09 +0900 Subject: [PATCH 17/19] =?UTF-8?q?fix/#339:=20=ED=94=84=EB=A1=9C=ED=95=84?= =?UTF-8?q?=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=84=A0=ED=83=9D=20=EB=AA=A8?= =?UTF-8?q?=EB=8B=AC=20=EB=8B=AB=ED=9E=90=20=EB=95=8C=20=ED=83=AD=EB=B0=94?= =?UTF-8?q?=20=EC=9E=AC=EB=85=B8=EC=B6=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SetProfile/SetProfileViewController.swift | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/MLS/MLSMyPageFeature/Sources/MLSMyPageFeature/Presentation/SetProfile/SetProfileViewController.swift b/MLS/MLSMyPageFeature/Sources/MLSMyPageFeature/Presentation/SetProfile/SetProfileViewController.swift index 99d0b157..505577a5 100644 --- a/MLS/MLSMyPageFeature/Sources/MLSMyPageFeature/Presentation/SetProfile/SetProfileViewController.swift +++ b/MLS/MLSMyPageFeature/Sources/MLSMyPageFeature/Presentation/SetProfile/SetProfileViewController.swift @@ -147,6 +147,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() @@ -188,16 +199,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) From 2dcb87b240220d735707c5aab1f2249d48452d0b Mon Sep 17 00:00:00 2001 From: JunYoung Date: Wed, 10 Jun 2026 22:59:15 +0900 Subject: [PATCH 18/19] =?UTF-8?q?fix/#339:=20=ED=94=84=EB=A1=9C=ED=95=84?= =?UTF-8?q?=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=A6=89=EC=8B=9C=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EC=A0=9C=EA=B1=B0,=20=EC=99=84=EB=A3=8C=20?= =?UTF-8?q?=EB=B2=84=ED=8A=BC=EC=97=90=EC=84=9C=20=EB=8B=89=EB=84=A4?= =?UTF-8?q?=EC=9E=84+=EC=9D=B4=EB=AF=B8=EC=A7=80=20=ED=95=A8=EA=BB=98=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20=EB=B0=8F=20=ED=8E=B8=EC=A7=91=20=EB=AA=A8?= =?UTF-8?q?=EB=93=9C=20=EC=A2=85=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Dependency/FactoryAssembly.swift | 5 +--- .../SelectImage/SelectImageFactoryImpl.swift | 8 ++---- .../SelectImage/SelectImageReactor.swift | 11 +++----- .../SelectImageViewContoller.swift | 4 +++ .../SetProfile/SetProfileReactor.swift | 27 ++++++++++++++++--- 5 files changed, 33 insertions(+), 22 deletions(-) diff --git a/MLS/MLSAppFeature/Sources/MLSAppFeature/Dependency/FactoryAssembly.swift b/MLS/MLSAppFeature/Sources/MLSAppFeature/Dependency/FactoryAssembly.swift index 11ad3b3b..6eb3b6d3 100644 --- a/MLS/MLSAppFeature/Sources/MLSAppFeature/Dependency/FactoryAssembly.swift +++ b/MLS/MLSAppFeature/Sources/MLSAppFeature/Dependency/FactoryAssembly.swift @@ -421,10 +421,7 @@ public enum FactoryAssembly { ) } DIContainer.register(type: SelectImageFactory.self) { - SelectImageFactoryImpl( - myPageRepository: DIContainer - .resolve(type: MyPageRepository.self) - ) + SelectImageFactoryImpl() } DIContainer.register(type: DetailOnBoardingFactory.self) { DetailOnBoardingFactoryImpl() 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/SetProfile/SetProfileReactor.swift b/MLS/MLSMyPageFeature/Sources/MLSMyPageFeature/Presentation/SetProfile/SetProfileReactor.swift index 3ec96ac1..c9d02a79 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) @@ -134,15 +148,20 @@ public final class SetProfileReactor: Reactor { case .cancelEditting: newState.setProfileState = .normal newState.nickName = state.profile?.nickname ?? "" + newState.pendingImageUrl = nil case .beginEditting: newState.setProfileState = .edit 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 From 32b3b9314b407bcb728f601c4c4ad9979a334656 Mon Sep 17 00:00:00 2001 From: JunYoung Date: Wed, 10 Jun 2026 23:06:44 +0900 Subject: [PATCH 19/19] =?UTF-8?q?fix/#339:=20=ED=94=84=EB=A1=9C=ED=95=84?= =?UTF-8?q?=20=ED=8E=B8=EC=A7=91=20=EB=AA=A8=EB=93=9C=20=EC=A7=84=EC=9E=85?= =?UTF-8?q?=20=EC=8B=9C=20=EC=99=84=EB=A3=8C=20=EB=B2=84=ED=8A=BC=20?= =?UTF-8?q?=EB=8F=99=EC=9E=91=20=EB=B6=88=EA=B0=80=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EB=B0=8F=20=EB=92=A4=EB=A1=9C=EA=B0=80=EA=B8=B0=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20=EC=88=A8=EA=B9=80=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Presentation/SetProfile/SetProfileReactor.swift | 2 ++ .../Presentation/SetProfile/SetProfileViewController.swift | 1 + 2 files changed, 3 insertions(+) diff --git a/MLS/MLSMyPageFeature/Sources/MLSMyPageFeature/Presentation/SetProfile/SetProfileReactor.swift b/MLS/MLSMyPageFeature/Sources/MLSMyPageFeature/Presentation/SetProfile/SetProfileReactor.swift index c9d02a79..0c12ba17 100644 --- a/MLS/MLSMyPageFeature/Sources/MLSMyPageFeature/Presentation/SetProfile/SetProfileReactor.swift +++ b/MLS/MLSMyPageFeature/Sources/MLSMyPageFeature/Presentation/SetProfile/SetProfileReactor.swift @@ -151,6 +151,8 @@ public final class SetProfileReactor: Reactor { newState.pendingImageUrl = nil case .beginEditting: newState.setProfileState = .edit + newState.isShowError = false + newState.isEditingNickName = false case .completeEditting: newState.setProfileState = .normal newState.pendingImageUrl = nil diff --git a/MLS/MLSMyPageFeature/Sources/MLSMyPageFeature/Presentation/SetProfile/SetProfileViewController.swift b/MLS/MLSMyPageFeature/Sources/MLSMyPageFeature/Presentation/SetProfile/SetProfileViewController.swift index 505577a5..e0d85b13 100644 --- a/MLS/MLSMyPageFeature/Sources/MLSMyPageFeature/Presentation/SetProfile/SetProfileViewController.swift +++ b/MLS/MLSMyPageFeature/Sources/MLSMyPageFeature/Presentation/SetProfile/SetProfileViewController.swift @@ -127,6 +127,7 @@ 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 {